news 2026/7/10 9:37:25

MQ 消费端 充电订单业务幂等完整实现(业务层面,非单纯数据库唯一键)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MQ 消费端 充电订单业务幂等完整实现(业务层面,非单纯数据库唯一键)

目录

一、业务幂等设计思路(三层业务保障)

二、核心代码实现

1. MQ 消息体定义(必须携带业务唯一订单号)

2. 幂等工具类 Redis 操作

3. RocketMQ 消费监听 完整幂等逻辑

4. 数据库兜底方案(防止 Redis 丢失数据极端情况)

三、三层业务幂等完整流程(面试口述重点)

四、关键业务设计细节(面试官高频追问)

1. 为什么幂等 key 要组合 orderNo + msgType?

2. 为什么标记幂等要放在业务执行成功之后,不能前置?

3. Redis 过期时间为什么设 24 小时?

4. 和单纯数据库唯一索引的区别(业务层面优势)

5. 业务场景举例:重复结算消息的完整拦截链路

五、拓展:事务消息场景幂等兼容(充电资金业务)


场景背景:RocketMQ 推送订单结算、账单生成、设备状态更新消息;重复投递、重试会导致重复扣费、重复生成账单、重复推送通知,需要业务层幂等控制,同一订单消息只执行一次。

一、业务幂等设计思路(三层业务保障)

  1. 业务唯一标识:每条 MQ 消息携带业务唯一键orderNo(订单号),作为幂等判断核心;
  2. 前置状态校验(业务第一道拦截,成本最低):查询订单业务状态,已完成结算直接丢弃消息,不执行后续逻辑;
  3. 幂等记录缓存(Redis):处理成功后存入 Redis 标记该订单已消费,短时间内重复消息直接拦截;
  4. 数据库唯一约束兜底:账单表基于 orderNo 唯一索引,防止极端情况重复落库;
  5. 消费成功后置标记:只有完整业务执行完毕才记录幂等标识,失败不标记,支持正常重试。

二、核心代码实现

1. MQ 消息体定义(必须携带业务唯一订单号)

import lombok.Data; import java.io.Serializable; /** * 充电订单结算消息 */ @Data public class ChargeOrderMsg implements Serializable { // 业务唯一幂等键:订单号(核心) private String orderNo; // 场站ID private Long stationId; // 充电度数 private BigDecimal chargeDegree; // 消息业务类型:SETTLE-结算、NOTICE-通知、STAT-统计 private String msgType; }

2. 幂等工具类 Redis 操作

import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; @Component public class MqIdempotentUtil { // 幂等key前缀 private static final String IDEM_PREFIX = "mq:charge:idempotent:"; // 过期时间:24小时,覆盖消息重试最大周期 private static final long EXPIRE_HOUR = 24; @Resource private StringRedisTemplate stringRedisTemplate; /** * 判断该订单消息是否已处理过 * @param orderNo 业务唯一订单号 * @param msgType 消息类型(同一订单不同消息类型互不干扰) * @return true=已处理,直接丢弃;false=未处理,执行业务 */ public boolean isProcessed(String orderNo, String msgType) { String key = IDEM_PREFIX + msgType + ":" + orderNo; return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key)); } /** * 业务执行成功后,标记当前订单消息已完成 */ public void markProcessed(String orderNo, String msgType) { String key = IDEM_PREFIX + msgType + ":" + orderNo; stringRedisTemplate.opsForValue().set(key, "1", EXPIRE_HOUR, TimeUnit.HOURS); } }

3. RocketMQ 消费监听 完整幂等逻辑

import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; import org.apache.rocketmq.spring.core.RocketMQListener; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service @RocketMQMessageListener( topic = "CHARGE_ORDER_SETTLE_TOPIC", consumerGroup = "charge-order-settle-consumer-group" ) public class ChargeOrderMqConsumer implements RocketMQListener<ChargeOrderMsg> { @Resource private MqIdempotentUtil mqIdempotentUtil; @Resource private ChargeOrderService chargeOrderService; @Resource private BillService billService; @Override public void onMessage(ChargeOrderMsg msg) { String orderNo = msg.getOrderNo(); String msgType = msg.getMsgType(); // 1. 参数校验,防止空消息 if (orderNo == null || msgType == null) { throw new RuntimeException("消息参数非法,丢弃"); } // ========== 第一层:Redis幂等标记拦截(最快,无DB查询)========== boolean processed = mqIdempotentUtil.isProcessed(orderNo, msgType); if (processed) { // 重复消息,直接消费成功,不再执行业务 System.out.printf("订单[%s]消息[%s]已处理,跳过\n", orderNo, msgType); return; } // ========== 第二层:业务状态前置校验(核心业务层面幂等)========== // 查询订单当前业务状态,已结算订单不重复执行结算逻辑 ChargeOrder order = chargeOrderService.getByOrderNo(orderNo); if (order == null) { throw new RuntimeException("订单不存在,重试"); } // 业务状态:已结算/已归档,直接拦截,不生成账单、不扣费 if ("SETTLED".equals(order.getOrderStatus()) || "ARCHIVED".equals(order.getOrderStatus())) { // 标记已处理,避免下次重复消息再查库 mqIdempotentUtil.markProcessed(orderNo, msgType); System.out.printf("订单[%s]已完成结算,跳过消费\n", orderNo); return; } try { // ========== 正常业务逻辑 ========== // 1. 计算费用、扣用户余额 chargeOrderService.settleOrder(order); // 2. 生成结算账单 billService.createBill(order); // 3. 推送用户扣费通知 chargeOrderService.sendUserNotice(order); // ========== 业务全部执行成功,再写入幂等标记 ========== // 关键点:必须后置标记!业务异常不标记,下次可以正常重试 mqIdempotentUtil.markProcessed(orderNo, msgType); } catch (Exception e) { // 业务异常,不打幂等标记,RocketMQ自动重试 throw new RuntimeException("订单消费业务异常,触发重试", e); } } }

4. 数据库兜底方案(防止 Redis 丢失数据极端情况)

账单表增加唯一联合索引UNIQUE uk_order_msg_type (order_no, msg_type)

CREATE TABLE charge_bill ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT '订单号', msg_type VARCHAR(16) NOT NULL COMMENT '消息类型', total_fee DECIMAL(10,2) NOT NULL COMMENT '结算金额', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- 业务唯一约束兜底,重复执行直接SQL报错 UNIQUE KEY uk_order_msg_type (order_no, msg_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

当 Redis 数据过期 / 丢失,重复消息走到入库逻辑时,唯一索引冲突抛出异常,消费抛出异常触发重试,但不会生成重复账单,不会产生脏数据。

三、三层业务幂等完整流程(面试口述重点)

  1. Redis 快速拦截(第一层)收到消息先查 Redis 是否存在该订单 + 消息类型的幂等 key,存在直接 return,确认消费成功,不访问数据库,性能最高。
  2. 业务状态校验(第二层,纯业务逻辑核心)即使 Rediskey 过期丢失,查询订单自身业务状态; 结算类消息只允许充电中状态执行,订单状态为已结算、已归档直接拦截,从业务规则上杜绝重复结算、重复扣费,这是最核心的业务幂等手段,不依赖中间件 / 缓存。
  3. 数据库唯一索引兜底(第三层)缓存失效 + 代码异常双重极端场景,数据库唯一约束拦截重复数据落地,杜绝脏账单、重复财务数据。

四、关键业务设计细节(面试官高频追问)

1. 为什么幂等 key 要组合 orderNo + msgType?

同一订单会下发多种消息:结算消息、统计消息、通知消息; 不同消息是独立业务,不能互相拦截,分开标记互不影响。

2. 为什么标记幂等要放在业务执行成功之后,不能前置?

如果先写入 Redis 再执行业务,中途代码异常、服务宕机,业务没做完但 key 已标记,消息直接丢失,造成账单不生成、用户收不到通知; 后置标记保证:只有完整业务执行成功,才记录已处理,失败不标记,RocketMQ 重试恢复。

3. Redis 过期时间为什么设 24 小时?

RocketMQ 重试周期一般不超过 12 小时,24 小时完全覆盖消息全部重试生命周期; 过期后少量重复消息会再次经过业务状态校验,第二层兜底拦截,不会产生业务问题。

4. 和单纯数据库唯一索引的区别(业务层面优势)

  • 仅用唯一索引:重复消息每次都会走到 DB,频繁报唯一冲突,大量无效 DB 查询,性能差;
  • 业务状态前置校验:纯业务规则判断,在更新数据库前直接阻断,不会产生 SQL 异常,友好无报错日志。

5. 业务场景举例:重复结算消息的完整拦截链路

  1. MQ 重复投递同一条结算消息;
  2. Redis 查到已标记,直接跳过;
  3. 若 Rediskey 过期,查询订单状态 = 已结算;
  4. 代码直接 return,不执行扣费、生成账单;
  5. 极端 Redis 丢失 + 状态判断 bug,入库账单表触发唯一索引报错,抛出异常,无重复账单。

五、拓展:事务消息场景幂等兼容(充电资金业务)

如果使用 RocketMQ 事务消息,幂等逻辑不变: 事务提交成功后才执行markProcessed;事务回滚不会写入幂等 key,下次正常消费,保证资金不会重复扣减。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/10 9:37:00

Ubuntu 24.04 上从源码构建 ROS 2 Jazzy 完整指南

1. 项目概述&#xff1a;为什么要在 Ubuntu 上从源码构建 ROS 2 Jazzy&#xff1f;你正在看的&#xff0c;是一份面向真实工程场景的 ROS 2 源码构建实操手记——不是官方文档的翻译复述&#xff0c;而是我过去三年在工业机器人、无人配送小车和高校科研平台项目中&#xff0c;…

作者头像 李华
网站建设 2026/7/10 9:31:52

模拟电路负反馈设计 5 大原则:从理论到运放电路实例解析

模拟电路负反馈设计5大核心原则与运放实战指南 负反馈技术如同模拟电路设计的"稳定器"&#xff0c;它能让桀骜不驯的放大电路变得温顺可控。当我们用LM358搭建电路时&#xff0c;若在反相输入端与输出端之间连上一个电阻&#xff0c;电路性能立刻发生奇妙变化——增益…

作者头像 李华
网站建设 2026/7/10 9:30:38

OpenCV 4.6.0 Java 实现遥感影像 2% 线性拉伸:3步算法与直方图统计优化

OpenCV 4.6.0 Java 实现遥感影像 2% 线性拉伸&#xff1a;算法优化与工程实践 遥感影像处理中&#xff0c;2%线性拉伸是一种广泛应用的对比度增强技术。本文将深入探讨如何利用Java和OpenCV 4.6.0实现这一算法&#xff0c;并分享性能优化与工程实践中的关键经验。 1. 遥感影像…

作者头像 李华
网站建设 2026/7/10 9:30:01

MiniMax M3 模型快速上手与部署指南

在开发智能应用时&#xff0c;很多开发者往往急于看到对话效果&#xff0c;却忽略了环境配置和基础架构的稳定性。结果就是代码跑不通&#xff0c;或者在关键时刻因为鉴权失败、超时而导致服务中断。其实&#xff0c;接入大模型 API 并不复杂&#xff0c;关键在于建立一套规范的…

作者头像 李华
网站建设 2026/7/10 9:28:27

STM32G431 FDCAN 滤波器配置实战:标准/扩展ID双通道过滤与3种模式详解

STM32G431 FDCAN 滤波器配置实战&#xff1a;标准/扩展ID双通道过滤与3种模式详解 在工业控制、汽车电子等对实时性和可靠性要求极高的领域&#xff0c;CAN总线凭借其出色的抗干扰能力和多主架构特性&#xff0c;成为不可替代的通信协议。而STM32G431系列集成的FDCAN控制器&…

作者头像 李华