MySQL 行鎖 表鎖機制

MySQL 表鎖和行鎖機制

行鎖變表鎖,是福仍是坑?若是你不清楚MySQL加鎖的原理,你會被它整的很慘!不知坑在何方?沒事,我來給大家標記幾個坑。遇到了可別亂踩。經過本章內容,帶你學習MySQL的行鎖,表鎖,兩種鎖的優缺點,行鎖變表鎖的緣由,以及開發中須要注意的事項。還在等啥?經驗等你來拿!html

MySQL的存儲引擎是從MyISAM到InnoDB,鎖從表鎖到行鎖。後者的出現從某種程度上是彌補前者的不足。好比:MyISAM不支持事務,InnoDB支持事務。表鎖雖然開銷小,鎖錶快,但高併發下性能低。行鎖雖然開銷大,鎖錶慢,但高併發下相比之下性能更高。事務和行鎖都是在確保數據準確的基礎上提升併發的處理能力。本章重點介紹InnoDB的行鎖。mysql

案例分析

目前,MySQL經常使用的存儲引擎是InnoDB,相對於MyISAM而言。InnoDB更適合高併發場景,同時也支持事務處理。咱們經過下面這個案例(坑),來了解行鎖和表鎖。git

業務:由於訂單重複導入,須要用腳本將訂單狀態爲"待客服確認"且平臺是"xxx"的數據批量修改成"已關閉"。
說明:避免直接修改訂單表形成數據異常。這裏用innodb_lock 表演示InnoDB的行鎖。表中有三個字段:id,k(key值),v(value值)。表在github上:https://github.com/ITDragonBlog/daydayup/tree/master/MySQL/
步驟:
第一步:鏈接數據庫,這裏爲了方便區分命名爲Transaction-A,設置autocommit爲零,表示需手動提交事務。
第二步:Transaction-A,執行update修改id爲1的命令。
第三步:新增一個鏈接,命名爲Transaction-B,能正常修改id爲2的數據。再執行修改id爲1的數據命令時,卻發現該命令一直處理阻塞等待中。
第四步:Transaction-A,執行commit命令。Transaction-B,修改id爲1的命令自動執行,等待37.51秒。github

總結:多個事務操做同一行數據時,後來的事務處於阻塞等待狀態。這樣能夠避免了髒讀等數據一致性的問題。後來的事務能夠操做其餘行數據,解決了表鎖高併發性能低的問題sql

# Transaction-A
mysql> set autocommit = 0;
mysql> update innodb_lock set v='1001' where id=1;
mysql> commit;

# Transaction-B
mysql> update innodb_lock set v='2001' where id=2;
Query OK, 1 row affected (0.37 sec)
mysql> update innodb_lock set v='1002' where id=1;
Query OK, 1 row affected (37.51 sec)

有了上面的模擬操做,結果和理論又驚奇的一致,彷佛能夠放心大膽的實戰。。。。。。但現實真的很殘酷。數據庫

現實:當執行批量修改數據腳本的時候,行鎖升級爲表鎖。其餘對訂單的操做都處於等待中,,,
緣由:InnoDB只有在經過索引條件檢索數據時使用行級鎖,不然使用表鎖!而模擬操做正是經過id去做爲檢索條件,而id又是MySQL自動建立的惟一索引,因此才忽略了行鎖變表鎖的狀況。
步驟:
第一步:還原問題,Transaction-A,經過k=1更新v。Transaction-B,經過k=2更新v,命令處於阻塞等待狀態。
第二步:處理問題,給須要做爲查詢條件的字段添加索引。用完後能夠刪掉。併發

總結:InnoDB的行鎖是針對索引加的鎖,不是針對記錄加的鎖。而且該索引不能失效,不然都會從行鎖升級爲表鎖。索引失效的緣由在上一章節中已經介紹:http://www.cnblogs.com/itdragon/p/8146439.html高併發

Transaction-A
mysql> update innodb_lock set v='1002' where k=1;
mysql> commit;
mysql> create index idx_k on innodb_lock(k);

Transaction-B
mysql> update innodb_lock set v='2002' where k=2;
Query OK, 1 row affected (19.82 sec)

從上面的案例看出,行鎖變表鎖彷佛是一個坑,可MySQL沒有這麼無聊給你挖坑。這是由於MySQL有本身的執行計劃。性能

當你須要更新一張較大表的大部分甚至全表的數據時。而你又傻乎乎地用索引做爲檢索條件。一不當心開啓了行鎖(沒毛病啊!保證數據的一致性!)。可MySQL卻認爲大量對一張表使用行鎖,會致使事務執行效率低,從而可能形成其餘事務長時間鎖等待和更多的鎖衝突問題,性能嚴重降低。因此MySQL會將行鎖升級爲表鎖,即實際上並無使用索引。
咱們仔細想一想也能理解,既然整張表的大部分數據都要更新數據,一行一行地加鎖效率則更低。其實咱們能夠經過explain命令查看MySQL的執行計劃,你會發現key爲null。代表MySQL實際上並無使用索引,行鎖升級爲表鎖也和上面的結論一致。學習

本章重點介紹InnoDB的行鎖及其相關的事務知識。若是想了解MySQL的執行計劃,請看上一章節

行鎖

行鎖的劣勢:開銷大;加鎖慢;會出現死鎖
行鎖的優點:鎖的粒度小,發生鎖衝突的機率低;處理併發的能力強
加鎖的方式:自動加鎖。對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖;對於普通SELECT語句,InnoDB不會加任何鎖;固然咱們也能夠顯示的加鎖:
共享鎖:select * from tableName where ... + lock in share more
排他鎖:select * from tableName where ... + for update
InnoDB和MyISAM的最大不一樣點有兩個:一,InnoDB支持事務(transaction);二,默認採用行級鎖。加鎖能夠保證事務的一致性,可謂是有人(鎖)的地方,就有江湖(事務);咱們先簡單瞭解一下事務知識。

MySQL 事務屬性

事務是由一組SQL語句組成的邏輯處理單元,事務具備ACID屬性。
原子性(Atomicity):事務是一個原子操做單元。在當時原子是不可分割的最小元素,其對數據的修改,要麼所有成功,要麼所有都不成功。
一致性(Consistent):事務開始到結束的時間段內,數據都必須保持一致狀態。
隔離性(Isolation):數據庫系統提供必定的隔離機制,保證事務在不受外部併發操做影響的"獨立"環境執行。
持久性(Durable):事務完成後,它對於數據的修改是永久性的,即便出現系統故障也可以保持。

事務常見問題

更新丟失(Lost Update)
緣由:當多個事務選擇同一行操做,而且都是基於最初選定的值,因爲每一個事務都不知道其餘事務的存在,就會發生更新覆蓋的問題。類比github提交衝突。

髒讀(Dirty Reads)
緣由:事務A讀取了事務B已經修改但還沒有提交的數據。若事務B回滾數據,事務A的數據存在不一致性的問題。

不可重複讀(Non-Repeatable Reads)
緣由:事務A第一次讀取最初數據,第二次讀取事務B已經提交的修改或刪除數據。致使兩次讀取數據不一致。不符合事務的隔離性。

幻讀(Phantom Reads)
緣由:事務A根據相同條件第二次查詢到事務B提交的新增數據,兩次數據結果集不一致。不符合事務的隔離性。

幻讀和髒讀有點相似
髒讀是事務B裏面修改了數據,
幻讀是事務B裏面新增了數據。

事務的隔離級別

數據庫的事務隔離越嚴格,併發反作用越小,但付出的代價也就越大。這是由於事務隔離實質上是將事務在必定程度上"串行"進行,這顯然與"併發"是矛盾的。根據本身的業務邏輯,權衡能接受的最大反作用。從而平衡了"隔離" 和 "併發"的問題。MySQL默認隔離級別是可重複讀。
髒讀,不可重複讀,幻讀,其實都是數據庫讀一致性問題,必須由數據庫提供必定的事務隔離機制來解決。

+------------------------------+---------------------+--------------+--------------+--------------+
| 隔離級別                      | 讀數據一致性         | 髒讀         | 不可重複 讀   | 幻讀         |
+------------------------------+---------------------+--------------+--------------+--------------+
| 未提交讀(Read uncommitted)    | 最低級別            | 是            | 是           | 是           | 
+------------------------------+---------------------+--------------+--------------+--------------+
| 已提交讀(Read committed)      | 語句級              | 否           | 是           | 是           |
+------------------------------+---------------------+--------------+--------------+--------------+
| 可重複讀(Repeatable read)     | 事務級              | 否           | 否           | 是           |
+------------------------------+---------------------+--------------+--------------+--------------+
| 可序列化(Serializable)        | 最高級別,事務級     | 否           | 否           | 否           |
+------------------------------+---------------------+--------------+--------------+--------------+

查看當前數據庫的事務隔離級別:show variables like 'tx_isolation';

mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+

間隙鎖

當咱們用範圍條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;對於鍵值在條件範圍內但並不存在的記錄,叫作"間隙(GAP)"。InnoDB也會對這個"間隙"加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。

Transaction-A
mysql> update innodb_lock set k=66 where id >=6;
Query OK, 1 row affected (0.63 sec)
mysql> commit;

Transaction-B
mysql> insert into innodb_lock (id,k,v) values(7,'7','7000');
Query OK, 1 row affected (18.99 sec)

危害(坑):若執行的條件是範圍過大,則InnoDB會將整個範圍內全部的索引鍵值所有鎖定,很容易對性能形成影響

排他鎖

排他鎖,也稱寫鎖,獨佔鎖,當前寫操做沒有完成前,它會阻斷其餘寫鎖和讀鎖。
排他鎖

# Transaction_A
mysql> set autocommit=0;
mysql> select * from innodb_lock where id=4 for update;
+----+------+------+
| id | k    | v    |
+----+------+------+
|  4 | 4    | 4000 |
+----+------+------+
1 row in set (0.00 sec)

mysql> update innodb_lock set v='4001' where id=4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.04 sec)
# Transaction_B
mysql> select * from innodb_lock where id=4 for update;
+----+------+------+
| id | k    | v    |
+----+------+------+
|  4 | 4    | 4001 |
+----+------+------+
1 row in set (9.53 sec)

共享鎖

共享鎖,也稱讀鎖,多用於判斷數據是否存在,多個讀操做能夠同時進行而不會互相影響。當若是事務對讀鎖進行修改操做,極可能會形成死鎖。以下圖所示。
共享鎖

# Transaction_A
mysql> set autocommit=0;
mysql> select * from innodb_lock where id=4 lock in share mode;
+----+------+------+
| id | k    | v    |
+----+------+------+
|  4 | 4    | 4001 |
+----+------+------+
1 row in set (0.00 sec)

mysql> update innodb_lock set v='4002' where id=4;
Query OK, 1 row affected (31.29 sec)
Rows matched: 1  Changed: 1  Warnings: 0
# Transaction_B
mysql> set autocommit=0;
mysql> select * from innodb_lock where id=4 lock in share mode;
+----+------+------+
| id | k    | v    |
+----+------+------+
|  4 | 4    | 4001 |
+----+------+------+
1 row in set (0.00 sec)

mysql> update innodb_lock set v='4002' where id=4;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

分析行鎖定

經過檢查InnoDB_row_lock 狀態變量分析系統上的行鎖的爭奪狀況 show status like 'innodb_row_lock%'

mysql> show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name                 | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0     |
| Innodb_row_lock_time          | 0     |
| Innodb_row_lock_time_avg      | 0     |
| Innodb_row_lock_time_max      | 0     |
| Innodb_row_lock_waits         | 0     |
+-------------------------------+-------+

innodb_row_lock_current_waits: 當前正在等待鎖定的數量
innodb_row_lock_time: 從系統啓動到如今鎖定總時間長度;很是重要的參數,
innodb_row_lock_time_avg: 每次等待所花平均時間;很是重要的參數,
innodb_row_lock_time_max: 從系統啓動到如今等待最常的一次所花的時間;
innodb_row_lock_waits: 系統啓動後到如今總共等待的次數;很是重要的參數。直接決定優化的方向和策略。

行鎖優化

1 儘量讓全部數據檢索都經過索引來完成,避免無索引行或索引失效致使行鎖升級爲表鎖。
2 儘量避免間隙鎖帶來的性能降低,減小或使用合理的檢索範圍。
3 儘量減小事務的粒度,好比控制事務大小,而從減小鎖定資源量和時間長度,從而減小鎖的競爭等,提供性能。
4 儘量低級別事務隔離,隔離級別越高,併發的處理能力越低。

表鎖

表鎖的優點:開銷小;加鎖快;無死鎖
表鎖的劣勢:鎖粒度大,發生鎖衝突的機率高,併發處理能力低
加鎖的方式:自動加鎖。查詢操做(SELECT),會自動給涉及的全部表加讀鎖,更新操做(UPDATE、DELETE、INSERT),會自動給涉及的表加寫鎖。也能夠顯示加鎖:
共享讀鎖:lock table tableName read;
獨佔寫鎖:lock table tableName write;
批量解鎖:unlock tables;

共享讀鎖

對MyISAM表的讀操做(加讀鎖),不會阻塞其餘進程對同一表的讀操做,但會阻塞對同一表的寫操做。只有當讀鎖釋放後,才能執行其餘進程的寫操做。在鎖釋放前不能取其餘表。
讀鎖

Transaction-A
mysql> lock table myisam_lock read;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from myisam_lock;
9 rows in set (0.00 sec)

mysql> select * from innodb_lock;
ERROR 1100 (HY000): Table 'innodb_lock' was not locked with LOCK TABLES

mysql> update myisam_lock set v='1001' where k='1';
ERROR 1099 (HY000): Table 'myisam_lock' was locked with a READ lock and can't be updated

mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
Transaction-B
mysql> select * from myisam_lock;
9 rows in set (0.00 sec)

mysql> select * from innodb_lock;
8 rows in set (0.01 sec)

mysql> update myisam_lock set v='1001' where k='1';
Query OK, 1 row affected (18.67 sec)

獨佔寫鎖

對MyISAM表的寫操做(加寫鎖),會阻塞其餘進程對同一表的讀和寫操做,只有當寫鎖釋放後,纔會執行其餘進程的讀寫操做。在鎖釋放前不能寫其餘表。
寫鎖

Transaction-A
mysql> set autocommit=0;
Query OK, 0 rows affected (0.05 sec)

mysql> lock table myisam_lock write;
Query OK, 0 rows affected (0.03 sec)

mysql> update myisam_lock set v='2001' where k='2';
Query OK, 1 row affected (0.00 sec)

mysql> select * from myisam_lock;
9 rows in set (0.00 sec)

mysql> update innodb_lock set v='1001' where k='1';
ERROR 1100 (HY000): Table 'innodb_lock' was not locked with LOCK TABLES

mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
Transaction-B
mysql> select * from myisam_lock;
9 rows in set (42.83 sec)

總結:表鎖,讀鎖會阻塞寫,不會阻塞讀。而寫鎖則會把讀寫都阻塞

查看加鎖狀況

show open tables; 1表示加鎖,0表示未加鎖。

mysql> show open tables where in_use > 0;
+----------+-------------+--------+-------------+
| Database | Table       | In_use | Name_locked |
+----------+-------------+--------+-------------+
| lock     | myisam_lock |      1 |           0 |
+----------+-------------+--------+-------------+

分析表鎖定

能夠經過檢查table_locks_waited 和 table_locks_immediate 狀態變量分析系統上的表鎖定:show status like 'table_locks%'

mysql> show status like 'table_locks%';
+----------------------------+-------+
| Variable_name              | Value |
+----------------------------+-------+
| Table_locks_immediate      | 104   |
| Table_locks_waited         | 0     |
+----------------------------+-------+

table_locks_immediate: 表示當即釋放表鎖數。
table_locks_waited: 表示須要等待的表鎖數。此值越高則說明存在着越嚴重的表級鎖爭用狀況。

此外,MyISAM的讀寫鎖調度是寫優先,這也是MyISAM不適合作寫爲主表的存儲引擎。由於寫鎖後,其餘線程不能作任何操做,大量的更新會使查詢很可貴到鎖,從而形成永久阻塞。

什麼場景下用表鎖

InnoDB默認採用行鎖,在未使用索引字段查詢時升級爲表鎖。MySQL這樣設計並非給你挖坑。它有本身的設計目的。
即使你在條件中使用了索引字段,MySQL會根據自身的執行計劃,考慮是否使用索引(因此explain命令中會有possible_key 和 key)。若是MySQL認爲全表掃描效率更高,它就不會使用索引,這種狀況下InnoDB將使用表鎖,而不是行鎖。所以,在分析鎖衝突時,別忘了檢查SQL的執行計劃,以確認是否真正使用了索引。

第一種狀況:全表更新。事務須要更新大部分或所有數據,且表又比較大。若使用行鎖,會致使事務執行效率低,從而可能形成其餘事務長時間鎖等待和更多的鎖衝突。

第二種狀況:多表級聯。事務涉及多個表,比較複雜的關聯查詢,極可能引發死鎖,形成大量事務回滾。這種狀況若能一次性鎖定事務涉及的表,從而能夠避免死鎖、減小數據庫因事務回滾帶來的開銷。

頁鎖

開銷和加鎖時間介於表鎖和行鎖之間;會出現死鎖;鎖定粒度介於表鎖和行鎖之間,併發處理能力通常。只需瞭解一下。

總結

1 InnoDB 支持表鎖和行鎖,使用索引做爲檢索條件修改數據時採用行鎖,不然採用表鎖。
2 InnoDB 自動給修改操做加鎖,給查詢操做不自動加鎖
3 行鎖可能由於未使用索引而升級爲表鎖,因此除了檢查索引是否建立的同時,也須要經過explain執行計劃查詢索引是否被實際使用。
4 行鎖相對於表鎖來講,優點在於高併發場景下表現更突出,畢竟鎖的粒度小。
5 當表的大部分數據須要被修改,或者是多表複雜關聯查詢時,建議使用表鎖優於行鎖。
6 爲了保證數據的一致完整性,任何一個數據庫都存在鎖定機制。鎖定機制的優劣直接影響到一個數據庫的併發處理能力和性能。

到這裏,Mysql的表鎖和行鎖機制就介紹完了,若你不清楚InnoDB的行鎖會升級爲表鎖,那之後會吃大虧的。如有打什麼不對的地方請指正。若以爲文章不錯,麻煩點個贊!來都來了,留下你的痕跡吧!

相關文章
相關標籤/搜索