臭名昭著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激活

一、💡 一句话理解

核心结论

aboss-sso 把最新账户详情发送到 SOFAMQ;ACL 和 HRMS 使用不同 Consumer Group 订阅同一 Topic,因此两个系统都能处理这条消息,同组多个实例再共同分担。

前置概念:

二、🧭 完整调用链

ExternalAccountUpdateCmdExe.execute()
    ↓ 修改账户数据
AbsAccountChangeCmdExe.publish(accId)
    ↓ 查询最新 AccountDetailDTO
AccountChangeMessage.publish(dto)
    ↓ 创建 Message
SOFAMQ Producer.send(message)
    ↓
TP_CIC_MIDABOSS_SSO_ACCOUNT_MODIFY
    ├── ACL Group  → 新增或更新 ACL 账户
    └── HRMS Group → 处理 HRMS 账户变化

三、⚙️ 生产端配置

# 是否创建普通消息 Producer。
spring.sofamq.normal.producers.enable=true
 
# 连接 MQ 使用的身份、实例和接入地址。
# 真实值位于各环境配置文件中,不能写进学习笔记。
spring.sofamq.normal.access-key=***
spring.sofamq.normal.secret-key=***
spring.sofamq.normal.instance-id=***
spring.sofamq.normal.endpoint=***
 
# 发送代码通过这个名称选择 Producer。
spring.sofamq.normal.producers[1].name=accountChangeMessage
 
# 账户变更消息的业务分类。
spring.sofamq.account.change.topic=TP_CIC_MIDABOSS_SSO_ACCOUNT_MODIFY
 
# Topic 内进一步过滤的标签。
spring.sofamq.account.change.tag=TG_CIC_MIDABOSS_SSO_ACCOUNT_MODIFY

主要代码位置:

aboss-sso-support-app/.../executor/account/AbsAccountChangeCmdExe.java
aboss-sso-support-infrastructure/.../mq/AccountChangeMessage.java
aboss-sso-support-infrastructure/.../config/ProducerConfig.java
start/src/main/resources/application.properties

四、📤 生产者怎样发送消息

4.1 业务执行器触发发布

@Transactional(rollbackFor = Exception.class)
public String execute(ExternalAccountUpdateCmd cmd) {
    Account account = init(cmd);
 
    update(account);                 // 更新 aboss-sso 账户数据。
    modify(account);                 // 同步修改 IDaaS 账户。
    buildRelatedAccount(account);    // 维护关联账户关系。
    publish(account.getAccountId()); // 发布账户变更消息。
 
    return account.getAccountId();
}

4.2 查询变更后的完整数据

protected void publish(String accId) {
    // 根据账户 ID 查询最新账户详情。
    List<AccountDetailDTO> details =
            accountListByAccountIdsQryExe.execute(
                    new AccountIdsQry()
                            .setAccIds(ImmutableList.of(accId))
            );
 
    if (CollectionUtils.isNotEmpty(details)) {
        AccountDetailDTO detail = details.get(0);
 
        // 离职账户发送原手机号,方便下游识别原账户。
        if (!detail.getIsValid()) {
            detail.setPhoneNo(detail.getBeforePhoneNo())
                    .setBeforePhoneNo(null);
        }
 
        // 把最新账户详情交给消息发送器。
        accountChangeMessage.publish(detail);
 
        // 清理账户详情缓存。
        accountGateway.cleanAccountDetailCache(accId);
    }
}

4.3 创建 Message 并发送

@Override
public void publish(AccountDetailDTO accountDetailDTO) {
    Message message = new Message(
            topic,
            tag,
            JSON.toJSONString(accountDetailDTO)
                    .getBytes(StandardCharsets.UTF_8)
    );
 
    try {
        getProducer().send(message);
    } catch (Exception e) {
        log.error(
                "account info modify mq error, body: {}",
                JSON.toJSONString(accountDetailDTO),
                e
        );
    }
}

getProducer() 的真实实现是按配置名称从 ProducerSelector 中取出 Producer:

@Override
public Producer getProducer() {
    return producerSelector.select(name);
}
概念项目代码
业务数据AccountDetailDTO
Message BodyDTO 序列化后的 JSON 字节数组
TopicTP_CIC_MIDABOSS_SSO_ACCOUNT_MODIFY
TagTG_CIC_MIDABOSS_SSO_ACCOUNT_MODIFY
ProducergetProducer() 返回的对象
发送send(message)

五、📥 ACL 怎样消费消息

5.1 ACL 消费配置

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

5.2 ACL 消费代码

下面是根据真实代码压缩后的学习版,validate 和 convert 用来代表原代码中的详细校验与字段转换:

// 根据配置创建一个属于 ACL Group 的 Consumer。
Consumer consumer = accessPoint.createConsumer(properties);
 
// 订阅与生产端一致的 Topic 和 Tag。
consumer.subscribe(topic, tag, new MessageListener() {
    @Override
    public Action consume(Message message, ConsumeContext context) {
        // 1. 将消息体从字节数组恢复成 JSON。
        String body = new String(
                message.getBody(),
                StandardCharsets.UTF_8
        );
 
        // 2. 转换成 ACL 使用的请求 DTO。
        SsoAccountDetailReqDTO account =
                JSONUtils.parseObject(body, SsoAccountDetailReqDTO.class);
 
        // 3. 校验账户 ID、登录名、组织等字段。
        validate(account);
 
        // 4. 按 accId 判断新增还是更新。
        AccountDetailResModel old =
                accountService.getAccountDetailByAccId(account.getAccId());
 
        if (old != null) {
            accountService.updateAccount(convert(account));
        } else {
            accountService.insertAccount(convert(account));
        }
 
        // 5. 业务完成后确认成功。
        return Action.CommitMessage;
    }
});
 
// 启动 Consumer,开始获取消息。
consumer.start();

这段代码的主线是:

订阅 → 读取 Body → 反序列化 → 校验 → 新增或更新 → Commit

六、👥 HRMS 怎样集群消费

HRMS 使用另一个 Group ID:

spring.sofamq.account.outsourced.group=GID_CIC_MIDABOSS_HRMS_ACCOUNT_OUTSOURCED_CHANG_STAFF
spring.sofamq.account.outsourced.change.topic=TP_CIC_MIDABOSS_SSO_ACCOUNT_MODIFY
spring.sofamq.account.outsourced.change.tag=TG_CIC_MIDABOSS_SSO_ACCOUNT_MODIFY

消费端明确设置集群模式:

properties.setProperty(PropertyKeyConst.GROUP_ID, groupId);
 
// 同一个 HRMS Group 的多个实例共同分担消息。
properties.setProperty(
        PropertyKeyConst.MESSAGE_MODEL,
        PropertyValueConst.CLUSTERING
);
 
Consumer consumer = accessPoint.createConsumer(properties);
 
consumer.subscribe(topic, accountChangedTag, (message, context) -> {
    String body = new String(
            message.getBody(),
            StandardCharsets.UTF_8
    );
 
    AccountDetailDTO account =
            JSONUtils.parseObject(body, AccountDetailDTO.class);
 
    // 此处省略真实业务分支。
    handle(account);
 
    return Action.CommitMessage;
});
 
consumer.start();

ACL 和 HRMS 的 Topic 相同,但 Group ID 不同,因此两个系统各自消费一遍。HRMS 如果部署三个实例,再由三个实例共同分担 HRMS Group 的消息。

七、🚨 消费失败怎样返回

这里的“返回”是什么意思

consume() 不是 HTTP 接口,所以不会向前端或生产者返回 JSON、错误码。它只把一个 Action 枚举值返回给调用它的 MQ 客户端 SDK,SDK 再根据这个值告诉 Broker:确认成功,或者稍后重新投递。

7.1 返回给谁

consume() 是 MQ 客户端 SDK 调用的回调方法:

Broker 把消息交给 Consumer SDK
        ↓
SDK 调用 consume(message, context)
        ↓
业务代码处理消息
        ↓
consume() 返回 Action 枚举值给 SDK
        ↓
SDK 向 Broker 确认成功,或者要求稍后重试

生产者 aboss-sso 不会收到这个返回值。普通 MQ 消息是异步通信;如果生产者必须知道下游业务处理结果,需要另外设计回执消息、状态表或查询接口。

7.2 具体返回什么

处理情况Java 返回值MQ 后续动作
业务处理成功Action.CommitMessage确认消费成功,这条消息不再投递给当前 Group
数据库暂时不可用、调用下游超时等临时故障Action.ReconsumeLater本次消费失败,Broker 稍后把消息重新投递给当前 Group
消息字段缺失、格式非法等永久性脏数据保存原消息并告警后返回 Action.CommitMessage停止重试,避免同一条坏消息不断失败

所以,“消费失败具体返回什么”的直接答案是:

return Action.ReconsumeLater;

它返回的不是下面这种业务响应:

{
  "code": 500,
  "message": "消费失败"
}

而是 com.aliyun.openservices.ons.api.Action 中的一个枚举值,由 MQ SDK 解释。

7.3 放进消费代码里怎样写

public Action consume(Message message, ConsumeContext context) {
    try {
        AccountDetailDTO account = parseAndValidate(message);
 
        // 业务必须支持重复执行。
        accountService.saveOrUpdate(account);
 
        // 返回给 MQ SDK:业务已经处理成功,不要再投递这条消息。
        return Action.CommitMessage;
    } catch (RetryableException e) {
        // 返回给 MQ SDK:本次没有处理成功,请让 Broker 稍后重新投递。
        // 适用于数据库临时不可用、下游超时等可恢复故障。
        return Action.ReconsumeLater;
    } catch (IllegalArgumentException e) {
        // 永久性脏数据:保存原消息并告警,避免无限重试。
        saveBadMessageAndAlarm(message, e);
 
        // 虽然本次业务没有成功,但继续重试也不会恢复,
        // 因此确认这条消息,阻止它反复投递。
        return Action.CommitMessage;
    }
}

官方文档还说明,TCP 客户端返回 null 或直接抛出异常也会触发重试。不过显式返回 Action.ReconsumeLater 更容易让人看懂代码意图,也方便区分“可恢复故障”和“永久性脏数据”。

返回 CommitMessage 不等于业务成功

它准确表达的是“MQ 不要再投递这条消息”。所以处理永久性脏数据时,可以先保存原消息并告警,再返回 CommitMessage。如果直接确认但没有留下记录,消息就可能静默丢失。

其中 parseAndValidate、saveOrUpdate、saveBadMessageAndAlarm 和异常类型是用于解释流程的占位内容,不代表项目一定存在同名实现。

记忆口诀:CommitMessage 表示“处理结束,不再发送”;ReconsumeLater 表示“这次没成,稍后再发”。

更完整的可靠性处理见 5. MQ 可靠性问题 和 6. 消息幂等。

八、🔍 怎样验证完整链路

  1. 在测试环境修改一个外部账户。
  2. 在执行器和 AccountChangeMessage.publish 设置断点。
  3. 确认 AccountDetailDTO 是数据库更新后的数据。
  4. 在 MQ 控制台按 Topic、Tag 或消息 ID 查询发送结果。
  5. 分别检查 ACL Group 和 HRMS Group 的消费进度与堆积量。
  6. 核对 ACL、HRMS 的最终业务数据。
  7. 重复投递同一业务消息,验证消费逻辑是否幂等。

当前代码的可靠性风险

AccountChangeMessage 捕获发送异常后只记录日志,没有继续抛出。MQ 发送失败时,外层事务不一定回滚,接口也可能继续返回成功;反过来,消息发送成功后数据库事务仍可能提交失败。重要链路还需要发送确认、失败补偿、事务消息或本地消息表,并让消费者保持幂等。

九、🎤 面试时怎么说

aboss-sso 在账户变更完成后查询最新的 AccountDetailDTO,将它序列化成 JSON,并通过 SOFAMQ/OpenMessaging Producer 发送到账户变更 Topic。ACL 和 HRMS 使用不同 Group ID 订阅,所以两个系统都能处理这条消息;HRMS 使用集群模式,同组多个实例共同分担。ACL 消费后按 accId 新增或更新本地账户,并在业务成功后返回 CommitMessage。这条链路实现了解耦,但生产端异常只记日志、消费端又可能重复收到消息,因此还需要发送补偿、消费幂等和监控告警。

十、📌 快速回顾

  • aboss-sso 是 Producer,ACL 和 HRMS 是 Consumer。
  • Topic 和 Tag 决定发送与订阅范围。
  • 不同 Group 各自消费,同组多个实例共同分担。
  • 业务成功后才返回 CommitMessage。
  • 可恢复故障可以返回 ReconsumeLater。
  • 发送只记日志是当前链路需要关注的可靠性风险。

十一、🔗 相关资料