实验指导书(翻译)Project 4: Transactions

项目4介绍了如何在TinyKV数据库中实现事务系统,解决并发写入问题。采用快照隔离(SI)和多版本并发控制(MVCC),确保事务的原子性和一致性。事务分为读写操作,通过两阶段提交协议(2PC)处理。客户端首先获取开始时间戳,然后进行预写操作,最后提交或回滚事务。预写涉及锁定key,主键选择,以及跨region的协调。如果事务与其它事务冲突,将回滚并重试。在MVCC中,每个key存储多个版本,根据时间戳确定有效值。
摘要由CSDN通过智能技术生成

Project 4: Transactions

在之前的项目中,你已经建立了一个 key/value 数据库,通过使用 Raft,该数据库在多个节点上是一致的。要做到真正的可扩展,数据库必须能够处理多个客户端。有了多个客户端,就有了一个问题:如果两个客户端试图 "同时 "写同一个 key,会发生什么?如果一个客户写完后又立即读取该 key,他们是否应该期望读取的值与写入的值相同?在 Project 4 中,你将通过在我们的数据库中建立一个事务系统来解决这些问题。

该事务系统将是客户端(TinySQL)和服务器(TinyKV)之间的协作协议。为了确保事务属性,这两个部分必须正确实现。我们将有一个完整的事务请求的API,独立于你在 Project 1 中实现的原始请求(事实上,如果一个客户同时使用原始和事务API,我们不能保证事务属性)。

事务使用快照隔离(SI)。这意味着在一个事务中,客户端将从数据库中读取数据,就像它在事务开始时被冻结一样(事务看到的是数据库的一致视图)。一个事务要么全部写入数据库,要么不写入(如果它与另一个事务冲突)。

为了提供SI,你需要改变数据在后备存储中的存储方式。你不是为每个 key 存储一个值,而是为一个 key 和一个时间(用一个时间戳表示)存储一个值。这被称为多版本并发控制(MVCC),因为一个值的多个不同版本被存储在每个 key 上。

你将在 Part A 实现 MVCC,在 Part B & Part C你将实现事务性API。

TinyKV中的事务

TinyKV的事务设计遵循 Percolator;它是一个两阶段提交协议(2PC)。

一个事务是一个读和写的组合。一个事务有一个开始时间戳,当一个事务被提交时,它有一个提交时间戳(必须大于开始时间戳)。整个事务从开始时间戳有效的 key 的版本中读取。在提交之后,所有的写入似乎都是在提交时间戳处写入的。任何要写入的 key 在开始时间戳和提交时间戳之间不得被其他事务写入,否则,整个事务被取消(这被称为写冲突)。

该协议开始时,客户端从 TinyScheduler 获得一个开始时间戳。然后,它在本地建立事务,从数据库中读取(使用包括开始时间戳的 KvGet 或 KvScan 请求,与 RawGet 或 RawScan 请求相反),但只在本地内存中记录写入。一旦事务被建立,客户端将选择一个 key 作为主键(注意,这与SQL主键无关)。客户端会向 TinyKV 发送KvPrewrite 消息。一个 KvPrewrite 消息包含了事务中的所有写内容。TinyKV 服务器将尝试锁定事务所需的所有key。如果锁定任何一个 key 失败,那么 TinyKV 就会向客户端回应该事务已经失败。客户端可以稍后重试该事务(即用不同的开始时间戳)。如果所有的 key 都被锁定,那么预写就会成功。每个锁都存储了事务的主键和一个生存时间(TTL)。

事实上,由于一个事务中的 key 可能在多个 region ,因此存储在不同的 Raft 组中,客户端将发送多个 KvPrewrite 请求,一个给每个 region 的领导者。每个预写只包含该 region 的修改内容。如果所有的预写都成功,那么客户端将为包含主键的 region 发送一个提交请求。提交请求将包含一个提交时间戳(客户端也从 TinyScheduler 获得),这是事务的写入被提交的时间,从而对其他事务可见。

如果任何预写失败,那么客户端就会通过向所有 region 发送 KvBatchRollback 请求来回滚该事务(以解锁该事务中的所有 key 并删除任何预写的值)。

在 TinyKV 中,TTL 检查不是自发进行的。为了启动超时检查,客户端在一个 KvCheckTxnStatus 请求中向 TinyKV 发送当前时间。该请求通过其主键和开始时间戳来识别事务。该锁可能丢失或已经提交;如果不是,TinyKV 将该锁的 TTL 与 KvCheckTxnStatus 请求中的时间戳相比较。如果锁已经超时,那么 TinyKV 就会回滚该锁。在任何情况下,TinyKV 都会响应锁的状态,这样客户端就可以通过发送KvResolveLock 请求来采取行动。当客户端由于另一个事务的锁而无法预写一个事务时,通常会检查事务状态。

如果主键提交成功,那么客户端将提交其他 region 的所有其他 key。这些请求应该总是成功的,因为通过对预写请求的积极响应,服务器承诺,如果它收到该事务的提交请求,那么它将会成功。一旦客户端得到了所有的预写响应,事务失败的唯一途径就是超时,在这种情况下,提交主键就会失败。一旦主键被提交,那么其他键就不能再超时了。

如果主键提交失败,那么客户端将通过发送 KvBatchRollback 请求来回滚该事务。

Part A

你在早期项目中实现的原始 API 直接将用户的 key 和 value 映射到存储在底层存储(Badger)中的 key 和 value 。由于 Badger 不知道分布式事务层,你必须在TinyKV 中处理事务,并将用户的 key 和 value 编码到底层存储中。这是用多版本并发控制(MVCC)实现的。在这个项目中,你将在 TinyKV 中实现 MVCC 层。

实现 MVCC 意味着使用一个简单的 key/value API来表示 事务API。TinyKV 不是为每个 key 存储一个值,而是为每个 key 存储每一个版本的值。例如,如果一个 key 的值是10,然后被设置为20,TinyKV 将存储这两个值(10和20)以及它们有效时的时间戳。

TinyKV使用三个列族(CF):default用来保存用户值,lock用来存储锁,write用来记录变化。使用 userkey 可以访问 lock CF;它存储一个序列化的 Lock 数据结构(在lock.go 中定义)。默认的 CF 使用 user key 和写入事务的开始时间戳进行访问;它只存储 user value。写入 CF 使用 user key 和写入事务的提交时间戳进行访问;它存储一个写入数据结构(在 write.go 中定义)。

user key 和时间戳被组合成一个编码的 key 。 key 的编码方式是编码后的 key 的升序首先是 user key(升序),然后是时间戳(降序)。这就保证了对编码后的 key 进行迭代时,会先给出最新的版本。编码和解码 key 的辅助函数在 transaction.go 中定义。

这个练习需要实现一个叫做 MvccTxn 的结构。在 PartB & PartC,你将使用 MvccTxn API 来实现事务性API。MvccTxn 提供了基于 user key 和锁、写和值的逻辑表示的读写操作。修改被收集在 MvccTxn 中,一旦一个命令的所有修改被收集,它们将被一次性写到底层数据库中。这就保证了命令的成功或失败都是原子性的。请注意,MVCC 事务与 TinySQL 事务是不同的。一个 MVCC 事务包含了对单个命令的修改,而不是一连串的命令。

MvccTxn 是在 transaction.go 中定义的。有一个 stub 实现,以及一些用于编码和解码 key 的辅助函数。测试在 transaction_test.go 中。在这个练习中,你应该实现MvccTxn 的每一个方法,以便所有测试都能通过。每个方法都记录了它的预期行为。

提示:

  • 一个 MvccTxn 应该知道它所代表的请求的起始时间戳。
  • 最具挑战性的实现方法可能是 GetValue 和 retrieving writes 的方法。你将需要使用 StorageReader 来遍历CF。牢记编码 key 的顺序,并记住当决定一个值是否有效时,取决于事务的提交时间戳,而不是开始时间戳。

Part B

在这一部分,你将使用 Part A 的 MvccTxn 来实现对 KvGet、KvPrewrite 和KvCommit 请求的处理。如上所述,KvGet 在提供的时间戳处从数据库中读取一个值。如果在 KvGet 请求的时候,要读取的 key 被另一个事务锁定,那么 TinyKV 应该返回一个错误。否则,TinyKV 必须搜索该 key 的版本以找到最新的、有效的值。

KvPrewrite 和 KvCommit 分两个阶段向数据库写值。这两个请求都对多个 key 进行操作,但是实现可以独立处理每个 key 。

KvPrewrite 是一个值被实际写入数据库的地方。一个 key 被锁定,一个 key 被存储。我们必须检查另一个事务没有锁定或写入同一个 key 。

KvCommit 并不改变数据库中的值,但它确实记录了该值被提交。如果 key 没有被锁定或被其他事务锁定,KvCommit 将失败。

你需要实现 server.go 中定义的 KvGet、KvPrewrite 和 KvCommit 方法。每个方法都接收一个请求对象并返回一个响应对象,你可以通过查看 kvrpcpb.proto 中的协议定义看到这些对象的内容(你应该不需要改变协议定义)。

TinyKV 可以同时处理多个请求,所以有可能出现局部竞争条件。例如,TinyKV 可能同时收到两个来自不同客户端的请求,其中一个提交了一个 key,而另一个回滚了同一个 key 。为了避免竞争条件,你可以锁定数据库中的任何 key。这个锁存器的工作原理很像一个每个 key 的 mutex。latches.go 定义了一个 Latches 对象,为其提供API。

提示:

  • 所有的命令都是一个事务的一部分。事务是由一个开始时间戳(又称开始版本)来识别的。
  • 任何请求都可能导致 region 错误,这些应该以相同的方式处理。

Part C

在这一部分,你将实现 KvScan、KvCheckTxnStatus、KvBatchRollback 和KvResolveLock。在高层次上,这与 Part B 类似 - - 使用 MvccTxn 实现 server.go 中的 gRPC 请求处理程序。

KvScan 相当于 RawScan 的事务性工作,它从数据库中读取许多值。但和 KvGet 一样,它是在一个时间点上进行的。由于 MVCC 的存在,KvScan 明显比 RawScan 复杂得多 - - 由于多个版本和 key 编码的存在,你不能依靠底层存储来迭代值。

KvCheckTxnStatus、KvBatchRollback 和 KvResolveLock 是由客户端在试图写入事务时遇到某种冲突时使用的。每一个都涉及到改变现有锁的状态。

KvCheckTxnStatus 检查超时,删除过期的锁并返回锁的状态。

KvBatchRollback 检查一个 key 是否被当前事务锁定,如果是,则删除该锁,删除任何值,并留下一个回滚指示器作为写入。

KvResolveLock 检查一批锁定的 key ,并将它们全部回滚或全部提交。

提示:

  • 对于扫描,你可能会发现实现你自己的扫描器(迭代器)抽象是有帮助的,它对逻辑值进行迭代,而不是对底层存储的原始值。
  • 在扫描时,一些错误可以记录在单个 key 上,不应该导致整个扫描的停止。对于其他命令,任何引起错误的单个 key 都应该导致整个操作的停止。
  • 由于 KvResolveLock 可以提交或回滚它的 key,你应该能够与KvBatchRollback 和 KvCommit 实现共享代码。
  • 一个时间戳包括一个物理和一个逻辑部分。物理部分大致是壁钟时间的单调版本。通常情况下,我们使用整个时间戳,例如在比较时间戳是否相等时。然而,当计算超时时,我们必须只使用时间戳的物理部分。要做到这一点,你可能会发现 transaction.go 中的 PhysicalTime 函数很有用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值