天天看點

InnoDB的行鎖,原來為你做了這麼多!

點選↑上方↑藍色“編了個程”關注我~

InnoDB的行鎖,原來為你做了這麼多!

每周至少一篇原創文章

這是本公衆号的第 31 篇原創文章

InnoDB的行鎖,原來為你做了這麼多!

荒腔走闆

這周暫時也沒有荒腔走闆環節,太忙了最近。。。

從事務的隔離級别談起

衆所周知,事務有四大特性,簡稱ACID:原子性、一緻性、隔離性、持久性。

對于隔離性,簡單來說就是多個事務之間是彼此隔離的,互不影響。但想要做到完全的互不影響是很難的,因為資料的強一緻性,很多時候需要犧牲性能去達成。比如如果我們能接受事務的串行執行,那一定是互不影響的。然而現實是,MySQL作為一個資料庫,必然是要支援一定程度的并行執行的,也就是多個事務同時去執行。

凡并行程式,往往是在性能和資料一緻性上做取舍。較好的解決方案要麼是最終一緻,要麼是盡量縮小串行執行的範圍。

如果多個事務同時并行執行,在沒有隔離的情況,可能會發生髒讀、不可重複讀、幻讀的問題。

案例資料(demo表):

id(主鍵) c(普通索引) d(無索引)
5 5 5
10 10 10
15 15 15
20 20 20
25 25 25

「髒讀」

一個事務讀取了另一個事務未送出的資料。

InnoDB的行鎖,原來為你做了這麼多!

髒讀

「不可重複讀」

一個事務讀取同一行資料,多次讀取結果不同。

InnoDB的行鎖,原來為你做了這麼多!

不可重複讀

「幻讀」

一個事務讀取到了别的事務插入的資料。

InnoDB的行鎖,原來為你做了這麼多!

幻讀

但InnoDB因為使用了MVCC,讀取的是“快照”版本,有一些不同,但如果不上鎖,同樣可能會有幻讀問題。

InnoDB的行鎖,原來為你做了這麼多!

InnoDB的幻讀

事務用了四種不同的隔離級别用來解決這些問題。

  • Read uncommitted(未送出讀)
  • Read Committed(已送出讀,簡稱RC)
  • Repeatable Reads(可重複讀,簡稱RR)
  • Serializable(串行化)

隔離級别越高,解決的問題越多,但并發性能也會越差。它們之間的關系如下表:

隔離級别 髒讀 不可重複讀 幻讀
Read uncommitted
Read Committed
Repeatable Reads
Serializable

但InnoDB有些許不同,InnoDB預設的隔離級别是RR,但是通過MVCC和間隙鎖來一定程度上的解決了幻讀的問題。這也是我們今天這篇文章後面會詳細介紹的。

無鎖思想:MVCC

MVCC即“多版本并發控制”,但是它在很多情況下避免了加鎖操作,是以開銷更低。

主流的關系型資料庫都實作了MVCC,但實作機制各有不同。實際上MVCC也沒有一個統一的标準。但大都實作了非阻塞的讀操作,寫操作也隻是鎖定必要的行。本文以下内容所說的MVCC都指的是InnoDB實作的MVCC。

在Mysql的InnoDB引擎,是通過給每行記錄後面儲存兩個隐藏的列來實作的。一個是儲存行的建立時間,另一個儲存了行的過期時間(或删除時間)。

實際上存儲的并不是實際的一個時間戳,而是“系統版本号”。

每次開啟一個事務,系統版本号都會遞增。事務開始時,系統版本号會作為事務的版本号,用來和查詢到的行的版本号進行比較。

MVCC隻在REPEATABLE READ和READ COMMITTED兩個隔離級别下工作,其它兩個隔離級别不能工作。因為READ UNCOMMITTED總是讀取最新的資料行,而不是符合目前事務版本的資料行。而SERIALIZABLE則會對所有讀取的行都加鎖。

在MySQL中,正常的

SELECT

語句,後面不加

FOR UPDATE

LOCK IN SHARE MODE

的,就是用的MVCC去讀。

MVCC和我們在應用層面去實作的“樂觀鎖”有一樣的思想:用版本号,在盡量無鎖的情況下實作一定程度的一緻性。

InnoDB行鎖的概念

InnoDB的行鎖(也稱為臨鍵鎖) Next-Key Locks,「是MySQL對外暴露的鎖的基本機關,它會智能選擇記錄鎖或間隙鎖,鎖住一行或多行或一個間隙」。而記錄鎖又分為共享鎖和排他鎖,間隙鎖的概念下面有一個插入意向鎖。這些鎖的關系大概是這樣:

InnoDB的行鎖,原來為你做了這麼多!

鎖的關系

記錄鎖

所謂記錄鎖 Record Locks,就是鎖住确定的一行行記錄。它分為共享鎖和排它鎖。分别對應不同的SQL寫法。

共享鎖

共享鎖 Shared Locks ,簡稱S鎖。使用以下SQL可能觸發:

SELECT ... LOCK IN SHARE MODE
           

之是以說“可能”觸發,是因為它查到了資料庫有确定的記錄才會鎖住這些記錄,否則會變成間隙鎖。這個其實很好了解,找到了資料,才鎖它。如果沒找到資料,就鎖這個間隙。

排他鎖

排他鎖 Exclusive locks ,簡稱X鎖。使用一下SQL可能觸發:

SELECT ... FOR UPDATE
           

這裡的“可能”含義與上面同理,不贅述。

間隙鎖

間隙鎖 Gap - Lock,顧名思義,鎖住一個間隙。上文我們提到過,InnoDB預設的隔離級别是RR,但是通過間隙鎖來一定程度上的解決了幻讀的問題。它是怎麼解決的呢?就是通過間隙鎖來解決的。

上面兩種SQL,如果沒有查找到确定的記錄,就會根據條件去鎖住一個間隙。間隙鎖是根據已有資料的一個左開右閉的區間。

還是這個案例資料(假設資料都是從1開始):

id(主鍵) c(普通索引) d(無索引)
5 5 5
10 10 10
15 15 15
20 20 20
25 25 25

對于下面這些區間的操作,會有對應的間隙鎖:(0, 5], (5, 10], (10, 15], (15, 20], (20, 25], (25, 正無窮)。

什麼意思呢?假如你的SQL查詢的範圍不同,那它鎖住的區間就不同。比如:

-- 鎖住(0, 5]
SELECT * FROM demo where id = 3;

-- 鎖住(10, 15]
SELECT * FROM demo where id = 11;
           

間隙鎖其實是“共享”的。也就是說,多個事務可以擷取同一個區間的間隙鎖。

InnoDB的行鎖,原來為你做了這麼多!

間隙鎖不互相阻塞

插入意向鎖

插入意向鎖 Insert Intention Locks,代表目前事務準備插入一行資料。使用INSERT/UPDATE/DELETE等語句會獲得插入意向鎖。

「插入意向鎖和插入意向鎖之間是相容的,隻要插入的鍵值不同,就不會互相阻塞」。比如以下兩個SQL,在不同的事務中,哪怕它們在同一個間隙,隻要沒有間隙鎖,就不會阻塞:

InnoDB的行鎖,原來為你做了這麼多!

插入兩條不同的記錄不阻塞

但如果兩個事務插入同一個key,那就會阻塞。

InnoDB的行鎖,原來為你做了這麼多!

兩個插入意向鎖阻塞的情況

插入意向鎖可以保證兩個事務插入key不同的資料的時候不沖突,提升并發性。

「但是間隙鎖會阻塞插入意向鎖」!這也可以了解,因為InnoDB想在RR隔離級别就解決幻讀問題。是以A事務用SELECT語句擷取了一個間隙鎖,自然不希望B事務在這個期間往這個間隙插入一條新的記錄。

InnoDB的行鎖,原來為你做了這麼多!

間隙鎖阻塞插入意向鎖

與索引的關系

不管哪種行級鎖,「行級鎖的其實都是索引」。是以在上面的demo中,如果對id(主鍵)或者column c(普通索引)操作,都會觸發相應的行級鎖,但如果對column d(無索引)做同樣的操作,InnoDB就會對表中所有資料加鎖,實際效果跟表級鎖一樣。

是以一定要注意,如果要上鎖,需要注意是否走了索引,不要弄成了表級鎖造成安全事故。

一個死鎖案例

最後給一個關于間隙鎖和插入意向鎖的死鎖案例吧,也是之前在項目上遇到過的真實案例。

過程

  • 事務A select ... for update 查找一個不存在的資料,獲得間隙鎖;
  • 事務B select ... for update 查找一個不存在的資料,獲得相同位置的間隙鎖;
  • 事務A insert into ... 到這個間隙,嘗試獲得插入意向鎖,但被事務B持有的間隙鎖阻塞。
  • 事務B insert into ... 到這個間隙,嘗試獲得插入意向鎖,但被事務A持有的間隙鎖阻塞,MySQL監測到死鎖,復原事務B;
  • 因為事務B復原,是以事務B持有的間隙鎖被釋放,是以之前事務A的插入語句不再阻塞,順利執行插入操作。

報錯資訊:Deadlock found when trying to get lock; try restarting transaction

用圖把這個過程描述出來:

InnoDB的行鎖,原來為你做了這麼多!

死鎖過程

最終結果

事務A插入成功,事務B因為死鎖被復原。

總結

間隙鎖主要還是用于防止幻讀的情況,是以多個事務能夠同時擷取同一段間隙鎖本身并沒有問題,間隙鎖能夠阻塞插入意向鎖也并沒有問題。

而插入意向鎖可以看成是一種特殊的間隙鎖,是用于在同一個間隙,插入不同的資料,不會互相阻塞。它比普通間隙鎖的資料一緻性更低,但并發性能更好;

解決思路

普通間隙鎖還是主要用于讀操作防止幻讀。是以我們在想進行插入操作的時候,其實沒有必要對即将插入的間隙使用普通間隙鎖,直接使用insert語句産生的插入意向鎖就好了。

如果要保證資料的一緻性,可以使用插入意向鎖配合主鍵、唯一鍵等限制。

附錄

那些在文章中用過的SQL:

-- 建表
create table demo
(
 id int unsigned not null,
 c int unsigned not null,
 d int unsigned not null,
 constraint demo_pk
  primary key (id),
 constraint idx_c
  unique (c)
);

-- 插資料
INSERT INTO demo VALUES
(5, 5, 5),
(10, 10, 10),
(15, 15, 15),
(20, 20, 20),
(25, 25, 25);

-- 設定隔離級别 未送出讀
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;

-- 設定隔離級别 已送出讀
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED ;

-- 設定隔離級别 可重複讀
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ ;

-- 設定隔離級别 串行化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE ;

-- 禁止自動送出事務
SET AUTOCOMMIT = 0;

-- 開啟事務
BEGIN;

-- 送出事務
COMMIT;

-- 會擷取鎖的查詢
SELECT * FROM demo WHERE id < 5 FOR SHARE ;
           
InnoDB的行鎖,原來為你做了這麼多!

關于作者

我是Yasin,一個在不斷精進的菜雞

微信公衆号:編了個程(blgcheng)

個人網站:https://yasinshaw.com

這裡很多技術幹貨,關注肯定不後悔

InnoDB的行鎖,原來為你做了這麼多!

加個星标可以第一時間看到最新文章

InnoDB的行鎖,原來為你做了這麼多!

聽說,轉發和在看的人都升職加薪了

InnoDB的行鎖,原來為你做了這麼多!

原創不易,四連走起,

關注在看,評論轉發,

感謝支援!

InnoDB的行鎖,原來為你做了這麼多!
InnoDB的行鎖,原來為你做了這麼多!

推薦閱讀

  • Pipeline設計模式,了解一下?
  • 關于Spring AOP的靈魂十問
  • 源碼解析 - Spring如何實作IoC的?
  • 一個慢SQL引起的慘案