Kafka消息可靠性:数据丢失与重复的根因、配置与幂等实践

发布时间:2026/10/20 15:02:54
Kafka消息可靠性:数据丢失与重复的根因、配置与幂等实践 1. 从一次线上告警说起Kafka的“靠谱”与“不靠谱”大概两年前我负责的一个订单消息系统在某次凌晨的大促压测后出现了诡异现象下游财务系统对账时发现有大约万分之三的订单支付成功消息“凭空消失”了。查了半天数据库里订单状态是“已支付”但消息队列里对应的“支付成功”事件就是没有。后来定位到是Kafka Producer在发送消息时因为 Broker 端某个分区所在的磁盘瞬时抖动导致 ISR 收缩消息虽然写入了 Leader 但没能被足够多的副本确认最终在重试机制和幂等性配置的双重作用下被“静默”丢弃了一部分。另一件事更常见某个数据同步任务因为消费端在“处理完业务逻辑之后、提交 Offset 之前”进程重启了重启后从头开始消费结果同一批用户画像数据被重复写入了两遍导致下游统计报表翻倍。这种问题几乎每个用 Kafka 的团队都会遇到而且是典型的“既丢数据、又重复数据”的并发场景。这篇文章我想把 Kafka 的数据丢失与数据重复这两件事从原理、配置、实操、排障四个层面完整拆开讲清楚。内容主要面向正在使用 Kafka 做核心业务消息、数据管道、日志采集的同学也适合刚接手 Kafka 运维、被老板问“Kafka 到底会不会丢消息”的开发者。读完你至少能回答三个问题数据在哪些环节可能丢怎么用配置和代码把丢失概率降到最低如果业务允许重复怎么把重复的副作用消除掉2. Kafka 到底在哪几个环节会丢数据一张链路图拆明白Kafka 的数据链路简化来看就是三段生产者Producer→ Broker 集群主题分区、副本→ 消费者Consumer。数据丢失可能发生在任意一段数据重复也一样。很多人一上来就调参数但不知道参数对应的是哪一段链路所以先要把模型建起来。2.1 生产端消息“发出去”不等于“存下来”生产端最容易丢数据也是最容易被忽视的。原因在于 Producer 的发送是异步的send()方法调用后消息只是进入了本地缓冲区buffer.memory真正的网络发送由后台 I/O 线程完成。如果此时发生以下情况消息就丢了Producer 端启用了重试retries但重试达到上限仍失败并且没有配置回调处理失败结果。消息在缓冲区积压超过max.block.ms后抛出异常而业务代码没有捕获。发送过程中Producer 把消息发给了某个 Broker但该 Broker 在消息写入本地日志之前就宕机了并且副本没有同步完成。这里有一个很重要的认知Kafka 的 Producer 默认并不能保证消息不丢。它只能保证“尽力发送”真正能不能落盘取决于 acks 参数和 Broker 的副本同步策略。我见过不少团队把acks0用在了日志采集上服务器一重启毛估估丢 5% 的消息这在日志场景也许能接受但在订单、支付、库存场景就是事故。2.2 Broker 端副本同步策略决定“存储安全线”Broker 端丢数据核心是“写入 Leader 成功”和“数据真正持久化且被多数副本持有”之间的时间窗口。Kafka 的每个分区有多个副本Replica其中一个是 Leader其余是 Follower。生产者只写 LeaderFollower 异步拉取数据同步。如果min.insync.replicas1意味着只要 Leader 自己写成功了就返回成功其他副本完全没跟上也没关系。此时 Leader 宕机新选举出来的 Follower 里没有这条数据消息就丢了。如果min.insync.replicas2则需要至少一个 Follower 也同步成功才返回确认。这样 Leader 宕机时至少有一个副本持有数据丢的概率大幅降低。还有一个细节Broker 刷盘log.flush.interval.messages和log.flush.interval.ms的默认配置其实相当“松弛”数据先进 Page Cache 就算“写入成功”操作系统还没真正刷到磁盘。如果机器突然断电或者内核崩溃Page Cache 里的数据会丢。对绝大多数场景来说依靠副本冗余比依靠刷盘更能保证数据安全。2.3 消费端Offset 提交时机是分水岭消费端丢数据和重复数据其实是同一个问题的两面根源都在于“Offset 提交”和“业务处理完成”之间的顺序关系如果先提交 Offset再执行业务逻辑业务逻辑执行失败但 Offset 已经提交了这条消息就“逻辑丢失”——数据还在 Kafka 里但你的系统不会再去消费它。如果先执行业务逻辑再提交 Offset业务逻辑执行成功了但 Offset 没来得及提交消费者就挂了重启后会重新消费这条消息造成重复。大部分业务系统使用的是第二种方式至少提交一次At Least Once因此重复是常态需要在下游做幂等。而第一种方式最多一次At Most Once虽然不重复但会丢数据一般只在日志分析这种“丢了不致命”的场景使用。2.4 数据重复三类主要来源数据重复和丢失往往纠缠在一起。我整理了三类主要来源Producer 重试导致的消息重复retries配置大于 0 时如果发送过程中网络超时或者 Broker 返回可重试错误Producer 会重新发送同一批消息。这时如果服务端其实已经写入了就会产生重复。消费者 Rebalance 导致的重复消费消费组内有消费者宕机或新增消费者触发 Rebalance分区被重新分配未提交 Offset 的分区会从上次提交的位置重新消费。消费者处理成功但提交失败导致的重复最常见处理完消息后提交 Offset 时网络抖动提交失败下次消费又从旧 Offset 开始。理解这一点很重要在 Kafka 的默认语义下重复是无法完全避免的只能通过幂等设计去“消化”重复。3. 生产端“数据不丢失”的完整配方参数、代码与测试方法3.1 关键参数逐项拆解acks、retries、buffer.memory生产端要想做到“不丢消息”核心是控制三个参数acks、retries、min.insync.replicas配合使用。acks 参数有三个值acks0Producer 不等待任何确认消息发出去就认为成功。吞吐最高但丢数据风险最大。只适合指标采集、日志等允许丢失的场景。acks1Leader 写入本地日志即返回成功。默认值吞吐较好但如果 Leader 在 Follower 同步前宕机会丢数据。acksall或-1Leader 会等待所有参与复制的副本都确认写入后才返回成功。这是“不丢消息”的基础配置。注意acksall只是“副本都确认”如果副本数只有 1那和acks1没有本质区别。所以必须同时设置replication.factor主题级别或者 broker 级的default.replication.factor为 3并且设置min.insync.replicas2。retries 参数默认值其实很大2147483647但关键在于如果设置了acksall且enable.idempotencetrue重试才是安全的。幂等生产者会为每条消息附加序列号Broker 端根据序列号去重。如果不开幂等重试可能产生重复。buffer.memory 与 max.block.msbuffer.memory默认 32MBmax.block.ms默认 60 秒。当发送速度超过 Broker 处理速度时缓冲区会满此时send()方法会阻塞直到超时抛出异常。很多人忽略这个异常导致消息丢失。建议业务代码里对send()的返回结果Future或者回调Callback做完整处理。这是一个标准的“不丢消息”配置组合Java 示例Properties props new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafka1:9092,kafka2:9092,kafka3:9092); props.put(ProducerConfig.ACKS_CONFIG, all); props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE); props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5); props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true); props.put(ProducerConfig.LINGER_MS_CONFIG, 5); props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); props.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 120000); props.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 30000);这里要注意enable.idempotencetrue时Kafka 会自动把acks设置为all并且把max.in.flight.requests.per.connection设置为 5 以内默认 5保证有序。所以如果你显式设置了acks0又开了幂等会直接报错。3.2 生产端不丢消息的代码级保障参数只是一半另一半在代码里。我踩过的一个坑是很多人用producer.send(new ProducerRecord(topic, message))之后完全不关心返回值。异步发送时如果 Broker 返回异常send()本身不会抛错错误只会在回调或者Future.get()时暴露。推荐的做法是给每个消息注册回调并且对失败消息做分类处理producer.send(record, (metadata, exception) - { if (exception null) { // 成功可记录日志或者计数器 } else { // 区分可重试异常和不可重试异常 if (exception instanceof RetriableException) { // 把消息重新投递到本地重试队列或者稍后重试 } else { // 不可重试需要人工介入或转存到死信主题 } } });另外一个推荐方案是把需要高可靠发送的消息在发送 Kafka 之前先写入本地文件或者事务性数据库等 Kafka 确认后再标记完成。这样 Kafka 挂了也能基于本地日志做补偿。这套方案在金融、订单系统里很常见叫“本地消息表 事务消息”虽然笨重但确实可靠。3.3 如何测试你的生产端会不会丢消息配置好了不能纸上谈兵我建议用两个测试验证测试一Broker 宕机测试。向一个 3 副本主题持续生产消息中途手动 kill 掉 Leader 所在的 Broker 进程观察生产端是否报错、恢复后消息是否完整。正确配置下生产者会短暂报NotLeaderForPartitionException但重试后恢复最终消息一条不丢。测试二批量发送加断电测试。生产端向单 Broker 集群发送大量消息发送过程中直接kill -9生产端进程再重启验证。这里考验的是 Producer 端的缓冲区和 Broker 端 Page Cache 的配合你会发现只有副本冗余才能兜底。4. Broker 端的可靠性与主题设置让存储不再“裸奔”4.1 副本因子与 ISR数据安全的“稳定三角”主题创建时有一个参数叫replication.factor它决定了每个分区有几个副本。很多开发环境为了省事用了 1生产环境必须 3 或以上。但注意副本多不代表一定安全还要配合min.insync.replicas。在 Kafka 的同步机制里有一个概念叫 ISRIn-Sync Replicas同步中副本集合只有“跟得上 Leader 同步节奏”的副本才会留在 ISR 里。当 Leader 宕机时Kafka 会从 ISR 中选择新的 Leader。如果你的min.insync.replicas2但是 ISR 里只有 Leader 自己了生产者写入时会返回NotEnoughReplicasException而不是把数据写入这个“孤立 Leader”然后假装成功。这个设计非常关键。它意味着当数据安全得不到保证时Kafka 宁可拒绝写入也不接受数据丢失的风险。那业务上怎么处理这种拒绝呢一般是把消息转入本地重试队列等 ISR 恢复后重新发送。如果你用acksall但不设置min.insync.replicas默认是 1那么当所有 Follower 都挂掉时Leader 还是会照常接收消息风险就潜伏下来了。推荐配置是replication.factor3min.insync.replicas2unclean.leader.election.enablefalse。unclean.leader.election.enable是另一个容易被忽略的参数。默认 false意思是当 ISR 里所有副本都挂了时Kafka 不会从“不同步的副本”非 ISR中选举新的 Leader而是让分区保持不可用。如果你把它设为 true虽然分区恢复得快但可能丢失大量已提交的数据因为那个“不同步的副本”根本不含最新数据。这个参数本质是在“可用性”和“一致性”之间做选择我强烈建议核心业务保持 false。4.2 Topic 级别的配置集中管理用命令创建主题时一定把这个可靠性配置写清楚kafka-topics.sh --create \ --topic order-event \ --partitions 12 \ --replication-factor 3 \ --config min.insync.replicas2 \ --config cleanup.policydelete \ --config retention.ms604800000 \ --bootstrap-server kafka1:9092如果你已经创建了主题也可以动态修改kafka-configs.sh --alter \ --entity-type topics \ --entity-name order-event \ --add-config min.insync.replicas2 \ --bootstrap-server kafka1:9092还有几个和可靠性相关的 broker 参数值得注意log.flush.interval.messages默认是9223372036854775807意味着“不按消息条数触发刷盘”只靠时间间隔和缓存压力触发。对可靠性要求极高时可以设置成10000或更小但会显著降低写入性能。num.replica.fetchers控制 Follower 从 Leader 拉取数据的线程数。默认 1如果副本同步延迟大可以调大比如 4加快 ISR 的同步速度。replica.lag.time.max.ms默认 30000 毫秒超过这个时间没跟上同步的副本会被踢出 ISR。这个值太短会导致 ISR 频繁收缩太长会延迟故障感知需要根据业务容忍度调整。4.3 磁盘故障与 Page Cache 的那些事Broker 端的“数据丢失”还有一种隐蔽情况磁盘损坏。Kafka 的 Segment 文件如果因为磁盘坏道读不出来但操作系统还没报错消费者会一直读到某个位置卡住。另一种情况是 Page Cache 里的数据还没来得及 fsync机器断电。对于这种情况除了副本冗余外一般没有更好的办法。不要指望 Kafka Broker 单机数据安全Kafka 的可靠性模型是基于“分布式冗余”而非“单机持久化”构建的。如果预算允许给 Broker 挂 SSD 和独立电源能显著降低这类风险。5. 消费端“不丢不重”的核心策略Offset 与幂等设计5.1 手动提交与自动提交两种模式的取舍Kafka 消费者默认使用enable.auto.committrue每 5 秒自动提交一次 Offset。这个模式写起来省事但埋了很大的坑假如你拉取了一批消息处理到第 100 条时进程崩溃此时自动提交周期还没到重启后从之前提交的位置消费会重复处理假如正好在提交后崩溃那么第 100 条之后的处理结果丢失了。核心业务一定要关闭自动提交Properties props new Properties(); props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, earliest); props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);然后采用手动提交的方式。手动提交也分两种同步提交commitSync提交失败会抛异常可以捕获后重试或记录。缺点是阻塞当前消费线程影响吞吐。异步提交commitAsync非阻塞通过回调感知提交结果但失败后不会自动重试需要自己补偿。实际项目中我推荐“异步提交 定期同步兜底”的组合。即大部分时候用commitAsync保证吞吐在消费者关闭前用commitSync保证最终提交完成。更稳妥的做法是在消息处理成功后再提交 Offset且提交的粒度是“本批次最大的 Offset”。比如拉取到 1000 条消息全部处理完后再提交这 1000 条中最后一条的 Offset。如果处理过程中失败就一条都不提交下次会重新处理整个批次。这会造成一些重复但不会丢。5.2 消费端重复消费的终极解法幂等既然“至少一次”语义下重复不可避免那唯一的解法就是让消费处理逻辑幂等。所谓幂等是指同一个消息无论处理多少次对系统产生的影响都相同。常用方案有这么几种唯一键约束在数据库表上对业务唯一键如订单号、消息 ID建立唯一索引。重复消费时插入会触发主键冲突捕获后直接跳过。去重表在业务库建一张专门的消息消费记录表存储消息 ID 和消费状态。每次处理前先查这张表处理成功后写入。注意这里要保证“查表 处理 写表”在同一个事务里否则并发下还是会重复。Redis 去重用SETNX命令把消息 ID 作为 key如果设置成功说明首次消费如果已存在说明重复。需要设置合理的过期时间并注意 Redis 和业务操作的原子性无法严格保证但大多数场景够用。状态机校验适用于订单、任务类业务。消息里带有一个目标状态处理时检查当前状态是否已经满足满足则直接忽略。比如“订单已支付”消息重复消费而订单状态已经是“已支付”直接返回成功。我在一个用户积分系统里用的是“去重表 事务”方案特别稳。处理消息时开启事务先查消息去重表如果没有记录就执行积分变更逻辑然后插入去重记录最后提交事务。并发重复消息会被去重表的唯一索引挡住事务保证原子性。这套方案对 MySQL 压力有一点但非常可靠。5.3 Rebalance 引发的重复消费可干预但难杜绝消费组内成员变化会触发 Rebalance每次 Rebalance 时所有消费者会撤销对当前分区的所有权然后重新分配。在撤销和重新分配的过程中有一部分分区可能从“已经被消费了一部分”的状态被交接到另一个消费者手里而 Offset 还没提交于是重复消费。可以减少 Rebalance 的影响但做不到完全避免调大session.timeout.ms和heartbeat.interval.ms让消费者有更多时间处理心跳避免因 GC 停顿被误判宕机。使用static membership静态成员 ID消费者实例重启后不会触发 Rebalance分区依然分配给原来的消费者。配置group.instance.id即可但注意它和自动均衡的取舍。如果消费者处理单条消息很慢可以调大max.poll.interval.ms默认是 5 分钟超过 5 分钟没拉取新消息会被认为“卡死”也会触发 Rebalance。5.4 一个完整的消费端“不丢不重”代码骨架把上面的内容落成一个代码骨架大概是这样的while (true) { ConsumerRecordsString, String records consumer.poll(Duration.ofMillis(1000)); if (!records.isEmpty()) { // 1. 逐条处理业务逻辑 for (ConsumerRecordString, String record : records) { try { processWithIdempotentCheck(record); } catch (Exception e) { // 2. 处理失败记录到死信主题不阻塞整个批次 sendToDlq(record, e); } } // 3. 全部处理完成后异步提交本批次最后的偏移量 OffsetAndMetadata metadata new OffsetAndMetadata( records.iterator().next().offset() records.count()); consumer.commitAsync(offsets, (offsets, exception) - { if (exception ! null) { // 4. 提交失败记录日志下一次轮询会继续提交 log.error(commit offset failed, exception); } }); } }注意代码里的两个细节处理单条消息时如果失败不要立刻中止整个批次否则会造成“队头阻塞”后面消息全部延迟。正确做法是“失败的转死信成功的继续”最终批量提交 Offset。如果一条消息反复失败每次都走重试会拖垮消费者吞吐。所以对处理失败的消息要设置最大重试次数超过后直接进死信主题人工或者定期任务去修复。6. 数据丢失与数据重复的排障实战从现象到根因6.1 常见问题速查表我整理了运维中最高频的几类问题对应现象、原因和排查方向现象可能原因排查方向消费者延迟越来越大但没报错消费能力不足或单条消息处理阻塞看kafka-consumer-groups.sh --describe的 LAG 指标检查消费者线程堆栈部分消息丢失producer 日志无异常acks 配置为 0 或 1Leader 宕机查 Producer 配置、Broker 端isr状态、是否有 unclean 选举下游报表数据翻倍消费端有重复消费无幂等查消费者日志中是否有 Rebalance、是否设置了enable.auto.committrue写入时报NotEnoughReplicasExceptionmin.insync.replicas设置太高或副本同步失败查kafka-topics.sh --describe的 ISR 列表检查副本是否全部挂掉消费组 Rebalance 频繁消费者心跳超时或 max.poll.interval 超时查看心跳线程日志、GC 日志、消费者处理时间统计消息乱序重试时max.in.flight.requests大于 1 且未开启幂等检查 Producer 是否设置enable.idempotencetrue6.2 一个完整排障案例订单消息“丢失”的定位过程说一个我实际经历的案例。A 团队反馈他们的订单状态变更消息偶尔会丢表现是下游通知服务收不到部分“订单已支付”事件。我上手排查的步骤是这样的第一步检查生产端配置。发现acks是 1同时retries为 3enable.idempotence没有开启。这意味着消息写入 Leader 即返回成功如果 Leader 在 Follower 复制前发生故障消息就没了。第二步看 Broker 端的情况。用kafka-topics.sh --describe检查主题的副本状态发现 ISR 只有一个副本说明另外两个 Follower 长期同步失败。查 Broker 日志发现其中一台机器磁盘空间接近满Follower 拉取数据频繁失败。第三步结合时间线。消息丢失发生的时间正是那台磁盘满的机器上 Follower 被踢出 ISR、随后 Leader 也发生短暂不可用的时段。由于min.insync.replicas默认是 1Leader 依然接受了写入但数据只有一份宕机后直接丢了。修复方案把订单主题的min.insync.replicas调整为 2ackall同时给磁盘告警加了阈值。从那以后这个主题再没发生消息丢失。这个案例说明一个道理Kafka 丢数据往往不是某一个配置的单独问题而是多个薄弱环节在故障时间点上恰好叠加。单独看每一项好像都“不至于丢”但组合在一起丢的概率就不小了。6.3 排障必备命令与脚本排障时最常用的几个命令建议收藏# 查看消费者组的消费进度和 LAG kafka-consumer-groups.sh --bootstrap-server kafka1:9092 --describe --group order-consumer-group # 查看主题分区的 Leader 和 ISR 列表 kafka-topics.sh --bootstrap-server kafka1:9092 --describe --topic order-event # 查看某个 Topic 指定分区的消息总量和最早/最新 Offset kafka-run-class.sh kafka.tools.GetOffsetShell --bootstrap-server kafka1:9092 --topic order-event --time -1 # 查看 Broker 的运行时指标需要开启 JMX kafka-run-class.sh kafka.tools.JmxTool --object-name kafka.server:typeBrokerTopicMetrics,nameMessagesInPerSec另外我建议给 Kafka 集群配上监控告警至少覆盖以下指标UnderReplicatedPartitions不为 0 说明有副本同步异常是丢数据的前兆。IsrShrinksPerSec短时间内频繁收缩说明 Broker 不稳定。OfflinePartitionsCount不为 0 说明有分区没有 Leader消费会中断。RequestsPerSec的 Error 分布特别是NotEnoughReplicas和NotLeaderForPartition的数量。7. 工具选型与使用心得哪些场景选 Kafka哪些场景别选说了这么多 Kafka 可靠性的问题我也想说点真心话。有些场景Kafka 其实不是最优选择不要硬上。7.1 Kafka 适合做什么削峰填谷的异步消息管道比如下单后发短信、发邮件、更新积分、同步搜索索引这些允许秒级延迟、可以重复消费的场景Kafka 非常合适。大数据流式计算配合流处理框架做实时 ETL、实时指标计算Kafka 的分区模型和消费组模型是天然的适配。日志收集与统一处理把各业务系统的日志集中到 Kafka再分发到 ES、数仓等系统这种场景允许少量丢失如果配置acks0吞吐优先。7.2 Kafka 不适合做什么金融级强一致事务消息虽然 Kafka 有事务 API但它的实现基于 Offset 原子提交和 RocketMQ 的事务消息模型不同。如果你的核心链路是“先扣款、再发消息、消息失败要回滚”Kafka 的分布式事务用起来很别扭。要求严格不重复的精确一次投递Kafka 的 EOS 语义需要在生产者、Broker、消费者三端配合且只适用于单个流处理应用内部。跨系统、跨数据库时还是要靠业务幂等。小消息低延迟场景Kafka 的延迟通常在毫秒到十毫秒级别远高于专门的 RPC 或消息队列。如果 RT 要求几个毫秒选其他中间件更合适。7.3 关于消息队列选型的一个个人看法我在实际项目中同时用过消息队列多个产品。如果要一句话总结业务消息选事务能力更强的日志和流数据选 Kafka轻量任务调度选其他简化产品。但这篇文章不讨论选型之争核心是不管你选了哪个都要把数据传输的每个环节的可靠性边界搞清楚。Kafka 最大的特点是“吞吐高、扩展性好、生态丰富”但可靠性是配置出来的不是默认自带的。8. 事务消息与精确一次处理Kafka 到底能不能做到“不丢不重”8.1 Kafka 事务的边界是什么Kafka 从 0.11 版本开始支持事务可以做到“跨多个分区的原子写入”配合流处理框架可以实现“端到端精确一次”的语义。但要注意这个“精确一次”是有边界的它只能保证在 Kafka 内部生产到消费的精确一次并不能保证你的业务数据库操作和 Kafka 消息写入是一个原子事务。举个典型的例子一个订单服务业务逻辑是“更新 MySQL 订单状态为已支付”然后“发送 Kafka 消息通知下游”。如果这两个操作不在同一个事务里就会有问题先更新 MySQL再发 KafkaMySQL 更新成功了但 Kafka 发送失败下游感知不到支付事件。先发 Kafka再更新 MySQL下游收到了支付消息但 MySQL 更新失败了数据不一致。Kafka 事务解决不了这个问题因为它的事务边界在 Kafka 内部。要解决得借助“本地消息表 定时补偿”或者使用支持事务消息的中间件。8.2 本地消息表的实现思路这是我给业务方推荐得比较多的一种方案思路很简单业务操作和消息写入放在同一个本地数据库事务里。比如订单表插入一条订单记录同时在消息表插入一条“待发送消息”。有一个定时任务扫描消息表把“待发送消息”发送到 Kafka发送成功后把消息表状态改成“已发送”。如果 Kafka 发送失败状态依然保持“待发送”定时任务不断重试直到成功。下游消费者需要幂等因为定时任务可能重复发送。这套方案的缺点是需要额外维护消息表增加一次数据库写入优点则是逻辑简单几乎不会丢消息也不需要引入额外中间件。我见过不少订单系统这么干稳定性比直接调 Kafka 事务 API 高很多。8.3 Kafka 事务 API 的一个坑如果你真的要用 Kafka 的事务 API有个地方容易踩坑initTransactions()和beginTransaction()之间如果 Producer 端抛出了TimeoutException有些人以为事务失败直接重试但旧的 Producer 实例可能已经和 Broker 端产生了未完成的事务状态需要abortTransaction()或者重新初始化。我自己调事务 API 时遇到并发生产者共用同一个transactional.id导致 ProducerFencedException 的概率也不低。简单说Kafka 事务 API 适合流处理框架内部用不适合业务系统裸用。9. 从“能运行”到“高可靠”可靠性治理的五个阶段最后我想分享一个团队治理 Kafka 可靠性的阶段模型是我在不同项目中总结出来的。每个阶段对应不同的工作重点你可以对照自己团队看处在哪个阶段。第一阶段能跑就行典型特征Topic 随便建分区数默认 1副本因子 1acks不设置消费端开自动提交。这个阶段主要是功能验证丢数据、重复数据都是“常态”不会有人追责。第二阶段核心链路加固核心业务消息开始配置acksall、replication.factor3、min.insync.replicas2消费端关闭自动提交。但测试环境和非核心链路依然是默认配置。这个阶段能覆盖 80% 的丢数据风险但重复还是靠运气。第三阶段全链路幂等开始对核心消费链路做幂等设计唯一键约束、去重表、状态机校验。数据重复的副作用被消除即使发生了重复消费也不会影响业务数据。这个阶段是“不丢不重”的实践起点。第四阶段监控与演练部署完整的监控告警覆盖 ISR 收缩、分区离线、消费 LAG 等指标。定期做主从切换演练、Broker 宕机演练验证数据是否完整。只有通过演练你才敢说“我们的 Kafka 是可靠的”。第五阶段架构级治理把 Kafka 的 Topic 命名、消息格式、权限控制、容量规划纳入规范管理。对每条消息定义生命周期和消费语义明确是 At Least Once、At Most Once 还是 Exactly Once。到这个阶段Kafka 不再是一个“中间件”而是像数据库一样被治理的核心基础设施。大多数团队停留在第二阶段。能意识到需要走到第四、第五阶段的通常都是被线上事故教育过的。我自己也是。10. 个人实操中的一些补充体会文章写到这里核心内容已经讲完了。我想再补充几个零碎但实用的经验是我在真实环境中反复验证过的。体会一不要轻易调大fetch.min.bytes和fetch.max.wait.ms来提升吞吐。这两个参数会影响消费延迟和批量行为一旦设置不当下游会出现明显的“一顿一顿”的感觉而且排障时很难查。体会二Kafka 的监控指标一定要看“趋势”而不是“绝对值”。比如UnderReplicatedPartitions瞬时升高可能是故障转移的瞬间不重要但如果持续 10 分钟不下降就要马上处理。体会三Producer 的linger.ms不是越大越好。默认是 0表示有数据立即发送吞吐不高。设成 5ms 左右是个合理的折中。设成 100ms 以上虽然能凑更大的批次但消息延迟显著增加实时性要求高的业务会出问题。体会四消费端不要在一个批次里处理太多消息。max.poll.records默认是 500如果你每条消息处理需要 100ms一批就是 50 秒很可能超过max.poll.interval.ms默认 5 分钟的半程如果遇到 GC 停顿很容易触发 Rebalance。建议单条消息处理时间超过 50ms 时把max.poll.records调小到 100 或者 50。体会五凡是“先改库、后发消息”的业务一定要考虑“改库成功但发消息失败”的补偿路径。不一定要用本地消息表但至少要有定期对账和补发机制。没有补偿路径的 Kafka 消息系统本质上只是“半可靠”。写这篇文章时我又回想了一遍自己从“以为 Kafka 很靠谱”到“理解 Kafka 的可靠性边界”的过程。Kafka 本身是一个非常优秀的分布式系统但它不会替你保证数据安全——所有可靠性都是你通过配置、代码、架构和运维一砖一瓦搭出来的。希望这篇文章能帮你少踩几个坑。