【架构实战】MySQL主从复制与读写分离:数据库高可用架构

【架构实战】MySQL主从复制与读写分离:数据库高可用架构

作者:架构实战系列 | 日期:2026-05-19


一次"静默故障"引发的数据库架构革命

2019年夏天,笔者接手了一个日订单量突破500万的中型电商平台的数据库运维工作。上线初期,团队只用了一台16核64GB的高配MySQL服务器跑主库,跑了半年相安无事。

直到某天凌晨3点,这台服务器的SSD突然出现坏道,数据库进程崩溃。由于没有主从复制,所有数据只存在这一台机器上,而RAID重建需要4个小时。这4个小时里,平台完全无法下单,损失超过200万。

这次"静默故障"让我们痛下决心:必须上主从复制,必须做读写分离,必须做高可用切换。这次事故也催生了本文的诞生。接下来,我将结合实际踩坑经验,系统讲解MySQL主从复制原理、半同步复制配置、MHA高可用切换,以及ShardingSphere-Proxy在生产环境中的实战应用。


一、主从复制原理:从Binlog到数据同步

1.1 为什么需要主从复制?

主从复制的价值体现在三个维度:

维度 说明
数据安全 主库数据实时同步到从库,从库可作为数据备份
读写分离 写操作走主库,读操作走从库,分担单库压力
故障高可用 主库故障时,从库可晋升为主库,保证服务连续性
数据分析 从库可专门跑报表和数据分析,不影响主库业务

1.2 主从复制原理详解

MySQL主从复制基于Binlog(二进制日志)实现,采用异步复制模式。整个链路分为3个步骤:

复制代码
[主库]                [从库]
   |                    |
   |  1. 事务提交        |
   |  → 写入Binlog      |
   |                    |
   |  2. (异步) Binlog  |→ [IO Thread] 拉取Binlog
   |    Dump Thread     |   写入Relay Log
   |    推送/拉取       |
   |                    |
   |                    |  3. SQL Thread 重放Relay Log
   |                    |  → 执行SQL语句
   |                    |
   |                    |  4. 数据同步完成

关键参数解析:

  • Binlog:记录所有DDL和DML语句,有Statement、Row、Mixed三种格式
  • IO Thread:运行在从库,负责连接主库、拉取Binlog
  • SQL Thread:运行在从库,负责读取Relay Log并执行SQL
  • Relay Log:从库本地的中转日志,存储从主库拉取的Binlog事件

1.3 三种Binlog格式对比

格式 原理 优点 缺点
Statement 记录SQL语句 Binlog小,支持函数执行 依赖确定性的SQL,可能出现主从不一致
Row 记录行级变更 精确复制,无不一致问题 Binlog体积大,高并发下性能下降
Mixed 混合模式 智能选择上述两者 问题排查复杂

生产环境推荐:Row格式。 某次数据迁移中,我们使用Statement格式迁移数据,结果NOW()UUID()等非确定性函数在主从执行结果不一致,排查了整整2天才定位到原因。Row格式虽然Binlog大,但数据一致性是底线。


二、半同步复制:让"异步"变成"准同步"

2.1 为什么普通异步复制有风险?

普通异步复制中,主库提交事务后不等待从库确认就返回成功。这意味着:如果主库宕机且Binlog还没传到从库,这部分数据就会丢失------这就是著名的"数据丢失问题"。

2.2 半同步复制的原理

半同步复制(Semi-Sync Replication)在主库事务提交后,至少等待一个从库写入Relay Log并返回确认后,才给客户端提交成功响应

复制代码
主库事务提交流程:
  事务执行完成
      ↓
  写入Binlog
      ↓
  等待至少1个从库ACK确认(可配置超时时间)
      ↓
  返回客户端:"提交成功"

2.3 半同步复制配置实战

主库配置(my.cnf)
ini 复制代码
[mysqld]
# 基础配置
server-id = 1
port = 3306
basedir = /usr/local/mysql
datadir = /var/lib/mysql
socket = /tmp/mysql.sock
pid-file = /var/run/mysqld/mysqld.pid

# Binlog配置
log-bin = mysql-bin
binlog_format = ROW
binlog_row_image = FULL
max_binlog_size = 1G
binlog_expire_logs_seconds = 604800  # 保留7天
sync_binlog = 1  # 每N次事务强制刷盘,生产环境建议设为1

# GTID配置(全局事务ID,便于故障转移)
gtid_mode = ON
enforce_gtid_consistency = ON

# 半同步复制插件
plugin_load = 'rpl_semi_sync_master=semisync_master.so'
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_master_timeout = 10000  # 等待从库确认超时:10秒

# InnoDB优化
innodb_buffer_pool_size = 32G  # 建议为物理内存的70%
innodb_log_file_size = 2G
innodb_flush_log_at_trx_commit = 1  # 每次提交强制刷盘,最安全
innodb_flush_method = O_DIRECT
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000

# 连接数
max_connections = 5000
从库配置(my.cnf)
ini 复制代码
[mysqld]
server-id = 2  # 每个节点server-id必须唯一
port = 3306
basedir = /usr/local/mysql
datadir = /var/lib/mysql
socket = /tmp/mysql.sock

# Relay Log配置
relay_log = mysql-relay-bin
relay_log_purge = ON
log_slave_updates = ON  # 从库执行的写操作也记录到自己的Binlog(级联复制需要)

# Binlog配置(从库也建议开启Binlog,作为其他从库的主库)
log-bin = mysql-bin
binlog_format = ROW
binlog_row_image = FULL

# GTID配置
gtid_mode = ON
enforce_gtid_consistency = ON

# 只读配置(从库默认只读,防止业务误写入)
read_only = ON
super_read_only = ON  # 包含SUPER权限用户也只能读

# 半同步复制插件
plugin_load = 'rpl_semi_sync_slave=semisync_slave.so'
rpl_semi_sync_slave_enabled = 1

# 跳过复制错误(临时使用,排查问题时开)
# slave_skip_errors = ddl_exist_errors
启动半同步复制
sql 复制代码
-- 主库安装半同步插件
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SHOW PLUGINS;

-- 从库安装半同步插件
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
SHOW PLUGINS;

-- 查看半同步复制状态
SHOW STATUS LIKE 'Rpl_semi_sync%';

-- 配置参数(运行时生效)
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 10000;
SET GLOBAL rpl_semi_sync_slave_enabled = 1;

-- 重启从库IO Thread使配置生效
STOP SLAVE IO_THREAD;
START SLAVE IO_THREAD;

-- 确认半同步状态
SHOW STATUS LIKE 'Rpl_semi_sync%';
-- 期望看到:
-- Rpl_semi_sync_master_clients = 1(有1个从库连上)
-- Rpl_semi_sync_master_status = ON
-- Rpl_semi_sync_slave_status = ON

2.4 半同步复制的"退化"机制

半同步复制有个重要特性:当等待从库ACK超过rpl_semi_sync_master_timeout(默认10秒)时,自动退化为普通异步复制,等从库恢复后自动恢复为半同步。这是MySQL的保护机制,避免主库无限等待。

sql 复制代码
-- 查看退化次数(正常应为0,若有退化需要排查网络或从库性能)
SHOW STATUS LIKE 'Rpl_semi_sync_master_no_times%';

-- 监控半同步状态脚本(可接入Prometheus)
SELECT 
    VARIABLE_NAME,
    VARIABLE_VALUE
FROM performance_schema.global_status
WHERE VARIABLE_NAME LIKE 'rpl_semi%';

三、MHA高可用架构:故障自动切换

3.1 MHA是什么?

MHA(MySQL High Availability)是日本DeNA工程师开发的主从复制高可用工具,实现了:

  • 自动故障检测:每秒检测主库是否存活
  • 自动故障切换:主库宕机后30秒内自动选主并切换
  • Binlog补偿:即使从库没有完全同步,也能尽量恢复数据
  • 零宕机切换:通过VIP漂移实现业务无感知切换

3.2 MHA架构

复制代码
                    [MHA Manager]
                         |
              +----------+----------+
              |                      |
         [Node1:Master]    [Node2:Slave1]
              |                      |
         [Node3:Slave2]    [Node4:Slave3]

VIP: 192.168.1.100 (指向Master)

故障时VIP自动漂移到新的Master节点

3.3 MHA安装与配置

环境说明
主机 IP 角色
Node1 192.168.1.101 Master
Node2 192.168.1.102 Slave1
Node3 192.168.1.103 Slave2
Manager 192.168.1.104 MHA Manager
所有节点配置SSH免密
bash 复制代码
# 所有节点执行(Node1, Node2, Node3, Manager)
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.1.101
ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.1.102
ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.1.103
ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.1.104

# 验证免密登录
ssh root@192.168.1.101 "echo OK"
安装MHA Node(所有节点)
bash 复制代码
# CentOS 7
yum install -y perl-DBD-MySQL perl-Config-Tiny perl-Log-Dispatch perl-Parallel-ForkManager

# 下载编译mha4mysql-node
wget https://github.com/yoshinorim/mha4mysql-node/releases/download/v0.58/mha4mysql-node-0.58.tar.gz
tar -xzf mha4mysql-node-0.58.tar.gz
cd mha4mysql-node-0.58
perl Makefile.PL && make && make install
安装MHA Manager(Manager节点)
bash 复制代码
wget https://github.com/yoshinorim/mha4mysql-manager/releases/download/v0.58/mha4mysql-manager-0.58.tar.gz
tar -xzf mha4mysql-manager-0.58.tar.gz
cd mha4mysql-manager-0.58
perl Makefile.PL && make && make install
MHA配置文件(/etc/app.cnf)
ini 复制代码
[server default]
# SSH登录配置
ssh_user=root
ssh_port=22

# MySQL复制用户
repl_user=repl_user
repl_password=ReplPassword@2025

# MHA Manager监控MySQL的用户
manager_workdir=/var/log/mha/app/manager
manager_log=/var/log/mha/app/manager/manager.log
remote_workdir=/var/log/mha/app/remote

# 故障切换脚本
master_ip_failover_script=/usr/local/bin/master_ip_failover
master_ip_online_change_script=/usr/local/bin/master_ip_online_change

# 主库 ping 测试次数
ping_interval=3
ping_type=SELECT

# 失败后从库恢复后自动从manager中移除
# secondary_check_script=/usr/local/bin/masterha_secondary_check -s 192.168.1.102 -s 192.168.1.103

[server1]
hostname=192.168.1.101
port=3306
master_binlog_dir=/var/lib/mysql
candidate_master=1
check_repl_delay=0  # 即使从库有延迟,也允许选为Master

[server2]
hostname=192.168.1.102
port=3306
master_binlog_dir=/var/lib/mysql
candidate_master=1
check_repl_delay=0

[server3]
hostname=192.168.1.103
port=3306
master_binlog_dir=/var/lib/mysql
candidate_master=0  # 不优先选为Master(配置较弱)
VIP漂移脚本(master_ip_failover)
perl 复制代码
#!/usr/bin/env perl

use strict;
use warnings FATAL => 'all';
use Getopt::Long;

my (
    $command, $orig_master_host, $orig_master_ip,
    $orig_master_port, $new_master_host, $new_master_ip,
    $new_master_port, $orig_master_ssh_port, $new_master_ssh_port
);

GetOptions(
    'command=s'             => \$command,
    'orig_master_host=s'    => \$orig_master_host,
    'orig_master_ip=s'      => \$orig_master_ip,
    'orig_master_port=i'    => \$orig_master_port,
    'new_master_host=s'     => \$new_master_host,
    'new_master_ip=s'       => \$new_master_ip,
    'new_master_port=i'     => \$new_master_port,
    'orig_master_ssh_port=i'=> \$orig_master_ssh_port,
    'new_master_ssh_port=i'=> \$new_master_ssh_port,
);

my $vip = '192.168.1.100/24';
my $key = '1';
my $ssh_start_vip = "/sbin/ip addr add $vip dev eth0";
my $ssh_stop_vip = "/sbin/ip addr del $vip dev eth0";

if ( $command eq "stop" || $command eq "stopssh" ) {
    my $exit = 0;
    print "Disabling the VIP $vip on old master: $orig_master_host\n";
    my $ssh_output = `$ssh_stop_vip 2>/dev/null`; 
    print "Disabled successfully\n";
    exit $exit;
}

if ( $command eq "start" || $command eq "startssh" ) {
    my $exit = 0;
    print "Enabling the VIP $vip on new master: $new_master_host\n";
    my $ssh_output = `$ssh_start_vip 2>/dev/null`; 
    print "Enabled successfully\n";
    exit $exit;
}

if ( $command eq "status" ) {
    print "Checking the VIP $vip on all hosts...\n";
    my $output = `ssh $orig_master_host \"ip addr show | grep $vip\" 2>/dev/null`;
    if ($output =~ /$vip/) {
        print "VIP found on $orig_master_host\n";
    } else {
        print "VIP not found on $orig_master_host\n";
    }
    exit 0;
}

exit 1;

3.4 MHA切换验证

bash 复制代码
# 检查所有SSH连接
masterha_check_ssh --conf=/etc/app.cnf

# 检查复制链路
masterha_check_repl --conf=/etc/app.cnf

# 启动MHA Manager
nohup masterha_manager --conf=/etc/app.cnf --ignore_last_failover < /dev/null > /var/log/mha/app/manager.log 2>&1 &

# 手动模拟故障切换
# masterha_master_switch --conf=/etc/app.cnf --master_state=dead --new_master_host=192.168.1.102

# 查看MHA状态
masterha_check_status --conf=/etc/app.cnf

四、ShardingSphere-Proxy实战:读写分离与数据分片

4.1 为什么选择ShardingSphere-Proxy?

在Java应用层做读写分离需要修改代码,耦合度高。ShardingSphere-Proxy是MySQL协议的中间件,对应用透明,支持:

  • 读写分离(自动路由)
  • 水平分片(分库分表)
  • 分布式事务
  • SQL黑名单
  • 影子库压测

4.2 ShardingSphere-Proxy安装

bash 复制代码
# 下载最新版本(截至2025年)
wget https://archive.apache.org/dist/shardingsphere/5.4.0/apache-shardingsphere-5.4.0-shardingsphere-proxy-bin.tar.gz
tar -xzf apache-shardingsphere-5.4.0-shardingsphere-proxy-bin.tar.gz -C /opt/
cd /opt/apache-shardingsphere-5.4.0-shardingsphere-proxy-bin

# 下载MySQL JDBC驱动(必须)
cp /root/.m2/repository/mysql/mysql-connector-java/8.0.33/mysql-connector-java-8.0.33.jar server/lib/

# 修改端口配置(可选,默认3307)
# conf/server.yaml 中配置端口和认证

4.3 读写分离配置

yaml 复制代码
# conf/config-readwrite-splitting.yaml

schemaName: readwrite_splitting_db

dataSources:
  # 主库(写库)
  primary_ds:
    url: jdbc:mysql://192.168.1.101:3306/app_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: sharding_proxy
    password: ShardingProxy@2025
    connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
    maxPoolSize: 50
    minPoolSize: 10
  
  # 从库1(读库)
  replica_ds_1:
    url: jdbc:mysql://192.168.1.102:3306/app_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: sharding_proxy
    password: ShardingProxy@2025
    connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
    maxPoolSize: 50
    minPoolSize: 10
  
  # 从库2(读库)
  replica_ds_2:
    url: jdbc:mysql://192.168.1.103:3306/app_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: sharding_proxy
    password: ShardingProxy@2025
    connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
    maxPoolSize: 50
    minPoolSize: 10

# 读写分离规则
readWriteSplittingRule:
  dataSources:
    prds:
      writeDataSourceName: primary_ds
      readDataSourceNames:
        - replica_ds_1
        - replica_ds_2
      # 负载均衡策略:ROUND_ROBIN/RANDOM/STATIC_REPLICA_QUERY
      # STATIC_REPLICA_QUERY:每次查询按顺序使用从库
      loadBalancerName: ROUND_ROBIN
  
  # 配置负载均衡算法
  loadBalancers:
    ROUND_ROBIN:
      type: ROUND_ROBIN
    RANDOM:
      type: RANDOM
    STATIC_REPLICA_QUERY:
      type: STATIC_REPLICA_QUERY

rules:
  - !READWRITE_SPLITTING
    dataSources:
      prds:
        writeDataSourceName: primary_ds
        readDataSourceNames:
          - replica_ds_1
          - replica_ds_2
        loadBalancerName: round_robin
    loadBalancers:
      round_robin:
        type: ROUND_ROBIN
        props:
          default: 0

4.4 分库分表配置(订单表)

yaml 复制代码
# conf/config-sharding.yaml

schemaName: sharding_db

dataSources:
  ds_0:
    url: jdbc:mysql://192.168.1.101:3306/app_ds_0?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: sharding_proxy
    password: ShardingProxy@2025
    connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
    maxPoolSize: 50
  
  ds_1:
    url: jdbc:mysql://192.168.1.101:3306/app_ds_1?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: sharding_proxy
    password: ShardingProxy@2025
    connectionPoolClassName: com.zaxxer.hikari.HikariDataSource
    maxPoolSize: 50

rules:
  - !SHARDING
    tables:
      # 订单表按user_id取模分4片(2库×2表)
      t_order:
        actualDataNodes: ds_${0..1}.t_order_${0..1}
        databaseStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: database_inline
        tableStrategy:
          standard:
            shardingColumn: order_id
            shardingAlgorithmName: table_inline
        keyGenerateStrategy:
          column: order_id
          keyGeneratorName: snowflake
    
    # 分片算法配置
    shardingAlgorithms:
      database_inline:
        type: INLINE
        props:
          algorithm-expression: ds_${user_id % 2}
          allow-range-query-with-inline-sharding: true
      table_inline:
        type: INLINE
        props:
          algorithm-expression: t_order_${order_id % 2}
    
    # 分布式主键生成器
    keyGenerators:
      snowflake:
        type: SNOWFLAKE
        props:
          # 工作机器ID(根据服务器IP生成,确保唯一)
          worker-id: 1
          max-vibration-offset: 1

4.5 应用连接ShardingSphere-Proxy

java 复制代码
// application.yml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 连接ShardingSphere-Proxy,而非直接连接MySQL
    url: jdbc:mysql://192.168.1.104:3307/readwrite_splitting_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    username: sharding_proxy
    password: ShardingProxy@2025
    hikari:
      minimum-idle: 20
      maximum-pool-size: 100
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000

# MyBatis Plus配置
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  # 开发环境打印SQL
  global-config:
    db-config:
      id-type: auto  # ShardingSphere管理主键,使用auto_increment
java 复制代码
// 验证读写分离路由
@Service
public class ShardingSphereTestService {
    
    @Autowired
    private JdbcTemplate jdbcTemplate;
    
    /**
     * 写操作:验证是否路由到主库
     */
    @Transactional
    public void writeData() {
        jdbcTemplate.execute("INSERT INTO t_user (name, email) VALUES ('张三', 'zhangsan@example.com')");
        
        // 查询当前连接的数据源(通过hint强制路由验证)
        String sql = "SELECT * FROM t_user ORDER BY id DESC LIMIT 1";
        Map<String, Object> result = jdbcTemplate.queryForMap(sql);
        System.out.println("写入数据: " + result);
    }
    
    /**
     * 读操作:验证是否路由到从库
     */
    public void readData() {
        String sql = "SELECT COUNT(*) FROM t_user";
        Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
        System.out.println("用户总数: " + count);
    }
}

五、踩坑实录:血泪凝结的避坑指南

踩坑1:主从复制延迟导致订单状态错乱

现象: 用户下单后立刻查询订单列表,新订单显示不出来;但数据库里明明已经插入了。

原因: 主从延迟导致订单刚写入主库,立即查询从库时数据还没同步过来。

排查方法:

sql 复制代码
-- 在从库查看复制延迟(seconds_behind_master)
SHOW SLAVE STATUS\G
-- 关键字段:
-- Seconds_Behind_Master: 0 表示无延迟,>0 表示延迟秒数
-- Slave_IO_Running: Yes
-- Slave_SQL_Running: Yes
-- Last_Error: (如果不为空,表示有错误)

解决方案:

java 复制代码
// 方案1:强一致性读强制走主库
@QueryHints(value = @QueryHint(name = "readFromMaster", value = "true"))
@Query("SELECT o FROM Order o WHERE o.orderId = :orderId")
Optional<Order> findByIdFromMaster(@Param("orderId") Long orderId);

// 方案2:应用层记录主库写入标记,前置读主库
public Order createOrderAndReturn(Order order) {
    orderMapper.insert(order);
    // 创建后立刻读取,使用强制路由主库
    return orderMapper.selectByIdForMaster(order.getId());
}

// 方案3:读取从库但等待延迟完成(半同步模式天然降低延迟)
// 已在本文第二章配置半同步复制,此处配合使用

踩坑2:GTID复制跳过错误导致从库停止

现象: 从库SQL线程报错Duplicate entry,直接停止复制,监控告警疯狂响。

原因: 某次数据修复时重复插入了主键冲突的数据,导致从库执行失败。

错误处理流程:

sql 复制代码
-- 1. 查看错误详情
SHOW SLAVE STATUS\G
-- Last_Error: Could not execute Write_rows_v1 event on table app_db.t_order; 
-- Duplicate entry '123456' for key 'PRIMARY'

-- 2. 临时跳过该错误(只跳过一次)
SET GLOBAL sql_slave_skip_counter = 1;
START SLAVE;
-- 注意:sql_slave_skip_counter 已废弃,GTID模式下不生效

-- 3. GTID模式下的正确处理方式
-- 先查看gtid_executed找出错在哪
SELECT * FROM performance_schema.replication_execute_status_by_coordinator
WHERE LAST_ERROR_NUMBER != 0;

-- 手动处理:在从库补录缺失数据,然后从GTID断点继续
SET GTID_NEXT = 'server_uuid:position';
BEGIN;
COMMIT;
SET GTID_NEXT = AUTOMATIC;
START SLAVE;

踩坑3:MHA切换后应用连接"粘"在旧主库

现象: MHA成功切换到新主库,VIP也漂移了,但业务还是报错"连接被拒绝"。

原因: 应用服务器DNS缓存了旧主库的连接,或者连接池里的连接还是指向旧IP。

解决方案:

java 复制代码
// 1. 数据库连接URL使用VIP而非IP(推荐)
jdbc:mysql://192.168.1.100:3306/app_db  // VIP会自动漂移到新主库

// 2. 连接池配置最小连接数和健康检查
HikariCP配置:
spring:
  datasource:
    hikari:
      minimum-idle: 10
      maximum-pool-size: 100
      connection-timeout: 30000
      idle-timeout: 600000
      max-lifetime: 1800000
      # 定期检查连接是否有效
      connection-test-query: SELECT 1

// 3. MHA切换后刷新应用端连接(通过配置中心推送)
// Consul/etcd 配置变更 → 应用收到通知 → 重启连接池

踩坑4:ShardingSphere分页查询性能悬崖

现象: 订单列表第1页加载很快(100ms),第10页加载要8秒,第100页直接超时。

原因: 分片环境下,分页查询需要跨多个节点合并数据。例如LIMIT 1000, 20需要每个分片先查出前1020条,然后合并后再取20条。分片越多,性能越差。

优化方案:

yaml 复制代码
# ShardingSphere配置
rules:
  - !SHARDING
    tables:
      t_order:
        # 限制最大偏移量,避免深度分页
        config:
          max-allowed-packet-size: 20MB
    props:
      # 开启改写优化
      sql-show: true  # 开发环境打印SQL改写
  
# 代码层优化:禁止深度分页
public PageResult<OrderVO> queryOrders(Long userId, Integer page, Integer size) {
    // 业务层强制限制最大页码
    if (page > 100) {
        throw new BusinessException("查询太深,请使用时间范围筛选");
    }
    
    // 使用游标分页替代偏移量分页(性能稳定)
    if (lastId != null) {
        // 基于ID的游标分页,性能恒定
        return orderMapper.selectByUserIdAndAfterId(userId, lastId, size);
    } else {
        return orderMapper.selectByUserIdWithPage(userId, page, size);
    }
}

六、总结与思考

核心要点回顾

  1. Binlog是主从复制的核心:Row格式是生产环境的最佳选择,虽然Binlog体积大,但数据一致性有保障。
  2. 半同步复制是安全与性能的平衡:在写入延迟可接受的前提下,大幅降低数据丢失风险。
  3. MHA是成熟的主从高可用方案:配合VIP漂移可实现故障自动切换,RTO可控制在30秒内。
  4. ShardingSphere-Proxy对应用透明:适合不想修改代码的团队,但也要注意分页性能、分布式事务等限制。
  5. 监控是数据库的"生命线":复制延迟、连接数、慢查询数都需要实时监控。

思考题

  1. 半同步复制在网络延迟极高的跨机房场景下,rpl_semi_sync_master_timeout应该如何设置?设太长会有什么后果?
  2. MHA切换过程中,如果正在有长事务执行,切换后数据一致性如何保证?有没有更好的方案(如MySQL Group Replication)?
  3. ShardingSphere-Proxy在分片数量较多时(如100个分片),SQL路由和结果聚合的性能如何评估?
  4. 如果主从延迟持续超过10秒,在业务层面应该采取什么策略来保证用户体验?

个人观点

MySQL主从复制和读写分离是数据库架构的"入门级"高可用方案,但越是基础的方案越容易踩坑。我的血泪经验是:不要过度依赖主从复制来解决性能问题------它主要解决的是高可用和数据安全,读写分离能分担一部分读压力,但核心的写入瓶颈还是要靠分库分表或NewSQL来解决。

另外,自动化运维工具(Ansible/MHA)配合标准化流程 比纯手工操作可靠100倍。笔者见过太多团队靠"胸牌DBA"(人肉运维)在凌晨手动执行故障切换,结果手抖把从库当主库的操作。建议任何切换操作前必须有可执行的回滚方案


📌 下一期预告:《【架构实战】分库分表ShardingSphere:突破数据库瓶颈》

敬请期待!

相关推荐
上海云盾-小余7 小时前
业务层 CC 攻击精准研判:行为识别与轻量化拦截方案
运维·服务器·安全·架构
Cosolar7 小时前
2026年全球向量数据库技术全景与架构演进深度解析报告
数据库·人工智能·架构·agent·智能体
IronMurphy7 小时前
Redis拷打第七讲(最终章)
数据库·redis·php
米高梅狮子8 小时前
03.OpenStack使用
linux·前端·云原生·容器·架构·kubernetes·openstack
张~颜8 小时前
PostgreSQL复制槽
数据库·postgresql
爱晒太阳的小老鼠8 小时前
数据库连接池Connection is not available, request timed out after 120000ms
数据库
SL_staff8 小时前
从Zoom/腾讯会议迁移到私有化会议系统:数据迁移完整方案
java·架构
2301_803934618 小时前
SQL如何进行分组后字符串拼接_使用GROUP_CONCAT或STRING_AGG
jvm·数据库·python
木易 士心8 小时前
深入理解 OKHttp:设计模式、核心机制与架构优势
android·设计模式·架构