跳到主要内容

读写分离

问题

什么是读写分离?如何实现?主从延迟导致的数据不一致如何处理?

答案

读写分离原理

将写操作路由到主库,读操作路由到从库,分散数据库压力:

实现方式

方式说明优势劣势
代码层实现应用代码判断读写灵活、无额外依赖侵入业务代码
中间件代理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: 读写分离有哪些实现方式?

答案

  1. 代码层:自定义数据源路由 + AOP 注解(如 @ReadOnly
  2. ORM 框架:Spring 的 AbstractRoutingDataSource,MyBatis 的 Plugin
  3. 中间件代理:ProxySQL、MaxScale、MyCat
  4. 云服务:RDS 读写分离代理

推荐:中小项目用 ORM 框架层面实现,大型项目用中间件代理(对应用透明)。

Q2: 如何处理写后立即读的一致性问题?

答案

  1. 关键读走主库:写后短时间内(如 5-30 秒)强制读主库
  2. GTID 等待:写操作后返回 GTID,读操作前等待从库同步
  3. 缓存写入:写入数据库同时写入缓存,读先查缓存
  4. 业务设计:写入后跳转到确认页面(增加延迟时间)

Q3: 所有读操作都走从库吗?

答案

不是,以下读操作必须走主库:

  • SELECT ... FOR UPDATE:加锁读,必须走主库
  • 写后立即读的场景
  • 事务中的读操作:一个事务内的读写应在同一个库
  • 对实时性要求极高的查询(如余额查询)

相关链接