熟悉MySQL數據庫的朋友們都知道,查詢數據常見模式有三種:數據庫
1. select ... :快照讀,不加鎖編程
2. select ... in share mode:當前讀,加讀鎖安全
3. select ... for update:當前讀,加寫鎖併發
從技術層面理解三種方式的應用場景其實並不困難,下面咱們先快速複習一下這三種讀取模式的在技術層面上的區別。負載均衡
注:爲了簡化問題的描述,下面全部結論均是針對MySQL數據庫InnoDB儲存引擎RR隔離級別的。分佈式
讀取當前事務開始時結果集的快照版本,快照版本也能夠理解爲歷史版本。性能
由於只需讀取一個歷史版本,而歷史不會被修改,故歷史版本自己就是一個不可變版本,因此本讀取模式對讀取先後的資源處理相對簡單:spa
1. 讀取行爲發生以前,若是有其餘還沒有提交的事務已經修改告終果集,本讀取模式不會等待這些事務結束,天然也讀取不到這些修改。對象
2. 讀取行爲發生以後,當前事務提交以前,本讀取模式也不會阻止其餘事務修改數據,產生更新版本的結果集。blog
讀取結果集的最新版本,同時防止其餘事務產生更新的數據版本。
因爲數據的最新版本是不斷變化的,因此本讀取模式須要強制阻斷最新版本的變化,保證本身讀取到的是全部人都一致承認的名副其實的最新版本。
本讀取模式在讀取先後對資源處理以下:
1. 讀取行爲發生以前,獲取讀鎖。這意味着若是有其餘還沒有提交的事務已經修改告終果集,本讀取模式會等待這些事務結束,以確保本身稍後能夠讀取到這些事務對結果集的修改。
2. 讀取行爲發生以後,當前事務提交以前,本讀取模式會阻塞其餘事務對結果集的修改。
3. 當前事務提交後,釋放讀鎖。這意味着全部以前被阻塞的事務可恢復繼續執行。
本讀取模式擁有select ... in share mode的一切功能,同時它還額外具有阻止其餘事務讀取最新版本的能力。
本讀取模式在讀取先後對資源的處理以下:
1. 讀取行爲發生以前,獲取寫鎖。這意味着若是有其餘還沒有提交的事務已經修改告終果集,本讀取模式會等待這些事務結束,以確保本身稍後能夠讀取到這些事務對結果集的修改。
2. 讀取行爲發生以後,當前事務提交以前,本讀取模式會阻塞其餘事務對結果集的修改,也會阻塞其餘事務對結果集最新版本的讀取(注:其餘事務仍能夠讀取快照版本)。
3. 當前事務提交後,釋放寫鎖。這意味着全部以前被阻塞的事務可恢復繼續執行。
三種讀取模式在技術層面的區別到此就複習完了,但是咱們在實際業務編程過程當中,讀取數據庫中的記錄到底何時要加讀鎖,何時要加寫鎖呢?
讀取快照版本的歷史數據和讀取最新版本的數據映射到業務層面是怎樣的一種業務邏輯需求?難道每寫一處數據庫查詢代碼,都要從技術層面去細細思考不一樣讀取模式其讀取行爲發生以前、以後對資源的處理是否符合業務需求嗎?這樣編程也太辛苦啦。
帶着上述疑問,本文將嘗試從每種讀取模式的技術性功能出發,將不一樣模式下的技術功能差別轉換爲業務需求差別,從而總結出不一樣功能的應用場景,最終產出少數的操做性強的場景斷定規則,用於快速回答不一樣業務場景下查詢數據庫是否應該加讀鎖或寫鎖這一問題。
不過在討論數據庫加鎖的應用場景以前,咱們先弄清楚一個問題,應用層能夠加鎖,數據庫也能夠加鎖,他們之間的功能彷佛有一點重疊,那麼什麼狀況下須要使用數據庫鎖而不是應用層鎖呢?
應用層加鎖,指的是在同一個進程內,經過同步代碼塊(臨界區)、信號量、Lock鎖對象等編程組件,實現併發資源的有序訪問。
理論上來講,數據庫加鎖須要解決的問題,經過應用層鎖都能解決。
可是應用層加鎖最大的侷限在於其做用範圍是單進程內。在分佈式集羣系統盛行的今天,絕大部分模塊都有可能會啓動多個進程實例,以實現負載均衡功能。若是兩個進程併發訪問數據庫,經過進程內的應用層鎖,是沒法將跨進程的多個處理流程協調成有序執行的。
同時咱們也應該認識到,數據庫鎖是稀缺資源,由於儲存着狀態的數據庫難以橫向擴展,幾乎是整個系統的最終瓶頸。而無狀態的計算處理模塊能夠輕鬆的彈性伸縮,一個性能不夠啓動兩個,兩個不夠啓動三個。。。
因此,咱們能夠得出以下結論:
結論1:只會在單進程內造成的資源爭用,進程內部應優先使用應用層鎖本身解決,而不該該將其轉嫁給數據庫鎖(雖然不少時候用巧妙地使用數據庫鎖可能編程更加方便)。數據庫鎖應主要用於解決多進程間併發處理數據庫中的數據時可能造成的混亂。
下面咱們討論的數據庫加鎖應用場景,其間說起的多個事務,均是指的這些事務在不一樣進程中開啓的狀況。
select ... for update相對於select ... in share mode而言,對讀取到的結果集的最新版本具備更強的獨佔性。select ... in share mode只是阻塞其餘事務對結果集產生更新版本,而select .. for update還會阻塞其餘事務對結果集最新版本的讀取。
業務層面在什麼狀況下須要阻塞其餘事務對結果集最新版本的讀取呢?
不想讓別人也能夠讀取到最新版本,每每是由於本身想在最新版本上進行修改,同時擔憂其餘人也和本身同樣。由於你們在修改數據時,老是但願本身的修改與數據的最新版本(而不是歷史版本)合併後存入數據庫中,因此你們在修改數據前,都會嘗試獲取數據的最新版本,基於最新版本進行修改。若是每一個人均可以同時獲取到數據的最新版本並在最新版本上加入本身的修改,最後你們一塊兒提交數據,必然會出現一我的的修改覆蓋了其餘人修改的狀況,這就是經典的「更新丟失」問題。以下圖所示:
其實這個問題還能夠反過來問,什麼狀況下沒必要阻塞其餘事務對結果集的讀取呢?
試想若是不管你阻不阻塞讀取,其餘事務讀取到的結果集都是同樣的,你又何須阻塞它呢?若是你不修改讀取出的結果集,那麼別人早讀晚讀又有什麼區別?
丟失更新問題場景有一種特殊狀況須要特別注意:當你嘗試讀取一條不存在的記錄,確認其確實不存在後,插入該記錄(常見的帶查重的插入操做)。此場景等價於你讀取了某個範圍的結果集,而後要更新此結果集,若是不加寫鎖,判重邏輯可能會失效。
經過上面的思考,咱們能夠得出以下結論:
結論2:若是讀取出的某個範圍的結果集本身不須要修改它,是確定不須要使用select ... for update的。
結論3:若是讀取出的某個範圍的結果集本身須要修改它,此時須要使用select ... for update。
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呈現出來的場景能夠總結爲:
結論4:當數據一致性和業務邏輯完整性只能由本身單方面保證時,且本身利用了數據的某種單調性增量處理數據時,需使用select ... in share mode查詢更新數據。
例2呈現出來的場景能夠總結爲:
結論5:當有關聯關係的兩個實體可能同時新增時,一方因新增實體修改關聯關係,需使用select ... in share mode查詢另外一方數據進行關聯關係的更新。
看了上面的介紹,你們可能巴不得全部查詢都使用最嚴格的select ... for update,這樣至少不會錯。可是做爲最多見的普通select語句,真的有那麼危險嗎?
快照讀意味着讀取歷史數據,其實把時間放長遠了看,基本上絕大部分數據後續都有更新的可能。因此即使是使用最嚴格的select ... for update讀取模式,讀到的數據也終究抵不過期間的流逝,淪爲歷史數據。用戶更多關注的並非某份數據有多新,而是某份數據不要太過期,快照讀讀取的歷史數據一般也就是最近幾十毫秒到幾秒前的歷史版本,徹底可以知足用戶的查看需求。
當讀取數據是爲了後臺嚴格的邏輯控制斷定時,咱們會擔憂讀取過程當中出現的更新版本的數據會錯過本次事務中的處理邏輯,可是這個擔憂通常來講也是多餘的,由於別人產生新版本的數據時,必然也會觸發一系列的處理來保證數據的一致性和業務邏輯的完整性,沒必要在本身的事務中過於操心別人的事情。
咱們的原則一般是,優先使用鎖範圍小的查詢模式,以儘可能提高數據庫的併發性能。即先選select ... ,不行再用select ... in share mode,再不行再提高爲select ... for update。而結論2告訴咱們什麼時候無需用select ... for update,在此原則下,咱們須要搞清楚的是什麼時候須要用select ... for update,因此這個結論能夠忽略。
咱們的平常開發中,大部分狀況下不須要本身單方面保證數據的一致性和業務邏輯的完整性,全部數據的修改方均可以通力合做。因此結論4能夠暫時忽略。
綜上,平常開發過程當中,咱們需記住:
1. 只會在單進程內造成的資源爭用,進程內部應優先使用應用層鎖本身解決,而不該該將其轉嫁給數據庫鎖。數據庫鎖應主要用於解決多進程間併發處理數據庫中的數據時可能造成的混亂。
2. 優先使用select ...
3. 當有關聯關係的兩個實體可能同時新增時,一方因新增實體修改關聯關係,需使用select ... in share mode查詢另外一方數據進行關聯關係的更新。
4. 若是讀取出來的結果集須要修改後再提交,需使用select ... for update讀取結果集。
若是你不幸須要與第三方系統(或難以修改的遺留系統)以數據庫的方式進行集成時,需再多記住一點:
5. 當數據一致性和業務邏輯完整性只能由本身單方面保證時,且本身利用了數據的某種單調性增量處理數據時,需使用select ... in share mode查詢更新數據。
若是還有其餘漏掉的場景規則,歡迎你們補充。