Postgresql可串行化快照隔离浅析
了解更多Greenplum相关内容,欢迎访问Greenplum中文社区网站
引言
PostgreSQL数据库采用快照隔离(Snapshot Isolation)技术实现并发控制,快照隔离是利用数据的多版本实现并发控制的一种方式。快照保存了某个特定时间点活跃事务的状态信息。在PostgreSQL数据库中,对于Read Committed隔离级别,事务中的每条SQL语句的执行都会获取一个快照,对于Repeatable Read隔离级别,事务只在第一条SQL语句的执行时获取一个快照,后续的SQL语句使用同一个快照。
快照隔离存在的异常现象
尽管快照隔离可以避免脏读、不可重复读、幻读这三种读异常现象,但并不是真正的可串行化隔离级别,文献[1]指出快照隔离级别中存在写偏斜(Write Skew)的异常现象。写偏斜发生在两个并发的事务读取相同的数据,然后都修改了数据的交集部分。
我们以一个示例讲述,这个示例来自于Dan和Kevin关于PostgreSQL数据库SSI实现的论文[2]。这个示例有两个并发的事务参与,每个事务完成的操作是:查找当前在值守的医生的数量,如果数量大于等于2,那么就选择一位医生离岗。在快照隔离级别下,假如两个事务执行之前,医生Alice和Bob都在岗,每个事务开始执行,首先取得自己的快照,查询满足条件的医生数量,就是Alice和Bob都在岗,然后更新两位医生为离岗状态。这就违背了可串行化隔离级别,因为业务逻辑是要保证至少有一位医生在岗。如果两个事务串行方式依次执行,就能保证这个业务逻辑。
图1:一个写偏斜的示例
再看一个有三个事务参与的异常现象。这个示例描述了一个收入报表的业务,在这个业务中,有两个表,收据表receipts记录每条收入的批号(batch,以日期表示)和其他相关信息,控制表记录当前批号。有三个事务参与这个业务,事务T1从控制表里获取当前批号,然后向收据表里增加一条记录;事务T2将控制表里的批号加一;事务T3是一个只读事务,先获取当前批号,然后查询上一个批号的所有收入,也就是获取昨天的所有收入。下图描述了三个事务一个执行顺序,在这个例子中,当T3统计昨天的所有收入,并不能查询到T1增加的新记录。
尽管快照和加锁方式实现并发控制相比有性能上的优势,但是快照隔离并不等同于可串行化隔离级别,会引入如上图两个例子所示的异常和数据不一致现象。所以为了既能享受快照的性能优势,又能让快照的实现满足可串行化隔离级别,业内研究者做了很多相关工作,提出了可串行化快照隔离(Serializable Snapshot Isolation,SSI),PostgreSQL社区也于2012年实现了SSI技术,使得PostgreSQL真正意义上支持可串行化隔离级别。
图2:多个事务参与的SI的异常现象
简单介绍SSI的理论基础。Adya在文献[3]提出了用多版本可序列化图(multiversion serialization graph,简称MVSG)来表示并发事务之间的关系。在MVSG图中,每个顶点代表一个事务,如果有两个事务T1和T2,T1必须在T2之前,那么就有一条从T1到T2的有向边。如下表所示,有三种有向边:
如果这些事务所构成的MVSG图中形成一个有向环,那么这些事务是无法对应一个可串行化调度,也是无法可串行化的。
有了MVSG图的定义,我们再回头看刚才的两个示例,第一个实际示例中两个事务互相读写反依赖于对方,所以形成了一个环路,这两个事务构成的调度不是可串行化的。第二个示例,T1和T2之间存在读写反依赖,而T2和T3之间存在写读依赖,三个事务的调度构成了一个环路,同样不是可串行化的。
图4:读写反依赖
Adya和Fekete的研究[4]发现一个定理:在序列化历史图里的每一个环路都存在着T1 -> T2 -> T3读写反依赖关系,其中,T3一定是这个环里的事务中最先提交的事务。
如果T1和T3为同一个事务就是在第一个例子的情况,T1和T2之间构成环。第二个示例的环中,同样存在T1 -> T2 -> T3读写反依赖的序列。
SSI技术的实现原理
Cahill等人根据这个定理使用快照隔离技术实现了可串行化快照隔离级别[4],这个实现的原理是在数据库运行的过程中,检查可能存在的异常现象,如果发现按需终止某些事务,以保证其余的事务以可串行化隔离级别运行。这个实现的思路,和通过SSN(Serial Safety Net)协议[5]类似。与之不同的是,SSN协议检查时查找的是一个环路,而SSI协议查找的是一个“危险结构”,也就是SSI PostgreSQL论文里所提到的“dangerous structure”,这个危险结构就是上面所说的两个相邻的读写反依赖关系。
但是这个结论反过来并不成立,也就是说,如果在MVSG图中发现两个相邻的读写反依赖,不一定构成一个环路。SSI实现的理论基础,就是在并发的事务中寻找这种危险的结构,然后采取一定的策略,回滚某个事务。所以SSI的实现是在发现这种危险结构后会主动回滚一些事务,而且也有可能出现错判的情况。SSI这种实现收益却是很明显的,首先不需要检测存在的环路,仅仅需要检测两个相邻的读写反依赖关系,代价比SSN方式小很多,另外,SSI实现仅需要追踪读写反依赖,并不需要追踪写写依赖或者写读依赖,也极大地减轻了实现层面上的复杂性和数据库系统运行时资源的开销。并且这种方式比两阶段加锁和乐观并发控制协议有更高的并发性能。两阶段加锁和乐观并发控制协议不允许出现读写反依赖,而SSI允许出现读写反依赖,只要不形成这种“危险结构”。
PostgreSQL的SSI实现基础就是基于这个定理,在并发的事务中查找可能存在的两个相邻的读写冲突(rw-conflicts),如果发现就根据一定策略选择一个事务回滚。并返回SQL ERROR 400。
PostgreSQL SSI使用示例
我们在PostgreSQL里用可重复读隔离级别和可串行化隔离级别分别运行上述的例子,可以发现在可重复读隔离级别,两个事务都能执行成功,也就出现了不一致现象。而在可串行隔离级别运行同样的例子,可以发现,第二个提交的事务返回一个SQL400的错误,这是因为SSI的实现检测到了两个事务存在“危险的结构”,于是根据“先提交者获胜”(first-commit-win)的原则,终止第二个提交的事务。
图5:在可重复读隔离级别的运行示例
图6:在可串行化隔离级别的运行示例
参考资料:
[1] H. Berenson, P. Bernstein, J. Gray, J. Melton,
E. O’Neil, , and P. O’Neil. A critique of ANSI SQL isolation levels. In Proceedings of ACM SIGMOD International Conference on Management of Data, pages 1–10. ACM Press, June 1995
[2] Dan R. K. Ports and Kevin Grittner. 2012. Serializable Snapshot Isolation in PostgreSQL. Proceedings of the VLDB Endowment vol. 5 (12) , August 2012
[3] A. Adya, B. Liskov, and P. O’Neil. Generalized isolation level definitions. In ICDE, pages 67–80, 2000.
[4] M.J.Cahill,U.Röhm,and A.D.Fekete. Serializable Isolation for Snapshot Databases. SIGMOD, 2008.
[5] T. Wang, R. Johnson, A. Fekete, and I. Pandis. Efficiently Making (Almost) Any Concurrency Control Mechanism Serializable. arXiv:1605.04292, 2016.

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
分布式集群如何实现高效的数据分布
一、前言 随着互联网的发展,用户产生的数据越来越多,企业面临着庞大数据的存储问题,目前市面上主流的分布式大数据文件系统,都是对数据切片打散,通过离散方法将数据散列在集群的所有节点上,本文将带你了解DHT(Distributed Hash Table):分布式哈希表是如何实现数据的分布式离散存储的。 DHT(Distributed Hash Table):分布式哈希表 二、技术背景 互联网发展早期,数据通常存储在单个服务器上,初期数据增长较为缓慢,可以通过提升单机的存储能力满足数据的增长需求;随着互联网普及程度的推进,用户数、用户产生和访问的数据呈指数增长;单机已无法存储用户需要的数据。为此,迫切需要多个服务器共同协作,存储量级更大的数据。 三、传统 Hash 传统 Hash 通过算法 hash()=X mod S 来对数据进行分散,在元数据比较分散的情况下,数据能够很好的散列在集群节点中。由于S代表了集群的节点数,当进行集群的扩容缩容时,S的变化会影响到历史数据的命中问题,因此为了提高数据命中率,会产生大量测数据迁移,性能较差。 四、一个简单的 DHT 分布式 Hash 通过构造一个长...
- 下一篇
我是这样给阿里面试官吹 ConcurrentHashMap的
因为上篇文章HashMap已经讲解的很详细了,因此此篇文章会简单介绍思路,再学习并发HashMap就简单很多了,上一篇文章中我们最终知道HashMap是线程不安全的,因此在老版本JDK中提供了HashTable来实现多线程级别的,改变之处重要有以下几点。 ❝ HashTable的 put, get, remove等方法是通过 synchronized来修饰保证其线程安全性的。 HashTable是 不允许key跟value为null的。 问题是 synchronized是个关键字级别的重量锁,在get数据的时候任何写入操作都不允许。相对来说性能不好。因此目前主要用的 ConcurrentHashMap来保证线程安全性。 ❞ ConcurrentHashMap主要分为JDK<=7跟JDK>=8的两个版本,ConcurrentHashMap的空间利用率更低一般只有10%~20%,接下来分别介绍。 JDK7 先宏观说下JDK7中的大致组成,ConcurrentHashMap由Segment数组结构和HashEntry数组组成。Segment是一种可重入锁,是一种数组和链表的结构,...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7安装Docker,走上虚拟化容器引擎之路
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- CentOS关闭SELinux安全模块
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Docker安装Oracle12C,快速搭建Oracle学习环境
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长