RR与RC隔离级别的异同

李宁

前言

发现目前我司Mysql的默认事物隔离级别配置均为RC(READ-COMMITED)级别, 而Mysql默认的隔离级别为RR(REPEATABLE-READ)。 咨询DBA反馈得到主要是考虑到并发问题,RC支持的并发更高、性能更好,也是阿里云rds的默认配置。故特意整理了下两者的区别。

1. 读可见性

对于普通的select,RC级别允许不可重复读,而RR级别不会出现。RC和RR隔离级别均是利用consistent read view(一致性读、快照读)方式支持的。

下面简单介绍几个一致性读相关知识点:

行结构

InnoDB数据的组织方式为主键聚簇索引,每个索引记录都包含一个DELETED_BIT(标识记录是否被删除),此外主键索引还包含DATA_TRX_ID(产生当前记录项的事物id)、DATA_ROLL_PTR(当前记录项的undo log回滚指针)。
回滚指针用于构建undo log, 事物id用于和read view结合判断记录对于事物是否可见。

undo log

当对记录做了INSERT/DELETE/UPDATE操作时就会产生undo log:相当于一个链表,每个节点存储的是老版本数据。

undo记录中包含了记录更改前的镜像,如果更改数据的事务未提交,对于隔离级别大于等于READ COMMITED的事务而言,它不应该看到已修改的数据,而是应该给它返回老版本的数据

read view

某个时刻的事物系统trx_sys的快照

主要属性:

  • low_limit_idundo log中大于等于low_limit_id的记录对于read view都是不可见的

  • up_limit_id:小于up_limit_id的记录对于read view一定是可见的

  • low_limit_no:小于low_limit_noundo log对于read view是可以purge的

  • rw_trx_ids:当前活跃的读写事务数组。如果trx_id落在[up_limit_id, low_limit_id),需要在活跃读写事务数组查找trx_id是否存在,如果存在,记录对于当前read view是不可见的

RR和RC级别创建read view的时机不同,RR只在事物开始后第一次select时创建一次,而RC是在每次语句创建执行的时创建的。因此RC级别每次读到的都是当前可见的最新数据,而RR读到的都是事物开始时可见的数据。

创建/关闭read view需要持有trx_sys的mutex,会降低系统性能

2. 锁

gap lock

RR需要用gap lock用来解决幻读问题,而RC不存在,所以RC的并发性好于RR。

一个间隙锁例子:

表数据:

id user_id balancecash
1110
2210
3310
session 1session 2
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM user_account WHERE id > 3 FOR UPDATE;
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED ;
INSERT INTO user_account(id, user_id) VALUES(4,4);
// 无需等待执行成功
COMMIT;
COMMIT;

如果切换成RR级别, 则SESSION 1会锁住 id:(3,+∞)的区间,SESSION2需要等待SESSION 1 提交

RC级别不存在间隙锁,锁冲突更少,并发程度高,且发生死锁的概率也更低

半一致性读

在RC级别下,一个update语句,如果读到一行已加锁的记录,InnoDB会返回记录最近提交的版本,由MySQL上层判断此版本是否满足update的where条件。若满足(需要更新),则MySQL会重新发起一次当前读操作,此时会读取行的最新版本并加锁

半一致性读例子:

表数据:

id user_id balancecash
1110
2210
3310
session 1session 2
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
UPDATE user_account SET balance=balance+10
// executed, 3 ROWS affected
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
UPDATE user_account SET cash =balance WHERE balance > 10;
// executed, 0 ROWS affected 不用等待session1提交,直接执行
COMMIT;
COMMIT;

如果在RR级别下执行这个例子,则SESSION 2会等待SESSION 1提交

半一致性读减少了更新同一记录时的锁等待,但这种非串行话冲突解决策略,对于binlog来说是不安全的,两条语句,根据执行顺序与提交顺序的不同,通过binlog复制到备库后的结果也会不同

在某些强一致性业务场景下也是不合适的,需要酌情考虑使用。

early unlock

RC级别下执行update时Innodb会为扫描到的每条记录创建X锁(排它锁),对于不满足查询条件的记录,可提前释放行X锁,进一步减少并发冲突概率

early unlock例子:

表数据:

id user_id balancecash
11110
22110
33110
session 1session 2
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED ;
UPDATE user_account SET cash =balance WHERE balance < 10 and id =1
// executed, 0 ROWS affected
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT * FROM user_account FOR UPDATE
// 执行成功无需等待
COMMIT;COMMIT;

SESSION 1进行全表扫描,完成加锁。但是由于均不满足where条件,SESSION 1会释放所有行上的锁(由MySQL Server层判断并调用unlock_row方法释放行锁)。

此时,session 2再次执行SELECT * FROM user_account FOR UPDATE语句,直接成功。因为session 1已经将所有的行锁提前释放。

如果SESSION 1的隔离级别为RR则SESSION 2会等待

3. 主从复制

RC隔离级别不支持statement格式的biglog,仅支持row格式的。如果设置成mixed格式的binlog,mysql会自动切换成row格式。
如果开启了statement格式的binlog,插入数据将报错:

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

那么,为什么RC隔离级别不支持语句级binlog呢?关闭binlog,做以下测试

表数据
t1: id c1 c2    t2: id c1 c2  
    1  1  1         1  1  1
    2  2  2         2  2  2
session 1session 2
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
UPDATE t2 SET c2 = 3 WHERE c1 IN (SELECT c1 FROM t1);
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
DELETE FROM t1 WHERE c1 = 2;
// 执行成功无需等待
COMMIT;
UPDATE t2 SET c2 = 4 WHERE c1 IN (SELECT c1 FROM t1);
COMMIT;

由于RC级别下 session2可以无需等待执行成功,影响了session1的第二次update, 最终结果为

t2: id c1 c2  
    1  1  4 
    2  2  3 

因为binlog中语句的顺序以commit为序,如果statement格式的binlog允许,两会话的执行时序是先执行session2的sql,再执行session1的sql,最终结果和实际情况不符:

t2: id c1 c2  
    1  1  4 
    2  2  2 

改用RR级别后session2的sql会被阻塞,原因是session1会对t1加s锁(读锁),session2的操作需要对t1加x锁,会阻塞,所以不会影响row格式的binlog

总结

  • RC(READ-COMMITTED)

    • 优势
      • 高并发低开销:半一致性读
      • No gap lock; early unlock。死锁可能性更低
    • 劣势
      • 不支持statement binlog,binlog消耗更大
      • No gap lock;no early unlock。数据一致性更差
  • RR(REPEATABLE-READ)

    • 优势
      • 支持gap lock:statement binlog
      • 事务级快照读
    • 劣势
      • 并发冲突高,加锁冲突更为剧烈
      • 不支持半一致性读,不支持early unlock

需要结合不同的业务场景,基于数据一致性、性能要求、并发程度、可靠性要求选则合适的隔离级别

希望我的文章对你能有所帮助,谢谢

reference