关系型数据库MySQL(四):读写分离

MySQL 读写分离:理论基础

1. 什么是读写分离? 在数据库访问压力较大的应用中,读写操作的比例往往是不均衡的。通常,读操作(SELECT)的数量远多于写操作(INSERT, UPDATE, DELETE)。读写分离的核心思想就是将读操作和写操作分发到不同的数据库服务器(或集群)上去执行。

  • 写操作: 由一个专门的服务器(通常称为主节点主库)处理。所有数据的修改(增、删、改)都只发生在主库上。
  • 读操作: 由一个或多个服务器(称为从节点从库)处理。这些从库的数据是通过某种机制(通常是主从复制)从主库同步过来的。应用系统可以将查询请求分发到这些从库上执行。

2. 为什么需要读写分离?

  • 分担负载: 将读压力分散到多个从库,减轻主库的负载,提高整体系统的并发处理能力和响应速度。
  • 提高可用性: 当主库出现故障时,可以快速将一个从库提升为新的主库(需要配合高可用方案)。读操作在从库故障时,也可以转移到其他可用从库或主库(需权衡一致性)。
  • 提升性能: 主库可以更专注于处理写操作,避免因大量读操作导致的锁竞争或 I/O 瓶颈。从库可以配置不同的硬件或优化策略(如只读模式、不同索引)来加速读查询。
  • 数据备份与恢复: 从库可以作为主库数据的实时备份,用于数据恢复或离线分析。

3. 如何实现? 实现读写分离的关键在于两点:

  • 数据同步: 确保从库的数据与主库尽可能保持一致(存在延迟)。MySQL 自身提供了强大的主从复制功能来实现这一点。
  • 请求路由: 应用程序在发出 SQL 请求时,需要能够判断该请求是读操作还是写操作,并将请求发送到正确的服务器(主库或某个从库)。这通常需要借助数据库中间件来实现,或者由应用程序框架(如 ShardingSphere-JDBC)在代码层面处理。

4. MySQL 主从复制原理简述 MySQL 主从复制是读写分离的基础。其基本原理如下:

  • 主库:
    1. 当主库上有数据修改操作(写操作)时,这些操作会被记录到二进制日志中。
    2. 主库上有一个线程(Binlog Dump Thread)负责读取二进制日志事件并发送给连接的从库。
  • 从库:
    1. 从库启动一个 I/O 线程(I/O Thread),连接到主库,请求获取主库的二进制日志事件。
    2. I/O 线程将接收到的日志事件写入从库本地的中继日志
    3. 从库启动一个 SQL 线程(SQL Thread),读取中继日志中的事件,并在从库上重放(执行)这些事件,从而使得从库的数据与主库同步。

复制格式: 主要有基于语句的复制和基于行的复制。现代 MySQL 版本通常使用混合模式或基于行的复制来保证更好的数据一致性。

复制延迟: 由于网络传输、从库重放速度等原因,从库的数据可能会稍微落后于主库,即存在复制延迟。这是读写分离架构中需要考虑的一个重要因素。


MySQL 读写分离:OpenEuler 系统运维实例

场景描述: 假设我们有一个运行在 OpenEuler 22.03 LTS 上的 Web 应用,数据库使用 MySQL 8.0。随着用户量增长,数据库读压力显著增加,主库经常出现高负载。我们决定实施读写分离方案来缓解压力。

目标架构: 采用一主(master)两从(slave1, slave2)的拓扑结构。使用数据库中间件 Mycat 来实现 SQL 请求的路由。

复制代码
+----------------+       +----------------+       +----------------+
|   Application  | ----> |     Mycat      | ----> | MySQL Master   |
|    (Web App)   |       | (Proxy/Middleware)|     | (Write)        |
+----------------+       +----------------+       +----------------+
                              |       |
                              |       +------> | MySQL Slave1  |
                              |               | (Read)        |
                              +------> | MySQL Slave2  |
                                       | (Read)        |
                                       +----------------+

实施步骤:

第一步:环境准备(所有节点均使用 OpenEuler 22.03 LTS)

  1. 安装 MySQL 8.0 (主、从1、从2): 使用 OpenEuler 的包管理器 dnf 安装 MySQL。

    bash 复制代码
    sudo dnf install mysql-server
    sudo systemctl start mysqld
    sudo systemctl enable mysqld
  2. 初始安全设置 (主、从1、从2): 运行 mysql_secure_installation 脚本设置 root 密码、移除测试数据库和匿名用户等。

第二步:配置主库 (master)

  1. 编辑 MySQL 配置文件 /etc/my.cnf 添加或修改以下配置项,开启二进制日志并设置唯一的服务器 ID。

    ini 复制代码
    [mysqld]
    server-id = 1          # 主库唯一ID,必须大于0且与其他节点不同
    log-bin = mysql-bin    # 启用二进制日志,指定日志文件前缀
    binlog_format = ROW    # 推荐使用基于行的复制格式
    expire_logs_days = 7   # 自动清理7天前的binlog
    max_binlog_size = 100M # 单个binlog文件最大100MB
    # 可选:确保复制的事务是安全的
    gtid_mode = ON
    enforce_gtid_consistency = ON
  2. 重启 MySQL 服务使配置生效:

    bash 复制代码
    sudo systemctl restart mysqld
  3. 创建用于复制的专用用户: 登录 MySQL (mysql -u root -p),执行:

    sql 复制代码
    CREATE USER 'repl'@'%' IDENTIFIED BY 'StrongReplPassword!';
    GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
    FLUSH PRIVILEGES;

    记录下主库当前的二进制日志文件名和位置(或 GTID 位置):

    sql 复制代码
    SHOW MASTER STATUS;

    输出类似:

    复制代码
    +------------------+----------+--------------+------------------+-------------------+
    | File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
    +------------------+----------+--------------+------------------+-------------------+
    | mysql-bin.000001 |      785 |              |                  |                   |
    +------------------+----------+--------------+------------------+-------------------+

    记下 FilePosition 的值(或 Executed_Gtid_Set)。

第三步:配置从库 (slave1, slave2)

  1. 编辑 MySQL 配置文件 /etc/my.cnf 为每个从库设置唯一的 server-id,并可选开启中继日志和 GTID。

    ini 复制代码
    [mysqld]
    server-id = 2          # slave1 用 2,slave2 用 3,必须唯一且不同于主库
    relay-log = relay-log  # 启用中继日志
    read_only = ON         # 建议设置为只读模式,防止误写
    # 如果主库启用了GTID,从库也需要配置
    gtid_mode = ON
    enforce_gtid_consistency = ON
  2. 重启 MySQL 服务:

    bash 复制代码
    sudo systemctl restart mysqld
  3. 配置复制源: 登录从库 MySQL (mysql -u root -p),执行:

    sql 复制代码
    -- 如果使用 GTID (推荐)
    CHANGE MASTER TO
    MASTER_HOST = 'master_ip_address', -- 主库IP
    MASTER_USER = 'repl',
    MASTER_PASSWORD = 'StrongReplPassword!',
    MASTER_AUTO_POSITION = 1; -- 启用基于GTID的自动定位
    
    -- 或者,如果不使用GTID (使用 File 和 Position)
    CHANGE MASTER TO
    MASTER_HOST = 'master_ip_address',
    MASTER_USER = 'repl',
    MASTER_PASSWORD = 'StrongReplPassword!',
    MASTER_LOG_FILE = 'mysql-bin.000001', -- 之前SHOW MASTER STATUS记录的File
    MASTER_LOG_POS = 785;                 -- 之前记录的Position
  4. 启动复制线程:

    sql 复制代码
    START SLAVE;
  5. 检查复制状态:

    sql 复制代码
    SHOW SLAVE STATUS\G

    关键观察点:

    • Slave_IO_Running: Yes (I/O 线程运行正常)
    • Slave_SQL_Running: Yes (SQL 线程运行正常)
    • Seconds_Behind_Master: 0 (或很小的值,表示延迟低)
    • 如果使用 GTID,检查 Retrieved_Gtid_SetExecuted_Gtid_Set 是否在增长并与主库接近。
    • 检查 Last_IO_ErrorLast_SQL_Error 是否为空。

    重复以上步骤配置 slave2

第四步:安装与配置 Mycat (读写分离中间件)

Mycat 是一个流行的开源数据库中间件,支持读写分离、分库分表等。这里我们用它来做 SQL 路由。

  1. 安装 Java 环境 (Mycat 依赖): Mycat 需要 Java 运行环境。

    bash 复制代码
    sudo dnf install java-11-openjdk-devel
  2. 下载并安装 Mycat: 从 Mycat 官网下载稳定版本 (例如 Mycat-server-1.6.7.6-release-20220524173810-linux.tar.gz)。解压到合适目录,如 /opt/mycat

    bash 复制代码
    sudo wget https://github.com/MyCATApache/Mycat-Server/releases/download/xxxx/mycat-server-xxxx.tar.gz # 替换为实际下载链接
    sudo tar -zxvf mycat-server-xxxx.tar.gz -C /opt
    sudo mv /opt/mycat-server-xxxx /opt/mycat
  3. 配置 Mycat: Mycat 的核心配置文件在 /opt/mycat/conf 目录下。

    • server.xml 定义 Mycat 服务本身参数、系统用户和权限、连接属性。

      XML 复制代码
      <user name="mycat_user"> <!-- Mycat 连接用户名 -->
          <property name="password">MycatPass123</property>
          <property name="schemas">TESTDB</property> <!-- Mycat 逻辑数据库名 -->
      </user>
      <user name="root"> <!-- 通常保留一个管理员用户 -->
          <property name="password">123456</property>
          <property name="schemas">TESTDB</property>
          <property name="readOnly">false</property>
      </user>
    • schema.xml 定义逻辑库、逻辑表、数据节点、数据源(连接物理数据库)。

      XML 复制代码
      <?xml version="1.0"?>
      <!DOCTYPE mycat:schema SYSTEM "schema.dtd">
      <mycat:schema xmlns:mycat="http://io.mycat/">
          <schema name="TESTDB" checkSQLschema="true" sqlMaxLimit="100"> <!-- 逻辑库名 -->
              <!-- 可以在此定义逻辑表及其分片/路由规则,本例简单读写分离不定义表 -->
          </schema>
          <!-- 定义数据节点 (DataNode),一个节点对应一个物理数据库分片 -->
          <dataNode name="dn_master" dataHost="dh_master" database="your_real_db" /> <!-- 主库节点 -->
          <dataNode name="dn_slave1" dataHost="dh_slave1" database="your_real_db" /> <!-- 从库1节点 -->
          <dataNode name="dn_slave2" dataHost="dh_slave2" database="your_real_db" /> <!-- 从库2节点 -->
          <!-- 定义数据主机 (DataHost),包含具体的物理数据库连接信息 -->
          <dataHost name="dh_master" maxCon="1000" minCon="10" balance="0"
                    writeType="0" dbType="mysql" dbDriver="jdbc" switchType="-1" slaveThreshold="100">
              <heartbeat>select user()</heartbeat> <!-- 心跳检测SQL -->
              <writeHost host="masterHost" url="jdbc:mysql://master_ip:3306" user="your_db_user" password="YourDBPass123"> <!-- 主库连接 -->
              </writeHost>
          </dataHost>
          <dataHost name="dh_slave1" maxCon="1000" minCon="10" balance="0"
                    writeType="0" dbType="mysql" dbDriver="jdbc" switchType="-1" slaveThreshold="100">
              <heartbeat>select user()</heartbeat>
              <writeHost host="slave1Host" url="jdbc:mysql://slave1_ip:3306" user="your_db_user" password="YourDBPass123"> <!-- 从库1连接 -->
              </writeHost>
          </dataHost>
          <dataHost name="dh_slave2" maxCon="1000" minCon="10" balance="0"
                    writeType="0" dbType="mysql" dbDriver="jdbc" switchType="-1" slaveThreshold="100">
              <heartbeat>select user()</heartbeat>
              <writeHost host="slave2Host" url="jdbc:mysql://slave2_ip:3306" user="your_db_user" password="YourDBPass123"> <!-- 从库2连接 -->
              </writeHost>
          </dataHost>
      </mycat:schema>
      • 注意:your_real_db 需要替换为实际的物理数据库名。
      • your_db_userYourDBPass123 替换为应用程序连接物理数据库的账号密码(需具有所需权限)。
    • rule.xml (可选): 定义分片规则。对于纯读写分离且不涉及分库分表的情况,可能不需要修改此文件。Mycat 内置了读写分离的规则(如 readindex)。

    • 配置读写分离规则: Mycat 通过 schema.xml 中的 balance 属性和 dataHost 下的 <readHost> (本例未使用,因为我们为每个从库定义了单独的 dataHost) 来实现读写分离。在本例架构中,我们定义了三个独立的 dataHost (dh_master, dh_slave1, dh_slave2)。要让 Mycat 知道如何路由,我们需要配置路由规则。这可以通过在 schema.xml<schema> 下不定义表(所有表默认路由),或者使用 Mycat 的注解(Hint)功能,但更常见的做法是配置 Mycat 的默认读写分离策略。 一个更贴近实际读写分离的 schema.xml 配置可能是这样的(使用一个 dataHost 包含主库和一个或多个从库):

      XML 复制代码
      <dataHost name="dh_all" maxCon="1000" minCon="10" balance="1" <!-- balance="1" 表示读操作在所有 readHost + writeHost 中随机负载均衡 -->
                writeType="0" dbType="mysql" dbDriver="jdbc" switchType="1" slaveThreshold="100">
          <heartbeat>select user()</heartbeat>
          <writeHost host="masterHost" url="jdbc:mysql://master_ip:3306" user="dbuser" password="dbpass">
              <!-- 定义读库 -->
              <readHost host="slave1Host" url="jdbc:mysql://slave1_ip:3306" user="dbuser" password="dbpass"/>
              <readHost host="slave2Host" url="jdbc:mysql://slave2_ip:3306" user="dbuser" password="dbpass"/>
          </writeHost>
      </dataHost>
      <dataNode name="dn_all" dataHost="dh_all" database="your_real_db" />
      <schema name="TESTDB" checkSQLschema="true" dataNode="dn_all"> <!-- 整个逻辑库指向这个包含主从的节点 -->
      </schema>

      这种配置下:

      • 所有写操作 (INSERT, UPDATE, DELETE, ALTER TABLE 等) 和涉及事务的读操作默认路由到 writeHost (主库)。
      • 非事务性的 SELECT 语句会根据 balance 的设置(这里是 1,随机负载均衡)路由到 writeHost 或任意一个 readHost (从库)。
      • switchType="1" 通常表示在主库故障时,从库可以接管写操作(需要配合其他高可用机制)。 选择哪种配置方式取决于管理偏好和具体的业务需求。本实例为了清晰展示主从角色,采用了第一种独立 dataHost 的方式,但需要额外的路由规则配置(如使用注解或在 Mycat 中定义规则)。对于新手,建议先采用第二种(一个 dataHost 包含主从)的方式配置 schema.xml 来实现读写分离,这样更简单直观。
  4. 启动 Mycat:

    bash 复制代码
    cd /opt/mycat/bin
    ./mycat start  # 启动
    ./mycat status # 查看状态
    ./mycat stop   # 停止

    Mycat 默认管理端口是 9066,服务端口是 8066

  5. 配置防火墙 (如果需要): 开放 Mycat 的服务端口(默认 8066)和应用服务器访问 Mycat 的端口。

    bash 复制代码
    sudo firewall-cmd --permanent --add-port=8066/tcp
    sudo firewall-cmd --reload

第五步:修改应用程序连接配置

不再直接连接 MySQL 主库或从库,而是连接 Mycat 服务。

  • 将应用程序中的数据库连接字符串(JDBC URL, ORM 配置等)修改为指向 Mycat 服务器和端口(默认 8066),并使用在 server.xml 中配置的 Mycat 用户名和密码(如 mycat_user/MycatPass123),数据库名使用 TESTDB (Mycat 逻辑库名)。

示例 (Java JDBC):

java 复制代码
String url = "jdbc:mysql://mycat_server_ip:8066/TESTDB?useSSL=false&characterEncoding=utf8";
String user = "mycat_user";
String password = "MycatPass123";
Connection conn = DriverManager.getConnection(url, user, password);

第六步:测试与验证

  1. 连接测试: 使用 MySQL 客户端或应用程序尝试连接 Mycat (mycat_server_ip:8066)。

  2. 写操作测试: 通过 Mycat 执行 INSERT 语句,检查数据是否成功写入主库,并观察是否同步到了两个从库。

  3. 读操作测试: 通过 Mycat 执行多个 SELECT 查询。观察 Mycat 的日志 (/opt/mycat/logs/mycat.log) 或使用 SHOW PROCESSLISTmaster, slave1, slave2 上查看,确认读请求被分发到了不同的从库(或在负载均衡策略下随机分发)。

  4. 延迟测试: 在主库写入后立即通过 Mycat 读取,观察是否能读到新数据(取决于复制延迟)。对于强一致性要求的读操作,可能需要使用 Mycat 的注解强制路由到主库,例如:

    sql 复制代码
    /*#mycat:sql=select * from table where id=1*/ SELECT * FROM table WHERE id = 1;

    (具体注解语法需参考 Mycat 文档)

  5. 压力测试: 使用工具(如 sysbench)模拟大量读请求,观察 Mycat 和各个从库的负载情况,验证读写分离是否生效。

运维注意事项:

  • 监控: 必须监控主库、从库、Mycat 的 CPU、内存、磁盘 I/O、网络流量、连接数、复制延迟 (Seconds_Behind_Master) 等关键指标。
  • 备份: 定期备份数据库,包括 Mycat 的配置文件。
  • 高可用: 读写分离本身不解决主库单点故障。需要结合主库高可用方案(如 MHA, Raft-based solutions, InnoDB Cluster)和 Mycat 自身的高可用(如 Keepalived + VIP)。
  • 复制延迟: 理解业务对数据一致性的容忍度。对于无法容忍延迟的读操作(如刚下订单后的查询),需要特殊处理(如路由回主库)。
  • 中间件维护: Mycat 需要维护和升级。关注其社区动态和版本更新。
  • 安全: 确保复制用户、应用连接用户、Mycat 管理用户的密码强度。限制数据库端口的访问来源。

总结

通过 MySQL 主从复制和 Mycat 中间件的配合,我们在 OpenEuler 系统上成功部署了一个读写分离架构。这个架构有效分散了数据库的读负载,提升了系统的整体性能和扩展性。实施过程中,需要仔细配置主从复制关系、Mycat 的数据源和路由规则,并在应用程序端调整数据库连接指向 Mycat。后续运维的重点在于监控、备份、处理复制延迟以及规划高可用方案。

请记住,读写分离是解决特定性能瓶颈(读多写少)的一种有效手段,但它也引入了额外的复杂性(复制延迟、中间件管理)。在实施前,务必评估业务需求和可能带来的影响。

相关推荐
Wyz201210242 小时前
SQL中如何处理GROUP BY的不可排序问题_ORDERBY与聚合
jvm·数据库·python
Polar__Star2 小时前
jsoup如何读取html
jvm·数据库·python
亚空间仓鼠2 小时前
关系型数据库MySQL(三):主从复制
数据库·mysql
a9511416422 小时前
怎么防范通过phpMyAdmin上传WebShell_禁止into outfile权限
jvm·数据库·python
InfinteJustice2 小时前
如何统计SQL分组汇总数据_详解GROUP BY与HAVING用法
jvm·数据库·python
zhangchaoxies2 小时前
如何使用 AWS Lambda 和 Python 获取 EMR 集群的标签列表
jvm·数据库·python
吕源林2 小时前
如何处理SQL插入后的数据一致性校验_使用Checksum比对
jvm·数据库·python
Austindatabases2 小时前
什么int类型里面能插入文字,还不能改字段类型--SQLite 五脏俱全系列 (2)
数据库·sqlite
2301_777599372 小时前
SQL如何实现动态分组统计_使用存储过程与动态SQL
jvm·数据库·python