从EDA事件驱动到CQRS
顾名思义,CQRS即命令查询职责分离,将CUD操作和R查询操作分离,对于CUD操作仍然参考传统的领域模型建模思路来实现,但是在命令中增加了消息事件机制,实现CUD操作变更通过消息事件异步写入到数据库。
在CQRS中,查询方面,直接通过方法查询数据库,然后通过DTO将数据返回,这个方面的操作相对比较简单。而命令方面,是通过发送具体Command,接着由CommandBus来分发到具体的CommandHandle来进行处理,CommandHandle在进行处理时,并没有直接将对象的状态保存到外部持久化结构中,而仅仅是从领域对象中获得产生的一系列领域事件,并将这些事件保存到Event Store中,同时将事件发布到事件总线Event Bus进行下一步处理;接着Event Bus同样进行协调,将具体的事件交给具体的Event Handle进行处理,最后Event Handler把对象的状态保存到对应Query数据库中。
对于CQRS,最容易想到的还是在数据库层面做的读写分离模式,可以看到CQRS本身和数据库的读写分离模式可以更好的匹配,由于采用事件驱动和消息订阅模式,对于R读库我们可以更加容易对数据变更信息进行更新,达到读库数据的及时同步更新。同时读库既可以采用读写分离数据库,也可以采用类似Solr,Nosql等分布式,非结构化数据来实现弹性水平扩展能力。
在命令查询职责没有分离的时候,可以看到一方面是模型本身的扩展性受到影响,另外一方面是原有的领域模型本身偏重,而且Entity实体本身也通过完整的DTO对象进行传输,这样在一些特殊的只需要更新或查询个别字段的时候,整个模型仍然偏重。
通过命令查询职责的解耦,不仅仅是提升整个框架模型的扩展性,更加重要是将两类业务规则和实现彻底的解耦开,方便后续的功能开发和运维,特别是在整个业务场景和逻辑实现复杂的情况下,这种解耦会使整个开发架构更加清晰简单。
同时也可以看到有一Command命令都是采用异步事件的方式进行写入,因此不存在同步和长连接占用的问题,有利于提升整个平台在大并发下的整体响应性能。
当然,采用CQRS模式最大的一个问题点就是无法实现命令和查询两部分内容的强一致性保障,即很可能你界面上查询到的数据不是最新的持久化数据库里面的数据,这个本身和消息管道异步写入的实时性有关系。
其次在使用CQRS模式的时候,有一个重要假设就是,在事件和命令发出后,无特殊情况在事件接收方都必须要能够接收事件成功处理,否则就存在大量的异常错误消息的异步回写,反而增加系统的复杂度。举个简单例子来说:
当我们在电商平台购买一个商品的时候,只要订单提交成功,那么这个订单就一定能够生效,也一定有库存能够发运和配送,而不是在后续到了配送环节的时才发现没有库存而导致订单取消。如果这样的话就极大的降低了系统本身的易用性。
即在异步命令和事件发送场景,当命令发送成功时候,虽然我们没有及时接收到处理方的事件处理结果信息,但是我们默认是接收方能够成功处理事件。但是我们也看到在CQRS场景框架下,只要命令事件发出,我们并不需要等待任何反馈信息。
另外还有一种CQRS实现场景,即虽然在内部对Command命令处理的时候是基于事件机制,异步响应,但是客户在前端的操作是同步等待返回。在这种情况下我们就可以保持前端连接,但是是否后端的类似DB连接等。
在CQRS模型下,由于职责分离,可以看到我们通过事件和消息的订阅,可以实现多个读库的订阅,这些读库既可以是结构化数据库,也可以是非结构化数据库;既可以用来实现业务功能本身的查询读,也可以用来做海量数据本身的分布式全文检索。
对于CQRS框架的实施,不是简单的设计模式使用问题,更加重要的仍然是是否能够接受最终一致性要求,同时在该要求下将传统的同步请求下业务功能和逻辑处理机制转变为异步事件价值下的事件链驱动模式。要实现这种转变就必须能够拆分出独立,自治的命令和事件,同时确保这些事件在朝后端业务功能和逻辑模块发送的时候能够处理成功(即该做的校验必须提前做完)。
将同步接口调用转为本地消息缓存这个类似消息中间件的功能,举例来说我们设计了一个同步发送订单到ERP系统的接口,如果在同步实时调用这个接口服务的时候出现异常,那么我们可以首先将消息存储到本地,然后设置定时任务和重试机制,通过重试方式将消息发送到目标系统。
即对于业务功能来说不用关心实时是否发送成功,而由业务系统自身机制来完成消息发送的重试。
而要做到这点,在接口功能设计时候,最好要做到单据业务完整性校验接口和实际的数据发送接口分离,即先调用接口进行完整性校验,在校验没有问题后再进行消息发送。以确保最终发送的消息不会因为数据完整性的原因导致无法发送成功。
查询数据的本地化缓存或落地memcached是一套分布式的高速缓存系统,由LiveJournal的Brad Fitzpatrick开发,但被许多网站使用。这是一套开放源代码软件,以BSD license授权发布。
memcached的API使用三十二比特的循环冗余校验(CRC-32)计算键值后,将数据分散在不同的机器上。当表格满了以后,接下来新增的数据会以LRU机制替换掉。由于memcached通常只是当作缓存系统使用,所以使用memcached的应用程序在写回较慢的系统时(像是后端的数据库)需要额外的代码更新memcached内的数据。
对于实时查询类接口,将查询的基础数据进行本地化缓存,即如果在实时查询出现异常的时候,我们可以直接查询本地缓存的数据,减少对业务功能使用的影响。
比如查询供应商接口服务,如果主数据系统提供的接口出现异常,我们可以直接查询本地缓存的供应商数据。这种模式对于变更不频繁的数据基本都适应,同时本身也减少实时调用接口带来的性能损耗。
如果是接口服务注册在API网关或ESB服务总线上面,我们还可以考虑在ESB服务总线上启用缓存能力,即对于调用过的接口,在同样参数重复调用的时候能够通过缓存数据获取,这样即使在源端业务系统不可用的情况下,也不好影响到当前接口服务的成功调用。
可以适度考虑数据落地
在微服务架构里面,我们一直在强调一点,即数据实时需要实时访问,不进行底层数据库的数据集成和同步,这既满足了数据的高一致性,也满足了数据实时性的要求。
但是带来的问题就是强耦合,如果数据提供方出现异常,那么导致消费方业务功能也无法使用。
因为我们可以适量考虑数据落地方式的数据集成在整体微服务架构实施过程中,对于变化不频繁的数据适度落地到微服务模块本地。这样本身可以减少实时的业务接口服务调用,增加单个微服务模块的可用性和可靠性。
对于已经出现强耦合如何重构