【原创】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大进阶架构专题。
【原创】004 | 搭上SpringBoot事务诡异事件分析专车的更多相关文章
- 【原创】002 | 搭上SpringBoot事务源码分析专车
前言 如果这是你第二次看到师长,说明你在觊觎我的美色! 点赞+关注再看,养成习惯 没别的意思,就是需要你的窥屏^_^ 专车介绍** 该趟专车是开往Spring Boot事务源码分析的专车 专车问题 为 ...
- 【原创】005 | 搭上SpringBoot请求处理源码分析专车
前言 如果这是你第二次看到师长,说明你在觊觎我的美色! 点赞+关注再看,养成习惯 没别的意思,就是需要你的窥屏^_^ 专车介绍 该趟专车是开往Spring Boot请求处理源码分析专车,主要用来分析S ...
- 原创001 | 搭上SpringBoot自动注入源码分析专车
前言 如果这是你第二次看到师长的文章,说明你在觊觎我的美色!O(∩_∩)O哈哈~ 点赞+关注再看,养成习惯 没别的意思,就是需要你的窥屏^_^ 本系列为SpringBoot深度源码专车系列,第一篇发车 ...
- 【原创】003 | 搭上基于SpringBoot事务思想实战专车
前言 如果这是你第二次看到师长,说明你在觊觎我的美色! 点赞+关注再看,养成习惯 没别的意思,就是需要你的窥屏^_^ 专车介绍 该趟专车是开往基于Spring Boot事务思想实战的专车,在上一篇 搭 ...
- Spring系列28:@Transactional事务源码分析
本文内容 @Transactional事务使用 @EnableTransactionManagement 详解 @Transactional事务属性的解析 TransactionInterceptor ...
- SpringBoot事件监听机制源码分析(上) SpringBoot源码(九)
SpringBoot中文注释项目Github地址: https://github.com/yuanmabiji/spring-boot-2.1.0.RELEASE 本篇接 SpringApplicat ...
- springboot+redis做事件过期通知业务
springboot+redis做事件过期通知 博主也是初次体验,不足之处多多指教 我的业务场景 系统管理员要给维护员分配巡查路口设施的工作,由于路口比较多,管理员不知道哪些路口已经被分配了,况且过了 ...
- springboot 事务创建流程源码分析
springboot 事务创建流程源码分析 目录 springboot 事务创建流程源码分析 1. 自动加载配置 2. InfrastructureAdvisorAutoProxyCreator类 3 ...
- springboot 事务执行全流程分析
springboot 事务执行全流程分析 目录 springboot 事务执行全流程分析 1. 事务方法执行前的准备工作 2. 业务代码的调用 3. 事务方法执行后处理 4. 业务代码在事务和非事务中 ...
随机推荐
- python学习之【第八篇】:Python中的函数基础
1.前言 函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段.函数能提高应用的模块性,和代码的重复利用率. 2.函数的定义 定义函数时需要遵守以下规则: 函数代码块以 def 关键词开头 ...
- 怎样判断有没有SQL注入漏洞及原理?
来源:实验楼 最为经典的单引号判断法: 在参数后面加上单引号,比如: http://xxx/abc.php?id=1' 如果页面返回错误,则存在 Sql 注入. 原因是无论字符型还是整型都会因为单引号 ...
- Python 基础 常用模块
Python 为我们提供了很多功能强大的模块,今天就主要使用的到的模块进行整理,方便后面来翻阅学习. 一.时间模块 在时间模块中我们重点介绍几种自己常用的功能,主要方便我们按照自己想要的方式获取时间 ...
- Apache Spark 3.0 预览版正式发布,多项重大功能发布
2019年11月08日 数砖的 Xingbo Jiang 大佬给社区发了一封邮件,宣布 Apache Spark 3.0 预览版正式发布,这个版本主要是为了对即将发布的 Apache Spark 3. ...
- 通过javascript 执行环境理解她
古往今来最难的学的武功(javascript)算其一. 欲练此功必先自宫,愿少侠习的此功,笑傲江湖. 你将了解 执行栈(Execution stack) 执行上下文(Execution Context ...
- Bootstrap——导航条(navbar)
导航条和导航从外观上差别不是太多,但在实际使用中导航条要比导航复杂得多. 导航条(navbar)中有一个背景色.而且导航条可以是纯链接(类似导航).表单以及表单和导航一起结合等多种形式. 在制作一个基 ...
- PL真有意思(一):引言
前言 断断续续学编译原理到之前发过写一个编译器和正则表达式引擎系列文章也有一段时间了,然后最近看完PLP这本书,这本书应该算是入门书,但是对我这种半吊子收获很大.所以为了弥补最近学操作系统和接外包摸的 ...
- 初识JVM内存模型
计算机内存模型 在程序运行时,CPU通过访问主存获取数据,但随着CPU的快速发展,CPU访问速度越来越高,硬件无法满足CPU的条件下,大多内存加入了高速缓存机制,不同CPU都有对应的多级(一般为三)缓 ...
- 记一个vue-resource请求的低级错误
对于初学的小菜鸡,经常会犯一些低级错误. 现在记录一下我在使用vue-resource发送post请求时的一个低级错误: window.BaseURL = '127.0.0.1:8888'; 8888 ...
- keypress 和 blur 事件冲突的问题
需求:点击需求:点击添加标签,出来input框,内容输入完成后点击enter键和blur时都可以执行提交标签的效果,提交时对内容进行判断,执行完成后清除input内的内容.如下图 问题:内容输入完成后 ...