相关文章:
今天组内一哥们反应说项目中读写分离出现了 BUG:明明加了读库的注解,而且日志也是显示应该要读从库,但是居然还是从主库中读取的数据。于是排查了一下原因,原因就是因为在数据源切换之前使用了声明式事务。
首先介绍一下这个项目中读写分离的实现方式:基于注解(AOP) + AbstractRoutingDataSource
;这是个很常见的使用方式,但是我个人不太推荐这个方式,因为基于注解可设置范围过大,如果不理解其中原理或者项目有特殊要求的时候很容易出现问题,个人比较推荐基于 MyBatis 拦截器做。这个项目在 Service 层和 DAO 层中间增加了一个 Manager 层,一般 Manager 层就是用来进行事务操作。
代码简化如下:
//adminService 方法
@Transactional(rollbackFor = Throwable.class)
public int updateDimensionRegion() {
int num = adminManager.countDimensionRegion();
//便于测试,主库和从库数据不一致
if (num == 2){
log.info("read slave.....");
}else {
log.info("read master....");
}
return num;
}
//adminManager 方法
@ReadDB //从库注解
public int countDimensionRegion() {
return dataMigrateMapper.countAdmin();
}
发现一直查询的是主库,但是方法上明明加了读从库的注解。
分析这个问题之前首先可以大概想一下整个程序运行的流程,来了一个请求,当在需要与数据库交互的时候根据基于 AbstractRoutingDataSource
实现的路由规则获取相应的数据源,然后与数据库进行交互。与数据库交互一个非常重要的类就是 Connection
,很明显,一般项目都不会使用 MySQL 全局事务,我们的事务都是 Connection
级别的,即 SqlSession
级别。而上面代码的 updateDimensionRegion
方法由于使用了 @Transactional
注解,那么很明显,在这个方法执行前需要开启事务,那么是谁来开启事务呢,是 Connection
,而 Connection
从哪里来呢,从 DataSource
中来,具体来自哪个 DataSource
是由项目中实现的路由规则来选择。
基于上面的分析,首先可以在 adminService
调用 adminManager
的 countDimensionRegion
方法之前打上断点,确定开启事务的 Connection
来自于哪个数据源。
发现存储数据源标识的 ThreadLocal
中的 value 表明当前数据源是主库。
接下来再在 @ReadDB
注解的 countDimensionRegion
方法中打上断点,再看:发现 value 的确表明现在要走从库了。但是为什么最终的结果还是显示走的是主库呢。其实这个原因在之前的一篇文章(分析 @Trannsactional 和 SqlSession 的关系)中已经进行了比较详细的分析。那篇文章中有一个测试结果:加了 @Transactional 注解的方法中就只有一次连接,共用 一个 SqlSession;不加事务,多次连接,生成多个 SqlSession。在这个例子中就是:事务方法已经获取到了
Connection
,后面的方法虽然配置的是要走从库,但是并不会生成新的链接,仍然沿用事务方法中已经获取到的 Connection
,所以就出现了“读写分离失效”了的现象(详细分析可参看分析 @Trannsactional 和 SqlSession 的关系
)。
最关键的是这个方法:org.mybatis.spring.SqlSessionUtils#getSqlSession
:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
SqlSession session = sessionHolder(executorType, holder);
if (session != null) {
return session;
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Creating a new SqlSession");
}
session = sessionFactory.openSession(executorType);
registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
return session;
}
可以发现,当可以从 TransactionSynchronizationManager
中获取到 SqlSession
的时候,就会直接返回不再获取新的 SqlSession
了。
到这里,其实读写分离失效的原因已经找到了,但是还有后续的疑问,就是为什么一开始获取的数据源就是主库呢,在这个项目中是因为存储数据源标识的 ThreadLocal
中的 value 已经初始化了一个默认值:
也就是说默认就走的是主库。即使这里没有设置默认值,其实 AbstractRoutingDataSource
也是提供了默认数据源配置的:
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
欢迎关注公众号
来源:CSDN
作者:Dongguabai
链接:https://blog.csdn.net/Dongguabai/article/details/104557247