分布式系统间的事务处理,一直是一个有意思的课题,这里有很多的文章去分析,这里笔者也有些思考,下面主要从消息队列、消息通道的角度说下自己的思考:
本着由易至难、由抽象到具体的原则,首先定义下一个简单业务场景:
假定有一个平台,里面有微服务A,微服务B,两个服务均提供外部接口,一个负责更新资源,一个负责提供资源服务,可以看到,由于两个服务存在关联(比如,A更新资源后,需要通知B拉取最新的资源),所以需要一定的内部通信机制,这是一个比较常见的平台服务场景,如图:
当然这里可以再复杂一下,把分布式的因素加进来,为了方便举例说明,这里只将微服务B变为分布式的微服务,如图:
第一个要解决的问题:选择什么样的内部通信机制:
内部通信通道有很多个选择,比如HTTP、RPC、Redis、Kafka(类似的数据队列)、数据库等
其中,同步的有:HTTP、RPC、Redis,异步的为:Kafka(类似的数据队列)、数据库,由于系统希望能够实时提供最新的资源服务,故选择同步通信机制
从服务间通信方式上,一般使用RPC比较多,特别是RPC支持跨语言的通信后,迅速成为业界的新宠,也是比较成熟的方案
但由于微服务A和微服务B都有外部服务,所以服务的部分接口是对外开放的,而使用HTTP和RPC服务,会增加内部服务对外化的风险(事实上,在复杂环境部署上,这样的金手指的案例很多),所以这里并不一定是第一选择
再看下这个场景,微服务A资源的更新了,只需要告知微服务B一声“要更新了”即可,而不是将最新的资源直接传过去,是比较简单的提醒通信,这样的通信比较轻量,即使在高并发下,也不太涉及边界数据竞争情况处理。那自然而然就会想到:Redis
场景就变成了这样的:
第二个要解决的问题:如何使用Redis做通信
redis作为通信方式有很多中,在分布式系统下最经典的场景就是分布式锁,即各个分布式的服务抢redis的唯一“锁key”赋值:
这种,基本上把redis作为外延内存来使用,把“锁”作为几个服务的“全局变量”,适用于同服务内部的简单通信,故不适用于这里。
另外一种是创建一个list,通过push和pop的方式获取内容:
似乎可以,但这里存在一个严重的问题,即不支持重复消费:消费者拉取消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费,即不支持多个消费者消费同一批数据,在分布式系统下无法满足,也不能选择这里
还有一种,则是比较经典的发布—订阅者模式
它正好可以解决前面提到的第一个问题:重复消费,即多组生产者、消费者的场景
所以场景再次更新:
当然,看到这里,可能会有疑问,直接将最新的资源从微服务A给到微服务B可以吗?
答案是否定的,因为Pub/Sub 最大问题是:丢数据。
特别是在以下三个场景中:消费者下线、Redis 宕机、消息堆积
这样就会对服务的稳定性造成比较大的挑战,所以还是需要持久化存储,同时要做资源更新的兜底任务:
第三个要解决的问题:收到信息后应该怎么做
从这个场景下,可能会觉得,收到信息就去更新资源就好了,其实这里是有坑的
刚才我们模拟的是微服务B是分布式服务,事实上,微服务A也可能是分布式服务,场景变成:
同时,这个通信机制是服务之间的约定,作为接收方,要天然不信任信息的频次和有效性,对应下来就是三种不信任:
1.信息发送的频次
2.信息的重复性
3.信息的丢失性
针对第三种,不仅仅是发送方的问题,也可能是redis的问题,所以需要一个定时拉取资源的兜底方案
针对第一种,拉去最新资源可能是一个耗时耗资源的操作,所以不能频繁拉去
针对第二种,重复的拉去也会导致资源消耗的浪费
所以针对第二种和第一种,就需要对接收的数据,进行防抖和去重,这里可以根据业务的需要进行整合,比较经典的方案是:
加一个定时器,判断即X秒内,微服务B收到需要更新范围Y的资源,这里就可以一定程度内避免了前两种情况
第四个要解决的问题:收到信息后的事件处理与服务内部事件处理资源抢夺怎么办
上图可以看到,通过数据整合和去重,可以降低事件处理的频次,降低资源的消耗
但在微服务B,可能不只一个事件通信,可能多个事件通信都用到了事件处理X(或者用到事件处理X的部分资源),也可能服务本身也会用到事件处理X(或者用到事件处理X的部分资源),由于收到信息的时间不可控,所以这里的事件处理X的资源使用时间也不可知,可能会造成资源抢夺,如下图:
这里,有两种解决方案:
一种是在公共资源和公共逻辑加锁,但这样增加了逻辑的复杂度,如果涉及读写锁等概念则会更加复杂
还有一种是将多个事件处理放入队列,这样也可以避免问题的发生
建议选后者,即:
最后,事实上,这里还可以更加复杂场景,我这里也是抛砖引玉额,如果有想继续的同学,欢迎交流