微服务无计数器并行计算:基于Google PubSub的任务完成判定方案
基于Google PubSub的并行子任务完成判定解决方案
背景
这套基于微服务架构的分布式系统可并行处理队列消息,包含两类应用:
A:REST API应用,负责接收各类处理请求。每个请求会被拆解为可独立支撑完整请求逻辑的子任务列表,A接收到新请求时,会向B应用的消息队列插入N条消息,N为动态值,随不同输入请求的逻辑复杂度变化。B:队列处理应用,负责消费子任务消息完成业务逻辑。
当同一请求对应的所有N条子任务均处理成功后,系统需向发起请求的用户告知整体处理结果。
原有初步方案
单个请求对应N个待处理子任务时,A会创建一个初始值为N的原子计数器,B每处理完一条队列消息,就将计数器值减1并读取最新值。最后一条消息处理完成后计数器值会归零,检测到计数器值为0时,由B向用户发送整体处理结果通知。
现存核心问题
- 必须依赖原子计数器同步全局处理进度,缺少该组件则无法准确判定任务的实际完成时点
- 包括Google PubSub在内的诸多分布式队列系统采用At Least Once投递策略,即同一条消息可能被多次取出重复处理,会导致尚未完成全部队列消息处理时,就提前向用户发送处理完成通知
正确落地方案
方案核心围绕「去原子计数器依赖」「幂等过滤重复消费」两个目标设计,完全适配Google PubSub的投递特性,具体实现逻辑如下:
1. 全局唯一标识绑定
给每个原始请求生成全局唯一的request_id,同时为同一请求下的所有子任务分配不重复的sub_task_id(可直接用1~N的顺序编号)。A在投递子任务消息时,必须将request_id、sub_task_id以及该请求的总子任务数N作为消息元数据,随子任务一起发送给B。
同时A可以提前在普通存储(KV/关系型数据库均可)中写入一条request_id的元数据,记录总子任务数N、当前状态为待处理,不需要初始化任何计数变量。
2. 子任务幂等处理
B消费到消息后,先做幂等校验:
- 先查询当前
(request_id, sub_task_id)是否已经标记为处理成功,可通过以下任意一种低成本存储实现:- 用Set结构存储:key为
request_id,value为所有已完成的sub_task_id集合 - 用关系型数据库表存储:字段包含
request_id、sub_task_id,二者加联合唯一索引
- 用Set结构存储:key为
- 如果已标记为成功,直接对消息执行ack(确认消费),不触发任何后续逻辑,完全过滤重复投递的影响
- 如果未标记为成功,先执行业务逻辑,处理成功后:
- 若是Set结构,执行
SADD命令将sub_task_id加入对应request_id的集合,因为Set的成员唯一性,重复执行也不会改变集合大小 - 若是关系型数据库,直接插入该条记录,唯一索引会自动拦截重复插入的请求
- 若是Set结构,执行
3. 完成判定与通知
只有当子任务处理成功、且完成状态写入成功(即SADD返回1/数据库插入成功,说明该子任务是首次处理成功)时,才触发完成判定逻辑:
- 查询当前
request_id对应的总子任务数N - 统计该
request_id下已完成的子任务数量(Set结构直接用SCARD取集合大小,数据库直接查询符合request_id的记录数) - 如果已完成数量等于N,说明所有子任务均处理完成,直接触发通知逻辑告知用户结果,同时将该
request_id的状态标记为已完成,避免重复通知 - 如果不等,直接结束流程,无额外操作
方案优势
- 无强原子计数器依赖:不需要依赖专用的原子计数组件,普通的KV、关系型数据库即可支撑逻辑,存储选型灵活
- 天然适配At Least Once投递策略:Set的唯一性、数据库的唯一索引自动过滤重复消费的影响,不会出现重复处理导致的计数不准问题
- 性能开销极低:所有操作都是O(1)或单条记录查询的级别,不会因为子任务数量多产生额外性能压力
内容的提问来源于stack exchange,提问作者No1Lives4Ever
相关产品推荐
相关产品推荐

