MySQL使用可重複讀作為預設隔離級別的原因(一)
一般的DBMS系統,預設都會使用讀提交(Read-Comitted,RC)作為預設隔離級別,如Oracle、SQL Server等,而MySQL卻使用可重複讀(Read-Repeatable,RR)。要知道,越高的隔離級別,能解決的資料一致性問題越多,理論上效能損耗更大,可併發性越低。隔離級別依次為
SERIALIZABLE > RR > RC > Read-Uncommited
在SQL標準中,前三種隔離級別分別解決了幻象讀、不可重複讀和髒讀的問題。那麼,為什麼MySQL使用可重複讀作為預設隔離級別呢?
1. 從Binlog說起
Binlog是MySQL的邏輯操作日誌,廣泛應用於複製和恢復。MySQL 5.1以前,Statement是Binlog的預設格式,即依次記錄系統接受的SQL請求;5.1及以後,MySQL提供了Row和Mixed兩個Binlog格式。
從MySQL 5.1開始,如果開啟語句級Binlog,就不支援RC和Read-Uncommited隔離級別。要想使用RC隔離級別,必須使用Mixed或Row格式。
mysql> set tx_isolation='read-committed'; Query OK, 0 rows affected (0.00 sec) mysql> insert into t1 values(1,1); ERROR 1598 (HY000): Binary logging not possible. Message: Transaction level 'READ-COMMITTED' in InnoDB is not safe for binlog mode 'STATEMENT'
那麼,為什麼RC隔離級別不支援語句級Binlog呢?我們關閉binlog,做以下測試。
會話1 |
會話2 |
use test; #初始化資料 create table t1(c1 int, c2 int) engine=innodb; create table t2(c1 int, c2 int) engine=innodb;
insert into t1 values(1,1), (2,2); insert into t2 values(1,1), (2,2);
#設定隔離級別 set tx_isolation='read-committed'; Query OK, 0 rows affected (0.00 sec)
#連續更新兩次 mysql> Begin; Query OK, 0 rows affected (0.03 sec)
mysql> update t2 set c2 = 3 where c1 in (select c1 from t1); Query OK, 2 rows affected (0.00 sec) Rows matched: 2 Changed: 2 Warnings: 0
mysql> update t2 set c2 = 4 where c1 in (select c1 from t1); Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from t2; +------+------+ | c1 | c2 | +------+------+ | 1 | 4 | | 2 | 3 | +------+------+ 2 rows in set (0.00 sec)
mysql> commit; |
#設定隔離級別 set tx_isolation='read-committed'; Query OK, 0 rows affected (0.00 sec)
#兩次更新之間執行刪除 mysql> delete from t1 where c1 = 2; Query OK, 1 row affected (0.03 sec)
|
由以上測試知,RC隔離級別下,會話2執行時序在會話1事務的語句之間,並且會話2的操作影響了會話1的結果,這會對Binlog結果造成影響。
由於Binlog中語句的順序以commit為序,如果語句級Binlog允許,兩會話的執行時序是
#會話2
set tx_isolation='read-committed';
delete from t1 where c1 = 2;
commit;
#會話1
set tx_isolation='read-committed';
Begin;
update t2 set c2 = 3 where c1 in (select c1 from t1);
update t2 set c2 = 4 where c1 in (select c1 from t1);
select * from t2;
+------+------+
| c1 | c2 |
+------+------+
| 1 | 4 |
| 2 | 2 |
+------+------+
2 rows in set (0.00 sec)
commit;
由上可知,在MySQL 5.1及以上的RC隔離級別下,語句級Binlog在DR上執行的結果是不正確的!
那麼,MySQL 5.0呢?5.0允許RC下語句級Binlog,是不是說很容易產生DB/DR不一致呢?
事實上,在5.0重複上述一個測試,並不存在這個問題,原因是5.0的RC與5.1的RR使用類似的併發和上鎖機制,也就是說,MySQL 5.0的RC與5.1及以上的RC可能存在相容性問題。
下面看看RR是怎麼解決這個問題的。
2. 預設隔離級別-可重複讀
導致RC隔離級別DB/DR不一致的原因是:RC不可重複讀,而Binlog要求SQL序列化!
在RR下,重複以上測試
會話1 |
會話2 |
use test; #初始化資料 create table t1(c1 int, c2 int) engine=innodb; create table t2(c1 int, c2 int) engine=innodb;
insert into t1 values(1,1), (2,2); insert into t2 values(1,1), (2,2);
#設定隔離級別 set tx_isolation='repeatable-read'; Query OK, 0 rows affected (0.00 sec)
#連續更新兩次 mysql> Begin; Query OK, 0 rows affected (0.03 sec)
mysql> update t2 set c2 = 3 where c1 in (select c1 from t1); Query OK, 2 rows affected (0.00 sec) Rows matched: 2 Changed: 2 Warnings: 0
mysql> update t2 set c2 = 4 where c1 in (select c1 from t1); Query OK, 2 rows affected (0.00 sec) Rows matched: 2 Changed: 2 Warnings: 0
mysql> select * from t2; +------+------+ | c1 | c2 | +------+------+ | 1 | 4 | | 2 | 4 | +------+------+ 2 rows in set (0.00 sec)
mysql> commit; |
#設定隔離級別 set tx_isolation=' repeatable-read'; Query OK, 0 rows affected (0.00 sec)
#兩次更新之間執行刪除 mysql> delete from t1 where c1 = 2; --阻塞,直到會話1提交
Query OK, 1 row affected (18.94 sec)
|
與RC隔離級別不同的是,在RR中,由於保證可重複讀,會話2的delete語句會被會話1阻塞,直到會話1提交。
在RR中,會話1語句update t2 set c2 = 3 where c1 in (select c1 from t1)會先在t1的記錄上S鎖(5.1的RC中不會上這個鎖,但5.0的RC會),接著在t2的滿足條件的記錄上X鎖。由於會話1沒提交,會話2的delete語句需要等待會話1的S鎖釋放,於是阻塞。
因此,在RR中,以上測試會話1、會話2的依次執行,與Binlog的順序一致,從而保證DB/DR一致。
幻象讀
除了保證可重複讀,MySQL的RR還一定程度上避免了幻象讀(幻象讀是由於插入導致的新記錄)。(為什麼說一定程度呢?參考第3節可重複讀和序列化的區別。)
會話1 |
會話2 |
use test; #初始化資料 create table t1(c1 int primary key, c2 int) engine=innodb; create table t2(c1 int primary key, c2 int) engine=innodb;
insert into t1 values(1,1), (10,10); insert into t2 values(1,1), (5,5), (10,10);
#設定隔離級別 set tx_isolation='repeatable-read'; Query OK, 0 rows affected (0.00 sec)
#連續更新兩次 mysql> Begin; Query OK, 0 rows affected (0.03 sec)
mysql> update t2 set c2 = 20 where c1 in (select c1 from t1); Query OK, 2 rows affected (0.00 sec) Rows matched: 2 Changed: 2 Warnings: 0
mysql> delete from where c1 in (select c1 from t1); Query OK, 2 rows affected (0.00 sec) Rows matched: 2 Changed: 2 Warnings: 0
mysql> select * from t2; +------+------+ | c1 | c2 | +------+------+ | 5 | 5 | +------+------+ 2 rows in set (0.00 sec)
mysql> commit; |
#設定隔離級別 set tx_isolation=' repeatable-read'; Query OK, 0 rows affected (0.00 sec)
#兩次更新之間執行插入 mysql> insert into t1 values(5,5); --阻塞,直到會話1提交
Query OK, 1 row affected (18.94 sec)
|
由上述例子知,會話2的插入操作被阻塞了,原因是RR隔離級別中,除了記錄鎖外,還會上間隙鎖(gap鎖)。例如,對於表t1,update t2 set c2 = 20 where c1 in (select c1 from t1)以上的鎖包括:
(-∞, 1), 1, (1, 10), 10, (10, +∞)
由於對t1做全表掃描,因此,所有記錄和間隙都要上鎖,其中(x,y)表示間隙鎖,數字表示記錄鎖,全部都是S鎖。會話2的insert操作插入5,位於間隙(1,10),需要獲得這個間隙的X鎖,因此兩操作互斥,會話2阻塞。
SQL標準的RR並不要求避免幻象讀,而InnoDB通過gap鎖來避免幻象,從而實現SQL的可序列化,保證Binlog的一致性。
要想取消gap lock,可使用引數innodb_lock_unsafe_for_binlog=1,預設為0。
3. 可重複讀與序列化的區別
InnoDB的RR可以避免不可重複讀和幻象讀,那麼與序列化有什麼區別呢?
會話1 |
會話2 |
use test; #初始化資料 create table t3(c1 int primary key, c2 int) engine=innodb;
#設定隔離級別 set tx_isolation='repeatable-read'; Query OK, 0 rows affected (0.00 sec)
mysql> Begin; Query OK, 0 rows affected (0.03 sec)
mysql> select * from t3 where c1 = 1; Empty set (0.00 sec)
mysql> select * from t3 where c1 = 1; Empty set (0.00 sec)
mysql> update t3 set c2 =2 where c1 = 1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from t3 where c1 = 1; +----+------+ | c1 | c2 | +----+------+ | 1 | 2 | +----+------+ 1 row in set (0.00 sec)
mysql> commit; |
#設定隔離級別 set tx_isolation=' repeatable-read'; Query OK, 0 rows affected (0.00 sec)
mysql> insert into t3 values(1,1); Query OK, 1 row affected (0.05 sec)
|
由上述會話1中,連續兩次讀不到資料,但更新卻成功,並且更新後的相同讀操作就能讀到資料了,這算不算幻讀呢?
其實,RR隔離級別的防止幻象主要是針對寫操作的,即只保證寫操作的可序列化,因為只有寫操作影響Binlog;而讀操作是通過MVCC來保證一致性讀(無幻象)。
然而,可序列化隔離級別要求讀寫可序列化。使用可序列化重做以上測試。
會話1 |
會話2 |
use test; #初始化資料 create table t3(c1 int primary key, c2 int) engine=innodb;
#設定隔離級別 set tx_isolation='SERIALIZABLE'; Query OK, 0 rows affected (0.00 sec)
mysql> Begin; Query OK, 0 rows affected (0.03 sec)
mysql> select * from t3 where c1 = 1; Empty set (0.00 sec)
mysql> select * from t3 where c1 = 1; Empty set (0.00 sec)
mysql> update t3 set c2 =2 where c1 = 1; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0
mysql> select * from t3 where c1 = 1; Empty set (0.00 sec)
mysql> commit; |
#設定隔離級別 set tx_isolation='SERIALIZABLE'; Query OK, 0 rows affected (0.00 sec)
mysql> insert into t3 values(1,1); #阻塞,直到會話1提交
Query OK, 1 row affected (48.90 sec) |
設定為序列化後,會話2的插入操作被阻塞。由於在序列化下,查詢操作不在使用MVCC來保證一致讀,而是使用S鎖來阻塞其他寫操作。因此做到讀寫可序列化,然而換來就是併發效能的大大降低。
4. 小結
MySQL使用可重複讀來作為預設隔離級別的主要原因是語句級的Binlog。RR能提供SQL語句的寫可序列化,保證了絕大部分情況(不安全語句除外)的DB/DR一致。
另外,通過這個測試發現MySQL 5.0與5.1在RC下表現是不一樣的,可能存在相容性問題。
轉自:http://www.cnblogs.com/vinchen/archive/2012/11/19/2777919.html