重复消费不可怕,真正让人害怕的反而是幂等性

现在咱们来聊聊消息队列里面那些让人头疼的事,其实问题不在 MQ 身上,大多是咱们自己没做好功课才出的错。咱们拿 Kafka 举个例子,每条消息在存硬盘的时候都会有个唯一的 offset,消费者处理完消息,会把这个 offset 提交到 Zookeeper 或者 Kafka 内部的一个专门主题。但要是碰到极端情况,比如直接用 kill -9 把进程干掉了,刚才还没提交的那些 offset 就全没了。这时候消费者以为自己还在原来的位置接着吃,结果就把同一批消息又吞了一遍。要是你的业务逻辑没做去重,数据库里肯定会多出一条一模一样的记录。 那为啥重复消费不可怕,真正让人害怕的反而是幂等性呢?再给你看个例子:假设有三条数据 1、2、3 按顺序进了 Kafka,它们的 offset 分别是 152、153、154。消费者刚把 offset 是 153 的数据读完了还没来得及提交就被重启了。重启后 Kafka 会把 152 和 153 的数据再推送给消费者一次。要是你的程序没有做幂等检查,数据库里肯定会多出来两条一模一样的数据。 幂等性说白了就是“来多少次都只算一次”,不管同一条数据来多少次,结果都得一样。如果做不到这点,重复消费顶多是个小麻烦,数据出错才是真正的大问题。 接下来教你四招保证幂等性的办法: 第一种是给数据库加个唯一键。不管是建索引还是加约束都行。你往表里插重复数据的时候数据库会直接报错回滚事务。消费者只要把这个异常抓一下,就能知道这数据之前已经处理过了,直接跳过就行。 第二种是用 Redis 的 SET 命令自带的原子性。消息里带个唯一的 ID,比如订单号或者流水号。消费的时候先用 SET key value NX PX 30 去设置值。这个命令只有在 key 不存在的时候才会成功,存在就直接跳过。30 秒之后这个 key 会自动过期失效,这样既不会漏掉有效消息也不会因为缓存永远占着内存导致问题。 第三种是在生产者那边就把全局 ID 塞到消息里。这种方法适合那些没法直接用数据库或者 Redis 来判断的情况。生产者给每条消息都塞个全局唯一的 ID,消费者收到消息后先去 Redis 或者数据库里查一下有没有这个 ID。如果有的话直接扔掉不处理;要是没有就正常处理然后记录状态。这样哪怕消息被重复发过来也能一眼认出来并安全忽略掉。 第四种是业务代码层加锁。这招是最后的保险。当上面三种方法都不管用时(比如业务逻辑涉及多个表的联合更新),可以在数据库层面给行加锁或者给表加锁。虽然等锁的时间可能会长一点能换来数据一致;再配合消息中间件的延时重试机制就能既保证最终的一致性又能避免因为无限等待导致死锁。 最后咱们来理个流程:先把你的场景定下来——到底是往数据库里插数据、改 Redis 还是做其他的业务逻辑;然后选一个合适的技术路线——是用唯一键、还是用 Redis 的 SET 命令、还是加全局 ID、或者直接给数据库上锁;最后一定要落地验证——可以先灰度放一部分消息出来测一下、专门压测重复的消息、再看看异常抛出的情况。把这三步走完了重复消费就不再是洪水猛兽了。