读写分离
问题
什么是读写分离?如何实现?主从延迟导致的数据不一致如何处理?
答案
读写分离原理
将写操作路由到主库,读操作路由到从库,分散数据库压力:
实现方式
| 方式 | 说明 | 优势 | 劣势 |
|---|---|---|---|
| 代码层实现 | 应用代码判断读写 | 灵活、无额外依赖 | 侵入业务代码 |
| 中间件代理 | ProxySQL/MyCat | 对应用透明 | 额外运维 |
| ORM 框架 | Spring AbstractRoutingDataSource | 框架级支持 | 绑定框架 |
代码层实现(Spring)
// 自定义数据源路由
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContext.get(); // 返回 "master" 或 "slave"
}
}
// 注解标记读写
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {}
// AOP 拦截
@Aspect
public class DataSourceAspect {
@Around("@annotation(ReadOnly)")
public Object around(ProceedingJoinPoint point) throws Throwable {
DataSourceContext.set("slave");
try {
return point.proceed();
} finally {
DataSourceContext.clear();
}
}
}
中间件代理(ProxySQL)
-- ProxySQL 配置读写分离规则
INSERT INTO mysql_query_rules (rule_id, active, match_pattern, destination_hostgroup)
VALUES
(1, 1, '^SELECT.*FOR UPDATE', 0), -- SELECT FOR UPDATE → 主库
(2, 1, '^SELECT', 1), -- 普通 SELECT → 从库
(3, 1, '.*', 0); -- 其他 → 主库
主从延迟导致的问题
场景:用户注册后立即登录,注册写主库,登录读从库,但从库还没同步到 → 登录失败。
延迟问题解决方案
1. 写后读强制走主库
// 写操作后设置标记,短时间内的读操作走主库
@Transactional
public void register(User user) {
userMapper.insert(user);
// 设置标记,30 秒内读操作走主库
DataSourceContext.forceReadMaster(30);
}
2. 判断主从延迟后路由
-- 从库延迟超过阈值时,自动读主库
SHOW SLAVE STATUS → Seconds_Behind_Master
-- 延迟 > 1s → 走主库
-- 延迟 ≤ 1s → 走从库
3. 等待 GTID 同步
-- MySQL 5.7+ 等待从库同步到指定 GTID
SELECT WAIT_FOR_EXECUTED_GTID_SET('uuid:txn_id', 1);
-- 等待最多 1 秒,如果从库已同步到该 GTID 则返回 0
4. 业务层面容忍
部分场景可以接受短暂的不一致:
- 评论发布后,其他用户晚几秒看到是可接受的
- 统计数据延迟几秒影响不大
从库负载均衡
| 策略 | 说明 |
|---|---|
| 轮询(Round Robin) | 均匀分配 |
| 权重(Weighted) | 按从库性能分配 |
| 最少连接(Least Connections) | 发给最空闲的从库 |
常见面试问题
Q1: 读写分离有哪些实现方式?
答案:
- 代码层:自定义数据源路由 + AOP 注解(如
@ReadOnly) - ORM 框架:Spring 的
AbstractRoutingDataSource,MyBatis 的 Plugin - 中间件代理:ProxySQL、MaxScale、MyCat
- 云服务:RDS 读写分离代理
推荐:中小项目用 ORM 框架层面实现,大型项目用中间件代理(对应用透明)。
Q2: 如何处理写后立即读的一致性问题?
答案:
- 关键读走主库:写后短时间内(如 5-30 秒)强制读主库
- GTID 等待:写操作后返回 GTID,读操作前等待从库同步
- 缓存写入:写入数据库同时写入缓存,读先查缓存
- 业务设计:写入后跳转到确认页面(增加延迟时间)
Q3: 所有读操作都走从库吗?
答案:
不是,以下读操作必须走主库:
SELECT ... FOR UPDATE:加锁读,必须走主库- 写后立即读的场景
- 事务中的读操作:一个事务内的读写应在同一个库
- 对实时性要求极高的查询(如余额查询)