feat(mq-starter-core): 同 group 订阅 tag 不一致启动 fail-fast,病态 tag 表达式打 WARN

同组配不同 tag 时消息只投组内某一个消费者,不匹配方直接 ACK 吞掉、另一方
永远收不到(订阅关系不一致,静默丢消息级事故,README 原本只靠文字约定)。
现在注解扫描期强校验:同一 group 声明不等价的 tag 表达式直接 IllegalStateException,
报错点名 group、冲突表达式与两个方法。等价性按 MqTagMatcher.validParts 有效
tag 集合比较("A || B" 与 "B||A" 等价,*/空/病态式同为全收),只覆盖单应用内。

- MqTagMatcher 新增 validParts / looksMisconfigured,matches 重构为基于
  validParts,解析口径单一来源(每消息多一次小集合分配,可忽略)
- 病态表达式("||"、纯空白:写了内容却解析不出有效 tag)扫描期打 WARN 不阻断,
  redis 端静默全收 vs rocketmq 端行为不同的分叉维持现状(拍板记录不修)
- README tag 小节、CLAUDE.md 签名约束一节同步

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 12:21:10 +01:00
parent 2bcce78763
commit 646cc6c238
6 changed files with 188 additions and 13 deletions

View File

@@ -129,7 +129,7 @@ public void onFile(FileData msg) { } // 只收 tag=node-01 的消息,
- 消费端不写 `tag` 默认 `*`,整个 topic 全收;发送端用两参 `send` 则消息不带 tag——**对端若按 tag 过滤,不带标签的消息它收不到**,跨系统对接时这是个经典坑。
- rocketmq 模式:过滤发生在 **broker 服务端**不匹配的消息根本不会投到消费者tag 表达式直接透传 RocketMQ 原生语法,`"A || B"` 多选一也支持。
- redis-stream 模式:**消费端模拟过滤**——driver 收到消息后按订阅 tag 比对,不匹配的直接 ACK 丢弃、不进消费链(不碰去重、不算重试、不落库),支持与 rocketmq 相同的写法子集(`*` 全收、单 tag 精确、`A || B` 多选一)。与 rocketmq 的差别只剩过滤位置与成本:消息会先走一趟网络到消费端才被丢弃。
- **同一 group 的所有订阅必须用同一个 tag 表达式**(含滚动发布期间的新旧实例)——要按 tag 分流,就各用各的 group。同组配不同 tag 时,消息只会投给组内某一个消费者,拿到"不归自己"消息的那个会直接 ACK 吞掉对方永远收不到两端皆然rocketmq 生态称之为「订阅关系不一致」)。
- **同一 group 的所有订阅必须用同一个 tag 表达式**(含滚动发布期间的新旧实例)——要按 tag 分流,就各用各的 group。同组配不同 tag 时,消息只会投给组内某一个消费者,拿到"不归自己"消息的那个会直接 ACK 吞掉对方永远收不到两端皆然rocketmq 生态称之为「订阅关系不一致」)。**同应用内这条规则由框架启动时强校验**,配错直接启动失败(等价性按有效 tag 集合比较,`"A || B"``"B||A"` 不算冲突);跨应用共用 group、滚动发布窗口内的新旧版本仍需靠约定保持一致。另外写了内容却解析不出任何有效 tag 的表达式(如 `"||"`、纯空白)启动时会打 WARN——这种写法 redis-stream 按全收处理、rocketmq 行为与之不同,多半是写错了,全收请用 `*` 或留空。
## 进阶用法