想象你经营一家开在写字楼里的热门餐厅:一楼收银台负责接单收钱,三楼厨房负责出菜。如果收银员冲着天花板喊一句“一份宫保鸡丁”,三天后八成会变成“宫保鸡丁?没听见”。于是老板立了一条规矩:收银员接单时,必须用同一块夹板上的复写纸,一次写下两份单——一份给顾客,一份塞进厨房票架。只要夹板上的笔没坏,菜和单永远成对出现。这套“票根系统”,就是今天要讲的本地消息表:用同一个数据库事务,把“业务操作”和“待发消息”绑在一起;再由传菜员定时翻票架、把单子送往厨房;厨房看单号防止重复做菜;打烊后三方对账,保证一道菜都不少。这套机制不追求“瞬间一致”,而是接受“晚一点一定一致”,这就是分布式系统里大名鼎鼎的最终一致性。
本地消息表是分布式系统里性价比最高的最终一致性方案之一。掌握它,你就能独立设计“业务+消息同库、异步投递、幂等消费”的可靠链路,在微服务架构实战与面试中拿出有说服力的方案。
目录
可视化演示
演示业务与消息同事务写入、定时扫描投递MQ与幂等消费闭环,理解本地消息表保障分布式最终一致性的机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
同事务写消息 业务+消息 原子落库 txn(biz,msg) 一个事务保证原子
问题背景:一张被喊丢的菜单
餐厅刚开业时,老板觉得装传菜系统太贵,让收银员直接对厨房喊单。周末爆满,喊声淹没在炒勺声里,顾客等了四十分钟,厨房却查无此菜,投诉率飙升。老板被迫思考:到底怎样才能让“楼下收钱”和“楼上做菜”这两件跨楼层的事,准确对上号?
在分布式系统里,“楼下”和“楼上”是两个独立服务,订单库和支付库分属不同数据库,单个数据库的事务管不到别人家。最经典的办法是两阶段提交(2PC):先让所有参与者“预提交”,都同意后再“正式提交”。这就像两个楼层各派一名主管,举着对讲机反复确认“你可以吗?我可以”。一旦有人手机没电,整条链路僵住,所有桌都别想上菜——同步阻塞、协调者单点、可用性差,是 2PC 的致命伤。
本地消息表的思路完全换了一个方向:不追求“同一瞬间达成一致”,而是先保证“我方这一亩三分地绝对可靠”,再靠异步任务把差异慢慢抹平。它放弃强一致,接受一个可量化的“最终一致窗口”,换取系统的可用性和吞吐。业务上只要窗口足够短、失败可监控可补偿,用户几乎无感。
节点越多,两阶段提交越容易整体失败,这个直觉可以用一个公式表达:
所以老板明白了一个道理:与其让两个主管举着对讲机互等,不如先把单子用复写纸锁死在夹板上,后面慢慢送。跨楼层的问题,从来不是“喊得响不响”,而是“有没有一张跑不掉的票根”。
- n 表示参与方数量分布式事务涉及的节点数,例如订单服务、支付服务、库存服务,每个参与者都要点头,事务才算成功。
- p_i 表示第 i 个节点的失败概率每个服务可能宕机、超时或网络抖动,失败概率各不相同。
- 1 - p_i 是第 i 个节点成功的概率成功概率等于 1 减去失败概率,这是概率论的基本关系。
- 连乘符号 \prod把所有节点的成功概率乘起来,得到“所有人都成功”的整体概率。
核心原理:同一块夹板上的复写纸
老板定下铁律:接单时,收银员必须把“顾客点了什么”和“这张单子已记录”同时写在复写纸上,一次落笔,两份都清晰。笔没水了?两份一起作废重写。这就是原子性:不许出现“单子记了菜没记”,或者“菜记了单子没记”的尴尬。
映射到技术:业务服务在操作自己的数据库时,把“业务变更”(如插入订单)和“往本地消息表插入一条待发消息”放进同一个数据库本地事务。消息表和业务表同库,共享同一个事务管理器,由数据库保证原子提交。
流程很朴素:开启事务 → 写业务记录 → 写消息记录 → 提交。提交成功,业务和消息同时对外可见;提交失败,一起回滚,连“消息曾经存在过”的痕迹都没有。这正是“本地消息表”名字的由来:消息不是先发给 MQ,而是先稳稳落在业务库的表里,事后由任务捞出来投递。它天然保证了“业务成功则消息必不丢”这一条最重要的可靠性。
复写纸的妙处在于:顾客和厨房各拿一份,互不打扰。同理,业务服务写完即返回“下单成功”,不用等支付、库存等服务同步响应——解耦换来的是响应更快、可用性更高,代价只是“别人晚一点知道”。
- T 表示一个数据库本地事务事务是数据库的“打包批量操作”,要么全做,要么全不做。
- 业务写入例如插入一条订单记录,这是对外可见的业务结果。
- 消息写入向本地消息表插入一条“待发”消息,记录着后续要通知谁、干什么。
- 并集符号 \cup表示这两类写入被装进同一个事务包里,共享命运。
- 要么全部提交,要么全部回滚提交成功则两条都生效;任何一步失败则两条一起消失,绝不留下半截状态。
消息表设计:一张票根上要印哪些栏
复写纸不能是白纸,老板设计了栏目:单号、桌号、菜品、金额、状态(待做/已出餐/已上桌)、重试次数、备注。栏太少,传菜员没法判断;栏太多,写字都费劲。
消息表字段也遵循同样的逻辑:主键 id;业务唯一键(如订单号),它是幂等的锚点;消息内容(JSON 字符串);状态(待发/已发/完成/死信);重试次数;下次投递时间;创建与更新时间。其中“状态 + 下次投递时间”要建联合索引,定时扫描就靠它命中。
状态机要严格单向:待发 → 已发 → 完成。投递失败不倒退,而是留在“待发”并累加重试次数,超过上限进入“死信”,由人工或补偿任务兜底。业务键要建唯一索引:同一订单无论产生多少条消息,业务语义上只能有一个“锚”,否则消费端根本无法去重。
老板还发现,若在票根上印“本单唯一编号”,就算单子送重复了,厨房也能认出来。消息表里的“业务唯一键”就是这个编号,它是后面幂等消费的第一道保险。
- S 表示消息状态消息表里每条记录都有一个状态字段,标记它走到了哪一个环节。
- 待发 \rightarrow 已发 \rightarrow 完成状态只能按这个方向流转:先待发,扫描投递后变已发,消费方处理完变完成。
- R 表示重试次数每次投递失败,重试次数字段加一。
- R_{\max} 表示最大重试上限超过上限的消息进入“死信”状态,等待人工介入,防止无限循环。
投递机制:传菜员定时翻票架
票根写好了,不能等顾客自己上厨房取。老板雇了一个传菜员,每 5 分钟巡视一次票架,每次最多抽 20 张送到厨房窗口,并在票根上盖“已送”章。如果厨房说没收到,传菜员就在重试次数上加一,按“1 分钟、2 分钟、4 分钟……”的节奏延长后再来。
技术实现:一个定时任务(Quartz、XXL-Job 或自研调度组件)周期性扫描消息表,选出 status=待发 且 下次投递时间 <= now 的记录,按 limit 分批取出,调用 MQ 发送接口。发送成功,状态置为“已发”;失败,则记录重试次数,并按指数退避更新下次投递时间。
工程要点:线上常常部署多个服务实例,如果每个实例都跑定时任务,同一消息可能被并发捞起。必须用行锁(SELECT …… FOR UPDATE)或乐观锁(update …… where status='待发')保证只有一台机器投递成功。此外,扫描要加 limit,避免一次捞几十万条把数据库拖垮;批大小要可配置,配合分片一起使用。
传菜员的节奏也很讲究:翻太勤,空转浪费;翻太慢,顾客饿肚子。退避重试就是让“越失败,越要冷静”,给下游系统喘息的时间,而不是用重试把对方打垮。
- t_{\text{当前}} 表示本次重试的时刻记录当下时间,作为计算的起点。
- b 表示基础间隔初始等待时长,例如 1 分钟。
- 2 的 R 次方重试次数越多,指数越大,等待时间成倍拉长,避免把系统打爆。
- t_{\text{下次}} 表示下次重试的时刻当前时间加上退避时长,得到下次扫描该再试的时间点。
幂等消费:厨房认单不认人
传菜员手一滑,把同一张单子送进厨房两次。第一次厨师做了菜,第二次怎么办?如果无脑再做一份,顾客会收到两份宫保鸡丁。老板的规矩是:先看单号,已出过餐的单子直接塞进废纸篓。认单不认人,就是幂等。
MQ 的消息投递语义是“至少一次”:网络超时、客户端重启、服务端重发,都会导致同一条消息被消费方收到多次。所以消费端绝不能假设“我收到的每条消息都是第一次”。唯一的防线,是业务逻辑自身的幂等。
常见做法:消费端建一张去重表,把消息里的业务唯一键(订单号/流水号)作为唯一索引,处理前先插入,冲突则说明已处理,直接 ACK 跳过;或者利用业务表本身的唯一约束(如支付流水号)。流程总结为:查重 → 业务处理 → 记录已处理 → 确认消费。四个步骤要和业务操作放在同一个本地事务里,才能保证“处理了就一定记下来”。
厨房认单号,顾客才能安心。幂等看似是消费端的“额外负担”,实则是整个方案的最后一道闸门:投递可以重发、扫描可以重复,只要闸门在,系统永远只会产生一次业务效果,最终一致就不会变成“最终混乱”。
- 效果(第 1 次处理)消息第一次被消费时产生的业务结果,比如给用户加了 100 积分。
- 效果(第 N 次处理)第 N 次重复消费同样的消息,业务结果必须和第一次完全一样。
- 等号表示两边结果等价,这正是幂等定义的核心。
- \forall N \ge 1对任意正整数 N 都成立,无论重投多少次,效果都不变。
性能优化:一张票架变成一排票架
餐厅火了,一天一万桌,一个票架塞满单子,传菜员翻找都要排队。老板把票架按楼层和区域拆成十几个,传菜员分组负责,翻单速度立刻提上去。这就是分片。
高并发下,消息表会迅速膨胀,单表扫描慢、索引大、写入变热。优化三板斧:第一,联合索引 + 分批 limit,让扫描走索引而不是全表;第二,分片,按订单号哈希或用户 ID 拆成多张表,每张表独立扫描投递;第三,归档,把“完成”状态的老消息定期迁移到历史表,缩小热表。
分片不是越多越好:分片键必须和业务查询模式对齐,否则某个分片成为热点,总吞吐被最短的木板卡住。多线程扫描时,不同分片天然隔离,不会互相竞争行锁;同一个分片内部仍要加锁防重投。容量监控要盯着每个分片的积压量、扫描延迟,而不是只看总量。
老板的经验是:拆票架之前,先数清楚哪个区域单量最大。技术同理,分片之前必须摸清业务键的分布,把热键打散,否则分片再多,也挡不住某个爆款桌把对应票架挤爆。
- QPS_{\text{单片}}单个分片每秒能处理的扫描与投递消息数,衡量单个票架的处理速度。
- N_{\text{片}}分片总数,消息表按业务键拆成了多少张表。
- 乘号 \times总吞吐约等于单分片吞吐乘以分片数,分片越多,并行度越高。
- 受热点分片约束如果某个分片写入特别集中,它会成为瓶颈,总吞吐就被它卡住。
生产实践:打烊后的三方对账
每天打烊,老板做三件事:看票架还有没有积压的单,翻重试次数异常高的票根,把点菜单、出餐记录、收银记录三方比对。发现“有单没菜”立刻补做,“有菜没单”追查谁漏了单。这套日常,就是生产的监控与对账。
上生产前必须配好监控:消息表积压量、扫描任务延迟、发送失败率、重试分布、死信数量。任何指标异常都应告警。更关键的是定期对账:业务库里的真实业务记录、消息表记录、MQ 消费方的处理结果,三方比对,发现缺口用补偿任务重新投递。因为本地消息表只能降低丢失概率,不能做到绝对零丢失,对账是最后的安全网。
经典故障复盘:某平台订单服务改造本地消息表后,定时任务误配成每分钟全表扫描,扫描 SQL 又没走索引,数据库 CPU 被打满,业务写入连带变慢。对策是强制 limit、加联合索引、按分片并发扫。另一个案例是双实例定时任务未加锁,同一条支付成功消息被投递两次,最终靠消费端幂等键拦截,用户感知无异常——这正好说明幂等设计不是锦上添花,而是防线。
老板的结论写在墙上:“可以慢,不能错;可以晚,不能丢。”本地消息表买到的是可靠性,付出的代价是一段可衡量的时间差。架构师要做的,是度量这个窗口、监控这个窗口,然后把窗口压缩到业务能接受的范围之内。
- t_{\text{提交}}业务和消息同库提交的时刻,也就是点菜单写下的瞬间。
- t_{\text{完成}}消费方处理完消息、状态变为“完成”的时刻,相当于菜最终端上桌。
- \Delta t两个时刻之差,即“最终一致窗口”的长度,这段时间里两边数据是不一致的。
- \mathbb{E}[\Delta t] \le \text{SLA}平均迟到时间要小于业务可接受的服务水平协议,例如“90% 的订单 1 秒内完成对账”。
AI 应用场景
场景 1:电商下单与库存扣减
阶段:数据一致性与可靠性工程(分布式事务)
解决的问题:传统下单流程先写订单再调库存服务,要么订单成功但库存没扣(超卖),要么扣了库存订单却失败;用强一致 2PC 又太慢。本地消息表让订单与“扣库存消息”同库提交,后台异步投递,库存服务幂等扣减,既防超卖又不阻塞下单。
具体案例:某生鲜电商秒杀场景,订单服务把“写订单表”和“写本地消息表”放进同一事务,定时任务把消息推送至库存服务;库存服务按订单号幂等扣减,重复消息直接 ACK。大促峰值下订单成功率和库存准确率双双达标。
场景 2:支付回调与积分发放
阶段:消息驱动的最终一致性
解决的问题:支付成功后要给用户加积分,涉及支付系统和积分系统两个服务,直接 RPC 同步调用会因积分服务抖动导致“钱扣了分没加”。用本地消息表,支付成功回调里把流水写入和“加积分消息”写入放进同一事务,异步投递,积分服务幂等发放。
具体案例:某银行 App 信用卡还款返积分,每笔还款生成一条“返积分”本地消息,定时投递到积分服务;积分服务用“还款流水号”做唯一键,重复投递不重复发放,对账时零差错。
场景 3:搜索索引与数据同步
阶段:数据管道与异步构建
解决的问题:商品信息变更后需要同步到搜索引擎或数仓,传统全量重建慢且成本高;本地消息表把“商品变更事件”和业务更新绑定,增量同步,保证搜索数据和业务数据最终一致。
具体案例:某零售平台商品服务每次改价,同库写入一条“商品变更消息”,同步任务扫表后推送至 ES 索引构建服务,消费者按商品 ID 幂等更新索引,白天改价五分钟内可被搜索到。
Python 演示
# -*- coding: utf-8 -*-# 模拟数据库: 本地消息表message_table = []msg_id_counter = 0 # 消费端幂等去重集合(模拟去重表)processed_key_set = set() # 模拟消息队列(投递目标)outbox = [] def create_order_with_message(order_id, amount): """核心: 业务操作与消息写入在同一个本地事务中原子完成""" global msg_id_counter # 模拟业务执行结果, 失败则两者都不落库 business_success = True if business_success: msg_id_counter += 1 message_table.append({ 'msg_id': msg_id_counter, 'order_id': order_id, 'amount': amount, 'status': 'PENDING', # 待发 'retry': 0 }) return True return False def scan_and_deliver(batch_size=2): """定时任务: 扫描待发消息, 模拟发送到 MQ""" pending = [m for m in message_table if m['status'] == 'PENDING'] for msg in pending[:batch_size]: msg['status'] = 'SENT' # 已发 outbox.append(msg) def consume(msg): """消费端: 先幂等校验, 再执行业务""" key = msg['order_id'] # 重复投递: 业务键已存在则直接跳过 if key in processed_key_set: print(' 幂等拦截: 订单 %s 已处理, 跳过重复消费' % key) return processed_key_set.add(key) print(' 处理订单 %s, 扣款金额 %.1f 元' % (key, msg['amount'])) msg['status'] = 'DONE' # 完成 def run(): print('== 第一步: 两笔订单的业务与消息同库提交 ==') create_order_with_message('A001', 99.9) create_order_with_message('A002', 199.0) print('== 第二步: 定时扫描并投递到 MQ ==') scan_and_deliver(batch_size=5) print('== 第三步: 消费端处理(含重复投递) ==') for msg in list(outbox): consume(msg) consume(msg) # 模拟 MQ 重复投递同一条消息 print('== 第四步: 新订单产生, 下一轮扫描重试 ==') create_order_with_message('A003', 59.0) scan_and_deliver(batch_size=5) consume(outbox[-1]) print('== 本地消息表最终状态 ==') for msg in message_table: print(' ', msg) if __name__ == '__main__': run()
message_table.append({把一条消息记录追加进本地消息表,只有业务成功才会执行这步if business_success:模拟业务执行成功才写消息,失败则两条都不落库for msg in pending[:batch_size]:分批取出待发消息,避免一次捞太多压垮数据库msg['status'] = 'SENT'投递成功后,状态从“待发”流转为“已发”if key in processed_key_set:幂等校验:业务键已处理过则直接跳过msg['status'] = 'DONE'消费完成后状态流转为“完成”,闭环收尾
常见误区
误区我以为本地消息表就是 MQ 的一种,业务直接把消息写到表里就等于发到了消息队列,不用再引入 RocketMQ 或 Kafka 了。
正解本地消息表只是业务数据库里的一张普通表,它的职责是“可靠地暂存待发事件”,本身不具备消息队列的任何能力——不削峰、不广播、不管理消费位点。真正投递还是要发到 MQ,消息表是“生产者和 MQ 之间的保险柜”。就像票根夹板不是厨房,只是把单子可靠送到厨房的临时载体。
为何常见:名字里带“消息”两个字,又确实承担了发送前的暂存功能,很容易被误当成队列产品。
误区只要业务和消息同库提交,消息就永远不会丢,可以完全不用考虑对账和补偿了。
正解同库提交只保证了“业务成功则消息必落库”,但从表里捞出来投递到 MQ、再到消费方处理的整条链路,仍可能丢:投递失败、MQ 数据丢失、消费方处理崩溃。所以必须配合重试、死信、定期对账和补偿任务,才能把丢失率压到可接受范围。票根是写下了,但传菜员摔一跤把纸掉进水沟,菜照样上不了桌。
为何常见:只看到“同库原子写”这一环,把局部可靠性当成了全局可靠性。
误区用了本地消息表,订单和支付就能像单库事务一样,任何时刻两边都一模一样。
正解本地消息表买入的是最终一致性:业务提交后,消息可能几秒甚至几分钟后才被消费,中间存在一个“不一致窗口”。它只承诺“最终会一致”,不承诺“此刻就一致”。点菜单已经写好但菜还没出锅,顾客此刻确实没吃到——这是设计上主动接受的取舍。
为何常见:把“可靠”和“强一致”画等号,没理解原子性只发生在本地库,跨库的同步依然靠异步。
误区我在去重表上建了唯一索引,重复消息插入会失败,所以业务就一定不会重复处理。
正解唯一索引只能拦截“重复插入”这一种路径。如果业务副作用发生在插入之前(如先加库存再写去重记录),或插入失败时业务已经执行,索引就拦不住。正确做法是把“查重 + 业务处理 + 记录已处理”放进同一个本地事务,让去重和业务同生共死。厨房光有单号清单不够,还必须保证“看一眼单号再下锅”和“在清单上打勾”是连着完成的。
为何常见:把数据库约束当成业务逻辑,忽略了执行时序问题。
误区定时扫描间隔越短,消息投递越快,一致性窗口越小,所以干脆 1 秒扫一次全表。
正解扫描是要花钱的:全表扫描不命中索引会打满数据库 CPU,反而拖慢业务写入,造成雪崩。正确做法是联合索引 + 分批 limit + 分片并行,扫描频率配合批大小做容量规划,而不是一味求快。传菜员每分钟跑一趟空票架纯属浪费体力;但高峰期每 5 分钟才跑一趟,票架堆到地上同样糟糕。
为何常见:直觉认为“越快越好”,忽略了扫描本身对数据库的巨大消耗。
面试题
Q1. 本地消息表方案中,业务操作与消息记录之间依靠什么保证原子性?
- A.消息队列的事务消息
- B.同一个数据库本地事务← 正确答案
- C.跨库的分布式事务锁
- D.Redis 的原子操作命令
答案:B。本地消息表的核心是“同库”:业务表和消息表在同一个数据库里,共享同一个本地事务,提交则同时生效,回滚则同时消失。MQ 事务消息是另一种方案,跨库锁和 Redis 都无法提供数据库级别的原子提交。就像复写纸的两联必须同一次落笔,而不是两个本子各写各的。
Q2. 本地消息表方案最终提供的一致性类型是?
- A.强一致性
- B.线性一致性
- C.最终一致性← 正确答案
- D.因果一致性
答案:C。本地消息表允许业务提交后存在一个“消息还没被消费”的窗口,之后通过扫描投递、幂等消费达到一致,因此是最终一致性。强一致要求任意时刻读取都相同,线性一致和因果一致是更强的模型,都不是本方案的目标。
Q3. 定时扫描“待发”消息时,最合理的取数方式是?
- A.一次全表扫描全部取出
- B.按状态索引分批取出← 正确答案
- C.将消息删除后重新插入
- D.随机抽查若干条即可
答案:B。待发消息量大时,一次捞全部会拖垮数据库。正确姿势是利用“状态 + 下次投递时间”联合索引,按 limit 分批取出并逐批处理。全表扫描无索引、先删后插破坏一致性、随机抽查会漏消息。
Q4. 多个服务实例同时部署扫描任务时,防止同一条消息被重复投递的常用手段是?
- A.扫描时对消息行加行锁← 正确答案
- B.增加 MQ 消费端并发
- C.缩短定时扫描的间隔
- D.增大每次批量的条数
答案:A。多实例并发扫描时,SELECT …… FOR UPDATE 能锁定待处理行,让其他实例拿到同样的行时被阻塞或跳过,保证一条消息只有一个投递者。提高并发、缩短间隔、增大批量反而会加剧竞争。传菜员甲翻票架时,传菜员乙不能同时抽走同一张单。
Q5. 消息投递失败后,推荐采用哪种重试策略?
- A.固定一秒重试一千次
- B.指数退避并设最大值← 正确答案
- C.失败后立即无限重试
- D.直接丢弃不再投递
答案:B。指数退避(1 分钟、2 分钟、4 分钟……)让下游有恢复时间,并设置最大重试次数,超出后进入死信待人工处理。固定高频重试会打爆下游,立即无限重试可能放大故障,直接丢弃则造成业务丢失。
Q6. 消费端做幂等处理时,最可靠的判断依据是?
- A.消息本身的全部内容
- B.业务唯一键如订单号← 正确答案
- C.MQ 的投递时间戳
- D.消费实例的机器 IP
答案:B。幂等必须以业务唯一键(订单号、流水号)为锚:同一订单的重复消息业务键相同,才能被识别并拦截。消息内容可能因重试而略有不同,时间戳和 IP 每次都可能变,都不具备“业务上唯一”的语义。
Q7. 相比两阶段提交,本地消息表方案的优点包括?
- A.不存在同步阻塞← 正确答案
- B.系统可用性更高← 正确答案
- C.能保证业务强一致
- D.工程实现更简单← 正确答案
答案:ABD。本地消息表提交完本地事务就返回,消费者异步处理,不存在 2PC 的同步阻塞;参与者故障不会拖死全局,可用性更高;实现也简单。但它只保证最终一致,C 错误。就像餐厅不怕传菜员迟到,只要单子不会丢。
Q8. 高并发下本地消息表性能优化的有效手段包括?
- A.按业务维度分片存储← 正确答案
- B.完成消息定期归档← 正确答案
- C.提高全表扫描频率
- D.设计联合索引查询← 正确答案
答案:ABD。分片把单表拆成多张独立表并行扫描,归档把已完成数据迁走缩小热表,联合索引让扫描走索引而非全表。提高全表扫描频率而不建索引只会把数据库打满,C 错误。拆票架、清旧单、编索引,才能跑得动高峰。
Q9. 消费端必须处理幂等,是因为?
- A.MQ 至少一次可能重复← 正确答案
- B.扫描重试可能重发← 正确答案
- C.业务要求强一致
- D.网络超时触发重发← 正确答案
答案:ABD。MQ 默认至少一次投递,扫描端重试和网络超时重发都会让消费方收到重复消息,所以幂等是刚需。C 说反了:本地消息表恰恰接受最终一致,而不是要求消费端强一致。重复投递是常态,幂等是消费端的“安全带”。
Q10. 某订单服务双实例均运行扫描任务,且扫描逻辑未加任何锁。最可能出现的线上后果是?
- A.消息表数据意外丢失
- B.同条消息被重复投递← 正确答案
- C.数据库连接池自动扩容
- D.本地事务被强制回滚
答案:B。两个实例同时捞起同一条待发消息并都投递,消费端会收到两条,只能靠幂等键拦截避免业务重复。消息表数据不会因并发扫描而丢失,连接池扩容和事务回滚与锁缺失没有直接关系。这提醒我们:扫描端加锁 + 消费端幂等,两道防线缺一不可。