You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

165 lines
17 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 14 | 隔离性:实现悲观协议,除了锁还有别的办法吗?
你好我是王磊你也可以叫我Ivan。
我们今天的主题是悲观协议,我会结合[第13讲](https://time.geekbang.org/column/article/282401)的内容将并发控制技术和你说清楚。在第13讲我们以并发控制的三阶段作为工具区分了广义上的乐观协议和悲观协议。因为狭义乐观很少使用所以我们将重点放在了相对乐观上。
其实,相对乐观和局部悲观是一体两面的关系,识别它的要点就在于是否有全局有效性验证,这也和分布式数据库的架构特点息息相关。但是关于悲观协议,还有很多内容没有提及,下面我们就来填补这一大块空白。
## 悲观协议的分类
要搞清楚悲观协议的分类,其实是要先跳出来,从并发控制技术整体的分类体系来看。
事实上并发控制的分类体系连学术界的标准也不统一。比如在第13讲提到的两本经典教材中“[Principles of Distributed Database Systems](https://link.springer.com/content/pdf/bfm%3A978-1-4419-8834-8%2F1.pdf)”的分类是按照比较宽泛的乐观协议和悲观协议进行分类,子类之间又有很多重叠的概念,理解起来有点复杂。
而“[Transactional Information Systems : Theory, Algorithms, and the Practice of Concurrency Control and Recovery](http://www.gbv.de/dms/weimar/toc/647210940_toc.pdf)”采用的划分方式是狭义乐观协议和其他悲观协议。这里狭义乐观协议就是指我们在第13讲提到过的基于有效性验证的并发控制也是学术上定义的OCC。
我个人认为,狭义乐观协议和其他悲观协议这种分类方式更清晰些,所以就选择了“ Transactional Information Systems : Theory, Algorithms, and the Practice of Concurrency Control and Recovery”中的划分体系。下面我摘录了书中的一幅图用来梳理不同的并发控制协议。
![](https://static001.geekbang.org/resource/image/ed/a1/ede7edbb88108b76d015fa69d14425a1.png)
这个体系首先分为悲观和乐观两个大类。因为这里的乐观协议是指狭义乐观并发控制,所以包含内容就比较少,只有前向乐观并发控制和后向乐观并发控制;而悲观协议又分为基于锁和非锁两大类,其中基于锁的协议是数量最多的。
## 两阶段封锁Two-Phase Locking2PL
基于锁的协议显然不只是2PL还包括有序共享Ordered Sharing 2PL, O2PL、利他锁Altruistic Locking, AL、只写封锁树Write-only Tree Locking, WTL和读写封锁树Read/Write Tree Locking RWTL。但这几种协议在真正的数据库系统中很少使用所以就不过多介绍了我们还是把重点放在数据库系统主要使用的2PL上。
2PL就是事务具备两阶段特点的并发控制协议这里的两个阶段指加锁阶段和释放锁阶段并且加锁阶段严格区别于紧接着的释放锁阶段。我们可以通过一张图来加深对2PL理解。
![](https://static001.geekbang.org/resource/image/54/75/540f30e8c3877b4951711e3c7305df75.png)
在t1时刻之前是加锁阶段在t1之后则是释放锁阶段我们可以从时间上明确地把事务执行过程划分为两个阶段。2PL的关键点就是释放锁之后不能再加锁。而根据加锁和释放锁时机的不同2PL又有一些变体。
**保守两阶段封锁协议**Conservative 2PLC2PL事务在开始时设置它需要的所有锁。
![](https://static001.geekbang.org/resource/image/66/81/6680049eacdd2bab61a65eyy0e36a881.png)
**严格两阶段封锁协议**Strict 2PLS2PL事务一直持有已经获得的所有写锁直到事务终止。
![](https://static001.geekbang.org/resource/image/b7/ec/b7291c52385b627282f5c4f010acacec.png)
**强两阶段封锁协议**Strong Strict 2PLSS2PL事务一直持有已经获得的所有锁包括写锁和读锁直到事务终止。SS2PL与S2PL差别只在于一直持有的锁的类型所以它们的图形是相同的。
理解了这几种2PL的变体后我们再回想一下[第13讲](https://time.geekbang.org/column/article/282401)中的Percolator模型。当主锁Primary Lock没有释放前所有的记录上的从锁Secondary Lock实质上都没有释放在主锁释放后所有从锁自然释放。所以Percolator也属于S2PL。TiDB的乐观锁机制是基于Percolator的那么TiDB就也是S2PL。
事实上S2PL可能是使用最广泛的悲观协议几乎所有单体数据都依赖S2PL实现可串行化。而在分布式数据库中甚至需要使用SS2PL来保证可串行化执行典型的例子是TDSQL。但S2PL模式下事务持有锁的时间过长导致系统并发性能较差所以实际使用中往往不会配置到可串行化级别。这就意味着我们还是没有生产级技术方案只能期望出现新的方式既达到可串行化隔离级别又能有更好的性能。最终我们等到了一种可能是性能更优的工程化实现这就是CockroachDB的串行化快照隔离SSI。而SSI的核心就是串行化图检测SGT
## 串行化图检测SGT
SSI是一种隔离级别的命名最早来自PostgreSQLCockroachDB沿用了这个名称。它是在SI基础上实现的可串行化隔离。同样作为SSI核心的SGT也不是CockroachDB首创学术界早就提出了这个理论但真正的工程化实现要晚得多。
### 理论来源PostgreSQL
PostgreSQL在论文“[Serializable Snapshot Isolation in PostgreSQL](http://vldb.org/pvldb/vol5/p1850_danrkports_vldb2012.pdf)”中最早提出了SSI的工程实现方案这篇论文也被VLDB2012收录。
为了更清楚地描述SSI方案我们先要了解一点理论知识。
串行化理论的核心是串行化图Serializable GraphSG。这个图用来分析数据库事务操作的冲突情况。每个事务是一个节点事务之间的关系则表示为一条有向边。那么什么样的关系可以表示为边呢
串行化图的构建规则是这样的,事务作为节点,当一个操作与另一个操作冲突时,在两个事务节点之间就可以画上一条有向边。
具体来说,事务之间的边又分为三类情况:
1. 写读依赖WR-Dependencies第二个操作读取了第一个操作写入的值。
2. 写写依赖WW-Dependencies第二个操作覆盖了第一个操作写入的值。
3. 读写反依赖RW-Antidependencies第二个操作覆盖了第一个操作读取的值可能导致读取值过期。
我们通过一个例子,看看如何用这几条规则来构建一个简单的串行化图。
![](https://static001.geekbang.org/resource/image/0d/73/0ddedf5e27966fac0388f36ddde7f473.png)
图中一共有三个事务先后执行事务T1先执行W(A)T2再执行R(A)所以T1与T2之间存在WR依赖因此形成一条T1指向T2的边同理T2的W(B)与T3的R(B)也存在WR依赖T1的W(A)与T3的R(A)之间也是WR依赖这样就又形成两条有向边分别是T2指向T3和T1指向T3。
![](https://static001.geekbang.org/resource/image/7e/f5/7e66dc945b0ed425781f1e922ed338f5.png)
最终我们看到产生了一个有向无环图Directed Acyclic GraphDAG。能够构建出DAG就说明相关事务是可串行化执行的不需要中断任何事务。
我们可以使用SGT验证一下典型的死锁情况。我们知道事务T1和T2分别以不同的顺序写两个数据项那么就会形成死锁。
![](https://static001.geekbang.org/resource/image/a2/7f/a2042eef98d2a84556fcbfb1814b517f.png)
用串行化图来体现就是这个样子,显然构成了环。
![](https://static001.geekbang.org/resource/image/88/ef/88d4955b226a9ee089cd8d361d86d0ef.png)
在SGT中WR依赖和WW依赖都与我们的直觉相符而RW反向依赖就比较难理解了。在PostgreSQL的论文中专门描述了一个RW反向依赖的场景这里我把它引用过来我们一起学习一下。
这个场景一共需要维护两张表一张收入表reciepts会记入当日的收入情况每行都会记录一个批次号另一张独立的控制表current\_batch里面只有一条记录就是当前的批次号。你也可以把这里的批次号理解为一个工作日。
同时还有三个事务T1、T2、T3。
* T2是记录新的收入NEW-RECEIPT从控制表中读取当前的批次号然后在收入表中插入一条新的记录。
* T3负责关闭当前批次CLOSE-BATCH而具体实现是通过将控制表中的批次号递增的方式这就意味着后续再发生的收入会划归到下一个批次。
* T1是报告REPORT读取当前控制表的批次号处理逻辑是用当前已经加一的批次号再减一。T1用这个批次号作为条件读取收据表中的所有记录。查询到这个批次也就是这一日所有的交易。
其实,这个例子很像银行存款系统的日终翻牌。
因为T1要报告当天的收入情况所以它必须要在T3之后执行。事务T2记录了当天的每笔入账必须在T3之前执行这样才能出现在当天的报表中。三者顺序执行可以正常工作否则就会出现异常比如下面这样的
![](https://static001.geekbang.org/resource/image/b2/01/b2223b52a92e76f5dfe338e366238501.png)
T2先拿到一个批次号x随后T3执行批次号关闭后x这个批次号其实已经过期但是T2还继续使用x记录当前的这笔收入。T1正常在T3后执行此时T2尚未提交所以T1的报告中漏掉了T2的那笔收入。因为T2使用时过期的批次号x第二天的报告中也不会统计到这笔收入最终这笔收入就神奇地消失了。
在理解了这个例子的异常现象后我们用串行化图方法来验证一下。我们是把事务中的SQL抽象为对数据项的操作可以得到下面这张图。
![](https://static001.geekbang.org/resource/image/88/7a/884e0f7b80fc32b9b8491ee76f8f127a.png)
图中batch是指批次号reps是指收入情况。
接下来我们按照先后顺序提取有向边先由T2.R(batch) -> T3.W(batch)得到T2到T3的RW依赖再由T3.W(batch)->T1.R(batch),得到 T3到T1的WR依赖最后由T1.R(reps)->T2.W(reps)得到T1到T2的RW依赖。这样就构成了下面的串行化图。
![](https://static001.geekbang.org/resource/image/bf/e8/bf6019d3e3953a4075b001f4853963e8.png)
显然这三个事务之间是存在环的,那么这三个事务就是不能串行化的。
这个异常现象中很有意思的一点是虽然T1是一个只读事务但如果没有T1的话T2与T3不会形成环依然是可串行化执行的。这里就为我们澄清了一点我们直觉上认为的只读事务不会影响事务并发机制其实是不对的。
### 工程实现CockroachDB
RW反向依赖是一个非常特别的存在而特别之处就在于传统的锁机制无法记录这种情况。因此在论文“[Serializable Snapshot Isolation in PostgreSQL](http://vldb.org/pvldb/vol5/p1850_danrkports_vldb2012.pdf)”中提出增加一种锁SIREAD用来记录快照隔离SI上所有执行过的读操作Read从而识别RW反向依赖。本质上SIREAD并不是锁只是一种标识。但这个方案面临的困境是读操作涉及到的数据范围实在太大跟踪标识带来的成本可能比S2PL还要高也就无法达到最初的目标。
针对这个问题CockroachDB做了一个关键设计**读时间戳缓存**Read Timestamp Cache简称RTC。
基于RTC的新方案是这样的当执行任何的读取操作时操作的时间戳都会被记录在所访问节点的本地RTC中。当任何写操作访问这个节点时都会以将要访问的Key为输入向RTC查询最大的读时间戳MRT如果MRT大于这个写入操作的时间戳那继续写入就会形成RW依赖。这时就必须终止并重启写入事务让写入事务拿到一个更大的时间戳重新尝试。
具体来说RTC是以Key的范围来组织读时间戳的。这样当读取操作携带了谓词条件比如where子句对应的操作就是一个范围读取会覆盖若干个Key那么整个Key的范围也可以被记录在RTC中。这样处理的好处是可以兼容一种特殊情况。
例如事务T1第一次范围读取Range Scan数据表where条件是“>=1 and <=5”读取到1、2、5三个值T1完成后事务T2在该表插入了4因为RTC记录的是范围区间\[1,5\]所以4也可以被检测出存在RW依赖。这个地方有点像MySQL间隙锁的原理。
RTC是一个大小有限的采用LRULeast Recently Used最近最少使用淘汰算法的缓存。当达到存储上限时最老的时间戳会被抛弃。为了应对缓存超限的情况会将RTC中出现过的所有Key上最早的那个读时间戳记录下来作为低水位线Low Water Mark。如果一个写操作将要写的Key不在RTC中则会返回这个低水位线。
## 相对乐观
到这里你应该大概理解了SGT的运行机制它和传统的S2PL一样属于悲观协议。但SGT没有锁的管理成本所以性能比S2PL更好。
CockroachDB基于SGT理论进行工程化使可串行化真正成为生产级可用的隔离级别。从整体并发控制机制看CockroachDB和上一讲的TiDB一样虽然在局部看是悲观协议但因为不符合严格的VRW顺序所以在全局来看仍是一个相对乐观的协议。
这种乐观协议同样存在[第13讲](https://time.geekbang.org/column/article/282401)提到的问题所以CockroachDB也在原有基础上进行了改良通过增加全局的锁表Lock Table使用加锁的方式先进行一轮全局有效性验证确定无冲突的情况下再使用单个节点的SGT。
## 小结
有关悲观协议的内容就聊到这里了,我们一起梳理下今天课程的重点。
1. 并发控制机制的划分方法很多,没有统一标准,我们使用了[Transactional Information Systems : Theory, Algorithms, and the Practice of Concurrency Control and Recovery](http://www.gbv.de/dms/weimar/toc/647210940_toc.pdf)提出的划分标准分为悲观协议与乐观协议两种。这里的乐观协议是上一讲提到的狭义乐观协议悲观协议又分为锁和非锁两大类我们简单介绍了2PL这一个分支。
2. 我们回顾了Percolator模型按照S2PL的定义Percoloatro本质就是S2PL因此TiDB的乐观锁也属于S2PL。
3. S2PL是数据库并发控制的主流技术但是锁管理复杂在实现串行化隔离级别时开销太大。而后我们讨论了非锁协议中的串行化图检测SGT。PostgreSQL最早提出了SGT的工程实现方式SSI。CockroachDB在此基础上又进行了优化降低了SIREAD的开销是生产级的可串行化隔离。
4. CockroachDB最初和TiDB一样都是局部采用悲观协议而不做全局有效性验证是广义的乐观协议。后来CockroachDB同样也将乐观协议改为悲观协议采用的方式是增加全局的锁表进行全局有效性验证而后再转入单个的SGT处理。
今天的课程中我们提到了串行化理论只有当相关事务形成DAG图时这些事务才是可串行化的。这个理论不仅适用于SGT2PL的最终调度结果也同样是DAG图。在更大范围内批量任务调度时DAG也同样被作为衡量标准例如Spark。
![](https://static001.geekbang.org/resource/image/a2/98/a2de442d44b6fd69c16e83a509c0a698.png)
## 思考题
课程的最后,我们来看看今天的思考题。
在[第11讲](https://time.geekbang.org/column/article/280925)中我们提到了MVCC。有的数据库教材中将MVCC作为一种重要的并发控制技术与乐观协议、悲观协议并列但我们今天并没有单独提到它。所以我的问题是你觉得该如何理解MVCC与乐观协议、悲观协议的关系呢
欢迎你在评论区留言和我一起讨论,我会在答疑篇回复这个问题。如果你身边的朋友也对悲观协议或者并发控制技术这个话题感兴趣,你也可以把今天这一讲分享给他,我们一起讨论。
## 学习资料
最早的SSI工程实现方案[Serializable Snapshot Isolation in PostgreSQL](http://vldb.org/pvldb/vol5/p1850_danrkports_vldb2012.pdf)
按照狭义乐观协议和其他悲观协议划分并发控制协议:[Transactional Information Systems : Theory, Algorithms, and the Practice of Concurrency Control and Recovery](http://www.gbv.de/dms/weimar/toc/647210940_toc.pdf)