分布式事务实战:订单服务 + 库存服务(基于本地消息表组件)
一、业务场景说明
核心需求:预创建订单成功后,必须确保库存扣减成功;若库存扣减失败,订单需自动回滚(取消),实现"订单创建"与"库存扣减"的分布式事务一致性。
业务流程
css
graph TD
A[用户下单] --> B[订单服务]
B -->|1. 本地事务| C{预创建订单 + 发送扣库存消息}
C -->|事务失败| D[直接返回下单失败]
C -->|事务成功| E[消息表状态:待发送]
E --> F[事务提交后发送MQ消息]
F --> G[库存服务消费扣库存消息]
G --> H{库存扣减成功?}
H -->|是| I[发送"库存扣减成功"确认消息]
H -->|否(库存不足/异常)| J[发送"库存扣减失败"消息]
I --> K[订单服务监听确认消息]
J --> K
K -->|成功| L[订单状态更新为"已确认"]
K -->|失败| M[订单状态更新为"已取消"(回滚)]
关键设计
- 订单状态设计 :
预创建(PENDING)→已确认(CONFIRMED)/已取消(CANCELED)(预创建状态避免库存扣减失败导致的订单数据不一致) - 分布式事务保证:订单服务通过之前的本地消息表组件发送"扣库存消息",确保"预创建订单"和"消息发送"原子性
- 幂等性处理 :库存服务通过
订单ID扣减库存,避免重复扣减;订单服务通过消息ID更新状态,避免重复处理 - 失败回滚:库存扣减失败时,订单服务监听失败消息,自动将订单状态改为"已取消",实现数据回滚
二、技术栈补充
在原有分布式消息组件基础上,新增:
| 组件 | 作用 |
|---|---|
| Spring Boot Starter Web | 提供HTTP接口(模拟下单请求) |
| Spring Cloud Stream | 库存服务消费/生产消息 |
| MyBatis-Plus | 简化订单/库存表CRUD(可选) |
三、整体架构(两个独立服务)
bash
# 订单服务(order-service)
com.order.service/
├── controller/ # 接口层(下单接口)
│ └── OrderController.java
├── service/ # 业务层
│ ├── OrderService.java # 订单核心业务(预创建、状态更新)
│ └── OrderMessageListener.java # 监听库存结果消息
├── entity/ # 实体类
│ └── Order.java
├── dto/ # DTO
│ ├── OrderCreateDTO.java
│ ├── StockDeductDTO.java
│ └── StockResultDTO.java
├── mapper/ # Mapper层
│ └── OrderMapper.java
├── resources/
│ └── application.yml # 配置(数据库、MQ、消息组件依赖)
# 库存服务(stock-service)
com.stock.service/
├── service/ # 业务层
│ ├── StockService.java # 库存扣减业务
│ └── StockMessageListener.java # 监听扣库存消息
├── entity/ # 实体类
│ └── Stock.java
├── dto/ # DTO(复用订单服务的StockDeductDTO、StockResultDTO)
├── mapper/ # Mapper层
│ └── StockMapper.java
├── resources/
│ └── application.yml # 配置(数据库、MQ)
四、完整代码实现
第一步:公共DTO(订单/库存服务共用)
1. 订单创建DTO:OrderCreateDTO
arduino
package com.order.dto;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class OrderCreateDTO {
/** 用户ID */
private String userId;
/** 商品ID */
private String productId;
/** 购买数量 */
private Integer quantity;
/** 订单金额 */
private BigDecimal amount;
}
2. 库存扣减DTO:StockDeductDTO
arduino
package com.order.dto;
import lombok.Data;
@Data
public class StockDeductDTO {
/** 订单ID(唯一标识,用于幂等性) */
private String orderId;
/** 商品ID */
private String productId;
/** 扣减数量 */
private Integer deductQuantity;
}
3. 库存结果DTO:StockResultDTO
arduino
package com.order.dto;
import lombok.Data;
@Data
public class StockResultDTO {
/** 订单ID */
private String orderId;
/** 扣减结果(SUCCESS/FAIL) */
private String result;
/** 失败原因(可选) */
private String reason;
/** 消息ID(用于订单服务幂等处理) */
private String messageId;
}
第二步:订单服务(order-service)实现
1. 订单实体:Order
arduino
package com.order.entity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class Order {
/** 订单ID(主键) */
private String orderId;
/** 用户ID */
private String userId;
/** 商品ID */
private String productId;
/** 购买数量 */
private Integer quantity;
/** 订单金额 */
private BigDecimal amount;
/** 订单状态:PENDING(预创建)、CONFIRMED(已确认)、CANCELED(已取消) */
private String status;
/** 创建时间 */
private Date createTime;
/** 更新时间 */
private Date updateTime;
}
2. 订单Mapper:OrderMapper
less
package com.order.mapper;
import com.order.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface OrderMapper {
/** 预创建订单 */
Integer insertOrder(Order order);
/** 根据订单ID更新状态 */
Integer updateOrderStatus(@Param("orderId") String orderId,
@Param("status") String status,
@Param("updateTime") Date updateTime);
/** 根据订单ID查询订单 */
Order selectOrderById(@Param("orderId") String orderId);
}
3. 订单Mapper XML(OrderMapper.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.order.mapper.OrderMapper">
<insert id="insertOrder" parameterType="com.order.entity.Order">
INSERT INTO "order" (order_id, user_id, product_id, quantity, amount, status, create_time, update_time)
VALUES (#{orderId}, #{userId}, #{productId}, #{quantity}, #{amount}, #{status}, #{createTime}, #{updateTime})
</insert>
<update id="updateOrderStatus">
UPDATE "order"
SET status = #{status}, update_time = #{updateTime}
WHERE order_id = #{orderId}
</update>
<select id="selectOrderById" resultType="com.order.entity.Order">
SELECT order_id, user_id, product_id, quantity, amount, status, create_time, update_time
FROM "order"
WHERE order_id = #{orderId}
</select>
</mapper>
4. 订单核心业务:OrderService(核心分布式事务逻辑)
java
package com.order.service;
import com.localmessage.dto.LocalMessagePayload;
import com.localmessage.service.ILocalMessageService;
import com.order.dto.OrderCreateDTO;
import com.order.dto.StockDeductDTO;
import com.order.entity.Order;
import com.order.mapper.OrderMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.Date;
import java.util.UUID;
@Service
public class OrderService {
private static final Logger log = LoggerFactory.getLogger(OrderService.class);
// 订单状态常量
private static final String STATUS_PENDING = "PENDING"; // 预创建
private static final String STATUS_CONFIRMED = "CONFIRMED"; // 已确认
private static final String STATUS_CANCELED = "CANCELED"; // 已取消
@Resource
private OrderMapper orderMapper;
// 注入之前的分布式消息组件核心服务
@Resource
private ILocalMessageService localMessageService;
/**
* 下单核心方法:预创建订单 + 发送扣库存消息(分布式事务核心)
*/
@Transactional(rollbackFor = Exception.class)
public String createOrder(OrderCreateDTO createDTO) {
// 1. 生成唯一订单ID和链路追踪ID
String orderId = "ORDER_" + UUID.randomUUID().toString().replace("-", "").substring(0, 16);
String traceId = "TRACE_" + System.currentTimeMillis();
MDC.put("traceId", traceId);
try {
// 2. 预创建订单(状态为PENDING,未确认)
Order order = new Order();
order.setOrderId(orderId);
order.setUserId(createDTO.getUserId());
order.setProductId(createDTO.getProductId());
order.setQuantity(createDTO.getQuantity());
order.setAmount(createDTO.getAmount());
order.setStatus(STATUS_PENDING);
order.setCreateTime(new Date());
order.setUpdateTime(new Date());
int insertCount = orderMapper.insertOrder(order);
if (insertCount != 1) {
throw new RuntimeException("预创建订单失败,orderId: " + orderId);
}
log.info("预创建订单成功,orderId: {}", orderId);
// 3. 构建扣库存消息体
StockDeductDTO deductDTO = new StockDeductDTO();
deductDTO.setOrderId(orderId);
deductDTO.setProductId(createDTO.getProductId());
deductDTO.setDeductQuantity(createDTO.getQuantity());
// 4. 调用分布式消息组件,发送可靠消息(事务提交后发送)
// 核心:预创建订单和消息表插入在同一事务,要么都成功,要么都回滚
String messageId = localMessageService.sendReliable(
"stock-deduct-queue", // 扣库存消息队列
"STOCK_DEDUCT", // 业务编码
deductDTO, // 消息体(扣库存参数)
3, // 最大重试3次
0 // 不延迟,立即发送
);
log.info("扣库存消息已写入本地表,等待事务提交后发送,orderId: {}, messageId: {}", orderId, messageId);
return orderId;
} catch (Exception e) {
log.error("下单失败,orderId: {}", orderId, e);
throw new RuntimeException("下单失败:" + e.getMessage());
} finally {
MDC.clear();
}
}
/**
* 接收库存扣减结果,更新订单状态(确认/取消)
*/
public boolean handleStockResult(String orderId, String result, String reason) {
Order order = orderMapper.selectOrderById(orderId);
if (order == null) {
log.error("处理库存结果失败:订单不存在,orderId: {}", orderId);
return false;
}
// 幂等性处理:已确认/已取消的订单不再处理
if (!STATUS_PENDING.equals(order.getStatus())) {
log.warn("订单状态已更新,无需重复处理,orderId: {}, 当前状态: {}", orderId, order.getStatus());
return true;
}
Date updateTime = new Date();
if ("SUCCESS".equals(result)) {
// 库存扣减成功,订单改为"已确认"
orderMapper.updateOrderStatus(orderId, STATUS_CONFIRMED, updateTime);
log.info("库存扣减成功,订单已确认,orderId: {}", orderId);
} else {
// 库存扣减失败,订单改为"已取消"(回滚)
orderMapper.updateOrderStatus(orderId, STATUS_CANCELED, updateTime);
log.info("库存扣减失败,订单已取消,orderId: {}, 原因: {}", orderId, reason);
}
return true;
}
}
5. 监听库存结果消息:OrderMessageListener
java
package com.order.service;
import cn.hutool.json.JSONUtil;
import com.order.dto.StockResultDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 监听库存服务发送的"扣库存结果"消息,更新订单状态
*/
@Component
public class OrderMessageListener {
private static final Logger log = LoggerFactory.getLogger(OrderMessageListener.class);
@Resource
private OrderService orderService;
/**
* 监听库存结果队列
*/
@StreamListener(Sink.INPUT)
public void listenStockResult(Message<String> message) {
try {
String payload = message.getPayload();
log.info("收到库存扣减结果消息:{}", payload);
// 解析消息体
StockResultDTO resultDTO = JSONUtil.toBean(payload, StockResultDTO.class);
if (resultDTO == null || resultDTO.getOrderId() == null) {
log.error("消息格式非法:{}", payload);
return;
}
// 处理结果:更新订单状态
orderService.handleStockResult(
resultDTO.getOrderId(),
resultDTO.getResult(),
resultDTO.getReason()
);
} catch (Exception e) {
log.error("处理库存结果消息异常", e);
}
}
}
6. 下单接口:OrderController
kotlin
package com.order.controller;
import com.order.dto.OrderCreateDTO;
import com.order.service.OrderService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("/order")
public class OrderController {
@Resource
private OrderService orderService;
/**
* 下单接口(模拟前端调用)
*/
@PostMapping("/create")
public String createOrder(@RequestBody OrderCreateDTO createDTO) {
try {
String orderId = orderService.createOrder(createDTO);
return "下单成功,订单ID:" + orderId + "(状态:预创建,等待库存确认)";
} catch (Exception e) {
return "下单失败:" + e.getMessage();
}
}
}
7. 订单服务配置:application.yml
yaml
spring:
# 数据库配置(订单库)
datasource:
url: jdbc:postgresql://localhost:5432/order_db?useSSL=false&serverTimezone=UTC
username: postgres
password: 123456
driver-class-name: org.postgresql.Driver
# RabbitMQ配置(与库存服务共用一个MQ)
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
publisher-confirm-type: correlated
publisher-returns: true
# Spring Cloud Stream配置
cloud:
stream:
rabbit:
binder:
persistent: true
acknowledge-mode: manual
bindings:
# 扣库存消息发送绑定
stock-deduct-queue:
destination: stock-deduct-exchange
producer:
required-groups: stock-group
# 库存结果消息接收绑定
input:
destination: order-result-exchange
group: order-group
# MyBatis配置
mybatis:
mapper-locations: classpath:com/order/mapper/xml/*.xml
type-aliases-package: com.order.entity
configuration:
map-underscore-to-camel-case: true
# 扫描分布式消息组件(如果是独立依赖,需指定包扫描)
@ComponentScan(basePackages = {"com.localmessage", "com.order"})
logging:
level:
com.order: INFO
com.localmessage: INFO
org.springframework.amqp: WARN
8. 订单服务数据库脚本(PostgreSQL)
sql
CREATE TABLE "public"."order" (
"order_id" varchar(64) COLLATE "pg_catalog"."default" NOT NULL,
"user_id" varchar(64) COLLATE "pg_catalog"."default" NOT NULL,
"product_id" varchar(64) COLLATE "pg_catalog"."default" NOT NULL,
"quantity" int4 NOT NULL,
"amount" numeric(10,2) NOT NULL,
"status" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
"create_time" timestamp(6) NOT NULL,
"update_time" timestamp(6) NOT NULL,
CONSTRAINT "order_pk" PRIMARY KEY ("order_id")
);
COMMENT ON COLUMN "public"."order"."order_id" IS '订单ID(主键)';
COMMENT ON COLUMN "public"."order"."user_id" IS '用户ID';
COMMENT ON COLUMN "public"."order"."product_id" IS '商品ID';
COMMENT ON COLUMN "public"."order"."quantity" IS '购买数量';
COMMENT ON COLUMN "public"."order"."amount" IS '订单金额';
COMMENT ON COLUMN "public"."order"."status" IS '订单状态(PENDING:预创建,CONFIRMED:已确认,CANCELED:已取消)';
COMMENT ON COLUMN "public"."order"."create_time" IS '创建时间';
COMMENT ON COLUMN "public"."order"."update_time" IS '更新时间';
COMMENT ON TABLE "public"."order" IS '订单表';
第三步:库存服务(stock-service)实现
1. 库存实体:Stock
arduino
package com.stock.entity;
import lombok.Data;
import java.util.Date;
@Data
public class Stock {
/** 主键ID */
private Long id;
/** 商品ID */
private String productId;
/** 商品名称 */
private String productName;
/** 库存数量 */
private Integer stockQuantity;
/** 锁定数量(可选,用于高并发场景) */
private Integer lockedQuantity;
/** 更新时间 */
private Date updateTime;
}
2. 库存Mapper:StockMapper
less
package com.stock.mapper;
import com.stock.entity.Stock;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface StockMapper {
/** 根据商品ID查询库存 */
Stock selectStockByProductId(@Param("productId") String productId);
/** 扣减库存(乐观锁:通过version或库存数量判断,避免超卖) */
Integer deductStock(@Param("productId") String productId,
@Param("deductQuantity") Integer deductQuantity,
@Param("updateTime") Date updateTime);
}
3. 库存Mapper XML(StockMapper.xml)
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.stock.mapper.StockMapper">
<select id="selectStockByProductId" resultType="com.stock.entity.Stock">
SELECT id, product_id, product_name, stock_quantity, locked_quantity, update_time
FROM stock
WHERE product_id = #{productId}
LIMIT 1
</select>
<!-- 扣减库存:通过stock_quantity >= deductQuantity保证不超卖(乐观锁思想) -->
<update id="deductStock">
UPDATE stock
SET stock_quantity = stock_quantity - #{deductQuantity},
update_time = #{updateTime}
WHERE product_id = #{productId}
AND stock_quantity >= #{deductQuantity}
</update>
</mapper>
4. 库存核心业务:StockService
java
package com.stock.service;
import cn.hutool.json.JSONUtil;
import com.stock.dto.StockDeductDTO;
import com.stock.dto.StockResultDTO;
import com.stock.entity.Stock;
import com.stock.mapper.StockMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Date;
import java.util.UUID;
@Service
public class StockService {
private static final Logger log = LoggerFactory.getLogger(StockService.class);
@Resource
private StockMapper stockMapper;
@Resource
private StreamBridge streamBridge;
/**
* 扣库存核心方法(支持幂等性、防超卖)
*/
@Transactional(rollbackFor = Exception.class)
public StockResultDTO deductStock(StockDeductDTO deductDTO) {
String orderId = deductDTO.getOrderId();
String productId = deductDTO.getProductId();
Integer deductQuantity = deductDTO.getDeductQuantity();
StockResultDTO resultDTO = new StockResultDTO();
resultDTO.setOrderId(orderId);
resultDTO.setMessageId(UUID.randomUUID().toString().replace("-", ""));
try {
// 1. 幂等性检查:查询是否已扣减过该订单的库存(实际场景可存储扣减记录)
// 此处简化:通过订单ID+商品ID判断(实际可新增stock_deduct_record表)
log.info("开始扣减库存,orderId: {}, productId: {}, 数量: {}", orderId, productId, deductQuantity);
// 2. 查询商品库存
Stock stock = stockMapper.selectStockByProductId(productId);
if (stock == null) {
resultDTO.setResult("FAIL");
resultDTO.setReason("商品不存在,productId: " + productId);
log.error(resultDTO.getReason());
return resultDTO;
}
// 3. 检查库存是否充足
if (stock.getStockQuantity() < deductQuantity) {
resultDTO.setResult("FAIL");
resultDTO.setReason("库存不足,当前库存: " + stock.getStockQuantity() + ", 需扣减: " + deductQuantity);
log.error(resultDTO.getReason());
return resultDTO;
}
// 4. 扣减库存(通过SQL条件保证不超卖)
int deductCount = stockMapper.deductStock(productId, deductQuantity, new Date());
if (deductCount != 1) {
resultDTO.setResult("FAIL");
resultDTO.setReason("扣库存失败(可能并发扣减导致库存不足)");
log.error(resultDTO.getReason());
return resultDTO;
}
// 5. 扣库存成功
resultDTO.setResult("SUCCESS");
resultDTO.setReason("扣库存成功");
log.info("扣库存成功,orderId: {}, productId: {}, 剩余库存: {}",
orderId, productId, stock.getStockQuantity() - deductQuantity);
return resultDTO;
} catch (Exception e) {
log.error("扣库存异常,orderId: {}", orderId, e);
resultDTO.setResult("FAIL");
resultDTO.setReason("系统异常:" + e.getMessage());
return resultDTO;
}
}
/**
* 发送扣库存结果到订单服务
*/
public void sendDeductResult(StockResultDTO resultDTO) {
try {
String payload = JSONUtil.toJsonStr(resultDTO);
Message<String> message = MessageBuilder.withPayload(payload).build();
// 发送到订单服务的结果队列
streamBridge.send("order-result-queue", message);
log.info("发送扣库存结果成功,orderId: {}, 结果: {}", resultDTO.getOrderId(), resultDTO.getResult());
} catch (Exception e) {
log.error("发送扣库存结果失败,orderId: {}", resultDTO.getOrderId(), e);
// 可重试发送(此处简化,实际可接入分布式消息组件的重试机制)
}
}
}
5. 监听扣库存消息:StockMessageListener
java
package com.stock.service;
import cn.hutool.json.JSONUtil;
import com.stock.dto.StockDeductDTO;
import com.stock.dto.StockResultDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.support.AmqpHeaders;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 监听订单服务发送的"扣库存"消息
*/
@Component
public class StockMessageListener {
private static final Logger log = LoggerFactory.getLogger(StockMessageListener.class);
@Resource
private StockService stockService;
@StreamListener(Sink.INPUT)
public void listenDeductMessage(Message<String> message,
@Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
@Header(AmqpHeaders.CONSUMER_TAG) String consumerTag) {
try {
String payload = message.getPayload();
log.info("收到扣库存消息:{}", payload);
// 解析消息体
StockDeductDTO deductDTO = JSONUtil.toBean(payload, StockDeductDTO.class);
if (deductDTO == null || deductDTO.getOrderId() == null) {
log.error("消息格式非法,拒绝确认:{}", payload);
// 手动确认消息(非法消息直接丢弃)
return;
}
// 扣库存业务处理
StockResultDTO resultDTO = stockService.deductStock(deductDTO);
// 发送结果到订单服务
stockService.sendDeductResult(resultDTO);
// 手动确认消息(处理成功才确认,避免重复消费)
log.info("扣库存消息处理完成,orderId: {}", deductDTO.getOrderId());
} catch (Exception e) {
log.error("处理扣库存消息异常", e);
// 消息重试(根据实际场景配置重试次数,失败后进入死信队列)
}
}
}
6. 库存服务配置:application.yml
yaml
spring:
# 数据库配置(库存库)
datasource:
url: jdbc:postgresql://localhost:5432/stock_db?useSSL=false&serverTimezone=UTC
username: postgres
password: 123456
driver-class-name: org.postgresql.Driver
# RabbitMQ配置(与订单服务共用)
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
publisher-confirm-type: correlated
# Spring Cloud Stream配置
cloud:
stream:
rabbit:
binder:
persistent: true
acknowledge-mode: manual
bindings:
# 接收扣库存消息绑定
input:
destination: stock-deduct-exchange
group: stock-group
# 发送库存结果消息绑定
order-result-queue:
destination: order-result-exchange
producer:
required-groups: order-group
# MyBatis配置
mybatis:
mapper-locations: classpath:com/stock/mapper/xml/*.xml
type-aliases-package: com.stock.entity
configuration:
map-underscore-to-camel-case: true
logging:
level:
com.stock: INFO
org.springframework.amqp: WARN
7. 库存服务数据库脚本(PostgreSQL)
sql
CREATE TABLE "public"."stock" (
"id" serial4 NOT NULL,
"product_id" varchar(64) COLLATE "pg_catalog"."default" NOT NULL,
"product_name" varchar(128) COLLATE "pg_catalog"."default" NOT NULL,
"stock_quantity" int4 NOT NULL DEFAULT 0,
"locked_quantity" int4 NOT NULL DEFAULT 0,
"update_time" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "stock_pk" PRIMARY KEY ("id"),
CONSTRAINT "stock_unique_product" UNIQUE ("product_id")
);
COMMENT ON COLUMN "public"."stock"."id" IS '主键ID';
COMMENT ON COLUMN "public"."stock"."product_id" IS '商品ID(唯一)';
COMMENT ON COLUMN "public"."stock"."product_name" IS '商品名称';
COMMENT ON COLUMN "public"."stock"."stock_quantity" IS '可用库存数量';
COMMENT ON COLUMN "public"."stock"."locked_quantity" IS '锁定库存数量(高并发场景用)';
COMMENT ON COLUMN "public"."stock"."update_time" IS '更新时间';
COMMENT ON TABLE "public"."stock" IS '库存表';
-- 初始化测试数据:商品ID=PROD_001,名称=测试商品,库存=100
INSERT INTO "public"."stock" ("product_id", "product_name", "stock_quantity")
VALUES ('PROD_001', '测试商品', 100);
五、分布式事务验证场景
场景1:正常流程(订单创建成功 + 库存扣减成功)
- 调用订单服务接口:
POST /order/create,请求体:
json
{
"userId": "USER_001",
"productId": "PROD_001",
"quantity": 10,
"amount": 999.00
}
- 订单服务预创建订单(状态PENDING),并通过本地消息组件发送扣库存消息
- 库存服务消费消息,扣减10个库存(剩余90),发送成功结果
- 订单服务监听结果,将订单状态改为CONFIRMED
- 最终结果:订单状态CONFIRMED,库存90 → 事务一致
场景2:库存不足(订单创建成功 + 库存扣减失败 → 订单回滚)
- 调用订单服务接口,请求体中
quantity=200(超过库存100) - 订单服务预创建订单(状态PENDING),发送扣库存消息
- 库存服务检查库存不足,扣减失败,发送失败结果
- 订单服务监听结果,将订单状态改为CANCELED
- 最终结果:订单状态CANCELED,库存100 → 事务回滚,数据一致
场景3:库存服务宕机(消息重试 → 最终一致)
- 订单服务创建订单并发送扣库存消息,但库存服务宕机,未消费消息
- 分布式消息组件的定时任务会重试发送消息(最多3次)
- 库存服务恢复后,消费消息并扣减库存,发送成功结果
- 订单服务更新订单状态为CONFIRMED
- 最终结果:即使中间服务宕机,通过重试机制保证最终一致性
六、核心注意事项
-
幂等性是关键:
- 库存服务:通过订单ID避免重复扣减(实际场景建议新增
stock_deduct_record表记录扣减历史) - 订单服务:通过订单状态(PENDING)避免重复更新
- 库存服务:通过订单ID避免重复扣减(实际场景建议新增
-
防超卖 :库存扣减SQL必须加
stock_quantity >= #{deductQuantity}条件,避免并发扣减导致超卖 -
消息确认机制 :MQ消费者必须手动确认消息(
acknowledge-mode: manual),确保业务处理完成后再确认 -
本地事务边界 :订单服务的
createOrder方法和库存服务的deductStock方法必须加@Transactional,确保本地操作原子性 -
监控告警:对"库存扣减失败"的消息添加监控,及时处理异常情况
总结
本实战基于之前的分布式消息组件,实现了订单服务与库存服务的分布式事务一致性,核心亮点:
- 无侵入性:分布式事务逻辑封装在消息组件中,业务代码只需调用接口
- 最终一致性:通过本地消息表+MQ重试,保证即使中间环节失败,最终数据一致
- 高可靠:支持消息必达、幂等处理、防超卖,覆盖大部分实际场景
- 易扩展:可快速扩展到其他服务(如支付服务、物流服务)的分布式事务