Mysql 中的事務與鎖
InnoDB與MyISAM的最大不同有兩點:一是支持事務(TRANSACTION);二是采用了行級鎖。
事務
- 原子性(Actomicity),事務是一個原子操作單元,其對數據的修改,要么全都執行,要么全都不執行;
- 一致性(Consistent),在事務開始和完成時,數據都必須保持一致狀態;
- 隔離性(Isolation),MySQL提供一定的隔離機制,保證事務在不受外部並發操作影響的“獨立”環境執行,這意味着事務處理過程中的中間狀態對外部是不可見的,反之亦然;
- 持久性(Durable),事務完成之后,它對於數據的修改是永久性的,即使出現系統故障也能夠保持。
由於事務的並發執行,可能會引起一些問題,比如:
- 更新丟失(Lost Update),當兩個或多個事務選擇同一行,然后基於最初選定的值更新該行時,由於每個事務都不知道彼此的存在,就會發生丟失更新問題——最后的更新覆蓋了其他事務所做的更新;
- 臟讀(Dirty Reads),一個事務正在對一條記錄做修改,在這個事務並提交前,這條記錄的數據就處於不一致狀態;這時,另一個事務也來讀取同一條記錄,如果不加控制,第二個事務讀取了這些“臟”的數據,並據此做進一步的處理;
- 幻讀(Phantom Reads),一個事務按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據(通常對應於INSERT);
- 不可重復讀(Non-Repeatable Reads),一個事務在讀取某些數據已經發生了改變、或某些記錄已經被刪除了(通常對應於UPDATE)。



注意,“更新丟失”不能單靠數據庫事務控制器來解決,而應該由用戶自己想辦法解決(比如加鎖);
但其它三個問題(臟讀、幻讀、不可重復讀)都是 讀一致性 問題,應該由數據庫提供一定的事務隔離機制來解決。數據庫實現事務隔離的方式,基本可以分為以下兩種:
- 加鎖,即在讀取數據前,對其加鎖,阻止其他事務對數據進行修改;
- 多版本並發控制(MultiVersion Concurrency Control, MVCC),通過生成一個數據請求時間點的一致性數據快照(Snapshot),並用這個快照來提供一定級別(語句級或事務級)的一致性讀取;從用戶的角度,好像是數據庫可以提供同一數據的多個版本。
數據庫的事務隔離級別越嚴格,事務並發的副作用越小,但付出的性能代價也就越大,因為事務隔離實質上就是使事務在一定程度上“串行化”進行,這顯然與“並發”是矛盾的,同時,不同的應用對讀一致性和事務隔離程度的要求也是不同的,比如許多應用對“不可重復讀”和“幻讀”並不敏感,可能更關心數據並發訪問的能力。
為了解決“隔離”與“並發”的矛盾,ISO/ANSI SQL92定義了4個事務隔離級別:
- Read uncommitted(讀未提交,允許臟讀),如果一個事務已經開始寫數據,則不允許其它事務同時進行寫操作,但允許其他事務讀此行數據。該隔離級別可以通過“排他寫鎖”實現;
- Read committed(讀已提交,允許不可重復讀),未提交的寫事務將會禁止其他事務訪問該行,這可以通過“瞬間共享讀鎖”和“排他寫鎖”實現;
- Repeatable read(可重復讀,允許幻讀),讀取數據的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務;這可以通過“共享讀鎖”和“排他寫鎖”實現;
- Serializable (串行化),事務最高隔離級別,在該級別下,事務串行化順序執行。僅僅通過“行級鎖”是無法實現事務序列化的,必須通過其他機制保證新插入的數據不會被剛執行查詢操作的事務訪問到。
最后要說明的是:各具體數據庫並不一定完全實現了上述4個隔離級別,例如,Oracle只提供Read committed和Serializable兩個標准級別,另外還自己定義的Read only隔離級別:SQL Server除支持上述ISO/ANSI SQL92定義的4個級別外,還支持一個叫做"快照"的隔離級別,但嚴格來說它是一個用MVCC實現的Serializable隔離級別。MySQL支持全部4個隔離級別,但在具體實現時,有一些特點,比如在一些隔離級下是采用MVCC一致性讀,但某些情況又不是。
Mysql的默認隔離級別是Repeatable read。
鎖
MySQL中的鎖大致可分為以下3種:
- 表級鎖:主要是 MyISAM引擎使用,開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的概率最高,並發度最低;
- 行級鎖:主要是 Innodb引擎使用,開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,並發度也最高;
- 頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,並發度一般。
可見,鎖的粒度越大,加鎖越快,但並發度越低。
表級鎖 (MyISAM引擎)
MySQL的表鎖有兩種模式:
- 表共享讀鎖(Table Read Lock),主要由讀操作(SELECT)使用,不會阻塞其他用戶對同一表的讀請求,但會阻塞寫請求;
- 表獨占寫鎖(Table Write Lock),主要由寫操作(INSERT/UPDATE等)使用,會阻塞其他用戶對同一表的讀、寫請求。
可見,當一線程獲得對一個表的寫鎖后,只有持有鎖的線程可以對表進行更新操作。其他線程的讀、寫操作都會等待,直到鎖被釋放為止。MyISAM表的讀和寫操作之間,以及寫和寫操作之間是串行的!
如下表,橫向title表示當前鎖模式,縱向表示請求鎖模式,是否兼容的是表示不會阻塞,否表示要阻塞:
當前鎖模式/是否兼容/請求鎖模式 |
None |
讀鎖 |
寫鎖 |
讀鎖 | 是 | 是 | 否 |
寫鎖 | 是 | 否 | 否 |
MyISAM在執行查詢語句(SELECT)前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不需要用戶干預,因此用戶一般不需要直接用LOCK TABLE命令給MyISAM表顯式加鎖。但在涉及到多張表的操作時,可能會需要使用者顯式加鎖,例如有一個訂單表orders,其中記錄有訂單的總金額total,同時還有一個訂單明細表order_detail,其中記錄有訂單每一產品的金額小計subtotal,假設我們需要檢查這兩個表的金額合計是否相等,可能就需要執行如下兩條SQL:
SELECT SUM(total) FROM orders; SELECT SUM(subtotal) FROM order_detail;
這時,如果不先給這兩個表加鎖,就可能產生錯誤的結果,因為第一條語句執行過程中,order_detail表可能已經發生了改變。因此,正確的方法應該是:
LOCK tables orders read local,order_detail read local; SELECT SUM(total) FROM orders; SELECT SUM(subtotal) FROM order_detail; Unlock tables;
這里要注意:
- 在用LOCKTABLES給表顯式加表鎖是時,必須同時取得所有涉及表的鎖,假如多個表加鎖有先后順序,則可能引發deadlock;
- 上面的例子在LOCK TABLES時加了‘local’選項,其作用就是在滿足MyISAM表並發插入條件的情況下,允許其他用戶在表尾插入記錄(表尾新增不會引起競爭條件)。
例子中LOCK TABLE語句使用 local 關鍵字可以使得如果MyISAM允許在一個讀表的同時,另一個進程從表尾插入記錄。事實上,MyISAM存儲引擎有一個系統變量 concurrent_insert,專門用以控制其並發插入的行為,其值分別可以為0、1或2。
- concurrent_insert = 0,不允許並發插入(此時讀和寫之間也是串行的);
- concurrent_insert = 1,如果MyISAM允許在一個讀表的同時,且表文件沒有空洞(被刪除的行),另一個進程從表尾插入記錄(這也是MySQL的默認設置);
- concurrent_insert = 2,無論MyISAM表中有沒有空洞,都允許在表尾插入記錄,都允許在表尾並發插入記錄。
注意,concurrent_insert 的並發是指在有SELECT的讀鎖時允許一個寫入操作(即讀和寫之間並發),而不是指多個寫入可以並發執行,如果有多個寫入操作之間仍然需要串行順序執行。
另外,對多數應用場景下,讀操作會遠多於寫操作,那這是否會導致頻繁的讀操作一直持有read鎖,而更新操作很難得到write鎖呢?答案是寫操作會優先獲得鎖,即使讀請求先進入鎖等待隊列,寫請求后到,寫鎖也會插到讀請求之前!這也正是MyISAM表不太適合於有大量更新操作和查詢操作應用的原因,因為,大量的更新操作會造成查詢操作很難獲得讀鎖,從而可能永遠阻塞。
- 通過指定啟動參數low-priority-updates,使MyISAM引擎默認給予讀請求以優先的權利(服務級);
- 通過執行命令SET LOW_PRIORITY_UPDATES=1,使該連接發出的更新請求優先級降低(連接級);
- 通過指定INSERT、UPDATE、DELETE語句的LOW_PRIORITY屬性,降低該語句的優先級(語句級);
以上幾個方法都是要么更新優先,要么查詢優先,MySQL也提供了一種折中的辦法來調節讀寫沖突,即給系統參數max_write_lock_count設置一個合適的值,當一個表的讀鎖達到這個值后,MySQL變暫時將寫請求的優先級降低,給讀進程一定獲得鎖的機會。
行級鎖 (Innodb引擎)
MySQL的行級鎖也有兩種模式:
- 共享鎖(S Lock),允許一個事務去讀一行,阻止其他事務獲得相同數據集的排他鎖;
- 排它鎖(X Lock),允許獲取排它鎖的事務更新數據,阻止其他事務取得相同的數據集共享讀鎖和排他寫鎖。
可以想到這里SS相互兼容,XX、XS、SX不兼容。
對於普通SELECT語句,InnoDB不會加任何鎖;對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及及數據集加排他鎖(X);
事務可以通過以下語句顯式給記錄集加共享鎖或排它鎖:
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE SELECT * FROM table_name WHERE ... FOR UPDATE
用SELECT .. IN SHARE MODE獲得共享鎖,主要用在需要數據依存關系時確認某行記錄是否存在,並確保沒有人對這個記錄進行UPDATE或者DELETE操作。但是如果當前事務也需要對該記錄進行更新操作,則很有可能造成死鎖。
對於鎖定行記錄后需要進行更新操作的應用,應該使用SELECT ... FOR UPDATE方式獲取排他鎖。
行鎖與索引
Innodb的行鎖到底鎖住的是什么呢?我們先假設它鎖定的是一行數據或者記錄(Record)。
看個例子,事務A 對user表中 id = 1的記錄執行 select ... for update,如果鎖的是Record,那么應該只對id=1的記錄加X鎖;
mysql> show create table user; +-------+-------------------------------------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +-------+-------------------------------------------------------------------------------------------------------------------------------------------+ | user | CREATE TABLE `user` ( `id` bigint(20) unsigned NOT NULL DEFAULT '0', `name` varchar(32) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8 | +-------+-------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> select * from user; +----+-------+ | id | name | +----+-------+ | 1 | Green | | 6 | Bush | +----+-------+ 2 rows in set (0.00 sec) mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id=1 for update; +----+-------+ | id | name | +----+-------+ | 1 | Green | +----+-------+ 1 row in set (0.00 sec) mysql>
另外一個 事務B 對記錄id=3加X鎖,它和事務A 操作的記錄並非同一條,此時似乎不應該被阻塞,但事實上它被阻塞了!
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id=6 for update; // blocked
至此,我們可以確定前面的假設是錯誤的,行鎖住的不是數據行(或者叫Record),否則不會出現整張表都被鎖住的情況。
如果我們對上面例子中的user表執行如下語句,增加主鍵id
mysql> alter table user add primary key (id); Query OK, 0 rows affected (12.97 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql> show create table user; +-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------+ | user | CREATE TABLE `user` ( `id` bigint(20) unsigned NOT NULL DEFAULT '0', `name` varchar(32) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 | +-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec)
然后重復上述例子,事務A
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id=1 for update; +----+-------+ | id | name | +----+-------+ | 1 | Green | +----+-------+ 1 row in set (0.00 sec)
事務B
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id=6 for update; // ok +----+-------+ | id | name | +----+-------+ | 1 | Green | +----+-------+ 1 row in set (0.00 sec)
結論:如果InnoDB的行鎖不是通過鎖定記錄實現的,那么可能和索引有關?
事實上,InnoDB行鎖是通過索引上的索引項來實現的,這一點MySQL與Oracle不同,后者是通過在數據中對相應數據行加鎖來實現的。InnoDB這種行鎖實現特點意味者:只有通過索引條件檢索數據,InnoDB才會使用行級鎖,否則,InnoDB將使用表鎖 (行鎖升級為表鎖)!
意向鎖(Intention Locks)
需要注意的是,當給某一行增加共享鎖、排他鎖時,數據庫會自動給這一行所處的表添加意向共享鎖(IS Lock)、意向排他鎖(IX Lock)也就是說,如果想給 r行 增加鎖,需要給 r行 所在的表先增加意向排他鎖。
- 意向共享鎖(IS Lock),事務打算給數據行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖;
- 意向排他鎖(IX Lock),事務打算給數據行加排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。
意向鎖是InnoDB自動加的,不需用戶干預。
下面來看一下意向鎖的作用,假設事務A對 r行 加了S鎖,之后事務B申請整個表的寫鎖,那么數據庫需要做的事情包括:
step1:判斷該表是否已被其他事務用表鎖鎖表;
step2:發現表上有意向共享鎖,說明表中有些行被共享行鎖鎖住了,因此,事務B申請表的寫鎖會被阻塞。
如果沒有意向鎖,那么在進行step2的時候,需要遍歷整個表判斷是否有行鎖的存在,以免發生沖突;但如果有了意向鎖,則只需要判斷該意向鎖與即將申請的表鎖是否兼容即可。因為意向鎖的存在,代表了有(或即將有)行級鎖的存在。
Intention locks do not block anything except full table requests (for example, LOCK TABLES ... WRITE).
The main purpose of intention locks is to show that someone is locking a row, or going to lock a row in the table.
可見意向鎖的作用主要是協調行鎖與表鎖的關系,如下表,注意,該表里的X和S都是表級鎖,而非行鎖!
當前鎖模式/是否兼容/請求鎖模式 | X | IX | S | IS |
X | 否 | 否 | 否 | 否 |
IX | 否 | 是 | 否 | 是 |
S | 否 | 否 | 是 | 是 |
IS | 否 | 是 | 是 | 是 |
行鎖可以再分為記錄鎖、臨健鎖、間隙鎖,假如表中某字段值有1、4、7、10幾條記錄,如下圖
圖中間隙鎖的記錄都是不存在的,而臨健鎖是一個左開右閉區間。
記錄鎖(Record Locks)
上面在講到 行鎖與索引 關系的時候,提到行鎖實際鎖的是index,而非record,也就是說 Record locks 其實是鎖索引數據,那么當表中沒有index怎么辦呢?
這種情況下Innodb會創建一個隱藏的clustered index,並對該聚集索引加鎖。
臨鍵鎖(Next-key Locks)
臨健鎖 是記錄鎖和間隙鎖的組合,它的封鎖范圍,既包含索引記錄(record),又包含索引區間(gap),所以它是一個左開右閉區間。
臨健鎖 只有在事務隔離級別RR(Repeated Read)下才生效,用於避免幻讀。
間隙鎖(Gap Locks)
當我們用范圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據的索引項加鎖;對於鍵值在條件范圍內但並不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖。
mysql> select * from user; +----+------+ | id | name | +----+------+ | 6 | Bush | +----+------+ 1 row in set (0.00 sec) mysql> mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from user where id > 6 for update; Empty set (0.00 sec)
此時事務A還未執行commit,然后另一個事物B先插入一條 id = 1 的記錄,可以成功;然后再插入一條 id = 7 的記錄,發現被夯住,這是因為 id > 6 的記錄被事務A 加上了間隙鎖。
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> insert into user values (1, "Green"); Query OK, 1 row affected (0.00 sec) mysql> insert into user values (7, "Brown");
再舉個例子,唯一索引有值1、5、7、11,那么該表隱藏的next-key lock包括(左開右閉區間)
(-infinity, 1] (1, 5] (5, 7] (7, 11] (11, +infinity]
如果 事務A 執行如下范圍檢索
begin; SELECT * FROM `user` WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;
此時,另一個事務B 執行
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> insert into user values (4, "4"); Query OK, 1 row affected (0.01 sec) mysql> insert into user values (6, "6"); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into user values (11, "11"); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql> insert into user values (12, "12"); Query OK, 1 row affected (0.00 sec)
可見,事務A產生的間隙鎖會鎖住 (5, 7] 和 (7, 11] 兩個區間。
再比如,若 事務A 執行如下檢索(不存在的記錄)
BEGIN; /* 查詢 id = 3 這一條不存在的數據並加記錄鎖 */ SELECT * FROM `user` WHERE `id` = 3 FOR UPDATE;
此時,另一個事務B執行
mysql> begin; Query OK, 0 rows affected (0.00 sec) mysql> INSERT INTO `user` (`id`, `name`) VALUES (2, '2'); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> INSERT INTO `user` (`id`, `name`) VALUES (6, '6'); Query OK, 1 row affected (0.00 sec)
可見,事務A產生的間隙鎖會鎖住 (1, 5] 。當然,如果事務A 檢索的條件能夠命中記錄(比如where id=5),就不會產生間隙鎖,而只會產生記錄鎖。
綜上對行鎖的一些結論:
- 加鎖的基本單位是 next-key lock;
- 加鎖是基於索引的,查找過程中訪問到的對象才會加鎖(如果沒有索引會退化為表鎖);
- 在唯一索引上的等值查詢,如果該記錄不存在,會產生gap lock,如果記錄存在,則只會產生record lock;
- 對於查找某一范圍內的查詢語句,會產生間隙鎖,如:WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;