天天看點

不可思議的一緻性讀場景

偶爾會被問到,老葉你上課是不是很簡單,隻要一份教材在手就可以反複講很多年,甚至會問,你上課是不是隻要照着念PPT就行?

我呸,都什麼年代了,怎麼還有這種想法。哪怕是在應試教育著稱的中國小裡,也是每個學期都要更新備課材料的,怎麼可能一份教案講一輩子,無非是中國小的課程内容變化沒那麼快。在以知識更新日新月異的IT行業,居然還有人抱着這種思想,簡直了。

抱怨歸抱怨,今天我要說一個在上課過程中被同學們問倒(是真的把我問倒了)的一個案例。

先交代下運作環境:

# MySQL版本:8.0.17 under MacOS
[[email protected]]>\s
mysql  Ver 8.0.17 for macos10.14 on x86_64 (MySQL Community Server - GPL)

Connection id:      19
...
Server version:     8.0.17 MySQL Community Server - GPL

# 事務隔離級别:RR
[[email protected]]> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ         |
+-------------------------+

# 測試表結構及資料
[[email protected]]> SHOW CREATE TABLE t1\G
**************** 1. row ****************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `c1` int(11) NOT NULL,
  `c2` int(11) DEFAULT NULL,
  `c3` int(11) DEFAULT NULL,
  PRIMARY KEY (`c1`), -- c1列是主鍵
  KEY `c2` (`c2`)  -- c2列是輔助索引
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

[[email protected]]> select * from t1;
+----+------+------+
| c1 | c2   | c3   |
+----+------+------+
|  0 |    0 |    0 |
|  1 |    1 |    1 |
|  2 |    2 |    2 |
|  3 |    3 |    3 |
+----+------+------+      

好,表演開始。

session1 session2

begin;

#發起快照讀

select * from t1 where c2=2;

...

2 | 2 | 2

#更新後立即送出事務

update t1 set c1=30 where c2=2;

commit;

30 | 2 | 2

#再讀一次,确認保持一緻性

#再來一次目前讀

select * from t1 where c2=2 for update;

#恢複快照讀

#更新資料

update t1 set c1=c1+1 where c2=2;

Rows matched: 1 Changed: 1 Warnings: 0

#更新完畢後讀取

31 | 2 | 2

#神奇的一幕發生了,可以看到新舊兩條記錄

#送出事務後再讀取

#這次正常了,隻能看到最新版本的資料

好,表演結束。相信看完後,你跟我的第一反應都是“握了個草,為毛會這樣,這不科學”,可事實上的确如此,我再三測試了幾次,都确認是這樣的結果。後來我請教了下InnoDB核心開發者之一蘇斌老師(蘇斌老師之前在知數堂做過一次公開課分享,主題是 MySQL 8.0 InnoDB新特性)。一開始他也覺得這個案例不太可思議,後來經過查閱确認,認為這是符合一緻性讀的規則,看文檔的解釋:

A consistent read means that InnoDB uses multi-versioning to present to
a query a snapshot of the database at a point in time.

The query sees the changes made by transactions that committed before that
point of time, and no changes made by later or uncommitted transactions.

# 注意從這段開始的說明
The exception to this rule is that the query sees the changes made by earlier
statements within the same transaction.

This exception causes the following anomaly: If you update some rows in a
table, a SELECT sees the latest version of the updated rows, but it might
also see older versions of any rows.

If other sessions simultaneously update the same table, the anomaly means
that you might see the table in a state that never existed in the database.      

簡言之,上述文檔說明了幾點:

  1. InnoDB利用MVCC機制保證在事務範圍内任意時間點的一緻性讀需求(也就是:RR級别下,在同一個事務内任意時間點的一緻性讀,總是能讀取到同樣的資料)
  2. RR級别下,是在發起第一個SELECT(不包含SELECT ... FOR UPDATE/FOR SHARE這種加鎖讀,以後另起一篇說這個事)時,建立的快照,是以能讀取到在此之前已經送出的事務資料,在本事務之後修改的事務資料是看不到的
  3. 上述第2條規則的一個例外場景時,能讀取到在本事務内自己修改的資料。是以當在事務内更新完一條記錄後發起SELECT可以讀取到更新後的資料,同時也可能讀取到舊版本的資料

在本案例中,由于兩個session都是直接更新主鍵列,又由于InnoDB引擎的特殊性,主鍵列會被選擇作為聚集索引。對InnoDB主鍵的更新是不能inplace的,需要新建立一條記錄。是以對主鍵索引的更新時,相當于此時同一條記錄在表内有兩個版本,一個是更新前(該版本後續會被删除),一個是更新後的,然後等待送出。在session2中,第一次SELECT後建立了一個快照版本 [2,2,2],而後的目前讀可以讀取到最新資料 [30,2,2],因為sesison1已經送出,不會被阻塞。而session2中的更新,會在目前讀的基礎上進行更新,是以更新後的版本是 [31,2,2],更新完畢後又再次進行一緻性讀,此時就可以看到新舊兩個版本的資料了(因為舊版本對本事務而言,還在快照裡)。本案例給我們的幾點啟示是

  1. 當你想更新一條記錄時,最好一開始就對其先加鎖(SELECT ... FOR UPDATE),而後再在事務中進行更新,這樣就可以避免被其他session給更新了。雖然一開始就加鎖可能會造成更多的鎖等待和死鎖機率,但為了資料一緻性,也必須如此了。或者,可以把事務隔離級别降為RC,這樣每次SELECT總能看到已送出的最新版本
  2. “永遠”不要更新主鍵列
  3. 想要做到第2點,就需要讓主鍵列“隻用作主鍵”,不具備業務屬性,也即是我們一直強調的一個開發規範“每個InnoDB表都要有一個自增整型列做主鍵,且該列沒有業務用途”

不知道我這樣解釋清楚了沒有。InnoDB的這種做法,看起來像是合理的,但仔細想想又好像不太合理,我去提了個bug(#96205),但被拒了,囧...

Enjoy MySQL :)

繼續閱讀