@transactional遇上@synchronized的生产问题
近日遇到一个问题,就是一个订单被两个用户抢了问题,排查后发现是由于 @transactional和@synchronized注解的使用问题

一、问题点:数据重复读
@transactional注解用于开启事务,当在高并发情况下我们可能为了保证数据的安全使用悲观锁,可以在方法上使用@synchronized使用悲观锁。一个线程执行完方法并释放锁后,事务并未提交,第二个线程又获得了该锁,导致数据出问题
@transactional注解通过aop实现事务管理,当标注该注解的方法执行完成后才提交事务,而synchronized代码块又是在一个事务内,就会出现第一个线程释放锁后但是事务还没提交,第二个线程就进入同步代码块获取到未提交的数据库数
@transactional控制事务的范围比sychronized 大,如图:

思路:既然事务下不能使用锁,那我们把锁和事务进行分开。使得在锁环境下包含事务,最终依然是线程安全的
- 方法一:将锁替换成数据库的锁比如select for update或者版本号version
- 方法二:在service下将事务代码的抽取单独使用,无事务方法调用有事务的方法
既然问题出在事务未提交,那么只要把对应事务操作的代码单独抽取出来,封装成一个单独的方法,在synchronized中调用该方法即可

@service
public class orderserviceimpl implements orderservicei {
@autowired
private orderdao orderdao;
public sychronized order updateorder(int id) { //加锁
return updateordersafely(id); //调用数据库
}
@transactional
public order updateordersafely(int id) {
return orderdao.updateorder(id);
}
}看起来好像是解决了事务未提交的问题,但会存在新的问题,可能会出现@transactional事务不生效的情况
二、问题点 >》方法二引申:@transactional事务不生效
在同一个类内部调用@transactional标注的方法事务也不会开启,原因是:
@transactional事务管理是基于动态代理对象的代理逻辑实现的,那么如果在类内部调用类内部的事务方法,这个调用事务方法的过程并不是通过代理对象来调用的,而是直接通过this对象来调用方法,绕过的代理对象,肯定就是没有代理逻辑了
方法:将锁提取到controller层,不包含任何事务
- controller类
@restcontroller
public class testcontroller {
@resource
private testservice testservice;
@postmapping("/test")
public synchronized void testinterface() {
testservice.functiona(); // 调用数据库操作方法
}
}- service类
@service("testservice")
public class testserviceimpl implements testservice {
@transactional(rollbackfor = exception.class,
propagation = propagation.requires_new)
public void functiona() {
//数据库读写操作
}
}注意点:
1、调用本类加@transactional的方法时,要使用一个aop对象来进行代理,获取到代理对象之后@trasactional才会正常生效,否则是不生效的。
2、synchronized要比事务的粒度要大,否则还是会出现重复读的现象。
3、被调用的functiona为什么要使用 requires_new的隔离级别呢? 因为这样就可以实现锁的粒度大于事务的粒度啦。无论如何,都创建新的事务,外层事务不受内层事务影响。但是有个问题,外层事务失败了,内层事务还是把记录入库了,有可能产生脏数据;
下面这段代码有什么问题呢?
lock lock = new reentrantlock();
@transactional
public void save(){
try{
lock.lock();
//业务代码……
}finally{
lock.unlock();
}
}1、显而易见就可以看出,spring 会在方法开始时开启一个事务,并在方法结束时提交或回滚该事务。如果在方法内部手动加锁,可能会导致锁的持有时间超过事务的生命周期,这样可能会引发死锁或其他并发问题。
2、lock.lock()会阻塞线程,直至获取到锁为止,具体来说,如果锁已经被其他线程持有,调用 lock.lock() 的线程会被阻塞,直到持有锁的线程释放锁
还有值得注意的一点,很多人看到@transactional和try……在一起使用,就以为@transactional会失效,其实并不是
①try-catch结构:
- 如果在try-catch块中捕获异常而不重新抛出,可能导致事务不按预期回滚。
- 解决方法:在@transactional注解中指定rollbackfor属性,或者在catch块中重新抛出异常
②try-finally结构:
- 使用try-finally结构来确保资源释放(如锁的解锁)不会影响事务的正常行为,只要异常最终被抛出。
- 而上述代码用的是try-finally结构来确保锁的释放,这通常不会影响事务的正常行为。只要在业务代码中抛出的异常没有被捕获,事务就会按照预期回滚
因此应该修改成:
private final reentrantlock lock = new reentrantlock();
@transactional(rollbackfor = exception.class)
public void save() {
if (!lock.trylock()) {
// 如果未能获取锁,可以记录日志或采取其他措施
system.out.println("failed to acquire the lock.");
return; // 或者抛出异常
}
try {
// 业务代码……
} catch (exception e) {
// 处理异常
throw e;
} finally {
lock.unlock();
}
}
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论