你真的瞭解樂觀鎖和悲觀鎖嗎
前言
我在網上查閱了很多關於樂觀鎖和悲觀鎖的介紹和使用場景,大都用這樣的一句話來概括了
- 悲觀鎖:比較適合寫入操作比較頻繁的場景,如果出現大量的讀取操作,每次讀取的時候都會進行加鎖,這樣會增加大量的鎖的開銷,降低了系統的吞吐量。
- 樂觀鎖:比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,資料發生衝突的可能性就會增大,為了保證資料的一致性,應用層需要不斷的重新獲取資料,這樣會增加大量的查詢操作,降低了系統的吞吐量。
但是隨著進一步的思考,我對這些結論又產生不同的看法,下面我就說說我對這個概念的理解和看法。
悲觀鎖
你和你老婆都去銀行取錢,一個賬戶裡只有5000元,然後使用`select for update`可以避免丟失更新。但是這個時候,你查詢餘額的時候接了一個電話,打了30分鐘。在這30分鐘內,你啥事都沒幹,這樣你的賬戶相當於被鎖住了,你老婆得等你,不僅你老婆要等你,這個時候,你的商業合作伙伴要給你打錢,你的整個賬戶都處於鎖定狀態,所有依賴於你的賬戶的修改操作,都得暫停,因為你的賬戶在這個過程一直被排它鎖佔領者,這樣你的賬戶在這30分鐘內,處於癱瘓狀態,除了你獨佔的事物中可以操作,其他事物全部癱瘓,如果併發很大,你的賬戶是一個業務很繁忙的賬戶(連鎖商店統一掃碼收款賬戶),很多收款業務都無法進行。
在`select for update`所在的事物中select的資料,是被你這個事物獨佔的,如果該事物耗時很長,並且,你鎖定的行又處在很多大併發的寫操作中,麻煩就來了,這種`select for update`的鎖定方式,防止丟失更新,表現形式上,是一種悲觀鎖定方式,它在操作的時候,必須要獨佔資料,它總認為有人會來修改它,很悲觀猶如驚弓之鳥,很有可能是,他10000次操作,只有1次,出現了有人修改,9999次,都沒人動,但是他依然很悲觀,採用了獨佔式鎖定。
樂觀鎖
都用過svn,我們從svn伺服器中下載了檔案到本地,然後對他進行修改,然後提交回svn。如果有很多人同事進行這個工作,按理說,會發生丟失更新的風險。但是svn很巧妙的解決了這個問題,採用版本機制。你從svn下載一個檔案,加入,這個檔案當前的版本號是1,你在本地做了修改,然後提交。提交的時候,把你原始的版本號一起提交到svn,svn在修改的時候,首先對比一下你提交過來的版本號和當前伺服器中該檔案的版本號是否一致。如果不一致說明你在本地修改的時候,有其他人提交了修改,這時候駁回你的修改,給你衝突提示。如果一致進行替換,並將版本號+1,這樣的機制有什麼好處?我們來想下,由於我們採用的是版本號機制,這樣我們在本地修改的時候,伺服器中的資料並沒有鎖定,人和人都可以併發的修改,並增加版本號。
回到我們取款的例子,你可以打30分鐘的電話,沒關係,但是你的賬戶並沒有鎖定,其他轉賬業務,收款業務,都可以正常進行。你老婆也可以正常收款,只不過,你在取款的時候,一旦發現你的版本號和你當前賬戶的版本號不一致,駁回你的請求,當你要修改餘額的時候,請你把版本號帶過來。也就是說,在這種機制下,隻影響了你,但是解放了他人,讓你去死,卻救活了他人。成千上萬的賬戶修改成功了,在這30分鐘內,發生了5000次收款2000比轉賬,代價只是你取錢不成功。而悲觀鎖下,為了保證你取款成功,拒絕了5000比收款2000比轉賬,就為了滿足你的私慾。
# 思考 我們為之振奮的`select for update`陷入了巨大的危機。但是我們要知道,資料庫中實現樂觀鎖的資料版本,沒你想的那麼容易,剛才都只是在高談闊論,沒你想的那麼容易,很多人會說,不就加一個版本號嗎?看我的
<pre> update account set money = money-100, version = version + 1 where aid = 001 and version = oldversion </pre>
然後返回受影響的行數進行判斷,完全正確,沒問題!但是如果再問一個複雜的,如果我要你查詢餘額,對餘額大於10000的賬戶,增加100個積分.你查詢了餘額,修改的卻不是餘額,你該怎麼設計。其實是一樣的,修改積分表的時候,把餘額的老本版號帶過來,如果一致進行積分修改,否則不修改積分,對比新的餘額版本號如下: <pre> update jifenTable set jifen = jifen + 100 where jid = 001 and exists(select 1 from account where aid = 001 and version = oldversion) </pre>
好了,看到了沒,樂觀鎖有一個很大的問題 1. 程式碼複雜,設計上有挑戰 2. 對於客戶端版本號不一致的衝突提示
而對於悲觀鎖,只需要一個`select for update`,簡單直觀,不容易出錯,樂觀鎖設計不當,容易出錯。
樂觀鎖還有一個更大的問題,那就是,有些時候,客戶端允許容忍一定的系統響應慢,也不容忍操作半天,居然給我提示一個操作不成功。但是悲觀鎖總會成功,而樂觀鎖,存在失敗的可能性,如果說改樂觀鎖的取錢併發很大,採用了樂觀鎖,將會死一大片,可能80%的操作都會失敗。假設我開了一個全國連鎖的超市都用的一個收款碼,有100個人同時對該賬戶進行付款,這一瞬間內,有80個人失敗,20個人成功,我們的系統如同噩夢一樣。
# 結論 以上面的取款為例子,如果一個accout賬戶表裡面,只有一個寫業務,就只有取款,而沒有其他併發寫業務。比如轉賬,收款等其他業務,而只有取款存在丟失更新,而轉賬收款是直接的update操作,我們採用悲觀鎖是必須的,直接`select for update`,因為其他的併發取款必須阻塞,這是沒有任何爭議的。
你用樂觀鎖幹嘛?明知道有併發,你用樂觀鎖,不是搗亂嗎?假如,我們除了取款(存在丟失更新),還有收款等大併發的業務,我們就不能鎖定了,一定要放開,讓其他大併發業務能夠正常進行,因為哪些業務都是一個update,不存在丟失更新,但是,這個併發到底有多大才叫大?這個是不好說的,一切看壓力測試下的實際軟體體驗,網路上的,清一色的說併發大,用樂觀,併發小,用悲觀,這是人云亦云。假如,只有取款業務(併發很大,存在丟失更新)你能用了樂觀鎖?一秒鐘來了1000個取款,難道你要999個都提示失敗嗎?併發越大,樂觀鎖導致的後果越嚴重,都是各種提示失敗。
實際上,如果只有丟失更新本身事務的併發,不存在其他的寫,那麼樂觀鎖哪一點比得上悲觀鎖?如果高併發下既有丟失更新,又存在其他寫業務,我們在這裡用悲觀鎖就會出現上文中你和你老婆一起去銀行取錢遇到的轉賬問題,這才是問題的關鍵!