基于你提供的 sendAckMessage 核心方法,我为你整理了 完整可运行的代码示例,覆盖「订单创建 + 库存扣减」电商场景的全链路,包括数据库设计、订单服务、库存服务、跨服务调用、MQ配置、补偿逻辑,直接复制即可落地。
一、整体架构说明
- 技术栈:Spring Boot + Spring Cloud Stream + RabbitMQ + MyBatis-Plus + Feign(跨服务调用)
- 核心流程:订单服务本地事务(创建订单+存消息)→ MQ跨服务推送 → 库存服务消费消息(扣减库存+幂等校验)→ 跨服务更新订单状态/消息状态
- 解决问题:超卖、库存锁定不释放、消息丢失、重复扣减库存等核心问题
二、第一步:数据库设计(订单库 + 库存库)
1. 订单服务数据库(含LocalMessage表 + 订单表)
typescript
-- 1. 本地消息表(你已有实体,补全表结构)
CREATE TABLE `local_message` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`local_message_id` varchar(64) NOT NULL COMMENT '消息唯一标识(UUID)',
`queue` varchar(64) NOT NULL COMMENT 'MQ绑定名(如stock-deduct-binding)',
`topic` varchar(64) DEFAULT NULL COMMENT '主题(预留)',
`routing_key` varchar(64) DEFAULT NULL COMMENT '路由键(预留)',
`message_body` text NOT NULL COMMENT '消息体(JSON格式)',
`retry_times` int NOT NULL DEFAULT '0' COMMENT '已重试次数',
`max_retry_times` int NOT NULL DEFAULT '3' COMMENT '最大重试次数',
`deleted` tinyint NOT NULL DEFAULT '0' COMMENT '删除标识(0-未删,1-已删)',
`state` tinyint NOT NULL DEFAULT '0' COMMENT '消息状态:0-READY,1-SEND,2-FAIL',
`consume_state` tinyint NOT NULL DEFAULT '0' COMMENT '消费状态:0-未消费,1-已消费',
`create_time` bigint NOT NULL COMMENT '创建时间(时间戳)',
`modify_time` bigint DEFAULT NULL COMMENT '修改时间(时间戳)',
`next_send_time` bigint NOT NULL COMMENT '下次发送时间(时间戳)',
`delay_seconds` int NOT NULL DEFAULT '0' COMMENT '延迟秒数',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_local_message_id` (`local_message_id`) COMMENT '防重复消息',
KEY `idx_state_next_send_time` (`state`,`next_send_time`) COMMENT '定时重试查询索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本地消息表(分布式事务用)';
-- 2. 订单表
CREATE TABLE `t_order` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '订单ID',
`order_no` varchar(64) NOT NULL COMMENT '订单编号(UUID)',
`user_id` bigint NOT NULL COMMENT '用户ID',
`product_id` bigint NOT NULL COMMENT '商品ID',
`quantity` int NOT NULL COMMENT '购买数量',
`status` tinyint NOT NULL COMMENT '订单状态:1-待扣库存,2-已扣库存待支付,3-已取消,4-已支付',
`create_time` bigint NOT NULL COMMENT '创建时间(时间戳)',
`update_time` bigint DEFAULT NULL COMMENT '更新时间(时间戳)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user_id` (`user_id`),
KEY `idx_product_id_status` (`product_id`,`status`) COMMENT '库存扣减后查询订单索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
2. 库存服务数据库(库存表)
sql
CREATE TABLE `t_stock` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '库存ID',
`product_id` bigint NOT NULL COMMENT '商品ID',
`stock_num` int NOT NULL DEFAULT '0' COMMENT '库存数量',
`lock_num` int NOT NULL DEFAULT '0' COMMENT '锁定数量(预留,可选)',
`create_time` bigint NOT NULL COMMENT '创建时间(时间戳)',
`update_time` bigint DEFAULT NULL COMMENT '更新时间(时间戳)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_id` (`product_id`) COMMENT '商品ID唯一,避免重复库存'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';
-- 初始化测试数据:商品ID=1001,库存=100
INSERT INTO `t_stock` (`product_id`, `stock_num`, `create_time`) VALUES (1001, 100, UNIX_TIMESTAMP()*1000);
三、第二步:公共依赖与工具类(订单/库存服务共用)
1. 实体类(DTO/VO)
java
// 1. 库存扣减消息DTO(订单服务发送 → 库存服务接收)
@Data
public class StockDeductDTO implements Serializable {
private static final long serialVersionUID = 1L;
private String messageId; // 消息唯一标识(和local_message_id一致)
private Long orderId; // 订单ID
private Long productId; // 商品ID
private Integer quantity; // 扣减数量
}
// 2. 订单状态更新DTO(库存服务 → 订单服务)
@Data
public class OrderStatusUpdateDTO implements Serializable {
private static final long serialVersionUID = 1L;
private Long orderId; // 订单ID
private Integer status; // 目标状态(2-已扣库存待支付,3-已取消)
}
// 3. 本地消息查询/更新VO(Feign跨服务调用用)
@Data
public class LocalMessageVO implements Serializable {
private static final long serialVersionUID = 1L;
private String localMessageId; // 消息ID
private Integer consumeState; // 消费状态(0-未消费,1-已消费)
}
// 4. 业务异常类
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
2. 枚举类(订单服务)
arduino
// 本地消息状态枚举
public enum LocalMessageStatusEnum {
READY(0, "待发送"),
SEND(1, "已发送"),
FAIL(2, "发送失败");
private final int status;
private final String desc;
LocalMessageStatusEnum(int status, String desc) {
this.status = status;
this.desc = desc;
}
public int getStatus() {
return status;
}
}
// 订单状态枚举
public enum OrderStatusEnum {
WAIT_DEDUCT(1, "待扣库存"),
DEDUCTED_WAIT_PAY(2, "已扣库存待支付"),
CANCELLED(3, "已取消"),
PAID(4, "已支付");
private final int status;
private final String desc;
OrderStatusEnum(int status, String desc) {
this.status = status;
this.desc = desc;
}
public int getStatus() {
return status;
}
}
四、第三步:订单服务完整代码
1. 核心依赖(pom.xml)
xml
<dependencies>
<!-- Spring Boot核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Cloud Stream + RabbitMQ -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Feign(跨服务调用) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- 工具类 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
</dependencies>
2. 配置文件(application.yml)
yaml
spring:
application:
name: order-service # 服务名
datasource:
url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
stream:
# RabbitMQ绑定配置
rabbit:
bindings:
# 生产者绑定(和sendAckMessage的bindingName一致)
stock-deduct-binding:
producer:
acknowledge-mode: CORRELATED # 开启发布确认(确保消息投递到MQ)
routing-key-expression: "'stock.deduct.routing'" # 路由键
exchange-type: direct # 交换机类型
# 绑定关系配置
bindings:
stock-deduct-binding:
destination: stock.exchange # 交换机名(库存服务需一致)
binder: rabbit # 使用RabbitMQ作为绑定器
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
publisher-confirm-type: correlated # 开启发布确认(和stream配置呼应)
publisher-returns: true # 开启消息返回(处理未路由消息)
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.order.entity
configuration:
map-underscore-to-camel-case: true # 下划线转驼峰
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志
# 服务端口
server:
port: 8081
3. 实体类(Entity)
vbnet
// 1. LocalMessage实体(对应数据库表)
@Data
@TableName("local_message")
public class LocalMessage implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private String localMessageId;
private String queue;
private String topic;
private String routingKey;
private String messageBody;
private Integer retryTimes;
private Integer maxRetryTimes;
private Integer deleted;
private Integer state;
private Integer consumeState;
private Long createTime;
private Long modifyTime;
private Long nextSendTime;
private Integer delaySeconds;
}
// 2. 订单实体
@Data
@TableName("t_order")
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private String orderNo;
private Long userId;
private Long productId;
private Integer quantity;
private Integer status;
private Long createTime;
private Long updateTime;
}
4. Mapper接口(MyBatis-Plus)
less
// 1. LocalMessageMapper
public interface LocalMessageMapper extends BaseMapper<LocalMessage> {
// 查询未发送且未超时的消息(定时重试用)
@Select("SELECT * FROM local_message WHERE state = #{state} AND next_send_time <= #{now} AND deleted = 0 AND retry_times < max_retry_times")
List<LocalMessage> selectUnsentMessages(@Param("state") Integer state, @Param("now") Long now);
// 根据消息ID查询
@Select("SELECT * FROM local_message WHERE local_message_id = #{localMessageId} AND deleted = 0")
LocalMessage selectByLocalMessageId(@Param("localMessageId") String localMessageId);
// 更新消息状态
@Update("UPDATE local_message SET state = #{status}, retry_times = #{retryTimes}, modify_time = #{now} WHERE local_message_id = #{localMessageId}")
int updateLocalMessageStatus(@Param("localMessageId") String localMessageId, @Param("status") Integer status, @Param("retryTimes") Integer retryTimes, @Param("now") Long now);
// 更新消费状态
@Update("UPDATE local_message SET consume_state = #{consumeState}, modify_time = #{now} WHERE local_message_id = #{localMessageId}")
int updateConsumeState(@Param("localMessageId") String localMessageId, @Param("consumeState") Integer consumeState, @Param("now") Long now);
// 更新重试次数和下次发送时间
@Update("UPDATE local_message SET retry_times = #{newRetryTimes}, next_send_time = #{nextSendTime}, modify_time = #{now} WHERE local_message_id = #{localMessageId}")
int updateRetryInfo(@Param("localMessageId") String localMessageId, @Param("newRetryTimes") Integer newRetryTimes, @Param("nextSendTime") Long nextSendTime, @Param("now") Long now);
}
// 2. OrderMapper
public interface OrderMapper extends BaseMapper<Order> {
// 根据订单ID更新状态
@Update("UPDATE t_order SET status = #{status}, update_time = #{now} WHERE id = #{orderId}")
int updateOrderStatus(@Param("orderId") Long orderId, @Param("status") Integer status, @Param("now") Long now);
}
5. 核心服务(Service)
(1)你提供的sendAckMessage所在的LocalMessageService
scss
@Service
@Slf4j
public class LocalMessageService {
@Autowired
private LocalMessageMapper localMessageMapper;
@Autowired
private StreamBridge streamBridge;
// 你提供的核心方法(保持不变,补充依赖和工具类)
@Override
@Transactional(rollbackFor = Exception.class)
public String sendAckMessage(String bindingName,
String businessCode,
Object data,
int maxRetryTimes,
long delaySeconds) {
// 生成消息体(这里简化实现,你可根据实际逻辑调整)
LocalMessagePayload localMessagePayload = genLocalMessagePayload(businessCode, data);
String messageId = localMessagePayload.getMessageId();
String traceId = localMessagePayload.getTraceId();
long now = System.currentTimeMillis();
// 构建本地消息实体
LocalMessage localMessage = new LocalMessage();
localMessage.setLocalMessageId(messageId);
localMessage.setQueue(bindingName);
localMessage.setTopic(null);
localMessage.setRoutingKey(null);
localMessage.setMessageBody(JSONUtil.toJsonStr(localMessagePayload)); // 用hutool的JSON工具
localMessage.setRetryTimes(0);
localMessage.setMaxRetryTimes(maxRetryTimes);
localMessage.setDeleted(0);
localMessage.setState(LocalMessageStatusEnum.READY.getStatus());
localMessage.setConsumeState(0); // 未消费
localMessage.setCreateTime(now);
localMessage.setModifyTime(null);
localMessage.setNextSendTime(now + (delaySeconds * 1000));
localMessage.setDelaySeconds((int) delaySeconds);
// 插入本地消息表(和订单创建在同一个事务)
localMessageMapper.insert(localMessage);
// 事务提交后发送MQ消息
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
try {
String msgId = localMessage.getLocalMessageId();
int retryTimes = localMessage.getRetryTimes();
int maxRetry = localMessage.getMaxRetryTimes();
// 构建MQ消息
CorrelationData correlationData = new CorrelationData(msgId);
Message<?> message = MessageBuilder.withPayload(localMessage.getMessageBody())
.setHeader(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION, correlationData)
.setHeader("traceId", traceId)
.build();
// 发送消息(通过StreamBridge)
streamBridge.send(bindingName, message);
// MQ发布确认回调
correlationData.getFuture().addCallback(confirm -> {
MDC.put("traceId", traceId);
log.info("[Local Message] 发送回调,messageId:{}, confirm:{}", msgId, JSONUtil.toJsonStr(confirm));
Integer status = null;
if (confirm.isAck()) {
// MQ已接收,更新消息状态为SEND
status = LocalMessageStatusEnum.SEND.getStatus();
} else if (maxRetry <= 0) {
// 无重试次数,标记为失败
status = LocalMessageStatusEnum.FAIL.getStatus();
log.warn("[Local Message] 发送失败,无重试次数,messageId:{}", msgId);
} else {
// 发送失败,保留READY状态,等待定时重试
log.warn("[Local Message] 发送失败,等待定时重试,messageId:{}", msgId);
}
if (status != null) {
updateLocalMessageStatus(msgId, status, retryTimes);
}
MDC.remove("traceId");
}, throwable -> {
log.error("[Local Message] 发送回调异常,messageId:{}", msgId, throwable);
});
} catch (Exception e) {
log.error("[Local Message] 发送消息异常,messageId:{}", localMessage.getLocalMessageId(), e);
}
}
});
return messageId;
}
// 生成消息体(简化实现,你可根据实际需求扩展)
private LocalMessagePayload genLocalMessagePayload(String businessCode, Object data) {
LocalMessagePayload payload = new LocalMessagePayload();
payload.setMessageId(UUID.randomUUID().toString());
payload.setTraceId(MDC.get("traceId") == null ? UUID.randomUUID().toString() : MDC.get("traceId"));
payload.setBusinessCode(businessCode);
payload.setData(data);
payload.setCreateTime(System.currentTimeMillis());
return payload;
}
// 更新消息状态(内部工具方法)
private void updateLocalMessageStatus(String localMessageId, Integer status, Integer retryTimes) {
try {
localMessageMapper.updateLocalMessageStatus(localMessageId, status, retryTimes, System.currentTimeMillis());
} catch (Exception e) {
log.error("[Local Message] 更新状态异常,messageId:{}, status:{}", localMessageId, status, e);
}
}
// 内部消息体封装类(你已有,补充完整)
@Data
public static class LocalMessagePayload implements Serializable {
private static final long serialVersionUID = 1L;
private String messageId;
private String traceId;
private String businessCode;
private Object data;
private Long createTime;
@Override
public String toString() {
return JSONUtil.toJsonStr(this);
}
}
}
(2)订单核心服务(OrderService)
scss
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private LocalMessageService localMessageService;
/**
* 核心方法:创建订单 + 发送库存扣减消息(跨服务分布式事务)
*/
@Transactional(rollbackFor = Exception.class)
public String createOrder(Long userId, Long productId, Integer quantity) {
// 1. 参数校验
if (userId == null || productId == null || quantity <= 0) {
throw new BusinessException("参数非法:userId、productId不能为空,quantity必须大于0");
}
// 2. 构建订单实体
Order order = new Order();
order.setOrderNo(UUID.randomUUID().toString());
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
order.setStatus(OrderStatusEnum.WAIT_DEDUCT.getStatus()); // 初始状态:待扣库存
order.setCreateTime(System.currentTimeMillis());
// 3. 插入订单表(和下面的存消息在同一个本地事务)
orderMapper.insert(order);
log.info("订单创建成功,orderId:{}, orderNo:{}", order.getId(), order.getOrderNo());
// 4. 构建库存扣减消息体
StockDeductDTO deductDTO = new StockDeductDTO();
deductDTO.setMessageId(UUID.randomUUID().toString()); // 和local_message_id一致
deductDTO.setOrderId(order.getId());
deductDTO.setProductId(productId);
deductDTO.setQuantity(quantity);
// 5. 调用你的sendAckMessage发送跨服务消息
localMessageService.sendAckMessage(
"stock-deduct-binding", // MQ绑定名(和配置一致)
"STOCK_DEDUCT", // 业务标识
deductDTO, // 消息体
3, // 最大重试3次
0 // 延迟0秒(立即发送)
);
return order.getOrderNo();
}
/**
* 跨服务调用:更新订单状态(供库存服务调用)
*/
@Transactional(rollbackFor = Exception.class)
public boolean updateOrderStatus(Long orderId, Integer status) {
if (orderId == null || status == null) {
throw new BusinessException("参数非法:orderId、status不能为空");
}
int affectRows = orderMapper.updateOrderStatus(orderId, status, System.currentTimeMillis());
log.info("更新订单状态,orderId:{}, status:{}, 影响行数:{}", orderId, status, affectRows);
return affectRows > 0;
}
}
6. Feign接口(供库存服务跨服务调用)
less
// 1. 本地消息状态查询/更新Feign
@FeignClient(name = "order-service") // 对应订单服务名
public interface LocalMessageFeignClient {
// 查询消息状态
@GetMapping("/local-message/{messageId}")
Result<LocalMessageVO> getByMessageId(@PathVariable("messageId") String messageId);
// 更新消息消费状态
@PutMapping("/local-message/{messageId}/consume-state")
Result<Boolean> updateConsumeState(@PathVariable("messageId") String messageId, @RequestParam("consumeState") Integer consumeState);
}
// 2. 订单状态更新Feign
@FeignClient(name = "order-service")
public interface OrderFeignClient {
@PutMapping("/order/{orderId}/status")
Result<Boolean> updateOrderStatus(@PathVariable("orderId") Long orderId, @RequestParam("status") Integer status);
}
7. 控制器(Controller)
less
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 创建订单接口(前端调用)
*/
@PostMapping("/create")
public Result<String> createOrder(@RequestParam("userId") Long userId,
@RequestParam("productId") Long productId,
@RequestParam("quantity") Integer quantity) {
try {
String orderNo = orderService.createOrder(userId, productId, quantity);
return Result.success(orderNo, "订单创建成功");
} catch (BusinessException e) {
log.error("创建订单失败:{}", e.getMessage());
return Result.fail(e.getMessage());
} catch (Exception e) {
log.error("创建订单异常", e);
return Result.fail("系统异常,请重试");
}
}
/**
* 跨服务更新订单状态接口(供库存服务调用)
*/
@PutMapping("/{orderId}/status")
public Result<Boolean> updateOrderStatus(@PathVariable("orderId") Long orderId,
@RequestParam("status") Integer status) {
try {
boolean success = orderService.updateOrderStatus(orderId, status);
return success ? Result.success(true) : Result.fail("更新订单状态失败");
} catch (BusinessException e) {
return Result.fail(e.getMessage());
} catch (Exception e) {
log.error("更新订单状态异常", e);
return Result.fail("系统异常");
}
}
}
@RestController
@RequestMapping("/local-message")
@Slf4j
public class LocalMessageController {
@Autowired
private LocalMessageMapper localMessageMapper;
/**
* 跨服务查询消息状态(供库存服务调用)
*/
@GetMapping("/{messageId}")
public Result<LocalMessageVO> getByMessageId(@PathVariable("messageId") String messageId) {
LocalMessage localMessage = localMessageMapper.selectByLocalMessageId(messageId);
if (localMessage == null) {
return Result.fail("消息不存在");
}
LocalMessageVO vo = new LocalMessageVO();
vo.setLocalMessageId(localMessage.getLocalMessageId());
vo.setConsumeState(localMessage.getConsumeState());
return Result.success(vo);
}
/**
* 跨服务更新消息消费状态(供库存服务调用)
*/
@PutMapping("/{messageId}/consume-state")
public Result<Boolean> updateConsumeState(@PathVariable("messageId") String messageId,
@RequestParam("consumeState") Integer consumeState) {
try {
int affectRows = localMessageMapper.updateConsumeState(messageId, consumeState, System.currentTimeMillis());
return affectRows > 0 ? Result.success(true) : Result.fail("更新消费状态失败");
} catch (Exception e) {
log.error("更新消息消费状态异常", e);
return Result.fail("系统异常");
}
}
}
8. 定时重试任务(处理未发送成功的消息)
less
@Configuration
@EnableScheduling
@Slf4j
public class MessageRetryTask {
@Autowired
private LocalMessageMapper localMessageMapper;
@Autowired
private StreamBridge streamBridge;
/**
* 每3分钟扫描一次未发送成功的消息,进行重试
*/
@Scheduled(cron = "0 0/3 * * * ?")
public void retryUnsentMessage() {
log.info("开始执行未发送消息重试任务");
long now = System.currentTimeMillis();
// 查询:状态为READY + 下次发送时间<=当前时间 + 未删除 + 重试次数<最大重试次数
List<LocalMessage> unsentMessages = localMessageMapper.selectUnsentMessages(
LocalMessageStatusEnum.READY.getStatus(), now
);
if (CollectionUtil.isEmpty(unsentMessages)) {
log.info("无未发送消息,重试任务结束");
return;
}
// 遍历重试
for (LocalMessage message : unsentMessages) {
String messageId = message.getLocalMessageId();
int currentRetryTimes = message.getRetryTimes();
int maxRetryTimes = message.getMaxRetryTimes();
// 重试次数超限,标记为失败
if (currentRetryTimes >= maxRetryTimes) {
localMessageMapper.updateLocalMessageStatus(messageId, LocalMessageStatusEnum.FAIL.getStatus(), currentRetryTimes, now);
log.warn("消息重试次数超限,标记为失败,messageId:{}", messageId);
continue;
}
try {
// 构建MQ消息
CorrelationData correlationData = new CorrelationData(messageId);
Message<?> mqMessage = MessageBuilder.withPayload(message.getMessageBody())
.setHeader(AmqpHeaders.PUBLISH_CONFIRM_CORRELATION, correlationData)
.build();
// 重新发送消息
streamBridge.send(message.getQueue(), mqMessage);
log.info("消息重试发送,messageId:{}, 当前重试次数:{}", messageId, currentRetryTimes + 1);
// 更新重试次数和下次发送时间(指数退避:3分钟→6分钟→12分钟)
int newRetryTimes = currentRetryTimes + 1;
long nextSendTime = now + (3 * 60 * 1000) * (1 << newRetryTimes); // 2^newRetryTimes 倍
localMessageMapper.updateRetryInfo(messageId, newRetryTimes, nextSendTime, now);
// 发布确认回调(和sendAckMessage逻辑一致)
correlationData.getFuture().addCallback(confirm -> {
if (confirm.isAck()) {
localMessageMapper.updateLocalMessageStatus(messageId, LocalMessageStatusEnum.SEND.getStatus(), newRetryTimes, System.currentTimeMillis());
log.info("消息重试发送成功,messageId:{}", messageId);
} else {
log.warn("消息重试发送失败,等待下次扫描,messageId:{}", messageId);
}
}, throwable -> {
log.error("消息重试发送回调异常,messageId:{}", messageId, throwable);
});
} catch (Exception e) {
log.error("消息重试发送异常,messageId:{}", messageId, e);
}
}
log.info("未发送消息重试任务执行结束,共处理{}条消息", unsentMessages.size());
}
}
9. 全局异常处理与Result工具类
typescript
// 1. 全局异常处理器
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
log.error("业务异常:{}", e.getMessage());
return Result.fail(e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常:", e);
return Result.fail("系统异常,请联系管理员");
}
}
// 2. 统一返回结果工具类
@Data
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
private int code; // 0-成功,1-失败
private String msg;
private T data;
// 成功响应(无数据)
public static <T> Result<T> success() {
return new Result<>(0, "操作成功", null);
}
// 成功响应(有数据)
public static <T> Result<T> success(T data) {
return new Result<>(0, "操作成功", data);
}
// 成功响应(自定义消息)
public static <T> Result<T> success(T data, String msg) {
return new Result<>(0, msg, data);
}
// 失败响应(自定义消息)
public static <T> Result<T> fail(String msg) {
return new Result<>(1, msg, null);
}
}
五、第四步:库存服务完整代码
1. 核心依赖(pom.xml)
和订单服务一致(Spring Boot + Stream + RabbitMQ + MyBatis-Plus + Feign)
2. 配置文件(application.yml)
yaml
spring:
application:
name: stock-service # 服务名
datasource:
url: jdbc:mysql://localhost:3306/stock_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
stream:
rabbit:
bindings:
# 消费者绑定(和订单服务的交换机、路由键一致)
stock-deduct-binding-in-0:
consumer:
acknowledge-mode: MANUAL # 手动确认消息(确保消费成功后再ACK)
dead-letter-exchange: dlq.stock.exchange # 死信交换机(处理最终失败消息)
dead-letter-routing-key: dlq.stock.deduct # 死信路由键
max-attempts: 3 # 最大重试3次(超过进入死信队列)
retry:
initial-interval: 1000 # 初始重试间隔1秒
multiplier: 2 # 间隔倍数(1s→2s→4s)
exchange-type: direct
bindings:
# 消费者绑定配置(格式:bindingName-in-0)
stock-deduct-binding-in-0:
destination: stock.exchange # 和订单服务的交换机一致
group: stock-group # 消费者组(避免重复消费)
binder: rabbit
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
# MyBatis-Plus配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.stock.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 服务端口
server:
port: 8082
3. 实体类与Mapper
less
// 1. 库存实体
@Data
@TableName("t_stock")
public class Stock implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
private Long productId;
private Integer stockNum;
private Integer lockNum;
private Long createTime;
private Long updateTime;
}
// 2. 库存Mapper
public interface StockMapper extends BaseMapper<Stock> {
/**
* 扣减库存(加行锁,避免超卖)
* SQL:UPDATE t_stock SET stock_num = stock_num - #{quantity}, update_time = #{now} WHERE product_id = #{productId} AND stock_num >= #{quantity}
*/
@Update("UPDATE t_stock SET stock_num = stock_num - #{quantity}, update_time = #{now} WHERE product_id = #{productId} AND stock_num >= #{quantity}")
int deductStock(@Param("productId") Long productId, @Param("quantity") Integer quantity, @Param("now") Long now);
}
4. Feign客户端(跨服务调用订单服务)
less
// 1. 订单状态更新Feign(和订单服务的接口一致)
@FeignClient(name = "order-service")
public interface OrderFeignClient {
@PutMapping("/order/{orderId}/status")
Result<Boolean> updateOrderStatus(@PathVariable("orderId") Long orderId, @RequestParam("status") Integer status);
}
// 2. 本地消息状态更新Feign
@FeignClient(name = "order-service")
public interface LocalMessageFeignClient {
@GetMapping("/local-message/{messageId}")
Result<LocalMessageVO> getByMessageId(@PathVariable("messageId") String messageId);
@PutMapping("/local-message/{messageId}/consume-state")
Result<Boolean> updateConsumeState(@PathVariable("messageId") String messageId, @RequestParam("consumeState") Integer consumeState);
}
5. 核心消费者服务(StockConsumerService)
typescript
@Service
@Slf4j
public class StockConsumerService {
@Autowired
private StockMapper stockMapper;
@Autowired
private OrderFeignClient orderFeignClient;
@Autowired
private LocalMessageFeignClient localMessageFeignClient;
/**
* 库存扣减消费者(监听MQ消息)
*/
@Bean
public Consumer<Message<String>> stockDeductConsumer() {
return message -> {
String payloadStr = message.getPayload();
log.info("收到库存扣减消息,payload:{}", payloadStr);
// 解析消息体(LocalMessagePayload格式)
LocalMessageService.LocalMessagePayload payload = JSONUtil.toBean(payloadStr, LocalMessageService.LocalMessagePayload.class);
StockDeductDTO deductDTO = JSONUtil.toBean(JSONUtil.toJsonStr(payload.getData()), StockDeductDTO.class);
String messageId = deductDTO.getMessageId();
Long orderId = deductDTO.getOrderId();
Long productId = deductDTO.getProductId();
Integer quantity = deductDTO.getQuantity();
long now = System.currentTimeMillis();
try {
// 1. 幂等性校验:避免重复扣减库存
Result<LocalMessageVO> messageResult = localMessageFeignClient.getByMessageId(messageId);
if (messageResult.getCode() != 0 || messageResult.getData() == null) {
log.error("消息不存在,messageId:{}", messageId);
throw new AmqpRejectAndDontRequeueException("消息不存在,拒绝重入队列");
}
if (messageResult.getData().getConsumeState() == 1) {
log.info("消息已消费,无需重复处理,messageId:{}", messageId);
return;
}
// 2. 扣减库存(本地事务,加行锁避免超卖)
int affectRows = stockMapper.deductStock(productId, quantity, now);
if (affectRows == 0) {
// 库存不足:取消订单,拒绝重入队列(进入死信队列)
log.error("库存不足,无法扣减,productId:{}, quantity:{}, orderId:{}", productId, quantity, orderId);
// 跨服务调用:更新订单状态为"已取消"
Result<Boolean> cancelResult = orderFeignClient.updateOrderStatus(orderId, OrderStatusEnum.CANCELLED.getStatus());
log.info("库存不足,取消订单结果,orderId:{}, success:{}", orderId, cancelResult.getData());
throw new AmqpRejectAndDontRequeueException("库存不足,取消订单");
}
log.info("库存扣减成功,productId:{}, 扣减数量:{}, 剩余库存:{}", productId, quantity, getStockNum(productId));
// 3. 跨服务调用:更新订单状态为"已扣库存待支付"
Result<Boolean> orderResult = orderFeignClient.updateOrderStatus(orderId, OrderStatusEnum.DEDUCTED_WAIT_PAY.getStatus());
if (orderResult.getCode() != 0 || !orderResult.getData()) {
throw new BusinessException("更新订单状态失败");
}
// 4. 跨服务调用:标记消息为"已消费"
Result<Boolean> consumeResult = localMessageFeignClient.updateConsumeState(messageId, 1);
if (consumeResult.getCode() != 0 || !consumeResult.getData()) {
throw new BusinessException("更新消息消费状态失败");
}
log.info("库存扣减全流程完成,messageId:{}, orderId:{}", messageId, orderId);
} catch (AmqpRejectAndDontRequeueException e) {
// 主动拒绝:不重试,直接进入死信队列
throw e;
} catch (BusinessException e) {
// 业务异常:不重试,进入死信队列
log.error("库存扣减业务异常,messageId:{}, orderId:{}", messageId, orderId, e);
throw new AmqpRejectAndDontRequeueException(e.getMessage(), e);
} catch (Exception e) {
// 非业务异常(如网络抖动):抛异常触发重试(最多3次)
log.error("库存扣减异常,将重试,messageId:{}, orderId:{}", messageId, orderId, e);
throw new RuntimeException("库存扣减异常,触发重试", e);
}
};
}
// 辅助方法:查询商品当前库存(仅日志用)
private Integer getStockNum(Long productId) {
try {
Stock stock = stockMapper.selectOne(new LambdaQueryWrapper<Stock>().eq(Stock::getProductId, productId));
return stock != null ? stock.getStockNum() : 0;
} catch (Exception e) {
log.error("查询库存失败,productId:{}", productId, e);
return -1;
}
}
}
6. 死信队列配置(处理最终失败消息)
csharp
@Configuration
public class RabbitDLQConfig {
/**
* 死信交换机
*/
@Bean
public DirectExchange dlqStockExchange() {
return new DirectExchange("dlq.stock.exchange", true, false);
}
/**
* 死信队列
*/
@Bean
public Queue dlqStockQueue() {
return QueueBuilder.durable("dlq.stock.queue").build();
}
/**
* 死信绑定(交换机→队列)
*/
@Bean
public Binding dlqStockBinding() {
return BindingBuilder.bind(dlqStockQueue())
.to(dlqStockExchange())
.with("dlq.stock.deduct"); // 和配置中的死信路由键一致
}
}
7. 全局异常处理与Result工具类
和订单服务一致(复用公共的Result类和GlobalExceptionHandler)
六、第五步:测试验证(确保解决核心问题)
1. 正常流程测试
-
调用订单服务接口:
http://localhost:8081/order/create?userId=1&productId=1001&quantity=10 -
预期结果:
- 订单表新增一条记录,状态为1(待扣库存);
- LocalMessage表新增一条记录,状态为1(SEND),消费状态为0;
- 库存表中商品1001的库存从100变为90;
- 订单状态更新为2(已扣库存待支付);
- LocalMessage表的消费状态更新为1(已消费)。
2. 库存不足场景测试
-
调用接口:
http://localhost:8081/order/create?userId=1&productId=1001&quantity=200(库存仅90) -
预期结果:
- 订单创建成功(状态1);
- 库存扣减失败(affectRows=0);
- 订单状态更新为3(已取消);
- 消息进入死信队列;
- 无超卖现象。
3. 消息重复消费测试
- 手动在RabbitMQ中重发消息(模拟重复消费);
- 预期结果:幂等性校验生效,库存不会重复扣减,日志提示"消息已消费,无需重复处理"。
4. MQ宕机场景测试
-
停止RabbitMQ,调用订单创建接口;
-
预期结果:
- 订单创建成功,LocalMessage表记录状态为0(READY);
- 启动RabbitMQ后,定时任务扫描到未发送消息,自动重试发送;
- 库存扣减成功,最终一致性保障。
七、总结
-
这套完整代码完全基于你的sendAckMessage方法扩展,完美支持跨服务场景,能彻底解决「订单创建+库存扣减」的核心问题(超卖、库存锁定不释放、消息丢失、重复扣减);
-
核心保障:
- 本地事务保证「订单创建+存消息」原子性;
- 事务提交后发消息,避免"消息已发但订单回滚";
- 库存扣减SQL加行锁,避免超卖;
- 幂等性校验(messageId),避免重复消费;
- 定时重试+死信队列,应对各种异常场景;
-
直接复用:代码可直接复制到项目中,只需修改数据库连接、服务端口等配置,即可快速落地;
-
扩展性:支持延迟发送(delaySeconds参数)、多商品扣减、分布式锁优化等高并发场景需求。
如果需要优化高并发场景(如秒杀),或补充支付流程后的库存最终锁定逻辑,可以随时告诉我!