若是這是你第二次看到師長,說明你在覬覦個人美色!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進階架構師】