數據庫中的鎖


數據庫中的鎖

鎖分類

按鎖的粒度划分:表級鎖、行級鎖、頁級鎖

按鎖級別划分:共享鎖、排它鎖、意向鎖

按加鎖方式划分:自動鎖、顯示鎖

按使用方式划分:樂觀鎖、悲觀鎖

MySQL中的行級鎖、表級鎖和頁級鎖

行級鎖:行級鎖分為共享鎖和排他鎖。行級鎖是MySQL中鎖定粒度最細的鎖。InnoDB引擎支持行級鎖和表級鎖,只有在通過索引條件檢索數據的時候,才使用行級鎖,否就使用表級鎖。行級鎖開銷大,加鎖慢,鎖定粒度最小,發生鎖沖突的概率最低,並發度最高。

表級鎖:表級鎖分為表共享鎖和表獨占鎖。表級鎖開銷小,加鎖快,鎖定粒度大,發生鎖沖突最高,並發度最低

頁級鎖:頁級鎖是MySQL中鎖定粒度介於行級鎖和表級鎖中間的一種鎖。表級鎖速度快,但沖突多,行級沖突少,但速度慢。所以取了折中的頁級,一次鎖定相鄰的一組記錄。BDB支持頁級鎖。開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖。鎖定粒度界於表鎖和行鎖之間,並發度一般。

MySQL中排它鎖和共享鎖

排它鎖(exclusive lock)

排他鎖又叫寫鎖,如果事務T對A加上排它鎖,則其他事務都不能對A加任何類型的鎖。獲准排它鎖的事務既能讀數據,又能寫數據

共享鎖(share lock)

共享鎖又叫讀鎖,如果事務T對A加上共享鎖,則其他事務只能對A再加共享鎖,不能加其他鎖。共享鎖的事務只能讀數據,不能寫數據。

意向鎖

其實有排它鎖和共享鎖就足夠了為什么還需要有意向鎖,這里舉一個比較形象的例子:

在mysql中有表鎖,讀鎖鎖表,會阻塞其他事務修改表數據。寫鎖鎖表,會阻塞其他事務讀和寫。

  1. Innodb引擎又支持行鎖,行鎖分為共享鎖,一個事務對一行的共享只讀鎖。排它鎖,一個事務對一行的排他讀寫鎖。
  2. 這兩中類型的鎖共存的問題考慮這個例子:事務A鎖住了表中的一行,讓這一行只能讀,不能寫。之后,事務B申請整個表的寫鎖。如果事務B申請成功,那么理論上它就能修改表中的任意一行,這與A持有的行鎖是沖突的。數據庫需要避免這種沖突,就是說要讓B的申請被阻塞,直到A釋放了行鎖。

 

數據庫要怎么判斷這個沖突呢?

  • step1:判斷表是否已被其他事務用表鎖鎖表
  • step2:判斷表中的每一行是否已被行鎖鎖住。

注意step2,這樣的判斷方法效率實在不高,因為需要遍歷整個表。於是就有了意向鎖。在意向鎖存在的情況下,事務A必須先申請表的意向共享鎖,成功后再申請一行的行鎖。

在意向鎖存在的情況下,上面的判斷可以改成

  • step1:不變
  • step2:發現表上有意向共享鎖,說明表中有些行被共享行鎖鎖住了,因此,事務B申請表的寫鎖會被阻塞。

注意:申請意向鎖的動作是數據庫完成的,就是說,事務A申請一行的行鎖的時候,數據庫會自動先開始申請表的意向鎖,不需要我們程序員使用代碼來申請。

 

 

InnoDB的鎖定模式實際上可以分為四種:共享鎖(S)、排它鎖(X)、意向共享鎖(IS)和意向排它鎖(IX),我們可以通過以下表來總結上面四種鎖的共存邏輯關系:

如果一個事務請求的鎖模式與當前的鎖兼容,InnoDB就將請求的鎖授予該事務;反之,如果兩者不兼容,該事務就要等待鎖釋放。
意向鎖是InnoDB自動加的,不需用戶干預。對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖(X);對於普通SELECT語句,InnoDB不會加任何鎖;

數據庫隔離級別

Read Uncommitted,讀寫均不使用鎖,數據的一致性最差,也會出現許多邏輯錯誤。

Read committed,使用寫鎖,但是讀會出現不一致,不可重復度

Repeatable Read,使用讀鎖和寫鎖,解決不可重復讀的問題,但會有幻讀

Serializable,使用事務串行化調度,避免出現因為插入數據沒法加鎖導致的不一致的情況

 

讀不提交,造成臟讀(Read Uncommitted)

一個事務中的讀操作可能讀到另一個事務中未提交修改的數據,如果事務發生回滾就可能造成錯誤。

例子:A打100塊給B,B看賬戶,這是兩個操作,針對同一個數據庫,兩個事物,如果B讀到了A事務中的100塊,認為錢打過來了,但是A的事務最后回滾了,造成損失。

避免這些事情的發生就需要我們在寫操作的時候加鎖,使讀寫分離,保證讀數據的時候,數據不被修改,寫數據的時候,數據不被讀取。從而保證寫的同時不能被另個事務寫和讀。

讀提交(Read Committed)

我們加了寫鎖,就可以保證不出現臟讀,也就是保證讀的都是提交之后的數據,但是會造成不可重讀,即讀的時候不加鎖,一個讀的事務過程中,如果讀取數據兩次,在兩次之間有寫事務修改了數據,將會導致兩次讀取的結果不一致,從而導致邏輯錯誤。

可重復度(Repeatable Read)

解決不可重復讀問題,一個事務中如果有多次讀取操作,讀取結果需要一致(指的是固定一條數據的一致,幻讀指的是查詢出的數量不一致)。 這就牽涉到事務中是否加讀鎖,並且讀操作加鎖后是否在事務commit之前持有鎖的問題,如果不加讀鎖,必然出現不可重復讀,如果加鎖讀完立即釋放,不持有,那么就可能在其他事務中被修改,若其他事務已經執行完成,此時該事務中再次讀取就會出現不可重復讀,

可串行話(Serializable)

解決幻讀問題,在同一個事務中,同一個查詢多次返回的結果不一致。事務A新增了一條記錄,事務B在事務A提交前后各執行了一次查詢操作,發現后一次比前一次多了一條記錄。幻讀是由於並發事務增加記錄導致的,這個不能像不可重復讀通過記錄加鎖解決,因為對於新增的記錄根本無法加鎖。需要將事務串行化,才能避免幻讀。

這是最高的隔離級別,它通過強制事務排序,使之不可能相互沖突,從而解決幻讀問題。簡言之,它是在每個讀的數據行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭

 

MySQL默認的事務隔離級別為可重復讀

間隙鎖(Next-Key鎖)

當我們用范圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的 索引項加鎖;對於鍵值在條件范圍內但並不存在的記錄,叫做"間隙(GAP)",InnoDB也會對這個"間隙"加鎖,這種鎖機制就是所謂的間隙鎖 (Next-Key鎖)。 
舉例來說,假如emp表中只有101條記錄,其empid的值分別是 1,2,…,100,101,下面的SQL:

Select * from emp where empid > 100 for update;

是一個范圍條件的檢索,InnoDB不僅會對符合條件的empid值為101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的"間隙"加鎖。

InnoDB使用間隙鎖的目的,一方面是為了防止幻讀,以滿足相關隔離級別的要求,對於上面的例子,要是不使 用間隙鎖,如果其他事務插入了empid大於100的任何記錄,那么本事務如果再次執行上述語句,就會發生幻讀;另外一方面,是為了滿足其恢復和復制的需要。

很顯然,在使用范圍條件檢索並鎖定記錄時,InnoDB這種加鎖機制會阻塞符合條件范圍內鍵值的並發插入,這往往會造成嚴重的鎖等待。因此,在實際應用開發中,尤其是並發插入比較多的應用,我們要盡量優化業務邏輯,盡量使用相等條件來訪問更新數據,避免使用范圍條件。

還要特別說明的是,InnoDB除了通過范圍條件加鎖時使用間隙鎖外,如果使用相等條件請求給一個不存在的記錄加鎖,InnoDB也會使用間隙鎖!下面這個例子假設emp表中只有101條記錄,其empid的值分別是1,2,……,100,101。 

InnoDB存儲引擎的間隙鎖阻塞例子 

MVCC

MVCC(Multi-Version Concurrency Control)即多版本並發控制

MySQL的大多數事務型(如InnoDB,Falcon等)存儲引擎實現的都不是簡單的行級鎖。基於提升並發性能的考慮,他們一般都同時實現了MVCC。當前不僅僅是MySQL,其它數據庫系統(如Oracle,PostgreSQL)也都實現了MVCC。MVCC並沒有一個統一的實現標准,所以不同的數據庫,不同的存儲引擎的實現都不盡相同。

MVCC是通過在每行記錄后面保存兩個隱藏的列來實現的。這兩個列,一個保存了行的創建時間,一個保存行的過期時間(或刪除時間)。當然存儲的並不是實際的時間值,而是系統版本號(system version number)。每開始一個新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會作為事務的版本號,用來和查詢到的每行記錄的版本號進行比較。

下面看一下在REPEATABLE READ隔離級別下,MVCC具體是如何操作的。

SELECT

InnoDB會根據以下兩個條件檢查每行記錄:

  1. InnoDB只查找版本早於當前事務版本的數據行(也就是,行的系統版本號小於或等於事務的系統版本號),這樣可以確保事務讀取的行,要么是在事務開始前已經存在的,要么是事務自身插入或者修改過的。
  2. 行的刪除版本要么未定義,要么大於當前事務版本號。這可以確保事務讀取到的行,在事務開始之前未被刪除。

只有符合上述兩個條件的記錄,才能返回作為查詢結果

INSERT

InnoDB為新插入的每一行保存當前系統版本號作為行版本號。

DELETE

InnoDB為刪除的每一行保存當前系統版本號作為行刪除標識。

UPDATE

InnoDB為插入一行新記錄,保存當前系統版本號作為行版本號,同時保存當前系統版本號到原來的行作為行刪除標識。
保存這兩個額外系統版本號,使大多數讀操作都可以不用加鎖。這樣設計使得讀數據操作很簡單,性能很好,並且也能保證只會讀取到符合標准的行,不足之處是每行記錄都需要額外的存儲空間,需要做更多的行檢查工作,以及一些額外的維護工作

舉例說明

transaction 1:

假設系統初始事務ID為1;

transaction 2:

SELECT

假設當執行事務2的過程中,准備執行語句(2)時,開始執行事務3:

transaction 3:

事務3執行完畢,開始執行事務2 語句2,由於事務2只能查詢創建時間小於等於2的,所以事務3新增的記錄在事務2中是查不出來的,這就通過樂觀鎖的方式避免了幻讀的產生

UPDATE

假設當執行事務2的過程中,准備執行語句(2)時,開始執行事務4:

transaction session 4:

InnoDB執行UPDATE,實際上是新插入了一行記錄,並保存其創建時間為當前事務的ID,同時保存當前事務ID到要UPDATE的行的刪除時間

事務4執行完畢,開始執行事務2 語句2,由於事務2只能查詢創建時間小於等於2的,所以事務修改的記錄在事務2中是查不出來的,這樣就保證了事務在兩次讀取時讀取到的數據的狀態是一致的

DELETE

假設當執行事務2的過程中,准備執行語句(2)時,開始執行事務5:

transaction session 5:

事務5執行完畢,開始執行事務2 語句2,由於事務2只能查詢創建時間小於等於2、並且過期時間大於等於2,所以id=2的記錄在事務2 語句2中,也是可以查出來的,這樣就保證了事務在兩次讀取時讀取到的數據的狀態是一致的


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM