
在 WhatsApp 多账号管理场景下消息从网关进入业务系统后往往要经历账号切换、网络重连、失败重试等环节。只要其中一个环节出现抖动就可能出现同一条消息被投递两次或者消息到达顺序与发送顺序不一致的问题。这篇文章分享我们在实际落地过程中用到的一套去重与顺序保证方案思路不复杂但细节容易踩坑。核心结论消息去重靠全局唯一消息 ID 幂等写入顺序保证靠发送端单调序列号 接收端按序重组。两者都依赖一个轻量级的中央索引层来协调多个账号之间的状态。为什么多账号场景下更容易乱序和重复WhatsApp 多账号架构里一个业务账号通常对应一个独立的会话实例。当某个实例因为网络波动断开重连时常见的情况有网关已经收到消息回执但业务层因为超时触发了重发导致同一条消息被处理两次多个账号并行处理消息后发送的消息反而先进入消费队列客户端切换账号后本地缓存的状态没有同步造成重复拉取。这些问题在单账号场景下可以通过本地锁简单解决但到了多账号环境必须引入一个跨账号的协调机制。设计一去重层我们的去重策略分两层生成端去重每条消息在业务系统产生时就分配一个全局唯一msg_id规则为biz_type:account_id:client_seq:timestamp保证同一账号同一秒内不会重复。消费端幂等消息进入处理队列前先在 Redis 中检查msg_id是否已存在。存在则直接丢弃不存在才允许消费。importhashlibimporttimedefmake_msg_id(account_id:str,client_seq:int)-str:tsint(time.time())rawfwa:{account_id}:{client_seq}:{ts}returnhashlib.sha256(raw.encode()).hexdigest()[:24]classDedupLayer:def__init__(self,redis_client):self.rredis_client self.ttl7*24*3600# 保留 7 天defis_duplicate(self,msg_id:str)-bool:keyfmsg:dedup:{msg_id}existsself.r.set(key,1,nxTrue,exself.ttl)returnexistsisNone这里的关键点是 TTL 要覆盖业务上可能出现的最大重试窗口。我们设 7 天是因为 WhatsApp 消息回执偶尔会出现跨天延迟窗口太短会导致过期后重复消费。设计二顺序保证层去重解决的是“不要重复处理”顺序保证解决的是“按正确顺序处理”。我们在每个账号上维护一个单调递增的client_seq发送端严格按顺序分配。接收端用 Redis Sorted Set 暂存消息score 就是client_seq消费线程只消费当前期望 seq 之前连续到达的消息。classOrderBuffer:def__init__(self,redis_client,account_id:str):self.rredis_client self.account_idaccount_id self.seq_keyfmsg:seq:{account_id}self.buf_keyfmsg:buf:{account_id}defpush(self,msg_id:str,seq:int,payload:str):self.r.zadd(self.buf_key,{payload:seq})self.r.set(fmsg:id:{msg_id},payload,ex7*24*3600)defconsume(self,handler):expectedint(self.r.get(self.seq_key)or1)whileTrue:itemsself.r.zrange(self.buf_key,0,0,withscoresTrue)ifnotitems:breakpayload,seqitems[0]ifint(seq)!expected:breakhandler(payload)self.r.zrem(self.buf_key,payload)self.r.incr(self.seq_key)expected1这个 buffer 有两个好处乱序到达的消息不会提前消费而是等待前面缺失的 seq 补齐重连后只要 seq 连续就可以自动追回顺序。与账号路由结合在多账号管理工具中消息首先根据目标客户的归属关系路由到具体账号再由该账号的 seq 体系保证顺序。WADesk 的实践中账号路由层会先把消息写入账号独立的待发送队列然后由账号工作线程按 seq 顺序取出、调用网关。这样可以避免多个账号之间互相抢占顺序。对于已经发出的消息如果网关返回“发送中”但没有回执我们不会立即重发而是进入补偿队列等待一个可配置的“静默期”后再判断是否需要补发。补发时仍然使用原来的msg_id因此即使网关实际已经送达也不会造成重复。异常场景处理seq 跳号如果消费端发现期望 seq 长时间未到达会触发告警并进入人工排查流程。通常原因是发送端崩溃导致 seq 分配不连续需要通过持久化 seq 生成器来避免。Redis 故障去重和顺序层都依赖 Redis一旦 Redis 不可用系统会降级为只去重不保序优先避免重复消息影响客户体验。账号切换当客户被重新分配到另一个账号时新账号从初始 seq1 开始避免与旧账号 seq 冲突。落地时的三个注意点seq 生成器必须持久化如果发送端进程重启后从 1 开始重新计数接收端会把新消息当成旧消息丢弃。我们把client_seq写入了 RocksDB每次分配前先刷盘。去重 key 的粒度要合适按账号维度去重会漏掉跨账号重复按全局维度去重又可能误判。我们最终采用biz_type account_id client_seq三段组合既保证准确性又避免不同业务互相影响。顺序 buffer 需要兜底清理如果某条消息长期缺失buffer 会无限膨胀。我们给每条待排序消息加了 5 分钟超时超时未补齐则触发告警并跳过该 seq由人工确认后决定是否补发。可观测性建议建议至少监控以下指标重复消息拦截数seq 等待队列长度和等待时长补发次数与补发成功率各账号发送速率与失败率WADesk 内部会按账号维度输出这些指标帮助运营团队快速定位是网关抖动还是路由策略问题。写在最后消息去重与顺序保证并不是高并发场景才有的需求。只要你的系统涉及多账号、重试、网络波动就早晚会遇到这些问题。提前把msg_id和client_seq设计好比事后再补要省事得多。如果你也在做 WhatsApp 相关的多账号消息系统希望这套思路能帮你少踩几个坑。