- 原文:How CockroachDB Does Distributed, Atomic Transactions
- 原文链接:https://www.cockroachlabs.com/blog/how-cockroachdb-distributes-atomic-transactions/
- 原作者: Matt Tracy
- 原文日期:Sep 2, 2015
- 译:zifeiy
CockroachDB是如何进行分布式原子事务的
CockroachDB的一个主要特性是他完全支持分布式数据库中任意键之间的ACID事务。
CockroachDB事务对数据库应用一系列的操作的同时,仍然能够保持ACID属性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
在这片文章中,我们将重点讨论CockroachDB如何在不适用锁的情况下实现原子事务。
__原子性(Atomicity)__可以被定义如下:
对于一组操作,要么全部执行,要么一个也不执行。
没有原子性的话,那些执行了一般的事务在被中断的情况下就可能只执行了部分修改。
策略(Strategy)
CockroachDB用于提供原子事务的测录遵循以下步骤:
- 开关(Switch):在修改任何键的值之前,事务创建一个开关,它是一个可写的值,不同于在批处理中改变的任何实际值。开关不能同时被访问——开关的读写是严格有序的。开关最初是“关(off)”的,它可以切换到“开(on)”的状态。
- 筹划(Stage):写进程对数据库进行一些列的改变,但是不覆盖任何已存在的值;新的值做好了替换旧的值的准备。
- 过滤(Filter):对于所有有筹划值(staged value)的键(key),读进程在获得该键对应的值之前不需要检测一下该事务的开关的状态。如果开关状态是“关(off)”,读进程将会该键的原始的值;如果开关状态是“开(on)”,读进程会返回筹划后的值。因此,所有对某一个键的读进程所获取到的值将直接取决于开关的状态。
- 快速切换(Flip):当写进程准备好了事务中的所有变化,写进程快速切换开关到“开(on)”状态。与过滤(Filter)相结合,所以该事务中的分级的值将做好准备返回给接下来所有的读进程。
- 取消筹划(Unstage):一旦事务完成(中止或提交),筹划值将会尽可能快地被清空。如果该事务成功,那么所有原始值将会被筹划值(staged values)取代;如果事务失败了,所有筹划值将会被丢弃。需要注意的是,取消筹划是异步完成的,它并不需要在事务提交之后再进行。
详细的事务过程
开关(Switch):CockroachDB事务记录
为了开始事务,写进程(Writer)首先需要创建 事务记录(transaction record) 。
CockroachDB使用事务记录为我们的总体策略提供切换。
每个事务记录具有以下字段:
- 一个对应该事务的唯一标识(UUID);
- 当前状态(current state),包括 挂起(PENDING)、中止(ABORTED)或 已提交(COMMITED);
- 一个Cockroach K/V键。这个键用于指定“开关(switch)”在分布式数据存储中的位置。
写进程(writer)在挂起(PENDING)状态下生成一个新的UUID的事务记录。
然后,写进程使用一个特殊的CockroachDB命令 BeginTransaction()
来存储事务记录。
该记录与事务记录中的密钥位于同一位置(即在分布式系统中的同一节点上)。
因为事务记录存储在单个Cockroach键中,对它的操作会严格排序(通过组合Raft和我们的底层存储引擎)。
事务的 状态(state) 是开关(switch)的开关状态,状态的挂起(PENDING)和终止(ABORTED)对应“关(off)”,已提交(COMMITED)状态对应“开(on)”。
因此,事务记录满足我们对开关的需求。
(译者注:开关是一个理论概念,事务记录是开关的实现)
注意,事务状态可以由挂起(PENDING)
转移到终止(ABORTED)
或已提交(COMMITTED)
,但是没有别的转移方式(因为终止(ABORTED)
和已提交(COMMITED)
状态都是最终状态)。
筹划(Stage):写意图(Write Intents)
为了 筹划(stage) 在事务中的更改,CockroachDB使用了一个名为 “写意图(write intent)” 的结构。
任何时候,一个值作为事务的一部分被写入一个键,它被写为一个写意图。
如果事务成功结束,那么该写意图结构中所包含的值将会被成功写入(数据库中)。
写意图还包含存储事务记录的 键(key) 。
这是至关重要的:如果一个读进程(reader)遇到了一个写意图,他使用这个键的值来定位事务记录(开关(the switch))。
作为最终规则,在任何键上只能有一个写意图。
如果有多个同时发生的存储过程,可能会发生一个存储过程在写一个键的数据,而与此同时这个键又对应了另一个存储过程中的一个活跃的写意图。
然而,事务并发是一个复杂的话题,我们将在以后的博客文章中讨论(《论事务隔离》);
而现在,我们假设一次只有一个事务,并且现有的写意图必须来自一个废弃的事务。
当写入已具有写入意图的键时:
- 如果已经存在的意图(intent)的事务记录(transaction record)是
PENDING
状态的话,那么将其转换为ABORTED
状态。如果之前的事务是COMMITED
或ABORTED
,那么什么都不做。 - 清除早期事务中的现有意图,这将删除意图。
- 为并发事务添加新的意图。
过滤(Filter):读取一个意图
在读取一个键时,必须遵循总体策略的原则3,并在返回值之前查阅任何开关的值。
如果键包含一个普通值(比如,不是一个写意图),读进程(reader)可以确信,没有涉及此键的事务正在进行中,
并且它包含最近提交的值。
因此,该值逐字返回。
但是,如果读进程读取到的是一个写意图,则意味着在删除意图之前,在某个点之前放弃了先前的事务(记住:我们已经假设过在同一时间只能进行一个存储过程)。
在继续之前,读进程需要先检查事务的开关的状态(事务记录)。
- 如果事务状态(transaction record)目前仍然是
PENDING
则将其转换为ABORTED
状态。 - 清除早期事务中的现有意图,这将删除意图。(译者注:前两步和stage阶段一样)
- 返回键所对应的普通值。如果早先的事务的状态是
COMMITED
,则清理操作(cleanup operation)将会将普通值(plain value)更新为筹划值(staged value);不然的话,该步操作将会在事务开始之前返回这个键所对应的原始的值。
快速切换(Flip):提交事务
为了提交事务,事务记录被更新为提交(COMMITED)
的状态。
所有由该事务写的写意图将会立即生效:
在未来的所有遇到此事务的写意图的读取操作都会通过事务记录来获得结果数据,
(译者:意思大概是你一样的读数据,但是在Flip之后的这段时间,你的读取将会由写意图对应的事务记录来代理)
因为事务已经提交,所以返回的值由写意图给你。
(译者:意思大概是,香港回归第二天,你去办理政府业务,虽然香港的这个机构还没有马上变,但是里面的人已经变成了中国的了。那么其实接下来大家可以猜到,也就是中国部门正式替换掉英国的部门,而现在只是派一个人过来代理一下)
终止(Aborting)事务
可以通过更新事务记录的状态为中止(ABORTED)
来中止事务。
此时,事务将永久中止,将来的读取将忽略该事务创建的写意图。
取消筹划(Unstage):清空意图
上面的系统已经提供了原子提交的属性;
然而,过滤步骤是昂贵的,因为它需要跨分布式系统的写入来过滤中心位置(事务记录)。
对于分布式系统来说,这是不期望的行为。
因此,在事务完成后,我们会尽快地删除其创建的写意图:
如果键具有没有写意图的普通值,则不需要对读取操作进行过滤,从而以适当的分发方式完成。
清理操作(Cleanup Operation)
当关联事务不再挂起(PENDING)时,可以在写意图上调用清除操作。
它遵循以下简单步骤:
- 如果事务状态是
终止(ABORTED)
,写意图将会被移除。 - 如果事务状态是
已提交(COMMITTED)
,该键对应地写意图地筹划值将会被转化成普通值,然后这个写意图将会被移除。 - 清除操作是幂等(idempotent)的;也就是说,如果两个进程试图清理同一个键和事务的意图,则第二个操作将会被忽略。
清理是在下列情况下进行的:
- 当一个写进程成功提交或者终止了一个事务,他尝试立即清空它之前写下的所有写意图。
- 当一个写进程碰到了之前的事务中的写进程产生的写意图。
- 当一个读进程碰到了之前的事务中的写进程产生的写意图。
通过多重途径积极地清理过期的写意图,最小化地过滤了所需地性能影响。
圆满完成(Wrap Up)
自此,我们已经讨论了CockroachDB确保其分布式、无锁事务的原子性的基本策略。
但是故事比我在这里所说的更多。
CockroachDB支持并发的事务,这将写入一个包含重叠键的集合(sets)中。
允许重叠的并发事务是AID中的“I”,它保证事务隔离。
我们将在以后的文章中详细介绍我们是如何实现事务隔离的。
敬请期待!