python乐观锁代码实现_Elasticsearch系列—并发控制及乐观锁实现原理
概要
本篇主要介紹一下Elasticsearch的并發(fā)控制和樂觀鎖的實(shí)現(xiàn)原理,列舉常見的電商場(chǎng)景,關(guān)系型數(shù)據(jù)庫的并發(fā)控制、ES的并發(fā)控制實(shí)踐。
并發(fā)場(chǎng)景
不論是關(guān)系型數(shù)據(jù)庫的應(yīng)用,還是使用Elasticsearch做搜索加速的場(chǎng)景,只要有數(shù)據(jù)更新,并發(fā)控制是永恒的話題。
當(dāng)我們使用ES更新document的時(shí)候,先讀取原始文檔,做修改,然后把document重新索引,如果有多人同時(shí)在做相同的操作,不做并發(fā)控制的話,就極有可能會(huì)發(fā)生修改丟失的。可能有些場(chǎng)景,丟失一兩條數(shù)據(jù)不要緊(比如文章閱讀數(shù)量統(tǒng)計(jì),評(píng)論數(shù)量統(tǒng)計(jì)),但有些場(chǎng)景對(duì)數(shù)據(jù)嚴(yán)謹(jǐn)性要求極高,丟失一條可能會(huì)導(dǎo)致很嚴(yán)重的生產(chǎn)問題,比如電商系統(tǒng)中商品的庫存數(shù)量,丟失一次更新,可能會(huì)導(dǎo)致超賣的現(xiàn)象。
我們還是以電商系統(tǒng)的下單環(huán)節(jié)舉例,某商品庫存100個(gè),兩個(gè)用戶下單購買,都包含這件商品,常規(guī)下單扣庫存的實(shí)現(xiàn)步驟 客戶端完成訂單數(shù)據(jù)校驗(yàn),準(zhǔn)備執(zhí)行下單事務(wù)。
客戶端從ES中獲取商品的庫存數(shù)量。
客戶端提交訂單事務(wù),并將庫存數(shù)量扣減。
客戶端將更新后的庫存數(shù)量寫回到ES。
示例流程圖如下:
如果沒有并發(fā)控制,這件商品的庫存就會(huì)更新成99(實(shí)際正確的值是98),這樣就會(huì)導(dǎo)致超賣現(xiàn)象。假定http-1比http-2先一步執(zhí)行,出現(xiàn)這個(gè)問題的原因是http-2在獲取庫存數(shù)據(jù)時(shí),http-1還未完成下單扣減庫存后,更新到ES的環(huán)節(jié),導(dǎo)致http-2獲取的數(shù)據(jù)已經(jīng)是過期數(shù)據(jù),后續(xù)的更新肯定也是錯(cuò)的。
上述的場(chǎng)景,如果更新操作越是頻繁,并發(fā)數(shù)越多,讀取到更新這一段的耗時(shí)越長,數(shù)據(jù)出錯(cuò)的概率就越大。
常用的鎖方案
并發(fā)控制尤為重要,有兩種通用的方案可以確保數(shù)據(jù)在并發(fā)更新時(shí)的正確性。
悲觀并發(fā)控制
悲觀鎖的含義:我認(rèn)為每次更新都有沖突的可能,并發(fā)更新這種操作特別不靠譜,我只相信只有嚴(yán)格按我定義的粒度進(jìn)行串行更新,才是最安全的,一個(gè)線程更新時(shí),其他的線程等著,前一個(gè)線程更新完成后,下一個(gè)線程再上。
關(guān)系型數(shù)據(jù)庫中廣泛使用該方案,常見的表鎖、行鎖、讀鎖、寫鎖,依賴redis或memcache等實(shí)現(xiàn)的分布式鎖,都屬于悲觀鎖的范疇。明顯的特征是后續(xù)的線程會(huì)被掛起等待,性能一般來說比較低,不過自行實(shí)現(xiàn)的分布式鎖,粒度可以自行控制(按行記錄、按客戶、按業(yè)務(wù)類型等),在數(shù)據(jù)正確性與并發(fā)性能方面也能找到很好的折衷點(diǎn)。
樂觀并發(fā)控制
樂觀鎖的含義:我認(rèn)為沖突不經(jīng)常發(fā)生,我想提高并發(fā)的性能,如果真有沖突,被沖突的線程重新再嘗試幾次就好了。
在使用關(guān)系型數(shù)據(jù)庫的應(yīng)用,也經(jīng)常會(huì)自行實(shí)現(xiàn)樂觀鎖的方案,有性能優(yōu)勢(shì),方案實(shí)現(xiàn)也不難,還是挺吸引人的。
Elasticsearch默認(rèn)使用的是樂觀鎖方案,前面介紹的_version字段,記錄的就是每次更新的版本號(hào),只有拿到最新版本號(hào)的更新操作,才能更新成功,其他拿到過期數(shù)據(jù)的更新失敗,由客戶端程序決定失敗后的處理方案,一般是重試。
ES的樂觀鎖方案
我們還是以上面的案例為背景,若http-2向ES提交更新數(shù)據(jù)時(shí),ES會(huì)判斷提交過來的版本號(hào)與當(dāng)前document版本號(hào),document版本號(hào)單調(diào)遞增,如果提交過來的版本號(hào)比document版本號(hào)小,則說明是過期數(shù)據(jù),更新請(qǐng)求將提示錯(cuò)誤,過程圖如下:
使用內(nèi)置_version實(shí)戰(zhàn)樂觀鎖控制效果
我們?cè)趉ibana平臺(tái)上模擬兩個(gè)線程修改同一條document數(shù)據(jù),打開兩個(gè)瀏覽器標(biāo)簽即可,我們使用原有的案例數(shù)據(jù):{
"_index": "music",
"_type": "children",
"_id": "2",
"_version": 2,
"found": true,
"_source": {
"name": "wake me, shark me",
"content": "don't let me sleep too late, gonna get up brightly early in the morning",
"language": "english",
"length": "55"
}
}
當(dāng)前的version是2,我們使用一個(gè)瀏覽器標(biāo)簽頁,發(fā)出更新請(qǐng)求,把當(dāng)前的version帶上:POST /music/children/2?version=2
{
"doc": {
"length": 56
}
}
此時(shí)更新成功{
"_index": "music",
"_type": "children",
"_id": "2",
"_version": 3,
"result": "updated",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 2,
"_primary_term": 2
}
同時(shí)我們?cè)诹硪粋€(gè)標(biāo)簽頁上,也使用version=2進(jìn)行更新,得到的錯(cuò)誤結(jié)果如下:{
"error": {
"root_cause": [
{
"type": "version_conflict_engine_exception",
"reason": "[children][2]: version conflict, current version [3] is different than the one provided [2]",
"index_uuid": "9759yb44TFuJSejo6boy4A",
"shard": "2",
"index": "music"
}
],
"type": "version_conflict_engine_exception",
"reason": "[children][2]: version conflict, current version [3] is different than the one provided [2]",
"index_uuid": "9759yb44TFuJSejo6boy4A",
"shard": "2",
"index": "music"
},
"status": 409
}
關(guān)鍵錯(cuò)誤信息:versionconflictengine_exception,版本沖突,將version升到3,模擬失敗后重試,此時(shí)更新成功。
真實(shí)的場(chǎng)景,重試的次數(shù)跟線程并發(fā)數(shù)有關(guān),線程越多,更新越頻繁,就可能需要重試多次才可能更新成功。
使用外部_version實(shí)戰(zhàn)樂觀鎖控制效果
ES允許不使用內(nèi)置的version進(jìn)行版本控制,可以自定義使用外部的version,例如常見的使用Elasticsearch做數(shù)據(jù)查詢加速的經(jīng)典方案,關(guān)系型數(shù)據(jù)庫作為主數(shù)據(jù)庫,然后使用Elasticsearch做搜索數(shù)據(jù),主數(shù)據(jù)會(huì)同步數(shù)據(jù)到Elasticsearch中,而主數(shù)據(jù)庫并發(fā)控制,本身就是使用的樂觀鎖機(jī)制,有自己的一套version生成機(jī)制,數(shù)據(jù)同步到ES那里時(shí),直接使用更方便。
請(qǐng)求語法上加上version_type參數(shù)即可:POST /music/children/2?version=2&version_type=external
{
"doc": {
"length": 56
}
}
唯一的區(qū)別內(nèi)置version,只有當(dāng)你提供的version與es中的version完全一樣的時(shí)候,才可以進(jìn)行更新,否則報(bào)錯(cuò);
外部version,只有當(dāng)你提供的version比es中的version大的時(shí)候,才能完成修改。
Replica Shard數(shù)據(jù)同步并發(fā)控制
在Elasticsearch內(nèi)部,每當(dāng)primary shard收到新的數(shù)據(jù)時(shí),都需要向replica shard進(jìn)行數(shù)據(jù)同步,這種同步請(qǐng)求特別多,并且是異步的。如果同一個(gè)document進(jìn)行了多次修改,Shard同步的請(qǐng)求是無序的,可能會(huì)出現(xiàn)”后發(fā)先至”的情況,如果沒有任何的并發(fā)控制機(jī)制,那結(jié)果將無法相像。
Shard的數(shù)據(jù)同步也是基于內(nèi)置的_version進(jìn)行樂觀鎖并發(fā)控制的。
例如Java客戶端向Elasticsearch某條document發(fā)起更新請(qǐng)求,共發(fā)出3次,Java端有嚴(yán)謹(jǐn)?shù)牟l(fā)請(qǐng)求控制,在ElasticSearch的primary shard中寫入的結(jié)果是正確的,但Elasticsearch內(nèi)部數(shù)據(jù)啟動(dòng)同步時(shí),順序不能保證都是先到先得,情況可能是這樣,第三次更新請(qǐng)求比第二次更新請(qǐng)求先到,如下圖:
如果Elasticsearch內(nèi)部沒有并發(fā)的控制,這個(gè)document在replica的結(jié)果可能是text2,并且與primary shard的值不一致,這樣肯定錯(cuò)了。
預(yù)期的更新順序應(yīng)該是text1–>text2–>text3,最終的正確結(jié)果是text3。那Elasticsearch內(nèi)部是如何做的呢?
Elasticsearch內(nèi)部在更新document時(shí),會(huì)比較一下version,如果請(qǐng)求的version與document的version相等,就做更新,如果document的version已經(jīng)大于請(qǐng)求的version,說明此數(shù)據(jù)已經(jīng)被后到的線程更新過了,此時(shí)會(huì)丟棄當(dāng)前的請(qǐng)求,最終的結(jié)果為text3。此時(shí)的更新順序?yàn)閠ext1–>text3,最終結(jié)果也是對(duì)的。
小結(jié)
本篇主要介紹并發(fā)場(chǎng)景出現(xiàn)數(shù)據(jù)錯(cuò)亂的原因,Elasticsearch樂觀鎖的實(shí)原理,以及ES內(nèi)部數(shù)據(jù)同步時(shí)的并發(fā)控制,有不正確之處或未詳盡之處請(qǐng)知會(huì)修改,謝謝。
專注Java高并發(fā)、分布式架構(gòu),更多技術(shù)干貨分享與心得,請(qǐng)關(guān)注公眾號(hào):Java架構(gòu)社區(qū)
https://juejin.im/post/5de8437d6fb9a0166316c02e
總結(jié)
以上是生活随笔為你收集整理的python乐观锁代码实现_Elasticsearch系列—并发控制及乐观锁实现原理的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 试管婴儿要大概多少钱
- 下一篇: python如何创建函数对列表里的元素进