SPR-16876:AbstractRoutingDataSource中TransactionSynchronizationManager返回值异常
核心问题
在非JPA/Hibernate的Spring Boot主从库场景中,使用@Transactional(readOnly = true)标注的服务方法,在RoutingDataSource的determineCurrentLookupKey()方法中调用TransactionSynchronizationManager.isCurrentTransactionReadOnly()始终返回false,getCurrentTransactionName()返回null;但在服务方法内部调用这两个方法却能得到正确结果,导致只读事务始终路由到主库(读写实例)。
这是Spring Framework的已知时序问题(对应内部编号SPR-16876):当使用DataSourceTransactionManager(非JPA事务管理器)时,事务的只读属性与事务名称会延迟到获取数据库连接后才被设置到TransactionSynchronizationManager中,而RoutingDataSource需要在获取连接前确定数据源,因此无法拿到已初始化的事务上下文。
解决方案
方案1:升级Spring Framework版本
该问题已在Spring Framework 5.1及以上版本中被修复。升级后,DataSourceTransactionManager会在事务启动阶段就将只读属性同步到TransactionSynchronizationManager,此时RoutingDataSource的determineCurrentLookupKey()可以正确读取到isCurrentTransactionReadOnly()的返回值,无需额外修改代码。
方案2:自定义线程标记与AOP切面(兼容低版本Spring)
如果无法升级Spring版本,可通过自定义线程本地存储(ThreadLocal)结合AOP切面,提前标记只读事务状态:
- 定义只读事务标记工具类
public class ReadOnlyTransactionMarker { private static final ThreadLocal<Boolean> READ_ONLY_FLAG = new ThreadLocal<>(); public static void setReadOnly(boolean readOnly) { READ_ONLY_FLAG.set(readOnly); } public static boolean isReadOnly() { Boolean flag = READ_ONLY_FLAG.get(); return flag != null && flag; } public static void clear() { READ_ONLY_FLAG.remove(); } }
- 编写AOP切面拦截事务方法
在事务启动前设置只读标记,事务结束后清理:
@Aspect @Component public class ReadOnlyTransactionAspect { // 拦截所有标注@Transactional的方法 @Before("@annotation(org.springframework.transaction.annotation.Transactional) && @annotation(tx)") public void beforeTransactionExecution(JoinPoint joinPoint, Transactional tx) { ReadOnlyTransactionMarker.setReadOnly(tx.readOnly()); } @After("@annotation(org.springframework.transaction.annotation.Transactional)") public void afterTransactionExecution() { ReadOnlyTransactionMarker.clear(); } }
- 修改RoutingDataSource的路由逻辑
使用自定义标记替代TransactionSynchronizationManager的判断:
public class MasterSlaveRoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return ReadOnlyTransactionMarker.isReadOnly() ? "SLAVE" : "MASTER"; } }
方案3:自定义TransactionInterceptor(进阶)
通过扩展TransactionInterceptor,在事务初始化阶段提前将只读属性同步到线程上下文,确保RoutingDataSource能读取到:
public class PreSetReadOnlyTransactionInterceptor extends TransactionInterceptor { @Override protected Object invokeWithinTransaction(Method method, Class<?> targetClass, InvocationCallback invocation) throws Throwable { TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass); if (txAttr != null) { // 提前设置只读标记到TransactionSynchronizationManager TransactionSynchronizationManager.setCurrentTransactionReadOnly(txAttr.isReadOnly()); TransactionSynchronizationManager.setCurrentTransactionName(txAttr.getName()); } try { return super.invokeWithinTransaction(method, targetClass, invocation); } finally { // 清理上下文 TransactionSynchronizationManager.clearCurrentTransactionReadOnly(); TransactionSynchronizationManager.clearCurrentTransactionName(); } } }
随后在事务管理器配置中替换默认的TransactionInterceptor即可。
内容的提问来源于stack exchange,提问作者Harsh Kanakhara

