需要将同步转换成异步的原因有很多。最常见的原因包括:
在微服务构架体系中,对于非关键用户可感知的主流程的任务,一般默认使用异步通信。
由于解开了对下游系统的依赖,意味着对于下游系统,在何时完成所请求的操作没有固定的要求。异步的系统,有时会也被称作为“无时系统”[1]。
下图描绘了调用方与服务提供方之间可能存在的几种不同的通信模式。请注意,这里用“异步”与“非阻塞”两个名字来区分后两种通信模式。
从交互流程上来讲,异步和非阻塞之间,最大的区别在于,非阻塞是主动轮询,通信双方是单向沟通,而异步需要反向通知,双方形成双向沟通。如下图所示:
双向沟通天然的问题就是存在循环依赖。所以一般会引入消息中间件来接触双向的循环依赖。
通常认为RPC是一种同步通信机制,但事实上,基于RPC也可以实现异步通信。这也受不少RPC框架的支持,如gRPC[2]。但是由于基于RPC的异步,还是无法解决服务器宕机时的稳定性问题。所以如果想进一步降低对服务的直接依赖,常常会基于消息组件构建异步通信机制。如下图所示:
一般而言,通信协议不应该影响上层行为的设计。但是异步通信是个特例。本质上,异步通信,是将一个单次的双向交互,变成了两次单向的通知。也即,原本的一个操作,变成了两个操作的协同。
试考虑以下功能实现:接收一个请求(或消息),完成支付,并更新状态。
当所有操作,都同步地,在一个方法里执行时,所有的操作,可以放在一个Transaction中。最终结果只有成功与失败。
当把支付操作变成一个异步操作之后,会变成如下图所示的两步操作:
你会发现,把状态变更成Payed这一步,无论放在第一步做还是第二步做,都会产生歧义,让人无法确定现在到底在哪一步上。于是就需要引入一个新的状态:这样每一个操作,都会独立地导致一个状态变更。
在这个过程中,因为用户对于第二步的发生是无感的,需要不断的查询才能知道第二步有没有做完。但是更合理的方式,是提供通向的能力。可以是回调,也可以是消息。
考虑到支付的这个动作的触发,可能会需要重试,同时也希望下游系统的可用性问题,不要影响到自身对外提供能力。常常还会使用Outbox或CDC(参考dual-writes)来完成支付操作的触发。于是会变成这样:
Michel Raynal, Fault-Tolerant Message-Passing Distributed Systems pp. 7 (2010) ↩︎