docs: review 未修项记录为已知语义(README/CLAUDE.md)
用户拍板:#5 批间不保序、#7 停机强刷无超时、#4 start 无回滚、#10 三处 快照样板重复,记录为已知语义/结构注意,不按缺陷修。 - README:批量坑段补两条业务方可感知的已知语义(批间不保序;停机会排队 等业务批量方法、挂死会卡住优雅停机)。 - CLAUDE.md:dispatcher 段新增"已知语义/结构注意"条目(四条全记,含 改快照样板须三处同步的提醒)。 全景手册(gitignored)已同步,不进本 commit。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,6 +65,7 @@ mq-spring-boot-starter 聚合 jar,业务方只引这一个(恒含 idemp
|
||||
- **单条消费失败**:先落库(**仅当配了 `errorHandler`**;`safeOnError` 对 null handler 静默跳过、不落库)+ `store.markFail()`(把去重标记从 processing 复位),再抛原异常交 driver 重投。跳过 `markFail` 会让标记卡在 processing,reclaim 重投时被去重挡掉并 ACK → 静默丢消息。
|
||||
- **批量消费**:满批 / 超时(`batchTimeoutMs` 缺省 60s,从本批首条算起;registry 全局单线程 `mq-batch-flush` 每 1s 扫)/ 停机(stop 时 `forceFlush`)三路径刷出(forceFlush 同时打烊:停机后迟到的消息拒收抛 InProgress、不 ACK、下次启动重投,堵住 driver.stop 异步返回的缝隙;已过 `tryAcquire` 才撞上打烊的拒收会先 `markFail` 复位标记再抛,避免重投一直撞 stale processing 白烧重试计数),统一走 `doFlush`(`flushLock` 串行化,业务批量方法任一时刻至多一个调用);kill -9 仍丢内存未刷批(上界 = 一个超时窗口)。批量失败因消息已逐条 ACK 无法精确重投整批,故落库(同样只在配了 `errorHandler` 时)+ 吞掉不抛。
|
||||
- **停机顺序**(`MqListenerRegistry.stop()`,2026-07-15 review 修复后):driver 停(**异常吞掉打 error,不得中断后续强刷**)→ 定时器 `shutdown()` + `awaitTermination(30s)` **等在途的超时刷批跑完**(超时快照已把批移出 buffer,forceFlush 看不见它,不等就是停机丢批)→ 逐监听器 `forceFlush` → `running=false`。改停机代码时别破坏这条链的任何一环。
|
||||
- **已知语义 / 结构注意**(2026-07-15 review 逐条拍板"记录不修"):① **批间不保序**——超时批快照后、抢到 `flushLock` 前可能被新满批插队(MQ 重投本身不保序,框架不承诺批间有序,README 已写给业务方);② **停机强刷无超时**——`forceFlush → doFlush` 排队 `flushLock` 无上界,挂死的业务方法会卡住 stop(与满批路径同风险,已接受,README 已写);③ **start 中途失败无回滚**——先 `dispatchers.add` 后 `driver.subscribe`,中途抛异常再重试 start 会重复订阅先成功的 endpoint(既有行为,Spring 实际几乎不重试 start);④ **"锁内快照→清空→锁外 doFlush"样板在三处重复**——dispatch 满批分支 / `flushIfTimeout` / `forceFlush`,仅 drain 条件不同,**改其中一处务必对照另外两处**,防静默分叉。
|
||||
|
||||
driver 侧对 `MqMessageInProgressException` 的对应处理:`RedisStreamMqDriver.java` 捕获后不 `acknowledge()`(留 PEL 待 reclaim);`RocketMqDriver.java` 在通用 `catch(Exception)` **之前**单独捕获并返回 `RECONSUME_LATER`(绝不进耗尽落库分支——catch 顺序不能动,它是 `Exception` 子类)。边缘(**两端共性**,2026-07-14 复核):InProgress 白跑会烧重试计数——rocketmq 每次 `RECONSUME_LATER` 使 `reconsumeTimes` +1,烧光进原生 `%DLQ%`(进死信、非丢消息);redis-stream 每次 reclaim(xClaim)使 PEL delivery +1,`deliveryCount > maxRetry` 时**不再 dispatch** 直接判耗尽(配了 `errorHandler` 落库,没配则静默丢弃——业务可能一次都没重跑)。缓解同构:`mq.idempotent.processing-ttl` 小于 maxRetry 轮重投累计时长(rocketmq 默认 ~100s / redis-stream 为 `maxRetry × reclaim-min-idle` 默认 90s)。
|
||||
|
||||
|
||||
@@ -166,6 +166,8 @@ public class MyErrorHandler implements MqConsumeErrorHandler {
|
||||
>
|
||||
> ⚠️ **批量有几个坑**:参数必须写带具体元素类型的 `List<FrontData>`(裸 `List` 启动扫描直接抛 `IllegalStateException`);攒不满时最多等 `batchTimeoutMs`(缺省 60 秒,从本批第一条算起)会强制刷出,正常停机也会把没攒满的批刷出去(若停机瞬间恰有一批正在业务方法里跑,停机会等它跑完,上限 30 秒),但 kill -9 / 断电仍会丢内存里那批(最多一个超时窗口内的量);整批业务失败只回调 SINGLE 落库、不重投(所以批量务必配 `errorHandler`)。批量方法同样可以加第二参 `MqContext`(拿到的是该批最后一条的元数据)。
|
||||
>
|
||||
> 两条**已知语义**(设计如此,不按缺陷处理):**批与批之间不保证顺序**——超时刷出的批可能被恰好凑满的新批插队,业务先收到新批再收到旧批(MQ 重投本身也不保序,消费逻辑别依赖批间先后);**停机会排队等正在跑的业务批量方法**——等定时刷批那步有 30 秒上限,但停机强刷没有,业务方法若挂死(如数据库连接僵住),优雅停机会被一直卡住。
|
||||
>
|
||||
> 另外 `batchSize` 与首参是否 `List` **错配时不报错**:启动时打一条 WARN 日志、仍按首参决定走不走批量(`batchSize=50` 配单个 payload 退化为逐条;`List` 首参配 `batchSize` 缺省按 1 条一批)。见启动日志 `[mq] @MqListener batchSize=...`。
|
||||
|
||||
## 两个 driver 不太一样
|
||||
|
||||
Reference in New Issue
Block a user