【架构实战】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);
}
}
六、总结与思考
核心要点回顾
- Binlog是主从复制的核心:Row格式是生产环境的最佳选择,虽然Binlog体积大,但数据一致性有保障。
- 半同步复制是安全与性能的平衡:在写入延迟可接受的前提下,大幅降低数据丢失风险。
- MHA是成熟的主从高可用方案:配合VIP漂移可实现故障自动切换,RTO可控制在30秒内。
- ShardingSphere-Proxy对应用透明:适合不想修改代码的团队,但也要注意分页性能、分布式事务等限制。
- 监控是数据库的"生命线":复制延迟、连接数、慢查询数都需要实时监控。
思考题
- 半同步复制在网络延迟极高的跨机房场景下,
rpl_semi_sync_master_timeout应该如何设置?设太长会有什么后果? - MHA切换过程中,如果正在有长事务执行,切换后数据一致性如何保证?有没有更好的方案(如MySQL Group Replication)?
- ShardingSphere-Proxy在分片数量较多时(如100个分片),SQL路由和结果聚合的性能如何评估?
- 如果主从延迟持续超过10秒,在业务层面应该采取什么策略来保证用户体验?
个人观点
MySQL主从复制和读写分离是数据库架构的"入门级"高可用方案,但越是基础的方案越容易踩坑。我的血泪经验是:不要过度依赖主从复制来解决性能问题------它主要解决的是高可用和数据安全,读写分离能分担一部分读压力,但核心的写入瓶颈还是要靠分库分表或NewSQL来解决。
另外,自动化运维工具(Ansible/MHA)配合标准化流程 比纯手工操作可靠100倍。笔者见过太多团队靠"胸牌DBA"(人肉运维)在凌晨手动执行故障切换,结果手抖把从库当主库的操作。建议任何切换操作前必须有可执行的回滚方案。
📌 下一期预告:《【架构实战】分库分表ShardingSphere:突破数据库瓶颈》
敬请期待!