暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

为PostgreSQL实现Serializability级别的隔离度

数据极客 2015-08-03
546

前一篇公众号文章里提到了关于数据库事务处理隔离级别的概念,有一个截图列举了目前主流数据库所提供的ACID隔离级别,其中令人惊讶的是Oracle居然没有提供最高隔离级别Serializability,只做到了快照隔离SI(Snapshot Isolation),今天本文来看一下PostgreSQL如何实现Serializability的。


在PostgreSQL 9.1之前,只提供快照隔离(Snapshot Isolation)做为最高的隔离级别,采用没有读锁的MVCC实现。快照隔离中,事务中的所有读操作都能够看到一致的视图,仿佛事务在第一次读行为之前产生了数据库快照;此外,快照隔离禁止并发事务修改同样的数据,这可以通过给行加写锁来实现。在并发更新下,快照隔离级别可能会导致写入错误,例如图中的例子,事务T1和T2先后执行,针对Alice和Bob的修改,属于不同的行,因此即便加了行锁业不会有冲突,但并发更新会导致数据出错。

快照隔离在很多数据库中都采用,甚至Oracle,因为顺序化的实现通常会引入严格的两阶段锁,严重影响并发性能。Postgresql在9.2版本中实现了顺序化快照隔离SSI,但并没有引入大部分顺序一致性保证的数据库所基于的两阶段锁,因此能够在确保数据安全的基础之上提供好的并发性能。其基本思想是仍然基于快照隔离提供事务,但对可能导致数据错误的场景进行检测,必要时让部分事务失败退出。在检测时,Postgresql依赖于三种读写依赖时序:

  1. 写-读(wr)依赖,事务T1针对某对象写入某版本,事务T2读取该版本。因此T1在语义上应当在T2前执行。

  2. 写-写(ww)依赖,事务T1针对某对象写入某版本,事务T2修改该版本为一新版本。语义上T1应当在T2前执行。

  3. 读-写(rw)反依赖,事务T1正在针对某对象写入某版本,事务T2正在读取该对象的上一个版本,那么T1语义上应当在T2之后执行,因为T2并没有看到T1的更新。这种依赖被称为读写冲突。

SSI认为,如果连续2个读写冲突在一起,就定义为危险结构,既:T1--->T2--->T3,并且从T1到T2,以及T2到T3都是读写冲突。任何错误的并发事务,都一定包含至少一个危险结构。这时SSI会让一个事务失败退出。


那么Postgresql是如何检测危险结构的呢?主要通过MVCC和一种SIREAD锁来实现的。在Postgresql中,存在三种不同的锁机制:轻量级锁,这是标准的读写锁;重量级锁,用于长时间锁定,尤其是事务执行中,支持死锁检测,在Postgresql中主要用于schema变化,删除表,建立索引等重量级操作;记录锁,用于避免针对记录的并发修改。由于SIREAD锁并不阻塞写入操作,因此在9.2.2版本中新增加了一种锁的实现。通常情况下,仅利用MVCC就可以检测出rw冲突:如果写入发生在读之前,直接通过MVCC数据就可以推断是否存在rw冲突;当一个事务读取记录时,首先进行可见性检测,推断该记录版本对于该事务快照是否可见,如果由于事务未提交而导致不可见,那么就是一个rw冲突。当读取在写入操作之前执行时,就需要借助SIREAD锁了。SIREAD锁是一个内存结构,因此只能细化到页级而不是记录级,此外,SIREAD锁不会阻塞任何操作,因此实际上它只是一系列标记而不是"锁"。


压测结果表明,SSI比之前的快照隔离,在吞吐量上仅仅有微微下降,比基于二阶段锁的传统顺序一致性实现吞吐量高一倍。


这就是顺序一致性的简要分析,读者一定会质疑,为什么要关注这些细节,它对于构建大数据应用并没有什么直接显著的用途。实际上,Postgresql是我们能够看到的高效实现Serializability的为数不多的数据库之一,在本公众号前边的文章中,提到了蟑螂数据库的SSI基于"A Critique of Snapshot Isolation"来实现,逐渐深入了解这些数据库底层细节,对于在某些场景下自造轮子,无疑总是大有裨益的。

文章转载自数据极客,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论