【原創】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大進階架構專題。