你有没有在快递柜前收过一个“重复包裹”?明明昨天签收过了,今天快递员又送来一件一模一样的。你皱着眉头拒收,心里嘀咕:这快递公司怎么搞的?另一边,网上买的生鲜迟迟没到,你催了客服三次,客服说丢件了,重新发一件。你等的海鲜坏了,但至少东西收到了。如果有一家“保价专送”快递,既保证一定送到,又保证绝不重复,你当然愿意多花点钱。 这就是分布式系统中的“消息可靠投递”。消息就像是快递包裹,从生产者手里出发,经由消息队列这个“快递中转站”,最终送到消费者手里。但网络会断、服务会崩、确认会丢,消息到底会不会丢、会不会重复,就是投递语义要回答的问题。本文用“快递公司”的比喻,带你彻底看懂三种投递语义(At-Most-Once、At-Least-Once、Exactly-Once),以及如何在工程里实现它们。

岗位关联

消息可靠投递是分布式系统里绕不开的“履约承诺”。理解三种语义与幂等设计,能帮你在面试中讲清 Kafka/RocketMQ 的高可靠机制,也能在系统设计时给出“既不重复、又不丢失”的稳妥方案。

目录
  1. 可视化演示
  2. 1. 三种投递语义
  3. 2. 至少一次投递
  4. 3. 恰好一次投递
  5. 4. 幂等消费设计
  6. 5. 可靠性权衡
  7. 6. 工程选型
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

演示三种消息投递语义:At Most Once 可能丢失、At Least Once 重试保证不丢、Exactly Once 幂等去重,展示生产者-消费者间的可靠传输机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

At Most Once 最多一次 发送后不确认,可能丢失 高吞吐低保证 适合指标采集、事件通知等可容忍丢失场景

1. 三种投递语义

你去快递公司寄东西,柜员会问:寄平邮、普通快递还是保价专送?平邮便宜,丢了不赔;普通快递承诺必达,丢件会补寄,可补寄可能让你收到两件;保价专送最贵,但签收时门卫会核对单号,同一个包裹绝不重复登记。你掏钱之前,其实是在选择一种“投递承诺”。

在分布式系统里,我们把“寄件人”叫做消息生产者(Producer),“快递公司”是消息队列(Message Queue),“收件人”是消费者(Consumer),而“包裹”就是一条业务消息。生产者把消息发给队列,队列再把它投递给消费者。网络波动、服务重启、进程崩溃,就像快递员迷路、车坏在高速上、包裹被雨淋湿。消息队列能给出多硬的承诺,被抽象成三种投递语义:

At-Most-Once(最多一次):发送后不确认、不重试,消息可能丢失;At-Least-Once(至少一次):发送后必须确认,超时则重发,消息不会丢但可能重复;Exactly-Once(恰好一次):在“至少一次”的基础上引入去重与事务,让每条消息恰好被处理一次。

选服务等级,本质是在签合同。签“平邮合同”,丢了别抱怨;签“普通快递合同”,请做好收两件的心理准备;签“保价合同”,多掏钱买安心。工程师设计系统时,第一件事就是问业务:你这单生意,是包裹,还是合同?

QoS={At-Most-Once,  At-Least-Once,  Exactly-Once}\text{QoS} = \{ \text{At-Most-Once}, \; \text{At-Least-Once}, \; \text{Exactly-Once} \}
QoS 是 Quality of Service(服务质量)的缩写。这个集合就是快递公司菜单上的三种“服务套餐”。
  1. 服务质量等级QoS 表示消息队列对外承诺的投递质量等级,是生产者与消费者之间的“服务合同”条款。
    QoS={⋯ }\text{QoS} = \{ \cdots \}
  2. 三种枚举值集合中包含三种可选的投递语义,它们从弱到强,对应不同的可靠性和成本。
    At-Most-Once<At-Least-Once<Exactly-Once\text{At-Most-Once} < \text{At-Least-Once} < \text{Exactly-Once}
  3. 强弱排序箭头表示可靠性递增,同时实现复杂度和性能代价也递增。
    可靠性 ↑    成本 ↑\text{可靠性} \, \uparrow \;\; \text{成本} \, \uparrow

2. 至少一次投递

加急快递的承诺是“必达”。快递员敲你家门,没人应,他就把包裹带回网点,第二天再送;再没人,再送。这个过程会一直重复,直到你签收为止。可你有没有想过:如果第一次其实送到了,只是回执单被风吹走了呢?快递公司不知道,只会再补送一件。你打开门,看到两个一模一样的包裹。

在消息队列里,这条“回执单”叫 Ack(Acknowledgement,确认回执)。消费者每处理完一条消息,就给队列回一个 Ack;队列收到 Ack 才认为投递成功。如果长时间没收到 Ack,队列就判定“投递失败”,把消息重新发给消费者。这就是超时重发机制。它保证了消息绝不会因为一次网络抖动而丢掉——要么正在处理,要么正在重发的路上。

但副作用也随之而来:如果 Ack 在网络传输途中丢失,队列会重发同一条消息,消费者就会处理两次。假设单次投递成功概率是 \(p\),那么重试 \(n\) 次后,消息最终被处理成功的概率是:

快递员一次次敲门,最终把包裹放到你手里,但可能多放了一箱。“至少一次”这个语义,牺牲的是“不重复”,换来的是“不丢失”。工程上,大部分业务可以接受这种“多收一箱”的烦恼,但前提是——你得有能力辨别哪一箱是多余的。

Pdelivered=1−(1−p)nP_{\text{delivered}} = 1 - (1 - p)^{n}
公式想表达:只要不断重试,失败概率会指数级下降,消息终将被送达,但送达次数可能不止一次。
  1. 单次成功率\(p\) 是一次投递成功的概率,比如 0.9 表示十次里成功九次。
    p=P(单次投递成功)p = P(\text{单次投递成功})
  2. 单次失败率\(1-p\) 是投递失败的概率,重试意味着每次失败后再次尝试。
    1−p=P(单次投递失败)1 - p = P(\text{单次投递失败})
  3. 重试后成功率连续失败 \(n\) 次的概率是 \((1-p)^n\),用 1 减去它就是“至少成功一次”的概率。
    1−(1−p)n1 - (1-p)^n

3. 恰好一次投递

保价专送是怎么做到“绝不重复”的?关键在门卫大爷手里的那个登记本。快递员送包裹来,先报单号;大爷翻开本子一查:哟,这单昨天已经登记过了。大爷挥挥手:这包裹我们收过了,你带回去吧。快递员也乐得轻松,少送一单。这就是“保价件”的精确:你的单号,只会在本子上出现一次。

消息系统实现 Exactly-Once,路数和门卫大爷完全一致。首先,它仍然依赖“至少一次”的重发能力——消息可以重复送;其次,消费者端(或者 Broker 端)维护一张“已处理登记表”,每次投递前先查重,发现相同的消息 ID 已存在,就直接丢弃。这个动作叫幂等消费(Idempotent Consumption)。

如果配合 Kafka 的事务 API 或 RocketMQ 的事务消息,系统还能在“发送消息”和“写入业务库”之间保持原子性:要么都成功,要么都失败。于是整体投递效果可以描述为一个分段决策函数:

门卫大爷的登记本,就是幂等表;快递单号,就是消息 ID。只要登记本记得住、查得快,重复送多少趟都白搭。但别忘了,保价专送也是收费最高的——登记本要存、要查、要维护,这就是 Exactly-Once 的代价。

σ(m)={apply,m.id∉Sseenignore,m.id∈Sseen\sigma(m) =\begin{cases}\text{apply}, & m.\text{id} \notin S_{\text{seen}} \\\text{ignore}, & m.\text{id} \in S_{\text{seen}}\end{cases}
\(S_{\text{seen}}\) 是“已处理消息 ID 集合”,也就是门卫大爷的登记本。查不到就处理,查得到就忽略。
  1. 判断条件\(m.\text{id} \notin S_{\text{seen}}\) 表示当前消息的 ID 还没有出现在已处理表里。
    m.id∉Sseenm.\text{id} \notin S_{\text{seen}}
  2. 首次处理当条件成立,说明是第一次收到这条消息,于是执行真正的业务逻辑。
    σ(m)=apply\sigma(m) = \text{apply}
  3. 重复忽略当条件不成立,说明是重试补发的重复包裹,直接跳过,业务效果等同于只处理一次。
    σ(m)=ignore\sigma(m) = \text{ignore}

4. 幂等消费设计

物业前台有一套自己的“查重登记表”。每个快递员的包裹送进来,前台先问:单号多少?翻一遍登记本,没有,登记,收下;发现已经登记过,直接拒收。这套流程之所以可靠,是因为它有三个要素:唯一的包裹单号、一本查得到的登记表、一个先查后写的动作。

对应到消息系统,就是要设计好三样东西。第一,每条消息都要携带唯一业务 ID,比如订单号、流水号、UUID;第二,要有一张去重表,可以是数据库唯一索引、Redis 的 SETNX 命令,或者业务表里的业务键;第三,处理消息时必须是“先查重、再写入、再返回成功”的顺序,绝不能反过来。

幂等设计还有一个高级形态:状态机幂等。比如订单状态只能从“已支付”流转到“已发货”,重复收到“支付成功”消息时,发现订单已经离开“已支付”状态,就拒绝这次流转。这种幂等不靠查重表,而是靠业务状态的“单向门”。幂等判断的数学表达非常简洁:

物业前台的登记本看起来简单,但在高并发下,每个人都要排队查表、登记,这就考验存储的并发能力。生产环境里常用 Redis 的原子操作或者数据库的唯一索引来扛住压力。设计幂等时记住一句话:没有唯一键的幂等,就像没有单号的快递,谁也不知道这包裹是不是收过了。

Valid(m)=[ MID(m)∉Tableprocessed ]\text{Valid}(m) = [\,\text{MID}(m) \notin \text{Table}_{\text{processed}}\,]
这条式子是个“判决器”:如果消息 ID 不在已处理表里,结果 Valid 为真,允许处理;否则为假,直接忽略。
  1. MID 取值\(\text{MID}(m)\) 取出消息 \(m\) 携带的唯一业务 ID。
    MID(m)=m.bizId\text{MID}(m) = m.\text{bizId}
  2. 集合查询\(\notin \text{Table}_{\text{processed}}\) 判断这个 ID 是否没有出现在已处理记录中。
    Tableprocessed={id1,id2,… }\text{Table}_{\text{processed}} = \{ \text{id}_1, \text{id}_2, \dots \}
  3. 判决结果方括号 \([\cdot]\) 是示性函数,条件为真时等于 1(可处理),为假时等于 0(跳过)。
    Valid(m)=1  或  0\text{Valid}(m) = 1 \;\text{或}\; 0

5. 可靠性权衡

顺丰为什么比平邮贵?因为它有飞机、有冷链、有实时轨迹。你每多要一分可靠性,就要多付一分钱。消息系统也一样,可靠性是一条“用成本铺出来的路”:At-Most-Once 几乎零开销,发完就不管;At-Least-Once 需要存储 Ack、处理超时重发;Exactly-Once 更狠,要维护分布式事务、幂等表、事务日志,吞吐量和延迟双双受损。

这组矛盾是工程上最核心的权衡(Trade-off)。延迟上,Ack 确认机制让每一次投递至少多一个网络往返;吞吐上,每一条消息都要查重写表,写入量翻倍;成本上,事务消息和幂等存储都要额外资源;一致性上,强一致和最终一致的选择直接决定实现复杂度。重试总耗时的模型是:

这就像你给快递公司打电话催件,每催一次,都要先排队听语音提示,再跟客服解释一遍。催得越勤,时间花得越多,包裹却不一定更快到。可靠性的每一档提升,都是用真金白银和系统性能换来的。所以,别一上来就喊“我要 Exactly-Once”——先看看钱包。

Ttotal=Tsend+Nretry×(Ttimeout+Ttransfer)T_{\text{total}} = T_{\text{send}} + N_{\text{retry}} \times (T_{\text{timeout}} + T_{\text{transfer}})
初始发送时间加上每次重试的“等待超时+重新传输”时间。重试次数越多,总耗时越长,系统越慢。
  1. 初始耗时\(T_{\text{send}}\) 是消息第一次从生产者发到消费者所用的时间。
    Tsend=Ttransfer+TqueueingT_{\text{send}} = T_{\text{transfer}} + T_{\text{queueing}}
  2. 单次重试开销每次重试都要先等超时 \(T_{\text{timeout}}\),再重新传输一次 \(T_{\text{transfer}}\)。
    Tretry=Ttimeout+TtransferT_{\text{retry}} = T_{\text{timeout}} + T_{\text{transfer}}
  3. 总开销累计\(N_{\text{retry}}\) 次重试的总耗时等于它们相加,\(N\) 越大,延迟越高。
    Ttotal=Tsend+Nretry⋅TretryT_{\text{total}} = T_{\text{send}} + N_{\text{retry}} \cdot T_{\text{retry}}

6. 工程选型

你寄水果,选顺丰冷链;寄文件,选普通快递;寄一包不值钱的广告传单,选最便宜的平邮就行。业务价值决定了快递等级,消息系统也是一样:没有最优的投递语义,只有最合适的。选型的第一步,是判断业务能不能容忍“丢”,能不能容忍“重”。

日志采集和指标监控,属于“丢了可惜,重复无妨”:日志少一条,统计稍微偏一点;重复一条,去个重就行。这类场景首选 At-Least-Once。订单、支付、库存流水,属于“丢了致命,重复也致命”:扣款消息丢一条,用户白嫖;重复扣一条,用户投诉。这类场景必须上 Exactly-Once。实时推荐、临时通知,属于“丢了拉倒,重复烦人”:推送重复了用户会烦,但这种业务一般选 At-Most-Once,省资源。

选型的数学抽象可以看作给每种语义打分:

回到快递比喻:冷链、航空、保价不是每一个包裹都需要的。真正成熟的架构师,会像精明的行政一样,把每年几百万元快递费的每一分钱花在刀刃上——重要文件走保价,日常物料走平邮,一切都以业务价值为标尺。选消息投递语义,亦然。

Score(s)=α⋅R(s)−β⋅C(s)−γ⋅L(s)\text{Score}(s) = \alpha \cdot R(s) - \beta \cdot C(s) - \gamma \cdot L(s)
对语义 \(s\) 打分:可靠性贡献加分,成本和延迟减分。得分最高的语义就是当前业务的最优解。
  1. 可靠性收益\(R(s)\) 是该语义的可靠性水平,\(\alpha\) 是可靠性的权重,交易系统里 \(\alpha\) 很高。
    Reliability(s)=α⋅R(s)\text{Reliability}(s) = \alpha \cdot R(s)
  2. 成本与延迟惩罚\(C(s)\) 是资源成本,\(L(s)\) 是延迟代价,\(\beta, \gamma\) 是对应的惩罚系数。
    Penalty(s)=β⋅C(s)+γ⋅L(s)\text{Penalty}(s) = \beta \cdot C(s) + \gamma \cdot L(s)
  3. 求最优解对三种语义分别打分,取分数最大者作为工程选型的最终答案。
    s∗=arg⁡max⁡s∈QoSScore(s)s^* = \arg\max_{s \in \text{QoS}} \text{Score}(s)

AI 应用场景

金融支付对账

阶段:数据集成与事务处理

解决的问题:传统账务系统依赖数据库本地事务,但跨系统调用一旦网络抖动,重复扣款和漏单时有发生。AI 风控链路中订单与账务解耦后,消息必须做到“不重不漏”,才能保证账实相符。

具体案例:用户在某支付平台完成一笔 100 元交易,支付服务发出“扣款成功”消息。账务服务如果重复消费这条消息,就会从用户余额里扣两次钱。引入交易号作为幂等键 + 事务消息后,重复投递的消息被消费端去重表拦截,余额只变动一次。

大数据日志采集

阶段:数据管道与特征工程

解决的问题:ELK 或云原生日志系统每天要处理百万级日志,少量重复可以容忍,但丢失会导致监控指标失真。AI 训练样本如果缺失关键日志,模型评估就会偏。

具体案例:容器平台把应用日志发送到 Kafka,由 Flink 消费写入 ClickHouse。使用 At-Least-Once 投递,Flink 端按日志 ID 去重。即使某条日志被重复投递三次,最终写入也只有一次,日志不丢、统计不偏。

电商订单状态机

阶段:业务编排与状态管理

解决的问题:微服务下订单状态流转涉及多个服务,重复的“支付成功”消息可能把订单状态打回,导致“已发货”变回“已支付”。AI 推荐系统读到的订单状态如果错乱,推荐策略也会跟着崩。

具体案例:订单服务把状态变化建模成状态机,每条消息带订单号和目标状态。收到重复消息时,若当前状态已经越过目标状态,直接忽略。这样即使消息重复一百次,订单状态也只会单向前进。

Python 演示

python58 行
class Message:    def __init__(self, mid, payload):        self.mid = mid        self.payload = payload        self.deliveries = 0  class SimpleQueue:    def __init__(self):        self.buffer = []     def enqueue(self, msg):        self.buffer.append(msg)     def dequeue(self):        return self.buffer.pop(0) if self.buffer else None  class IdempotentConsumer:    def __init__(self):        self.seen = set()        self.events = []     def handle(self, msg):        if msg.mid in self.seen:            self.events.append("跳过重复: " + msg.mid)            return        self.seen.add(msg.mid)        if msg.mid == "ORDER-1" and msg.deliveries == 0:            self.events.append("处理中宕机: " + msg.mid)            msg.deliveries += 1            raise RuntimeError("simulated crash")        msg.deliveries += 1        self.events.append("成功处理: " + msg.mid)  queue = SimpleQueue()consumer = IdempotentConsumer()msgs = [Message("ORDER-1", "创建订单"), Message("ORDER-2", "扣减库存")] for m in msgs:    queue.enqueue(m) while True:    msg = queue.dequeue()    if msg is None:        break    try:        consumer.handle(msg)    except RuntimeError:        queue.enqueue(msg) queue.enqueue(msgs[1])msg = queue.dequeue()consumer.handle(msg) for e in consumer.events:    print(e)
目的:模拟消息队列的“至少一次投递 + 幂等消费”。第一条消息在处理中模拟宕机,被重新入队后成功消费;第二条消息被重复投递,但被消费者按 ID 去重跳过。
  1. class IdempotentConsumer:消费者类,用 self.seen 集合模拟门卫大爷的登记本。
  2. if msg.mid in self.seen:查重操作:ID 已存在,说明是重复包裹,直接跳过。
  3. raise RuntimeError("simulated crash")模拟消费者处理到一半宕机,迫使队列重新投递。
  4. queue.enqueue(msg)异常处理中把消息放回队列,这正是 At-Least-Once 的重发逻辑。
  5. queue.enqueue(msgs[1])手动模拟网络抖动导致的重复投递,验证幂等能否兜底。

常见误区

误区我用了 Exactly-Once 语义,消息就一定不会丢,系统就和地震海啸一样坚不可摧。

正解Exactly-Once 解决的是“不重复”,它仍然建立在 At-Least-Once 的重发机制之上,因此要先保证消息不丢,才能谈恰好一次。如果消息在生产者本地就没发出去,或者 Broker 整个集群都烧毁了,再牛的语义也变不出包裹来。

为何常见:因为“恰好一次”听起来级别最高,人们容易把“最强保证”误解为“绝对可靠”的万能钥匙。

误区既然一次失败就重试,那我设置重试一万次,消息不就永远不丢了?

正解重试次数增加确实降低丢消息概率,但会无限放大延迟和重复风险。真正可靠的设计是“有限重试 + 失败告警 + 人工补偿”,而不是无限重试。否则,消费者一挂机,所有消息都堵在队列里,整个系统直接雪崩。

为何常见:人总觉得“用力就行”,忽略了每一次重试都要消耗时间、带宽和存储,系统负担会指数级上升。

误区我在表上建了唯一索引,消息重复就插入失败,这不就幂等了吗?

正解唯一索引只是“去重表”的一种实现,并且只防重复写入,不防重复业务动作。比如消息的含义是“给用户加 100 积分”,加唯一索引只能让两条 SQL 一条失败,但如果你把两条消息都做了“先查有没有积分记录”再插入,逻辑复杂时依然可能重复。真正可靠的幂等必须把唯一业务 ID、写入动作和返回结果绑定成一个原子操作。

为何常见:数据库唯一索引是大家最熟悉的“查重工具”,容易以偏概全,忽略业务动作本身的幂等性。

误区我选了 At-Least-Once 语义,队列保证消息不丢,剩下的我什么都不用管了。

正解At-Least-Once 只保证“至少一次”,意味着重复消息是必然的。如果不做幂等处理,重复下单、重复扣款、重复发短信就会层出不穷。正确姿势是把它当成“默认投递等级”,然后靠消费端幂等消化重复。

为何常见:初学者容易把“语义”当成平台的铁律,忽略它只是“承诺”,不是“结果”。

面试题

Q1. 哪种投递语义保证消息“至多被处理一次”?

单选题 · easy

  • A.至多一次 At-Most-Once← 正确答案
  • B.至少一次 At-Least-Once
  • C.恰好一次 Exactly-Once
  • D.异步非阻塞投递

答案:A。At-Most-Once 发送后不确认、不重试,消息可能丢失,因此最多被处理一次。B 会重试所以可能多次,C 会去重所以恰好一次,D 是发送方式而非投递语义。

Q2. At-Least-Once 语义最本质的特点是?

单选题 · easy

  • A.消息一定不会重复
  • B.消息可能会重复← 正确答案
  • C.消息一定不会丢失
  • D.消息既不丢失也不重复

答案:B。At-Least-Once 通过 Ack 确认和超时重发保证消息不丢,但 Ack 丢失会触发重复投递。因此“可能重复但不会丢”是它的身份证。A、D 描述的是 Exactly-Once,C 只描述了它的一半。

Q3. 消息队列长时间未收到消费者的 Ack,会执行什么操作?

单选题 · mid

  • A.丢弃该消息
  • B.重发该消息← 正确答案
  • C.阻塞所有生产者
  • D.清空整个队列

答案:B。Ack 超时说明消费者可能没收到或没处理完,队列把消息重新入队投递,这就是 At-Least-Once 的“补寄”。A 是 At-Most-Once 的行为,C、D 都是灾难性操作,不会发生。

Q4. 实现幂等消费时最核心的依据是什么?

单选题 · mid

  • A.增加消费者线程数量
  • B.唯一业务标识符← 正确答案
  • C.消费者所在服务器 IP
  • D.消息队列的分区编号

答案:B。幂等靠“同一个消息看起来像同一个包裹”来查重,唯一业务 ID(订单号、流水号)就是包裹单号。线程数、IP、分区号都不能唯一标识一条业务消息,无法作为去重依据。

Q5. 下列哪一项最适合作为消息幂等键?

单选题 · mid

  • A.订单号或流水号← 正确答案
  • B.消费者进程 ID
  • C.服务器 IP 地址
  • D.消息队列分区号

答案:A。幂等键必须唯一且与业务绑定。订单号在全局唯一并能代表一次业务操作;进程 ID、IP、分区号都无法区分“这是哪一笔业务”。

Q6. Kafka 实现 Exactly-Once 的核心机制是什么?

单选题 · hard

  • A.事务与幂等机制← 正确答案
  • B.多副本强同步
  • C.消费者组均衡分配
  • D.高吞吐批量发送

答案:A。Kafka 通过幂等 Producer(PID+序列号)和事务 API(原子写入多个分区)共同实现跨分区精确一次。多副本强同步只是高可用,不是精确一次;消费者组均衡和批量发送与 Exactly-Once 无关。

Q7. 关于 At-Most-Once 语义,下列哪种说法是正确的?

单选题 · hard

  • A.消息可以重复投递
  • B.消息可能被丢弃← 正确答案
  • C.消息每次必能送达
  • D.消息可靠性级别最高

答案:B。At-Most-Once 发完不管结果,网络一抖动消息就丢了,因此可靠性最低。A 是 At-Least-Once 的表现,C 和 D 与它的行为完全相反。

Q8. 下列哪些场景适合使用 At-Least-Once 投递?

多选题 · mid

  • A.日志采集与统计← 正确答案
  • B.银行账户扣款
  • C.验证码通知发送← 正确答案
  • D.库存精确扣减

答案:AC。日志多一条少一条影响很小,验证码重复发送也只是打扰用户,两者都可以容忍重复,适合至少一次。银行扣款和库存扣减不能多扣一次也不能少扣一次,必须用 Exactly-Once。

Q9. 实现消费幂等的常用方法有哪些?

多选题 · mid

  • A.建立去重记录表← 正确答案
  • B.使用 Redis SETNX← 正确答案
  • C.依赖本地事务表← 正确答案
  • D.增加 CPU 核数提升并发

答案:ABC。去重表(数据库唯一索引)、Redis SETNX 原子操作、本地事务表都是经典幂等方案,它们能保证“先查重再写入”的原子性。增加 CPU 核数只提升性能,不解决重复问题。

Q10. 关于 Exactly-Once 与 At-Least-Once 的关系,正确的有哪些?

多选题 · hard

  • A.前者通常以后者为基础← 正确答案
  • B.前者需要幂等操作配合← 正确答案
  • C.前者可以完全取代后者
  • D.前者比后者代价更高← 正确答案

答案:ABD。Exactly-Once 必须建立在至少一次的重发机制上,并通过幂等和事务消除重复,所以 A、B 正确;它的成本显著更高,D 正确。没有一种语义能包打天下,“完全取代”是绝对化的错误说法。