Kafka重复消费怎么防
Kafka默认是At-least-Once语义,生产者发消息没收到ACK会重试,Broker上就可能存了两条一样的消息。解法就是开启生产者幂等性--把enable.idempotence设为true,Kafka通过ProducerId和SequenceNumber在单分区内自动去重。
单分区自动去重是不够的,如果跨分区的话。
跨分区或者生产者启动后PID变了,幂等性就兜不住了。这时候上Kafka事务--设置一个全局唯一的transational_id绑定PID,就算生产者重启也能保证Exactly-Once语义。但这主要解决Kafka内部流转的精确一次,消费端到业务库这条链路还得靠业务幂等。
业务幂等怎么处理
三层兜底。
第一层数据库唯一索引--订单号做唯一索引约束,重复插入直接报错忽略,最简单最可靠;
第二Redis去重--消费前用SETNX写消息ID,存在就跳过,适合高吞吐但得处理缓存过期问题;
第三层就是状态机--业务表记录处理状态,已完成的直接跳过。关键业务三层都上,别省。
总结:第一道防线:生产者端防重复发送
生产者发消息到Broker,Broker收到了,但是ACK因为网络抖动没回来。生产者误以为失败就重试,结果Broker上存在两条一样的消息。
解法:开启生产者幂等性,把enable.idempotence设为true.这是Kafa 0.11版本引入的机制---给每个生产者分配一个ProducerId,每条消息带一个递增的SequenceNumber,Broker收到重复的PID加序列号直接丢弃。
但是有个坑,幂等性只能保证单分区内驱重。跨分区或者生产者重启后PID就变了,就不灵了。需要跨分区原子写入的,得上Kafka事务,配一个全局唯一的transactional.id.
打个比方:幂等性就像你给快递贴了唯一条码,同一个条码的包裹快递站只收一次。但如果你换了快递站---分区变了,或者重新贴了条码--PID变了,就兜不住了。
第二条防线:消费端减少重复窗口。
默认情况下Kafa消费者是自动提交offset的,每5秒提交一次。这意味着你处理完消息还没到提交时间,消费者挂了,重启后从上次提交的offset开始消费,已经处理过的消息又被消费一次。
基础操作:关闭自动提交,改成手动提交。处理完业务逻辑在提交offset,把重复的窗口缩到最小。
但手动提交不是万能的。两个高频的坑:一个是Rebalance--消费者组里有人上下线,分区重新分配,正在处理的消息offset还没提交,新消费者从旧offset开始,直接重复;二是处理耗时太长--跳过max.poll.interval.ms(默认5分钟),Coordinator认为消费者死了,踢出去触发Rebalance,又重复了。
防Rebalance的要点:合理设置session.time.out.ms,建议25到30秒,heartbeat.interval.ms设为session.timeout的三分之一左右;max.poll.interval.ms按实际处理耗时留余量。另外在onPartitonsRevoked回调里同步提交当前offset,这是Rebalance前最后的提交机会,别浪费了。
第三道防线:业务端做幂等兜底
前两道防线大幅度减少重复,但没法100%消除。网络抖动、硬件故障这些不可抗力永远存在。所以业务端必须做幂等--重复消费不影响业务结果。
(1)数据库唯一索引最可靠,用业务唯一键(订单号,消息id)做唯一约束,重复插入直接忽略。
(2)redis去重最快,消费前用SETNX写消息id,存在就跳过,但得设合理的过期时间--太短重复消费来了缓存已经没了,太长内存撑不住。
(3)状态机最灵活,业务表加处理状态,消费前查状态,已完成就跳过,但多了一次查询开销。
关键业务的正确姿势:
三层一起上,redis做第一道快速过滤,数据库唯一索引最终兜底,状态机做业务层保护。别省,重复扣款的代价远大于多写几行代码。

浙公网安备 33010602011771号