關於數據庫鎖,是一個很重要的知識點;mysql
很多人在開發的時候,應該不多會注意到這些鎖的問題,也不多會給程序加鎖(除了庫存這些對數量準確性要求極高的狀況下);程序員
通常也就聽過常說的樂觀鎖和悲觀鎖,瞭解過基本的含義以後就沒了,沒有去實際的操做過,本文將簡單的整理一下數據庫鎖的知識,但願對你們有所幫助;sql
本文參考文章:數據庫的兩大神器數據庫
在MySQL中鎖看起來是很複雜的,由於有一大堆的東西和名詞:排它鎖,共享鎖,表鎖,頁鎖,間隙鎖,意向排它鎖,意向共享鎖,行鎖,讀鎖,寫鎖,樂觀鎖,悲觀鎖,死鎖。這些名詞有的博客又直接寫鎖的英文的簡寫--->X鎖,S鎖,IS鎖,IX鎖,MMVC等等之類。鎖的相關知識又跟存儲引擎,索引,事務的隔離級別都是關聯的;併發
以上的一大堆鎖可能不少人都只是知道一些概念,可是咱們的程序在通常狀況下仍是能夠跑得好好的。由於這些鎖數據庫隱式幫咱們加了:post
UPDATE、DELETE、INSERT
語句,InnoDB會自動給涉及數據集加排他鎖(X),也就是咱們常說的寫鎖;SELECT
前,會自動給涉及的全部表加讀鎖,在執行更新操做(UPDATE、DELETE、INSERT
等)前,會自動給涉及的表加寫鎖,這個過程並不須要用戶干預從鎖的粒度咱們能夠分爲兩大類,它們各自的特色以下:學習
一樣,不一樣的存儲引擎支持的鎖的力度也不同:測試
表鎖也分爲兩種模式:spa
總結獲得:線程
咱們使用MySQL通常是使用的InnoDB引擎,上面也提到了InnoDB和MyISAM的一些區別:
InnoDB實現瞭如下兩種類型的行鎖:
爲了容許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖:
意向鎖也是數據庫隱式幫咱們作了,不須要程序員操心!
上面咱們提到了InnoDB支持行鎖,可是是基於索引的狀況,下面咱們來實際的看一下:
首先咱們用客戶端鏈接上MySQL數據庫,爲了測試鎖的效果,咱們須要打開兩個或者兩個以上的客戶端(我打開了兩個)而後建立一個數據庫;
CREATE DATABASE test CHARACTER SET utf8;
複製代碼
而後咱們須要創建一個表:
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`age` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8;
複製代碼
咱們簡單的建了一個user表,表中有三個字段,其中id爲自增主鍵,你們都知道主鍵是自帶索引的,也就是聚簇索引(主鍵索引),其餘的字段都是不帶索引的。
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| user |
+----------------+
1 row in set (0.01 sec)
複製代碼
如今咱們簡單的往裏面添加幾條數據:
INSERT INTO `user`(username,age) VALUES ('tom',23),('joey',22),('James',21),('William',20),('David',24);
複製代碼
mysql> select * from user;
+----+----------+-----+
| id | username | age |
+----+----------+-----+
| 1 | tom | 23 |
| 2 | joey | 22 |
| 3 | James | 21 |
| 4 | William | 20 |
| 5 | David | 24 |
+----+----------+-----+
5 rows in set (0.00 sec)
複製代碼
好的,如今前提都已經弄好了,咱們能夠開始測試了:
咱們知道MySQL的事務是自動提交的,爲了測試,咱們須要把事務的自動提交關閉;
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.01 sec)
複製代碼
如今咱們來查看一下MySQL的事務提交狀態:
mysql> show VARIABLES like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set, 1 warning (0.04 sec)
複製代碼
從上面能夠看出,咱們把事務的自動提交已經關閉了,下面咱們開始測試(打開的窗口都須要關閉事務的自動提交);
首先,我打開了兩個窗口,分別爲A和B,如今,咱們兩個窗口的狀態都已經調整完畢(關閉事務自動提交)。咱們在A窗口,輸入如下語句:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id = 1 for update;
+----+----------+-----+
| id | username | age |
+----+----------+-----+
| 1 | tom | 23 |
+----+----------+-----+
1 row in set (0.02 sec)
mysql>
複製代碼
很明顯,以上語句中,打開了事務,而後執行了一條SQL語句,在select 語句後邊加了 for update
至關於加了排它鎖(寫鎖),加了寫鎖之後,其餘的事務就不能對它修改了!須要等待當前事務修改完提交以後才能夠修改;
如今咱們在窗口B執行相同的操做:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id = 1 for update;
-
複製代碼
注意到了嗎,窗口B並無數據出現,由於窗口A執行的時候加了排他鎖,可是窗口A並無提交事務,因此鎖也沒有獲得釋放,如今咱們在窗口A提交事務:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id = 1 for update;
+----+----------+-----+
| id | username | age |
+----+----------+-----+
| 1 | tom | 23 |
+----+----------+-----+
1 row in set (0.02 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
複製代碼
同時,窗口B出現瞭如下狀況:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id = 1 for update;
+----+----------+-----+
| id | username | age |
+----+----------+-----+
| 1 | tom | 23 |
+----+----------+-----+
1 row in set (4.34 sec)
mysql>
複製代碼
沒錯,由於窗口A提交了事務,釋放的排他鎖,因此窗口B獲取到了數據並從新爲該數據添加了排他鎖,因此此時你在A窗口在重複以前操做的時候仍是會阻塞,由於窗口B沒有提交事務,也就是沒有釋放排他鎖;
如今,咱們在窗口A執行如下語句:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id = 2 for update;
+----+----------+-----+
| id | username | age |
+----+----------+-----+
| 2 | joey | 22 |
+----+----------+-----+
1 row in set (0.00 sec)
mysql>
複製代碼
有的同窗可能會說,不對啊,我窗口B尚未提交事務,釋放排他鎖啊。
可是,你們注意看個人SQL語句,此次查的是id = 2的數據;
這是InnoDB的一大特性,我上面說了,InnoDB的行鎖是基於索引的 ,由於此時咱們的條件是基於主鍵的,而主鍵是自帶索引的,因此加的是行鎖,這個時候窗口A鎖的是id = 2的這條數據,窗口B鎖的是id = 1的這條數據,他們互不干擾;
如今,咱們再來測試一下,沒有索引,走表鎖的狀況;
咱們上面有提過,InnoDB的行鎖是基於索引,沒有索引的話,鎖住的就是整張表:
咱們在窗口A輸入執行如下操做:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where age = 20 for update;
+----+----------+-----+
| id | username | age |
+----+----------+-----+
| 4 | William | 20 |
+----+----------+-----+
1 row in set (0.04 sec)
mysql>
複製代碼
你們注意,此次的條件是使用的age,可是age是沒有索引的,因此咱們在B窗口執行相同的操做:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where age = 20 for update;
-
複製代碼
很清楚的能看到,窗口B處於阻塞狀態,咱們換個條件繼續執行:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where age = 22 for update;
-
複製代碼
一樣,儘管查詢的數據換成了age = 22,可是仍是會阻塞住,也就證實看不是鎖的行;
咱們再來試試換一個列做爲條件:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id = 1 for update;
-
複製代碼
一樣的結果,咱們如今在A窗口提交事務,再來看一下B窗口:
A:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where age = 20 for update;
+----+----------+-----+
| id | username | age |
+----+----------+-----+
| 4 | William | 20 |
+----+----------+-----+
1 row in set (0.04 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
複製代碼
B:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where id = 1 for update;
+----+----------+-----+
| id | username | age |
+----+----------+-----+
| 1 | tom | 23 |
+----+----------+-----+
1 row in set (0.00 sec)
mysql>
複製代碼
當窗口A提交事務後,也就釋放了鎖,這個時候窗口B獲取到了鎖,獲得了數據,並鎖住了id = 1的這一行數據;
關於聯合索引中,須要注意的一點就是最左匹配原則 ,說白了就是查詢是否走了索引,若是走了索引,一樣加的仍是行鎖,不然鎖的仍是表,下面咱們來看一下。首先,咱們須要把表中的username和age建一個聯合索引:
mysql> create index index_username_age on user(username,age);
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> show index from user;
+-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| user | 0 | PRIMARY | 1 | id | A | 5 | NULL | NULL | | BTREE | | |
| user | 1 | index_username_age | 1 | username | A | 4 | NULL | NULL | | BTREE | | |
| user | 1 | index_username_age | 2 | age | A | 5 | NULL | NULL | | BTREE | | |
+-------+------------+--------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
3 rows in set (0.00 sec)
mysql>
複製代碼
上面能夠看出,咱們創建聯合索引成功,下面咱們開始測試,首先,咱們在窗口A執行如下操做:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where username='tom' and age = 20 for update;
+----+----------+-----+
| id | username | age |
+----+----------+-----+
| 1 | tom | 20 |
+----+----------+-----+
1 row in set (0.00 sec)
mysql>
複製代碼
能夠看出,和咱們以前的操做沒啥兩樣,一樣是打開事務進行操做,如今咱們在窗口B執行如下操做:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where username='tom' and age = 20 for update;
-
複製代碼
很清楚的看到B窗口被鎖住了,可是咱們如今肯定的是加的鎖,並不知道是行鎖仍是表鎖,不要緊,咱們換個條件:
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user where username='joey' and age = 22 for update;
+----+----------+-----+
| id | username | age |
+----+----------+-----+
| 2 | joey | 22 |
+----+----------+-----+
1 row in set (0.00 sec)
mysql>
複製代碼
這樣,咱們很清楚的就能看到走的是行鎖了。
只不過你們要注意聯合索引的命中規則也就是最左匹配原則,咱們能夠試一試單獨使用username做爲條件看看走的什麼鎖,也能夠看看單獨使用age走的什麼鎖,這裏就再也不演示了,你們能夠自行的嘗試。
前提:必須在事務裏面
樣例:select * from table where column = condition for update;
結果:
悲觀鎖是從數據庫層面加鎖。老是假設最壞的狀況,每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會阻塞直到它釋放鎖;
上面其實關於行鎖和表鎖的測試那裏咱們使用的排他鎖也就是悲觀鎖;
select * from table where xxx for update
複製代碼
在上面咱們舉的例子夠多了,這裏再也不多說;
老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據;
表中有一個版本字段,第一次讀的時候,獲取到這個字段。處理完業務邏輯開始更新的時候,須要再次查看該字段的值是否和第一次的同樣。若是同樣就更新,反之拒絕。之因此叫樂觀,由於這個模式沒有從數據庫加鎖,等到更新的時候再判斷是否能夠更新。
update table set xxx where id = 1 and version = 1;
複製代碼
上面的語句就很清楚的說明了樂觀鎖,在對id = 1的數據進行更新的同時添加了version = 1的條件,version是當前事務開始以前查詢出來的版本號,若是這個時候其餘事務對id = 1的數據進行了更新會將version+1,因此若是其餘事務進行了更新,這條語句是執行不成功的;
當咱們用範圍條件檢索數據而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合範圍條件的已有數據記錄的索引項加鎖;對於鍵值在條件範圍內但並不存在的記錄,叫作「間隙(GAP)」。InnoDB也會對這個「間隙」加鎖,這種鎖機制就是所謂的間隙鎖。
值得注意的是:間隙鎖只會在Repeatable read
隔離級別下使用~
例子:假如emp表中只有101條記錄,其empid的值分別是1,2,...,100,101
Select * from emp where empid > 100 for update;
複製代碼
上面是一個範圍查詢,InnoDB不只會對符合條件的empid值爲101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的「間隙」加鎖。
InnoDB使用間隙鎖的目的有兩個:
Repeatable read
隔離級別下再經過GAP鎖便可避免了幻讀)併發的問題就少不了死鎖,在MySQL中一樣會存在死鎖的問題。
但通常來講MySQL經過回滾幫咱們解決了很多死鎖的問題了,但死鎖是沒法徹底避免的,能夠經過如下的經驗參考,來儘量少遇到死鎖:
本文介紹了MySQL數據鎖以及事務的一些知識點,下面咱們來總結一下;
不一樣的存儲引擎支持的鎖的力度也不同:
數據庫鎖從鎖的粒度咱們能夠分爲兩大類,它們各自的特色以下::
悲觀鎖:老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據;
樂觀鎖:老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據;
最後說一下,本文的參考文章:數據庫的兩大神器
你們能夠去看一下原文,本人也是小菜雞一枚,說的有問題還望你們指出來;
你們共同窗習,一塊兒進步。