楼层: 首页/ 软件技术/ Spring Cloud 微服务/ 完整案例:电商下单 + 分布式事务
十二

完整案例:电商下单 + 分布式事务

End-to-End Order Flow with Seata

上一章搭了"三件套",这一章把最后一块拼图Seata 分布式事务加上,凑成一个接近真实的下单链路:下单 → 扣库存 → 扣余额 → 整个过程要么全成功、要么全回滚。架构长这样:

论架构一图流

client(前端)→ api-gateway(9000,路由+鉴权+限流)→ order-service(8083,开全局事务)

order-service 里:① 写订单;② OpenFeign 调 stock-service(8084)扣库存;③ OpenFeign 调 account-service(8085)扣余额。

三个服务都注册到同一个 Nacos;Seata Server(TC)独立部署,管全局事务状态;Sentinel 在每个 Feign 调用上做熔断兜底。

关键:order-service 的下单方法加 @GlobalTransactional,扣库存或扣余额任何一步失败,Seata 自动把已扣的库存、已扣的钱全回滚。

第一步:起 Seata Server(TC),每个业务库建 undo_log 表

# 1. 下载 Seata 2.x Server,conf 里设 registry=nacos,启动 TC # 2. order_db / stock_db / account_db 三个库,每个都执行官方 undo_log 建表脚本 CREATE TABLE undo_log ( branch_id BIGINT NOT NULL, xid VARCHAR(128) NOT NULL, rollback_info LONGBLOB NOT NULL, log_status INT, log_created DATETIME, log_modified DATETIME );

第二步:三个服务都加 seata 依赖 + 配 TC 地址

<dependency> <groupId>io.seata</groupId> <artifactId>seata-spring-boot-starter</artifactId> <!-- 版本随 spring-cloud-alibaba BOM 管理 --> </dependency> # application.yml 里: seata: tx-service-group: order-tx-group service: vgroup-mapping: order-tx-group: default registry: type: nacos nacos: server-addr: localhost:8848 # TC 也注册在 Nacos

第三步:扣库存、扣余额两个远程接口(被 order-service 调)

// stock-service:扣库存 @RestController @RequestMapping("/stock") public class StockController { @PostMapping("/deduct") public boolean deduct(@RequestParam Long productId, @RequestParam Integer count) { // 本地事务:update stock set count = count - #{count} return stockMapper.deduct(productId, count) > 0; } } // account-service:扣余额(Feign 接口在 order-service 侧定义) @PostMapping("/deduct") public boolean deduct(@RequestParam Long userId, @RequestParam BigDecimal money) { // 本地事务:update account set balance = balance - #{money} return accountMapper.deduct(userId, money) > 0; }

第四步:order-service 下单方法,@GlobalTransactional 一键管全局

@Service public class OrderService { @Autowired StockClient stockClient; // Feign 调 stock-service @Autowired AccountClient accountClient; // Feign 调 account-service @GlobalTransactional // ← 就这一行,全局事务开关 public void placeOrder(Long userId, Long productId, Integer count) { // 1. 本地:插入订单(order_db) orderMapper.insert(new Order(userId, productId, count)); // 2. 远程:扣库存(stock_db) boolean ok1 = stockClient.deduct(productId, count); // 3. 远程:扣余额(account_db)—— 假设这里余额不足抛异常 boolean ok2 = accountClient.deduct(userId, count); // 任何一步抛异常,Seata 自动反向回滚: // 已扣的库存补回去、已插的订单删掉,不留半成品 } }

验证:故意让扣余额失败,看全局回滚

# 1. 正常下单:库存扣减、余额扣减、订单生成,三处一致 POST /order/place?userId=1&productId=1&count=1 → 下单成功 # 2. 把 account-service 里的余额设成 0,再下单: # placeOrder 走到扣余额那步抛异常 # 预期:订单没了、库存也没扣(被 Seata 回滚) POST /order/place?userId=1&productId=1&count=1 → 余额不足,事务回滚 # 查 stock_db:count 没变;查 order_db:没有这条订单 —— 全回滚干净
记
这串链路怎么读

① Gateway 统一入口做鉴权限流;② 所有服务靠 Nacos 互相发现;③ 服务间用 OpenFeign 声明式调;④ 下游挂了 Sentinel 兜底;⑤ 跨库一致性用 Seata @GlobalTransactional 管。

⑥ 别一上来就全用:先 Nacos+Gateway+Feign,跑通了再加 Sentinel,最后才是 Seata。