臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

Consumer 是一个正在干活的程序实例;Consumer Group 是一组执行相同逻辑的 Consumer。不同 Group 各自消费一遍,同一 Group 内的实例通常共同分担。

Group ID 相同:共同分担
Group ID 不同:各自消费

前置概念:2. MQ 核心角色与代码对应。

二、🧭 Consumer 和 Consumer Group 的区别

假设 HRMS 部署三台机器:

HRMS Consumer Group
├── HRMS 实例 1:Consumer 1
├── HRMS 实例 2:Consumer 2
└── HRMS 实例 3:Consumer 3
  • Consumer 是一个实际运行的实例。
  • Consumer Group 是三个实例共同使用的消费身份。
  • 同组实例应使用相同的 Topic、Tag 和消费逻辑。

配置中的 Group ID:

spring.sofamq.normal.consumers[7].group-id=GID_CIC_MIDABOSS_ACL_ACCOUNT_MODIFY
spring.sofamq.normal.consumers[7].topic=TP_CIC_MIDABOSS_SSO_ACCOUNT_MODIFY
spring.sofamq.normal.consumers[7].tag=TG_CIC_MIDABOSS_SSO_ACCOUNT_MODIFY

可以翻译成:

我是 ACL Group 中的一个 Consumer,
订阅账户变更 Topic,
只接收符合指定 Tag 的消息。

三、🔀 不同 Group 为什么都能收到消息

TP_CIC_MIDABOSS_SSO_ACCOUNT_MODIFY
│
├── ACL Group
│   ├── ACL 实例 1
│   └── ACL 实例 2
│
└── HRMS Group
    ├── HRMS 实例 1
    └── HRMS 实例 2

一条账户变更消息会由 ACL Group 和 HRMS Group 分别处理一次。两个 Group 还会分别保存自己的消费进度:

ACL Group  已处理到第 100 条
HRMS Group 已处理到第 97 条

ACL 处理成功不会代替 HRMS 确认,两个消费身份互不影响。

四、👥 集群消费 CLUSTERING

// 设置当前 Consumer 属于哪个 Group。
properties.setProperty(PropertyKeyConst.GROUP_ID, groupId);
 
// CLUSTERING:同组多个实例共同分担消息。
properties.setProperty(
        PropertyKeyConst.MESSAGE_MODEL,
        PropertyValueConst.CLUSTERING
);

假设同组有三个实例:

消息 1 → 实例 A
消息 2 → 实例 B
消息 3 → 实例 C
消息 4 → 实例 A

一条消息通常只由组内某一个实例处理,适合订单处理、数据同步、账户变更和异步任务。

增加实例通常可以提高消费能力,但实际并行度还受 Queue 数量、线程数和单条处理速度影响。

五、📢 广播消费 BROADCASTING

// BROADCASTING:同组每个实例都处理每条消息。
properties.setProperty(
        PropertyKeyConst.MESSAGE_MODEL,
        PropertyValueConst.BROADCASTING
);

假设组织服务部署三台机器:

同一条缓存失效消息
├── 实例 A:清理自己的本地缓存
├── 实例 B:清理自己的本地缓存
└── 实例 C:清理自己的本地缓存

项目中的典型代码:

consumer.subscribe(topic, tag, (message, context) -> {
    OrganizationMessageModel org = parse(message);
 
    // 本地缓存属于当前 JVM,每台机器都必须自己清理一次。
    organizationLocalCache.invalidLocalOrgCacheByCode(
            org.getBranchOrgCode()
    );
 
    return Action.CommitMessage;
});

广播适合每台机器都有独立本地状态的场景,例如刷新本地缓存或本机配置。

六、🆚 三个容易混淆的层次

情况一条消息怎样处理
不同 Consumer Group 订阅同一 Topic每个 Group 各自处理一遍
同一 Group 使用 CLUSTERING组内某一个实例处理
同一 Group 使用 BROADCASTING组内每个实例都处理

发布订阅不等于广播消费

不同 Group 各自收到消息属于发布订阅;同一个 Group 内每个实例都收到消息才是广播消费。

七、⚠️ Group ID 复用踩坑

曾经有两个不同应用误用了同一个 HRMS Group ID:

spring.sofamq.account.outsourced.group=GID_CIC_MIDABOSS_HRMS_ACCOUNT_OUTSOURCED_CHANG_STAFF

两个应用都使用集群模式,Broker 会把它们当成同一应用的两个实例:

应用 A ─┐
        ├─ 相同 Group ID → Broker 认为属于同一组
应用 B ─┘               → 消息在两者之间分配

于是可能出现:

消息 1 → 应用 A
消息 2 → 应用 B
消息 3 → 应用 A
消息 4 → 应用 B

从单个应用看,就像随机丢消息。实际上消息被同组的另一个应用消费并提交了。

7.1 正确规则

只有同时满足下面条件的实例才应该共用 Group ID:

同一个业务应用
+ 相同 Topic
+ 相同 Tag
+ 相同消费逻辑
= 可以共用 Group ID 做水平扩容

不同业务应用都需要收到消息时,应使用不同 Group:

账户变更 Topic
├── ACL Group   → ACL 独立消费一遍
├── HRMS Group  → HRMS 独立消费一遍
└── Other Group → 其他应用独立消费一遍

7.2 修复后还要检查什么

修改 Group ID 后,新 Group 会拥有独立消费进度,不一定自动收到旧 Group 已确认的历史消息。还要检查:

  1. 旧 Group 的消息轨迹和实际消费客户端。
  2. 新旧应用的 Topic、Tag 和消费模式。
  3. 新 Group 的起始消费位点。
  4. 是否需要重置位点或重新投递历史消息。
  5. 补发前是否已经做好消费幂等。

八、🎤 面试时怎么说

Consumer 是实际运行的消费实例,Consumer Group 是一组执行相同逻辑的 Consumer,也是 MQ 管理订阅关系和消费进度的身份。不同 Group 订阅同一 Topic 时会各自消费一遍;集群模式下,同组实例共同分担消息;广播模式下,同组每个实例都处理每条消息。不同业务应用不能误用同一个 Group ID,否则 Broker 会把它们当成同组实例进行负载均衡,表现为每个应用只能收到部分消息。

九、📌 快速回顾

  • Consumer 是实例,Consumer Group 是消费身份。
  • 不同 Group 各自消费,同组实例通常共同分担。
  • 集群模式适合业务处理,广播模式适合清理每台机器的本地状态。
  • 不同应用复用 Group ID 会产生竞争消费和订阅不一致风险。
  • 修改 Group ID 后还要检查历史消费位点。

下一篇:4. SOFAMQ 账户变更代码实践。

十、🔗 官方资料