这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 本章以“订单交易系统”为例,把 Spring Boot、Spring Cloud、数据访问、缓存、消息、观测和发布整合成一个可落地的工程方案。
33.1 业务流程
用户下单
-> 校验商品和价格
-> 预占库存
-> 创建订单
-> 发起支付
-> 支付成功
-> 扣减库存
-> 创建履约单
-> 发送通知
非功能目标:
| 目标 | 指标 |
|---|---|
| 下单 P99 | <= 300ms |
| 可用性 | >= 99.95% |
| 消息可靠性 | 至少一次 + 幂等 |
| 发布 | 无损滚动 |
| 排障 | trace 全链路 |
| 回滚 | 5 分钟内 |
33.2 服务划分
gateway-service
order-service
product-service
inventory-service
payment-service
notification-service
数据所有权:
| 服务 | 数据 |
|---|---|
| order | orders、order_events |
| product | products、prices |
| inventory | stock、stock_logs |
| payment | payments、payment_logs |
| notification | notifications |
服务间:
- 下单前价格和库存用 API;
- 下单后流程用事件;
- 查询列表读订单投影;
- 报表进入数仓。
33.3 订单状态机
CREATED
-> STOCK_RESERVED
-> PAYING
-> PAID
-> FULFILLING
-> COMPLETED
-> CANCELLED
PAYING -> CLOSED
PAID -> REFUNDING -> REFUNDED
表结构:
create table orders (
id bigint primary key,
order_no varchar(32) not null unique,
user_id varchar(64) not null,
amount decimal(18,2) not null,
status varchar(32) not null,
idempotency_key varchar(64) not null unique,
created_at datetime(3) not null,
updated_at datetime(3) not null,
key idx_user_time(user_id, created_at)
);
状态迁移必须用条件更新:
update orders
set status = 'PAID', updated_at = now(3)
where id = ? and status = 'PAYING';
33.4 下单接口
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderView create(@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody CreateOrderRequest request) {
return orderService.create(request.toCommand(idempotencyKey));
}
}
请求:
public record CreateOrderRequest(
@NotBlank String userId,
@NotNull @Positive Long skuId,
@Min(1) @Max(100) int quantity) {
public CreateOrderCommand toCommand(String idempotencyKey) {
return new CreateOrderCommand(idempotencyKey, userId, skuId, quantity);
}
}
幂等键由客户端生成,服务端唯一约束兜底。
33.5 下单服务
@Service
public class OrderService {
@Transactional
public OrderView create(CreateOrderCommand command) {
if (repository.existsByIdempotencyKey(command.idempotencyKey())) {
return repository.findByIdempotencyKey(command.idempotencyKey()).toView();
}
ProductSnapshot product = productClient.get(command.skuId());
Reservation reservation = inventoryClient.reserve(
new ReserveRequest(command.orderNo(), command.skuId(), command.quantity()));
Order order = Order.create(command, product, reservation);
repository.save(order);
outboxRepository.save(OrderCreatedEvent.outbox(order));
return order.toView();
}
}
注意:
- 库存预占接口必须幂等;
- 本地事务保护订单和 outbox;
- 支付在订单确认后发起;
- 所有外部调用有超时;
- 失败要有明确状态或补偿。
33.6 库存设计
表:
create table stock (
sku_id bigint primary key,
available int not null,
frozen int not null,
version bigint not null default 0
);
预占:
update stock
set available = available - ?, frozen = frozen + ?, version = version + 1
where sku_id = ? and available >= ?;
确认:
update stock
set frozen = frozen - ?
where sku_id = ? and frozen >= ?;
释放:
update stock
set available = available + ?, frozen = frozen - ?
where sku_id = ? and frozen >= ?;
同时记录 stock_log,用于幂等、审计和对账。
33.7 Outbox 表
create table outbox_events (
event_id varchar(64) primary key,
event_type varchar(100) not null,
aggregate_id varchar(64) not null,
payload json not null,
status varchar(20) not null,
retry_count int not null default 0,
next_retry_at datetime(3) null,
created_at datetime(3) not null,
sent_at datetime(3) null,
key idx_status_time(status, created_at)
);
投递任务:
1. 查询 PENDING 且到达 next_retry_at
2. 按 aggregateId 顺序发送
3. 成功标记 SENT
4. 失败递增 retry_count
5. 超过阈值标记 FAILED 并告警
33.8 支付回调
@PostMapping("/api/payments/callback")
public void callback(@Valid @RequestBody PaymentCallback callback) {
paymentService.handle(callback);
}
处理:
1. 验签
2. 根据支付流水号查询本地记录
3. 校验金额和币种
4. 幂等更新支付状态
5. 发布 PaymentSucceeded
6. 返回成功
如果金额不一致,不能直接更新为成功,应进入异常处理流程。
33.9 缓存策略
| 数据 | 缓存 | TTL |
|---|---|---|
| 商品详情 | Redis + Caffeine | 5min |
| 用户基础信息 | Redis | 30min |
| 库存数量 | 短缓存或实时 | 视业务 |
| 订单列表 | 不缓存或个性化 | - |
商品更新:
更新 DB
-> 删除 Redis
-> 发布失效事件
-> 实例删除本地缓存
库存展示允许短暂近似值,下单扣减必须实时校验数据库。
33.10 观测方案
指标:
order_create_total
order_create_seconds
order_status_total
payment_callback_total
inventory_reserve_seconds
outbox_pending
consumer_lag
saga_compensation_total
关键日志:
orderId
orderNo
userId
paymentId
traceId
state transition
amount
告警:
- 下单失败率;
- 下单 P99;
- 支付回调异常;
- outbox 积压;
- Saga 补偿异常;
- 库存负数或冻结异常。
33.11 测试
| 层 | 测试 |
|---|---|
| 单元 | 状态机、金额、库存规则 |
| Web | 参数校验、响应、异常 |
| 集成 | MySQL、Redis、Kafka Testcontainers |
| 契约 | 服务 API 和事件 schema |
| E2E | 下单、支付、取消、退款 |
| 混沌 | 下游超时、DB 抖动、消息重复 |
必须覆盖:
- 重复 Idempotency-Key;
- 库存不足;
- 支付超时;
- 回调重复;
- 补偿失败;
- 消息乱序。
33.12 部署
配置:
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
memory: "2Gi"
发布:
- 数据库迁移;
- 服务金丝雀;
- 观察核心指标;
- 扩大流量;
- 保留回滚;
- 发布窗口避开高峰。
本章小结
订单系统实战的核心是状态机、幂等、数据所有权、Outbox 事件、库存补偿和全链路可观测。同步 API 用于实时校验,异步事件驱动后续流程,所有跨服务操作必须假设失败、重复和乱序,并通过对账兜底。
思考题
- 为什么订单创建和 Outbox 写入要在同一事务?
- 库存预占为什么不能只依赖缓存?
- 支付回调如何防伪造和重复?
- Saga 补偿失败如何处理?
- 订单系统至少要监控哪些指标?