【原創】004 | 搭上SpringBoot事務詭異事件分析專車

前言

若是這是你第二次看到師長,說明你在覬覦個人美色!java

點贊+關注再看,養成習慣mysql

沒別的意思,就是須要你的窺屏^_^程序員

專車介紹

該趟專車是開往Spring Boot事務詭異事件的專車,主要來複現和分析事務的詭異事件。面試

專車問題

  • @Transaction標註的同步方法,在多線程訪問狀況下,爲何還會出現髒數據?
  • 在service中經過this調用事務方法,爲何事務就不起效了?

專車示例

示例一

控制器代碼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的結果,那麼你們再猜一下數據庫中會不會插入一條記錄?架構

專車分析

示例一結果

image.png

從如上數據庫結果能夠看到,開啓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();
    }
}

能夠看到,加鎖的代碼包含了事務代碼,能夠保證事務執行完成才釋放鎖。

示例一解決方案結果

image.png

能夠看到數據庫中的結果最終和咱們想要的結果是一致的。

示例二結果

image.png

能夠看到即使執行的代碼具備事務特性,而且事務方法裏面執行了會報錯的代碼,數據庫中最終仍是插入了一條數據,徹底不符合事務的特性。

示例二分析

咱們在來看下示例二的代碼

@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對象是被代理的對象,調用被代理對象的方法的時候,會執行回調,在回調中開啓事務、執行目標方法、提交或者回滾事務。

示例二解決方案結果

image.png

能夠看到數據庫中並無插入新的記錄,說明咱們service方法具備了事務的特性。

專車總結

研讀@Transactional源碼並不僅是爲了讀懂事務是怎麼實現的,還能夠幫助咱們快速定位問題的源頭,並解決問題。

專車回顧

下面咱們來回顧下開頭的兩個問題:

  • @Transaction標註的同步方法,在多線程訪問狀況下,爲何還會出現髒數據?是由於事務在鎖外層,鎖釋放了,事務尚未提交。解決方案就是讓鎖來包裹事務,保證事務執行完成才釋放鎖。
  • 在service中經過this調用事務方法,爲何事務就不起效了?由於this指的是當前對象,只是方法見的普通調用,並不能開啓事務特性。瞭解事務的咱們都知道事務是經過代理來實現的,那麼咱們須要使用被代理對象來調用service中的方法,就能夠開啓事務特性了。

本專車系列文章

【原創】001 | 搭上SpringBoot自動注入源碼分析專車

【原創】002 | 搭上SpringBoot事務源碼分析專車

【原創】003 | 搭上基於SpringBoot事務思想實戰專車

最後

師長,【java進階架構師】號主,短短一年在各大平臺斬獲15W+程序員關注,專一分享Java進階、架構技術、高併發、微服務、BAT面試、redis專題、JVM調優、Springboot源碼、mysql優化等20大進階架構專題,關注【java進階架構師】回覆【架構】領取2019架構師完整視頻一套。

轉載說明:請務必註明來源(本文首發於公衆號:【java進階架構師】

相關文章
相關標籤/搜索