Spring 事务常见问题

Spring事务不生效原因

Spring 事务传播类型

  1. REQUIRED如果当前没有事务,则自己新建一个事务,如果当前存在事务,则加入这个事务
  2. SUPPORTS当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
  3. MANDATORY当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
  4. REQUIRES_NEW创建一个新事务,如果存在当前事务,则挂起该事务。
  5. NOT_SUPPORTED始终以非事务方式执行,如果当前存在事务,则挂起当前事务
  6. NEVER不使用事务,如果当前事务存在,则抛出异常
  7. 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);
现在的这种写法,会导致所有的query方法也被包含在同一个事务当中。 如果query方法非常多,调用层级很深,而且有部分查询方法比较耗时的话,会造成整个事务非常耗时,而从造成大事务问题。

解决办法手动声明事务

@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
                   事务开始
                         // 业务代码
                   事务结束
            解锁
posted @ 2022-03-31 22:34  aaaak  阅读(213)  评论(0编辑  收藏  举报