注: 理解本文 前置需要掌握的基礎知識:事務隔離、鎖的概念、并發知識;
事務隔離 尤其是事務延伸問題 是個重難點,絕非八股文那幾句話就能說完的,在實際場景中,分析起來有一定難度
@author: csdn博主 孟秋與你
本文將徐徐漸進,由簡入深分析一個案例
我們先看一個最簡單的例子:
Test test = new Test();long id = 1L;test .setId(id);test .setDes(System.currentTimeMillis()+"描述");try {testMapper.insert(test );} catch (DuplicateKeyException e) {Test t1 = tTestTMapper.selectById(id);return t1;}
我們插入一個id = 1 的數據,如果發現數據庫存在了 那就返回數據庫已有的數據;
然而 表面人畜無害的代碼,在高并發場景下 都有可能出現問題
當我們給代碼加上事務注解時:
@Transactionalpublic void test() {Test test = new Test();long id = 1L;test .setId(id);test .setDes(System.currentTimeMillis()+"描述");try {testMapper.insert(test );} catch (DuplicateKeyException e) {Test oldRes= testMapper.selectById(id);return oldRes;}// 模擬其它業務try {Thread.sleep(5500L);} catch (InterruptedException e) {e.printStackTrace();}}
重點來了
spring中事務是和線程綁定的,假設有A線程和B線程 同時執行insert 方法,實際只會有一個插入成功,我們假設A執行成功了,B執行失敗,B線程會進入catch模塊 并查找數據庫已有的數據。
我們可以思考一下:
- B線程是如何知道自己失敗了的?
- B線程能成功查詢到結果嗎?
A1:id是唯一的(唯一索引同理),當A線程插入id=1 的數據后,mysql會為它加上一把排它鎖,當B線程企圖insert的時候 會一直等待鎖釋放,假設線程A執行時間是6s 在6s后才將事務提交, 那么線程B在6秒后才會進入到catch模塊 而不是立即拋出異常
A2:線程B在這個例子中能獲取到結果;但是會有些隱藏的坑。
catch模塊 事務B能獲取到事務A提交的結果嗎?
這取決于事務B是當前讀還是快照讀,在上面代碼中,由于之前沒有建立快照 在catch中才開始查詢 所以可以查到A事務提交的結果。
快照讀解釋:事務開始時會建立一個快照,這個快照代表了事務開始時刻數據庫的狀態。當事務執行SELECT查詢時,它看到的是該事務開始時數據庫數據的一個一致視圖,而不是當前時刻可能還在被其他事務修改的數據。這意味著,即使其他事務對數據進行了修改并提交,正在進行快照讀的事務看到的仍然是它自己快照中的數據版本,除非那些修改在該事務自己的更新操作中可見。
實際業務中 代碼會更加復雜:
@Transactionalpublic void test() {Test test = new Test();long id = 1L;test.setId(id);test.setDes(System.currentTimeMillis()+"");// 模擬查詢已有數據 實際業務中可能是service來回調用查詢方法Test exist= testMapper.selectById(id);// 省略其它業務代碼 如校驗、關聯查詢等try {testMapper.insert(test);} catch (DuplicateKeyException e) {exist= testTMapper.selectById(id);System.out.println("數據已存在"+testT1);if (testT1 == null) {System.out.println("==============================================================================");}}try {Thread.sleep(5500L);} catch (InterruptedException e) {e.printStackTrace();}}
此時,并發場景下 線程B獲取到的就為null了;
因為在 這行代碼中, 已經生成了一個快照 , 所以在catch模塊中,也是查詢這個快照結果,它并不能感知到A線程已經插入了數據
Test exist= testMapper.selectById(id);
梳理一下執行情況 如下圖:
如果我們希望catch模塊能拿到A線程已提交的數據 可以將快照讀改成當前讀:
// catch 模塊中 快照讀修改為當前讀// Test t1 = tTestTMapper.selectById(id);Test t1 = tTestTMapper.selectForUpdateById(id);
selectForUpdateById 示例:
select * from test where id = 1 for update
for update給當前數據加鎖,也正因為它會加鎖 所以是當前讀(否則數據就不正確了)
總結本文知識點:
- mysql唯一鍵插入數據時 會加上排它鎖,其它線程會等待它的鎖釋放
(注意 不是表級鎖,例如我們的例子 只是id=1的數據加鎖 不要誤解) - 事務默認是快照讀,當已建立快照時 是不能感知到其它線程并發修改的
- 可以通過for update 改成當前讀(需要注意for update會加鎖 )