1. 一次线上接口超时,把我带到了路由源码面前
1.1 故障现场
先说一个我亲历的故障。有一天下午,运维同事突然在群里丢了一张监控截图,某个核心查询接口的P99耗时从平时的80ms直接飙到了1.8秒,而且是一阵一阵的,不是持续高延迟。一开始我以为是慢SQL,拿着日志去数据库管理平台看了半天,慢查询记录里啥都没有,数据库CPU也不高,连接数曲线倒是有点意思——主库连接数在超时时间段里几乎打满,从库却闲得很。
当时就觉得很奇怪:这个接口标注的是走只读数据源,为什么压力全落在主库上?顺着这个疑问往下查,才意识到问题根本不在SQL层面,而在MyBatisPlus多数据源的路由和连接管理机制上。也正是这次排查,让我下定决心把MyBatisPlus多数据源的源码完整过了一遍。
1.2 初步排查:从现象到嫌疑点
我先看了接口代码,标准的业务代码,方法上标注了@DS("slave"),理论上应该走从库。于是我在本地复现,开启数据源切换日志,发现了一个有意思的细节:虽然方法上的@DS生效了,但同一个请求链路里,后面的Mapper调用又切回了主数据源。
这里就牵扯出一个关键认知:MyBatisPlus的动态数据源是基于AOP+ThreadLocal实现的,它只对加了@DS注解的方法做线程上下文切换,一旦方法执行完,上下文数据源key就会出栈恢复。如果方法内部调用了另一个没有@DS注解的服务方法,或者事务边界比数据源切换边界更大,连接就会回头去找主数据源。
这不是源码的bug,而是使用姿势的问题,但如果不读源码,你根本想不到一个简单的顺序调用会引发连接归属的漂移。后面我会把这条链路完整拆开。
1.3 为什么性能优化必须接触源码
很多同学接触MyBatisPlus多数据源,第一反应是看文档配置两个DataSource就完事了,优化也停留在“把连接池调大一点”。但真实场景里,性能和故障的根因往往在框架的底层行为里,比如:
- 数据源key从ThreadLocal的栈里存取,嵌套切换到底怎么恢复;
- 每次
getConnection()和AOP拦截器之间的调用顺序; - 批量操作
saveOrUpdateBatch为什么在多数据源下反而变慢; - 分页插件为什么会偶尔失效,
selectCount为什么查出来是0。
这些光靠配置文档无法解释,必须到源码里找答案。本文我就按这次排障和后续优化的实际路径,带你走一遍MyBatisPlus多数据源的核心源码,并给出可以直接落地的性能优化方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从@DS注解到数据库连接:拆开多数据源的核心路由链
2.1 DynamicRoutingDataSource:路由总开关
要理解MyBatisPlus的多数据源实现,第一个要看的类是com.baomidou.dynamic.datasource.DynamicRoutingDataSource。这个类继承了Spring的AbstractRoutingDataSource,但干的事比父类多得多。
关键方法就两个:determineCurrentLookupKey()和getConnection()。前者决定当前线程应该用哪个数据源key,后者负责真正返回连接。
java复制public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
private Map<String, DataSource> dataSourceMap;
@Override
protected Object determineCurrentLookupKey() {
return DynamicDataSourceContextHolder.peek();
}
@Override
public Connection getConnection() throws SQLException {
String dsKey = determineCurrentLookupKey();
DataSource dataSource = dataSourceMap.get(dsKey);
if (dataSource == null) {
dataSource = dataSourceMap.get(primary);
}
return dataSource.getConnection();
}
}
注意这里有个细节:determineCurrentLookupKey()返回的是DynamicDataSourceContextHolder.peek()取出的当前key。peek()拿到的是栈顶元素,如果栈为空,返回的是null。null去map里查,查不到就会回退到主数据源。这就是很多同学遇到的“明明我配置了从库,结果请求还是打到主库”的源码级原因之一——注解没被解析到,或者线程上下文里根本没有压入数据源key。
2.2 DynamicDataSourceContextHolder:ThreadLocal中的栈
`DynamicDataSourceC
