怎样强制查询走主库?

wen IT资讯 243

怎样强制查询走主库?——数据库读写分离场景下的主库路由全攻略

目录导读

  1. 为什么需要强制查询走主库? —— 理解读写分离的痛点
  2. 强制走主库的常见场景 —— 数据一致性是关键
  3. 六大强制路由主库的技术方案 —— 从应用层到中间层
    • 注解/标签驱动
    • ThreadLocal上下文标记
    • 动态数据源切换
    • SQL注释/Hint机制
    • 读写分离中间件路由规则
    • 数据库连接属性控制
  4. 实战示例:Spring Boot + AOP 强制主库查询
  5. 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 强制主库查询

步骤

  1. 定义注解@Master
  2. 定义AOP切面,在执行标注@Master的方法前设置上下文
  3. 实现AbstractRoutingDataSource,根据上下文返回数据源
  4. 在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注释(方案四,但慎用)

无论哪种方案,关键是明确业务边界:只在“写后立即读”、“强一致性要求”的少数路径上强制走主库,避免滥用导致主库成为瓶颈,对“最终一致”的查询,坚持走从库,是读写分离架构的黄金法则。

抱歉,评论功能暂时关闭!