mysql-選擇使用Repeatable read的原因
問題背景
在mysql調優的過程中發現,mysql的默認隔離級別是可重復讀(repeatable read),其他幾類關系型數據庫pg,以及sybase,oracle,sqlserver的默認的隔離級別都是讀已提交(read committed)。
我們都知道隔離級別一共有四種,讀未提交,讀已提交,可重復讀,序列化。隔離級別越高,並發性能也就越低。
疑問
1、那么mysql為什么要選擇使用可重復讀來作為默認的隔離級別呢?
2、可重復讀,會帶來哪些問題?
3、我們在開發過程中是否要修改默認值,將其改為我們常見的讀已提交呢?
- 可重復讀(Repeatable Read),簡稱為RR;
- 讀已提交(Read Commited),簡稱為RC;
四種隔離級別
首先我們了解下四種隔離級別的區別。
-
READ UNCOMMITTED
:未提交讀
- 讀取未提交內容,所有事務可看到其他未提交事務的結果,很少實際使用
- 讀取未提交的數據稱為臟讀(Dirty Read)
-
READ COMMITTED
:提交讀
- 多數數據庫的默認隔離級別(MySQL默認不是,默認為REPEATABLE-READ)
- 滿足隔離的簡單定義:一個事務只能看到已提交事務所做的改變
- 這種隔離級別,支持所謂的不可重讀(Non-repeatable Read),同一事務的其他實例在該實例過程中可能有新commit,所以同一個select可能返回不同結果(同一個事務如何做到其他實例?)
-
REPEATABLE READ
:重復讀
- 可重復讀(MySQL默認事務隔離),但可能出現幻讀(Phantom Read)
幻讀(Phantom Read)
:當用戶讀取某范圍數據行時,另一事務在此范圍內插入新行,當用戶再次讀取此范圍數據行時,讀取到新的幻影行- InnoDB通過多版本並發控制MVCC機制解決該問題
- PS:新版MySQL采用
Next-Key鎖
來解決幻讀問題
-
SERIALIZABLE
:串行化
- 最高隔離級別,強制事務排序(串行化),不會互相沖突
- 每個讀數據航增加共享鎖
- 此級別,可能導致大量超時現象和鎖競爭
按事務隔離級別來說,級別越低數據一致性保障效果越差,而並發能力則越強。
為什么選擇REPEATABLE READ?
mysql為什么選擇使用可重復讀來作為默認的隔離級別呢?
查了下文檔,發現是有歷史原因的,這和mysql的復制有關系,mysql的復制基於binlog,在配置文件中我們可以發現有一個參數binlog_format,binlog有三種格式
# binary logging format - mixed recommended
binlog_format=row
statement:記錄的是修改SQL語句
row:記錄的是每行實際數據的變更
mixed:statement和row模式的混合
在mysql5.0以前binlog只支持statement這種格式,這種格式在讀已提交(read commited)這個隔離級別下主從復制是有bug的。
產生bug的原因如下圖:在主庫上面執行先刪除后插入,但是在從庫如果binlog為statement格式,記錄的順序就是先插入后刪除,從庫執行的順序和主庫不一致,最后主庫有數據,從庫的數據被刪掉了。
因此mysql將可重復讀(repeatable read)作為默認的隔離級別!
當然在可重復讀上面也有解決方案
一是使用間隙鎖,當session 1執行delete語句的時候,鎖住間隙,session 2就會被阻塞
二是將binlog_format設置為row格式,基於行的復制,就不會出現sql執行順序不一樣的問題。但是這個格式是mysql5.1以后才有的。由於歷史的原因,mysql將默認的隔離級別設置為可重復讀,並一直延續了下來,保證主從復制不出問題。
可重復讀,會帶來哪些問題?
1、隔離級別越高,並發能力越低。
2、在可重復讀級別下,如果使用間隙鎖的方式,那么導致死鎖的幾率比讀已提交大的多。
select *from test where n_id < 5 for update
在可重復讀級別下,可以鎖住間隙,防止其他事務插入數據。
在讀已提交級別下,不會影響插入,其他事務任然可以插入數據。
3、在可重復讀級別下,條件列未命中索引會鎖表
!而在讀已提交隔離級別下只鎖行
INSERT INTO order_record SELECT
*
FROM
order_today
WHERE
pay_success_time < '2020-03-08 00:00:00';
在可重復讀的隔離級別下,執行上面的sql,會對order_record加表鎖,order_today逐步鎖(掃描一個鎖一個)
當pay_success_time沒有索引,或者因為其它原因導致沒有走鎖定的時候order_today就會被鎖住。
測試:
mysql> show profiles;
Empty set (0.00 sec)
--默認隔離級別 REPEATABLE-READ
mysql> select @@global.tx_isolation,@@session.tx_isolation;
+-----------------------+------------------------+
| @@global.tx_isolation | @@session.tx_isolation |
+-----------------------+------------------------+
| REPEATABLE-READ | REPEATABLE-READ |
+-----------------------+------------------------+
1 row in set (0.00 sec)
--測試1 使用默認的binlog_format
mysql> select @@binlog_format;
+-----------------+
| @@binlog_format |
+-----------------+
| STATEMENT |
+-----------------+
1 row in set (0.00 sec)
--session 1
mysql> insert into test2 select * from test limit 500000;
Query OK, 500000 rows affected, 1 warning (31.10 sec)
Records: 500000 Duplicates: 0 Warnings: 0
--session 2 可以看到插入test的數據會一直等待session 1執行完成才能插入
mysql> insert into test values('4028e481513e66bc015156c3e359001a','1','1');
Query OK, 1 row affected (29.48 sec)
--測試2 將binlog_format設置為row
mysql> set global binlog_format='row';
Query OK, 0 rows affected (0.00 sec)
mysql> select @@binlog_format;
+-----------------+
| @@binlog_format |
+-----------------+
| ROW |
+-----------------+
1 row in set (0.00 sec)
-- session 1
mysql> insert into test2 select * from test limit 500000;
Query OK, 500000 rows affected (32.09 sec)
Records: 500000 Duplicates: 0 Warnings: 0
--session 2 可以看到session 1不會阻塞session 2 的插入
mysql> insert into test values('4028e481513e66bc015156c3e359001a','1','1');
Query OK, 1 row affected (0.00 sec)
--測試3 將binlog_format設置為mixed
mysql> set global binlog_format='mixed';
Query OK, 0 rows affected (0.00 sec)
mysql> select @@binlog_format;
+-----------------+
| @@binlog_format |
+-----------------+
| MIXED |
+-----------------+
1 row in set (0.00 sec)
--session 1
mysql> insert into test2 select * from test limit 500000;
Query OK, 500000 rows affected (32.16 sec)
Records: 500000 Duplicates: 0 Warnings: 0
--session 2 ,session 1會阻塞session2的插入
mysql> insert into test values('4028e481513e66bc015156c3e359001a','1','1');
Query OK, 1 row affected (30.71 sec)
mysql在默認隔離級別可重復讀(REPEATABLE-READ)時,binlog_format設置為statement和mixed都會阻塞,設置為row模式時不會阻塞。
insert into select 有兩種阻塞方式,一種是逐行鎖住,一種是全表鎖住。詳情
解決這個阻塞的方式有兩種:一是可以先把查詢出來的數據落地,然后在還原到另外一張表。或者將binlog_format改為row
最好的方式是使用讀已提交的模式,並且將binlog_format設置為row
另外。在mysql中設置隔離級別為讀已提交時,binlog_format如果設置為statement插入數據的時候會報錯:
--默認binlog_format=statement會報錯
mysql> create table test3(id int)engine=innodb;
Query OK, 0 rows affected (0.16 sec)
mysql> insert into test3 values(1);
ERROR 1598 (HY000): Binary logging not possible. Message: Transaction level 'READ-COMMITTED' in InnoDB is not safe for binlog mode 'STATEMENT'
--設置binlog_format=row無問題
mysql> set session binlog_format=row;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test3 values(1);
Query OK, 1 row affected (0.06 sec)
--設置binlog_format=mixed無問題
mysql> set session binlog_format=mixed;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test3 values(1);
Query OK, 1 row affected (0.07 sec)
報錯的原因是因為read committed可能會導致不可重復讀,也就是說可以讀取到后面進入並提交的數據,如果基於STATEMENT格式的話,會導致主從數據不一樣,因為STATEMENT是基於SQL語句的復制模式。
使用讀已提交的時候,binlog_format只能設置為row或者mixed。建議使用row
總結
1、mysql為什么選擇使用可重復讀來作為默認的隔離級別?
原因是在mysql5.0以前binlog只支持statement這種格式,這種格式在讀已提交(read commited)這個隔離級別下主從復制是有bug的,因此mysql將可重復讀(repeatable read)作為默認的隔離級別!
2、可重復讀會帶來那些問題?
1)、隔離級別越高,並發能力越低。
2)、在可重復讀級別下,如果使用間隙鎖的方式,那么導致死鎖的幾率比讀已提交大的多。
3)、在可重復讀級別下,條件列未命中索引會鎖表
!而在讀已提交隔離級別下只鎖行
3、是否可以將mysql的默認隔離級別改為讀已提交(read commited)
這個是可以的,在修改隔離級別為讀已提交的同時,將binlog_format修改為row,可以提高並發能力。