MySQL的默认隔离级别为什么是RR,而不是RC

771 阅读10分钟

开心一刻

今天和朋友们去K歌,看着这群年轻人一个个唱的贼嗨,不禁感慨道:年轻真好啊!
想到自己年轻的时候,那也是拿着麦克风不放的人
现在的我没那激情了,只喜欢坐在角落里,默默的听着他们唱,就连旁边的妹子都劝我说:大哥别摸了,唱首歌吧

开心一刻

基础回顾

我们一起来回顾下八股文

面试官:关系型数据库的隔离级别有哪些

你:读未提交(Read Uncommited 简称 RU)、读已提交(Read Commited 简称 RC)、可重复度(Repeatable Read 简称 RR)、串行化(Serializable)

面试官:主流关系型数据库的默认隔离级别是什么

你:MySQL 的默认隔离级别是可重复度,其他的如 Oracle、SQL Server、PostgreSQL、DB2 默认隔离级别是读已提交

面试官:MySQL 的默认隔离级别为什么是 RR,而不是 RC

你:呃...,这个...,昂昂昂昂昂,这个没去研究过

面试官:那你回去等通知吧

MySQL 5.5 才用 InnoDB 代替 MyISAM 作为 MySQL 的默认存储引擎,而事务才有隔离级别一说,MyISAM 本就不支持事务,所以谈 MySQL 的隔离级别都是基于 MySQL 5.5 及其之后的版本

binlog 格式

在回答问题

MySQL 的默认隔离级别为什么 是 RR,而不是 RC

之前,我们需要先了解下 MySQL 的 binlog

binlog 全称 binary log,即 二进制日志,有时候也称 归档日志,记录了对 MySQL 数据库执行了更改的所有操作,包括表结构变更(CREATE、ALTER、DROP TABLE…)、表数据修改(INSERT、UPDATE、DELETE...),但不包括 SELECT 和 SHOW 这类操作,因为这类操作对数据本身并没有修改;若更改操作并未导致数据库变化,那么该操作也会写入 binlog,例如

create table tbl_t1(name varchar(32));
insert into tbl_t1 values('zhangsan');
update tbl_t1 set name = 'lisi' where name = '123';
show master status\G;
show binlog events in 'mysql-bin.000002'\G;

此时的:update tbl_t1 set name = 'lisi' where name = '123'; 并未引起数据库的变化,但还是被记录到了 binlog 中

binlog 的格式有三种:STATEMENTROWMIXED,一开始只有 STATEMENT,后面慢慢衍生出了 ROW、MIXED;MySQL 5.1.5 之前 binlog 的格式只有 STATEMENT,5.1.5 开始支持 ROW,从 5.1.8 版本开始支持 MIXED;MySQL 5.7.7 之前,binlog 的默认格式都是 STATEMENT,在 5.7.7 及更高版本中,binlog_format 的默认值才是 ROW;三种格式的 binlog 各长什么样,它们有什么区别,各有什么优劣,我们往下看

STATEMENT

从 MySQL 第一个版本,到 8.0.x,STATEMENT 一直坚挺在 binlog 的格式中,只是从 5.7.7 开始,它退居幕后,头把交椅给了 ROW

binglog 与我们开发中的代码日志是不一样的,它包含两类文件

  1. 索引文件

    文件名.index,记录了哪些日志文件正在被使用,内容如下

    索引文件
  2. 日志文件

    文件名.00000*

    日志文件

    记录了对 MySQL 数据库执行了更改的所有操作

因为 binlog 的日志文件是二进制文件,不能用文本编辑器直接打开,需要用特定的工具来打开,MySQL 提供了 mysqlbinlog 来帮助我们查看日志文件内容,其可选参数很多,具体可用

 mysqlbinlog.exe --help 

查看,我们可以使用如下命令

mysqlbinlog.exe ../data/mysql-bin.000004 

查看日志文件内容

mysqlbinlog查看日志文件

可以看到,对数据库表的操作

insert tbl_t1 values ('aaa'),('bbb');
update tbl_t1 set name = 'a1' where name = 'aaa';
delete from tbl_t1 where name = 'bbb';

都是以明文形式的 SQL 记录在日志文件中

ROW

MySQL 5.7.7 及之后版本,binlog 的默认格式是 ROW,我们基于 5.7.30 版本,来看下 ROW 格式 binlog 内容是怎样的;先产生数据库更改操作

create table tbl_row(
    name varchar(32),
    age int
);
insert into tbl_row values('qq',23),('ww',24);
update tbl_row set age = 18 where name = 'aa';
update tbl_row set age = 18 where name = 'qq';
delete from tbl_row where name = 'aa';
delete from tbl_row where name = 'ww';

master 当前正在写入的 binlog 文件:mysql-bin.000002,position 从 28853929,我们看下日志文件中是怎么记录的,执行

mysqlbinlog.exe --start-position=2885 --stop-position=3929 ../data/mysql-bin.000002
ROW格式日志文件

可以看到,表结构变更操作以明文形式的 SQL 记录在日志文件中(与 STATEMENT 一样),但表数据变更的操作却是以一坨一坨的密文形式记录在日志文件中,不便于我们阅读,庆幸的是,mysqlbinlog 提供参数 -v 或 -vv 来解密查看,执行

mysqlbinlog.exe --base64-output=decode-rows -v --start-position=2885 --stop-position=3929 ../data/mysql-bin.000002
ROW格式日志文件_解密

INSERT 没什么好注意的,每一列都插入对应的值

insert into tbl_row values('qq',23),('ww',24);

对应

### INSERT INTO `my_project`.`tbl_row`
### SET
###   @1='qq'
###   @2=23
### INSERT INTO `my_project`.`tbl_row`
### SET
###   @1='ww'
###   @2=24

UPDATE 就有需要注意的了,虽然我们修改列只有一列,条件列也只有一列,但是日志中记录的却是:修改列是全部列,条件列也是全部列,并且列值是具体的值,而没有 NOW()、UUID() 这样的函数

update tbl_row set age = 18 where name = 'qq';

对应

### UPDATE `my_project`.`tbl_row`
### WHERE
###   @1='qq'
###   @2=23
### SET
###   @1='qq'
###   @2=18

表没有明确的指定主键,满足更新条件的记录也只有一条,大家可以去试试这种情况

明确指定主键且满足更新条件的记录有多条的情况

看看 binlog 日志是怎么记录的

DELETE 与 UPDATE 一样,虽说条件列只有一个,但日志中记录的确实全部列

delete from tbl_row where name = 'ww';

对应

### DELETE FROM `my_project`.`tbl_row`
### WHERE
###   @1='ww'
###   @2=24

相较 STATEMENT,ROW 显得更复杂,内容多很多

MIXED

字面意思:混合,那它混合谁? 还能混合谁?只能混合 STATEMENT 和 ROW

大多数情况下,是以 STATEMENT 格式记录 binlog 日志(因为 MySQL 默认隔离级别是 RR,而又很少有人去修改默认隔离级别),当隔离级别为 RC 模式的时候,则修改为 ROW 模式记录;有些特殊场景,也是以 ROW 格式来记录的,就不区分 RR 和 RC 了(摘自:关于binary log那些事——认真码了好长一篇

使用ROW格式的特殊场景

当然还有一个 NOW(),说白了就是,只有具体的值才最可靠,其他依赖于上下文、环境的函数、系统变量都不可靠,因为它们会因上下文、环境而变化

这个就不去展示具体的日志内容了,有兴趣的小伙伴自行去跑结果

优缺点对比

三种格式都已介绍完毕,相信大家对它们各自的特点、优缺点已经有一定的了解了,我给大家总结下

binlog日志格式优缺点对比

MIXED 的愿景(结合 STATEMENT 和 ROW 两者的优点,产生一个完美的格式)是好的,但事与愿违,它还是会有一些问题;相比于准确性而言,性能优先级会低一些(随着技术的发展,硬件性能已不再是不可接受的瓶颈),所以推荐使用 ROW 格式

默认隔离级别

从上面 binlog 格式的内容来看,似乎与默认隔离级别 RR 没有半毛钱关系,我只能说你们先莫急,慢慢往下看

  1. RC 隔离级别,binlog 格式是 STATEMENT 时,各版 MySQL 执行表数据修改操作

    表引擎肯定得是 InnoDB,我们分别看下 MySQl5.0.96MySQL5.1.30MySQL5.5.8MySQL5.7.30 执行表数据更改操作的情况

    RC_STATEMENT_各版MySQL执行情况

    MySQl5.0.96 可以正常执行

    MySQL5.1.30 执行报错,提示

    ERROR 1598 (HY000): Binary logging not possible. Message: Transaction level 'READ-COMMITTED' in InnoDB is not safe for binlog mode 'STATEMENT'
    

    MySQL5.5.8、MySQL5.7.30 执行报错,都提示

    ERROR 1665 (HY000): Cannot execute statement: impossible to write to binary log since BINLOG_FORMAT = STATEMENT and at least one table uses a storage engine limited to row-based logging. InnoDB is limited to row-logging when transaction isolation level is READ COMMITTED or READ UNCOMMITTED.
    

    也就是说,MySQL5.1.30及之后,RC 隔离级别的 InnoDB 对 binlog_format 是有限制的,不能是 STATEMENT,否则表数据无法进行修改

  2. 不同 session 的操作记录在 binlog 中的记录顺序

    我们用两个 session 来执行更新操作,看下不同 session 的操作记录在 binlog 中的记录顺序有什么决定

    不同session在binlog中记录顺序

    可以看到

    update tbl_rr_test set age = 20 where id = 1;
    

    先执行,后 commit,而

    update tbl_rr_test set age = 21 where id = 2;
    

    后执行,先 commit,日志中记录的是

    先commit的记录在前面,后commit的记录在后面,与执行时间点无关

    就单个 session 来说,好理解,执行顺序就是记录顺序;多个 session 之间的话,先 commit 的先记录

    主库对数据库的更改是按执行时间的先后顺序进行的,而 binlog 却是按 commit 的先后顺序记录的,理论上来说就会出现 MySQL Bug23051 中的示例问题

  3. 默认隔离级别(RR)与 binlog 关系

    MySQL Bug23051 里面有说到,MySQL 5.1 的早期版本,隔离级别是 RC、binlog 格式是 STATEMENT 时,InnoDB 的主从复制是有 bug 的(5.1.21 中修复),而 5.0.x 是没问题的,我们在 5.0.96 上跑下 Bug23051 中的例子

    主从复制bug

    可以看到,5.0.96 下的 InnoDB,在 RC 级别,binlog_format=STATEMENT 时

    UPDATE t1 SET a=11 where b=2;
    

    的事务未提交,则

    UPDATE t1 SET b=2 where b=1;
    

    的事务会被阻塞,那么从库复制的时候,数据是没问题的

    所以,综合前面的来看,从 MySQL5.0 开始,InnoDB 在 RC 级别,binlog_format=STATEMENT 时主从复制是没有 bug 的(5.0没问题,5.1.21之前的5.1.x有问题,但官方不提供下载了,5.1.21及之后的版本不支持 RC 隔离级别下设置 binlog 为 STATEMENT)

    那么 binlog 与 默认级别 RR 的关系就清楚了,就是 【原创】互联网项目中mysql应该选什么事务隔离级别 中的一段话

    那Mysql在5.0这个版本以前,binlog只支持STATEMENT这种格式!而这种格式在读已提交(Read Commited)这个隔离级别下主从复制是有bug的,因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别!

    也就是说,在 MySQL5.0之前,将 RR 作为默认隔离级别,是为了规避大部分主从复制的bug(具体什么bug,可详看 Bug23051 中的案例,或者【原创】互联网项目中mysql应该选什么事务隔离级别中的案例),然后一直被沿用了下来而已;为什么不是规避全部的主从复制 bug,因为在 RR 隔离级别、binlog_format=STATEMENT 下,使用系统函数(NOW()UUID() 等)时,还是会导致主从数据不一致

总结

  1. binlog 格式

    目前主流的 MySQL 版本中,binlog 格式有 3 种:STATEMENT、ROW、MIXED,从数据准确性考虑,推荐使用 ROW 格式

  2. binlog 默认格式

    MySQL 5.1.5 之前只支持 STATEMENT 格式的 binlog,5.1.5 开始支持 binlog_format=ROW,MySQL 5.7.7 之前,binlog 的默认格式都是 STATEMENT,在 5.7.7 及更高版本中,binlog_format 的默认值才是 ROW

  3. binlog 用途

    主要包括:主从复制、数据恢复、审计

  4. 主从复制 bug(InnoDB 引擎)

    MySQL 5.1.30及之后,InnoDB 下,开启 RC 隔离级别的话是不能启用 binlog_format=STATEMENT的

    RC、RR 隔离级别,binlog_format=MIXED,主从复制仍会有数据不一致的问题(受系统函数影响)

    RR 隔离级别,binlog_format=STATEMENT,主从复制仍会有数据不一致的问题(受系统函数影响)

    binlog_format=ROW,不管是 RC 隔离级别,还是 RR 隔离级别,主从复制不会有数据不一致的问题

  5. MySQL 的默认隔离级别为什么是 RR,而不是 RC

    为了规避 MySQL5.0 以前版本的主从复制问题,然后一直被沿用了下来而已