重谈datastore的事务与实体组
2010 12 21 09:55 PM 1952次查看
分类:Google App Engine 标签:Google App Engine
由于发现很多人对事务和实体组还是很不了解,于是今天就重新谈谈这个话题。
其实有用过关系数据库的应该都知道为什么要用事务,它的目的主要是为了保证数据的一致性。
简单来说,如果你做了一个银行系统,用户A要转账给用户B 1000元钱,那么你的系统必须保证转账完成后,用户A少了1000元(外加手续费),而用户B则多了1000元存款。
说起来简单,可要完成它却并不简单,因为对数据库的操作是串行执行的:假如刚扣完用户A的钱,系统就死机、断电或数据库停止响应了,如果不做处理的话,那么这笔钱就消失了。
为了避免这种情况,数据库设计者就设计了一个事务的概念。当开始一个事务后,所有对数据库的写操作都并不写入实际的存储系统,而是写入一个日志;直到用户提交这个事务,才会完成写入,并真正生效。而在提交之前,你可以在任意时刻选择Rollback(回滚),撤销这个事务中所有对数据库的写操作;而如果在提交前失败了,这些写操作因为还没生效,所以你可以在系统恢复正常后,重新读取日志信息,选择是否继续执行这个事务;而如果在提交时失败了,你也可以选择是否继续完成写操作(Roll forward,前滚),还是撤销已经完成的写操作(Rollback)。
可以注意到,这里面还存在bug:如果用户A同时转账1000元给用户B和C,但是他只有1000多元钱,使用这种方式仍能完成扣款。为此,事务在执行过程中还需要进行加锁,由于A的存款是需要更改的,它会被加上一个写锁;而在第2个事务中,读取A的存款时必须加个读锁,由于已被加了写锁,因此必须等待第一个事务完成,这样就会发现存款已经不足1000元,而回滚这个事务。
因此要实现事务,不但要有日志记录,还要有锁。
但是加锁是个很低效的行为,直接导致了这条数据不能被并行访问,这也是使用事务会比不使用事务更慢的原因。
然而为了数据的一致性,这却是必须的,就好像多线程编程时,访问公用资源必须加锁一样。
在继续介绍datastore的事务之前,我先介绍一下实体组。
Datastore中的每个实体在创建时,都可以设置一个父实体(或它的key),一旦设置了,它就与它的父实体处于同一个实体组,且这种关系是不能更改的。由此可见,一个实体最多只能有一个父实体,但可以有多个子实体;而那些没有父实体的实体,则被称为根实体。
实体组关系并不需要根实体或父实体来维系,你可以删除根实体或其中一个父实体,这并不意味着剩余的实体就不属于这个实体组了。你甚至可以通过直接设置key,来创建一个没有父实体的子实体。但是一旦这些缺失的key被用于创建实体,它们仍会自动成为实体组中的根实体和父实体。
那么使用实体组有什么好处呢?一是确定了一种关系,于是可以查询一个实体的父实体和子实体;二是同一个实体组的实体可以在一个事务中进行更改。
那么datastore的事务是怎样实现的呢?它实际上也是要加锁的,只不过这个锁的实现比较特别。
举例来说,假设A、B和C同在一个实体组,当A准备转账给B和C时,会向datastore发送一个开始事务的信号,这时datastore就会记录这个实体组的版本号(假设是1)。接着在提交事务时,datastore会比较这个实体组的版本号是否更改了,由于并没有其他操作在第一个事务执行时更改了版本号,因此第一个事务可以正常提交;而第二个事务在提交时,发现版本号已经变成2了,与开始事务时不同,于是自动撤销了本次事务,并重新执行该事务,再次读取A的存款,发现不足1000,于是拒绝了这次交易,撤销该事务。
那么如果提交前或提交时失败怎么办?事实上提交的最后一步才是更改实体组的版本号,而只要版本号没变,那些中间状态都是无效的,不论任何时候,get、fetch、put和delete都只会访问当期有效的实体。
此外还有一点,假设用户A去开户,分别在2个窗口存了1000元,如果创建账户这个操作没有使用事务的话,用户可能就损失了1000元的存款。然而datastore也可以解决事务开始时,实体组还不存在的问题,方法就是事务开始时读取版本号,发现不存在,于是认为是0(或None),在保存时,就变成1了;而第2次开户时,发现版本号已经是1了,就会撤销这个事务。
Datastore的事务不但特别在加锁方式上,还特别在一个事务只能访问同一个实体组的实体上。
实际上看了上文就明白原因了:datastore必须维护每一个实体组的版本号。由于datastore是分布式的,它可以保证更改一个实体组的版本号是原子性的,但无法保证同时更改2个实体组的版本号是原子性的,这就会带来事务的不一致性。
那么对这2个实体组再维护一个共同的版本号能否解决这个问题呢?事实上这就等于让2个实体组本身的版本号无效了,因此实质上就是合并了2个实体组,只使用一个实体组而已。实际上,这就和解决2个实体不能在同一个事务中进行更改,而把它们放在一个实体组的道理是一样的。
那么把所有实体都放在一个实体组会怎样?
答案很简单,你会遇到大量的冲突。因为对任何实体的写操作,都会更改实体组的版本号,于是导致同时进行的其他事务全部失败并重新执行。如果你这样设计银行系统的话,所有用户的存取款都会受到其他用户的影响,这显然是不可接受的。
因此,如果要考虑到你的应用的扩展性,最好的方式是让实体组以用户来分组(例如把用户作为根实体),但这就导致转账这种操作无法使用事务了。
要解决这个问题,就必须使用分布式事务了。
最后再提醒一下,事务实际上是从第一次访问实体组中的实体开始的,因为不访问datastore的话,也就不知道该记录哪个实体组的版本号。
因此,如果你直接把实体作为参数传给事务执行函数的话,由于没有访问数据库,datastore并不会记录实体组版本,也就无法保证事务的正确性。
正确的方法是只传实体的key、id或name,然后在事务函数中获取实体,这样才能保证实体组版本被正确记录。
向下滚动可载入更多评论,或者点这里禁止自动加载。