事务隔离级别回顾

隔离级别的发展

ANSI标准下的隔离级别

一般我们课本都是按照1992年的ANSI标准来定义数据库事务的几种隔离级别[2]:

  • 读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到;
  • 读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到;
  • 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;
  • 串行化(serializable );会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;

每种隔离级别存在的问题则如下:
image.png

RR隔离级别配合谓词锁来避免幻读

上面的隔离级别以及对应的问题并不是完全一层不变的,只是根据早期ANSI定义来说的一般情况。像在RR隔离级别完全可以配合谓词锁来避免幻读问题。

谓词锁:谓词锁是一种基于谓词的锁机制。谓词是指一个表达式或函数,它返回一个布尔值,用于描述一个或多个数据项的状态。在谓词锁机制中,锁的名称不是数据项,而是一个谓词,锁保护的是满足谓词条件的一组数据项。例如mysql中谓词锁就是gap锁

快照隔离性SI

在1995年,微软的Jim Gray研究员们在《A Critique of ANSI SQL Isolation Levels》论文中批判了ANSI标准,新增了Lost Update(更新丢失)、Read/Write Skew(读写偏序)的异像场景,同时正式提出基于MVCC的快照隔离级别SI。

过去ANSI标准主要是对更新丢失、读写偏序这类场景没有涉及。随着SI的出现,大家对这类新增问题有了新的认识。同时SI也为数据库实现更高性能的隔离级别提供了新思路。

下面是新增几种异常的case:

  • 更新丢失(Lost Update): 本质是并发更新事务LWW(last write win)的覆盖问题。账户本来有50块,事务1和事务2同时向同一个账户x分别充20和10块(各自看到的余额都是50块),事务1后提交,将70块写入数据库,事务2提交结果60块被覆盖。正确的情况下,事务1和2提交成功,账户里应该有80块。
  • 读偏序(read skew): 本质是事务并发执行时,一个事务读取了另外个事务提交前的旧数据导致数据不一致。x和y账户分别有50块钱,加起来共100块。事务1读x(50块)后,事务2将x账户的40块转到y账户,事务2提交后,事务1读y(90块)。在事务1看来,x+y=140,出现了不一致。这里是因为事务1读到了事务2执行前的旧数据,确保事务2执行完毕后执行事务1即可解决问题。
  • 写偏序(write skew): 本质是事务并发执行时,一个事务写的时候受到另外个事务提交前的旧数据影响,从而导致数据不一致。** **x和y账户分别有50块钱,加起来共100块。假设存在某种约束,x和y账户的钱加起来不得少于60块。事务1和事务2在自认为不破坏约束的情况下(分别读了x账户和y账户的余额,从自己角度来看加起来是100元),事务1和事务2再分别从y账户和x账户取走40。但事实上,这两个事务完成后,x+y=20,约束条件被破坏。

对应的解决办法,本质是确保有资源争用的事务串行执行:

  • MVCC
  • TSO与顺序一致性
  • 物理串行:利用单核,完全串行化
  • 存储过程:相当于将多个事务打包成一个整体发给数据库执行。在存储过程级别并行。不过总的来说存储过程如果放开给用户还是弊大于利,用户滥用会导致产生大量不可优化的、非常重的存储过程

序列化快照隔离性(SSI)

在1999年,在《Generalized Isolation Level Definitions》论文中提出了有向串行图DSG(Direct Serialization Graph)的隔离定义,可以在SI隔离级别的基础上解决Read/Write Skew(读写偏序)的异像问题,称之为SSI(Serializable Snapshot Isolation),主要的代表数据库为PostgreSQL/CockroachDB。

MySQL的事务隔离

mysql innodb默认的事务隔离级别是RR,但是和SQL92中的RR不太一样,他通过MVCC的方式解决了幻读。

RR隔离级别的说明

官方文档中[4]对MySQL RR的实现其实已经做了说明,针对不同的涉及读的SQL,表现行为会有以下两种

  • 只用事务中第一个读的快照(国内很多文章也把这个叫做快照读):如果事务中仅仅包含简单的select,则只会用第一次读的快照。
  • 每个操作都产生一个最新快照(锁定读,locking read,国内也有叫当前读的):select for update、select for share、update、delete都是如此。锁定读会加锁,具体如何加锁分以下两种情况:
    • 用行锁:走唯一索引的SQL,只进行行锁
    • 间隙锁:涉及范围条件的采用gap lock和next-key lock。注意间隙锁解决了RR级别上SQL92提到的换读问题,因为上锁别人也就没法改这个数据导致出现幻读的问题。

TIPS: RC和RR都是基于MVCC的,RC每次读都创建新快照。RU和SERIAL不依赖MVCC,但是一个有脏读问题,一个并发不行,基本不太用。

RR隔离级别下快照读总体上是可以避免幻读问题的,不过不是完全避免。一些case可以参考[5]

MVCC的实现

MVCC实现的关键主要在于将事务和其快照数据作为整体互相隔离开。MySQL(行级)和Oracle(块级)都是基于undo log实现的,PostgreSQL(行级)则直接在数据表中保存版本数据。这块了解细节可以参考[2][6]

参考资料