读写分离不是把从库地址配置到应用里就结束了。它背后的前提是数据库主从复制,核心则是路由策略与一致性保障。主库负责接收INSERT、UPDATE、DELETE等写操作,并将变更通过binlog同步给一个或多个从库;从库只承担SELECT这类只读查询。业务侧或中间件根据SQL类型自动选择目标库,这样写入负载不会被大量查询进一步放大,查询吞吐也可以借助从库横向扩展。要让这套架构真正稳定落地,业务接入方式、连接管理、事务策略和主从延迟处理都需要提前设计。

一、读写分离的核心原理与适用场景
先看主从复制的工作链路。以MySQL为例,主库上的写事务提交后,会按照提交顺序生成binlog事件。从库的I/O线程不断向主库请求增量binlog,并把内容写入本地的relay log;随后从库的SQL线程读取relay log并重放这些变更。由于网络传输、从库重放以及大事务拆分都会消耗时间,从库数据通常会落后主库一小段时间,这个时间差就是常说的主从延迟。读写分离正是建立在这样一套异步复制架构上,读请求发往从库,写请求发往主库。
并不是所有业务都适合一刀切地做读写分离。读多写少的场景收益最大,例如订单列表、商品详情、报表查询、用户中心的基础信息展示等。这类业务的特点是一次写入会被几十次甚至上百次读取,把读取压力转移到从库后,主库可以集中资源处理写入、更新和事务一致性要求更高的操作。反过来,如果业务写入非常频繁,或者读操作与写操作之间的实时性要求极高,比如扣减库存后立即查询剩余库存,那么读写分离带来的延迟问题可能比性能提升更棘手。
判断是否接入读写分离,可以先从慢查询和资源占用角度观察。如果主库CPU、连接数长期处于高位,但写入量并不大,大量SELECT语句占据了资源,就说明读请求已经对写操作造成了干扰。此时引入从库并把高频查询路由过去,通常能快速缓解压力。但要注意,事务内的一致性读、写后立即读、跨库关联查询等场景不能简单路由到从库,否则可能读到旧数据或导致业务逻辑错误。
二、业务接入架构设计:直连多数据源与代理层
业务接入读写分离主要有两条路线。第一条是应用内维护多数据源,由代码在运行时根据规则动态切换。Spring生态里可以基于AbstractRoutingDataSource实现动态数据源,再配合AOP和自定义注解标记哪些方法走从库、哪些方法走主库。这种方案架构简单,不需要额外部署中间件,适合数据源数量不多、团队对框架比较熟悉的项目。缺点也明显:切换逻辑分散在各个服务里,配置变更通常需要重新发布,后期如果从库扩容或主从拓扑变化,维护成本会上升。
public class DbContextHolder {
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
public static void set(String dbType) {
CONTEXT.set(dbType);
}
public static String get() {
return CONTEXT.get();
}
public static void clear() {
CONTEXT.remove();
}
}
第二条路线是引入数据库中间件,业务应用只连接一个虚拟地址,中间件统一解析SQL并将读写请求分流到主库或从库。ShardingSphere、MyCat是常见的开源方案。以ShardingSphere-JDBC为例,它作为客户端组件嵌入应用内,通过配置文件定义主从数据源和读写分离规则;而ShardingSphere-Proxy则以独立进程形式部署,应用只需修改数据库连接地址即可接入。代理层方案对业务代码几乎无侵入,SQL兼容性和路由策略集中在中间件统一维护,适合服务数量较多、希望集中管控读写分离规则的场景。
两条路线不是非此即彼。早期业务量不大时,可以先使用应用内多数据源快速落地;当接入服务增多、从库节点变多、需要统一监控和切换策略时,再迁移到ShardingSphere-Proxy这类中间件架构。迁移过程中要注意保留数据源命名规范,例如主库统一叫master,从库统一叫slave或slave-1、slave-2,避免各服务自行定义造成混淆。同时要明确哪些接口属于只读接口,哪些接口涉及写事务,这是后续配置路由规则的基础。
三、读写分离落地中的关键配置
使用ShardingSphere-JDBC时,读写分离规则可以通过YAML方式配置。下面是一个典型主从配置示例,主库用于写入,从库用于读取,读库之间采用轮询负载均衡。配置项中需要明确数据源名称、驱动、JDBC URL以及读写分离规则,这样ShardingSphere才能在运行时根据SQL类型自动路由。
spring:
shardingsphere:
datasource:
names: master,slave
master:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.10.11:3306/order_db
username: root
password: root
slave:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.10.12:3306/order_db
username: root
password: root
rules:
readwrite-splitting:
data-sources:
order_rw:
type: Static
props:
write-data-source-name: master
read-data-source-names: slave
load-balancer-name: round_robin
load-balancers:
round_robin:
type: ROUND_ROBIN
如果团队选用应用内动态数据源方案,则可以通过自定义注解和切面来控制路由。例如编写一个@ReadOnly注解,标注在只读查询方法上,切面在执行前把数据源切换为从库,方法执行结束后清理线程上下文。这样业务代码只需要在Service层方法上添加注解,无需到处操作数据源切换。但要注意,这种方案默认把所有不带注解的方法都走主库,避免漏配导致读请求打到主库或写请求误走从库。
@Aspect
@Component
public class ReadOnlyAspect {
@Before("@annotation(readOnly)")
public void setReadDataSource(ReadOnly readOnly) {
DbContextHolder.set("slave");
}
@After("@annotation(readOnly)")
public void clearDataSource(ReadOnly readOnly) {
DbContextHolder.clear();
}
}
事务内强制走主库是另一个关键配置。由于主从存在延迟,事务开启后如果读操作被路由到从库,可能读到尚未同步的旧数据,导致业务判断错误。对于写后读的接口,要么在事务内显式指定主库,要么使用Hint标记让中间件把当前事务的所有SQL都路由到主库。ShardingSphere支持HintManager方式,可以在代码中强制指定数据源,但使用后必须及时关闭,避免线程复用造成数据源污染。
try (HintManager hintManager = HintManager.getInstance()) {
hintManager.setWriteRouteOnly();
// 事务内的读操作会强制走主库
Order order = orderMapper.selectById(orderId);
}
四、主从复制延迟与一致性保障策略
主从延迟是读写分离架构中最常见的坑。延迟来源包括大事务提交时间过长、从库硬件配置低于主库、从库同时承担过多离线查询、网络抖动以及binlog重放速度跟不上等。通常可以通过SHOW SLAVE STATUS查看Seconds_Behind_Master字段大致判断延迟秒数,但该字段在部分场景下会漂移,更可靠的方式是比较主从库的GTID集合或binlog位置差。监控系统应当持续采集这些指标,当延迟超过业务可接受阈值时触发告警。
解决写后读一致性问题,不能只靠调大从库配置。最直接有效的方式是让需要强一致性的读操作强制走主库,例如用户刚提交订单后立即跳转的订单详情页,可以走主库查询。对于可以容忍短暂延迟的列表页、统计页,则继续走从库。也可以结合缓存标记,在写入成功后把相关资源标记为需要走主库,一段时间后再切换回从库。另一种折中方案是采用半同步复制,主库等待至少一个从库确认收到binlog后再返回写入成功,这会增加写入延迟,但能显著缩小主从数据差距。
从库故障摘除也需要提前设计。如果从库宕机或延迟过高,中间件或动态数据源应能自动把读流量摘除,避免请求堆积继续拖慢业务。ShardingSphere等中间件支持从库健康检查,当节点不可用时会将其标记为不可路由,后续请求自动转向其他从库或主库。对于应用内多数据源方案,则需要自己实现心跳检测和重试机制。无论哪种方案,都应避免在单个从库出现问题时直接把所有读压力倒回主库,导致主库瞬时过载,最好设置限流和降级策略,宁可暂时降低查询吞吐,也要保证核心写入链路稳定。
读写分离的核心不在于增加几个从库,而在于把读写路由、延迟处理和故障切换做成一套可观测、可调整的机制。业务接入时优先明确哪些接口可以读从库,哪些必须读主库;数据源切换逻辑要统一封装,避免散落在业务代码里;主从延迟指标要纳入监控,必要时通过强制走主库、半同步复制或缓存标记保证一致性。只有把这些架构细节设计清楚,读写分离才能真正减轻主库压力,而不是给线上埋下新的不确定性。