He3DB目前依旧是以8K的数据页的形式在对象存储上保存数据,但可以保存同一个数据页的多个版本,以供不同的节点针对一些不要求强一致性读的请求提供服务。
在CRDB中也提供了低延迟的过期读的功能。在CRDB中,数据会根据一定的规则划分成不同的range。在每一个range上会有一个raft group用于保持range在跨域场景下的一致性。在raft共识协议下,存在两种主要角色,一是leaseholder,另一种是follower。Range的leaseholder是唯一允许提供最新读写的副本。Follower副本则可以在足够旧的MVCC快照上提供只读查询。这就是follower reads。Follower reads提供了两个好处,首先,它减少了因地理位置引起的读操作的延迟,因此客户端可以就近从一个follower中读取数据,而不用去更远的leaseholder上读取。其次,可以在不同副本之间平衡读取的流量。Follower可以提供显式请求的过期读取,也可以用于长时间运行的本地事务或全局事务。
CRDB是一个序列化的基于时间戳的MVCC系统。当一个非leaseholder的副本能够提供一个时间戳为T的读时要满足两个条件:
(1)在follower的MVCC的时间戳为T,在T之后不会再有向range中写且时间戳小于T的操作
(2)Follower拥有读操作所需的所有服务,即follower已经回放所有小于读操作的时间戳的raft日志。
CRDB事务在启动时被分配了一个读时间戳和一个写时间戳(即一个临时的提交时间戳)。读时间戳标识了事务将读取的MVCC快照。临时提交时间戳标识了事务写入值的MVCC时间戳,以控制那些读操作能够获取这些新写入的值。由于事务可以运行任意长的时间,因此临时提交时间戳可以由于过期而更新。除非事务因为冲突而被回滚,不冲突的事务可以按时间戳的顺序提交。
为了满足上述的条件(1),需要一种机制来防止因在T之后有一个时间戳小于T的写操作导致时间戳为T的读操作的失效。这个机制就是closed timestamps。Closed timestamps是有leaseholder给出的一个承诺,它将不接受小于等于closed timestamp的写入操作。这个承诺会被串行化到range的复制流中,并包装成一个raft命令。当follower回放一个带有closed timestamp T的命令时,就知道不会再有时间戳小于T的写的命令了,这时follower就可以启动小于时间戳T的follower read了。
每个range都维护一个closed timestamp,由range的leaseholder将三秒前的时间戳设置为closed timestamp。由于新的写操作的时间戳不能小于等于closed timestamp,长时间执行的读写事务(比如,如果有一个事务执行的时间足够长,以至于临时提交时间戳被某些range的leaseholder关闭了)会被强制增加其临时时间戳。当然也有例外情况,有些临时写操作的时间戳在range关闭timestamp前已经被评估过了,那么这个临时写的时间戳将不会受到range关闭时间戳的影响,尽管临时写的时间戳可能会小于关闭时间戳。在这种情况下,长期运行的事务提交的时候就不会事先验证之前的写入,这也意味着closed timestamp不会保证在更早的时间戳MVCC中存在修改。为了解决这个问题,当读的时间戳小于closed timestamp时,follower可能会加一个排它锁,当出现这种情况的时候,会被重定向到leaseholder来解决冲突,因此读取操作会阻塞。因此,follower提供读操作的条件就是读操作的时间戳要小于等于closed timestamp且小于将要在同一个key写入的时间戳。
为了具备从follower低延迟读的能力,需要将副本尽可能的发配到每个区域中。但是这样就会带来一个问题就是会牺牲共识的延迟,因为需要为写入投票的的副本变多了。为了解耦读写的延迟,crdb引入了不投票的副本,只接受raft日志,但是不为共识投票,因而不会影响写延迟。