springboot 事务_原创004 | 搭上SpringBoot事务诡异事件分析专车

前言

如果这是你第二次看到师长,说明你在觊觎我的美色!

点赞+关注再看,养成习惯

没别的意思,就是需要你的窥屏^_^

2ecad156bd158d79a63fb69d27e5d804.png

专车介绍

该趟专车是开往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代码

@Servicepublic 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代码

@Servicepublic 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的结果,那么大家再猜一下数据库中会不会插入一条记录?

专车分析

示例一结果

执行顺序idNameAge执行前10001xxx0执行后10001xxx994

从如上数据库结果可以看到,开启1000个线程执行所谓带有事务、同步特性的方法,结果并没有1000,出现了脏数据。

示例一分析

我们再来看一下示例一的代码

@Servicepublic 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); }}

我们可以把如上方法转换成如下方法

@Servicepublic 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(); }}

可以看到,加锁的代码包含了事务代码,可以保证事务执行完成才释放锁。

示例一解决方案结果

执行顺序idNameAge执行前10001xxx0执行后10001xxx1000

可以看到数据库中的结果最终和我们想要的结果是一致的。

示例二结果

执行顺序idNameAge执行前10001xxx1000执行后66666transaction22

可以看到即便执行的代码具有事务特性,并且事务方法里面执行了会报错的代码,数据库中最终还是插入了一条数据,完全不符合事务的特性。

示例二分析

我们在来看下示例二的代码

@Servicepublic 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对象是被代理的对象,调用被代理对象的方法的时候,会执行回调,在回调中开启事务、执行目标方法、提交或者回滚事务。

示例二解决方案结果

执行顺序idNameAge执行前10001xxx1000

可以看到数据库中并没有插入新的记录,说明我们service方法具有了事务的特性。

专车总结

研读@Transactional源码并不只是为了读懂事务是怎么实现的,还可以帮助我们快速定位问题的源头,并解决问题。

专车回顾

下面我们来回顾下开头的两个问题:

  • @Transaction标注的同步方法,在多线程访问情况下,为什么还会出现脏数据?是因为事务在锁外层,锁释放了,事务还没有提交。解决方案就是让锁来包裹事务,保证事务执行完成才释放锁。
  • 在service中通过this调用事务方法,为什么事务就不起效了?因为this指的是当前对象,只是方法见的普通调用,并不能开启事务特性。了解事务的我们都知道事务是通过代理来实现的,那么我们需要使用被代理对象来调用service中的方法,就可以开启事务特性了。

本专车系列文章

【原创】001 | 搭上SpringBoot自动注入源码分析专车

【原创】002 | 搭上SpringBoot事务源码分析专车

【原创】003 | 搭上基于SpringBoot事务思想实战专车

最后

师长,【java进阶架构师】号主,短短一年在各大平台斩获15W+程序员关注,专注分享Java进阶、架构技术、高并发、微服务、BAT面试、redis专题、JVM调优、Springboot源码、mysql优化等20大进阶架构专题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值