標籤:技術 mysql資料庫 需求 取資料 增量處理 曆史版本 強制 結束 範圍
一、背景
熟悉MySQL資料庫的朋友們都知道,查詢資料常見模式有三種:
1. select ... :快照讀,不加鎖
2. select ... in share mode:當前讀,加讀鎖
3. select ... for update:當前讀,加寫鎖
從技術層面理解三種方式的應用情境其實並不困難,下面我們先快速複習一下這三種讀模數式的在技術層面上的區別。
註:為了簡化問題的描述,下面所有結論均是針對MySQL資料庫InnoDB儲存引擎RR隔離等級下的。
1.1 select ...
讀取當前事務開始時結果集的快照版本,快照版本也可以理解為曆史版本。
因為只需讀取一個曆史版本,而曆史不會被修改,故曆史版本本身就是一個不可變版本,所以本讀模數式對讀取前後的資源處理相對簡單:
1. 讀取行為發生之前,如果有其他尚未提交的事務已經修改了結果集,本讀模數式不會等待這些事務結束,自然也讀取不到這些修改。
2. 讀取行為發生之後,當前事務提交之前,本讀模數式也不會阻止其他事務修改資料,產生更新版本的結果集。
1.2 select ... in share mode
讀取結果集的最新版本,同時防止其他事務產生更新的資料版本。
由於資料的最新版本是不斷變化的,所以本讀模數式需要強制阻斷最新版本的變化,保證自己讀取到的是所有人都一致認可的名副其實的最新版本。
本讀模數式在讀取前後對資源處理如下:
1. 讀取行為發生之前,擷取讀鎖。這意味著如果有其他尚未提交的事務已經修改了結果集,本讀模數式會等待這些事務結束,以確保自己稍後可以讀取到這些事務對結果集的修改,同時等待期間會阻塞其他事務對結果集的修改。
2. 讀取行為發生之後,當前事務提交之前,本讀模數式會持續阻塞其他事務對結果集的修改。
3. 當前事務提交後,釋放讀鎖。這意味著所有之前被阻塞的事務可恢複繼續執行。
1.3 select ... for update
本讀模數式擁有select ... in share mode的一切功能,同時它還額外具備阻止其他事務讀取最新版本的能力。
本讀模數式在讀取前後對資源的處理如下:
1. 讀取行為發生之前,擷取寫鎖。這意味著如果有其他尚未提交的事務已經修改了結果集,本讀模數式會等待這些事務結束,以確保自己稍後可以讀取到這些事務對結果集的修改,同時等待其他會組織其他事務對結果集最新版本的讀取和修改。
2. 讀取行為發生之後,當前事務提交之前,本讀模數式會持續阻塞其他事務對結果集的修改,也會阻塞其他事務對結果集最新版本的讀取。
3. 當前事務提交後,釋放寫鎖。這意味著所有之前被阻塞的事務可恢複繼續執行。
三種讀模數式在技術層面的區別到此就複習完了,可是我們在實際業務編程過程中,讀取資料庫中的記錄到底什麼時候要加讀鎖,什麼時候要加寫鎖呢?
讀取快照版本的曆史資料和讀取最新版本的資料對應到業務層面是怎樣的一種商務邏輯需求?難道每寫一處資料庫查詢代碼,都要從技術層面去細細思考不同讀模數式其讀取行為發生之前、之後對資源的處理是否符合業務需求嗎?這樣編程也太辛苦啦。
帶著上述疑問,本文將嘗試從每種讀模數式的技術性功能出發,將不同模式下的技術功能差異轉換為業務需求差異,從而總結出不同功能的應用情境,最終產出少數的操作性強的情境判定規則,用於快速回答不同業務情境下查詢資料庫是否應該加讀鎖或寫鎖這一問題。
二、技術功能差異到業務需求差異的轉換2.1 select ... for update vs select ... in share mode
select ... for update相對於select ... in share mode而言,對讀取到的結果集的最新版本具有更強的獨佔性。select ... in share mode只是阻塞其他事務對結果集產生更新版本,而select .. for update還會阻塞其他事務對結果集最新版本的讀取。
業務層面在什麼情況下需要阻塞其他事務對結果集最新版本的讀取呢?
不想讓別人也可以讀取到最新版本,往往是因為自己想在最新版本上進行修改,同時擔心其他人也和自己一樣。因為大家在修改資料時,總是希望自己的修改與資料的最新版本(而不是曆史版本)合并後存入資料庫中,所以大家在修改資料前,都會嘗試擷取資料的最新版本,基於最新版本進行修改。如果每個人都可以同時擷取到資料的最新版本並在最新版本上加入自己的修改,最後大家一起提交資料,必然會出現一個人的修改覆蓋了其他人修改的情況,這就是經典的“更新丟失”問題。
其實這個問題還可以反過來問,什麼情況下不必阻塞其他事務對結果集的讀取呢?
試想如果無論你阻不阻塞讀取,其他事務讀取到的結果集都是一樣的,你又何必阻塞它呢?如果你不修改讀取出的結果集,那麼別人早讀晚讀又有什麼區別?
通過上面的思考,我們可以得出如下結論:
結論一:如果讀取出的資料自己不需要修改它,是肯定不需要使用select ... for update的。
結論二:如果讀取出的資料自己需要修改它,“更新丟失”問題在絕大部分業務情境中都是應該避免的,所以此時需要使用select ... for update。
2.2 select ... in share mode vs select ...
select ... in share mode相對於select ... 而言,主要新增了兩點約束:
1. 讀取資料之前,等待修改了這些資料的事務提交。
2. 讀取資料之後,防止其他事務修改這些資料。
我們先用業務層面的語言將上述兩點約束合并簡述為:希望讀取到所有人都一致認可的最新版本的資料(即沒有其他人還正在修改這些資料)並鎖定它。
那麼什麼樣的業務情境下,我們需要達到這樣的效果呢?
我能想到的有如下兩個典型的情境:
例1. 基於更新時間戳記增量處理資料
我此次讀取並處理了時間點A之前的資料,下次就不會再讀取並處理這個範圍內的資料了,這就是增量處理的要求。如果我讀取之前有人已經修改這個範圍內的資料,只是事務尚未提交(由於修改行為發生在時間點A之前,所以這些資料的更新時間戳記也在時間點A之前),我讀取之後這些修改提交了。
若我採用的是普通的select ... 意味著雖然我讀取並處理了時間點A之前的資料,但是在我讀取之後這個範圍內又出現了新的資料。這就會漏掉部分尚未處理的資料。
若果我採用的是select ... in share mode,則會等待待查詢時間範圍內的修改均提交後,再處理這個範圍內的資料,就可以避免漏處理問題。
本例中出現的問題隱含了一個前提條件,那就是新的資料提交時,新增資料的一方並沒有主動通知我進行處理,而是由我去基於時間戳記掃描新增資料。相當於商務邏輯的完整性由我單方面保證,而另外一方並不願意為此事效勞。事實上基於更新時間戳記增量處理資料的情境中,通常處理常式是第三方,基於時間戳記掃描增量資料只是為了盡量保證原資料表上應用系統無需修改,即減少侵入性。
(註:基於更新時間戳記處理新增資料時,設定安全讀取時延是更加常用的解決方式。即每次讀取的時間點設定為目前時間X分鐘前,X分鐘大於系統中事物持續的最大時間,以保證抽取時間點之前的所有修改都已提交。但是這種方式會降低資料處理的即時性。)
那麼,假設修改資料的每一方都願意通力配合,竭盡全力地保證資料的一致性和商務邏輯的完整性時,就不會出問題了嗎?請看下面這個例子。
例2. 更新關聯關係
比如,比如有Books和Students兩張表,一張BooksToStudents的多對多關聯表。新增Book需要讓每個Studuent都有這個Book。新增Student需要讓所有Book都屬於該Student。無論何時,對資料一致性的要求是:所有Student都擁有所有的Book。
如果兩個人A和B,同時開啟事務,一人新增BookA,一人新增StuduentB,大家各自嚴格按照資料一致性要求去維護BooksToStudents關聯表。
如果不使用select ... in share mode而是使用select ... ,由於每個事務都無法讀取到對方的尚未提交的新增實體,A不知道有StudentB,所以A的BookA不會屬於StudentB;B不知道有BookA,所以B的StudentB下不會有BookA。最終兩個事務提交後,結果就是StudentB沒有擁有BookA。
A和B都有機會建立起StudentB下擁有BookA這一關聯記錄,但是這份關聯記錄的建立只在A添加BookA時,以及B添加StudentB時處理,如果這兩個時刻均讀取不到需要的記錄,這份關聯記錄的建立將永遠不會再被觸發。對於多對多關聯這種“我中有你,你中有我”的循環相依性關係的更新,雙方的行為如果不加約束的隨意發生,就可能發生
但是,如果使用select ... in share mode,當A讀取Students表時,發現沒有StudentB後,B也無法再往Students表中添加StudentB,直至A的事務提交。屆時,B再讀取Books表時,也能發現A提交的BookA,進而正確新增StudentB下擁有BookA這一關聯記錄。
本例雖以多對多關聯關係為例,其實在一對多關聯關係中也可能存在類似問題。
例1呈現出來的情境可以總結為:
結論3:當資料一致性和商務邏輯完整性只能由自己單方面保證時,且自己利用了資料的某種單調性增量處理資料時,需使用select ... in share mode查詢更新資料。
例2呈現出來的情境可以總結為:
結論4:當有關聯關係的兩個實體可能同時新增時,一方因新增實體修改關聯關係,需使用select ... in share mode查詢另一方資料進行關聯關係的更新。
2.3 select ... 快照讀有那麼危險嗎?
看了上面的介紹,大家可能恨不得所有查詢都使用最嚴格的select ... for update,這樣至少不會錯。但是作為最常見的普通select語句,真的有那麼危險嗎?
快照讀意味著讀取曆史資料,其實把時間放長遠了看,基本上絕大部分資料後續都有更新的可能。所以即便是使用最嚴格的select ... for update讀模數式,讀到的資料也終究抵不過時間的流逝,淪為曆史資料。使用者更多關注的並不是某份資料有多新,而是某份資料不要太過時,快照讀讀取的曆史資料通常也就是最近幾十毫秒到幾秒前的曆史版本,完全能夠滿足使用者的查看需求。
當讀取資料是為了後台嚴格的邏輯控制判定時,我們會擔心讀取過程中出現的更新版本的資料會錯過本次事務中的處理邏輯,但是這個擔心一般來說也是多餘的,因為別人產生新版本的資料時,必然也會觸發一系列的處理來保證資料的一致性和商務邏輯的完整性,不必在自己的事務中過於操心別人的事情。
三、總結
我們的原則通常是,優先使用鎖範圍小的查詢模式,以盡量提升資料庫的並發效能。即先選select ... ,不行再用select ... in share mode,再不行再提升為select ... for update。而結論1告訴我們何時無需用select ... for update,在此原則下,我們需要搞清楚的是何時需要用select ... for update,所以這個結論可以忽略。
我們的日常開發中,大部分情況下不需要自己單方面保證資料的一致性和商務邏輯的完整性,所有資料的修改方都可以通力合作。所以結論3可以暫時忽略。
所以,日常開發過程中,我們需記住:
1. 優先使用select ...
2. 當有關聯關係的兩個實體可能同時新增時,一方因新增實體修改關聯關係,需使用select ... in share mode查詢另一方資料進行關聯關係的更新。
3. 如果讀取出來的資料需要修改後再提交,需使用select ... for update讀取資料。
如果你不幸需要與第三方系統(或難以修改的遺留系統)以資料庫的方式進行整合時,需再多記住一點:
4. 當資料一致性和商務邏輯完整性只能由自己單方面保證時,且自己利用了資料的某種單調性增量處理資料時,需使用select ... in share mode查詢更新資料。
如果還有其他漏掉的情境規則,歡迎大家補充。
資料庫的讀鎖和寫鎖在業務上的應用情境總結