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