Java EE 企业应用 31:消费重投、去重与死信
处理过,不等于已经确认
订单通知任务从 outbox 发到队列后,消费端可能写好本地收据,却在 JMS 提交前回滚;再投递必须被设计进去。当前消息体实际只有十进制 event ID,本地仅写收据,并没有“通知结果”副作用。另一个尚未实现的路径是永远无法解析的旧版本消息,若无限重试便会占住队列。数据库收据、JMS 重投、外部通知真正生效,分别需要证据。
本篇源码、迁移、脚本与测试可从固定版本完整归档取得;版本 1b08ada,SHA-256 见源码清单。
已实现的接收入口是 BrokerLabResource.receiveOne:只在 JAVAEE_DEMO_MODE=true 的回环实验服务中由 HTTP 显式调用,使用 JMSContext.SESSION_TRANSACTED 同步接收。它依赖 server-messaging.xml 的内嵌队列与 003 收据迁移;普通 server.xml 不含消息配置。当前没有 MDB、无毒消息隔离/死信地址,也没有真实邮件或外部供应商调用。
把重复投递转成一次业务结果
当前消息体只含 notification_outbox.id 的十进制文本。同步接收器解析 event ID,以 INSERT INTO notification_receipt(event_id,tenant_id) SELECT id,tenant_id FROM notification_outbox WHERE id=? ON CONFLICT(event_id) DO NOTHING 记账;收据表的唯一键是 event_id bigint PRIMARY KEY,没有 task_id、effect_type、payload_hash 或订单业务效果表。相同事件的消息可以送达两次,但后一次 newReceipt=false,这验证的是本地收据去重而非“供应商通知仅发生一次”。JMSRedelivered 和 JMSMessageID 不能替换稳定 event ID;当前脚本也没有断言这些 JMS 头字段。
1 | |
当真实副作用是发给供应商的 HTTP 请求时,本地收据无法与供应商系统的提交做原子事务。请求可能成功但回包丢失;“先写收据”可能漏通知,“后写收据”可能重复通知。应传稳定幂等键并核对供应商侧结果;没有对端支持,就把未知结果标记为 UNKNOWN 等待查询或人工核对,而不是宣称端到端恰好一次。第 32 篇再对这个外部边界单独验收。
当前同步入口在 recordReceipt(eventId) 提交数据库更新后,可通过 abortAfterRecord=true 显式回滚 JMS 本地事务并返回 HTTP 500。证据中 event 5 的收据先为 1;随后再次同步接收同一 event 返回 newReceipt=false,重启后的接收也返回 false。这里未建立数据库与消息队列的两资源 XA;对未来 MDB 的容器确认、事务回滚和重投行为必须另行配置、测试,不能把客户端 JMSContext 的显式 commit()/rollback() 或 CLIENT_ACKNOWLEDGE 推广成 MDB 保证。Jakarta Messaging 没有统一死信地址与固定重投次数。
确认与数据库提交不能靠口头约定
未来 MDB 的 onMessage() 若参与容器事务,消息资源与数据库资源是否属于同一事务,要由实际激活配置和资源参与情况决定;不能仅凭“容器确认”四个字推断提交顺序。若 broker 与数据库都参加同一 XA 事务,必须连同事务日志与资源恢复配置一起验收;两资源在进程里同时调用不等于分布式原子性。如果只有数据库本地提交、消息另行确认,提交后而确认前退出会使同一业务任务重新出现。反过来,先确认消息、再写收据,一旦数据库失败便可能永久丢失待处理任务。此处保留稳定 event ID 与本地收据,不能将它们合称“恰好一次交付”。
回调的抛错行为也不能只靠异常类名推断:选用容器事务时,应在隔离适配器上检验 onMessage() 返回、抛出运行时异常、显式回滚及服务器强停之后是否重投,消息头中是否设置 JMSRedelivered 和实际投递计数。确认模式 CLIENT_ACKNOWLEDGE 适用于另一类客户端接收契约,不能复制到 MDB 并声称设置了局部确认。对两条携带相同业务 ID 的独立消息,即使两条 JMSRedelivered 都是 false,业务去重仍需生效。消费时间线必须同时记录 MDB 开始/结束、DB 收据提交和 broker 中消息的终态。
目前 notification_receipt 已存在,事件 ID 是主键;recordReceipt 对冲突回查收据是否存在,但不校验 payload 摘要或已存在行与消息声称的租户是否一致。更强的业务效果账本需将收据和对应副作用在同一数据库事务内更新,校验订单/租户/版本及重复消息载荷一致性;如果副作用是外部调用,还须远端幂等/结果查询。FUTURE/NOT_RUN:新的业务效果表至少需要 (tenant_id, task_id, effect_type) 唯一键,以及 order_id、payload_hash、完成时间;这些字段不是 003 的组成部分。不要在没有新迁移、载荷生成和事务验收的当前库里直接查询或写入它们。
当前收据只证明同一 event ID 的本地收据最多一行。上面的未来新表及事务方案也只能在实际写入业务效果并验收后证明对应本地效果;如果邮件或供应商 HTTP 调用已经发生,数据库事务回滚不了外部行为。FUTURE/NOT_RUN:需对端幂等键、结果查询与未知结果的人工核对,不能因本地唯一键便宣称整条链路只执行一次;UNKNOWN 也不是当前 003 表的状态列。
毒消息与业务失败是两类告警
schema 版本不受支持、必需字段缺失和签名/租户验证失败可以被判为不可处理输入;供应商临时 503 或数据库连接不可用是可能恢复的故障。两类错误一律以同样间隔重投,会让永远不可能成功的旧消息占住队列。为不可恢复输入规定有限尝试与归档原因码;为可恢复错误规定退避、告警阈值和总预算,再交由实际 broker 的投递次数策略实现。规范不会替运维创建归档地址,也不会保证应用设置某个 JMSXDeliveryCount 后就自动死信。使用所选实现的版本化文档确认计数含义、调度延迟与配置加载位置。
失败归档不是垃圾桶:需允许从 task ID 追到原始采购单、消费端版本、载荷摘要、首次/最后一次异常、尝试次数和当前人工处理人。人工重放前要检验原任务是否已有成功收据,对错误载荷制作修复版本,保留原 ID 与补偿关联;直接将归档原字节再塞回原队列,若解析错误仍在,会生成第二轮毒消息。归档的保留期与敏感内容脱敏也要由运行环境确定,不能把失败消息永久置于所有人可读的管理界面。
现在能复核哪些事情
当前LabRequestsResource.order()把业务订单与 outbox 一并入库,随后需调用受控 BrokerLabResource 的 HTTP 接口才会消费。部署消息配置并迁移 002/003 后,下列只读命令应得到 false(表已存在)和收据行数;普通 server.xml 不能代替消息配置:
1 | |
若查询为 true,说明本机未正确执行 003,不能把该环境的 broker 实验标为通过;false 本身也不能证明有消息送达。详细场景的 SQL 收据行数、退出码与 WAR 摘要见 verification/20261004T-broker-lab/;MDB/死信地址仍未配置,不存在可引用的归档控制台命令。
验收重投与人工重放
在 Open Liberty 26.0.0.5 内嵌引擎下,同步消费和唯一收据去重已实测,30 脚本的原始记录位于 verification/20261004T-broker-lab/:event 5 在收据提交并触发 JMS 回滚后再次被消费,newReceipt=false,独立连接查询收据行数仍为 1;重启后另一条原始响应仍是 false。但发送侧故障先产生了两条携同一 ID 的独立消息;现有记录没有 JMSMessageID/JMSRedelivered,不能确定后一次收到的是回滚那条还是另一条首次投递。WAR 构建/部署摘要一致,场景退出码 0。这不是 MDB onMessage()、毒消息或供应商效果的证据。复跑前需用 server-messaging.xml 部署,隔离库执行 001–003 迁移,准备 JDK 21、Maven wrapper、jq 与本地口令;仅限回环实验环境(工程说明):
1 | |
正常验收是脚本退出 0、注入回滚收到预期 HTTP 500、后续收到同一 event ID 且 newReceipt=false;回滚后 SQL 收据为 1。30-broker-lab.sh 的最后断言要求 event_id|PUBLISHED|1;2026-10-05 的 verification/20261005T-final-check/ 重跑了这一断言,原始输出与构建摘要以该目录为准。接收十轮无同 ID 消息会使脚本非零退出。下一阶段 MDB/死信 FUTURE/NOT_RUN:新增入站激活配置、容器回调与实际失败归档后,再注入不支持的载荷版本;在冻结的重投上限内核对具体失败地址及人工重放。003 没有载荷版本或外部效果列,不能对现有表宣称这些验收已经通过。双实例同 event ID 并发、XA 恢复和真实外部调用也 NOT_RUN;步骤见 消费实验合同。
让同一消息的几种轨迹都可见
本次实际轨迹是 outbox event 5 PUBLISHED、同步 receive-one 写收据、按开关回滚 JMS、再次同步接收 newReceipt=false;一次服务器重启后的独立接收仍返回同一 event 且 newReceipt=false。脚本核对事件 ID 和独立数据库收据,没有 MDB 回调日志、外部业务账本、毒消息或全部消息终态指标。未来若要单独证明某条消息的重投,发送侧应先隔离为唯一一条消息,保存 JMSMessageID、接收时间和 JMSRedelivered,让确认失败后再次接收与同一条物理消息相对应;若只想检验业务幂等,则应明确再发送独立消息,并检查两个不同 message ID 映射同一 event ID。两种测试都不能拿当前 HTTP 路由回包充数。
不同故障不能混称“崩溃”:第 30 脚本在 JMS send commit 后抛异常、在收据 commit 后 JMS rollback,后者与实际杀进程、MDB 确认失败或数据库回滚都不是同一个实验。recordReceipt 的 ON CONFLICT 抵挡同一个 event ID 的收据重复;没有验证两条不同 ID 却指向同一供应商订单的幂等处理。FUTURE:若引入 MDB,分别测试接收前出错、收据前回滚、收据提交后实际进程退出及毒消息归档,并记录真实资源的确认与恢复边界。
当前重复路径在 ON CONFLICT(event_id) 后仅查询行是否存在,没有核对原先记录的租户或关联订单摘要;错误地重用一个 event ID 时可能返回 newReceipt=false,这是教学去重不覆盖的安全边界。FUTURE:按受信订单/租户及载荷摘要校验重复消息,再让唯一约束裁决并发;对唯一键等待、超时、死锁单独做有界测试,不要把任何冲突都翻译成“已成功通知”。
当前消息体又只有一个数字,不能从消息自身校验订单状态、事件类型或格式版本;接收器必须回查 outbox 才能把数字与租户关联。若查不到 event ID,recordReceipt 会报错而不是制造空收据;若出错前已经发生别处的外部效果,这个本地异常无法撤回它。未来引入丰富载荷时应定义哪些字段参与签名、哪些只用于显示,验证失败后保留消息和失败原因供隔离处理;现在既没有签名也没有这类失败归档。
对人工重放也要规定可复核的决策:归档消息对应的订单若已撤销,不能无条件给供应商发通知;若订单已履约而收据缺失,应先查询外部供应商账本,再决定补写收据或再次调用;若只是旧 schema 格式不再可解析,应按经过审计的转换生成新版本载荷,保留来源归档 ID 和操作人。重放结束后,任务 T 在 broker、归档和消费收据三个位置只能有可解释的状态,不允许留下“归档删了、收据也没有”的隐性丢失。以上结果要在隔离 broker 的真实配置下核验,生产手工重放必须加审批和速率限制。同一订单有多种事件时,不应因为旧事件重放就把 ORDERED 倒退成 APPROVED;消费前读取当前订单版本与允许的状态转换,过期事件写入受限审计而不是覆盖新状态。顺序依赖不能靠消息抵达先后隐式保证。
当前 003 已有收据表。下面是对脚本输出的 event_id 做只读终态核对的命令;请在已迁移的独立 javaee_lab 环境使用脚本实际 ID(5 只是上述一次原始记录),而非直接把样例参数当成新一次运行结果:
1 | |
期望 1 仅证明该事件一次收据;003 不含 order_id、payload_hash,不能查询不存在的列,更不能以此推断 MDB 死信或供应商副作用。
两道练习
练习一:JMSRedelivered=false 的消息还能命中已存在的收据吗?解:可以。发布器在标记 outbox 进度前失败,可能发送两条独立消息,传输层各自都是首次投递,却携带同一个 outbox event ID。现有收据的主键是这个稳定 ID,不是投递标志或 JMSMessageID。
练习二:对不能解析的消息一直抛异常,等部署新版本再处理,可以吗?解:只有有限重试与可核对失败归档时才可控。冻结 broker/适配器的上限和归档策略,验证消息确实进入归档,再核对版本与业务状态后人工重放;未找到失败记录不能把消息视为已处理。
如果消费顺序与审批状态有关,再投递一条旧通知不得覆盖新状态。针对任务携带的订单版本做条件校验,发现当前状态已经前进就保存“过期事件已审阅”的记录;只有业务明确要求重新通知,才生成一项新的业务任务。否则即使收据唯一键成功限制了相同消息,两个不同 ID 的旧新消息仍可能按相反顺序执行,业务上仍会出现错误结果。
适用范围与版本资料
已有内嵌 broker 的同步 HTTP 接收和本地唯一收据证据;外部邮件/供应商送达、MDB 激活与失败归档均 NOT_RUN。参考 Jakarta Messaging 3.1:交付与重投、Jakarta Enterprise Beans 4.0:message-driven beans、Jakarta Transactions 2.0;实际死信策略须另附冻结实现的文档及运行记录。






