事務(wù)與鎖


多個(gè)事務(wù)的并發(fā)會(huì)出現(xiàn)以下幾個(gè)問題:

1)臟讀:事務(wù)A讀取了一條記錄的值,然后基于這個(gè)值做業(yè)務(wù)邏輯,在事務(wù)A提交之前,事務(wù)B回滾了該記錄,導(dǎo)致事務(wù)A讀到的這條記錄是一個(gè)臟數(shù)據(jù);

2)不可重復(fù)讀:在同一個(gè)事務(wù)里面,兩次讀到同一行記錄,但結(jié)果不一樣,因?yàn)榱硪粋€(gè)事務(wù)在此期間對(duì)該條記錄進(jìn)行update操作;

3)幻讀:在同一個(gè)事務(wù)里面,同樣的select查詢語句執(zhí)行兩次,得到的記錄條數(shù)不一樣,因?yàn)榱硪粋€(gè)事務(wù)在此期間對(duì)表記錄進(jìn)行insert/delete操作;

4)丟失更新:兩個(gè)事務(wù)同時(shí)修改同一條記錄,事務(wù)A的修改被事務(wù)B覆蓋了。

為了解決這些問題,數(shù)據(jù)庫設(shè)置了不同的事務(wù)隔離級(jí)別。包括:

1)讀未提交:

2)讀已提交:只解決了臟讀問題

3)可重復(fù)讀:解決了臟讀、不可重復(fù)讀、幻讀

4)串行化:解決了臟讀、不可重復(fù)讀、幻讀、丟失更新

以上四種隔離級(jí)別一級(jí)比一級(jí)嚴(yán)格,通常讀未提交和串行化不會(huì)被數(shù)據(jù)庫查詢引擎采用,因?yàn)榍罢呤裁锤綦x措施都沒有做,后者將事務(wù)完全按照先后順序依次執(zhí)行,導(dǎo)致數(shù)據(jù)庫的事務(wù)吞吐量太小。


MySQL的InnoDB引擎采用的是可重復(fù)讀隔離級(jí)別。這就導(dǎo)致使用者要自己處理丟失更新問題,例如在下表(tbl)中

user_id balance
1 30
2 80

兩個(gè)事務(wù)操縱同一條記錄,一個(gè)充錢一個(gè)扣錢,偽代碼如下:

# 事務(wù)A
start transaction
    int b = select balance from tbl where user_id = 1
    b = b + 50
    update tbl set balance = b where user_id = 1
commit

# 事務(wù)B
start transaction
    int b = select balance from tbl where user_id = 1
    b = b - 50
    update tbl set balance = b where user_id = 1
commit

在事務(wù)A與B并發(fā)的情況下,由于事務(wù)A與B的多條語句之間可能被交叉執(zhí)行,因此最后balance的值可能是80、-20、30。解決這個(gè)問題有如下的幾種方法:

1)利用單條SQL語句的原子性改寫事務(wù)代碼,例如:

# 事務(wù)A
start transaction
    update tbl set balance = balance + 50 where user_id = 1
commit

# 事務(wù)B
start transaction
    update tbl set balance = balance - 50 where user_id = 1
commit

2)使用悲觀鎖(也就是select xxx for update語句),悲觀鎖在讀記錄之前就上鎖,偽代碼如下:

# 事務(wù)A
start transaction
    int b = select balance from tbl where user_id = 1 for update
    b = b + 50
    update tbl set balance = b where user_id = 1
commit

# 事務(wù)B
start transaction
    int b = select balance from tbl where user_id = 1 for update
    b = b - 50
    update tbl set balance = b where user_id = 1
commit

# 之所以說悲觀鎖降低了事務(wù)的并發(fā)度,是因?yàn)閱蝹€(gè)記錄被鎖住之后,其他訪問該記錄的事務(wù)都會(huì)被阻塞
# 另外,使用悲觀鎖,會(huì)導(dǎo)致死鎖問題(例如事務(wù)A鎖住user_id = 1的記錄后,在Commit之前發(fā)生故障
# 會(huì)導(dǎo)致鎖無法被釋放)

3)使用樂觀鎖,即利用版本號(hào) + CAS操作來控制,這需要對(duì)表加一個(gè)版本號(hào)字段(可以使用version、flag、tag等來命名)

user_id balance version
1 30
2 80

有了版本號(hào)字段,就可以使用下面的偽代碼來規(guī)避丟失更新問題:

# 事務(wù)A
while(!success) {
    start transaction
        int b, v1 = select balance, version from tbl where user_id = 1
        b = b + 50
        int v2 = v1 + 1
        success = update tbl set balance = b, version = v2 where user_id = 1 and version = v1
    commit
}

# 事務(wù)B
while(!success) {
    start transaction
        int b, v1 = select balance, version from tbl where user_id = 1
        b = b - 50
        int v2 = v1 + 1
        success = update tbl set balance = b, version = v2 where user_id = 1 and version = v1
    commit
}

# CAS的核心思想是:記錄讀出來的時(shí)候帶一個(gè)版本號(hào)v1,然后在內(nèi)存里面修改(包括修改業(yè)務(wù)字段和更
# 新版本號(hào),比如更新成v2),當(dāng)再寫回去的時(shí)候,如果發(fā)現(xiàn)版本號(hào)不是v1,說明自己在修改期間有別的
# 事務(wù)修改了該記錄,則放棄當(dāng)前修改。重新讀取記錄,重新修改記錄以及更新版本號(hào),重新對(duì)比版本號(hào),
# 如此不斷重試

# 使用樂觀鎖,即利用版本號(hào) + CAS操作的時(shí)候,有一個(gè)要點(diǎn):version的比較、balance的更新、version
# 的更新要寫在同一個(gè)SQL語句中,使用單條SQL的原子性保證整個(gè)過程的原子性

4)使用分布式鎖,以上方法3)只能解決同一張表的事務(wù)操作場(chǎng)景中的丟失更新問題,但是一個(gè)事務(wù)可能跨表進(jìn)行操作,例如:

start transaction
    select xxx from tbl1
    select yyy from tbl2
    根據(jù)tbl1和tbl2的查詢結(jié)果進(jìn)行邏輯計(jì)算,然后用計(jì)算結(jié)果更新tbl3
    update tbl3
commit

要實(shí)現(xiàn)update tbl3的同時(shí),tbl1和tbl2是鎖住的狀態(tài),不能讓其他事務(wù)修改,這種情況可以考慮使用分布式鎖。


實(shí)現(xiàn)讀寫并發(fā)往往有三種策略:互斥鎖、讀寫鎖、COW

1)互斥鎖:一個(gè)數(shù)據(jù)對(duì)象上面只有一把鎖,任何時(shí)候只能由一個(gè)線程持有鎖,其他線程被阻塞。這種策略能夠?qū)崿F(xiàn)寫寫互斥、讀寫互斥、讀讀互斥;

2)讀寫鎖:一個(gè)數(shù)據(jù)對(duì)象上面只有一把鎖,但是有兩個(gè)視圖${TO-COMPREHENSION}。這種策略能夠?qū)崿F(xiàn)寫寫互斥、讀寫互斥、讀讀并發(fā);

3)COW:CopyOnWrite,寫的時(shí)候,把該數(shù)據(jù)對(duì)象拷貝一份,在拷貝對(duì)象上進(jìn)行寫操作,等寫完之后,再用拷貝對(duì)象代替原始對(duì)象(將引用從原始對(duì)象指向拷貝對(duì)象),讀的時(shí)候讀取原始對(duì)象,這種策略能夠?qū)崿F(xiàn):讀讀并發(fā)、讀寫并發(fā)、寫寫并發(fā)。

以上三種策略,并發(fā)度越來越高。MySQL中的InnoDB引擎就利用了COW策略實(shí)現(xiàn)了MVCC,以提高數(shù)據(jù)庫的查詢并發(fā)度。

每個(gè)事務(wù)修改某個(gè)記錄之前,都會(huì)先把這個(gè)記錄拷貝一份出來,每一次修改,就是一個(gè)版本,因此InnoDB維護(hù)了數(shù)據(jù)從舊到新的每一個(gè)版本,各個(gè)版本之間的記錄通過鏈表串聯(lián)。也正是因?yàn)槊織l記錄都有多個(gè)版本,每個(gè)版本都和一個(gè)事務(wù)相關(guān)聯(lián),因此才容易實(shí)現(xiàn)事務(wù)的隔離性。

要實(shí)現(xiàn)多個(gè)事務(wù)同時(shí)操縱同一條記錄的并發(fā)特性,要實(shí)現(xiàn)并發(fā)過程中的“讀未提交”、“可重復(fù)讀”的事務(wù)隔離級(jí)別,就不能讓事務(wù)讀取到正在被修改的數(shù)據(jù),只能讀取與該事務(wù)關(guān)聯(lián)的歷史版本,這體現(xiàn)了COW思想。

InnoDB的這個(gè)特性叫做多版本并發(fā)控制,即MVCC。InnoDB正是有了MVCC的特性,通常的select xxx from語句都是不加鎖的,讀取的全部是數(shù)據(jù)的歷史版本,從而支持高并發(fā)的查詢,這種讀,叫做“快照讀”;與之相對(duì)的是“當(dāng)前讀”,使用當(dāng)前讀的語句包括:select xxx from yyy for update語句、select xxx from yyy lock in share mode語句、insert / update / delete語句。


MVCC解決了快照讀和寫之間的并發(fā),但是對(duì)于寫與寫之間、當(dāng)前讀與寫之間的并發(fā),MVCC就無能為力了。這時(shí)候就需要用到鎖。MySQL的官方文檔列出了InnoDB的七種鎖:共享鎖 / 排它鎖、意向鎖、記錄鎖、間隙鎖、臨鍵鎖、插入意向鎖、自增鎖。(MySQL的官方文檔從不同的維度劃分,得到了這七種鎖。樂觀鎖與悲觀鎖是對(duì)行鎖的另一種維度的劃分。)

1)共享鎖 / 排它鎖:共享鎖(S)與排它鎖(X)是讀寫鎖的另外一種叫法,共享鎖即讀鎖,支持讀讀之間并發(fā);排它鎖就是寫鎖,保證讀寫之間不能并發(fā)、寫寫之間也不能并發(fā)。一個(gè)事務(wù)讀記錄的時(shí)候,對(duì)該記錄添加共享鎖,其他事務(wù)也可以繼續(xù)添加共享鎖,但不能添加排它鎖,從而使得共享鎖支持讀讀并發(fā);一個(gè)事務(wù)寫記錄的時(shí)候,對(duì)該記錄添加排他鎖,其他事務(wù)既不能再對(duì)該記錄添加共享鎖,也不能添加排它鎖,只能阻塞,從而保證讀寫之間不能并發(fā)、寫寫之間也不能并發(fā)。InnoDB通常對(duì)行添加共享鎖 / 排它鎖,但有些場(chǎng)景會(huì)對(duì)整個(gè)表加共享鎖 / 排它鎖,如DDL語句。

2)意向鎖:意向鎖是一種表鎖,具體分為意向排它鎖(IX)和意向共享鎖(IS)。當(dāng)一個(gè)事務(wù)要給某表的某記錄添加共享鎖的時(shí)候,必先給這個(gè)表添加IS(IS可重入,有多少個(gè)S,IS就重入多少次);當(dāng)一個(gè)事務(wù)要給某表的某記錄添加排他鎖的時(shí)候,必先給這個(gè)表添加IX(IX可重入,有多少個(gè)X,IX就重入多少次)。反之,行共享鎖 / 排它鎖撤銷的時(shí)候,意向鎖的重入次數(shù)也會(huì)相應(yīng)減掉,直至為0,意向鎖撤銷。有了意向鎖,事務(wù)在給整個(gè)表加共享鎖 / 排它鎖的時(shí)候,就不用遍歷所有記錄以確定表中是否存在行的共享鎖 / 排它鎖,只需看表上是否存在意向鎖即可,這樣就很容易避免表鎖與行鎖的沖突。

3)記錄鎖:即行鎖,相對(duì)來講還有表鎖,間隙鎖(Gap Lock,鎖住一個(gè)范圍內(nèi)的記錄,也可以叫做范圍鎖),這些鎖的叫法反映了鎖的粒度。

4)間隙鎖:按照鎖的粒度劃分,除了有行鎖、表鎖之外,還有一種用于鎖住范圍的間隙鎖(Gap Lock)。鎖Gap和鎖行密切相關(guān),Gap Lock肯定建立在某一行的基準(zhǔn)之上,Gap Lock鎖住一個(gè)范圍時(shí)不包含記錄本身,一個(gè)事務(wù)用Gap Lock鎖住一個(gè)表區(qū)間時(shí),可以避免另一個(gè)事務(wù)在這個(gè)區(qū)間上插入新紀(jì)錄。一個(gè)事務(wù)操作表記錄時(shí)是否需要加間隙鎖,這和事務(wù)的隔離級(jí)別密切相關(guān)。之所以要鎖Gap,一個(gè)主要目的是避免幻讀,如果事務(wù)的隔離級(jí)別允許幻讀,則不需要使用間隙鎖。另外,間隙鎖往往針對(duì)非唯一索引,因?yàn)閷?duì)唯一索引(例如主鍵索引、唯一字段上的輔助索引)的每次修改都可以具體定位到哪條或哪幾條記錄,不需要鎖Gap。

5)臨鍵鎖(Next-Key Lock):臨鍵鎖可以看成是行鎖和間隙鎖的綜合,不僅鎖住某個(gè)記錄,也會(huì)鎖住該記錄之前的范圍。

6)插入意向鎖:插入意向鎖也算作一種間隙鎖,專門針對(duì)Insert操作。多個(gè)事務(wù)在同一索引、同一個(gè)范圍內(nèi)的記錄可以并發(fā)插入,即插入意向鎖之間并不互相阻礙。

7)自增鎖:例如有兩個(gè)事務(wù):

事務(wù)A                                   事務(wù)B
insert into tbl1 values xxx
                                        insert into tbl1 values xxx
insert into t1 values xxx
select xxx from tbl1
                                        insert into tbl1 values xxx

在事務(wù)A中,連續(xù)進(jìn)行了兩次記錄插入,并且表tbl1使用了自增主鍵。因此事務(wù)A中最后一條語句查詢出來的結(jié)果里,我們期望看到插入的兩條記錄的id遞增且連續(xù)。但是由于事務(wù)B和事務(wù)A的語句可能會(huì)交叉執(zhí)行,且事務(wù)B中也在插入記錄,因此最后事務(wù)A中看到的兩條插入的記錄的id是不連續(xù)的,這不符合主鍵自增的要求。因此,需要一種自增鎖來保證同一個(gè)事務(wù)中插入的遞增列的值是連續(xù)的。

InnoDB設(shè)計(jì)了如此多類型的鎖,是因?yàn)椴煌腟QL語句、不同的事務(wù)并發(fā)場(chǎng)景、不同的事務(wù)隔離級(jí)別、不同的索引類型,所需求的鎖的特性可能都不一樣,針對(duì)具體場(chǎng)景使用合適的鎖,才能更好保證事務(wù)的隔離、查詢的并發(fā)、查詢的速度等等。

InnoDB通過MVCC + 鎖實(shí)現(xiàn)了事務(wù)的隔離性和并發(fā)性。


利用數(shù)據(jù)庫鎖進(jìn)行數(shù)據(jù)操作時(shí),無論是顯式的鎖還是隱式的鎖,若使用不當(dāng)會(huì)導(dǎo)致死鎖問題。利用有向圖對(duì)事務(wù)對(duì)鎖的持有進(jìn)行建模,利用有向圖的環(huán)檢測(cè)算法可以檢測(cè)死鎖。

下面給出一個(gè)數(shù)據(jù)庫發(fā)生死鎖的實(shí)例:

事務(wù)A                                   事務(wù)B
delete from tbl1 where id = 1
                                        update tbl2 set xxx where id = 2
update tbl2 set xxx where id = 2
                                        delete from tbl1 where id = 1

再給出一個(gè)較為隱晦的死鎖實(shí)例:

事務(wù)A                                   事務(wù)B
delete from tbl1 where id = 1
                                        update tbl1 set xxx where id = 5
insert into tbl1 values xxx
                                        insert into tbl1 values xxx

# 在InnoDB使用可重復(fù)讀的事務(wù)隔離級(jí)別時(shí),以上兩個(gè)事務(wù)的執(zhí)行可能會(huì)產(chǎn)生死鎖。原因是insert操作
# 會(huì)對(duì)tbl1加間隙鎖,導(dǎo)致兩個(gè)事務(wù)死鎖。${TO-COMPREHENSION}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容