最近项目中遇到并发的问题,所以需要设置锁。由于并发量并不大,所以采用了悲观锁来锁住数据。但是按照网上设置好了悲观锁之后,运行时却报 no transaction is in progress,于是查找事务注解失效的原因,并总结几个事务注解会失效的情况。
背景
测试逻辑:
实体新增test字段,初始化为0,调用save函数时开始测试,接着调用filter函数。
filter函数用了事务注解,模拟100个线程高并发情况下,每个线程都用悲观锁获取同一个id的实体,让test字段+1。
dao层的findClientById用了悲观锁,在我们没有将其提交事务之前,其他线程是不能获取修改的,需要等待。
文章图片
期待结果:test字段为100。
client实体:
@JsonView(base.class)
private int test = 0;
service层:
public void save(List logs) throws ParseException {
this.filter(logs);
} @Transactional(rollbackFor = Exception.class)
void filter(List logs) throws ParseException {
for (int i = 0;
i < 100 ;
i++) {
new Thread(() -> {
Client client = clientRepository.findClientByIdWithPessimisticLock(9L).get();
client.setTest(client.getTest() + 1);
clientRepository.save(client);
}).start();
dao层:
/**
* 查询时加上悲观锁
* 在我们没有将其提交事务之前,其他线程是不能获取修改的,需要等待
* @param id clientId
* @return
*/
@Lock(value = https://www.it610.com/article/LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Client a where a.id = :id")
Optional findClientByIdWithPessimisticLock(Long id);
测试结果: no transaction is in progress
文章图片
于是我就搜索了相关的资料,按照资料
@Lock(value = https://www.it610.com/article/LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Client a where a.id = :id")
@Transational
Optional findClientByIdWithPessimisticLock(Long id);
做了一个错误的操作:在dao层加上了@Tansational事务注解
结果:虽然没有报错,但是test结果为8,不是预期的100,说明在高并发下悲观锁没有生效。
猜测:加上该事务注解后,仅仅是在一条select语句执行完,事务就会自动commit,这时候锁已经去除了,并不是期待的修改完数据后才去除锁。其他线程可以继续获取修改数据。
这种写法不对,我们需要探清filter函数注解失效的真正原因。
那么为什么事务注解会失效呢?我们先来看一下失效的几种原因。
@Transational事务注解失效原因
- 最常见的原因: 事务方法被内部类调用。
例如如下代码,在同一个类中调用事务方法的时候,事务注解就会失效。这也是我上面代码事务失效的原因之一。
@Service
public class ServiceImpl implements Service {public void update(Order order) {
updateOrder(order);
}@Transactional
public void updateOrder(Order order) {
}
}
原理: 在之前的文章说过,@Transational实现原理是Spring AOP。而Spring AOP 是通过动态代理的方式实现的。简单来说就是会生成一个代理类,事务方法会由这个 Spring 生成的代理对象来管理。
只有目标方法由外部调用,才能被 Spring 的事务拦截器拦截。在同一个类中的两个方法直接调用,不会被 Spring 的事务拦截器拦截。
为什么会这样子?以给出的代码为例,我们来看图:
文章图片
现在updateOrder方法已经被代理类管理。代理类把原方法包装了起来,原方法在原类中并没有被增强。
所以,当外部调用 update 方法(没有事务注解),代理类判断此方法不需进行事务拦截,直接调用原类。原类再调用 this.updateOrder,此时 this 指向的是原类,并不含有事务拦截逻辑(事务拦截逻辑在代理类中),因此注解失效。
但是,如果外部直接调用 updateOrder 方法,是会经过代理类拦截的,这时候事务注解生效。
文章图片
可以看出,这种情况下 @Transactional 注解失效的原因在于原类中的 this 并没有被增强。如果 this 能够指向外部的 Proxy 类,这个问题就不会发生了。
总结:不要在一个类中调用事务方法。可以换种写法,或者考虑把事务方法提到一个单独的类中,由外部调用事务方法。
2.事务方法开启一个新线程
这也是我代码事务失效的原因之一。
例如以下代码:
@Service
public class ServiceImpl implements Service {@Transactional
public void update(Order order) {
new Thread(() -> {
repository.update()
}).start();
}
}
原因:spring 的事务是通过LocalThread来保证线程安全的,事务和当前线程绑定, 开启新的线程会让事务失效。
3.异常没有被抛出或异常类型不对
@Transactional
public void update(Order order) {
try {
// update order
} catch (Exception e) {
e.printStackTrace();
}
}
如上,当异常被捕获后,并且没有再抛出,那么update是不会回滚的
@Transactional
public void update(Order order) {
try {
// update order
} catch {
throw new Exception("更新错误");
}
}
如上,之前文章也说过,这样事务也是不生效的,因为默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如:@Transactional(rollbackFor = Exception.class)
4.事务方法不是 public 的
5.没有被 Spring 管理,类没有使用@Service等注解
解决问题 既然不能在类内调用,已经事务方法内新开线程,那么可以采用外部调用的方法进行测试。
controller:
public void Save() {
for (int i = 0;
i < 100;
i++) {
new Thread(() -> {
Service.test();
}).start();
}
Service:
@Transactional(rollbackFor = Exception.class)
public void test() {
Client client = clientRepository.findClientByIdWithPessimisticLock(9L).get();
client.setTest(client.getTest() + 1);
clientRepository.save(client);
}
dao:
@Lock(value = https://www.it610.com/article/LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Client a where a.id = :id")
Optional findClientByIdWithPessimisticLock(Long id);
测试结果:test结果为100,符合预期,成功上锁。
文章图片
大概复制了几行的运行日志展示一下:
DEBUG 3270 --- [Thread-74] org.hibernate.SQL: select client0_.id as id1_0_,
DEBUG 3270 --- [Thread-65] org.hibernate.SQL: update client set delete_at=?,
DEBUG 3270 --- [Thread-75] org.hibernate.SQL: select client0_.id as id1_0_,
DEBUG 3270 --- [Thread-66] org.hibernate.SQL: update client set delete_at=?,
DEBUG 3270 --- [Thread-76] org.hibernate.SQL: select client0_.id as id1_0_,
DEBUG 3270 --- [Thread-67] org.hibernate.SQL: update client set delete_at=?,
DEBUG 3270 --- [Thread-77] org.hibernate.SQL: select client0_.id as id1_0_,
DEBUG 3270 --- [Thread-68] org.hibernate.SQL: update client set delete_at=?,
select语句代表查询实体,update代表更新。
可以看到,
select的线程是逐渐递增的:74,75,76,
update的线程也是逐渐递增的:65, 66, 67
【事务注解失效的问题】说明线程在排队等待操作,等待上一个线程的锁去除后,再执行更新操作。
推荐阅读
- SpringBoot|SpringBoot, SpringMVC系列知识的学习路线 以及 教程推荐
- SpringBoot|SpringBoot 入门(环境搭建与HelloWorld~)
- Vue|基于vue+srpingboot的学生成绩管理系统
- springboot|SpringBoot集成Minio搭建自己的分布式文件服务器(Minio集成篇)
- springboot|SpringBoot系统笔记
- Spring|SpringBoot2——SpringBoot入门、SpringBoot的自动配置、配置文件和开发小技巧
- SpringBoot|【SpringBoot 2】(六)配置文件 web开发相关
- SpringBoot|【SpringBoot 2】(五)自动配置简析源码 开发中小技巧
- SpringBoot|SpringBoot(二)——自动装配原理