怎样强制查询走主库?——数据库读写分离场景下的主库路由全攻略
目录导读
- 为什么需要强制查询走主库? —— 理解读写分离的痛点
- 强制走主库的常见场景 —— 数据一致性是关键
- 六大强制路由主库的技术方案 —— 从应用层到中间层
- 注解/标签驱动
- ThreadLocal上下文标记
- 动态数据源切换
- SQL注释/Hint机制
- 读写分离中间件路由规则
- 数据库连接属性控制
- 实战示例:Spring Boot + AOP 强制主库查询
- FAQ:开发者最关心的5个问题
为什么需要强制查询走主库?
在典型的“一主多从”架构中,读写分离通过将更新操作(INSERT/UPDATE/DELETE)路由到主库,将只读查询(SELECT)分发到从库,来实现性能扩展,但“主从同步延迟”是一个经典陷阱:当业务刚写入主库,立即查询从库时,可能因数据尚未同步而读到旧数据(脏读),强制查询走主库是保证“写后读一致性”的唯一手段。

核心矛盾:从库的读性能 vs 主库的数据实时性。
强制走主库的常见场景
| 场景 | 典型业务示例 | 一致性要求 |
|---|---|---|
| 用户注册后立即登录 | 注册成功 → 查询用户信息 | 强一致 |
| 订单支付后状态查询 | 支付回调 → 查订单状态 | 最终一致+实时 |
| 评论发布后展示 | 发帖 → 查看自己的评论 | 读己之所写 |
| 秒杀库存扣减后校验 | 扣减 → 查剩余库存 | 不允许多卖 |
| 修改密码后登录 | 改密 → 验证新密码 | 必须主库 |
六大强制路由主库的技术方案
注解/标签驱动(推荐,代码侵入低)
在DAO方法或Service方法上使用自定义注解(如@Master),通过AOP拦截方法调用,将读操作强制路由到主库。
伪代码示例:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Master {}
// 在Service层使用
@Master
public User getUserAfterWrite(Long userId) {
return userMapper.selectById(userId);
}
原理:AOP切面在执行前设置DataSourceContextHolder.set("master"),执行后清除。
优点:精确控制、对业务代码侵入最小;缺点:需手工标注。
ThreadLocal上下文标记(框架级)
在“写操作完成”后,自动在当前线程设置一个“走主库”标记,后续的读操作默认走到主库,直到标记过期或清除。
典型实现(开源框架ShardingSphere的Hint机制):
// 强制路由主库
HintManager hintManager = HintManager.getInstance();
hintManager.setWriteRouteOnly();
try {
userService.getUserById(id);
} finally {
hintManager.close();
}
适用:写后紧跟读的“事务内一致性”场景。注意:需配合事务边界使用,且ThreadLocal易引发内存泄漏,务必finally清理。
动态数据源路由(AbstractRoutingDataSource)
Spring官方提供的AbstractRoutingDataSource,通过重写determineCurrentLookupKey()方法,根据上下文返回数据源标识。
关键配置:
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType(); // master/slave
}
}
强制走主库:只需在业务代码中将DbContextHolder设置为master,这个方案灵活,但需自行管理上下文。
SQL注释/Hint指令(数据库原生支持)
MySQL支持在SQL前添加/*FORCE_MASTER*/或/*master*/注释,配合中间件(如MyCat、Atlas)解析路由。
示例:
/*master*/ SELECT * FROM user WHERE id = 1001;
优点:无需代码修改,DBA可离线配置;缺点:依赖中间件支持,且注释书写容易遗漏,目前已较少用于新项目。
中间件读写分离规则(零代码侵入)
以Apache ShardingSphere为例,通过YAML配置“hint分片策略”,指定某些表或某些SQL强制走主库。
配置示例:
rules:
- !READWRITE_SPLITTING
dataSources:
ds_0:
writeDataSourceName: master
readDataSourceNames: [slave1, slave2]
loadBalancerName: round_robin
# 强制主库策略
- !HINT
writeOnly: true # 对所有带Hint的SQL生效
适用:希望DBA控制路由策略,开发人员零代码,但需增加运维复杂度,且Hint生效范围不易追溯。
数据库连接属性控制(仅限特殊场景)
某些连接池或JDBC驱动支持设置readOnly=false强制走主库,但这通常用于事务控制,而非合理路由方案,多数据源下无效。
实战示例:Spring Boot + AOP 强制主库查询
步骤:
- 定义注解
@Master - 定义AOP切面,在执行标注
@Master的方法前设置上下文 - 实现
AbstractRoutingDataSource,根据上下文返回数据源 - 在Service需要强一致性的查询上标注
@Master
关键代码片段(基于ShardingSphere的HintManager实现):
@Around("@annotation(master)")
public Object forceMaster(ProceedingJoinPoint pjp, Master master) throws Throwable {
HintManager hintManager = HintManager.getInstance();
try {
hintManager.setWriteRouteOnly(); // 强制走主库
return pjp.proceed();
} finally {
hintManager.close();
}
}
最佳实践:
- 将
@Master标注在Service层方法,而不是DAO层,因为业务语义更清晰。 - 配合
@Transactional(propagation = Propagation.REQUIRES_NEW),避免事务内多次路由冲突。 - 对强制主库的方法进行QPS监控,防止突发流量压垮主库。
FAQ:开发者最关心的5个问题
Q1:强制走主库会影响所有线程吗?
A:不会,无论是ThreadLocal还是HintManager,都只对当前线程生效,这意味着一个请求中开启强制主库,不会影响其他请求。
Q2:如果主库挂了,强制走主库的查询会怎样?
A:若主库不可用,业务将直接报错,建议在强制主库逻辑中加入熔断降级(如重试从库读一次),或配置主库连接池的健康检测。
Q3:能不能让“同一个事务内的所有读都走主库”?
A:可以,在事务开始时,在事务拦截器中设置DbContextHolder.setMaster();事务结束时清除,ShardingSphere的@Transactional 天然支持,事务内的写会优先使用主库。
Q4:强制走主库和“读从库+延迟判断”哪个更好?
A:延迟判断(如对比主从同步位置)虽精确,但实现复杂且增加网络开销,强制走主库更简单粗暴,适合大多数业务,只有对数据一致性要求极高且主库压力敏感的场合,才考虑延迟判断。
Q5:在MyBatis-Plus中如何实现?
A:MyBatis-Plus原生支持多数据源插件(dynamic-datasource-spring-boot-starter),在方法上加@DS("master")即可,原理与方案一相同,只是框架已封装。
选择最适合你的方案
- 小团队/快速迭代 → 注解+AOP(方案一)
- 大型分布式系统 → 中间件Hint策略(方案五)
- 已有ShardingSphere → 直接使用HintManager(方案二变种)
- 不想引入框架 → 动态数据源+ThreadLocal(方案三)
- 旧系统改造 → SQL注释(方案四,但慎用)
无论哪种方案,关键是明确业务边界:只在“写后立即读”、“强一致性要求”的少数路径上强制走主库,避免滥用导致主库成为瓶颈,对“最终一致”的查询,坚持走从库,是读写分离架构的黄金法则。