SpringNotes

第 33 章:项目实战

zjc 于 2026-02-02 发布

这是《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

服务间:

  1. 下单前价格和库存用 API;
  2. 下单后流程用事件;
  3. 查询列表读订单投影;
  4. 报表进入数仓。

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();
    }
}

注意:

  1. 库存预占接口必须幂等;
  2. 本地事务保护订单和 outbox;
  3. 支付在订单确认后发起;
  4. 所有外部调用有超时;
  5. 失败要有明确状态或补偿。

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

告警:

  1. 下单失败率;
  2. 下单 P99;
  3. 支付回调异常;
  4. outbox 积压;
  5. Saga 补偿异常;
  6. 库存负数或冻结异常。

33.11 测试

测试
单元 状态机、金额、库存规则
Web 参数校验、响应、异常
集成 MySQL、Redis、Kafka Testcontainers
契约 服务 API 和事件 schema
E2E 下单、支付、取消、退款
混沌 下游超时、DB 抖动、消息重复

必须覆盖:

  1. 重复 Idempotency-Key;
  2. 库存不足;
  3. 支付超时;
  4. 回调重复;
  5. 补偿失败;
  6. 消息乱序。

33.12 部署

配置:

resources:
  requests:
    cpu: "1"
    memory: "2Gi"
  limits:
    memory: "2Gi"

发布:

  1. 数据库迁移;
  2. 服务金丝雀;
  3. 观察核心指标;
  4. 扩大流量;
  5. 保留回滚;
  6. 发布窗口避开高峰。

本章小结

订单系统实战的核心是状态机、幂等、数据所有权、Outbox 事件、库存补偿和全链路可观测。同步 API 用于实时校验,异步事件驱动后续流程,所有跨服务操作必须假设失败、重复和乱序,并通过对账兜底。

思考题

  1. 为什么订单创建和 Outbox 写入要在同一事务?
  2. 库存预占为什么不能只依赖缓存?
  3. 支付回调如何防伪造和重复?
  4. Saga 补偿失败如何处理?
  5. 订单系统至少要监控哪些指标?