并行与分布式系统中的分布式事务处理:乐观并发控制(Optimistic Concurrency Control, OCC)算法
算法描述
在分布式数据库或存储系统中,多个客户端可能并发地读写同一数据项,为了保证事务的“可串行化”(Serializable)正确性,需要并发控制机制。乐观并发控制(OCC)是一种无锁并发控制策略。与悲观锁(如两阶段锁,2PL)在事务执行过程中就加锁不同,OCC假设事务间的冲突概率很低,允许事务无阻塞地执行读写操作,只在事务提交时验证其执行过程中是否有其他事务破坏了可串行性。如果验证通过,则提交;否则,事务将中止并回滚。在并行与分布式环境中,OCC尤其适合读多写少的场景,因为它能减少锁带来的阻塞和死锁,但其回滚代价可能较高。
核心思想:
- 读阶段:事务对数据的读写都在本地私有空间(如工作区)进行,不直接影响全局数据库。
- 验证阶段:在事务提交前,验证其执行过程中读到的数据是否被其他已提交事务修改过,以确保可串行性。
- 写阶段:如果验证通过,则将事务在私有空间的修改原子性地写入全局数据库;否则,中止并重启事务。
分布式OCC挑战:在分布式环境中,数据可能分片存储在不同节点。事务可能访问多个节点,验证和提交必须协调所有相关节点,保证原子性(所有节点要么都提交,要么都中止)和全局可串行性。
解题过程循序渐进讲解
我们将以一个典型的分布式OCC协议(基于时间戳或版本号验证)为例,详细讲解其步骤。
第一阶段:事务执行(读阶段)
每个事务 T 在执行时,会维护三个集合:
ReadSet(T):记录T读取的所有数据项的标识符(如键)及其读取时刻的版本号(或时间戳)。WriteSet(T):记录T将要写入的所有数据项的新值。- 一个私有工作区,用于暂存对数据项的修改。
步骤1:读取数据项
- 当
T需要读取数据项X时:- 从存储
X的节点获取X的当前值value(X)及其当前版本号version(X)。 - 将
(X, version(X))记录到ReadSet(T)中。 - 将
value(X)返回给事务逻辑使用。
- 从存储
- 关键点:读取操作不会阻塞其他事务的读写。多个事务可以并发读取同一数据项的不同版本。
步骤2:写入数据项
- 当
T需要写入新值到数据项X时:- 将
X和新值记录到私有工作区的WriteSet(T)中。注意:这个写操作不立即更新全局数据库中的X,因此其他事务看不到T的修改,直到T成功提交。
- 将
第二阶段:事务验证与提交
当事务 T 完成所有读写操作,准备提交时,进入验证阶段。这是OCC的核心,目标是检查事务执行期间,ReadSet(T) 中的数据项是否被其他已提交的事务修改过。
验证的基本逻辑(本地视角):
对于 ReadSet(T) 中的每个数据项 X,比较事务 T 读取时的版本号 read_version_T(X) 与 X 当前的全局版本号 current_version(X)。
- 如果对所有
X都有read_version_T(X) == current_version(X),则验证通过。 - 如果存在某个
X使得read_version_T(X) != current_version(X),则说明在T执行期间,有其他事务修改了X并已提交。T的读已失效,验证失败,T必须中止。
分布式环境下的验证挑战:ReadSet(T) 和 WriteSet(T) 中的数据项可能分布在多个节点。验证和后续的写操作必须是原子性的(全有或全无)。常用方法是采用一个协调者节点(例如,事务发起节点或一个指定的协调者)来管理两阶段提交。
分布式OCC协议步骤详解:
步骤3:启动提交与全局验证
- 准备请求:事务
T的协调者向所有涉及ReadSet(T)和WriteSet(T)的节点(称为参与者节点)发送一个 PREPARE 请求。这个请求包含了T的ID、ReadSet(T)的列表(每个数据项及其读取的版本号)以及WriteSet(T)的列表(每个数据项及其新值)。 - 本地验证:每个参与者节点
P_i收到 PREPARE 请求后,对其本地存储的、属于ReadSet(T)的数据项进行验证:- 对于每个本地数据项
X,检查X的当前版本号current_version(X)是否等于T报告的read_version_T(X)。 - 同时,
P_i会尝试为T获取对WriteSet(T)中本地数据项的“写锁”或预留提交权限。这是为了防止在验证通过后、实际写入前,有其他并发验证的事务冲突。一种常见策略是为每个数据项维护一个“已提交但未完成写入”的事务列表。
- 对于每个本地数据项
- 投票:每个参与者
P_i根据本地验证结果进行投票:- 如果所有本地相关数据项的版本号都匹配,并且成功预留了写入权限,则
P_i回复协调者 VOTE-COMMIT。 - 否则(任一验证失败或预留失败),回复 VOTE-ABORT。
- 如果所有本地相关数据项的版本号都匹配,并且成功预留了写入权限,则
步骤4:全局决定与写入
- 收集与决定:协调者收集所有参与者的投票。
- 如果所有参与者都返回 VOTE-COMMIT,协调者做出 GLOBAL-COMMIT 的决定。
- 如果有任何一个参与者返回 VOTE-ABORT,协调者做出 GLOBAL-ABORT 的决定。
- 通知与执行:协调者将决定(COMMIT 或 ABORT)发送给所有参与者。
- 如果决定是 COMMIT:
a. 每个参与者P_i将WriteSet(T)中属于本地的数据项的新值原子性地写入数据库。
b. 同时,增加这些数据项的版本号(例如,版本号+1或设置为一个全局递增的时间戳)。
c. 释放为T预留的写入权限。 - 如果决定是 ABORT:
a. 参与者直接丢弃T的WriteSet信息。
b. 释放预留的写入权限。
- 如果决定是 COMMIT:
- 确认:参与者在完成写入(或清理)后,向协调者发送确认。协调者在收到所有确认后,整个事务提交过程完成。
第三阶段:事务中止与重启
如果事务 T 在验证阶段失败(收到 ABORT 决定),它必须中止。这意味着:
- 丢弃其私有工作区中的所有修改(
WriteSet)。 - (可选)等待一段随机时间后,重启整个事务(从阶段一开始重新执行)。重启时,它会重新读取数据项的最新版本,从而基于最新的数据状态重新计算。
关键点与优化
- 版本号管理:版本号是实现高效验证的核心。每次成功提交的写操作都会增加数据项的版本号。验证时只需比较版本号,无需比较完整数据内容,效率很高。
- 验证粒度:除了上述后向验证(提交时检查读集是否过时),还有前向验证(检查当前事务的写集是否会破坏其他正在运行事务的读集)。分布式环境下后向验证更常见。
- 分布式原子提交:上述步骤3和4本质是一个两阶段提交(2PC) 协议,用于保证所有节点对事务提交结果达成一致。这是分布式OCC的重要组成部分。
- 性能权衡:OCC在低冲突下性能优异(无锁,高并发)。但在高冲突(大量写竞争)场景下,频繁的验证失败和事务重启会导致性能急剧下降。因此适用于读多写少、冲突概率低的应用。
总结
分布式乐观并发控制算法将事务执行与冲突检测解耦,通过“先执行,后验证”的策略,在分布式环境中实现了无锁的并发控制。其核心步骤是:1) 无阻塞地读取数据并记录版本号;2) 在提交前,协调所有相关节点验证读集版本号的有效性(通过2PC协议);3) 如果验证成功,则原子性地提交所有写操作并更新版本号,否则中止并重启事务。该算法的有效性高度依赖于数据访问冲突的频率。