SpringBoot+Mybatis保證讀寫事務隔離性的三種實現方式


SpringBoot+Mybatis保證讀寫事務隔離性的三種實現方式

實際開發中經常會有這樣的需求,注冊用戶,如果用戶名存在則失敗,否則注冊成功。

在單線程下,邏輯很簡單,但是高並發下需要保證事務隔離性,這里舉一個簡化版的例子來講述自己的實現方法。

問題

在實際開發的時候,我們經常會做這種事情:

  1. 先查詢數據庫中的數據,得到一些臨時結果
  2. 根據一些臨時結果做判斷,進行增刪改查操作

也就是說,第二個階段的增刪改查操作依賴於在第一個階段的結果

舉個例子,我們的表結構很簡單

image-20210302163200742

查詢是否存在dname=TEST的部門,如果不存在插入一個部門名為TEST,如果存在則不操作。

要求必須不能使數據庫中存在兩個dname相同的行

我們知道SpringBoot中的Controller、Service都是單例的,在實際環境下,面對高並發量的請求,每個請求會起一個線程來進行操作,那么就會發生一些問題

舉一個類似“臟讀”的例子

@Service
public class DeptServiceImpl implements DeptService{
    @Autowired
    DeptDAO deptDAO;

    @Override
    public int concurrent() {
        Dept dept = deptDAO.queryByName("TEST");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (dept == null) {
            System.out.println("不存在TEST,插入");
            Dept nDept = new Dept().setDName("TEST");
            deptDAO.addDept(nDept);
        } else {
            System.out.println("存在");
        }
        return 1;
    }
}

如果不加以處理,我們對以下接口連着發兩個請求調用這個方法試試

    @PostMapping("/concurrent")
    public int concurrent(){
        return deptService.concurrent();
    }

結果明顯是有問題的

image-20210302163724061

image-20210302163758834

方法一:加synchronized鎖

最簡單方法,犧牲一些性能,加synchronized鎖即可

    @Override
    public synchronized int concurrent() {
        Dept dept = deptDAO.queryByName("TEST");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (dept == null) {
            System.out.println("不存在TEST,插入");
            Dept nDept = new Dept().setDName("TEST");
            deptDAO.addDept(nDept);
        } else {
            System.out.println("存在");
        }
        return 1;
    }

執行結果沒問題

image-20210302163933812

注意,這里為了實驗使用了Thread.sleep(3000);,切記不能使用object.wait(3000);,因為wait會釋放鎖

方法二:使用dual表寫sql

出現這個問題的原因在於,我們使用了兩次mapper,是否能在一個語句里實現需求呢?

其實是可以的,使用dual表

修改Mybatis語句

    <insert id="addDept" parameterType="com.cpaulyz.PO.Dept">
        insert into dept(dname, db_source) select #{dName},DATABASE() from dual where not exists(
          select * from dept where dname = #{dName}
        );
    </insert>

實際上就是

insert into dept(dname, db_source) select "TEST",DATABASE() from dual where not exists(
  select * from dept where dname = "TEST"
);

然后直接插入即可

    @Override
    public synchronized int concurrent() {
        Dept nDept = new Dept().setDName("TEST");
        deptDAO.addDept(nDept);
        return 1;
    }

方法三:行鎖+@Transactional

分析一下出現問題的原因,主要在於Dept dept = deptDAO.queryByName("TEST");時,默認使用的是快照讀,即select * from dept where xxxx;

我們可以進行當前讀,類似MySQL的鎖策略

修改mybatis映射

    <select id="queryByName" resultType="com.cpaulyz.PO.Dept" resultMap="DeptMap">
        select * from dept where dname=#{name} for update;
    </select>

在方法頭上加上@Transactional注解(該注解還可以進行隔離級別的配置,這里不再贅述)

    @Override
    @Transactional
    public  int concurrent() {
        Dept dept = deptDAO.queryByName("TEST");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (dept == null) {
            System.out.println("不存在TEST,插入");
            Dept nDept = new Dept().setDName("TEST");
            deptDAO.addDept(nDept);
        } else {
            System.out.println("存在");
        }
        return 1;
    }

測試,成功

image-20210302171607298


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM