最近一次寫了一個介面在併發場景出現了資料覆蓋的問題,記得從一開始學資料庫的時候就沒有深入瞭解MySQL的鎖和事務這塊,每次一想到這塊就總有一些疑惑,特此記錄一下使用場景以便後期回顧。
業務場景
學生答題每道題有多個空,每個空的正確與否以逗號分隔的方式儲存在一個欄位中,批改人員多次呼叫批閱介面分別批改每道題不同的空,在併發場景如果沒有加鎖就會導致多個空的批改記錄被覆蓋。
表結構大體如下:
CREATE TABLE `test_answer` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`test_id` bigint(20) NOT NULL,
`question_id` bigint(20) NOT NULL,
`student_id` bigint(20) NOT NULL,
`correct_detail` varchar(64) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_test_question_student_id` (`test_id`,`question_id`,`student_id`) USING BTREE,
KEY `idx_student_id` (`student_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
需要執行的sql大體如下
begin;
#a
select correct_detail from test_answer where test_id = 1 and question_id = 1 and student_id = 1;
#b
update test_answer set correct_detail = '0,1' where id = 1;
commit;
我們先假定答案的原始批改記錄是0,0
,2次呼叫分別要將第一第二個空都改成1,當併發批改同一條記錄時都先執行到a
而沒有執行b
並提交時,因為MySQL的預設隔離級別是repeatable read(可重複讀),所以兩個事務讀取的都是0,0
(兩個事務均未提交),就會導致本來想要得到的結果是1,1
,但卻變成了0,1
或1,0
,如下圖
事務A | 事務B |
---|---|
begin | begin |
select correct_detail from test_answer where test_id = 1 and question_id = 1 and student_id = 1; | |
select correct_detail from test_answer where test_id = 1 and question_id = 1 and student_id = 1; | |
update test_answer set correct_detail = '0,1' where id = 1; | |
update test_answer set correct_detail = '1,0' where id = 1; | |
commit | commit |
悲觀鎖與樂觀鎖
解決上面的場景有幾種方式,如果不單說從資料庫角度考慮,可以基於訊息保證唯一索引的記錄線性執行;當然從資料庫的角度除了將隔離級別設定為serializable級別以外,就是針對記錄新增悲觀鎖與樂觀鎖了。
悲觀鎖
悲觀鎖方式的SQL如下
begin;
#a
select correct_detail from test_answer where test_id = 1 and question_id = 1 and student_id = 1 for update;
#b
update test_answer set correct_detail = '0,1' where id = 1;
commit;
使用悲觀鎖時因為查詢的是唯一索引,所以是針對這條資料的行加排他鎖(X鎖),此時其他事務無法讀和寫,只能等待這個事務結束或回滾,排他鎖釋放。(此處多說一句因為InnoDB的二階段提交,所以每次加鎖的時候是立即加鎖而釋放要等事務結束或回滾後才釋放,所以這些加鎖操作應該儘可能放在整個事務中儘可能靠後的位置,降低鎖的時間)
樂觀鎖
樂觀鎖方式的SQL如下
begin;
#a
select correct_detail from test_answer where test_id = 1 and question_id = 1 and student_id = 1;
#b
update test_answer set correct_detail = '0,1' where id = 1 and correct_detail = '0,0';
commit;
樂觀鎖的方式因為查詢時不會加鎖所以可以提高併發,但是也產生了一個問題就是可能當兩個事務併發執行時其中一個無法修改資料,因為需要修改的資料已經變化如上例當其他事務在b執行之前提交了事務那麼b語句並不會更改這條記錄。
用哪個好?
上面就是這個業務場景悲觀與樂觀鎖的使用。具體在實際場景用用哪個,還要具體問題具體分析,如果對併發要求沒那麼高可以使用悲觀鎖的方式;如果這張表修改比較頻繁且對併發要求較高可使用樂觀鎖,從業務角度將修改失敗的資訊返回由使用者做判斷。