大家好,我是杰哥
事务篇,又回来啦!第三篇,与大家共同分享一下项目组中曾经遇到过的一个真实问题案例,来再次深层次的了解一下 Spring 事务
一、开篇
初始情况下,我们的表格中只有一条id为2的数据,姓名为wangjie,年龄设置为0岁
1 不添加synchronized关键字
首先,大家先来看看这个程序有没有什么问题
@Transactional
public void transactionalMethod(){
User user = userDao.findOne(2);
user.setAge(user.getAge()+1);
userDao.updateUser(user);
}
复制代码
说明
transactionalMethod()方法,首先通过 findOne()
方法获取到id为2的用户记录,然后重新为该条用户记录的age加1,再通过 updateUser()
方法,更新到t_user表中
即,每次方法调用之后,实现的效果就是为id为2的用户加一岁
这是一个更新操作,想想,这个方法存在什么问题呢?
对,因为没有加锁,所以在并发的情况下,很可能出现线程不安全
的情况,导致执行结果与预期不一定一致的情况
我们来测试验证一下:
@GetMapping("/transactionalMethod")
public void transactionalMethod() {
final CountDownLatch latch = new CountDownLatch(1000);
try {
for (int i = 0; i < latch.getCount() ; i++) {
new Thread(() -> {
userService.transactionalMethod();
latch.countDown();
}).start();
}
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
latch.countDown();
}
}
复制代码
说明
在 controller 类中,创建 transactionalMethod
方法。方法中开启 1000
个线程,来模拟 1000
个并发请求
预期:年龄被更新为1000岁
启动之后,我们访问 /transactionalMethod 接口,结果如下:
结果1
即,wangjie
用户的年龄被更新为了 93
岁,与预期时不一致的
再来看看执行日志
发现出现多条一样的数据,验证了在执行过程中的确发生了并发问题,导致多个线程同时获取到相同的值,执行得到脏数据
那么,接下来,我们为方法 transactionalMethod
加锁,即加上 synchronized 关键字:
2 添加 synchronized 关键字
@Transactional
public synchronized void transactionalWithSynchronized(){
User user = userDao.findOne(2);
user.setAge(user.getAge()+1);
userDao.updateUser(user);
复制代码
说明
在原方法的基础上添加 synchronized 关键字
上层调用中,改为调用 transactionalWithSynchronized
方法
@GetMapping("/transactionalMethod")
public void transactionalMethod() {
final CountDownLatch latch = new CountDownLatch(1000);
try {
for (int i = 0; i < latch.getCount() ; i++) {
new Thread(() -> {
userService.transactionalWithSynchronized();
latch.countDown();
}).start();
}
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
latch.countDown();
}
}
复制代码
依旧是 1000
个线程的并发执行,结果如下:
结果2
咦?结果为 817
,依旧不是 1000
?这又是为何呢?
我们可以看到sql执行过程中,存在重复的 age
,说明产生了并发现象
这不科学呀,明明加了 synchronized
关键字,居然还出现了并发冲突?这岂不是属于一个很奇怪的 bug ? 当时一个同事说他起初都有点怀疑人生了
那怎么破?遇到了问题,就得想办法解决呀,不符合常理的 bug ,很多人往往会通过与其他正常使用场景进行对比来寻找答案,这种方法虽然没有太高的技术含量,但个人认为是效率比较高的一种方式。先找出不同的地方在哪里,再考虑问题原因,然后入手解决就行
于是我们也是先跟其他正常使用 synchronized
的方法进行对比,发现唯一不同的一点就是:transactionalWithSynchronized 方法上有 @Transactional 注解
那我们去掉 @Transactional 注解试试
3 添加 synchronized(去掉 @Transactional 注解)
public synchronized void transactionalWithSynchronized(){
User user = userDao.findOne(2);
user.setAge(user.getAge()+1);
userDao.updateUser(user);
}
复制代码
结果3
这样,就正常了,就符合常理啦。对于当前这个场景的话呢,我们的问题就算是解决啦!
也就是说,问题现象是,同时加上 @Transactional 注解和 synchronized 关键字,并发问题依旧存在
但是,你肯定会想,这究竟是为什么呢?
二、原理解析
1 原因分析
Spring 声明式事务,其实是采用 Spring
的 AOP
思想,在目标方法执行之前开启事务,在目标方法执行之后提交或者回滚事务
由于 Spring
的 AOP
事务机制,添加了 @Transactional
注解的方法的事务是由 spring
生成的一个代理类来处理的,当一个线程执行完该方法并释放锁后,代理类还并没有提交事务。也就是说,线程在进入 synchronized
之前会开启事务,然后再使用 synchronized
为方法加锁
我们来分析一下,带有添加 @Transactional
注解 的 synchronized
方法的请求过程
那么,对于图中的线程A来说,它执行完代码还未提交事务时,在并发请求的情况下,很容易出现线程 B 也过来请求。那么这个时候就会出现 线程 A 和线程 B 在同一个事务中的情况,也就发生了 mysql 重复读的问题
2 解决方案
问题的原因找到了,那么,我们应该如何解决这个问题呢?
上面说到,针对当前这个问题,我们只需要去掉我们可以在 方法上添加的 @Transactional
注解即可,因为对于当前这个方法来说,它去掉事务之后,效果也是一样的
但是,当然不能取巧,我们需要考虑一种通用的解决方案,来解决一定需要添加事务,并且需要控制并发的场景
问题的原因在于,线程在提交事务之前,便释放了锁,导致其他线程与自己处在同一事务的情况
那么,我们只需要在 transactionalWithSynchronized()
方法之前,即调用该方法的方法上加上 synchronized
关键字。也就是说,在还没有开事务之间就加锁,那么就可以保证线程同步
比如,在controller 类中的 testTransactionalWithSynchronized()
方法上添加 synchronized
关键字
private synchronized void testTransactionalWithSynchronized() {
userService.transactionalWithSynchronized();
}
@GetMapping("/testTransactionalWithSynchronized")
public void invokeMethod() {
final CountDownLatch latch = new CountDownLatch(1000);
try {
for (int i = 0; i < latch.getCount() ; i++) {
new Thread(() -> {
testTransactionalWithSynchronized();
latch.countDown();
}).start();
}
}catch (Exception e){
System.out.println(e.getMessage());
}finally {
latch.countDown();
}
}
复制代码
然后在 invokeMethod()
中并发调用 testTransactionalWithSynchronized()
方法,观察结果:
结果4
正如预期的那样,年龄变成了 1000 ,并且日志中也并未出现 各个线程获取到的 age 的值相同的情况
三、总结
今天,为大家演示了 方法中同时加上 @Transactional 注解和 synchronized 关键字,并发问题依旧存在的问题,我们发现原因在于:线程在提交事务之前,便释放了锁,导致其他线程与自己处在同一事务的情况
可以通过在调用这个方法的方法 (可以理解为父级方法)上添加 synchronized
关键字,使得线程在释放锁之前,就提交事务
来解决即可
之前的文章中其实也有提到过,Spring 中使用 @Transactional
注解的原理
若大家真正了解了这一点的话,这个问题应该是很快就会发现的,要是相反,那类似这样的问题肯定就会存在并且变为难题咯~
其实,道理就是这样,知识范围之外东西的就是会觉得很难;在知识范围之内的自然会觉得很容易,容易有成就感
想要让自己成就感多一点的话,那就跟着 青梅主码
一起坚持学习,拓宽自己的知识范围吧~