Distributed Database System —— Write Intent In CockroachDB

文章目录

  • 概述
  • 两阶段提交
    • Phase 1: Prepare
    • Phase2: Commit or Rollback

概述

CockroachDB实现了 Serializability 的分布式 ACID 事务,本文主要理解原子性和隔离性的实现方案。

由于在CockroachDB(以下简称CRDB)中存在多个Range,所以一次事务可能会涉及到多个Range上的存储, 所以 CRDB 是使用 Two-Phase Commit 来保证事务在多个 store 同时进行事务修改操作的原子性。不过CRDB在此基础上做了很多优化。

两阶段提交

Phase 1: Prepare

在执行事务的DML语句时,DML修改的数据会以被称为WriteIntent的特殊MVCC的形式作为“未提交数据”存储到底层的RocksDB中,这部分数据由于还未真正的Commit,所以对外是不可见的对自己本事务是可见的)。

为了证明Write Intent的数据在一段时间后,应该作为已提交或者未提交数据来对待(在Phase2:Commit阶段会被决定),所以在Prepare阶段,会在Transanction Record上来记录这个Write Intent数据的相关信息,作为一个中心化的开关控制。这个Transaction Record可能也会被记录到某一个RocksDB上。

下面来看看这个Transaction Record:
CRDB每个 Transaction 会需要有一条 Transaction Record,用来表示整个分布式事务的状态,后面遇到读操作的时候如果遇到Write Intent的数据,回去找到对应的Transaction Record来判断是否应该读取数据。
在存储Transaction Record的RocksDB上,一个事务会以KV的形式做存储:

  • Key
    每个事务会选取读取或者写入的第一个Range的Start Key作为该事务的Anchor key,而实际存储的Key是通过Anchor Key+Transaction Suffix+事务的UUID组合的,被称为Range-Local Key,这样做的好处是一个Range中的所有Transaction Record都被有序的聚集在一起,方便找到所有的Records。
  • Value
    对应的Value则是Transaction Record结构体,其中包含Transaction的Metadata、Timestamp以及其他信息,这点之后会详细介绍。

  • 写Write Intent

在 Phase 1 执行的 DML 在 CRDB 中会直接发向 range 对应的存储节点,并作为特殊的 mvcc 保存, write intent 能知道 transaction record 的所在, write intent 作为特殊的 mvcc 可以理解为"未提交的临时数据", Phase1 中写入在 Phase2 提交后会转换为正常的 mvcc 数据而真正"生效"。

在RocksDB中,对于一个已经提交的数据(非Write Intent),它的Key的保存方式是在编码后的Key之后附加一个Version,可以简单的理解为:key+timestamp;而对于一个Write Intent的数据,它不会携带timestamp,因为此时还没有提交(或者提交了还没来得及修改状态)。这个没有timestamp的key,对应的Value则是MVCCMetadata结构体,可以用于找到对应的Transaction Record

  • 读Write Intent

CRDB 中 write-intent 是写入到 mvcc 的,所以读事务会根据MVCCMetadata来判断当前的读事务与Write Intent是否是同一个事务。而如果遇到其他事务的的Write Intent则表明有W/R冲突(如果是写事务遇到了Write Intent则表明有W/W冲突),冲突的解决方案在Distributed Database System —— Snapshot Isolation 快照隔离的各种实现方案一节中说过,对于W/W冲突可能会根据优先级abort其中一个,对于W/R冲突则会选择推迟写事务。

一个事务碰到了一个write intent,那么说明有可能写write intent的事务还没有结束(因为write intent是异步清除的),这就说明有可能碰到了uncommitted的数据。这时,当前事务会去检查write intent所在的事务的状态,如果已经提交了,将write intent覆盖旧值然后清除write intent即可。如果已经回滚了,那么直接清除write intent就行。如果是Pending,正在运行呢?这个时候,就要看事务的优先级了,优先级低的事务需要abort,事务开始时赋予的优先级是random的。CockroachDB会保证被abort的事务在restart之后优先级会提高。

Phase2: Commit or Rollback

在 begin 之后执行完成各种 DML 语句后,最后应用会发起 commit 或 rollback(如果不发就挂了会有 TxnHeartbeat 来将事务 abort)。

在一个事务开始的时候,会往底层存储系统中写入一条记录,这个记录叫做Transaction Record,record会记录事务ID,事务状态,Pending(正在运行)还是Committed,还是Aborted,而write intent里存在key指向这个Transaction Record。提交事务,只需要把Transaction Record中的事务状态改成Committed即可,回滚事务改成Aborted即可。一旦事务状态修改成功,就可以返回给客户端,遗留的write intent会异步处理:commit时,将write intent的值覆盖原始值,删掉write intent,rollback时直接删掉write intent即可。

随后客户端过来读的时候,如果碰到了write intent(之前说了,write intent是异步删除),就会沿着write intent找到Transaction Record,看看事务的状态,如果状态是committed,返回write intent中的值,如果Abort就会返回原始的值。如果是Pending,说明这个事务还在正常跑,遇到了写写冲突。

你可能感兴趣的:(分布式架构)