从CRUD到系统设计

3 阅读1分钟

在 Java 后端开发中,下单接口是一个高频且高风险的场景。

很多项目在功能层面可以快速上线,但在流量上涨或异常重试时容易暴露系统性问题。常见评审意见也集中在同一点:

功能实现完整,但系统设计考虑不足。

本文用一个简化的 POST /orders 场景,对比“功能可用”与“工程可用”的实现差异。

【场景:下单接口的典型风险】

促销活动开始后,POST /orders 可能出现三类问题:

  1. 1. 用户重复点击,出现重复订单。

  2. 2. 瞬时流量冲高,服务被打挂。

  3. 3. 热点商品被频繁查询,数据库飙高。

如果只关注业务主流程,问题通常会在高峰流量或异常链路下出现。

更稳妥的做法是先定义系统边界:请求量级、重试行为、依赖稳定性和数据一致性要求。

【一个“可运行但风险高”的实现】

@PostMapping("/orders")
public Long create(@RequestBody CreateOrderRequest req) {
   Order order = new Order(req.getUserId(), req.getProductId(), req.getQuantity());
   return orderRepository.save(order).getId();
}

这段代码在测试环境通常可以通过,但线上容易暴露以下问题:

  • • 没有幂等:重复请求会下多单

  • • 没有限流:峰值流量直接压垮服务

  • • 没有缓存:热点查询都打到数据库

【系统化改造:3个基础动作】

1)限流:先保护系统,再谈体验

if (!rateLimiter.tryAcquire()) {
   return ApiResponse.fail("系统繁忙,请稍后再试");
}

作用:将故障影响从系统级扩散控制为可接受的局部拒绝。

2)幂等:保证一次业务只生效一次

if (!idempotencyService.tryStart(request.getIdempotencyKey())) {
   return ApiResponse.fail("重复请求,请勿重复提交");
}

作用:避免连点、超时重试、回放请求导致重复下单。

3)缓存:把热点从数据库前移

@Cacheable(cacheNames = "productStock", key = "#productId")
public Integer queryStock(Long productId) {
   return stockRepository.queryStock(productId);
}

作用:对读多写少的热点数据进行前置缓存,降低数据库读取压力。

【把3个动作串起来:一段伪代码就够了】

下面的伪代码给出一个可复用的下单主流程模板:

public ApiResponse createOrder(CreateOrderRequest req) {
    // 1. 限流保护
    if (!rateLimiter.allow("create-order", req.getUserId())) {
        return ApiResponse.fail("系统繁忙,请稍后再试");
    }
    // 2. 幂等控制(前端传 Idempotency-Key)
    String key = req.getIdempotencyKey();
    if (!idempotency.tryLock(key, 30)) {
        return ApiResponse.fail("重复请求,请勿重复提交");
    }
    try {
        // 3. 读缓存,减少数据库压力
        int stock = cache.getOrLoad("stock:" + req.getProductId(), () -> db.queryStock(req.getProductId())); 
        if (stock < req.getQuantity()) {
            return ApiResponse.fail("库存不足");
        }
        // 4. 扣减库存 + 创建订单(建议同事务)
        transaction.execute(() -> {
            db.decreaseStock(req.getProductId(), req.getQuantity());
            db.createOrder(req.getUserId(), req.getProductId(), req.getQuantity());
        });
        return ApiResponse.ok("下单成功"); 
   } finally {
        idempotency.release(key);
   }
}

这段流程不依赖特定框架,重点是把“流量保护、幂等控制、数据一致性”放在同一条链路中统一处理。

【系统设计的检查清单】

每次设计接口,建议先回答以下 4 个问题:

  1. 1. 如果用户重复提交,会发生什么?

  2. 2. 如果流量突然翻 10 倍,会发生什么?

  3. 3. 如果依赖服务抖动,会发生什么?

  4. 4. 如果数据库压力过高,有没有前置缓冲?

如果这 4 个问题能形成固定的评审习惯,接口稳定性和可维护性会显著提升。

后续会继续写:

  • • Java API 设计 7 个坑

  • • MySQL 慢查询排查路线

  • • Redis 三大问题工程解法

  • • Kafka vs RabbitMQ 选型模板

  • • 分布式事务落地策略