mysql-選擇使用Repeatable read的原因


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隔離級別.jpg

因此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 有兩種阻塞方式,一種是逐行鎖住,一種是全表鎖住。詳情

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,可以提高並發能力。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM