简介
在《关于在分布式环境中RVN和使用场景的介绍2》和《关于在分布式环境中RVN和使用场景的介绍1》中我们介绍了RVN的概念和在一些具体用例中的使用。在本文中我们讨论一下在分布式环境中使用RVN需要注意的问题。
问题
我们在收到一条待处理的事件时,需要检查该事件的RVN是否已经太旧。如果该事件的RVN已经太旧,我们就不再处理该事件,并且丢弃该事件。通过这个逻辑,我们可以保障在分布式环境中正确的处理消息的乱序,丢失,和重复等问题。就这个逻辑本身而言是非常简单的。但是我们现在要考虑到分布式环境中所有的消息处理都是并行的,我们需要小心的检查这个逻辑的实现。在本文中,我们讨论一下如何在分布式环境中正确的处理这个逻辑。
解决方案
关于RVN更新的基本逻辑和它所能解决的问题,可以参看《关于在分布式环境中RVN和使用场景的介绍1》。下面是基于两种不同方法的RVN的实现方案。
基于锁的解决方案
在并行编程中,锁(lock)是很常见的工具。锁的功能是可以在多个并行逻辑都想访问同一个资源时进行同步。具体来说,我们可以认为锁提供了如下的接口:
Acquire(key)
Release(key)
当一个并行逻辑试图访问某个资源(该资源以“key”作为标识),它需要先lock该资源的key。假如没有其它的并行逻辑已经获得该资源的锁,则此并行逻辑可以获得该资源的锁,并且可以继续执行以使用该资源;否则此并行逻辑将被阻塞在该锁上,直到使用该资源的其它并行逻辑release了该锁。
在分布式环境中同样可以实现lock。比如AWS里的DynamoDB Lock Client。这是一个基于DynamoDB的表实现的锁,任何AWS里的计算资源(比如Lambda,ECS,EC2)都可以通过创建DynamoDB Lock Client以达到互相同步的目的。关于DynamoDB Lock Client的具体介绍可以参看这里。
下面是一个可以正常工作的使用DynamoDB Lock Client来维护RVN的逻辑流程:
在这里我们需要注意对于RVN的检查和RVN检查以后对于当前消息的处理都必须一直在持有锁的状态下执行。这是因为一旦锁已经释放,就有可能更新的RVN的消息开始被处理,从而和更新RVN和消息处理产生冲突。
基于锁的解决方案的缺点是需要额外的二次DynamoDB的操作,也就是对锁的获取和释放。当我们需要处理很多消息时,这样做的效率是比较低的,同时也会产生大量额外的费用。
基于DynamoDB的Condition Update的解决方案
另一种对RVN的维护方式可以使用DynamoDB的condition update。DynamoDB的condition update允许我们在update的请求里指定一些条件,只有当该条件满足时该update的请求才会被执行,否则就会被拒绝。关于DynamoDB的condition update可以参看这个文档。
利用condition update,我们可以使用RVN作为condition,要求被更新的数据或者不存在,或者RVN小于新的数据。该condition会被如下形式定义:
attribute_not_exists(#hashKey) OR (#versionNumber <= :versionNumber)
当我们使用condition update来解决冲突问题时,我们应该考虑将RVN和被存储的数据写在一个表中,从而可以使用condition update在更新数据时检查RVN是否是新的RVN。这个方案解决了需要额外访问DynamoDB的问题,但是这个方案的缺点也很明显,就是对于复杂的处理的逻辑灵活性很差。比如说我们考虑使用收到的消息更新两个表的数据。那么我们就必须在两个表中都存储RVN的信息。再比如说我们需要使用收到的两种消息更新同一个表。那么我们就必须在一个表中维护两种消息的RVN。当消息的处理逻辑比较复杂时,这种混乱的表定义无疑会增加开发和维护的难度。
问题的扩展
现在我们考虑如果我们的消息里有大量的消息是针对同一个key的,也就是说大量的消息之间需要同步执行。假如我们使用锁的方案,我们就会发现这些消息本质上是串行处理的,效率很低。在这里我们除了可以考虑使用《关于在分布式环境中RVN和使用场景的介绍2》中提到的方法来减少需要处理的消息数量外,还可以在获取锁之前先检查RVN是否有效来达到优化的目的。
我们具体来考虑如下的例子。我们收到RVN的顺序是:1,4,3, 2,5。假如我们按照普通的流程,我们需要串行处理这五条消息。其中每一条都需要两次DynamoDB的操作(也就是锁的获取和释放),其中RVN 1,4,5需要消息的处理,所以我们总共需要10次DynamoDB的操作,3次次消息处理,总共有5条消息被串行处理。但是如果我们在试图获取锁之前,先检查RVN是否已经太旧,我们就会在获取锁之前将RVN 3和2丢弃,所以我们只需要6次DynamoDB的操作,3次消息处理,总共有3条消息被串行处理。这样的流程图如下所示:
在这里我们有两点需要注意:
- 第一,即便我们有了这个防御机制,我们在获取锁之后对于仍然需要对RVN进行第二次检查。这是因为在我们获取锁的过程中,我们的RVN有可能已经变得不再有效。
- 第二,第一次对于RVN的检查仍然需要一次DynamoDB的读操作。如果该次检查RVN是有效的,则该次DynamoDB的读是浪费的。所以是否采用这种防御机制仍然需要根据具体情况决定,并不能保证一定是更好的方案。
通过对本文中问题的讨论,我们可以看到在分布式环境中编写程序是复杂和混乱的,当然带来的好处就是效率的提升,未来扩展的灵活,还有容错性增强等等。但是我们需要主要注意的是在分布式环境中给出一个简介同时高效的设计尤为重要,因为一旦在分布式环境中出现设计上的缺陷以及设计上的重构,都会比集中环境要复杂的多。我们会在以后的文章中对其它一些模式做进一步讨论。