在GAE上实现分布式事务

标签:Google App Engine

这几天在Google I/O视频里注意到distributed transaction(分布式事务)这个词,也叫做global transaction(全局事务),与之对应的是local transaction(局部事务)。也许是我听漏了什么吧,一直没弄清什么是DT,直到看了Nick这篇《Distributed Transactions on App Engine》才搞懂。

实际上这源于我的一个疑问。
视频里解释事务时一直用银行做例子,当Alice要转账给Bob时,为了保证事务性,他们得在一个实体组,而这的确能实现。
而如果我有几百万客户,这些客户都可能彼此进行交易,那么我必须让他们同处于一个实体组才能保证事务性。
于是这就发生问题了,对实体组的操作是串行化的,就算是几个无关的客户发生交易,也不能并行执行。而磁盘寻道时间是10ms,一个事务至少1次读和3次写操作,其中读操作可以缓存,那么一秒内就最多完成33次事务了。这对于几百万客户而言,严重受到了扩展性限制。而且如此大的实体组会导致严重的冲突率,这也是很麻烦的。

而实际上我考虑的一直是局部事务,想在数据库层完成这个事务。如果将它分离成多个事务,在应用层依次执行也是可以达到目的的,这就是分布式事务。
具体实现如下:
首先定义一个账户模型,它存储账户的金额:
class Account(db.Model):
  owner = db.UserProperty(required=True)
  balance = db.IntegerProperty(required=True, default=0)
接着定义一个转账模型,它存储转账的金额、目标用户、对应的转账实体和转账时间:
class Transfer(db.Model):
  amount = db.IntegerProperty(required=True)
  target = db.ReferenceProperty(Account, required=True)
  other = db.SelfReferenceProperty()
  timestamp = db.DateTimeProperty(required=True, auto_now_add=True)
当进行转账时,分为如下3步:
  1. 从源账户扣除转账金额,然后将源账户实体作为转账实体的父实体,目标用户自然是目标账户实体,对应的转账实体为None,尝试完成这个事务。
  2. 如果这个事务正常完成,将返回一个transfer转账实体。再创建或获取一个dest_transfer转账实体,它的父实体为目标账户实体,key_name是transfer的key,目标用户是transfer的父实体,对应的转账实体则是transfer,尝试完成这个事务。
  3. 若这个事务也正常,将transfer实体对应的转账实体设置为dest_transfer,整个DT完成。这一步可以不放在事务中执行,因为其他进程不会对transfer进行写操作。
当然,第2步仍可能遇到突发故障,例如超时、配额用完等,这样事务就不完整了。
最简单的办法就是把第2步放在任务队列中,而失败的任务会被GAE不断重试直到成功。

不过任务队列连续失败后就会降低重试的频率,所以还有一种办法来找出未完成的转账操作:
用一个计划任务定期查询转账时间是30秒前(即转账进程已结束)且对应的转账实体为None(即没有完成第3步)的转账实体,对其中的每个实体进行第2步的操作。如果第2步已完成,则直接返回获取的dest_transfer,无需创建,接着进行第3步。

其中的代码我就不转了,这里只介绍下思路,需要的可以去看原文。
从这个例子也能看出,LT能实现强一致性,而DT则是最终一致(Eventual Consistency),但不管怎样,数据是没有丢失的,也是可靠的。不过实现时还得考虑一下转账顺序,如果先给目标用户加钱的话,那么就很难保证最终一致了。

2010年12月21日更新:
为了避免执行第一个事务成功后,在添加任务时失败而导致无法继续执行后续事务的情况,GAE又增加了一个Tasks within Transactions
当任务的transactional参数为True时,就可以把它放在事务函数中添加,它只有在事务提交成功后才会被执行,且保证会执行。
要注意的是一个事务最多只能添加5个transactional task,且不能用用户指定的任务名(注意有些任务会莫名其妙地被执行2次,由于不能指定任务名,因此你必须保证这些任务被执行2次也不会引起副作用)。

3条评论 你不来一发么↓ 顺序排列 倒序排列

    向下滚动可载入更多评论,或者点这里禁止自动加载

    想说点什么呢?