MySQL鎖相關

你的益達_發表於2019-04-15

前言

關於資料庫鎖,是一個很重要的知識點;

不少人在開發的時候,應該很少會注意到這些鎖的問題,也很少會給程式加鎖(除了庫存這些對數量準確性要求極高的情況下);

一般也就聽過常說的樂觀鎖和悲觀鎖,瞭解過基本的含義之後就沒了,沒有去實際的操作過,本文將簡單的整理一下資料庫鎖的知識,希望對大家有所幫助;

引入

本文參考文章:資料庫的兩大神器

資料庫鎖

簡介

在MySQL中鎖看起來是很複雜的,因為有一大堆的東西和名詞:排它鎖,共享鎖,表鎖,頁鎖,間隙鎖,意向排它鎖,意向共享鎖,行鎖,讀鎖,寫鎖,樂觀鎖,悲觀鎖,死鎖。這些名詞有的部落格又直接寫鎖的英文的簡寫--->X鎖,S鎖,IS鎖,IX鎖,MMVC等等之類。鎖的相關知識又跟儲存引擎,索引,事務的隔離級別都是關聯的;

MySQL鎖相關

以上的一大堆鎖可能很多人都只是知道一些概念,但是我們的程式在一般情況下還是可以跑得好好的。因為這些鎖資料庫隱式幫我們加了:

  • 對於UPDATE、DELETE、INSERT語句,InnoDB自動給涉及資料集加排他鎖(X),也就是我們常說的寫鎖;
  • MyISAM在執行查詢語句SELECT前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不需要使用者干預

表鎖和行鎖

從鎖的粒度我們可以分為兩大類,它們各自的特點如下:

  • 表鎖:開銷小,加鎖快;不會出現死鎖;鎖定力度大,發生鎖衝突概率高,併發度低;
  • 行鎖:開銷大,加鎖慢;會出現死鎖;鎖定力度小,發生鎖衝突的概率低,併發度高;

同樣,不同的儲存引擎支援的鎖的力度也不一樣:

  • InnoDB:表鎖行鎖都支援(InnoDB的行鎖是基於索引的 ,稍後會演示);
  • MyISAM:只支援表鎖;

表鎖

表鎖也分為兩種模式:

  • 表讀鎖(Table Read Lock)
  • 表寫鎖(Table Write Lock)
    • 讀讀不阻塞:當前使用者讀取資料,其他使用者也在讀取資料,不會加鎖;
    • 讀寫阻塞:當前使用者在讀取資料的時候,其他使用者不能修改當前使用者讀的資料;
    • 寫寫阻塞:當前使用者在修改資料,其他使用者不能修改當前使用者正在修改的資料;

總結得到:

  1. 讀讀不阻塞,讀寫阻塞,寫寫阻塞
  2. 讀鎖和寫鎖是互斥的,讀寫操作是序列
  3. 在mysql裡邊,寫鎖是優先於讀鎖的

行鎖

我們使用MySQL一般是使用的InnoDB引擎,上面也提到了InnoDB和MyISAM的一些區別:

  • InnoDB行鎖表鎖都支援,MyISAM只支援表鎖;
  • InnoDB支援事務,MyISAM不支援;

InnoDB實現了以下兩種型別的行鎖:

  • 共享鎖(s鎖):允許一個事務去讀一行,會阻止其他事務獲取相同資料集的排他鎖(讀取資料的時候不允許修改)
    • 也被稱為讀鎖:讀鎖是共享的,多個執行緒可以同時讀取統一資料集,但是不允許其他執行緒進行修改(也就是不允許其他事務獲取相同資料集的排他鎖);
  • 排他鎖(x鎖):允許獲取排他鎖去做更新操作,阻止其他事務獲取相通資料的共享鎖和排他鎖(一個事務修改資料的時候,阻止其他事務對相同資料集做更新或者查詢操作);
    • 也被稱為寫鎖:寫鎖是排他的,寫鎖會阻塞其他的寫鎖和讀鎖;

意向鎖

為了允許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖

  • 意向共享鎖(IS):事務打算給資料行加共享鎖,事務在給一個資料行加共享鎖前必須先取得該表的IS鎖;
  • 意向排他鎖(IX):事務打算給資料行加排他鎖,事務再給一個資料行加排他鎖前必須先取得該表的IX鎖;

意向鎖也是資料庫隱式幫我們做了,不需要程式設計師操心

表鎖行鎖測試

準備

上面我們提到了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;

結果:

  • 當coulmn是索引列的時候,也就是查詢走的索引的時候,這個時候鎖的就是行(行鎖);
  • 當coulmn不是索引的時候,也就是查詢沒走索引的時候,這個時候鎖的就是整個表(表鎖);

悲觀鎖

含義

悲觀鎖是從資料庫層面加鎖。總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它釋放鎖;

例子

上面其實關於行鎖和表鎖的測試那裡我們使用的排他鎖也就是悲觀鎖;

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,所以如果其他事務進行了更新,這條語句是執行不成功的;

參考文章:juejin.im/post/5b4977…

間隙鎖GAP

當我們用範圍條件檢索資料而不是相等條件檢索資料,並請求共享或排他鎖時,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通過回滾幫我們解決了不少死鎖的問題了,但死鎖是無法完全避免的,可以通過以下的經驗參考,來儘可能少遇到死鎖:

  • 1)以固定的順序訪問表和行。比如對兩個job批量更新的情形,簡單方法是對id列表先排序,後執行,這樣就避免了交叉等待鎖的情形;將兩個事務的sql順序調整為一致,也能避免死鎖。
  • 2)大事務拆小。大事務更傾向於死鎖,如果業務允許,將大事務拆小。
  • 3)在同一個事務中,儘可能做到一次鎖定所需要的所有資源,減少死鎖概率。
  • 4)降低隔離級別。如果業務允許,將隔離級別調低也是較好的選擇,比如將隔離級別從RR調整為RC,可以避免掉很多因為gap鎖造成的死鎖。
  • 5)為表新增合理的索引。可以看到如果不走索引將會為表的每一行記錄新增上鎖,死鎖的概率大大增大。

總結

本文介紹了MySQL資料鎖以及事務的一些知識點,下面我們來總結一下;

不同的儲存引擎支援的鎖的力度也不一樣:

  • InnoDB:表鎖行鎖都支援(也做了演示);
    • 當查詢走的索引的時候,這個時候鎖的就是行;
    • 當查詢沒走的索引的時候,這個時候鎖的就是表;
  • MyISAM:只支援表鎖;

資料庫鎖從鎖的粒度我們可以分為兩大類,它們各自的特點如下::

  • 表鎖:開銷小,加鎖快;不會出現死鎖;鎖定力度大,發生鎖衝突概率高,併發度低;
  • 行鎖:開銷大,加鎖慢;會出現死鎖;鎖定力度小,發生鎖衝突的概率低,併發度高;

悲觀鎖:總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料;

樂觀鎖:總是假設最好的情況,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料;

最後

最後說一下,本文的參考文章:資料庫的兩大神器

大家可以去看一下原文,本人也是小菜雞一枚,說的有問題還望大家指出來;

大家共同學習,一起進步。

相關文章