【原创】004 | 搭上SpringBoot事务诡异事件分析专车
前言
如果这是你第二次看到师长,说明你在觊觎我的美色!
点赞+关注再看,养成习惯
没别的意思,就是需要你的窥屏^_^
本专车系列文章
目前连载到第四篇,本专题是深入讲解springboot源码,毕竟是源码分析,相对会比较枯燥,但是通读下来会让你对boot有个透彻的理解!初级boot实战小白教程,我后续也会出。大家放心。 前面三篇,还没看过的大家可以看看。
【原创】001 | 搭上springboot自动注入源码分析专车
【原创】002 | 搭上springboot事务源码分析专车
【原创】003 | 搭上基于springboot事务思想实战专车
专车介绍
该趟专车是第四篇,开往spring boot事务诡异事件的专车,主要来复现和分析事务的诡异事件。
专车问题
- @transaction标注的同步方法,在多线程访问情况下,为什么还会出现脏数据?
- 在service中通过this调用事务方法,为什么事务就不起效了?
专车示例
示例一
控制器代码
@restcontroller @requestmapping("/test") public class testcontroller { @autowired private testservice testservice; /** * @param id */ @requestmapping("/addstudentage/{id}") public void addstudentage(@pathvariable(name = "id") integer id){ for (int i = 0; i < 1000; i++) { new thread(() -> { try { testservice.addstudentage(id); } catch (interruptedexception e) { e.printstacktrace(); } }).start(); } } }
service代码
@service public class testservice { @autowired private studentmapper studentmapper; @autowired private testservice testservice; @transactional(rollbackfor = exception.class) public synchronized void addstudentage(integer id) throws interruptedexception { student student = studentmapper.getstudentbyid(id); studentmapper.updatestudentagebyid(student); } }
示例代码很简单,开启1000个线程调用service的方法,service先从数据库中查询出用户信息,然后对用户的年龄进行 + 1操作,service方法具有事务特性和同步特性。那么大家来猜一下最终的结果是多少?
示例二
控制器代码
@restcontroller @requestmapping("/test") public class testcontroller { @autowired private testservice testservice; @requestmapping("/addstudent") public void addstudent(@requestbody student student) { testservice.middlemethod(student); } }
service代码
@service public class testservice { @autowired private studentmapper studentmapper; public void middlemethod(student student) { // 请注意此处使用的是this this.addstudent(student); } @transactional(rollbackfor = exception.class) public void addstudent(student student) { this.studentmapper.savestudent(student); system.out.println(1/ 0); } }
示例代码同样很简单,首先往数据库中插入一条数据,然后输出1 / 0的结果,那么大家再猜一下数据库中会不会插入一条记录?
专车分析
示例一结果
执行顺序 | id | name | age |
---|---|---|---|
执行前 | 10001 | xxx | 0 |
执行后 | 10001 | xxx | 994 |
从如上数据库结果可以看到,开启1000个线程执行所谓带有事务、同步特性的方法,结果并没有1000,出现了脏数据。
示例一分析
我们再来看一下示例一的代码
@service public class testservice { @autowired private studentmapper studentmapper; @autowired private testservice testservice; @transactional(rollbackfor = exception.class) public synchronized void addstudentage(integer id) throws interruptedexception { student student = studentmapper.getstudentbyid(id); studentmapper.updatestudentagebyid(student); } }
我们可以把如上方法转换成如下方法
@service public class testservice { @autowired private studentmapper studentmapper; @autowired private testservice testservice; // 事务切面,开启事务 public synchronized void addstudentage(integer id) throws interruptedexception { student student = studentmapper.getstudentbyid(id); studentmapper.updatestudentagebyid(student); } // 事务切面,提交或者回滚事务 }
通过转换我们可以清楚的看到方法执行完成后就释放锁,此时事务还没来得及提交,下一个请求就进来了,读取到的是上一个事务提交之前的结果,这样就会导致最终脏数据的出现。
示例一解决方案
解决的重点:就是我们要在事务执行完成之后才释放锁,这样可以保证前一个请求实实在在执行完成,包括提交事务才允许下一个请求来执行,可以保证结果的正确性。
解决示例代码
@requestmapping("/addstudentage1/{id}") public void addstudentage1(@pathvariable(name = "id") integer id){ for (int i = 0; i < 1000; i++) { new thread(() -> { try { synchronized (this) { testservice.addstudentage1(id); } } catch (interruptedexception e) { e.printstacktrace(); } }).start(); } }
可以看到,加锁的代码包含了事务代码,可以保证事务执行完成才释放锁。
示例一解决方案结果
执行顺序 | id | name | age |
---|---|---|---|
执行前 | 10001 | xxx | 0 |
执行后 | 10001 | xxx | 1000 |
可以看到数据库中的结果最终和我们想要的结果是一致的。
示例二结果
执行顺序 | id | name | age |
---|---|---|---|
执行前 | 10001 | xxx | 1000 |
执行后 | 66666 | transaction | 22 |
可以看到即便执行的代码具有事务特性,并且事务方法里面执行了会报错的代码,数据库中最终还是插入了一条数据,完全不符合事务的特性。
示例二分析
我们在来看下示例二的代码
@service public class testservice { @autowired private studentmapper studentmapper; public void middlemethod(student student) { // 请注意此处使用的是this this.addstudent(student); } @transactional(rollbackfor = exception.class) public void addstudent(student student) { this.studentmapper.savestudent(student); system.out.println(1/ 0); } }
可以看到middlemethod方法是通过this来调用其它事务方法,那么就是方法间的普通调用,不存在任何的代理,也就不存在事务特性一说。所以最终即便方法报错,数据库也插入了一条记录,是因为该方法虽被 @transactional注解标注,却不具备事务的功能。
示例二解决方案
解决方案很简单,使用被代理对象来替换this
public void middlemethod1(student student) { testservice.addstudent(student); }
因为testservice对象是被代理的对象,调用被代理对象的方法的时候,会执行回调,在回调中开启事务、执行目标方法、提交或者回滚事务。
示例二解决方案结果
执行顺序 | id | name | age |
---|---|---|---|
执行前 | 10001 | xxx | 1000 |
可以看到数据库中并没有插入新的记录,说明我们service方法具有了事务的特性。
专车总结
研读@transactional源码并不只是为了读懂事务是怎么实现的,还可以帮助我们快速定位问题的源头,并解决问题。
专车回顾
下面我们来回顾下开头的两个问题:
- @transaction标注的同步方法,在多线程访问情况下,为什么还会出现脏数据?是因为事务在锁外层,锁释放了,事务还没有提交。解决方案就是让锁来包裹事务,保证事务执行完成才释放锁。
- 在service中通过this调用事务方法,为什么事务就不起效了?因为this指的是当前对象,只是方法见的普通调用,并不能开启事务特性。了解事务的我们都知道事务是通过代理来实现的,那么我们需要使用被代理对象来调用service中的方法,就可以开启事务特性了。
最后
师长,【java进阶架构师】号主,短短一年在各大平台斩获15w+程序员关注,专注分享java进阶、架构技术、高并发、微服务、bat面试、redis专题、jvm调优、springboot源码、mysql优化等20大进阶架构专题。
上一篇: 5 分钟快速学习,缓存一致性优化方案!
推荐阅读
-
Android TabLayout和ViewPager关联原理分析
-
php笔记之:有规律大文件的读取与写入的分析_PHP教程
-
关于扩展 Laravel 默认 Session 中间件导致的 Session 写入失效问题分析,laravelsession
-
PHP语言中global跟$GLOBALS[]的分析【转】
-
基于php的CMS中展示文章类实例分析_PHP
-
Smarty高级应用之缓存操作技巧分析_PHP
-
iOS事件传递及处理方法
-
PHP 事件驱动框架 实践
-
ThinkPHP中__initialize()和类的构造函数__construct()用法分析_php实例
-
PHP面向对象分析设计的61条军规_PHP教程