微服务最佳实践:同一服务多实例的数据合并实现方案
场景说明:
我们部署了同一微服务的2个运行实例,两个实例分别从Kafka接收Event1、Event2两类事件。实例需要将自身完成的事件转换结果与另一实例的转换结果做合并处理,最终仅向下游发送1条通知。
现需明确该需求的最优实现路径,具体需要解决以下核心问题:
- 如何实现微服务的两个实例互相等待对方的处理结果;
- 如何将两个实例各自生成的转换结果合并为统一结果;
- 如何在发送通知前完成校验:若另一实例已经完成结果合并并发送了下游通知,则当前实例直接跳过发送逻辑,避免重复推送。
以下为场景逻辑示意图,可辅助理解:
最优实现方案(生产踩坑验证可用)
别搞两个实例之间直连RPC互相等结果那套,太脆了——一个实例重启、网络抖一下直接卡超时,线上排查问题能把人折磨死。最稳的思路是用共享存储做状态中转+原子操作做幂等控制,实例之间完全不用直接通信,靠公共组件协调状态,复杂度低还好排查。
前置约定
首先给两类事件绑定同一个全局唯一关联键:如果Event1和Event2属于同一个业务流程,就拿业务单号、请求ID这类全局唯一值作为关联key,后续所有状态存储、锁的粒度都和这个key绑定。
跨实例等待+结果存储实现
优先选Redis作为共享存储(性能最高,运维成本低,绝大多数团队都有现成的集群),用Hash结构存中间结果:
- Key命名规则:
event:merge:{关联key} - Hash内固定存两个字段:
event1_result、event2_result,分别对应两个实例处理完的转换结果,Key要设置合理的过期时间(比如按业务最大处理超时设30分钟,避免垃圾数据占内存)
每个实例的处理逻辑:
- 拿到自己负责的事件后,先完成本地转换逻辑
- 执行
HSET 上述Key 对应事件的字段名 自身转换结果,把自己的结果写入公共存储 - 检查这个Hash下是不是两个字段都存在:
- 嫌轮询费资源就开Redis键空间通知,Hash字段写满时会主动推送通知,没有空转开销
- 嫌配置通知麻烦可以直接跳过等待步骤:写完自己的结果就直接尝试抢发送锁,抢锁时顺便检查结果是否完整,不完整直接退出进程,等另一个实例写完结果自然会来处理后续流程,逻辑更简单
结果合并逻辑
当检测到两个事件的结果都已写入Hash后,直接从Hash中读取两份结果,按照业务约定的规则做字段拼接、格式转换,生成最终的下游通知内容即可。
幂等防重复发送实现
这步完全靠Redis原子命令实现,不需要额外组件:
- 单独建一个锁Key,命名规则:
event:send:lock:{关联key},同样设置合理过期时间 - 实例生成完最终通知后,执行原子命令
SET 锁Key 1 NX EX 过期时间,这个命令只有当Key不存在时才会写入成功- 如果返回写入成功:说明当前实例是第一个拿到发送资格的,直接调用下游接口发送通知,发送完成后可以给锁Key续更长的过期时间,或者打个已发送的永久标记,避免锁过期导致重复发送
- 如果返回写入失败:说明另一个实例已经拿到锁完成发送,当前实例直接退出逻辑即可,不用做任何额外操作
要是你们团队本来就深度使用Kafka生态,也可以直接用Kafka Streams的事件Join能力:把Event1和Event2拆成两个独立流,按业务关联Key做开窗Join,窗口时间设为业务允许的最大事件到达间隔,Join出来的结果直接往下游投递即可,连额外的Redis组件都不用搭。但前提是团队有人能hold住Kafka Streams的状态管理、故障恢复逻辑,不然出问题排查门槛比Redis方案高好几个量级,小团队不推荐上来就用。
内容的提问来源于stack exchange,提问作者Somjit

