多数据源配置,配起来不难,难的是配完之后不出事。我见过太多项目,读写分离上线后从库延迟导致数据不一致,多租户切换失效串了数据,事务回滚只回滚了一个库。今天不讲怎么配,只讲那些让你深夜加班、线上翻车的坑。
坑一:事务中切换数据源,切了个寂寞
这是最经典的坑。你在Service方法上加了@DataSource("slave"),又加了@Transactional,结果查询还是走了主库。原因很简单:Spring事务在方法开始时就已经绑定了连接,事务内的数据源切换不会生效。
解决方案:把@DataSource放在@Transactional外层。如果必须在一个事务内切库,用REQUIRES_NEW开新事务,但要注意这会导致两个独立事务,无法一起回滚。更好的做法是:读写分离的从库查询不要加事务,纯查询用从库,写操作走主库。
坑二:AOP切面顺序,注解被“吃掉”了
你写了@DataSource注解和对应的AOP切面,但发现切面没执行。大概率是切面顺序问题。如果事务切面先执行,数据源切面后执行,切换就失效了。Spring事务默认的Order是Ordered.LOWEST_PRECEDENCE,你的切面如果没指定顺序,可能排在事务后面。
解决方案:给数据源切面加@Order(Ordered.HIGHEST_PRECEDENCE),确保它在事务切面之前执行。或者干脆把数据源切换放在事务注解外层,用编程式事务控制。
坑三:连接池各自为政,数据库被打爆
每个数据源都有独立的连接池,默认配置往往偏大。主库配了50个连接,从库也配了50个,三个数据源就是150个连接。数据库最大连接数可能才200,稍微一压就满了。
解决方案:给每个数据源单独配置HikariCP参数,根据实际流量调整maximum-pool-size。读写分离场景下,从库连接池可以稍大,主库要保守。同时开启连接池监控,观察活跃连接数,别等数据库报警了才想起来调。
坑四:MyBatis-Plus的@DS注解,不生效的N种原因
用dynamic-datasource-spring-boot-starter时,@DS("slave")不生效,常见原因有:注解加在Controller上而不是Service上;方法内部调用(this调用)导致AOP失效;类上加了@DS但方法上又加了@Transactional,事务优先级更高。
解决方案:@DS尽量加在Service方法上,避免内部调用。如果必须内部调用,注入自身Bean或使用AopContext。事务和@DS同时存在时,确保@DS在事务外层。
坑五:多数据源事务,回滚只回了一个库
默认的DataSourceTransactionManager只能管理一个数据源。你在主库插入订单,从库更新库存,主库回滚了,从库却没回滚。这不是bug,是设计如此。
解决方案:如果业务必须强一致,引入JTA或Seata,但复杂度极高。更实际的做法是:尽量避免跨库事务,用最终一致性替代。比如订单和库存通过消息队列异步同步,保证最终一致。
坑六:ThreadLocal没清理,线程池串数据
DataSourceContextHolder用ThreadLocal存数据源key,如果请求结束后没调用clear(),线程被复用时会带着上一个请求的数据源标识,导致数据串库。这个问题在Tomcat线程池下尤其隐蔽。
解决方案:在AOP的finally块中一定要调用DataSourceContextHolder.clear(),确保每次请求后清理干净。
坑七:默认数据源没设置,启动就报错
AbstractRoutingDataSource必须设置defaultTargetDataSource,否则当key找不到对应数据源时会抛异常。很多人只设置了targetDataSources,忘了默认值,结果某个方法没加注解就挂了。
解决方案:在配置动态数据源时,务必调用setDefaultTargetDataSource(masterDataSource)。
总结
多数据源配置,配是小事,稳是大事。事务边界、AOP顺序、连接池、ThreadLocal清理、跨库事务,每一个坑都可能让你线上翻车。记住一条原则:能单库解决就别拆,真要拆,先把事务和切换边界想清楚。代码写之前多问一句“这个操作在哪个库、有没有事务、切了之后会不会失效”,能省下无数个深夜排查的时间。