近期看到了一個前阿里資深開發的學術分析影片:
高併發情況下,一個事務內有更新操作還有查詢操作,那是先更新好,還是先無鎖查詢好?
僅70秒的影片,深感學問太深,但是海哥講的有待補充,於是寫下了這篇文章,作為補充。
鳴謝:前阿里資深開發極海Channel的技術分享。
先說答案
這是個開放性的問題,必須看業務場景,拋開業務場景談架構設計,都是耍流氓。
- 場景1:如果update語句的引數操作依賴於查詢操作,那麼必須先查詢,再更新,否則update語句的引數都湊不齊。
- 場景2:如果update語句的引數操作不依賴於查詢操作,但兩個操作的是一張表,業務強制要求select獲取的資料必須是最新的,則也需要先更新再讀取。
- 場景3:以上兩種情況除外,則優先考慮先查詢再更新。
場景1受引數限制,順序毋庸置疑。
場景2受業務限制,順序毋庸置疑。
場景3是為了效能最佳化,才去選擇的方案。
測試表
CREATE TABLE `cs` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`num` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '數字列',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `temp`.`cs` (`id`, `num`) VALUES (1, 1);
INSERT INTO `temp`.`cs` (`id`, `num`) VALUES (2, 2);
INSERT INTO `temp`.`cs` (`id`, `num`) VALUES (3, 3);
假設場景2的程式碼如下:
start transaction;
update cs set num = 13 where id = 1;
SELECT * FROM `cs`;
commit;
場景3的程式碼如下:
start transaction;
SELECT * FROM `cs`;
update cs set num = 13 where id = 1;
commit;
對場景2,同一個事務內,先執行update,事務沒提交,能讀取到最新的資料嗎?
能。
步驟 | SQL | 補充 |
---|---|---|
1 | start transaction; | 開啟事務 |
2 | select num from cs where id = 1; | num值是1 |
3 | update cs set num = 12 where id = 1; | 更新num值為12 |
4 | select num from cs where id = 1; | num值是12 |
5 | rollback; | 回滾事務 |
都說不加鎖的select是快照讀,為什麼select還能讀取當前事務的最新資料?
根據現象反推,個人認為:
不加鎖的select是快照讀,針對的是事務之外的不加鎖的select,MySQL RR的隔離級別,只要更新的事務未提交,其它事務就讀取不到更改的新資料。
當前事務內的select,就是不加鎖的當前讀(個人稱謂)。當然,真正的當前讀概念是為了保證讀的最新資料,必須加鎖。
對場景3的效能最佳化原理分析
得知道4個前提:
- 場景3的實現,不會影響業務和開發。
- 因為select沒有加S或X鎖,所以不會阻塞。
- update語句會上X鎖,可能是行鎖、間隙鎖,或者表鎖(受索引、where條件的影響),所以併發請求過來後,其它事務的update有被阻塞的可能。
- 用事務,就需要InnoDB引擎,InnoDB引擎支援行級鎖,如果確定update的X鎖,在併發情況下鎖定的範圍沒有交集,這種最佳化方式起不到作用,這意味著不會阻塞。
- 如果先更新再查詢:
分析原理:
事務A的update語句會上鎖,併發情況下阻塞事務B的update操作,如果事務A的select是個慢查詢,事務A的X鎖釋放需要等到事務提交,而不是update語句本身執行完畢,這就意味著事務A select的環節,X鎖也未釋放,從而阻塞其它事務的update,降低效能。
實操模擬:
步驟 | 事務A | 事務B | 補充 |
---|---|---|---|
1 | start transaction; | start transaction; | 雙方開啟事務,模擬併發請求 |
2 | update cs set num = 1234 where id = 1; | update cs set num = 1234; | 兩個不同where範圍的update,模擬線上的場景 |
3 | / | 阻塞 | 事務A的行X鎖,阻塞了事務B的表X鎖 |
4 | select * from cs where id = 1; | 阻塞 | 這一步很重要,最佳化就是為了避免這一步的阻塞耗時,特別是慢查詢 |
5 | commit; | 阻塞 | 事務A提交 |
6 | / | select * from cs where id = 1; | 事務A完成,事務B不會再阻塞了 |
7 | / | commit; | 結束事務B |
- 如果先查詢再更新:
分析原理:事務A的select語句不會上鎖,此時事務不會導致事務B阻塞,如果執行到事務A執行到update,才回去上X鎖,直到事務提交鎖資源釋放,即使事務A的select是一個慢查詢,也不會加大事務A釋放鎖資源的事件,進而減少事務B的阻塞時間。
實操模擬:
步驟 | 事務A | 事務B | 補充 |
---|---|---|---|
1 | start transaction; | start transaction; | 雙方開啟事務,模擬併發請求 |
2 | select * from cs where id = 1; | select * from cs where id = 1; | 兩個事務不加鎖不阻塞,這一步的阻塞時間省了 |
3 | update cs set num = 1234 where id = 1; | update cs set num = 1234; | 兩個事務更新 |
4 | / | 阻塞 | 兩個X鎖範圍有衝突,阻塞 |
5 | commit; | 阻塞 | 事務A提交 |
6 | / | commit; | 事務B提交 |
所以說,只要業務允許,調整SQL語句的執行順序,高併發情況下,就能得到不小的效能提升,但是這一點很容易忽略。