Spring 事务常见问题
Spring事务不生效原因
Spring 事务传播类型
- REQUIRED如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
- SUPPORTS当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
- MANDATORY当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
- REQUIRES_NEW创建一个新事务,如果存在当前事务,则挂起该事务。
- NOT_SUPPORTED始终以非事务方式执行,如果当前存在事务,则挂起当前事务
- NEVER不使用事务,如果当前事务存在,则抛出异常
- NESTED如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)
例子
- REQUIRED 例子
@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
A(a1);
testB();
}
@Transactional(propagation = Propagation.REQUIRED)
public void testB(){
B(b1);
throw Exception; //发生异常抛出
B(b2);
}
数据库没有插入新的数据,testMain上声明了事务,在执行testB方法时就加入了testMain的事务(当前存在事务,则加入这个事务),在执行testB方法抛出异常后事务会发生回滚,又testMain和testB使用的同一个事务,所以事务回滚后testMain和testB中的操作都会回滚,也就使得数据库仍然保持初始状态
public void testMain(){
A(a1);
testB();
}
@Transactional(propagation = Propagation.REQUIRED)
public void testB(){
B(b1);
throw Exception; //发生异常抛出
B(b2);
}
数据a1存储成功,数据b1和b2没有存储。由于testMain没有声明事务,testB有声明事务且传播行为是REQUIRED,所以在执行testB时会自己新建一个事务(如果当前没有事务,则自己新建一个事务),testB抛出异常则只有testB中的操作发生了回滚,也就是b1的存储会发生回滚,但a1数据不会回滚,所以最终a1数据存储成功,b1和b2数据没有存储
- SUPPORTS
public void testMain(){
A(a1);
testB();
}
@Transactional(propagation = Propagation.SUPPORTS)
public void testB(){
B(b1);
throw Exception; //发生异常抛出
B(b2);
}
这种情况下,执行testMain的最终结果就是,a1,b1存入数据库,b2没有存入数据库。由于testMain没有声明事务,且testB的事务传播行为是SUPPORTS,所以执行testB时就是没有事务的(如果当前没有事务,就以非事务方法执行),则在testB抛出异常时也不会发生回滚,所以最终结果就是a1和b1存储成功,b2没有存储。
- REQUIRES_NEW
@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
A(a1);
testB();
throw Exception; //发生异常抛出
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void testB(){
B(b1);
B(b2);
}
这种情形的执行结果就是a1没有存储,而b1和b2存储成功,因为testB的事务传播设置为REQUIRES_NEW,所以在执行testB时会开启一个新的事务,testMain中发生的异常时在testMain所开启的事务中,所以这个异常不会影响testB的事务提交,testMain中的事务会发生回滚,所以最终a1就没有存储,而b1和b2就存储成功了。
- NOT_SUPPORTED
@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
A(a1);
testB();
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void testB(){
B(b1);
throw Exception; //发生异常抛出
B(b2);
}
该场景的执行结果就是a1和b2没有存储,而b1存储成功。testMain有事务,而testB不使用事务,所以执行中testB的存储b1成功,然后抛出异常,此时testMain检测到异常事务发生回滚,但是由于testB不在事务中,所以只有testMain的存储a1发生了回滚,最终只有b1存储成功,而a1和b1都没有存储
- NESTED
@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
A(a1);
testB();
throw Exception; //发生异常抛出
}
@Transactional(propagation = Propagation.NESTED)
public void testB(){
B(b1);
B(b2);
}
该场景下,所有数据都不会存入数据库,因为在testMain发生异常时,父事务回滚则子事务也跟着回滚了
@Transactional(propagation = Propagation.REQUIRED)
public void testMain(){
A(a1);
try{
testB();
}catch(Exception e){
}
A(a2);
}
@Transactional(propagation = Propagation.NESTED)
public void testB(){
B(b1);
throw Exception; //发生异常抛出
B(b2);
}
这种场景下,结果是a1,a2存储成功,b1和b2存储失败,因为调用方catch了被调方的异常,所以只有子事务回滚了。
@Transactional
事务注解原理
我们知道,@Transactional
的工作机制是基于 AOP
实现的,AOP
又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
如果一个类或者一个类中的 public 方法上被标注@Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor
类中的 invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
常见问题
1 大事务问题
点击查看代码
@Service
public class UserService {
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
query1();
query2();
query3();
roleService.save(userModel);
update(userModel);
}
}
@Service
public class RoleService {
@Autowired
private RoleService roleService;
@Transactional
public void save(UserModel userModel) throws Exception {
query4();
query5();
query6();
saveData(userModel);
}
}
点击查看代码
roleService.save(userModel);
update(userModel);
saveData(userModel);
解决办法手动声明事务
@Autowired
private TransactionTemplate transactionTemplate;
public void save(final User user) {
queryData1();
queryData2();
transactionTemplate.execute((status) => {
addData1();
updateData2();
return Boolean.TRUE;
})
}
- 少用@Transactional注解
- 将查询(select)方法放到事务外
- 事务中避免远程调用
- 事务中避免一次性处理太多数据
- 非事务执行
- 异步处理
2 Spring事务 与 redisson 的RedLock 锁失效问题
最开始使用redisson加锁,写在事务里面,高并发的时候抢券,导致券数量发超了,后来根据查看日志,排查发现是由于redisson解锁之后,事务还没有完全提交,此时另外一个线程又获得了锁,导致券数量判断异常
开启事务--》上锁--》执行业务--》解锁--》提交事务
Synchronized 失效关键原因:是因为Synchronized锁定的是当前调用方法对象,而Spring AOP 处理事务会进行生成一个代理对象,并在代理对象执行方法前的事务开启,方法执行完的事务提交,所以说,事务的开启和提交并不是在 Synchronized 锁定的范围内
在第一个线程解锁时候,还没提交事务。第二个线程已经开启事务,上锁,这时候读取的数据不是最新的,造成业务出错。并且mysql默认重复读,所以出现上面的问题。
HelloController
--HelloService
事务开始
加锁
// 业务代码
解锁
事务结束
把redisson加锁解锁移动到事务外面。
HelloController
--LockService
加锁
--HelloService
事务开始
// 业务代码
事务结束
解锁