1. 程式人生 > >MySQL使用可重複讀作為預設隔離級別的原因(一)

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