消息中间件的使用场景

在微服务架构中,基于消息中间件的交互方式可以解决同步请求/响应模式中服务高度耦合、服务交互灵活性脆弱、交互失败导致服务不可用等问题。

假设我们现在有两个微服务:权限管理服务和人力资源管理服务。下面使用不同集成方式将人力资源服务的数据库变动同步到权限管理服务中。

● 基于请求/响应模式的方案,权限管理服务提供REST方式调用人力资源管理服务提供的API。这种方式的问题是它需要二者必须同时在线,如果一方因为异常宕机,很容易导致数据丢失。另外,当人力资源数据信息很少变化时,权限管理服务每次同步数据都需要全量从人力资源服务中获得数据,效率比较低。所以,请求/响应模式对于服务之间的增量数据同步的场景并不友好,虽然也可以实现,但是这种做法增加了代码的逻辑复杂性。

● 基于共享存储的方案,人力资源管理服务可以将所有人力信息缓存到共享存储(如Redis)中,这样权限管理服务可以不用访问人力资源管理服务,而是访问Redis获得同步信息。这种模式的问题是,两个独立自治的服务会产生系统的紧耦合问题,人力资源管理服务对Redis的结构修改会对权限管理服务产生级联影响。另外从微服务数据自治的角度看,共享存储是不独立的,也是不易于扩展的。

● 另外一种方式就是采用消息传递的机制,在人力资源管理服务和权限管理服务之间建立一个消息队列,人力信息的变化通过事件的方式发送出去,人力资源管理服务只需要监听队列的变化,当监听到有新的事件后,就会“消费”这一事件,更新权限管理服务自己的缓存。这一模式可以带来诸多好处,概括为下面几点。

○ 系统解耦

降低工程间的强依赖程度,针对异构系统进行适配。在项目启动之初预测项目将来会碰到什么需求是极其困难的,通过消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口,当应用发生变化时,可以独立地扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束,提供服务的可用性,如下图所示。

我们可以想象一下,假设有上百个系统都需要系统A的核心数据,一旦有系统加入,系统A就需要修改代码,将数据发送到新加入的系统。反之,如果有系统不再需要系统A发送数据,那么系统A又得修改代码不再向其发送数据。这样的架构设计耦合度太高了,我们可以引入消息中间件来实现系统之间的解耦。即核心系统A生产核心数据,然后将核心数据发送到消息中间件,下游消费系统根据自身的需求从消息中间件里获取消息进行消费,当不再需要数据时不获取消息即可,这样系统之间耦合度就大大降低了。改变后的系统架构如下图所示。

○ 异步调用

有些业务不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。可以将消息队列作为缓存,然后在需要的时候再去处理它们,这样就增加了系统交互的灵活性。

假设有一个系统调用为:系统A调用系统B耗时20ms,系统B调用系统C耗时20ms,而系统C调用系统D需要2s,这样下来整个调用链路需要耗时2040ms。但实际上系统A调用系统B、系统B调用系统C只需要40ms,而系统D的引入直接导致系统性能下降。此时我们应该考虑将系统D的调用抽离出来,做一个异步调用,如下图所示。

如果使用消息队列进行优化,系统A到系统B再到系统C就直接结束了,然后系统C再将消息发送到消息中间件,系统D从消息中间件里获取消息进行消费,这样系统的性能就提高了接近50倍,如下图所示。

○ 削峰填谷

假设有一个系统,正常时间每秒也就有几百个请求,部署在一个8核16GB的机器上,运行起来非常轻松。然而突然由于一个大促活动,高峰期请求数达到了几千,出现了瞬时流量高峰,此时最容易想到的是加机器,部署超过10台机器,也能扛住此时的高并发,如下图所示。

另外一种方式就是我们可以考虑引入消息中间件,进行流量削峰。一旦流量高峰到来,大量消息会堆积在消息队列里,机器只需要按照自己的最大负荷从消息队列里消费,等流量高峰过了,慢慢地队列里的消息也消费完毕了,这样就达到了削峰填谷的目的,如下图所示。

○ 持久化、冗余

有些情况下,处理数据的过程会失败。除非数据被持久化,否则将丢失。消息队列可以把数据进行持久化,直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的“插入-获取-删除”范式中,在把一个消息从队列中删除之前,需要处理系统明确地指出该消息已经被处理完毕,从而确保数据被安全地保存,直到使用完毕。