摘要:从一次"秒杀活动主从延迟5秒导致超卖"的严重故障出发,深度剖析主从延迟的6种高级解决方案。通过并行复制的配置优化、MTS多线程复制的原理图解、以及半同步复制的性能权衡,揭秘如何将主从延迟从5秒降到50ms。配合时序图展示复制流程,给出不同业务场景下的最佳选型和配置参数。
💥 翻车现场
双十一当天,中午12点。
告警:
🚨 秒杀活动出现超卖!
🚨 库存100件,卖出了127件!
🚨 用户投诉:显示有货,下单后提示缺货
哈吉米(崩溃):"怎么又超卖了?我明明做了库存检查啊!"
紧急查看代码:
// 秒杀接口(读写分离架构)
public boolean seckill(Long userId, Long productId) {
// 1. 查询库存(走从库)
Stock stock = stockMapper.selectByProductId(productId);
if (stock.getNum() <= 0) {
return false;
}
// 2. 扣减库存(写主库)
stockMapper.decreaseStock(productId);
// 3. 创建订单
orderMapper.insert(order);
return true;
}
哈吉米:"代码没问题啊……"
紧急查看主从延迟:
-- 从库执行
SHOW SLAVE STATUS\G
Seconds_Behind_Master: 5 ← 延迟5秒!
问题分析:
时间线:
T1: 主库库存=100
T2: 用户A扣减库存(主库库存=99),但从库还是100(延迟5秒)
T3: 用户B查询库存(从库库存=100),检查通过 ✅
T4: 用户B扣减库存(主库库存=98)
T5: 用户C查询库存(从库库存=100),检查通过 ✅
T6: 用户C扣减库存(主库库存=97)
...
结果:100个库存,因为主从延迟,多卖了27件!
哈吉米:"卧槽,主从延迟导致的超卖!"
晚上,南北绿豆和阿西噶阿西来了。
南北绿豆:"主从延迟在高并发场景下会放大问题!必须优化!"
阿西噶阿西:"我给你讲6种高级解决方案。"
🚀 方案1:并行复制(MTS多线程复制)
问题:SQL线程是单线程
主库:
1秒写入10万条SQL
从库:
SQL线程是单线程,1秒只能执行5万条
结果:延迟越来越大
解决:开启并行复制
MySQL 5.6+支持并行复制
-- 从库执行
-- 查看当前配置
SHOW VARIABLES LIKE 'slave_parallel%';
+------------------------+----------+
| Variable_name | Value |
+------------------------+----------+
| slave_parallel_workers | 0 | ← 0表示单线程
| slave_parallel_type | DATABASE |
+------------------------+----------+
-- 开启并行复制(4个SQL线程)
STOP SLAVE SQL_THREAD;
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK'; -- MySQL 5.7+
SET GLOBAL slave_parallel_workers = 4; -- 4个线程
START SLAVE SQL_THREAD;
并行复制的类型
| 类型 | 并行粒度 | 适用场景 |
|---|---|---|
| DATABASE | 不同数据库并行 | 多个数据库 |
| LOGICAL_CLOCK | 同时提交的事务并行 | ⭐⭐⭐⭐⭐ 推荐 |
并行复制流程图
sequenceDiagram
participant Master as 主库
participant IOThread as 从库IO线程
participant SQL1 as 从库SQL线程1
participant SQL2 as 从库SQL线程2
participant SQL3 as 从库SQL线程3
participant SQL4 as 从库SQL线程4
Master->>IOThread: binlog(10万条SQL)
IOThread->>IOThread: 写relay log
par 并行执行
IOThread->>SQL1: 分配事务1
IOThread->>SQL2: 分配事务2
IOThread->>SQL3: 分配事务3
IOThread->>SQL4: 分配事务4
SQL1->>SQL1: 执行SQL
SQL2->>SQL2: 执行SQL
SQL3->>SQL3: 执行SQL
SQL4->>SQL4: 执行SQL
end
Note over SQL1,SQL4: 4个线程并行<br/>吞吐量提升4倍
性能对比:
| 配置 | 吞吐量 | 延迟 |
|---|---|---|
| 单线程 | 5万条/秒 | 5秒 |
| 4线程并行 | 20万条/秒 | 0.5秒 |
性能提升:10倍
🚀 方案2:半同步复制 + 组提交
半同步复制配置
-- 主库安装插件
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
-- 从库安装插件
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
-- 主库开启半同步
SET GLOBAL rpl_semi_sync_master_enabled = 1;
SET GLOBAL rpl_semi_sync_master_timeout = 1000; -- 超时1秒降级为异步
-- 从库开启半同步
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
组提交优化
-- 主库开启组提交(批量发送binlog)
SET GLOBAL binlog_group_commit_sync_delay = 1000; -- 延迟1ms
SET GLOBAL binlog_group_commit_sync_no_delay_count = 10; -- 或攒够10个事务
原理:
不用组提交:
事务1提交 → 发送binlog → 等待从库确认
事务2提交 → 发送binlog → 等待从库确认
...
用组提交:
事务1提交 →
事务2提交 → 攒一批
事务3提交 →
一起发送binlog → 一次等待从库确认
性能提升:减少网络往返次数
性能对比:
| 配置 | TPS | 主从延迟 |
|---|---|---|
| 异步复制 | 10000 | 2-5秒 |
| 半同步(无组提交) | 3000 | 50ms |
| 半同步 + 组提交 | 7000 | 100ms |
🚀 方案3:读写分离智能路由
原理:刚写入的数据强制走主库
@Service
public class SmartDataSourceService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 写入数据时,记录"刚写入"
*/
@Transactional
public void createOrder(Order order) {
// 1. 写主库
orderMapper.insert(order);
// 2. 在Redis标记"刚写入"(1秒过期)
String key = "just_wrote:order:" + order.getId();
redisTemplate.opsForValue().set(key, "1", 1, TimeUnit.SECONDS);
}
/**
* 读取数据时,智能路由
*/
public Order getOrderById(Long orderId) {
String key = "just_wrote:order:" + orderId;
// 检查是否刚写入
if (redisTemplate.hasKey(key)) {
// 刚写入的,强制走主库
return orderMapper.selectByIdFromMaster(orderId);
}
// 否则走从库
return orderMapper.selectByIdFromSlave(orderId);
}
}
时序图:
sequenceDiagram
participant User as 用户
participant App as 应用
participant Redis
participant Master as 主库
participant Slave as 从库
User->>App: 1. 创建订单
App->>Master: 2. INSERT订单
App->>Redis: 3. 标记"刚写入"(1秒)
App->>User: 4. 返回成功
User->>App: 5. 查询订单(1秒内)
App->>Redis: 6. 检查是否刚写入?
Redis->>App: 7. 是,有标记
App->>Master: 8. 走主库查询 ✅
Master->>User: 9. 返回订单
Note over User,Slave: 1秒后
User->>App: 10. 再次查询
App->>Redis: 11. 检查是否刚写入?
Redis->>App: 12. 否,标记过期了
App->>Slave: 13. 走从库查询
Slave->>User: 14. 返回订单
优点:
- ✅ 99%的查询走从库(性能好)
- ✅ 刚写入的数据走主库(一致性好)
- ✅ 自适应
🚀 方案4:分离核心业务和非核心业务
原理:核心业务强制主库,非核心业务可以从库
@Service
public class OrderService {
/**
* 核心业务:支付后查询(强制主库)
*/
@DataSource("master")
public Order getOrderAfterPayment(Long orderId) {
return orderMapper.selectById(orderId);
}
/**
* 非核心业务:订单列表(可以从库)
*/
@DataSource("slave")
public List<Order> listOrders(Long userId) {
return orderMapper.selectByUserId(userId);
}
}
业务分类:
| 业务类型 | 一致性要求 | 推荐数据源 |
|---|---|---|
| 支付后查询 | 强 | 主库 |
| 创建后查询 | 强 | 主库 |
| 订单列表 | 弱 | 从库 |
| 历史订单 | 弱 | 从库 |
| 统计报表 | 弱 | 从库 |
🚀 方案5:延迟监控 + 自动降级
原理:主从延迟超过阈值,自动切换主库
@Component
public class ReplicationLagMonitor {
@Autowired
private DataSource slaveDataSource;
private volatile boolean slaveLagging = false;
/**
* 定时检测主从延迟
*/
@Scheduled(fixedDelay = 1000) // 每秒检测
public void checkReplicationLag() {
try (Connection conn = slaveDataSource.getConnection()) {
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SHOW SLAVE STATUS");
if (rs.next()) {
int secondsBehind = rs.getInt("Seconds_Behind_Master");
// 延迟超过2秒,标记为延迟
if (secondsBehind > 2) {
slaveLagging = true;
log.warn("主从延迟: {}秒,切换到主库", secondsBehind);
} else {
slaveLagging = false;
}
}
} catch (Exception e) {
log.error("检测主从延迟失败", e);
slaveLagging = true; // 检测失败,保守起见走主库
}
}
/**
* 是否延迟
*/
public boolean isSlaveLagging() {
return slaveLagging;
}
}
@Service
public class SmartOrderService {
@Autowired
private ReplicationLagMonitor monitor;
public Order getOrderById(Long orderId) {
// 如果主从延迟,走主库
if (monitor.isSlaveLagging()) {
return orderMapper.selectByIdFromMaster(orderId);
}
// 否则走从库
return orderMapper.selectByIdFromSlave(orderId);
}
}
优点:
- ✅ 自动切换
- ✅ 保证一致性
🚀 方案6:业务补偿机制
原理:容忍短暂不一致,后续补偿
/**
* 秒杀场景:先扣库存,后续异步校验
*/
@Service
public class SeckillServiceV2 {
@Transactional
public boolean seckill(Long userId, Long productId) {
// 1. 强制走主库查询库存
Stock stock = stockMapper.selectByIdFromMaster(productId);
if (stock.getNum() <= 0) {
return false;
}
// 2. 扣减库存(主库)
int updated = stockMapper.decreaseStock(productId);
if (updated == 0) {
return false;
}
// 3. 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setStatus(0); // 待确认
orderMapper.insert(order);
// 4. 发送异步消息:延迟校验库存
rabbitTemplate.convertAndSend("seckill.verify.queue", order.getId());
return true;
}
}
/**
* 延迟校验(1秒后,主从已同步)
*/
@Component
public class SeckillVerifyConsumer {
@RabbitListener(queues = "seckill.verify.queue")
public void verifyOrder(Long orderId) {
// 延迟1秒
Thread.sleep(1000);
// 校验库存(此时主从已同步)
Order order = orderMapper.selectById(orderId);
Stock stock = stockMapper.selectByProductId(order.getProductId());
if (stock.getNum() < 0) {
// 超卖了,取消订单
orderMapper.updateStatus(orderId, -1); // 已取消
// 恢复库存
stockMapper.increaseStock(order.getProductId(), 1);
// 通知用户
notifyUser(order.getUserId(), "抢购失败,库存不足");
} else {
// 确认订单
orderMapper.updateStatus(orderId, 1); // 已确认
}
}
}
优点:
- ✅ 性能好(异步校验)
- ✅ 自动补偿
缺点:
- ⚠️ 用户体验稍差(可能先显示成功,后续取消)
📊 性能对比
测试场景:秒杀100个库存
| 方案 | 主从延迟 | 是否超卖 | TPS |
|---|---|---|---|
| 异步复制 + 读从库 | 5秒 | ✅ 超卖27个 | 10000 |
| 强制走主库 | - | ❌ 不超卖 | 3000 |
| 半同步复制 | 50ms | ❌ 不超卖 | 7000 |
| 并行复制(4线程) | 0.5秒 | ✅ 可能超卖3个 | 9000 |
| 智能路由(Redis标记) | 1秒 | ❌ 不超卖 | 8000 |
| 业务补偿 | 5秒(异步校验) | ❌ 不超卖 | 9500 |
🎓 面试标准答案
题目:主从延迟有哪些解决方案?
答案:
6种解决方案:
-
强制走主库
- 写入后立即读取,走主库
- 适合:核心业务(支付、订单)
-
二次读取
- 从库查不到,再查主库
- 适合:大部分场景
-
并行复制
- 开启多线程SQL线程
- 延迟从5秒降到0.5秒
-
半同步复制
- 等从库确认接收binlog
- 延迟降到50ms
-
智能路由
- Redis标记刚写入的数据
- 刚写入的走主库,其他走从库
-
业务补偿
- 容忍短暂不一致
- 后续异步校验和补偿
选型建议:
- 金融支付:半同步 + 强制主库
- 电商订单:智能路由 + 业务补偿
- 社交内容:最终一致性
🎉 结束语
一周后,哈吉米把主从延迟优化完了。
哈吉米:"开启了并行复制,延迟从5秒降到0.5秒!"
南北绿豆:"对,4个SQL线程并行,吞吐量提升4倍。"
阿西噶阿西:"核心业务加了智能路由,刚写入的数据走主库,再也不会因为主从延迟导致超卖了。"
哈吉米:"还有半同步复制,虽然性能下降30%,但数据更安全。"
南北绿豆:"对,根据业务场景选方案,没有银弹!"
记忆口诀:
并行复制多线程,吞吐量翻四倍强
半同步等从库认,延迟五十毫秒降
智能路由Redis标,刚写数据走主库
业务补偿异步校,容忍延迟后补偿
核心业务强主库,非核心可从库
希望这篇文章能帮你搞定主从延迟的各种场景!根据业务选方案,性能和一致性都能兼顾!💪