若是这是你第二次看到师长,说明你在觊觎个人美色!java
点赞+关注再看,养成习惯mysql
没别的意思,就是须要你的窥屏^_^程序员
该趟专车是开往Spring Boot事务诡异事件的专车,主要来复现和分析事务的诡异事件。面试
控制器代码redis
@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代码sql
@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方法具备事务特性和同步特性。那么你们来猜一下最终的结果是多少?数据库
控制器代码mysql优化
@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的结果,那么你们再猜一下数据库中会不会插入一条记录?架构
从如上数据库结果能够看到,开启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(); } }
能够看到,加锁的代码包含了事务代码,能够保证事务执行完成才释放锁。
能够看到数据库中的结果最终和咱们想要的结果是一致的。
能够看到即使执行的代码具备事务特性,而且事务方法里面执行了会报错的代码,数据库中最终仍是插入了一条数据,彻底不符合事务的特性。
咱们在来看下示例二的代码
@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对象是被代理的对象,调用被代理对象的方法的时候,会执行回调,在回调中开启事务、执行目标方法、提交或者回滚事务。
能够看到数据库中并无插入新的记录,说明咱们service方法具备了事务的特性。
研读@Transactional源码并不仅是为了读懂事务是怎么实现的,还能够帮助咱们快速定位问题的源头,并解决问题。
下面咱们来回顾下开头的两个问题:
【原创】001 | 搭上SpringBoot自动注入源码分析专车
【原创】002 | 搭上SpringBoot事务源码分析专车
【原创】003 | 搭上基于SpringBoot事务思想实战专车
师长,【java进阶架构师】号主,短短一年在各大平台斩获15W+程序员关注,专一分享Java进阶、架构技术、高并发、微服务、BAT面试、redis专题、JVM调优、Springboot源码、mysql优化等20大进阶架构专题,关注【java进阶架构师】回复【架构】领取2019架构师完整视频一套。转载说明:请务必注明来源(本文首发于公众号:【java进阶架构师】