分布式事务实战:订单服务 + 库存服务(基于本地消息表组件)

分布式事务实战:订单服务 + 库存服务(基于本地消息表组件)

一、业务场景说明

核心需求:预创建订单成功后,必须确保库存扣减成功;若库存扣减失败,订单需自动回滚(取消),实现"订单创建"与"库存扣减"的分布式事务一致性。

业务流程

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[订单状态更新为"已取消"(回滚)]

关键设计

  1. 订单状态设计预创建(PENDING)已确认(CONFIRMED) / 已取消(CANCELED)(预创建状态避免库存扣减失败导致的订单数据不一致)
  2. 分布式事务保证:订单服务通过之前的本地消息表组件发送"扣库存消息",确保"预创建订单"和"消息发送"原子性
  3. 幂等性处理 :库存服务通过订单ID扣减库存,避免重复扣减;订单服务通过消息ID更新状态,避免重复处理
  4. 失败回滚:库存扣减失败时,订单服务监听失败消息,自动将订单状态改为"已取消",实现数据回滚

二、技术栈补充

在原有分布式消息组件基础上,新增:

组件 作用
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:正常流程(订单创建成功 + 库存扣减成功)

  1. 调用订单服务接口:POST /order/create,请求体:
json 复制代码
{
  "userId": "USER_001",
  "productId": "PROD_001",
  "quantity": 10,
  "amount": 999.00
}
  1. 订单服务预创建订单(状态PENDING),并通过本地消息组件发送扣库存消息
  2. 库存服务消费消息,扣减10个库存(剩余90),发送成功结果
  3. 订单服务监听结果,将订单状态改为CONFIRMED
  4. 最终结果:订单状态CONFIRMED,库存90 → 事务一致

场景2:库存不足(订单创建成功 + 库存扣减失败 → 订单回滚)

  1. 调用订单服务接口,请求体中quantity=200(超过库存100)
  2. 订单服务预创建订单(状态PENDING),发送扣库存消息
  3. 库存服务检查库存不足,扣减失败,发送失败结果
  4. 订单服务监听结果,将订单状态改为CANCELED
  5. 最终结果:订单状态CANCELED,库存100 → 事务回滚,数据一致

场景3:库存服务宕机(消息重试 → 最终一致)

  1. 订单服务创建订单并发送扣库存消息,但库存服务宕机,未消费消息
  2. 分布式消息组件的定时任务会重试发送消息(最多3次)
  3. 库存服务恢复后,消费消息并扣减库存,发送成功结果
  4. 订单服务更新订单状态为CONFIRMED
  5. 最终结果:即使中间服务宕机,通过重试机制保证最终一致性

六、核心注意事项

  1. 幂等性是关键

    1. 库存服务:通过订单ID避免重复扣减(实际场景建议新增stock_deduct_record表记录扣减历史)
    2. 订单服务:通过订单状态(PENDING)避免重复更新
  2. 防超卖 :库存扣减SQL必须加stock_quantity >= #{deductQuantity}条件,避免并发扣减导致超卖

  3. 消息确认机制 :MQ消费者必须手动确认消息(acknowledge-mode: manual),确保业务处理完成后再确认

  4. 本地事务边界 :订单服务的createOrder方法和库存服务的deductStock方法必须加@Transactional,确保本地操作原子性

  5. 监控告警:对"库存扣减失败"的消息添加监控,及时处理异常情况

总结

本实战基于之前的分布式消息组件,实现了订单服务与库存服务的分布式事务一致性,核心亮点:

  1. 无侵入性:分布式事务逻辑封装在消息组件中,业务代码只需调用接口
  2. 最终一致性:通过本地消息表+MQ重试,保证即使中间环节失败,最终数据一致
  3. 高可靠:支持消息必达、幂等处理、防超卖,覆盖大部分实际场景
  4. 易扩展:可快速扩展到其他服务(如支付服务、物流服务)的分布式事务
相关推荐
溪饱鱼1 小时前
NextJs + Cloudflare Worker 是出海最佳实践
前端·后端
哈哈哈笑什么1 小时前
完整分布式事务解决方案(本地消息表 + RabbitMQ)
分布式·后端·rabbitmq
LDG_AGI1 小时前
【推荐系统】深度学习训练框架(十):PyTorch Dataset—PyTorch数据基石
人工智能·pytorch·分布式·python·深度学习·机器学习
小周在成长1 小时前
Java 抽象类 vs 接口:相同点与不同点
后端
expect7g1 小时前
Paimon Branch --- 流批一体化之二
大数据·后端·flink
幌才_loong1 小时前
.NET 8 实时推送魔法:SSE 让数据 “主动跑” 到客户端
后端
tanxiaomi1 小时前
Redisson分布式锁 和 乐观锁的使用场景
java·分布式·mysql·面试
00后程序员1 小时前
如何解决浏览器HTTPS不安全连接警告及SSL证书问题
后端
00后程序员1 小时前
苹果App上架审核延迟7工作日无反应:如何通过App Store Connect和邮件询问进度
后端