做后端这几年,几乎每个用过 Kafka 的团队都问过同一个问题:Kafka 到底能不能保证消息只被消费一次?这个问题看着简单,实际上背后藏着好几层语义。我自己在业务里折腾过订单流水、支付回调、库存同步各种场景,也踩过重复消费的坑,这篇就把“只消费一次”从原理到落地彻底讲清楚。
1. 先别急着写代码:“只消费一次”至少可以拆成三个问题
“消息只消费一次”这句话在分布式系统里有歧义。我和同事讨论时发现,不同岗位背景的人说这句话,指的根本不是同一件事。
从上游到下游,消息至少要经过三段旅程:
| 阶段 | 要解决的问题 | 对应的语义 |
|---|---|---|
| 生产者到 Broker | 消息是否被重复写入 Kafka | 生产者幂等 |
| Broker 到消费者 | 消息是否被重复投递给消费者 | 消费端交付语义 |
| 业务处理链路 | 消息被处理两次的结果是否一致 | 业务幂等 |
很多人说“Kafka 保证只消费一次”,潜意识里默认的是“Broker 到消费者”这一段。但实际业务出问题,往往出在第一段和第三段——上游重试导致消息重复写入,或者下游处理逻辑本身不具备幂等性。
Kafka 官方对“交付语义”(Delivery Semantics)有明确定义,分三个等级:
- 最多一次(At most once):消息可能丢,但绝不会重复。消费者处理完再提交偏移量,提交失败就跳过。
- 至少一次(At least once):消息不会丢,但可能重复。消费者先处理业务再提交偏移量,处理成功但提交失败时,下轮还会消费到同一条。
- 恰好一次(Exactly once):消息既不会丢,也不会重复,而且处理结果和“只处理一次”完全一致。
这里要泼一盆冷水:Kafka 默认配置下是“至少一次”,不是“恰好一次”。把 enable.auto.commit 设为 false,处理完业务手动提交偏移量,也只是“至少一次”,因为提交动作本身可能失败。
而且“恰好一次”在中间件层面实现,和业务层面实现是两回事。中间件能保证“这条消息我只投递一次”,但你的业务方法被执行两次时,仍然可能产生重复数据——比如没有唯一键约束的 INSERT 语句。所以真正要解决“只消费一次”,必须把消费者业务逻辑、存储设计、提交机制三者一起考虑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 为什么 Kafka 默认就在重复消费:深入了解偏移量提交与重平衡机制
想解决重复消费,先得搞清楚重复从哪来。Kafka 做的是“至少一次”投递,重复的根源集中在三个环节:偏移量提交时机、消费者组重平衡、以及生产者端的重试机制。
2.1 自动提交偏移量:处处透着省事,也处处都是坑
很多人刚开始用 Kafka 时,直接用了默认配置:enable.auto.commit=true,auto.commit.interval.ms=5000。这个配置的意思是,消费者每 5 秒自动把当前拉取到的最新偏移量提交给 Broker。看起来挺省事,但有个致命问题:提交偏移量的时机和业务处理结果没有绑定关系。
举个例子,消费者拉取了一批消息,正在处理第 10 条,此时距离上一次自动提交已经过去了 4 秒。第 10 条处理到一半,消费者进程突然崩溃。重启后,消费者从上次自动提交的偏移量继续消费——第 10 条之前的几条消息,很可能还没处理完就被跳过了,而第 10 条本身处理了一半,没提交结果。如果业务逻辑里把“处理一半”当成“处理成功”,那这条消息就丢了;如果重启后消费者把这一批重新拉一次,那已经成功处理的第 1~9 条,就会被重复处理一遍。
我当时维护过一个统计系统,用的就是自动提交。某次发布后,线上数据翻了一倍,排查下来就是自动提交加重启导致的批量重复。从那之后,所有核心链路我都改成手动提交,宁可代码多写几行,也不碰自动提交。
2.2 消费者组重平衡:不是宕机才会触发
消费者组重平衡是另一个高频重复来源。Kafka 的消费模式是:一个分区只能被同一个消费者组里的一个消费者实例消费。当消费者实例数量变化、订阅的 Topic 变化、或者消费者心跳超时,都会触发重平衡(Rebalance),把分区重新分配一遍。
这里容易被忽略的点是:重平衡不一定发生在宕机时。比如某个消费者处理消息太慢,超过 max.poll.interval.ms(默认 5 分钟),Broker 就认为它“失联”了,把它踢出消费者组,触发重平衡。这个消费者还没崩溃,只是处理慢,但它已经持有的分区会被分配给别的消费者。如果它手里还有一批消息没处理完、没提交偏移量,重平衡完成后,这批消息就会在新的消费者实例上被重新消费一遍。
更隐蔽的情况是 session.timeout.ms 设置太小。某次线上消费延迟升高,消费者实例 GC 暂停了十几秒,心跳没来得及发,被判定为超时,触发重平衡。等 GC 结束,它发现自己已经不是某些分区的所有者了,正在处理的消息自然也要重来一次。
以下是我整理的心跳与动态协调相关核心参数:
| 参数 | 默认值 | 影响 |
|---|---|---|
session.timeout.ms |
45000 | 消费者定期发送心跳,超过该时间未收到心跳,判定为死亡 |
max.poll.interval.ms |
300000 | 两次 poll 之间的最大间隔,超时会被移出消费者组 |
heartbeat.interval.ms |
3000 | 心跳间隔,建议设置为 session.timeout 的 1/3 |
max.poll.records |
500 | 单次 poll 拉取的最大消息数,影响处理时长 |
partition.assignment.strategy |
range | 分区分配策略,会影响重平衡时代价 |
结合这套参数,给一个针对高延迟场景的稳妥配置组合:session.timeout.ms 调到 100000,max.poll.interval.ms 调到 600000,heartbeat.interval.ms 调到 10000。这样普通 GC 造成的短暂停顿不会轻易触发重平衡。但要让处理逻辑本身足够快,如果业务处理大量消息需要几分钟,建议调低 max.poll.records,或者把重活拆出去异步做。
2.3 生产者重试导致重复写入:消费者无法拦截的一类重复
还有一类重复,是消息在源头就已经是重复的。生产者发送消息时设置了 retries>0,如果 Broker 返回了可重试的异常(比如网络分区、Leader 选举),生产者会重新发送。此时 Kafka 的 ACK 机制面临一个经典问题:重试时,无法区分“前一次发送实际已经成功,只是 ACK 丢了”和“前一次发送真的失败”。
举一个具体场景:生产者发送消息给 Partition 0,Broker 写入成功,但在返回 ACK 之前,网络抖动导致生产者和 Broker 之间的连接断开。生产者认为发送失败,触发重试,把同一条消息再次发送给 Partition 0。Broker 并不知道这两条是“同一条”,它只会按新消息写入,分区里就有了两条内容完全一样的消息。
消费者收到这两条消息,无论偏移量提交多完美,业务逻辑都会被触发两次。所以你会发现:消费者端怎么优化,都拦不住生产者端造成的重复。要解决这一层,必须回到生产端开启幂等,或者用消息里携带的业务唯一 ID 在消费端去重。
3. 当下最通用的实践:把消费逻辑做成幂等,顺便处理生产端重复
既然“至少一次”是常态,那最务实的思路就是:承认可能重复,让重复不影响结果。这就是幂等设计,也是我目前在绝大多数项目里推荐的首选方案。
3.1 幂等的本质:重复调用和单次调用的最终效果一致
幂等这个概念,用大白话说就是:同一个操作执行一次和执行一百次,产生的最终状态是相同的。
以订单系统为例,消费一条“创建订单”消息,业务逻辑是往订单表 INSERT。这条消息被消费两次,就会插入两条一模一样的订单——这就是非幂等。如果给订单表加一个业务单号唯一索引(比如用消息里的 bizId),第二次 INSERT 会因为唯一键冲突而失败,最终表里只有一条订单——这就是幂等。
幂等设计的核心其实是两件事:
- 找到天然的幂等键:业务单号、流水号、外部回调 ID,这类字段在业务上天然唯一。
- 围绕幂等键做约束:数据库唯一索引、Redis SETNX、状态机流转判断,选一种和业务量匹配的方案。
只要这两点做到位,重复消费带来的就不是灾难,最多是多了一次无效的处理动作。
3.2 去重表的落地细节:不只是加个唯一索引
用“去重表”实现幂等,是最常见也最稳的方案。做法很简单:在业务数据库里建一张去重表(或直接给业务表加唯一键),消费消息时先把 bizId 插入去重表,插入成功才继续处理业务;插入失败则说明这条消息之前处理过,直接跳过。
这里有很多细节值得注意:
第一,去重插入和业务处理必须在同一个事务里。 否则会出现:去重表插入成功,业务处理失败,事务回滚后,业务没写入,但去重记录也没了,下一条重复消息又会重新触发一次处理。用 Spring 的话,就是在同一个 @Transactional 方法里,先执行去重表的 INSERT IGNORE,再执行业务表的写入。
第二,沿用唯一索引,不要只用“先 SELECT 再判断”。 并发场景下,两个消费者实例同时处理同一条消息时,SELECT 结果都是“不存在”,然后双双执行 INSERT,还是会插入两条重复数据。唯一索引是数据库层面的硬约束,能兜底并发场景。比如 MySQL 可以这样建表:
sql复制CREATE TABLE order_records (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
biz_id VARCHAR(64) NOT NULL,
order_info VARCHAR(512),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_biz_id (biz_id)
) DEFAULT CHARSET=utf8mb4;
第三,定时清理去重表或做归档,防止膨胀。 去重表里永远只保留最近一段时间的数据。比如我常设 30 天清理一次,用分区表按天分区,到期直接 DROP 分区,比 DELETE 扫表高效得多。
这个方案的优点很多:不依赖 Kafka 的版本和配置,任何消息队列都能用;不增加额外的中间件依赖;数据库本身就有事务和唯一约束,可靠性高。缺点也很明显:每条消息都多一次数据库写入,吞吐会打折扣。但大多数业务系统瓶颈根本不在这里,几千 TPS 的场景用这个方案毫无压力。
3.3 幂等处理不了的场景:外部副作用
用数据库唯一索引做幂等,解决的是“写自己的库”这类问题。但有些业务动作发生在系统外部——调用第三方支付接口、发短信、推送通知。这些场景下,消费两次等于外部系统被调用了两次,而你自己的数据库里可能完全看不出重复。
处理思路是给外部调用加“业务幂等键”。很多外部接口本身就支持幂等,比如支付接口允许你提交同一个 merchant_order_no,重复请求只处理一次。如果外部接口不支持幂等,就只能用一个前置状态表:消费消息时,先把这笔操作的“处理状态”置为“处理中”,再调用外部接口;外部接口返回后,从“处理中”更新到“成功”;如果中途失败,状态还是“处理中”,由定时任务补偿查询外部系统的真实结果。
这套“状态机 + 外部查询补偿”的模式,比单纯加唯一索引复杂一个数量级。如果业务允许,我更建议:让外部系统提供查询接口,先把外部调用改成可补偿的最终一致性模式,而不是指望 Kafka 只投递一次。
3.4 幂等和“恰好一次”不是一回事
很多人会把“业务幂等设计”和 Kafka 官方说的“Exactly Once Semantics(EOS)”混为一谈。其实它们是两个维度的事:
- 幂等设计:不管中间件投递多少次,业务最终结果一致。这是业务层的兜底,通用性强。
- EOS:中间件层面尽量做到“只投递一次”,把重复的概率降到极低,但依然需要消费端配合。
所以我的经验是:哪怕你最后用了事务和 EOS,也别放弃幂等设计。两条腿走路,才是生产级系统该有的姿态。
4. 深入 Kafka 的“恰好一次语义”:事务与幂等生产者的真实能力
Kafka 从 0.11 版本开始支持“精确一次语义”,实现依赖两个东西:幂等生产者(Idempotent Producer)和事务(Transaction)。理解它们能做什么、不能做什么,比机械地抄配置更重要。
4.1 幂等生产者:解决“生产者重试导致数据重复写入”
前面说过,生产者重试会导致同一条消息被写入多次。幂等生产者的作用,就是在 Broker 侧做重复消息识别。
它的底层原理是:每个生产者实例有一个 Producer ID(PID),每条消息带一个 序列号(Sequence Number)。Broker 收到消息后,会检查 PID + 分区维度上的序列号是否连续。如果新消息的序列号比已写入的最大序列号大 1,就是正常消息;如果比最大序列号小或相等,说明是重复消息,直接丢弃;如果比最大序列号大很多,说明中间有消息丢失,返回错误。
用大白话类比:发快递时每个包裹都有单号且连续编号,快递站发现手里是“编号 5 的包裹”,但记录里最新收到的是“编号 3”,就知道“编号 4”在路上丢了。
开启方式很简单,生产者配置加一行:
properties复制enable.idempotence=true
在 Kafka 3.0 及以后版本,enable.idempotence 默认就是 true。只要生产者是用新版客户端,默认就开启了幂等,配合 acks=all 就能保证“不会因为生产者重试而把同一条消息写入多次”。
但要注意,幂等是有边界条件的:
- 只在单个生产者会话内有效。生产者重启后 PID 会变,旧 PID 的序列号记录可能被清理,重启后如果重发旧消息,Broker 无法识别为重复。
- 只针对单分区连续写入有效,跨分区的重复无法靠 PID+序列号解决。
- 只能解决“写入”阶段的重复,不能保证读端的处理只发生一次。
所以幂等生产者解决的是第一段旅程的问题,也就是“生产者到 Broker 的消息重复写入”,后续我们还是需要消费者配合。
4.2 事务与 Transactional ID:把跨分区的写入变成原子操作
幂等生产者只能管单分区,事务则是跨分区、甚至跨 Topic 的原子性保证。Kafka 事务的经典应用是:同一批消费的消息,处理结果写多个分区,最终要么全部写入成功,要么全部不写。
举个例子,我在一个下游系统里,需要根据用户行为消息同时更新用户画像表和积分表,如果只写了画像表、积分表写失败,数据就不一致了。把两个写入包进同一个 Kafka 事务,就能避免“部分成功”。
事务的实现涉及几个关键组件:
- Transactional ID:给生产者一个稳定的业务标识。它和 PID 绑定,生产者重启后,Broker 能通过 Transactional ID 找到旧的 PID 并触发“ fencing”机制——把旧 PID 标记为失效,防止旧实例继续写。
- 事务协调器(Transaction Coordinator):每个 Producer 初始化事务时需要找到对应的协调器,协调器管理事务状态机,并把状态持久化到内部主题
__transaction_state。 - 控制消息(Control Batch):事务结束时,Broker 会在数据流末尾写入一个特殊的控制批次,标记事务是 COMMIT 还是 ABORT。消费者通过
isolation.level决定是读取“已提交事务数据”还是“全部数据”。
一次完整事务的流程大致是这样的:
- 生产者调用
initTransactions(),获得 PID 和事务协调器分配的事务 ID。 - 调用
beginTransaction()开启事务。 - 正常发送若干条消息到不同分区。
- 调用
commitTransaction(),协调器把事务标记为PREPARE_COMMIT并推进到提交状态,最终在涉及的分区写入控制消息。 - 如果中间出异常,调用
abortTransaction(),协调器标记为 ABORT,消费者的read_committed模式不会看到这部分未提交数据。
这里有个容易混淆的点:Kafka 事务不是数据库那种“全有或全无”的 ACID 事务。消息在事务提交之前,可能已经出现在分区的 log 里,只是消费者配了 read_committed 之后看不到它们。一旦事务提交,这些消息立即对所有消费者可见。
4.3 消费者侧读已提交数据:isolation.level 的正确姿势
事务发送的消息,消费者端也要配合才能规避“看到未提交的数据”。核心参数是 isolation.level:
| 取值 | 行为 | 适用场景 |
|---|---|---|
read_committed |
只消费已提交事务的消息,未提交的事务消息不可见 | 需要事务语义的消费端 |
read_uncommitted |
看到所有消息,包括未提交事务的消息(默认值) | 兼容旧逻辑、无事务场景 |
改成 isolation.level=read_committed 的注意点有两个:
第一,提交但未结束的事务会产生消费者端滞留。如果一个事务长时间不提交,消费者在读到该事务的第一个消息后,必须等待控制消息出现才能继续读后续消息。我之前用事务批量写入时,某次业务事务跑了几十分钟没提交,下游数据延迟跟着涨,故障报告一查才发现是被未提交事务挡住了。
第二,read_committed 下,消费者拉取到的数据末尾有 LSO(Last Stable Offset),它表示“已提交数据的稳定水位”。偏移量提交时如果把位置推进到 LSO 之外,一旦事务回滚,消费者可能会重新消费回滚前的数据,造成重复。
4.4 Kafka Streams 的端到端 EOS:这才是真正的“恰好一次”
聊到这里,你会发现一个尴尬的事实:原生 Kafka 生产者事务 + 消费者 read_committed,并没有做到“端到端只消费一次”。事务保证的只是“写入多个分区的动作是原子的”,但消费者把数据读出来、算一遍、再写到别处的整条链路,依然可能出现处理中断、结果写入一半的问题。
真正把“端到端 EOS”落到实处的,是 Kafka Streams 这类的流处理框架。它把流处理的“读-处理-写”和“偏移量提交”绑定在同一个事务里:处理结果写入下游 Topic 的事务提交,同时把上游消费偏移量也提交到同一条流中。这样要么“下游数据写了、偏移量也提交了”,要么什么都没变,重启恢复后从头处理,不会有重复结果。
但 Kafka Streams 的 EOS 也有限制条件:
- 只保证“Kafka 到 Kafka”的链路。
- 处理逻辑必须是确定性的,不能依赖外部系统的副作用。
- 下游消费者如果不再用
read_committed,还是可能读到“中间状态”。
所以严格来说,Kafka 官方实现的“恰好一次”本质上是流处理框架内部的一致性,而不是一句放之四海而皆准的承诺。
5. 实操:我的事务配置代码和踩坑清单
如果你决定在项目里启用 Kafka 事务,我给你一份可以直接落地的配置和代码,顺带把常见的坑列出来。
5.1 生产者端:开启幂等和事务的完整配置
properties复制# Kafka 生产者核心配置
acks=all # 必须配合事务使用,保证副本同步
enable.idempotence=true # 开启幂等,3.0+ 默认开启
transactional.id=txn-order-service-001 # 事务 ID,同一应用实例必须唯一
transaction.timeout.ms=60000 # 事务超时时间,默认 60000
max.in.flight.requests.per.connection=5 # 事务和幂等开启时允许乱序,可设为 5
retries=10 # 配合事务,重试次数要足够
delivery.timeout.ms=120000 # 若重试次数不设上限,由总超时兜底
事务 ID 的命名非常关键。假设你是多实例部署,每个实例的事务 ID 必须全局唯一。比如订单服务有 5 个实例,事务 ID 可以是 txn-order-service-001 到 txn-order-service-005。如果有两个实例用同一个事务 ID,后启动的实例会触发 fencing,把先启动的实例踢掉,前一个实例后续所有事务都会失败。
5.2 消费者端:读已提交数据
properties复制# Kafka 消费者核心配置
group.id=order-consume-group
enable.auto.commit=false # 事务场景下必须手动提交偏移量
isolation.level=read_committed # 只读已提交事务
auto.offset.reset=latest
max.poll.records=200
手动提交偏移量的代码模式,推荐“先处理业务,再提交偏移量”:
java复制while (running) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1000));
for (ConsumerRecord<String, String> record : records) {
handleBusiness(record); // 业务处理逻辑
}
consumer.commitSync(); // 全部成功后再提交
}
这个模式在“业务处理成功但 commitSync 抛出异常”时会重复消费,但重复消费由幂等设计兜底,这是可接受的代价。
5.3 踩坑清单:事务不是银弹
我在项目里实际使用事务时,遇到过的坑整理如下:
坑一:事务 ID 重复导致生产者实例被 fencing。
项目发布时有两个批次实例同时启动,事务 ID 没区分实例号,后启动的实例把先启动的标记失效,先启动实例的所有事务抛 ProducerFencedException。排查了很久才发现是两个批次配置了相同的事务 ID。后来强制规定事务 ID 必须包含随机后缀或实例序号,发布流程里由发布系统自动生成。
坑二:事务超时时间设置不当,长事务直接失败。
默认 transaction.timeout.ms=60000(60 秒),如果事务里套了外部接口调用,比如调支付网关花了 80 秒,事务会直接超时失败。我把事务超时和业务超时分开理解:事务超时只约束 Kafka 事务状态机的停留时间,不约束你的业务方法执行时间。但 Kafka 事务开启后,所有消息发送都被标记为事务消息,长时间挂起会让下游消费者一直等到超时才能读到数据。最佳实践是事务范围只包住 Kafka 消息发送,不要包住外部业务调用。
坑三:事务版本兼容问题。
旧版本 Kafka 客户端(0.10.x 及之前)不认识事务关系,它们在 read_uncommitted 模式下能读到未提交事务的消息。如果集群里同时存在升级前后的生产者,数据可能出现短暂不一致。升级迁移期要么让所有客户端同时升级,要么接受短暂不一致。
坑四:Kafka 事务和数据库事务是两个事务,不能混为一谈。
我见过有同事在 @Transactional 方法里包着 Kafka 事务,以为“数据库回滚时 Kafka 也会回滚”。实际上这两个事务的生命周期和中间件完全不同。数据库回滚和 Kafka 事务提交/回滚没有绑定关系。如果想做到“数据库写和 Kafka 写要么都成功要么都失败”,需要做本地消息表、或者用事务发件箱(Outbox)模式——这已经超出 Kafka 本身能解决的范围了。
坑五:事务场景下消息吞吐量明显下降。
事务模式下 Broker 要多写控制消息、协调器要多做状态持久化,实测吞吐比非事务模式低 10%~20%。如果你的业务是用事务来做无关紧要的批量写入,性价比很低。
5.4 我的选型建议:先幂等,再事务,最后才考虑流处理
基于上面的分析,我给出一个比较务实的选型路径:
- 默认用幂等消费者 + 去重表/唯一键。这是最低成本、最高兼容性的方案,99% 的业务场景都够用。
- 需要跨分区原子写入时,才引入 Kafka 事务。比如一个事件需要同时更新两个下游 Topic,或者要保证多个分区写入的原子性。
- 需要“端到端保证只处理一次”,考虑用 Kafka Streams 或 Flink 这类流处理框架,而不是在裸消费者上强行拼凑事务。
- 外部系统副作用场景,永远不要只依赖于消息队列的语义,要在自己的业务系统里维护调用状态。
如果读者现在正在处理一个“重复消费导致数据异常”的问题,我强烈建议先把问题定性:重复来自生产者重试、消费端偏移量提交失败,还是业务逻辑非幂等?只有先定位是哪一层,才能选对手段。盲目开启事务、或者盲目上幂等生产者,都治标不治本。
6. 结合我的经验:几个判断“只消费一次”是否做对的检验方法
最后分享几个我在做系统评审或故障排查时常用的小技巧,不用重新发版也能快速判断当前系统是否有能力承接“重复消费”。
6.1 唯一索引观察法
查业务表有没有基于“消息业务 ID”的唯一索引。如果没有,直接把消息 Consumer 里打的日志捞出来,看同一 bizId 是否出现过两次处理日志。出现两次没关系,只要第二次被唯一键拦下来就行;如果两次都插入成功了,那就是非幂等,必须处理。
6.2 偏移量与状态齐步走
看消费逻辑里,“写业务结果”和“提交偏移量”之间有没有可能性保证。最理想的是同库事务(写业务表和写 offset 到同一 MySQL 库里),或者写 Kafka Streams 的 EOS。如果两者之间隔着网络调用,则一定有重复窗口,需要幂等兜底。
6.3 模拟重放测试
在测试环境里把消费者组重置(kafka-consumer-groups.sh --reset-offsets),把最近几小时的消息重新消费一遍,观察业务数据是否出现异常。这是验证幂等设计最直接的方法。我几乎在每个核心链路上线前都会做一次重放测试,虽然需要准备测试环境,但远比线上出故障时手动修数据要快。
6.4 事务隔离级别检查
如果启用了事务,检查消费者是否设为 read_committed。没设的话,事务回滚期间的数据会被消费端提前看到,产生“幽灵数据”。这个参数很隐蔽,很多人配置时容易漏。
这些方法有个共性:不迷信中间件配置,用数据判断系统行为。生产环境的故障,大概率出在程序员自己没预料到的边界条件上,而不是 Kafka 没做到它承诺的事情。
“Kafka 怎么保证消费只消费一次”这个问题,我的最终答案也很简单:Kafka 在默认配置下保证的是“不丢但可能重复”;如果要用事务,它能保证“写入多分区不重复依次落”的原子性;“端到端只消费一次”需要业务幂等设计、流处理框架、同库事务等手段组合实现。做技术方案时,优先考虑简单的幂等设计,再按需引入事务和流处理,才是性价比最高的路径。
