为了更高一致性-盘点MySQL,PostgreSQL,Cockroach Serializable隔离级别的实现

Serializable隔离级别

事务的一致性和隔离性是事务的两个重要的特性,从隔离级别的角度看,两者是息息相关的,更高的隔离级别代表更严格的一致性。

在ANSI SQL标准中,事务有4个隔离级别,Read Uncommitted,Read Committed,Repeatable Read,Serializable,其中Serializable是最高的隔离级别。可以说,在传统数据库中,只有Serializable的事务才能完全满足ACID的特性,因为低隔离级别的事务之间还是有可感知的互相影响,比如幻读,严格来说,这既破坏原子性又破坏隔离性。

现实中,即使是面向OLTP的数据库,大部分也没有实现Serializable隔离级别,或没有采用Serializable作为默认隔离级别,见下表

Database Default Isolation Maximum Isolation
IBM DB2 10 for z/OS CS S
IBM Informix 11.50 Depends RR
MySQL 5.6 RR S
MemSQL 1b RC RC
MS SQL Server 2012 RC S
Oracle 11g RC SI
Postgres 9.2.2 RC S
SAP HANA RC SI
Cockroach S S
tidb SI SI
cloud spanner S S
Legend RC: read committed, RR: repeatable read, S: serializability,SI: snapshot isolation, CS: cursor stability, CR: consistent read

​ (摘自 When is "ACID" ACID? Rarely.,有增删)

在表中,比较重要的没有实现Serializable的数据库有Oracle 11g,身为商业数据库的霸主,没有提供Serializable隔离级别,不能不说是一种遗憾。

隔离级别是在一致性和并发性之间的一次权衡,对于互联网应用,Serializable往往是不必要的,相较之下,吞吐量更为重要,这是nosql,newsql,sharding中间件能够流行的一个重要原因。但对于金融领域的某些场景,Serializable的重要性就凸显出来了。

如何实现Serializable

悲观方案-加锁

最简单的方法,当然是加数据库锁,读加共享锁,写加排它锁。

但是在tp类型的数据库,这种方法显然不可行,这会导致数据库所有事务的读写和写写行为全部串行化。为了提高并发性,可以采用加表锁的方式,但是,这种方法也不能满足并发性的要求。对于数据库事务,行是修改的最小单位。因此采用行加锁的方式实现串行化,是并发性最高的方案。

乐观方案-SSI

在SIGMOD 2008,论文《Serializable Isolation for Snapshot Databases》提出了一个使用乐观机制实现Serializable的方法。
事务的有害依赖可以分为:

  1. rw-dependency,代表事务T1读取了元素X,事务T2随后修改了元素X;
  2. wr-dependency,代表事务T1修改了元素X,事务T2随后读了元素X;
  3. ww-dependency,代表事务T1修改了元素X,事务T2随后再次修改了元素X;

由于MVCC机制的特殊性,对于snapshot隔离级别,当事务T1,T2并行执行时,是互相读不到对方修改后的结果的,因此任何事务读取了对方修改的数据,一定是前一个版本(或者前几个版本),即使读操作是发生在写之后,这会形成一个rw-dependency,而不是wr-dependency。

显然,对于任何一个非串行化的调度,那么一定有多个事务组成的读写关系形成了环。

这篇论文证明了,对于snapshot隔离级别,任何一个成环的调度必然包含连续的两个rw-dependency,这是一个充分不必要条件。以write skew为例:

write-skew.png

事务T1和T2分为读取了对方修改前的数据,形成了两个rw-dependency,如下图:

RW conflict.png

也就是说,只要破坏这两个rw-dependency,既可以实现串行化。同时,这个方案会带来一定误判,并不是所有包含两个rw-dependency的事务都构成了环。

常见数据库Serializable隔离级别的实现

MySQL(innodb)如何实现Serializable

MySQL是通过gap lock的方式实现了Serializable,简单来说,在snapshot读的基础上,对读操作加锁。

  1. 如果查询包括索引,在索引上使用gap lock,锁下一个key;
  2. 如果查询不包含索引,使用表锁;

考虑一下,对于上图中的write skew会发生什么?

事务T1读X的时候加锁,事务T2读Y的时候加锁,事务T1修改Y的时候被阻塞,事务T2修改X的时候被阻塞,形成死锁。死锁检测导致其中一个事务回滚,环被打破,实现Serializable。

见下面这个例子,id是主键。

mysql serilizable-write skew.png

为什么这种加锁的方式可以实现SSI?

读写操作都加锁之后,无论两个事务之间如何发生冲突,都不可能形成环,至于为什么锁下一个key,这是一种谓词锁的实现方式,代表对满足条件但是尚未插入的数据加锁(幻读)。

PostgreSQL如何实现Serializable

PostgreSQL在版本9.2之后根据论文《Serializable Isolation for Snapshot Databases》提出的算法实现了Serializable隔离级别,可以说是这篇论文的开源实现。

回想一下《Serializable Isolation for Snapshot Databases》论文中的内容,为了实现Serializable,我们需要追踪rw-dependency,这不仅需要记录每行的修改事务,同时也要记录每行的读取事务。

PostgreSQL是行级MVCC机制的,这点和MySQL相同。行级MVCC机制表示该行的每个版本都记录了是由哪个事务的修改的,由此可见,我们缺少的就是该行的读取事务的信息。

PostgreSQL通过predicate lock维护事务的读取信息,以记录物理时间上读后写产生的rw-dependency。和MySQL的gap lock不同,predicate lock并不会阻塞任何其他事务,仅仅用于生成rw-dependency,并在生成rw-dependency时和事务提交时判断是否以事务为中心构成了两个连续的rw-dependency,以此判断事务是否需要回滚。

PostgreSQL通过对整个index page加锁的方式实现了类似MySQL next key lock的效果,这同样是为了解决读操作发生后有满足条件的数据插入。

见下面这个例子,同样,id是主键(不过,pg是heap表,不是IOT表)

pg serializable-write skew.jpg

虽然是同样的语句,一样是write skew的例子,但是pg的事务行为和MySQL不同,只有在提交时,后提交的事务(右边)显式的回滚了,这是乐观机制和悲观机制的不同。

Cockroach如何实现Serializable

Cockroach是跨数据中心部署的分布式数据库,全局数据结构的维护代价要远远高于单机数据库,因此无法像MySQL和PostgreSQL一样维护全局锁表,也就无法使用MySQL和PostgreSQL的方式实现Serializable,同时,Cockroach是纯乐观机制,实现上并没有锁,也不可能为了实现Serializable推翻自己的根本设计基础。

那Cockroach是如何实现的Serializable隔离级别?Cockroach lab有一篇文章,介绍了他们是如何实现的Serializable隔离级别,见Serializable, Lockless, Distributed: Isolation in CockroachDB

想想PostgreSQL实现Serializable隔离级别时需要解决的几个问题:

  1. 实现类似next key lock的机制(索引页面加锁),解决读后插入数据的问题;
  2. 读加锁,解决读后修改的问题;

Cockroach为了实现Serializable,提供了两个关键的机制:

  1. Cockroach允许snapshot隔离级别的事务在提交时使用一个更新的timestamp,这是符合snapshot隔离级别的语义的,但是不允许Serializable隔离级别的事务使用更新的timestamp,Serializable隔离级别的事务会用一个timestamp读取数据,同时也用这个timestamp提交数据,因此Serializable隔离级别的事务逻辑上等价在这个时间瞬时完成,虽然物理上并非如此。
  2. Cockroach内所有的key都是按照range组织的,而非hash,这为serializab的实现提供了极大的便利,类似PostgreSQL的predicate lock,Cockroach维护了一个单独的timestamp cache,记录了读写指定范围内的key的timestamp。在写操作发生时,会检查这个范围的最大read timestamp。如果read timestamp > 事务的timestamp,那么这两个事务之中就有一个需要回滚。

结合这两点,我们可以发现,在Cockroach中,Serializable隔离级别的实现有两个关键点:

  1. 通过固定timestamp,将所有Serializable隔离级别的事务按照Serializable隔离级别进行串行化排序;
  2. 通过timestamp cache,进行读写冲突的判断,同时,cache以range的形式维护,也避免了幻读的问题;

同样的,我们分析一下为什么Cockroach的这种方法可以实现Serializable?

Cockroach中,每个Serializable隔离级别的事务他们的读写行为都在另一个发生冲突的Serializable隔离级别的事务之前或之后,按照timestamp的顺序严格排序,没有任何交叉的可能,因此一定是Serializable的;

同样,举一个Cockroach处理write skew的例子。

cockraoch-write skew.png

总结

Mysql使用gap lock的实现和PostgreSQL使用SSI的实现更像是悲观机制和乐观机制之间的比较。Cockroach则使用了最严格的方式,但他的并发性也是这三个数据库中最差的。看下面这个例子

-- run in serialzable isolation level
start transaction; --Tx1               |             start transaction; --Tx2
select * from t1 where id=0;           |
                                       |             select * from t1 where id=0;
update t1 set id2=id2+1 where id=1;    |
commit;                                |             
                                       |             commit;

明显,这两个事务可等价于Tx2->Tx1的串行调度。但是在不同的数据库中,他们的行为就各不相同。

  1. 在MySQL中,事务Tx1需要等待事务Tx2提交后才能提交;
  2. 在PostgreSQL中,两个事务可以无阻塞的并发执行下去;
  3. 在Cockroach中,左边的事务会回滚

当然,这并不能说明PostgreSQL的实现方式就优于MySQL,在上面write skew的例子中,PostgreSQL会继续执行注定失败的update,如果其后有一些其他的语句,那么无用操作的开销更多,而MySQL在update发生时就会立刻回滚,避免后续空耗资源。很明显,这还是乐观机制和悲观机制的差别。当冲突较多时,悲观机制更优,当冲突较少时,乐观机制更优。

而cockroach,采用了串行化事务的所有操作的方式,有最差的并发性,但也避免了实现全局锁的巨大工程开销。

参考文档

  1. A Critique of ANSI SQL Isolation Levels
  2. 《Highly Available Transactions: Virtues and Limitations》
  3. When is "ACID" ACID? Rarely.
  4. 《Serializable Isolation for Snapshot Databases》
  5. PostgreSQL SSI README
  6. PostgreSQL-wiki Serializable
  7. 《Serializable Snapshot Isolation in PostgreSQL》
  8. MySQL Phantom Rows
  9. Cockroach design doc
  10. How CockroachDB Does Distributed, Atomic Transactions
  11. Serializable, Lockless, Distributed: Isolation in CockroachDB
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容