Spring @Transactional踩坑记
@Transactional踩坑记
总述
Spring在1.2引入@Transactional
注解, 该注解的引入使得我们可以简单地通过在方法或者类上添加@Transactional
注解,实现事务控制。 然而看起来越是简单的东西,背后的实现可能存在很多默认规则和限制。而对于使用者如果只知道使用该注解,而不去考虑背后的限制,就可能事与愿违,到时候线上出了问题可能根本都找不出啥原因。
踩坑记
1. 多数据源
事务不生效
背景介绍
由于数据量比较大,项目的初始设计是分库分表的。于是在配置文件中就存在多个数据源配置。大致的配置类似下面:
<!-- 数据源A和事务配置 -->
<bean id="dataSourceA" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
....
</bean>
<bean id="dataSourceTxManagerA"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSourceA" />
</bean>
<!-- mybatis自动扫描生成Dao类的代码 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="annotationClass" value="com.rampage.mybatis.annotation.mapperDao" />
<property name="nameGenerator" ref="sourceANameGenerator" />
<property name="sqlSessionFactoryBeanName" value="sourceAsqlSessionFactory" />
<property name="basePackage" value="com.rampage" />
</bean>
<!-- 自定义的Dao名称生成器,prefix属性指定在bean名称前加上对应的前缀生成Dao -->
<bean id="sourceANameGenerator" class="com.rampage.mybatis.dao.namegenerator.MyNameGenerator">
<property name="prefix" value="sourceA" />
</bean>
<bean id="sourceAsqlSessionFactory" class="org.mybatis.spring.PathSqlSessionFactoryBean">
<property name="dataSource" ref="dataSourceA" />
</bean>
<!-- 数据B和事务配置 -->
<bean id="dataSourceB" class="com.mchange.v2.c3p0.ComboPooledDataSource"
destroy-method="close">
....
</bean>
<bean id="dataSourceTxManagerB"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSourceB" />
</bean>
但是在实际部署的时候,因为是单机部署的,多个数据源实际上对应的是同一个库,不存在分布式事务的问题。所以在代码编写的时候,直接通过在@Transactional
注解来实现事务。具体代码样例大致如下:
@Service
public class UserService {
@Resource("sourceBUserDao") // 其实这时候Dao对应的是sourceB
private UserDao userDao;
@Transactional
public void update(User user) {
userDao.update(user);
// Other db operations
...
}
}
这中写法的代码一直在线上运行了一两年,没有出过啥问题.....反而是我在做一个需求的时候,考虑到@Transactional
注解里面的 数据库操作,如果没有同时成功或者失败的话,数据会出现混乱的情况。于是自己测试了一下,开启了这段踩坑之旅.....
原因分析:
开始在网上搜了一下Transactional
注解不支持多数据源, 于是我当时把所有数据库操作都采用sourceB作为前缀的Dao进行操作。结果测试一遍发现还是没有事务效果。没有什么是源码解决不了的,于是就开始debug源码,发现最终启动的事务管理器竟然是dataSourceTxManagerA
。 难道和事务管理器声明的顺序有关?于是我调整了下xml配置文件中,事务管理器声明的顺序,发现事务生效了,因此得证。
具体来说原因有以下两点:
@Transactional
注解不支持多数据源的情况- 如果存在多个数据源且未指定具体的事务管理器,那么实际上启用的事务管理器是最先在配置文件中指定的(即先加载的)
解决办法:
对于多数据下的事务解决办法如下:
- 在
@Transactional
注解添加的方法内,数据库更新操作统一使用一个数据源下的Dao,不要出现多个数据源下的Dao的情况- 统一了方法内的数据源之后,可以通过
@Transactional(transactionManager = "dataSourceTxManagerB")
显示指定起作用的事务管理器,或者在xml中调节事务管理器的声明顺序
死循环问题
这个问题其实也是多数据源导致的,只是更难分析原因。具体场景是:假设我的货仓里有1000个货物,我现在要给用户发货。每批次只能发100个。我的货物有一个字段来标识是否已经发过了,对于已经发过的货不能重新发(否则只能哭晕在厕所)!代码的实现是外层有一个while(true)
循环去扫描是否还有未发过的货物,而发货作为整体的一个事务,具体代码如下:
@Transactional
public void deliverGoods(List<Goods> goodsList) { // 传入的参数是前面循环查出来的未发货的100个货物,作为一个批次统一发货
updateBatchId(goodsList, batchId); // 更新同批次货物的批次号字段
// do other things
updateGoodsStatusByBatchId(batchId, delivered); // 根据前面更新的批次号取修改数据库相关货物的发送状态为已发送
}
从整体上来看,这段代码逻辑上没有任何问题。实际运行的时候却发现出现了死循环。还好测试及时发现,没有最终上线。那么具体原因是咋样的呢?
出现这个问题的时候,配置文件的配置还是同前面一个问题一样的配置。即实际上@Transactional
注解默认起作用的事务是针对dataSourceA
的。然后跟进updateBatchId
方法,发现其最终调用的方法采用的Dao是sourceA
为前缀的Dao,而updateGoodsStatusByBatchId
方法最终调用的Dao是sourceB
为前缀的Dao。细细分析,我终于知道为啥了
最新文章
- HDU 5510---Bazinga(指针模拟)
- Provider:SSL提供程序,error:0 - 接收到的消息异常,或格式不正确
- MyEclipse破解(MEGen.java)
- PHP 的 HMAC_SHA1算法 实现
- 第三方登录(2)Android客户瑞上第三方登录百度教程
- hdu 3068(最长回文)
- 20150503-struts2入门-标签
- 採集和输出 DeckLink Studio 4K
- webview 上 postUrl 发送参数过程中数据丢失或错误 的问题
- 剑指offer-面试题13.在O(1)时间删除链表节点
- C#邮件收发
- vmware 解决 authentication token manipulation error
- 用 virtualenv 创建隔离的 Python 运行环境
- jQuery $.ajax传递数组的traditional参数传递必须true
- (PMP)第8章-----项目质量管理
- Codeforces.100633J.Ceizenpok&#39;s formula(扩展Lucas)
- tensorflow实战系列(一)
- NSString 拼接字符串
- Linux C ftruncate 函数清空文件注意事项(要使用 lseek 重置偏移量)
- CAN总线实际运用分析问题。