十二
完整案例:电商下单 + 分布式事务
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。