MySQL介於普通讀和加鎖讀之間的讀取方式:semi-consistent read

標籤: 公衆號文章mysql


在閱讀本文前最好先看過三篇語句加鎖分析文章:程序員

超全面MySQL語句加鎖分析(上篇)sql

超全面MySQL語句加鎖分析(中篇)bash

超全面MySQL語句加鎖分析(下篇)學習

事前準備

爲了故事的順利發展,咱們先建一個表,並向表中插入一些記錄,下邊是SQL語句:測試

CREATE TABLE hero (
    number INT,
    name VARCHAR(100),
    country varchar(100),
    PRIMARY KEY (number),
    KEY idx_name (name)
) Engine=InnoDB CHARSET=utf8;

INSERT INTO hero VALUES
    (1, 'l劉備', '蜀'),
    (3, 'z諸葛亮', '蜀'),
    (8, 'c曹操', '魏'),
    (15, 'x荀彧', '魏'),
    (20, 's孫權', '吳');
複製代碼

如今hero表中的記錄狀況就以下所示:fetch

mysql> SELECT * FROM hero;
+--------+------------+---------+
| number | name       | country |
+--------+------------+---------+
|      1 | l劉備      | 蜀      |
|      3 | z諸葛亮    | 蜀      |
|      8 | c曹操      | 魏      |
|     15 | x荀彧      | 魏      |
|     20 | s孫權      | 吳      |
+--------+------------+---------+
5 rows in set (0.01 sec)
複製代碼

現象

在小冊答疑羣裏有一位同窗提了一個問題:說是在READ COMMITTED隔離級別下發生了一件百思不得其解的事兒。好的,首先構造環境,將當前會話默認的隔離級別設置成READ COMMITTED優化

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
複製代碼

事務T1先執行:ui

# T1中,隔離級別爲READ COMMITTED
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM hero WHERE country = '魏' FOR UPDATE;
+--------+---------+---------+
| number | name    | country |
+--------+---------+---------+
|      8 | c曹操   | 魏      |
|     15 | x荀彧   | 魏      |
+--------+---------+---------+
2 rows in set (0.01 sec)
複製代碼

country列並非索引列,因此本條語句執行時確定是使用掃描聚簇索引的全表掃描方式來執行,EXPLAIN語句也證實了咱們的想法:spa

mysql> EXPLAIN SELECT * FROM hero WHERE country = '魏' FOR UPDATE;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | hero  | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    5 |    20.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.02 sec)
複製代碼

咱們以前學過MySQL語句的加鎖分析,知道READ COMMITTED隔離級別下,若是採用全表掃描的方式執行查詢語句時,InnoDB存儲引擎將依次對每條記錄加正經記錄鎖,在server層測試該記錄是否符合WHERE條件,若是不符合則將加在該記錄上的鎖釋放掉。本例中使用FOR UPDATE語句,確定加的是X型正經記錄鎖。只有兩條記錄符合WHERE條件,因此最終其實只對這兩條符合條件的記錄加了X型正經記錄鎖(就是number列值爲815的兩條記錄)。固然,咱們可使用SHOW ENGINE INNODB STATUS命令證實咱們的分析:

mysql> SHOW ENGINE INNODB STATUS\G
... 省略了不少內容

------------
TRANSACTIONS
------------
Trx id counter 39764
Purge done for trx's n:o < 39763 undo n:o < 0 state: running but idle History list length 36 Total number of lock structs in row lock hash table 1 LIST OF TRANSACTIONS FOR EACH SESSION: ---TRANSACTION 281479653009568, not started 0 lock struct(s), heap size 1160, 0 row lock(s) ---TRANSACTION 281479653012832, not started 0 lock struct(s), heap size 1160, 0 row lock(s) ---TRANSACTION 39763, ACTIVE 468 sec 2 lock struct(s), heap size 1160, 2 row lock(s) MySQL thread id 19, OS thread handle 123145470611456, query id 586 localhost 127.0.0.1 root TABLE LOCK table `xiaohaizi`.`hero` trx id 39763 lock mode IX RECORD LOCKS space id 287 page no 3 n bits 72 index PRIMARY of table `xiaohaizi`.`hero` trx id 39763 lock_mode X locks rec but not gap Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 80000008; asc ;; 1: len 6; hex 000000009b4a; asc J;; 2: len 7; hex 80000001d3012a; asc *;; 3: len 7; hex 63e69bb9e6938d; asc c ;; 4: len 3; hex e9ad8f; asc ;; Record lock, heap no 5 PHYSICAL RECORD: n_fields 5; compact format; info bits 0 0: len 4; hex 8000000f; asc ;; 1: len 6; hex 000000009b4a; asc J;; 2: len 7; hex 80000001d30137; asc 7;; 3: len 7; hex 78e88d80e5bda7; asc x ;; 4: len 3; hex e9ad8f; asc ;; ... 省略了不少內容 複製代碼

其中id39763的事務就是指T1,能夠看出它爲heap no值爲45的兩條記錄加了X型正經記錄鎖(lock_mode X locks rec but not gap)。

而後再開啓一個隔離級別也爲READ COMMITTED的事務T2,在其中執行:

# T2中,隔離級別爲READ COMMITTED
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM hero WHERE country = '吳' FOR UPDATE;
(進入阻塞狀態)
複製代碼

很顯然,這條語句也會採用全表掃描的方式來執行,會依次去獲取每一條聚簇索引記錄的鎖。不過由於number值爲8的記錄已經被T1加了X型正經記錄鎖T2想得卻得不到,只能眼巴巴的進行阻塞狀態,此時的SHOW ENGINE INNODB STATUS也能證實咱們的猜測(只截取了一部分):

---TRANSACTION 39764, ACTIVE 34 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1160, 1 row lock(s)
MySQL thread id 20, OS thread handle 123145471168512, query id 590 localhost 127.0.0.1 root Sending data
SELECT * FROM hero WHERE country = '吳' FOR UPDATE
------- TRX HAS BEEN WAITING 34 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 287 page no 3 n bits 72 index PRIMARY of table `xiaohaizi`.`hero` trx id 39764 lock_mode X locks rec but not gap waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
 0: len 4; hex 80000008; asc     ;;
 1: len 6; hex 000000009b4a; asc      J;;
 2: len 7; hex 80000001d3012a; asc       *;;
 3: len 7; hex 63e69bb9e6938d; asc c      ;;
 4: len 3; hex e9ad8f; asc    ;;
複製代碼

能夠看到T2正在等待獲取heap no4的記錄上的X型正經記錄鎖(lock_mode X locks rec but not gap waiting)。

以上是很正常的阻塞邏輯,咱們均可以分析出來,不過若是在T2中執行下邊的UPDATE語句:

# T2中,隔離級別爲READ COMMITTED
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)

mysql> UPDATE hero SET name = 'xxx' WHERE country = '吳';
Query OK, 1 row affected (0.02 sec)
Rows matched: 1  Changed: 1  Warnings: 0
複製代碼

WTF? 居然沒有阻塞,就這麼隨意地執行成功了?一樣的WHERE條件,一樣的執行計劃,怎麼SELECT ... FOR UPDATEUPDATE語句的加鎖狀況不同?

緣由

哈哈,是的,的確不同。其實MySQL支持3種類型的讀語句:

  • 普通讀(也稱一致性讀,英文名:Consistent Read)。

    這個就是指普通的SELECT語句,在末尾不加FOR UPDATE或者LOCK IN SHARE MODE的SELECT語句。普通讀的執行方式是生成ReadView直接利用MVCC機制來進行讀取,並不會對記錄進行加鎖。

    小貼士: 對於SERIALIZABLE隔離級別來講,若是autocommit系統變量被設置爲OFF,那普通讀的語句會轉變爲鎖定讀,和在普通的SELECT語句後邊加LOCK IN SHARE MODE達成的效果同樣。

  • 鎖定讀(英文名:Locking Read)。

    這個就是事務在讀取記錄以前,須要先獲取該記錄對應的鎖。固然,獲取什麼類型的鎖取決於當前事務的隔離級別、語句的執行計劃、查詢條件等因素,具體可參見:

  • 半一致性讀(英文名:Semi-Consistent Read)。

    這是一種夾在普通讀和鎖定讀之間的一種讀取方式。它只在READ COMMITTED隔離級別下(或者在開啓了innodb_locks_unsafe_for_binlog系統變量的狀況下)使用UPDATE語句時纔會使用。具體的含義就是當UPDATE語句讀取已經被其餘事務加了鎖的記錄時,InnoDB會將該記錄的最新提交的版本讀出來,而後判斷該版本是否與UPDATE語句中的WHERE條件相匹配,若是不匹配則不對該記錄加鎖,從而跳到下一條記錄;若是匹配則再次讀取該記錄並對其進行加鎖。這樣子處理只是爲了讓UPDATE語句儘可能少被別的語句阻塞。

    小貼士: 半一致性讀只適用於對聚簇索引記錄加鎖的狀況,並不適用於對二級索引記錄加鎖的狀況。

很顯然,咱們上邊所嘮叨的例子中是由於事務T2執行UPDATE語句時使用了半一致性讀,判斷number列值爲815這兩條記錄的最新提交版本的country列值均不爲UPDATE語句中WHERE條件中的'吳',因此直接就跳過它們,不對它們加鎖了。

本知識點容易被忽略,各位同窗在工做過程當中分析的時候別忘記考慮一下Semi-Consistent Read喔,碼字不易,有幫助幫着轉發喔,麼麼噠~

小冊

想看更多MySQL進階知識能夠到小冊中查看:《MySQL是怎樣運行的:從根兒上理解MySQL》的連接 。小冊的內容主要是從小白的角度出發,用比較通俗的語言講解關於MySQL進階的一些核心概念,好比記錄、索引、頁面、表空間、查詢優化、事務和鎖等,總共的字數大約是四十多萬字,配有上百幅原創插圖。主要是想下降普通程序員學習MySQL進階的難度,讓學習曲線更平滑一點~

題外話

寫文章挺累的,有時候你以爲閱讀挺流暢的,那實際上是背後無數次修改的結果。若是你以爲不錯請幫忙轉發一下,萬分感謝~ 這裏是個人公衆號「咱們都是小青蛙」,裏邊有更多技術乾貨,時不時扯一下犢子,歡迎關注:

相關文章
相關標籤/搜索