本文将介绍一下谷歌在2010发表的论文《Large-scale Incremental Processing Using Distributed Transactions and Notifications》。
Percolator是由Google公司开发的、为大数据集群进行增量处理更新的系统,主要用于google网页搜索索引服务。使用基于Percolator的增量处理系统代替原有的批处理索引系统后,Google在处理同样数据量的文档时,将文档的平均搜索延时降低了50%。
Percolator是在Bigtable之上实现的,是为Bigtable定制的,它以Client library的方式实现。Percolator利用Bigtable的单行事务能力,仅仅依靠客户端侧的协议和一个全局的授时服务器TSO就实现了跨机器的多行事务。
用户指定的Perocolator中的Column在Bigtable中会映射到如下3个Column:
实际的数据由这三列共同决定,下面看一个例子:
如何查看Bob账户有多少钱?
先查询column write
获取最新时间戳的数据,获取到data@5
,然后查询column data
里面时间戳为5
的数据,即$10
,也就是说Bob账户目前有10美元。
一个事务的所有Write在提交之前都会先缓存在Client,然后在提交阶段一次性写入;Percolator 的事务提交是标准的两阶段提交,分为Prewrite和Commit。在每个Transaction开启时会从TSO获取timestamp作为start_ts,在Prewrite成功后Commit前从TSO获取timestamp作为 commit_ts。协议伪代码如下图:
先来看一下数据结构struct Write
包含
Row row
表示某一行Column col
表示某一列string value
表示实际写入的数据另外
vector<Write> writes_
用于在内存中缓存需要写入的数据int start_ts_
表示事务开始的timestamp
初始化Transaction
的时候获取了一下start_ts_
Set
只是简单的在内存中做个缓存,实际写入会在Commit
时触发
Get操作
timestamp
小于start_ts
的锁,如果有的话需要先Cleanup lock
commit timestamp
小于start_ts
的最新的数据Cleanup primary lock: 看该主锁是否已超时,如果超时直接把锁删除,否则不做任何操作
Cleanup secondary lock: 先看主锁的状态
prewrite阶段
commit timestamp
大于start_ts
的数据,如果已经有了说明已经有后续的事务提交了数据,这是本事务不能再往里面写了,直接return falsedata
和lock
字段commit阶段
primary
,其他的作为secondary
prewrite primary
secondary rows
进行循环调用prewrite secondary
commit_ts
primary row
上是否有锁,如果没有说明被已经超时被清理,直接退出primary row
写入write
列同时清除lock
列secondary rows
循环,写入write
列同时清除lock
列下面来看一个银行转账的案例,Bob向Joe转账7美元。
初始状态下Bob有10美元,Joe有2美元。
开始prewrte阶段,首先
timestamp=7
作为事务的start_ts
primary row
lock
列写入Primary Lock
data
列写入3
同样使用上一步获取的start_ts=7
9
写入到data
列lock
列指向上一步的Primary Lock
开始commit阶段,
commit_ts=8
lock
列的锁write
列写入提交时间戳指向的数据存储data@7
依次遍历secondary rows
write
column中写入数据时间戳lock
列的锁Percolator基于Bigtable的单行事务提供了多行事务的能力,核心思想包括:
primary row
是否commit
成功==== 20190617更新 ====
如果在prewrite的时候发现有lock,并且尝试resolve lock失败,说明有写写冲突,可以直接return false,让客户端进行重试。
举个读写冲突的例子:
start_ts = 10
start_ts = 50
当前timestamp = 60
,此时事务a还没commit,这时事务b去读取数据时发现了事务a留下的lock,
事务b需要读取commit_ts <= 50
的数据,但是事务a在timestamp=60
的时候还没发送commit操作,
照理说事务b不需要管事务a留下的lock,但是存在这么一种情况需要让事务b去等待事务a的锁:
start_ts=10
prewrite
commit_ts=40
start_ts=50
读取数据
commit
如果事务b不去等事务a的lock,事务b会溜掉commit_ts=40
的数据。
主要的原因是在分布式环境下获取commit_ts
和执行commit
操作是两个操作,中间可能会因为机器、网络等原因导致一定时间的间隔。
在大事务的场景下,读写冲突会非常严重,想象一下一个大事务需要写大量数据,持续几十分钟到几个小时,在这段时间内所有读取写冲突的数据都会返回失败,对于OLTP场景根本无法接受,但其实读取的数据是已经存在在数据库,导致冲突只是为了避免写的commit_ts
小于当前读的timestamp
的情况。
解决这个问题需要引入一个新的事务类型LargeTxn。
在LargeTxn 的事务类型下,读事务不再只是被动的等待事务的提交结果,而是去主动的影响事务的提交过程,在查看事务状态的同时更新事务的状态,把不确定的状态变成确定的状态,从而避免无谓的等待。
startTS + 1
CommitTS >= MinCommitTS
ResolveLargeTxnLockForRead的伪代码如下:
lock := loadLock(key)
if lock != nil {
if isExpired(lock.StartTS, lock.TTL) {
db.Rollback(key)
} else if readTS >= lock.MinCommitTS {
lock.MinCommitTS = readTS + 1
db.Put(lock)
}
return readOld
} else {
if db.isCommited(largeTxnTS) {
return readNew
}
// txn is rollbacked.
return readOld
}