事務(wù)與鎖的愛恨情仇-msyql更新丟失問題的解決方案

問題背景

一個(gè)砍價(jià)助力功能,用戶下完單后可以邀請好友砍價(jià)。

問題描述

在好友砍價(jià)過程中記錄好友砍價(jià)人數(shù),是先從數(shù)據(jù)庫查詢已砍人數(shù)然后加一再更新數(shù)據(jù)庫的方式實(shí)現(xiàn)的崭歧。如果有并發(fā)砍價(jià)情況出現(xiàn)隅很,后一個(gè)事務(wù)更新的已砍人數(shù)便會覆蓋前一個(gè)事務(wù)更新的已砍人數(shù)(更新丟失問題)撞牢。

問題分析

在mysql默認(rèn)的事務(wù)隔離級別(可重復(fù)讀)中事務(wù)查詢數(shù)據(jù)是不加鎖的,只有更新數(shù)據(jù)才加排他鎖叔营。并發(fā)情況下屋彪,倘若出現(xiàn)兩個(gè)事務(wù)都查詢了數(shù)據(jù)后,其中一個(gè)事務(wù)才去獲取這條數(shù)據(jù)的排它鎖去更新數(shù)據(jù)的情況绒尊,就會出現(xiàn)更新丟失畜挥。如下:
事務(wù)100和事務(wù)101同時(shí)開啟并同時(shí)查詢了同一條數(shù)據(jù),然后再這條數(shù)據(jù)的某個(gè)值上+1再去獲取這條數(shù)據(jù)的X鎖去更新數(shù)據(jù)婴谱,其中事務(wù)100搶占到了鎖蟹但,事務(wù)100先將數(shù)據(jù)的這個(gè)值更改,然后事務(wù)100提交后釋放了這條數(shù)據(jù)的鎖谭羔,此時(shí)事務(wù)101經(jīng)過等待獲取了這條數(shù)據(jù)的X鎖华糖,然后再去更新這條數(shù)據(jù)的那個(gè)值,更新完成后我們發(fā)現(xiàn)瘟裸,事務(wù)101的更新覆蓋了事務(wù)100的更新客叉,這就是更新丟失問題。

我本以為如果使用事務(wù)的話话告,每個(gè)事務(wù)都會一一獲取事務(wù)中相關(guān)記錄所有的鎖后才開始執(zhí)行事務(wù)的兼搏,這樣如果并發(fā)請求的話第一個(gè)事務(wù)優(yōu)先獲取要更新記錄的x鎖,第二個(gè)事務(wù)雖然先查詢但也無法獲取s鎖沙郭。顯然不是這樣的佛呻,(倘若是這樣的,豈不是變成了順序執(zhí)行)病线。這說明我對數(shù)據(jù)庫鎖在事務(wù)中的運(yùn)用理解還不深刻件相。以mysql為例再扭,在可重復(fù)讀的隔離級別下,一個(gè)事務(wù)中的查詢并不會獲取該記錄的共享鎖(S鎖),而是采用mvcc實(shí)現(xiàn)了類似樂觀鎖的機(jī)制夜矗。所以還是會出現(xiàn)更新丟失泛范。

解決方法

雖然用上了事務(wù),但是更新丟失的問題依然還沒解決(除非數(shù)據(jù)庫事務(wù)隔離級別設(shè)置成序列化紊撕,但是嚴(yán)重影響性能罢荡,相當(dāng)于順序執(zhí)行,沒法并發(fā)对扶,顯然這樣是不行的)区赵。所以我們這種情況經(jīng)常用前兩種解決方案

  • 悲觀鎖實(shí)現(xiàn) 查詢時(shí)加鎖(select .....for update),這種方案使用悲觀鎖,預(yù)設(shè)每次更新都會有別的事務(wù)在更新此數(shù)據(jù)浪南,查詢時(shí)就上鎖笼才,這種方案固然能解決問題且容易實(shí)現(xiàn)。但是其實(shí)大部分情況下都不會發(fā)生并發(fā)络凿,所以這種方法無疑浪費(fèi)了資源骡送。但是在高爭用條件下還是值得推薦的。
  • 樂觀鎖實(shí)現(xiàn): 數(shù)據(jù)記錄加版本標(biāo)記絮记,每次更新版本加1摔踱,更新時(shí)where條件后跟上查詢出的版本號,這樣如果此時(shí)版本號被別的事務(wù)更新過怨愤,那么更新條數(shù)就會返回0派敷,根據(jù)這個(gè)更新條數(shù)來判斷后續(xù)操作。這樣做增加了程序的復(fù)雜性撰洗,但是提高了并發(fā)效率篮愉,在資源低爭用條件下下值得推薦。
  • 隔離級別實(shí)現(xiàn): 此外很多網(wǎng)上的文章介紹可以將數(shù)據(jù)庫事務(wù)隔離級別設(shè)置為REPEATABLE-READ來避免更新丟失差导。但是這種解決方案大多數(shù)數(shù)據(jù)庫下是不生效的试躏,至少在mysql里是不生效的,標(biāo)準(zhǔn)的REPEATABLE-READ只保證在同一事務(wù)中的可重復(fù)讀柿汛,并沒有保證更新不丟失冗酿。或許有些許數(shù)據(jù)庫或某些版本的數(shù)據(jù)庫實(shí)現(xiàn)可重復(fù)讀的方案是在select 上也加共享鎖(S鎖)直到事務(wù)結(jié)束釋放络断,這樣確實(shí)可以避免更新丟失裁替。但是mysql的實(shí)現(xiàn)方案是mvvc并不能避免更新丟失。mysql可以將事務(wù)隔離級別設(shè)置為Serializable可避免更新丟失貌笨,但是這樣數(shù)據(jù)庫的效率變得很低弱判。

引申 事務(wù)隔離性的實(shí)現(xiàn)原理

本節(jié)主要引申討論事務(wù)的隔離性實(shí)現(xiàn),重點(diǎn)討論鎖在事務(wù)中的運(yùn)用锥惋。首先有幾個(gè)基本概念需要先理解下:
數(shù)據(jù)庫的鎖一般有兩種維度的定義昌腰,按獨(dú)享和共享分為以下類型

  • Exclusive Locks(排它鎖/X鎖)顧名思義這種鎖加在數(shù)據(jù)上开伏,別的事務(wù)不能再次加別的任何鎖了。
  • Shared Locks(共享鎖/S鎖) 此種鎖可以加在另外的已有s鎖的數(shù)據(jù)上

按鎖的粒度分為以下鎖:

  • table locks(表鎖) 此鎖是鎖在表級別的使用這種鎖會極大的降低數(shù)據(jù)庫的并發(fā)量遭商。
  • Record Locks(行鎖) 此鎖是鎖在索引行上的固灵,注意是索引行。
  • Gap Locks(間隙鎖) 此種鎖是加載一個(gè)范圍上的 如 where id>1 and id <10 for update
  • Next-Key Locks(間隙鎖) 另外一種間隙鎖劫流,是Gap 鎖和行鎖的結(jié)合巫玻,where id>=1 and id<=10 for update就是使用此種間隙鎖。
  • 讀未提交 READ UNCOMMITTED隔離級別下, 讀不會加任何鎖祠汇。而寫會加X鎖仍秤,直到事務(wù)結(jié)束釋放X鎖。既然寫會加排他鎖可很,那為何別的事務(wù)依然可以讀呢诗力,因?yàn)檫@個(gè)隔離級別下,讀不加任何鎖我抠。
  • 讀已提交 READ COMMITTED顧名思義苇本,事務(wù)之間可以讀取彼此已提交的數(shù)據(jù)。我們當(dāng)然可以在讀上加s鎖(讀完就可以釋放s鎖屿良,不必等到事務(wù)結(jié)束)圈澈,寫加X鎖直到事務(wù)結(jié)束來達(dá)到讀已提交的隔離級別惫周,但是尘惧,如果鎖沖突很頻繁的情況下讀寫不能同時(shí)進(jìn)行會降低數(shù)據(jù)庫的并發(fā)度。mysql采用mvcc的方案來實(shí)現(xiàn)這種隔離級別递递。mvcc機(jī)制可以實(shí)現(xiàn)讀寫并發(fā)執(zhí)行喷橙,稍后重點(diǎn)再說。
  • 可重復(fù)讀 REPEATABLE READ在這種隔離級別下登舞,同一事務(wù)中讀同一條數(shù)據(jù)的結(jié)果是相同的贰逾。我們當(dāng)然也可以使用鎖來實(shí)現(xiàn)這個(gè)特性。select時(shí)加s鎖菠秒,并到事務(wù)結(jié)束釋放疙剑,寫加X鎖直到事務(wù)結(jié)束釋放。這樣就可以實(shí)現(xiàn)可重復(fù)讀了践叠。但是mysql還是使用了mvcc來實(shí)現(xiàn)的言缤,原因和上一個(gè)一樣,鎖沖突較多的情況下并發(fā)性的問題禁灼。
  • 序列化 SERIALISABLE此種隔離級別消除了幻讀管挟。但是代價(jià)也是最大的。幾乎是串行執(zhí)行事務(wù)了弄捕,同時(shí)只能有一個(gè)事務(wù)在執(zhí)行僻孝。這種方案對于互聯(lián)網(wǎng)高并發(fā)的業(yè)務(wù)來說幾乎不可接受导帝。但是mysql在 REPEATABLE READ級別已經(jīng)解決了部分情況的幻讀問題(innodb引擎),我們稍后再看穿铆。

再引申 mvcc多版本并發(fā)控制

mvcc是為了提高事務(wù)的并發(fā)性能而提供的一種解決方案您单,使用mvcc可以在讀的時(shí)候不加鎖就能實(shí)現(xiàn)可重復(fù)讀。mvcc的實(shí)現(xiàn)需要借助兩個(gè)隱藏列一個(gè)是trx_id表示的是這行的數(shù)據(jù)版本荞雏,實(shí)際這個(gè)列存儲的是更新這行數(shù)據(jù)的事務(wù)id睹限,另外一個(gè)rollback_pointer是指向上版本數(shù)據(jù)記錄的指針,mvcc下數(shù)據(jù)被更新時(shí)會copy一份老數(shù)據(jù)到undo日志讯檐,然后在修改這行數(shù)據(jù)并在rollback_pointer里加個(gè)指向老版本數(shù)據(jù)的指針羡疗。
假如有條數(shù)據(jù)初始化如下:

id name trx_id rollback_pointer
1 小明 90

同時(shí)開啟兩個(gè)事務(wù)100 101

1 .事務(wù)100執(zhí)行了查詢語句

select * from user where id =1

首先這個(gè)事務(wù)會建立一個(gè)readview,這個(gè)readview包含當(dāng)前活動的事務(wù)id [100,101]
然后判斷數(shù)據(jù)行中trx_id與readview中事務(wù)id比較

  • 如果當(dāng)前數(shù)據(jù)行中trx_id比readview中所有事務(wù)id都小則說明更新這條數(shù)據(jù)的事務(wù)早已提交,隨意當(dāng)前數(shù)據(jù)對查詢事務(wù)可見别洪。
  • 如果trx_id大于readview中最小事務(wù)id 且小于readview中最大事務(wù)id則但不包含在readview中叨恨,則說明更新這行數(shù)據(jù)的事務(wù)不活躍已提交,當(dāng)前數(shù)據(jù)對查詢數(shù)據(jù)可見挖垛。
  • 如果trx_id大于所有readview中事務(wù)id痒钝,則說明更新數(shù)據(jù)的事務(wù)開啟時(shí)間在當(dāng)前事務(wù)后,所以此時(shí)數(shù)據(jù)對當(dāng)前事務(wù)不可見
  • 如果trx_id在readview中且trx_id不等于當(dāng)前事務(wù)id痢毒,這說明更新這條數(shù)據(jù)的事務(wù)還未提交送矩,則此數(shù)據(jù)不可見。

上述如果數(shù)據(jù)對當(dāng)事務(wù)不可見的情況哪替,會根據(jù)roollback_pointer找到上版本的數(shù)據(jù)栋荸,再次做上述判斷,以此類推凭舶,知道找到可見版本的數(shù)據(jù)為止晌块。

那再來看事務(wù)100查的數(shù)據(jù)trx_id此時(shí)為90,那對事務(wù)100的查詢是可見的帅霜。查詢出的數(shù)據(jù)就是當(dāng)前數(shù)據(jù)匆背。

2.事務(wù)101執(zhí)行了sql

update user set name='小白' where id = 1

那么此時(shí)數(shù)據(jù)便成了這樣:

id name trx_id rollback_pointer
1 小白 101 指向undo舊版本數(shù)據(jù)的指針

undo日志中記錄的舊版本數(shù)據(jù)

id name trx_id rollback_pointer
1 小明 90

3.然后事務(wù)100如果再次執(zhí)行查詢的話,(readview在可重復(fù)讀的隔離級別下首次執(zhí)行select創(chuàng)建身冀,在讀已提交的隔離級別下钝尸,每次select都會創(chuàng)建新的readview)
判斷 當(dāng)前數(shù)據(jù)trx_id(101)在readview中且不等于當(dāng)前事務(wù)id:100,所以該數(shù)據(jù)不可見搂根,根據(jù)rollback_pointer找到undo中舊版本數(shù)據(jù)在此做比較 trx_id(90)<當(dāng)前事務(wù)id100且不在readview中珍促,所以undo中舊版本數(shù)據(jù)對事務(wù)100是可見的。這樣就實(shí)現(xiàn)了可重復(fù)讀兄墅。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末踢星,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子隙咸,更是在濱河造成了極大的恐慌沐悦,老刑警劉巖成洗,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異藏否,居然都是意外死亡瓶殃,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門副签,熙熙樓的掌柜王于貴愁眉苦臉地迎上來遥椿,“玉大人,你說我怎么就攤上這事淆储」诔。” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵本砰,是天一觀的道長碴裙。 經(jīng)常有香客問我,道長点额,這世上最難降的妖魔是什么舔株? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮还棱,結(jié)果婚禮上载慈,老公的妹妹穿的比我還像新娘。我一直安慰自己珍手,他們只是感情好办铡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著珠十,像睡著了一般料扰。 火紅的嫁衣襯著肌膚如雪凭豪。 梳的紋絲不亂的頭發(fā)上焙蹭,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天,我揣著相機(jī)與錄音嫂伞,去河邊找鬼孔厉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛帖努,可吹牛的內(nèi)容都是我干的撰豺。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拼余,長吁一口氣:“原來是場噩夢啊……” “哼污桦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起匙监,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤凡橱,失蹤者是張志新(化名)和其女友劉穎小作,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稼钩,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡顾稀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了坝撑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片静秆。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖巡李,靈堂內(nèi)的尸體忽然破棺而出抚笔,到底是詐尸還是另有隱情,我是刑警寧澤侨拦,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布塔沃,位于F島的核電站,受9級特大地震影響阳谍,放射性物質(zhì)發(fā)生泄漏蛀柴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一矫夯、第九天 我趴在偏房一處隱蔽的房頂上張望鸽疾。 院中可真熱鬧,春花似錦训貌、人聲如沸制肮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽豺鼻。三九已至,卻和暖如春款慨,著一層夾襖步出監(jiān)牢的瞬間儒飒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工檩奠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留桩了,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓埠戳,卻偏偏與公主長得像井誉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子整胃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355