一、原因分析
Spring
中通过在方法上添加注解 @Transactional
可以很好的处理事务问题。Spring
对此的处理原理是对 加了 @Transactional
注解的方法 添加 AOP切面来时先事务管理的。
而 synchronized
最大范围也就是方法级别的。 事务和synchronized
关系如下所示
由上图可以看出,当线程1 释放了锁,还未提交事务之前,线程2 已经获取锁并提前提交了事务,从而导致了并发的问题。
二、解决方法
1、方法一 增强事务隔离级别
可以把事务的隔离级别设置为 SERIALIZABLE
不允许事务并发执行,而必须串行化执行,最安全,不可能出现更新、脏读、不可重复读、幻读,但是效率最低。
@Transactional(isolation = Isolation.SERIALIZABLE)
public synchronized void update(Long id, Long seq){
Test1Entity entity = test1Mapper.selectById(id);
test1Mapper.updateById(new Test1Entity(id, seq + entity.getSeq()));
}
2、方法二 提升加锁位置
如果业务逻辑非常简单,对高并发行要求不高的话,可以把 锁操作添加到 控制层,如下所示:
// Controller
@PutMapping("/update/{id}/{seq}")
public Test1Entity update(@PathVariable("id") Long id,
@PathVariable("seq") Long seq){
// 添加锁操作。
synchronized (Class.class){ // 此处为了示例,要根据业务合理加锁
testService.update(id, seq);
}
return new Test1Entity(id, seq);
}
// Service
@Service
public class TestServiceImpl {
@Resource
private Test1Mapper test1Mapper;
@Transactional
public void update(Long id, Long seq){
Test1Entity entity = test1Mapper.selectById(id);
test1Mapper.updateById(new Test1Entity(id, seq + entity.getSeq()));
}
}
3、方法三 抽离事务代码
方法一和方法二的效率都比较低,另一种方式可以把,可以把需要 把需要并发控制的业务,单独抽离出来,进行事务控制操作。如下所示:
public void complex(Long id, Long seq){
// 其他业务处理
lock.lock();
try {
service.update(id, seq);
} finally {
lock.unlock();
}
// 其他业务处理
}
@Transactional
public void update(Long id, Long seq){
Test1Entity entity = test1Mapper.selectById(id);
test1Mapper.updateById(new Test1Entity(id, seq + entity.getSeq()));
}
4、方法四 手动开启事务
方法三中可能会增加一个类的编写,也可以在同一个方法中通过手动开启事务的方式实现。如下所示:
@Service
public class TestServiceImpl {
@Resource
private Test1Mapper test1Mapper;
private static final Lock lock = new ReentrantLock();
@Resource
private DataSourceTransactionManager transactionManager;
@Resource
TransactionDefinition transactionDefinition;
public void update(Long id, Long seq){
// 其他业务处理
lock.lock();
// 开启事务
TransactionStatus transaction = transactionManager.getTransaction(transactionDefinition);;
try {
Test1Entity entity = test1Mapper.selectById(id);
test1Mapper.updateById(new Test1Entity(id, seq + entity.getSeq()));
// 提交事务
transactionManager.commit(transaction);
}catch (Exception e){
// 回滚事务
transactionManager.rollback(transaction);
}finally {
lock.unlock();
}
// 其他业务处理
}
}
这个手动开启事务,需要每个方法都需要实现,这个也是比较繁琐,这种方式可以抽象出一个公共类,统一来实现事务的处理。 可以自己脑补