数据库读写分离怎么做?从主从复制到应用层路由的完整配置
读写分离本身不产生数据同步,它只是把写操作发给主库、读操作发给从库。
真正让从库有数据可读的是主从复制。
所以做读写分离前,必须先确认主从复制已经正常工作,否则读请求会读到旧数据甚至空结果。
下面以 MySQL 8.0 和 Spring Boot 项目为例,从环境准备、主从复制检查、应用层路由配置到延迟避坑,给出可以照着做的步骤。
如果你还没搭主从复制,需要先完成主库开启 binlog、从库配置复制账号并启动复制。
先确认主从复制状态
在从库执行 SHOW SLAVE STATUS\G(MySQL 8.0.22 起也可用 SHOW REPLICA STATUS\G),重点看两个字段:
Slave_IO_Running: Yes和Slave_SQL_Running: Yes,两者都必须是 Yes。Seconds_Behind_Master表示从库落后主库的秒数,0 或接近 0 才适合承载实时性要求高的读请求。
如果复制线程不是 Yes,先解决主从复制问题,不要急着配读写分离。
准备两个数据源
在 Spring Boot 项目里,读写分离通常用动态数据源实现。
准备主库和从库的连接信息,例如:
spring:
datasource:
master:
url: jdbc:mysql://192.168.1.10:3306/appdb?useSSL=false&serverTimezone=Asia/Shanghai
username: app_rw
password: 主库密码
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://192.168.1.11:3306/appdb?useSSL=false&serverTimezone=Asia/Shanghai
username: app_ro
password: 从库密码
driver-class-name: com.mysql.cj.jdbc.Driver
主库账号需要写权限,从库账号建议只给 SELECT 权限,降低误写从库的风险。
配置动态数据源与路由
核心是继承 AbstractRoutingDataSource,用 ThreadLocal 保存当前线程要用的数据源 key。
默认走主库,只有明确标记读的方法才切到从库。
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContext.get();
}
}
public class DataSourceContext {
private static final ThreadLocal KEY = new ThreadLocal<>();
public static void set(String key) { KEY.set(key); }
public static String get() { return KEY.get(); }
public static void clear() { KEY.remove(); }
}
再通过配置类把 master 和 slave 两个数据源注册进去,默认目标数据源设为 master。
然后用 AOP 在方法上标记读操作:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {}
@Aspect
@Component
public class ReadOnlyAspect {
@Around("@annotation(readOnly)")
public Object around(ProceedingJoinPoint pjp, ReadOnly readOnly) throws Throwable {
DataSourceContext.set("slave");
try {
return pjp.proceed();
} finally {
DataSourceContext.clear();
}
}
}
在查询方法上加上 @ReadOnly 即可。
注意必须在 finally 里清理 ThreadLocal,否则线程池复用时会串数据源。
主从延迟怎么处理
读写分离最常见的坑是“写完立刻读不到”。
比如用户下单后马上跳转到订单详情,写操作在主库,读请求到了从库,从库还没同步完,就会显示旧数据。
处理方式按场景选择:
- 对实时性要求高的读,比如支付结果、订单详情,强制走主库,不标
@ReadOnly。 - 对实时性要求不高的列表、报表,走从库。
- 如果业务必须读从库又要保证一致性,可以在写后短暂等待,或使用半同步复制降低延迟,但半同步复制会增加写响应时间,需要评估。
另外,事务内不要随意切换数据源。
同一个事务里先写主库再读从库,读到的可能不是本事务未提交的数据。
建议事务内统一走主库。
验证读写是否真的分离
配置完成后,用两种方式验证:
- 在从库执行
SHOW PROCESSLIST,观察应用连接是否打到从库。 - 在代码里临时打印当前数据源 key,确认读方法走 slave、写方法走 master。
更直接的办法是在主库和从库分别插入一条只有主库有的测试数据,然后调用读接口,看能否读到。
如果读不到,检查 AOP 是否生效、数据源 key 是否拼写正确、从库复制是否正常。
读写分离能分担读压力,但它不解决单表过大和写入瓶颈。
如果写请求已经是瓶颈,需要从分库分表或业务拆分入手,而不是继续加从库。