MySQL鎖相關

前言

關於數據庫鎖,是一個很重要的知識點;mysql

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

通常也就聽過常說的樂觀鎖和悲觀鎖,瞭解過基本的含義以後就沒了,沒有去實際的操做過,本文將簡單的整理一下數據庫鎖的知識,但願對你們有所幫助;sql

引入

本文參考文章:數據庫的兩大神器數據庫

數據庫鎖

簡介

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

以上的一大堆鎖可能不少人都只是知道一些概念,可是咱們的程序在通常狀況下仍是能夠跑得好好的。由於這些鎖數據庫隱式幫咱們加了:post

  • 對於UPDATE、DELETE、INSERT語句,InnoDB自動給涉及數據集加排他鎖(X),也就是咱們常說的寫鎖;
  • MyISAM在執行查詢語句SELECT前,會自動給涉及的全部表加讀鎖,在執行更新操做(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不須要用戶干預

表鎖和行鎖

從鎖的粒度咱們能夠分爲兩大類,它們各自的特色以下:學習

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

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

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

表鎖

表鎖也分爲兩種模式:spa

  • 表讀鎖(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:只支持表鎖;

數據庫鎖從鎖的粒度咱們能夠分爲兩大類,它們各自的特色以下::

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

悲觀鎖:老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據;

樂觀鎖:老是假設最好的狀況,每次去拿數據的時候都認爲別人不會修改,因此不會上鎖,可是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據;

最後

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

你們能夠去看一下原文,本人也是小菜雞一枚,說的有問題還望你們指出來;

你們共同窗習,一塊兒進步。

相關文章
相關標籤/搜索