背景
樂觀鎖普通用在OLTP系統(tǒng)中就解決高并發(fā)問題,樂觀鎖如果用的不好出現(xiàn)錯位的時候,定位時間一般都比較久(通常你要你列出所有更新線程進(jìn)行分析)。
這篇文章進(jìn)行如下闡述:
1.對樂觀鎖的概念進(jìn)行大致闡述。
2.樂觀鎖在實踐中的已經(jīng)用
3.樂觀鎖使用規(guī)范
什么是樂觀鎖
悲觀鎖和樂觀鎖,其概念名稱來自于其數(shù)據(jù)被外界改變所抱有的代碼。
悲觀鎖對外部系統(tǒng)的改變抱有保守的態(tài)度及不允許外界修改我剛讀取的數(shù)據(jù),在整個數(shù)據(jù)處理過程中數(shù)據(jù)處于鎖定的狀態(tài)。
而樂觀鎖是覺得數(shù)據(jù)被外界改變是一個無所謂的心態(tài)。
悲觀鎖一般是通過數(shù)據(jù)庫的 select * from table where id = '$(id)' for update /或者for update nowait來實現(xiàn),hibernate 可以在應(yīng)用層使用悲觀鎖但是你如果查看其運行時SQL語句,它也是通過for update來實現(xiàn)的。
現(xiàn)在我們進(jìn)行具體的場景分析,假設(shè)有數(shù)據(jù)data1, 線程1:thread1,線程2:thread2,樂觀鎖其具體場景如下:
當(dāng)thread1查詢到data1的時候并不占為己有,thread1不擔(dān)心或者不care這條數(shù)據(jù)是否被別的線程修改,只有thread1需要操作data1的時候才去鎖住data1且更新。
其具體偽代碼如下:
語句Q:Object data1 = dao.queryById;
...done something...// 這里一定不要做更新data1的操作
語句U:dao.update(data1) // 更新語句的SQL一般是這樣寫的,update dataTable set amount = newAmount version = version+1 where version = $(version) and id = $(id)
實際線上可能出現(xiàn)以下的執(zhí)行序列:
thread1: 語句Q
thread2:語句Q
thread1:語句U
thread2:語句U //
在這樣場景下,“thread2:語句U”執(zhí)行會失敗,且語句Q與語句U之間執(zhí)行時間越短(性能提升),樂觀鎖沖突的幾率越小。
樂觀鎖在實踐中的應(yīng)用
當(dāng)使用樂觀鎖時單個線程不會獨占數(shù)據(jù)資源(真正更新時才鎖),這樣整個系統(tǒng)并發(fā)處理效率就提升了。
下面對一次更新場景和多次更新場景進(jìn)行分別闡述:
a).同業(yè)務(wù)類型的意思是更新表的操作都是同類型的業(yè)務(wù),比如金融系統(tǒng)中前臺通知和后臺通知,通知可能調(diào)用多次但是只要有一次調(diào)用成功把訂單狀態(tài)更新成功就可以了。
b).不同業(yè)務(wù)類型場景來說,業(yè)務(wù)1和業(yè)務(wù)1都需要更新record,如果兩個都更新成功最好。如果出現(xiàn)其中一個業(yè)務(wù)失敗,那么需要額外的不就措施,比如重試或者把失敗的存下來通過定時任務(wù)來執(zhí)行。
不同業(yè)務(wù)類型場景
我們在實際中應(yīng)該根據(jù)具體的場景來合理的使用樂觀鎖,比如在金融系統(tǒng)中,由于網(wǎng)絡(luò)的不確定性銀行/支付寶/微信有時會批量發(fā)送一樣的后臺通知,在此場景下我們?nèi)绾胃掠唵螤顟B(tài)和金額呢?
我們會進(jìn)行查詢訂單和更新訂單的動作,
//語句Q1 :查詢 非成功的訂單,進(jìn)行狀態(tài)更新操作;一下語句Q1是查詢動作,語句U1是更新動作
select * from charge where charge_id = ${chargeId} and status=${status}
//語句U1:更新
update charge set status = ${success/failed} ,version= version +1 where charge_id = ${chargeId} and version = ${version}
為什么語句Q1中查詢需要帶上status=${status}參數(shù)呢?請看如下執(zhí)行序列:
thread1:語句Q1
thread1:語句U1
thread2:語句Q1
thread2:語句U1
如果這兩個thread都是由于銀行后臺通知出發(fā),那么chargeId這條數(shù)據(jù)會被更新兩次,而我們需要的場景是只對 “非最終狀態(tài),success/fail”進(jìn)行更新。
當(dāng)然你也可以把全部數(shù)據(jù)查詢出來判斷狀態(tài)對不對然后在更新。
同業(yè)務(wù)類型場景
下面我們談一談一次只有一次調(diào)用樂觀鎖更新失敗的場景,其場景如下:
table1:有字段amount,status,version字段。
table2:有字段totalAmount,version字段。
如果執(zhí)行語句1:select * from table1 where id=${id}, update table1 set amount=${amount} , staus=${success} 成功,但是執(zhí)行語句2 :select * from table2 where id=${id},update table2 set totalAmount+=${amount} ,version =version+1 where version=${version}失敗時如何處理?
我們有以下幾個辦法:
1.同步重試語句2,但是如果一直失敗會導(dǎo)致一直重試。
2.另起一個定時任務(wù),通過一個標(biāo)識掃描table1未更新到table2的記錄,執(zhí)行定時更新。
3.table2很類似金融系統(tǒng)中的賬戶表,在金融系統(tǒng)中table2中可能存在熱點賬戶的問題。而熱點賬戶的問題其實就是通過把update語句轉(zhuǎn)換成insert 表中插入到 table2_hotTable表中,把更新都table2/account1變成insert table2_hottable/account1操作,就不會存在行鎖鎖住table2/account1的問題。
其具體表現(xiàn)如下:
a)table2的數(shù)據(jù)
account1 50¥ ......
account2 100¥
2).table2_hotTable 的數(shù)據(jù)
account1 2¥ notCaculated
account1 3¥ notCaculated
account1 5¥ notCaculated
這樣當(dāng)凌晨空閑的時候,我們通過定時任務(wù)把table2_hotTable表中的數(shù)據(jù)修正到table2中。
當(dāng)然在計算查詢account1 賬戶余額的時候,要把table2和table2_hotTable兩個表的數(shù)據(jù)全部計算進(jìn)來。
樂觀鎖的使用規(guī)范
上面我們說到在同一個線程中 語句Q和語句U之間不要有其他更新動作,不然會導(dǎo)致version不對更新錯誤。
對于有version字段的表,我們應(yīng)該對更新操作規(guī)范起來,讓其只有一個入口被調(diào)用。
樂觀鎖相對于悲觀鎖性能提升但是代碼上相對維護(hù)難度比較大,比較好的辦法是把Query和Update操作綁定作為一個整體,或者明顯提示后來代碼維護(hù)者不要再其中間插入其他更新該表的代碼。
另外只有一個入口的話,我們可以通過集中的日志查看其version和數(shù)據(jù)的變化情況。對于開放的設(shè)計必須在其他渠道給與監(jiān)管,在這里想到了共享單車是很開放可以隨意停放但是也會造成隨意占用公共空間的問題。
樂觀鎖在其它場景下的應(yīng)用
樂觀鎖不僅僅用于用戶庫中,而且用于緩存、ElasticSearch、JDK集合框架比如LOCK鎖的實現(xiàn)。
說在后面的話
討論技術(shù)問題一定是先大體描述其場景,然后對其場景進(jìn)行分析。當(dāng)面對含糊的場景時,一定是你偷懶不想一個個場景進(jìn)行闡述。
多用語言描述寫下來,把你的想法寫下來。語言不一定能夠全部表達(dá)你的思維,但是能夠反作用你的想法,使你的思維更清晰。