Spring在1.2引入@Transactional
注解, 该注解的引入使得我们可以简单地通过在方法或者类上添加@Transactional
注解,实现事务控制。 然而看起来越是简单的东西,背后的实现可能存在很多默认规则和限制。而对于使用者如果只知道使用该注解,而不去考虑背后的限制,就可能事与愿违,到时候线上出了问题可能根本都找不出啥原因。
由于数据量比较大,项目的初始设计是分库分表的。于是在配置文件中就存在多个数据源配置。大致的配置类似下面:
<!-- 数据源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。细细分析,我终于知道为啥了 ?
sourceA
的, 也就是updateBatchId
方法实际上作为一个事务,他是要在方法执行完成之后才提交的READ_COMMITTED
, 所以在updateGoodsStatusByBatchId
方法去更新的时候其实还读取不到对应批次号的记录,也就不会做更新
解决办法这里就不说了,最终还是同前面一个问题,或者更新的时候根据货物列表去更新。
内部调用不生效的问题其实大部分大家都知道。举一个简单的例子:
假设我有一个类的定义如下:
@Service
public class UserService {
public void updateUser(User user){
// do somothing
updateWithTransaction(user);
}
@Transactional
public void updateWithTransaction(User user) {
}
}
@Service
public class BusinessService {
@Autowired
private UserService userService;
public void doUserUpdate(User user) {
// do somothing
userService.updateUser(user);
}
}
这种情况下大家都知道事务最终是不会生效的。因为对于updateWithTransaction
方法是通过内部调用的,这时候@Transactional
注解压根就不会生效。但是有时候情况并不这么明显,考虑下面的代码:
@Service
public class UserService extends AbstractUserService {
public void updateUser(User user){
// do somothing
updateWithTransaction(user);
}
}
public abstract class AbstractUserService {
protected abstract void updateUser(User user);
@Transactional
public void updateWithTransaction(User user) {
// do update
}
}
@Service
public class BusinessService {
public void doUserUpdate(User user) {
// do somothing
AbstractUserService userService = getUserService(); // 假设最终得到是UserService类的实例
userService.updateUser(user);
}
}
这段代码初一分析,最终调用的updateUser
方法是UserService
的方法, 然后调用的updateWithTransaction
是属于AbstractUserService
类的。 好像是调用的不是同一个类的方法,按道理事务应该是可以生效的。其实并没有..... 原因其实还是是内部调用。其实这种场景我也是在项目中发现的(坑太多),当时的代码比这个复杂的多,Abstract类包含了一堆可以被子类重写的方法。原来的代码大致如下:
public class AbstractService {
// 被外部调用的方法
public void outMethod() {
if (A) {
transactionalMethod1();
} else if (B) {
transactionalMethod2();
} else {
transactionalMethod3();
}
}
@Transactional
public void transactionalMethod1() {
// do something
}
@Transactional
public void transactionalMethod2() {
// do something
}
@Transactional
public void transactionalMethod3() {
// do something
}
}
其中三个事务方法都可能被子类重写,修改必须兼容老代码。思考了兼容和接口改造的方式,我最终实现如下:
public class AbstractService implements TransactionIntf {
@Autowired
private TransactionService transactionService;
public void outMethod() {
transactionService.setTransactionIntf(this); // 最终数据库服务类注册为当前实现类
transactionService.processIntransaction(); // 调用数据库操作
}
@Override
public void transactionalMethod1() {
// do something
}
@Override
public void transactionalMethod2() {
// do something
}
@Override
public void transactionalMethod3() {
// do something
}
}
public interface TransactionIntf {
void transactionalMethod1();
void transactionalMethod2();
void transactionalMethod3();
}
@Service
public class TransactionService {
// 定义局部线程变量,存储对应的服务
ThreadLocal<TransactionIntf> serviceLocal = new ThreadLocal<TransactionIntf>();
@Transactional // 在此处加注解
public void processIntransaction() {
try {
if (A) {
serviceLocal.get().transactionalMethod1();
} else if (B) {
serviceLocal.get().transactionalMethod2();
} else {
serviceLocal.get().transactionalMethod3();
}
} finally {
// 最终在本地线程局部变量移除
serviceLocal.remove();
}
}
// 设置服务到本地线程局部变量
public void setTransactionIntf(TransactionIntf service) {
this.serviceLocal.set(service);
}
}
下面直接给出网站上关于@Transactional
使用的注意点:
另外附上验证是否是否开启的工具类源码(我只是搬运工):
class TransactionTestUtils {
private static final boolean transactionDebugging = true;
private static final boolean verboseTransactionDebugging = true;
public static void showTransactionStatus(String message) {
System.out.println(((transactionActive()) ? "[+] " : "[-] ") + message);
}
// Some guidance from: http://java.dzone.com/articles/monitoring-declarative-transac?page=0,1
public static boolean transactionActive() {
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
Class tsmClass = contextClassLoader.loadClass("org.springframework.transaction.support.TransactionSynchronizationManager");
Boolean isActive = (Boolean) tsmClass.getMethod("isActualTransactionActive", null).invoke(null, null);
return isActive;
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
// If we got here it means there was an exception
throw new IllegalStateException("ServerUtils.transactionActive was unable to complete properly");
}
public static void transactionRequired(String message) {
// Are we debugging transactions?
if (!transactionDebugging) {
// No, just return
return;
}
// Are we doing verbose transaction debugging?
if (verboseTransactionDebugging) {
// Yes, show the status before we get to the possibility of throwing an exception
showTransactionStatus(message);
}
// Is there a transaction active?
if (!transactionActive()) {
// No, throw an exception
throw new IllegalStateException("Transaction required but not active [" + message + "]");
}
}
}
http://blog.timmattison.com/archives/2012/04/19/tips-for-debugging-springs-transactional-annotation/