摘要:从一次"用户刚下单就查不到订单"的线上故障出发,深度剖析MySQL主从复制原理与读写分离架构。通过图解binlog的三种格式、三个线程的协作过程,揭秘异步复制、半同步复制、组提交的区别。详解主从延迟的5种根因和6种解决方案,配合ShardingSphere、MyCat等中间件的实战配置,以及强制主库、延迟补偿、二次读取等数据一致性保障手段,让你彻底搞懂"为什么读写分离会数据不一致"、"如何在性能和一致性之间权衡"等核心问题。
💥 翻车现场
周五晚上8点,哈吉米正在吃晚饭,手机突然疯狂震动。
客服主管:@哈吉米 紧急!大量用户反馈下单后查不到订单!
哈吉米:???怎么可能,我看看日志!
用户A(user_id=10086):
20:05:32 - 下单成功(order_id=100001)
20:05:33 - 查询订单列表 → 空!
20:05:35 - 再次查询 → 看到了!
用户B(user_id=10087):
20:06:15 - 下单成功(order_id=100002)
20:06:16 - 查询订单详情 → 订单不存在!
20:06:18 - 刷新页面 → 出现了!
哈吉米:"卧槽,下单后立马查不到,过几秒就能查到了?"
紧急回滚后,哈吉米查看架构图:
【应用服务器】
↓
写操作 ↓ 读操作
↓
MySQL主库 → MySQL从库1
(写) 同步 (读)
↘
MySQL从库2
(读)
哈吉米:"我上周刚上的读写分离,难道是主从延迟?"
第二天早上,南北绿豆和阿西噶阿西来了。
南北绿豆:"典型的主从延迟问题!你写入主库,立马读从库,但主从还没同步完成。"
阿西噶阿西:"而且我猜你用的是异步复制,根本不管从库有没有同步成功。"
哈吉米:"那怎么办?改成同步复制?"
南北绿豆:"不是这么简单!来,我给你讲讲主从复制的原理。"
🤔 主从复制的原理:binlog的奇妙旅程
南北绿豆在白板上画了一个流程图。
主从复制的完整流程
【主库 Master】
↓
1. 执行SQL(UPDATE、INSERT)
↓
2. 写入binlog(二进制日志)
↓
3. 提交事务
↓
┌──────────────┴──────────────┐
↓ ↓
【Dump线程】 【主库业务】
读取binlog 继续处理请求
↓
通过网络发送
↓
【从库 Slave】
↓
【IO线程】接收binlog
↓
写入relay log(中继日志)
↓
【SQL线程】读取relay log
↓
执行SQL,写入从库
↓
从库数据更新完成
三个关键线程:
| 线程 | 所在位置 | 作用 |
|---|---|---|
| Dump线程 | 主库 | 读取binlog,发送给从库 |
| IO线程 | 从库 | 接收binlog,写入relay log |
| SQL线程 | 从库 | 读取relay log,执行SQL |
哈吉米:"所以主从复制是异步的?主库不等从库同步完就返回了?"
阿西噶阿西:"对!这就是异步复制的特点,性能好但可能丢数据。"
详细时序图
时间轴:
T1: 主库执行 INSERT INTO order (order_id, user_id) VALUES (100001, 10086);
T2: 主库写入binlog
T3: 主库提交事务 → 返回"插入成功"给应用
↓
此时从库还没同步!
↓
T4: Dump线程读取binlog(主库)
T5: 通过网络发送binlog(可能有延迟)
T6: IO线程接收binlog(从库)
T7: IO线程写入relay log(从库)
T8: SQL线程读取relay log(从库)
T9: SQL线程执行INSERT(从库)
T10: 从库数据更新完成
应用查询时间轴:
T3.5: 应用查询从库(SELECT * FROM order WHERE order_id = 100001)
→ 查不到!(因为从库还在T6-T9的同步过程中)
南北绿豆:"看到了吗?T3主库就返回成功了,但T10从库才真正写入,中间有7个步骤的延迟!"
主从延迟的来源
阿西噶阿西:"主从延迟来自这几个环节:"
| 环节 | 延迟来源 | 典型耗时 |
|---|---|---|
| 网络传输 | 主库 → 从库的网络延迟 | 1-10ms(局域网) 50-200ms(跨地域) |
| IO线程写入 | relay log写入磁盘 | 1-5ms |
| SQL线程执行 | 从库执行SQL | 取决于SQL复杂度 |
| 锁等待 | 从库有其他查询占用锁 | 0-1000ms+ |
| 大事务 | 主库一个大事务,从库要完整执行 | 可能几秒甚至几分钟 |
总延迟 = 网络 + 写入 + 执行 + 锁等待 + 大事务
哈吉米:"所以主从延迟不可避免?"
南北绿豆:"对!但可以优化到很小(10ms以内)。"
📊 binlog的三种格式:ROW vs STATEMENT vs MIXED
阿西噶阿西:"主从复制靠binlog,但binlog有三种格式,选错了会出大问题!"
格式1️⃣:STATEMENT(语句复制)
定义:记录原始SQL语句。
示例:
-- 主库执行
UPDATE account SET balance = balance + 100 WHERE user_id = 10086;
-- binlog记录(STATEMENT格式)
UPDATE account SET balance = balance + 100 WHERE user_id = 10086;
-- 从库执行
UPDATE account SET balance = balance + 100 WHERE user_id = 10086;
优点:
- ✅ binlog体积小(只记录SQL)
- ✅ 网络传输快
缺点:
- ❌ 某些SQL可能导致主从数据不一致
危险案例:
-- 主库执行(假设当前时间是 2024-10-07 20:05:32)
INSERT INTO log (user_id, create_time)
VALUES (10086, NOW());
-- binlog记录
INSERT INTO log (user_id, create_time)
VALUES (10086, NOW()); -- NOW()是函数,不是具体值
-- 从库执行(假设执行时间是 2024-10-07 20:05:35,延迟了3秒)
INSERT INTO log (user_id, create_time)
VALUES (10086, NOW()); -- NOW()在从库上是20:05:35!
结果:
主库:create_time = 2024-10-07 20:05:32
从库:create_time = 2024-10-07 20:05:35 ← 数据不一致!
其他危险函数:
| 函数 | 问题 |
|---|---|
NOW() | 主从执行时间不同 |
UUID() | 每次生成的UUID不同 |
RAND() | 随机数不同 |
USER() | 主从用户可能不同 |
@@auto_increment_increment | 自增步长可能不同 |
哈吉米:"卧槽,这不是坑吗?"
南北绿豆:"所以现在基本不用STATEMENT格式了。"
格式2️⃣:ROW(行复制,推荐)
定义:记录每一行数据的实际变化。
示例:
-- 主库执行
UPDATE account SET balance = balance + 100 WHERE user_id = 10086;
-- 假设user_id=10086的balance原本是500
-- binlog记录(ROW格式,简化表示)
### UPDATE `account`
### WHERE
### user_id = 10086
### balance = 500 -- 记录修改前的值(用于回滚)
### SET
### balance = 600 -- 记录修改后的值(具体数字)
优点:
- ✅ 数据一致性强(记录的是实际值,不是函数)
- ✅ 可以精确恢复数据(闪回)
- ✅ 不受函数影响(NOW()、UUID()等)
缺点:
- ❌ binlog体积大(每行都记录)
- ❌ 批量操作binlog巨大
批量操作的坑:
-- 主库执行
UPDATE account SET balance = balance + 100; -- 更新了100万行
-- STATEMENT格式的binlog
UPDATE account SET balance = balance + 100; -- 只有1条SQL,很小
-- ROW格式的binlog
### UPDATE `account` WHERE user_id=1 SET balance=600
### UPDATE `account` WHERE user_id=2 SET balance=750
### UPDATE `account` WHERE user_id=3 SET balance=820
### ... (100万条记录)
结果:ROW格式的binlog可能有几百MB!
阿西噶阿西:"所以ROW格式虽然安全,但批量操作时binlog会很大,传输慢,从库同步慢。"
格式3️⃣:MIXED(混合模式,智能选择)
定义:MySQL自动选择STATEMENT或ROW。
规则:
- 普通SQL → 用STATEMENT(体积小)
- 有不确定函数的SQL → 用ROW(保证一致性)
示例:
-- 这条SQL用STATEMENT
UPDATE account SET balance = 600 WHERE user_id = 10086;
-- 这条SQL用ROW(因为有NOW())
INSERT INTO log (user_id, create_time) VALUES (10086, NOW());
优点:
- ✅ 兼顾性能和一致性
- ✅ 自动选择
缺点:
- ⚠️ 有时候MySQL判断不准确
三种格式对比
| 格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| STATEMENT | binlog小,传输快 | 某些SQL主从不一致 | ❌ 不推荐 |
| ROW | 一致性强,可精确恢复 | binlog大,批量操作慢 | ✅ 推荐(默认) |
| MIXED | 自动平衡 | 判断可能不准 | ⚠️ 可选 |
查看和设置binlog格式:
-- 查看当前binlog格式
SHOW VARIABLES LIKE 'binlog_format';
-- 设置binlog格式(全局)
SET GLOBAL binlog_format = 'ROW';
-- 设置binlog格式(当前会话)
SET SESSION binlog_format = 'ROW';
南北绿豆:"现在MySQL 8.0默认就是ROW格式,保证数据一致性。"
🔧 复制模式对比:异步 vs 半同步 vs 组提交
哈吉米:"既然异步复制有延迟,能不能改成同步复制?"
阿西噶阿西:"有三种模式,各有优缺点。"
模式1️⃣:异步复制(Asynchronous Replication,默认)
流程:
主库执行SQL
↓
写入binlog
↓
提交事务 → 立即返回"成功"
↓
后台异步发送binlog给从库
时序图:
时间轴:
T1: 主库写入binlog
T2: 主库提交事务 → 返回"成功"(此时从库还没收到)
T3: Dump线程发送binlog
T4: 从库接收并执行
问题:T2和T4之间有延迟,如果T2.5查询从库,查不到数据
优点:
- ✅ 性能最好(主库不等从库)
- ✅ 主库不受从库影响(从库挂了,主库正常)
缺点:
- ❌ 主从延迟
- ❌ 主库宕机可能丢数据(binlog还没发送给从库)
适用场景:对一致性要求不高,追求性能
模式2️⃣:半同步复制(Semi-Synchronous Replication)
流程:
主库执行SQL
↓
写入binlog
↓
发送binlog给从库
↓
等待至少1个从库确认接收 ← 关键!
↓
提交事务 → 返回"成功"
时序图:
时间轴:
T1: 主库写入binlog
T2: 发送binlog给从库
T3: 从库IO线程接收binlog,写入relay log
T4: 从库发送ACK确认 → 主库收到
T5: 主库提交事务 → 返回"成功"(此时从库已经接收)
T6: 从库SQL线程执行SQL
注意:T5时从库虽然接收了binlog,但还没执行(T6才执行)
所以仍有小延迟!
优点:
- ✅ 数据更安全(至少1个从库收到binlog)
- ✅ 主库宕机不丢数据(从库有完整binlog)
缺点:
- ❌ 性能下降(主库要等从库确认)
- ❌ 仍有延迟(从库接收≠从库执行)
配置方法:
-- 主库安装半同步插件
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;
-- 设置超时时间(10秒内从库没确认,降级为异步)
SET GLOBAL rpl_semi_sync_master_timeout = 10000;
-- 从库开启半同步
SET GLOBAL rpl_semi_sync_slave_enabled = 1;
超时降级:
如果10秒内从库没确认(网络故障、从库宕机):
主库自动降级为异步复制,保证主库可用
哈吉米:"所以半同步也不能保证实时一致?"
南北绿豆:"对!因为从库接收binlog ≠ 从库执行完SQL,中间还有SQL线程执行的时间。"
模式3️⃣:组提交(Group Commit)
定义:多个事务的binlog一起发送,减少网络开销。
流程:
事务1提交 → 写入binlog
事务2提交 → 写入binlog
事务3提交 → 写入binlog
↓
攒够一批(或达到时间阈值)
↓
一次性发送给从库
优点:
- ✅ 减少网络开销(批量发送)
- ✅ 提升主从复制性能
配置方法:
-- 开启binlog组提交
SET GLOBAL binlog_group_commit_sync_delay = 1000; -- 延迟1ms攒一批
SET GLOBAL binlog_group_commit_sync_no_delay_count = 100; -- 攒够100个事务就发送
三种模式对比
| 模式 | 性能 | 一致性 | 数据安全 | 适用场景 |
|---|---|---|---|---|
| 异步复制 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ | 非核心业务,追求性能 |
| 半同步复制 | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 金融、支付等核心业务 |
| 组提交 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | 配合半同步使用 |
阿西噶阿西:"实际项目中,推荐半同步+组提交,兼顾性能和一致性。"
🚨 主从延迟的5种原因
南北绿豆:"主从延迟不只是网络问题,还有很多隐藏原因。"
原因1️⃣:从库配置低
主库:8核16G SSD
从库:2核4G HDD ← 配置太差
结果:主库1秒写入1万条,从库1秒只能执行5000条,越积越多
解决方案:从库配置不能比主库差太多,至少要达到主库的70%
原因2️⃣:主库写入过快
主库:秒杀活动,1秒写入10万条
从库:SQL线程是单线程,1秒只能执行5万条
结果:延迟越来越大
查看延迟:
-- 从库执行
SHOW SLAVE STATUS\G
Seconds_Behind_Master: 15 ← 延迟15秒!
解决方案:开启并行复制(MySQL 5.7+)
-- 从库开启并行复制(多个SQL线程)
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL slave_parallel_workers = 4; -- 4个SQL线程
原因3️⃣:大事务阻塞
主库执行大事务:
START TRANSACTION;
UPDATE order SET status = 1; -- 更新100万行,执行了30秒
COMMIT;
从库:
必须等这个大事务完整执行完(30秒),其他事务都要等待
解决方案:
- 避免大事务(拆成小批次)
- 业务高峰期不执行批量操作
-- ❌ 错误写法
UPDATE order SET status = 1 WHERE create_time < '2024-01-01'; -- 100万行
-- ✅ 正确写法(分批执行)
UPDATE order SET status = 1
WHERE create_time < '2024-01-01'
LIMIT 1000; -- 每次1000行,循环执行
原因4️⃣:锁等待
从库的SQL线程执行:
UPDATE account SET balance = 600 WHERE user_id = 10086;
但从库上有个慢查询正在执行:
SELECT * FROM account WHERE user_id = 10086 FOR UPDATE; -- 加了锁,还没释放
结果:SQL线程被阻塞,等待锁释放
排查方法:
-- 从库执行,查看锁等待
SELECT * FROM information_schema.INNODB_LOCKS;
SELECT * FROM information_schema.INNODB_LOCK_WAITS;
解决方案:
- 从库只做读操作,不要有写操作
- 慢查询加超时限制
原因5️⃣:网络抖动
主库到从库的网络延迟:
正常:5ms
抖动:500ms(丢包、重传)
结果:binlog传输变慢
排查方法:
# 测试主从网络延迟
ping 从库IP
# 测试网络带宽
iperf -c 从库IP
解决方案:
- 主从部署在同一机房
- 使用高速网络(万兆网卡)
- 跨地域用专线
延迟原因总结
| 原因 | 现象 | 解决方案 |
|---|---|---|
| 从库配置低 | 延迟持续增长 | 升级从库硬件 |
| 主库写入快 | 高峰期延迟大 | 开启并行复制 |
| 大事务 | 突然延迟几十秒 | 拆分大事务 |
| 锁等待 | 延迟不稳定 | 从库避免写操作 |
| 网络抖动 | 延迟波动大 | 主从同机房 |
💡 主从延迟的6种解决方案
哈吉米:"知道原因了,怎么解决主从数据不一致的问题?"
阿西噶阿西:"有6种方案,各有适用场景。"
方案1️⃣:强制走主库(最简单)
思路:写入后立即读取,强制查主库。
代码实现:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Transactional
public void createOrder(Order order) {
// 写入主库
orderMapper.insert(order);
}
/**
* 查询刚创建的订单(强制主库)
*/
@DataSource("master") // 强制路由到主库
public Order getOrderById(Long orderId) {
return orderMapper.selectById(orderId);
}
/**
* 查询历史订单列表(可以走从库)
*/
@DataSource("slave") // 路由到从库
public List<Order> listOrders(Long userId) {
return orderMapper.selectByUserId(userId);
}
}
自定义注解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
String value() default "slave"; // 默认从库
}
AOP切面:
@Aspect
@Component
public class DataSourceAspect {
@Around("@annotation(dataSource)")
public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
// 设置数据源
DynamicDataSourceHolder.setDataSource(dataSource.value());
try {
return point.proceed();
} finally {
// 清除数据源
DynamicDataSourceHolder.clearDataSource();
}
}
}
优点:
- ✅ 100%数据一致
- ✅ 实现简单
缺点:
- ❌ 主库压力大
- ❌ 读写分离效果打折扣
适用场景:核心业务(订单、支付)
方案2️⃣:延迟补偿(简单粗暴)
思路:写入后等待一小段时间,再查询从库。
代码实现:
@Service
public class OrderService {
@Transactional
public Order createOrderAndQuery(Order order) {
// 1. 写入主库
orderMapper.insert(order);
// 2. 等待100ms(等主从同步)
Thread.sleep(100);
// 3. 查询从库
return orderMapper.selectById(order.getId());
}
}
优点:
- ✅ 实现超简单
- ✅ 大部分场景有效
缺点:
- ❌ 不够优雅
- ❌ 延迟不确定(100ms可能不够)
- ❌ 白白浪费100ms
适用场景:临时方案,不推荐生产使用
方案3️⃣:二次读取(推荐)
思路:先查从库,查不到再查主库。
代码实现:
@Service
public class OrderService {
/**
* 智能查询(先从库,查不到再主库)
*/
public Order getOrderById(Long orderId) {
// 1. 先查从库
Order order = orderMapper.selectByIdFromSlave(orderId);
// 2. 从库查不到,再查主库
if (order == null) {
order = orderMapper.selectByIdFromMaster(orderId);
}
return order;
}
}
优化版(缓存标记):
@Service
public class OrderService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Transactional
public void createOrder(Order order) {
// 1. 写入主库
orderMapper.insert(order);
// 2. 在Redis中标记"刚写入"(1秒过期)
String key = "just_created:order:" + order.getId();
redisTemplate.opsForValue().set(key, "1", 1, TimeUnit.SECONDS);
}
public Order getOrderById(Long orderId) {
String key = "just_created:order:" + orderId;
// 1. 检查Redis标记
if (redisTemplate.hasKey(key)) {
// 刚写入的,强制走主库
return orderMapper.selectByIdFromMaster(orderId);
}
// 2. 否则走从库
return orderMapper.selectByIdFromSlave(orderId);
}
}
优点:
- ✅ 大部分请求走从库(性能好)
- ✅ 数据一致性好
- ✅ 自适应(自动判断)
缺点:
- ⚠️ 需要Redis
- ⚠️ 代码稍复杂
适用场景:⭐⭐⭐⭐⭐ 推荐!
方案4️⃣:中间件路由(ShardingSphere)
思路:用数据库中间件自动路由。
ShardingSphere配置:
# application.yml
spring:
shardingsphere:
datasource:
names: master,slave1,slave2
master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.10:3306/db
username: root
password: 123456
slave1:
jdbc-url: jdbc:mysql://192.168.1.11:3306/db
slave2:
jdbc-url: jdbc:mysql://192.168.1.12:3306/db
rules:
readwrite-splitting:
data-sources:
myds:
type: Static
props:
write-data-source-name: master
read-data-source-names: slave1,slave2
load-balancer-name: round_robin
load-balancers:
round_robin:
type: ROUND_ROBIN # 轮询
代码:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
// 写操作自动路由到master
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order);
}
// 读操作自动路由到slave
public Order getOrderById(Long orderId) {
return orderMapper.selectById(orderId);
}
// 强制主库(在同一个事务中)
@Transactional
public Order getOrderByIdFromMaster(Long orderId) {
return orderMapper.selectById(orderId); // 事务中自动走主库
}
}
优点:
- ✅ 代码无侵入
- ✅ 配置化管理
- ✅ 自动负载均衡
缺点:
- ⚠️ 引入中间件,架构复杂
- ⚠️ 学习成本高
适用场景:大型项目,统一管理读写分离
方案5️⃣:分布式ID(避免依赖自增ID)
问题场景:
// 主库插入数据,返回自增ID
orderMapper.insert(order);
Long orderId = order.getId(); // 自增ID = 100001
// 立即查询从库
Order result = orderMapper.selectById(orderId); // 查不到(主从延迟)
解决方案:用分布式ID,不依赖自增ID
@Service
public class OrderService {
@Autowired
private SnowflakeIdGenerator idGenerator;
@Transactional
public void createOrder(Order order) {
// 1. 提前生成ID(不依赖数据库自增)
Long orderId = idGenerator.nextId();
order.setId(orderId);
// 2. 插入数据
orderMapper.insert(order);
// 3. 立即返回ID给前端
return orderId;
}
// 前端可以直接用这个ID查询(不依赖主从同步)
public Order getOrderById(Long orderId) {
// 可以放心走从库,因为ID是提前生成的
return orderMapper.selectById(orderId);
}
}
雪花算法实现:
@Component
public class SnowflakeIdGenerator {
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & 4095;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - 1288834974657L) << 22)
| (datacenterId << 17)
| (workerId << 12)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}
优点:
- ✅ 彻底解决依赖自增ID的问题
- ✅ 性能好(本地生成,不依赖数据库)
缺点:
- ⚠️ 需要引入分布式ID方案
方案6️⃣:最终一致性(接受短暂不一致)
思路:业务上容忍短时间内的数据不一致。
场景:
用户发帖:
1. 写入主库
2. 立即查询"我的帖子列表"(从库)
3. 可能看不到刚发的帖子(1-2秒后才能看到)
方案:前端提示"发布成功,审核中..."
实际:不是审核,是主从延迟,但用户不知道
代码:
@Service
public class PostService {
@Transactional
public Result createPost(Post post) {
// 写入主库
postMapper.insert(post);
// 返回"审核中"(其实是等主从同步)
return Result.success("发布成功,审核中...");
}
// 列表查询走从库(容忍短暂看不到)
public List<Post> listMyPosts(Long userId) {
return postMapper.selectByUserId(userId);
}
}
优点:
- ✅ 性能最好(读全走从库)
- ✅ 架构简单
缺点:
- ❌ 用户体验稍差
- ❌ 不适合强一致性业务
适用场景:社交、内容类产品(帖子、评论、点赞)
6种方案对比
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 强制主库 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ | 核心业务(支付、订单) |
| 延迟补偿 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐ | 临时方案 |
| 二次读取 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ 推荐 |
| 中间件路由 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | 大型项目 |
| 分布式ID | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 配合其他方案 |
| 最终一致性 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | 非核心业务 |
🎯 读写分离架构设计
南北绿豆:"最后,来看看完整的读写分离架构。"
架构1️⃣:一主一从(最简单)
【应用服务器】
↓
写 ↙ ↘ 读
↓ ↓
MySQL主库 ← MySQL从库
(写) 同步 (读)
优点:架构简单
缺点:从库单点,挂了就没法读
架构2️⃣:一主多从(常用)
【应用服务器】
↓
写 ↙ ↘ 读(负载均衡)
↓ ↙ ↘
MySQL主库 ← MySQL从库1
(写) 同步↘ (读)
↘
MySQL从库2
(读)
优点:读能力水平扩展
缺点:写能力受限于主库
架构3️⃣:双主(互为主从)
MySQL-A ←→ MySQL-B
(主) 双向 (主)
同步
优点:写能力翻倍
缺点:容易冲突(两边同时写同一行)
架构4️⃣:级联复制(减轻主库压力)
MySQL主库
↓ 同步
MySQL从库1(中继)
↙ ↘ 同步
从库2 从库3
(读) (读)
优点:主库只需同步给1个从库,减轻网络压力
缺点:延迟增加(二级从库延迟更大)
🎓 面试高频题
题目1:主从复制的原理是什么?
答案:
主从复制依赖binlog,有3个关键线程:
- Dump线程(主库):读取binlog,发送给从库
- IO线程(从库):接收binlog,写入relay log
- SQL线程(从库):读取relay log,执行SQL
流程:主库写binlog → Dump线程发送 → 从库IO线程接收 → 写入relay log → SQL线程执行 → 从库数据更新
题目2:binlog有哪几种格式?区别是什么?
答案:
| 格式 | 记录内容 | 优点 | 缺点 |
|---|---|---|---|
| STATEMENT | 原始SQL | binlog小 | 某些函数(NOW、UUID)会导致主从不一致 |
| ROW | 每行数据的变化 | 一致性强 | binlog大,批量操作慢 |
| MIXED | 自动选择 | 平衡 | 判断可能不准 |
推荐ROW格式(MySQL 8.0默认)
题目3:如何解决主从延迟问题?
答案:
原因:网络传输、从库配置低、大事务、锁等待
解决方案:
- 强制走主库(核心业务)
- 二次读取(先从库,查不到再主库)
- 半同步复制(等从库确认)
- 并行复制(多个SQL线程)
- 分布式ID(不依赖自增ID)
- 最终一致性(容忍短暂不一致)
题目4:读写分离后,如何保证数据一致性?
答案:
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 强制主库 | 写入后立即读取,走主库 | 订单、支付 |
| 二次读取 | 从库查不到,再查主库 | 推荐方案 |
| Redis标记 | 刚写入的数据标记"走主库" | 高并发场景 |
| 最终一致性 | 容忍1-2秒延迟 | 帖子、评论 |
题目5:异步复制、半同步复制、同步复制的区别?
答案:
| 模式 | 主库等待从库确认? | 性能 | 数据安全 |
|---|---|---|---|
| 异步复制 | 不等 | 高 | 可能丢数据 |
| 半同步复制 | 等至少1个从库接收binlog | 中 | 更安全 |
| 全同步复制 | 等所有从库执行完成 | 低 | 最安全(MySQL不支持) |
🎉 结束语
晚上10点,三人终于把主从复制和读写分离讲透了。
哈吉米:"原来主从延迟是必然的,异步复制性能好但有延迟,半同步复制更安全但性能差一些。"
南北绿豆:"对!核心业务用强制主库或二次读取,非核心业务用最终一致性。"
阿西噶阿西:"还有,binlog格式推荐用ROW,虽然体积大但数据一致性强。"
哈吉米:"我明天就改成二次读取+Redis标记的方案!"
南北绿豆:"对,这个方案最实用。下周咱们聊聊分库分表?"
哈吉米:"好!我想知道单表多大需要分表!"
主从复制记忆口诀:
binlog记录主库变化,三个线程协同工作
Dump发送IO接收,SQL线程执行更新
ROW格式保一致,异步复制性能好
半同步更安全,主从延迟要优化
强制主库保一致,二次读取最实用
希望这篇文章能帮你彻底搞懂MySQL主从复制和读写分离!记住:性能和一致性是权衡,根据业务场景选择合适的方案!
收藏+点赞,面试不慌张!💪