到目前为止,我们都是直接使用AWS Lambda提供函数,以后端API的形式从客户端里调用出来。这种做法通常称为自定义事件。但是我们也可以让函数订阅并接收其他资源发出的事件,例如订阅文件的上传或数据库的记录变化等。
使用事件订阅,我们可以改变后端应用的内部行为模式。这些后端应用不仅仅接收和响应来自客户端应用程序的直接调用,也会对订阅的资源事件作出响应。我们大可不必单独开发一个总线式的工作流引擎来实现资源间的交互,而是通过资源的关系和响应的事件来触发对应的处理函数。例如,文件上传后,可以通过触发事件来执行相关函数,负责把文件有关信息写入数据库。
注意
这种方式简化了应用程序的设计和今后的架构演化。受益于微服务架构
给我们带来的价值,自下而上的软件模块编排相比自上而下的设计要容易得多。
在这种方式下,我们的后端服务成为了一个分布式的应用,因为它们不再被集中地管理和执行,因此我们应该采用分布式架构的一些最佳实践。例如,应该避免在多个资源之间进行同步的事务(transaction),因为分布式事务通常都难以开发、难以维护,性能往往也很糟糕。可行的做法是让数据流遵从最终一致性的原理,使每一个函数借助事件订阅的方式独立地工作。
定义
最终一致性是指我们不能期待分布式后端之间的所有交互和数据的读写都是同步完成的,但系统会保证这些数据在经过一些时间后会汇聚并成为最新的状态
。
事件驱动应用程序的定义是:在没有一个集中式工作流引擎协调处理的情况下,应用被设计为响应内部或外部的事件,并驱动后续处理逻辑。让我们通过一个实际的例子更好地理解这个概念。
假设我们在开发一个多媒体文件共享应用,用户可以通过他们的客户端(Web浏览器或者手机)上传图片等文件,公开分享这些图片,或仅与指定的好友分享。
为了实现这个功能,我们需要两种类型的数据存储库:
·一个文件存储库用来保存图片等多媒体内容。
·一个数据库用来保存用户账号(用户表)、用户之间的朋友关系(朋友关系表)和多媒体文件的元数据(内容表)。
我们需要实现如下的基本功能:
·允许用户上传包含元数据信息的图片,元数据是指这张图片被上传时用户指定的一些设置信息,如图片是公开还是只允许好友查看、图片的用户信息、图片拍摄的时间地点、图片是否包含标题,等等。
·在拥有权限的情况下,用户可以查看其他用户分享的图片。
·获取特定用户可以访问的图片清单,这个清单应该包括所有的公共图片,以及这个用户有权限访问的私密图片。
·更新内容的元数据,例如:用户在上传了一个仅供好友访问的私密图片后,他可以修改图片的元数据,把图片公开给所有人访问。
·获取图片的若干元数据信息,并在客户端连同图片的缩略图一并显示,例如:在缩略图下显示图片的作者、拍摄日期、地点和标题。
当然,一个真正的应用还需要更多的功能和职能,但为了简化起见,我们目前仅考虑上述列出的功能。我们在本书的第8章将会构建一个更加复杂的图片分享应用。
因为图片内容相对来说是固定不变的,如果能够事先完成计算(预处理),就可以提高效率。终端用户通常选择查看比较新的图片,并希望尽快看到内容。为新的内容创建一个预处理的索引是非常有好处的,图片的访问和渲染速度更快,后端所消耗的资源更少。如果用户查阅比较旧的已经超过了预处理索引范围的内容,我们仍旧可以动态地计算处理。但这种情况发生的概率比较低,并且可以被管理。预处理的索引必须在内容被更新时刷新,如果用户的好友关系发生了变化,也需要更新,因为图片的查看权限是基于好友关系的。
在图1-10中,我们可以看到这些功能的AWS Lambda函数,以及它们访问存储库的实现方式。
图1-10 由AWS Lambda函数实现的媒体共享应用的简单功能,目前仍旧缺少基本的后端功能实现
在目前情况下,所有来自客户端的交互都已经被实现,但是我们仍旧缺失一些基本的后端功能,例如:
·用户上传新文件后应该采取什么操作?
·如果用户修改了元数据,应该如何相应地调整索引?
·需要构建在客户端上显示预览之用的缩略图。
我们刚才讨论的那些后端功能与之前的前端功能列表有很大的不同之处,因为这些功能的触发依赖于存储库变化所引发的事件。我们可以把这些功能设计为几个额外的AWS Lambda函数,这些函数订阅存储库上的特定事件,例如:
·如果文件(图片)被上传或修改,执行构建缩略图的函数,并把缩略图保存回到文件库。
·如果文件(图片)被上传或修改,执行获取元数据的函数,把元数据保存到数据库中(内容表格)。
·当数据库有更新时,不论是用户表、关系表还是内容表,执行重新构建有关预处理索引的函数,对应用户所能查看的图片也会发生变化。
通过AWS Lambda函数和事件订阅来实现这些功能,开发者将获得一个高效的架构,无须开发和实现任何集中式工作流引擎,最终用户所作出的任何数据修改都可以由函数作出对应的处理。我们在图1-11中可以查看这些后端新功能,它们是订阅了事件的函数。
图1-11 由事件驱动的函数作为后端的简单媒体共享应用程序,函数订阅了包括文件共享和数据库发出的事件
以订阅了数据库事件的函数为例,不论是最终用户直接修改了数据库(如对图片的元数据做出修改),还是其他函数修改了数据库(如新的图片被上传,另一个AWS Lambda函数会对数据库做出修改),订阅了数据库事件的函数都将被调用。
我们不需要分别管理这两个用例,它们都是由同一个事件订阅来管理。订阅描述了资源之间的关系,以及事件发生时需要进行的操作(调用函数)。
在进行实际的应用开发时,你会发现有些Lambda函数可以被替换为对后端资源的直接调用。例如:我们可以直接编程把文件和对应的元数据写入文件共享,或者更新数据库。订阅了这些资源的Lambda函数会实现所需要的后端逻辑。
这个事件驱动的媒体共享应用就是一个简单但行之有效的例子。通过订阅事件,函数自动地串联在一起,例如:当图片和元数据被上传,或图片的标题被修改后,文件库发出的事件会触发调用第一个Lambda函数,进行数据库内的元数据更新操作。由于数据库发生了变化,数据库事件触发了第二个函数,进行预处理索引的调用。
注意
我所描述的这一串连锁反应有些像是Excel的功能:我们对一个单元格内的数据进行修改后,所有关联的单元格都会通过公式(如sum、average等)被自动重新计算。Excel是事件驱动应用的一个很好的例子,这是通向响应式编程
(Reactive Programming,简称RP)的第一步,接下来我们将逐步深入。
设想我们的媒体共享应用还有其他的一些新功能,例如:创建、更新或者删除用户,修改朋友关系(增加或删除好友),我们可以试想如何在图1-11所描述的架构基础上增加新的函数,在需要时,让这些函数订阅对应的后端资源事件,这样可以通过事件本身驱动应用的逻辑,而不是让函数实现全部的工作流。
例如:如果我们使用类似Amazon SNS这类的短信提醒服务,那么当内容被上传或更新后,如何选择最佳的方式向用户发出提醒通知?如何向图1-11添加新的资源、事件和函数呢?