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

posted @ 2021-06-10 19:06  月图灵  阅读(1021)  评论(0编辑  收藏  举报