幂等:IM webhook 重试时,怎么保证不重复处理
接入 IM 机器人后,你几乎一定会遇到这个问题:同一个事件,被推了两次。 渠道为了保证”别丢消息”,往往选择至少一次投递——也就是说,你的 webhook 可能因为超时、网络抖动、渠道侧重试,而收到同一个事件的重复回调。
这时候如果你不做处理,就会重复回复、重复建会话、重复触发动作,用户收到两遍消息,或者后台跑了两次同一件事。这篇文章讲我把”去重”落在哪。
先接受一个前提:默认至少一次
别指望渠道”只推一次”。”至少一次”是很多消息系统的默认,你要做的不是祈祷不重,而是让重复变得无害。
所以第一件事是:把”可能重复”当成常态来设计。读回调的代码,默认就是”这可能是我见过的,也可能是新的”。
用什么做幂等键
去重的核心是幂等键——每一条消息/事件,都用一个稳定、唯一的标识来代表”这是我”。常见来源:
- 渠道给的事件/消息 ID:比如
message_id、event_id、msgId。这是最可靠的,因为它代表”渠道眼里这一条是什么”; - 会话 + 时间戳:如果渠道不给稳定 ID,只能用(会话标识, 时间)去凑,但这样会有撞键风险;
- 自己生成:如果上面都没有,就得用回调里的内容去算一个指纹,但要小心不同渠道的内容结构不一样。
关键是用渠道自己给的 ID,别自己发明。因为渠道重试时,会带着同一个 ID 回来,你才能认出”这条我见过”。
去重放哪里:先查再写,还是写时判重
有两种做法:
- 先查再写:收到回调,查一下这个 ID 处理过没有,处理过就跳过。简单,但有两个坑——并发下两个人同时查到”没处理过”,都会执行;以及”查和写”不是原子的。
- 写时判重(用唯一约束):在存储层把一个字段设为唯一,处理前先写入这个 ID;如果插入冲突,说明已经处理过了,直接跳过。这个更稳,因为唯一约束在数据库层面保证了原子性。
我更推荐第 2 种:用一个唯一约束兜底,而不是只靠”先查”。单机下可以用内存 set,但要考虑进程重启会丢;有一个持久化、带唯一约束的地方(比如 SQLite 的唯一索引),才真正靠得住。
别忘了去重之外的两个点
- 顺序:重试不一定是按顺序来的。如果业务对顺序敏感(比如”先建会话再发消息”),要去重之外还要处理乱序,否则可能”先处理了后面的”。
- 可观测:你要能看到”到底处理了多少条、重复了多少条、跳过了多少条”。因为如果渠道疯狂重试,说明你的处理有 bug,或者超时了。重复率陡增,往往是你处理慢或坏掉的信号。
最后几条
- 默认”至少一次投递”,把重复当常态设计。
- 用渠道给的稳定 ID 做幂等键,别自己发明。
- 用唯一约束 + 写时判重兜底,别只靠”先查再写”。
- 关注顺序和可观测:重复率上升 = 有 bug 或慢了。
- 目标不是”绝对不重”,而是“重了也没事”。
把幂等做好,你就能坦然面对渠道的每一次重试,而不用提心吊胆。