gitbook/分布式数据库30讲/docs/282401.md
2022-09-03 22:05:03 +08:00

15 KiB
Raw Permalink Blame History

13 | 隔离性:为什么使用乐观协议的分布式数据库越来越少?

你好我是王磊你也可以叫我Ivan。

我们在11、12两讲已经深入分析了读写冲突时的控制技术这项技术的核心是在实现目标隔离级别的基础上最大程度地提升读写并发能力。但是读写冲突只是事务冲突的部分情况更多时候我们要面对的是写写冲突甚至后者还要更重要些。

你可能经常会听到这类说法“某某网站的架构非常牛能抗住海量并发”“某某网站很弱搞个促销让大家去抢红包结果活动刚开始2分钟系统就挂了”。其实要想让系统支持海量并发很重要的基础就是数据库的并发处理能力而这里面最重要的就是对写写冲突的并发控制。因为并发控制如此重要所以很多经典教材都会花费大量篇幅来探讨这个问题进而系统性地介绍并发控制技术。在这个技术体系中虽然有多种不同的划分方式但最为大家熟知就是悲观协议和乐观协议两大类。

TiDB和CockroachDB的流行一度让大家对于乐观协议这个概念印象深刻。但是经过几年的实践两款产品都将默认的并发控制机制改回了悲观协议。他们为什么做了这个改变呢我们这一讲专门来探讨什么是乐观控制协议以及为什么TiDB和CockroachDB不再把它作为默认选项。

并发控制技术的分类

首先,无论是学术界还是工业界,都倾向于将并发控制分为是悲观协议和乐观协议两大类。但是,这个界限在哪,其实各有各的解释。

我先给一个朴素版的定义。所谓悲观与乐观,它和我们自然语言的含义大致是一样的,悲观就是对未来的一种负面预测,具体来说,就是认为会出现比较多的事务竞争,不容易获得充足的资源完成事务操作。而乐观,则完全相反,认为不会有太多的事务竞争,所以资源是足够的。这个定义虽然不那么精准,但大体表示了两种机制的倾向。

再进一步,落到实现机制上,有一个广泛被提到的定义版本。乐观协议就是直接提交,遇到冲突就回滚;悲观协议就是在真正提交事务前,先尝试对需要修改的资源上锁,只有在确保事务一定能够执行成功后,才开始提交。

总之,这个版本的核心就是,悲观协议是使用锁的,而乐观协议是不使用锁的。这就非常容易把握了。

但是这个解释和真正分布式数据库产品的实现还是有些差距的比如TiDB就宣称自己使用了乐观锁。对你没听错是乐观锁。怎么有锁还能乐观呢是不是有点蒙了

为了让你在学习过程中不背负着这个巨大的问号我们就先放下对定义的探讨先来看看TiDB乐观锁的实现方式。

乐观锁TiDB

TiDB的乐观锁基本上就是Percolator模型这个模型我们在第9讲时曾经介绍过,它的运行过程可以分为三个阶段。

  1. 选择Primary Row

收集所有参与修改的行从中随机选择一行作为这个事务的Primary Row这一行是拥有锁的称为Primary Lock而且这个锁会负责标记整个事务的完成状态。所有其他修改行也有锁称为Secondary Lock都会保留指向Primary Row的指针。

  1. 写入阶段

按照两阶段提交的顺序执行第一阶段。每个修改行都会执行上锁并执行“prewrite”prewrite就是将数据写入私有版本其他事务不可见。注意这时候每个修改行都可能碰到锁冲突的情况如果冲突了就终止事务返回给TiDB那么整个事务也就终止了。如果所有修改行都顺利上锁完成prewrite第一阶段结束。

  1. 提交阶段

这是两阶段提交的第二阶段,提交 Primary Row也就是写入新版本的提交记录并清除 Primary Lock如果顺利完成那么这个事务整体也就完成了反之就是失败。而Secondary Rows上的锁则会交给异步线程根据Primary Lock的状态去清理。

你看这个过程中不仅有锁,而且锁的数量还不少。那么,为什么又说它是乐观协议呢?

想回答这个问题,我们就需要一些理论知识了。

并发控制的三个阶段

在经典理论教材“Principles of Distributed Database Systems”中作者将乐观协议和悲观协议的操作都统一成四个阶段分别是有效性验证V、读R、计算C和写W。两者的区别就是这四个阶段的顺序不同悲观协议的操作顺序是VRCW而乐观协议的操作顺序则是RCVW。因为在比较两种协议时计算C这个阶段没有实质影响可以忽略掉。那么简化后悲观协议的顺序是VRW而乐观协议的顺序就是RVW。

RVW的三阶段划分也见于研究乐观协议的经典论文“On Optimistic Methods for Concurrency Control”。

关于三个阶段的定义在不同文献中稍有区别其中“Principles of Distributed Database Systems”对这三个阶段的定义通用性更强对于RVW和VRW都是有效的我们先看下具体内容。

读阶段Read Pharse,每个事务对数据项的局部拷贝进行更新。

要注意,此时的更新结果对于其他事务是不可见的。这个阶段的命名特别容易让人误解,明明做了写操作,却叫做“读阶段”。我想它大概是讲,那些后面要写入的内容,先要暂时加载到一个仅自己可见的临时空间内。这有点像我们抄录的过程,先读取原文并在脑子里记住,然后誊写出来。

有效性确认阶段Validation Pharse,验证准备提交的事务。

这个验证就是指检查这些更新是否可以保证数据库的一致性,如果检查通过进入下一个阶段,否则取消事务。再深入一点,这段话有两层意思。首先这里提到的检查与隔离性目标有直接联系;其次就是检查可以有不同的手段,也就是不同的并发控制技术,比如可以是基于锁的检查,也可以是基于时间戳排序。

写阶段Write Pharse,将读阶段的更新结果写入到数据库中,接受事务的提交结果。

这个阶段的工作就比较容易理解了,就是完成最终的事务提交操作。

还有一种关于乐观与悲观的表述,也与三阶段的顺序相呼应。乐观,重在事后检测,在事务提交时检查是否满足隔离级别,如果满足则提交,否则回滚并自动重新执行。悲观,重在事前预防,在事务执行时检查是否满足隔离级别,如果满足则继续执行,否则等待或回滚。

我们再回到TiDB的乐观锁。虽然对于每一个修改行来说TiDB都做了有效性验证而且顺序是VRW可以说是悲观的但这只是局部的有效性验证从整体看TiDB没有做全局有效性验证不符合VRW顺序所以还是相对乐观的。

下面这一段,我们稍微延伸一下有关乐观并发控制的知识。

狭义乐观并发控制OCC

Transactional Information Systems : Theory, Algorithms, and the Practice of Concurrency Control and Recovery”给出了一个专用于RVW的三阶段定义也就是说它是专门描述乐观协议的。其中主要差别在“有效性确认阶段”是针对可串行化的检查检查采用基于时间戳的特定算法。

这个定义是一个更加具体的乐观协议严格符合RVW顺序所以我把它称为狭义上的乐观并发控制Optimistic Concurrency Control也称为基于有效性确认的并发控制Validation-Based Concurrency Control。很多学术论文中的OCC就是指它。而在工业界真正生产级的分布式数据库还很少使用狭义OCC进行并发控制唯一的例外就是FoundationDB。与之相对应的则是TiDB这种广义上的乐观并发控制说它乐观是因为它没有严格遵循VRW顺序。

乐观协议的挑战

TiDB的乐观锁已经讲清楚了我们再回到这一讲的主线为啥乐观要改成悲观呢主要是两方面的挑战一是事务冲突少是使用乐观协议的前提但这个前提是否普遍成立二是现有应用系统使用的单体数据库多是悲观协议兼容性上的挑战。

事务频繁冲突

首先,事务冲突少这个前提,随着分布式数据库的适用场景越来越广泛,显得不那么有通用性了。比如,金融行业就经常会有一些事务冲突多又要保证严格事务性的业务场景,一个简单的例子就是银行的代发工资。代发工资这个过程,其实就是从企业账户给一大批个人账户转账的过程,是一个批量操作。在这个大的转账事务中可能涉及到成千上万的更新,那么事务持续的时间就会比较长。如果使用乐观协议,在这段时间内,只要有一个人的账户余额发生变化,事务就要回滚,那么这个事务很可能一直都在重试、回滚,永远也执行不完。这个时候,我们就一点也不要乐观了,像传统单体数据库那样,使用最悲观的锁机制,就很容易实现也很高效。

当然为了避免这种情况的出现TiDB的乐观锁约定了事务的长度默认单个事务包含的 SQL 语句不超过 5000 条。但这种限制其实是一个消极的处理方式,毕竟业务需求是真实存在的,如果数据库不支持,就必须通过应用层编码去解决了。

遗留应用的兼容性需求

回到悲观协议还有一个重要的原因那就是保证对遗留应用系统的兼容性。这个很容易理解因为单体数据库都是悲观协议甚至多数都是基于锁的悲观协议所以在SQL运行效果上与乐观协议有直接的区别。一个非常典型的例子就是select for update。这是一个显式的加锁操作或者说是显式的方式进行有效性确认广义的乐观协议都不提供严格的RVW所以也就无法支持这个操作。

select for update是不是一个必须的操作呢其实也不是的这个语句出现是因为数据库不能支持可串行化隔离给应用提供了一个控制手段主导权交给了应用。但是这就是单体数据库长久以来的规则已经是生态的一部分为了降低应用的改造量新产品还是必须接受。

乐观协议的改变

因为上面这些挑战TiDB的并发控制机制也做出了改变增加了“悲观锁”并作为默认选项。TiDB悲观锁的理论基础很简单就是在原有的局部有效性确认前增加一轮全局有效性确认。这样就是严格的VRW自然就是标准的悲观协议了。具体采用的方式就是增加了悲观锁这个锁是实际存在的表现为一个占位符随着SQL的执行即时向存储系统TiKV发出这样事务就可以在第一时间发现是否有其他事务与自己冲突。

另外悲观锁还触发了一个变化。TiDB原有的事务模型并不是一个交互事务它会把所有的写SQL都攒在一起在commit阶段一起提交所以有很大的并行度锁的时间较短死锁的概率也就较低。因为增加了悲观锁的加锁动作变回了一个可交互事务TiDB还要增加一个死锁检测机制。

小结

到这里,今天的内容就告一段落了,让我们一起梳理下今天内容。

  1. 并发控制分为乐观和悲观两种,它们的语义和自然语言都是对未来正面或负面的预期。乐观协议预期事务冲突很少,所以不会提前做什么,悲观协议认为事务冲突比较多,所以要有所准备。
  2. 按照经典理论并发控制都是由三个阶段组成分别是有效性确认V、读R和写W。悲观协议的执行顺序是VRW乐观协议的执行顺序是RVW。所以又可以得到两个乐观协议的定义狭义上必须满足RVW才是乐观并发控制而且三阶段有更具体的要求这个就是学术论文上的OCC。另外广义的定义只要不是严格VRW的并发控制都是相对乐观的都是乐观并发控制TiDB就是相对乐观。生产级的分布式数据库很少有使用狭义的OCC协议FoundationDB是一个例外。
  3. 乐观协议的挑战来自两个方面。第一点乐观协议在事务冲突较少时因为避免了锁的管理开销能提供更好的性能但在事务冲突较多时会出现大量的回滚效率低下。总的来说乐观协议的通用性并不好。第二点传统单体数据库几乎都是基于锁的悲观协议乐观协议在语义层面没有对等的SQL例如select for update。因此TiDB和CockroachDB都由乐观协议转变为悲观协议并且是基于锁的悲观协议。
  4. TiDB的悲观协议符合严格的VRW顺序在原有的两阶段提交前增加了一轮悲观锁占位操作实现全局有效性确认。但是随着悲观锁的引入TiDB转变为一个可交互事务模式出现死锁的概率大幅提升对应增加死锁检测功能。

今天的课程中我们澄清了对乐观协议的常见误解也解释了为什么TiDB要把默认选项从乐观锁改为悲观锁。当然你一定注意到了除了基于锁的悲观协议外还有一些其他技术比如基于时间戳排序TO和串行化图检测SGT我们将在下一讲中继续探讨这个话题。

思考题

课程的最后,我们来看看今天的思考题。

在这一讲开头我们说TiDB和CockroachDB都经历了从乐观协议到悲观协议的转变但在文中只展开了TiDB变化前后的设计细节。其实对于CockroachDB你应该也不陌生了我们在最近几讲中都有提及到它的一些特性。所以我今天的问题是请你推测下CockraochDB的悲观协议大概会采用什么方式而这种方式又有什么优势当然CockroachDB的实现方式很容易查到所以我更关心你的思考的过程。

欢迎你在评论区留言和我一起讨论,我会在答疑篇和你继续讨论这个问题。如果你身边的朋友也对乐观协议或者并发控制技术这个话题感兴趣,你也可以把今天这一讲分享给他,我们一起讨论。

学习资料

Gerhard Weikum and Gottfried Vossen: Transactional Information Systems : Theory, Algorithms, and the Practice of Concurrency Control and Recovery

H. T. Kung and John T. Robinson: On Optimistic Methods for Concurrency Control

M. Tamer Özsu and Patrick Valduriez: Principles of Distributed Database Systems