场景面试题:如何避免用户重复下单(多次下单未支付,占用库存)
📅 2025-12-29 21:20:52阅读时间: 16分钟
Spring Boot 如何避免用户重复下单(多次下单未支付,占用库存)?—— 面试题详解
在高并发电商系统中,一个常见的问题是:用户短时间内多次点击“下单”按钮,生成多个未支付订单,导致库存被无效占用,影响其他用户购买。这不仅浪费系统资源,还可能引发超卖或库存不足的问题。
本文将围绕 Spring Boot 技术栈,从原理、方案设计到代码实现,详细讲解如何有效防止用户重复下单,并形成一套可落地的解决方案。
一、问题场景还原(面试题背景)
面试官提问:
“在我们的电商平台中,经常有用户在未支付的情况下多次点击下单按钮,造成多个待支付订单和库存锁定。请问你如何在 Spring Boot 项目中解决这个问题?”
这是一个典型的 幂等性 + 库存管理 + 分布式锁 综合问题。
二、核心思路分析
要解决重复下单问题,需从以下维度入手:
- 前端防重(基础但不可靠)
- 后端接口幂等性控制
- 库存预占与释放机制
- 分布式锁防止并发创建订单
- 定时任务清理未支付订单
注意:仅靠前端防重是不够的,必须在后端做兜底。
三、Spring Boot 实现方案详解
方案一:基于 Redis 的幂等令牌(Token)机制 ✅(推荐)
原理:
- 用户进入下单页面时,后端生成一个唯一
orderToken并存入 Redis(带过期时间,如 5 分钟)。 - 提交订单时,携带该
orderToken。 - 后端校验 Token 是否存在且有效,若存在则消费(删除),并继续创建订单;否则拒绝请求。
优点:
- 简单高效,天然支持幂等。
- 可防止用户刷新页面重复提交。
代码实现:
java
// 1. 生成下单 Token
@RestController
public class OrderController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/createOrderToken")
public String createOrderToken(@RequestParam Long userId) {
String token = UUID.randomUUID().toString();
String key = "ORDER_TOKEN:" + userId;
redisTemplate.opsForValue().set(key, token, Duration.ofMinutes(5));
return token;
}
// 2. 提交订单
@PostMapping("/submitOrder")
public ResponseEntity<String> submitOrder(@RequestBody OrderRequest request) {
String tokenKey = "ORDER_TOKEN:" + request.getUserId();
String storedToken = redisTemplate.opsForValue().get(tokenKey);
if (storedToken == null || !storedToken.equals(request.getOrderToken())) {
return ResponseEntity.badRequest().body("重复提交或 Token 无效");
}
// 尝试删除 Token(原子操作)
Boolean deleted = redisTemplate.delete(tokenKey);
if (!deleted) {
return ResponseEntity.badRequest().body("请勿重复提交");
}
// 执行创建订单逻辑(需加分布式锁,见下文)
createRealOrder(request);
return ResponseEntity.ok("下单成功");
}
}
方案二:基于用户 + 商品维度的分布式锁
即使有 Token,仍需防止极端情况下的并发请求(如用户开多个 tab 同时下单)。
使用 Redisson 分布式锁:
java
@Autowired
private RedissonClient redisson;
private void createRealOrder(OrderRequest request) {
String lockKey = "ORDER_LOCK:" + request.getUserId() + ":" + request.getProductId();
RLock lock = redisson.getLock(lockKey);
try {
boolean acquired = lock.tryLock(1, 10, TimeUnit.SECONDS);
if (!acquired) {
throw new RuntimeException("系统繁忙,请稍后再试");
}
// 再次检查是否已有未支付订单(双重校验)
if (hasUnpaidOrder(request.getUserId(), request.getProductId())) {
throw new RuntimeException("您有未支付订单,请先完成支付");
}
// 扣减预占库存 & 创建订单
inventoryService.reserveStock(request.getProductId(), request.getQuantity());
orderService.createOrder(request);
} finally {
lock.unlock();
}
}
方案三:库存预占 + 定时释放
- 下单时 预占库存(非真实扣减),设置过期时间(如 15 分钟)。
- 若用户未支付,通过 定时任务 或 延迟队列 自动释放库存。
示例(使用 Redis 过期键 + 监听):
java
// 预占库存
public void reserveStock(Long productId, Integer quantity) {
String stockKey = "STOCK_RESERVED:" + productId;
// 使用 Lua 脚本保证原子性
redisTemplate.execute(reserveStockScript,
Collections.singletonList(stockKey),
quantity.toString(),
Duration.ofMinutes(15).getSeconds()
);
}
// Lua 脚本示例(伪代码)
// eval "if redis.call('GET', KEYS[1]) then ... end"
更高级方案:使用 RabbitMQ 延迟队列 或 Redis Streams + 消费者监听过期事件。
方案四:数据库层面兜底(最终防线)
在订单表中添加唯一约束或状态检查:
sql
-- 订单表增加联合索引(用户ID + 商品ID + 状态)
CREATE UNIQUE INDEX idx_user_product_unpaid
ON orders(user_id, product_id)
WHERE status = 'UNPAID';
注意:MySQL 需 8.0+ 支持函数索引,否则可建普通索引 + 应用层查询判断。
Java 层校验:
java
public boolean hasUnpaidOrder(Long userId, Long productId) {
return orderRepository.existsByUserIdAndProductIdAndStatus(
userId, productId, OrderStatus.UNPAID
);
}
四、完整流程图(逻辑顺序)
用户 → 获取 orderToken → 提交订单(带 Token)
↓
后端校验 Token(Redis)→ 失败?→ 返回“重复提交”
↓ 成功
获取分布式锁(用户+商品维度)
↓
检查是否存在未支付订单 → 是?→ 返回“请先支付”
↓ 否
预占库存(Redis + 过期)
↓
创建订单(DB)
↓
返回成功
↓
【后台】15分钟后未支付 → 释放库存 + 关闭订单
五、面试回答模板(精简版)
“针对用户重复下单问题,我会采用 前后端结合 + 多层防护 的策略:
- 前端:点击后禁用按钮,防止快速连点;
- 后端:
- 引入 下单 Token 机制,利用 Redis 实现接口幂等;
- 使用 Redisson 分布式锁,按用户+商品维度加锁,防止并发创建;
- 下单前 查询是否存在未支付订单,若有则拒绝;
- 采用 库存预占 + 定时释放 机制,避免库存长期被占用;
- 兜底:数据库加唯一索引或应用层校验,确保数据一致性。
此外,配合 延迟队列 或 定时任务 自动清理超时未支付订单,释放库存。”
六、扩展思考
- 如何处理 Token 被恶意刷取?
→ 限流(如 Guava RateLimiter 或 Sentinel) - 如何保证库存预占的原子性?
→ Redis Lua 脚本 or 分布式事务(Seata,但成本高) - 高并发下 Redis 成为瓶颈?
→ 分片、本地缓存 + 二级缓存策略
总结
防止重复下单不是单一技术点,而是 系统性工程。在 Spring Boot 项目中,应结合 Redis 幂等令牌、分布式锁、库存预占、定时清理 等手段,构建多层次防御体系。既保障用户体验,又确保系统稳定性和数据一致性。
💡 关键点:幂等性是核心,分布式锁是保障,库存释放是闭环。
希望这篇文章能帮助你在面试中从容应对“重复下单”类问题!