分布式事务实战:订单服务 + 库存服务(基于本地消息表组件)
一、业务场景说明
核心需求:预创建订单成功后,必须确保库存扣减成功;若库存扣减失败,订单需自动回滚(取消),实现“订单创建”与“库存扣减”的分布式事务一致性。
业务流程
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)避免重复更新
- 防超卖:库存扣减SQL必须加stock_quantity >= #{deductQuantity}条件,避免并发扣减导致超卖
- 消息确认机制:MQ消费者必须手动确认消息(acknowledge-mode: manual),确保业务处理完成后再确认
- 本地事务边界:订单服务的createOrder方法和库存服务的deductStock方法必须加@Transactional,确保本地操作原子性
- 监控告警:对“库存扣减失败”的消息添加监控,及时处理异常情况
总结
本实战基于之前的分布式消息组件,实现了订单服务与库存服务的分布式事务一致性,核心亮点:
- 无侵入性:分布式事务逻辑封装在消息组件中,业务代码只需调用接口
- 最终一致性:通过本地消息表+MQ重试,保证即使中间环节失败,最终数据一致
- 高可靠:支持消息必达、幂等处理、防超卖,覆盖大部分实际场景
- 易扩展:可快速扩展到其他服务(如支付服务、物流服务)的分布式事务
1158

被折叠的 条评论
为什么被折叠?



