最近在遷移代碼,遷移過程中發現了一段應用了樂觀鎖的代碼(這是偽代碼):
public boolean doWork(final int count, Entity entity) throws InterruptedException { if (3 == count) { return false; } int j = dao.update(entity); if (j == 0) { Thread.sleep(100); count++; Entity entity = dao.selectEntity(entity.id); return doWork(count, entity); } return true; }
大概意思是先執行更新。更新對應的表有一個version字段,entity的實例中是包含當前版本號的,更新時版本號匹配則返回true;如果不匹配則去數據庫查詢新的版本號,如果重試達到三次則報錯。其實遞歸次數不多本來這也沒什么問題,但是問題在於這段代碼在一個事務中,而事務用的是默認的隔離級別,關鍵是數據庫用的是mysql,和大多數數據庫有點不一樣,它用的默認隔離級別是可重復讀(REPEATABLE_READ)。
於是問題出現了,可重復讀的隔離級別中,除非更新操作執行成功,否則同一個事務對同一條記錄的讀取返回總是一樣的,也就是說,這個版本號如果出現不匹配,那后面三次重試一定不會成功,因為重新讀出來的版本號就不是最新的,根本就沒變:
具體的測試代碼在:https://github.com/saaavsaaa/warn-report/blob/master/src/test/java/DBTest.java。
也就是說這個樂觀鎖白寫了。。。
其實樂觀鎖主要是應對並發問題,減少數據庫的排隊的手段,其實更多是應用在拋開事務的場景下來保證執行正確的。不過說並發其實也抗不了多少並發,就算不用事務,這種使用數據庫去抗所有處理的方式。。。,其實我更喜歡用Redis解決類似情況,因為單線程、因為快、因為減少持久化存儲的壓力,數據庫其實是互聯網應用中相對最經不起故障的重要部分。將計數保存在Redis中,所有操作都只對Redis的值進行操作,發現Redis中無值,通知同步服務同步數據到Redis中,然后再計算服務從Redis取值計算,不過要注意同步服務需要判斷Redis有值了,就不執行寫操作了。
另外,降低隔離級別也可以解決這個用例的問題(其實讀已提交級別是其他大多數數據庫的默認隔離級別),下面方法僅供參考,上生產出問題不負責。。。,下面是讀未提交的測試中默認5.6版本的庫需要改的配置:http://www.cnblogs.com/saaav/p/5943393.html,spring當中有七種事務傳播規則:PROPAGATION_REQUIRES_NEW 新建事務,如果當前存在事務,把當前事務掛起。(兩個事務互補干擾 各自管理各自的事務),可以對樂觀鎖方法使用這個配置,其他事務使用默認級別,降低風險,雖然我也不知道有什么風險,目前也沒時間深入研究,但是數據庫開發團隊既然決定使用這個默認級別,應該是有什么原因。。。
那個測試類里還有一個測試並發的方法testConcurrentUpdate,我機器配置一般,更新並發超過1100就有失敗的了,1500的時候失敗已經過半了,大部分是鎖超時,不過並發寫到這個程度一般也不會用數據庫生抗了,所以細節就不說了,知道有這么個事就成了。
==========================================================
咱最近用的github:https://github.com/saaavsaaa
微信公眾號:
轉載請注明出處