Redis 在订单系统中的实战应用:防重、限流与库存扣减
一、背景:为什么订单系统离不开 Redis?
在高并发订单系统中,我们常常面临三大问题:
问题类型 | 说明 | 常见后果 |
---|---|---|
防重下单 | 用户重复点击“提交订单”按钮 | 重复扣款、重复库存 |
接口限流 | 秒杀/大促活动瞬时请求暴增 | 系统崩溃、拒绝服务 |
库存扣减 | 多人同时抢购同一商品 | 超卖、库存为负数 |
Redis 凭借高性能 + 原子操作 + 多数据结构支持,成为电商类系统“抗高并发”的首选组件。
二、整体架构思维导图
Redis 在订单系统中的实战
├── 防重下单
│ ├── 原理:唯一 Key + TTL
│ └── 实现:SETNX 防重复提交
├── 限流算法
│ ├── 固定窗口
│ ├── 滑动窗口
│ ├── 令牌桶(Token Bucket)
│ └── 漏桶(Leaky Bucket)
└── 库存扣减├── 原理:Redis 原子操作 + Lua 脚本└── 实现:watch + eval 实现库存一致性
三、防重下单:用 Redis 保证接口幂等性
理论分析
在电商系统中,用户点击“提交订单”可能由于:
网络延迟;
前端多次触发;
用户误操作。
导致同一订单被重复创建。
用户操作 | 请求时间 | 服务器行为 | 结果 |
---|---|---|---|
第一次点击下单 | 0ms | 创建订单A | ✅ 成功 |
第二次点击下单 | 10ms | 再次创建订单B | ❌ 重复下单 |
由于 HTTP 请求是无状态且幂等性不保证的,后端必须自己设计防重机制。
要解决这个问题,我们通常采用 幂等性机制:
相同业务请求(用户ID + 订单号)无论请求多少次,结果都一致。
Redis 提供的 SETNX
(set if not exists)正好适合这种场景。
示例代码
@Service
public class OrderService {@Autowiredprivate StringRedisTemplate redisTemplate;private static final String ORDER_REPEAT_KEY = "order:repeat:";public String createOrder(String userId, String productId) {String key = ORDER_REPEAT_KEY + userId + ":" + productId;Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "1", 5, TimeUnit.SECONDS); // 设置5秒过期防重复点击if (Boolean.FALSE.equals(success)) {throw new RuntimeException("请勿重复提交订单!");}// 模拟生成订单逻辑return "订单创建成功:" + UUID.randomUUID();}
}
四、接口限流:Redis 实现令牌桶算法
限流算法原理对比
算法 | 机制 | 特点 |
---|---|---|
固定窗口 | 每分钟允许 N 次请求 | 简单但可能突发请求 |
滑动窗口 | 实时统计过去 N 秒请求数 | 更平滑,但计算复杂 |
漏桶算法 | 固定速率流出 | 稳定但响应不够灵活 |
令牌桶算法 | 按速率生成令牌,请求需消耗令牌 | ✅ 最灵活且常用 |
在分布式环境中,我们用 Redis 原子 Lua 脚本 实现令牌桶算法,保证并发安全。
2.Redis 令牌桶原理
令牌桶算法的核心思想是:
- 初始化:设置一个桶(bucket),里面有一定数量的令牌(tokens)。
- 令牌生成:每隔一定的时间(例如每秒),向桶中添加一定数量的令牌。
- 请求处理:每次处理请求时,从桶中取出一个令牌。如果没有足够的令牌,则请求被阻塞或延迟处理。
+---------------------+| Redis Key: rate:api ||---------------------|| tokens = 10 || last_time = 1690000 |+---------------------+|┌────────────────────────────┐│ 每次请求到来: ││ 1. 计算可新增令牌数量 ││ 2. 判断是否有足够令牌 ││ 3. 减少令牌 or 拒绝请求 │└────────────────────────────┘
3. 示例代码(Redis + Lua 实现令牌桶)
@Component
public class RedisRateLimiter {@Autowiredprivate StringRedisTemplate redisTemplate;private static final String LUA_SCRIPT = """local key = KEYS[1]local rate = tonumber(ARGV[1]) -- 每秒生成令牌数local capacity = tonumber(ARGV[2]) -- 桶容量local now = tonumber(ARGV[3])local requested = tonumber(ARGV[4])local data = redis.call("HMGET", key, "tokens", "timestamp")local tokens = tonumber(data[1])local timestamp = tonumber(data[2])if tokens == nil thentokens = capacitytimestamp = nowendlocal delta = math.max(0, now - timestamp)local new_tokens = math.min(capacity, tokens + delta * rate)local allowed = new_tokens >= requestedif allowed thennew_tokens = new_tokens - requestedendredis.call("HMSET", key, "tokens", new_tokens, "timestamp", now)redis.call("EXPIRE", key, 60)return allowed""";public boolean tryAcquire(String key, int rate, int capacity, int requested) {long now = System.currentTimeMillis() / 1000;DefaultRedisScript<Long> script = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);Long result = redisTemplate.execute(script,Collections.singletonList("rate:" + key),String.valueOf(rate),String.valueOf(capacity),String.valueOf(now),String.valueOf(requested));return result != null && result == 1L;}
}
✅ 使用示例
@RestController
@RequestMapping("/api/order")
public class OrderController {@Autowiredprivate RedisRateLimiter rateLimiter;@GetMapping("/create")public String createOrder(@RequestParam String userId) {boolean pass = rateLimiter.tryAcquire("createOrder", 5, 10, 1);if (!pass) {return "请求过多,请稍后再试!";}return "订单创建成功!";}
}
五、库存扣减:Lua 保证原子性防止超卖
1️⃣ 问题分析
在秒杀场景下,多个用户同时购买同一商品:
请求1:读取库存=1;
请求2:读取库存=1;
两个线程同时扣减;
库存 = -1(超卖!)
所以必须保证库存扣减原子执行。
2️⃣ Redis Lua 脚本实现
@Service
public class StockService {@Autowiredprivate StringRedisTemplate redisTemplate;private static final String STOCK_LUA = """local stock = tonumber(redis.call('GET', KEYS[1]))if stock <= 0 thenreturn -1endredis.call('DECR', KEYS[1])return stock - 1""";public boolean deductStock(String productId) {DefaultRedisScript<Long> script = new DefaultRedisScript<>(STOCK_LUA, Long.class);Long result = redisTemplate.execute(script, Collections.singletonList("stock:" + productId));return result != null && result >= 0;}
}
✅ 使用效果
@RestController
@RequestMapping("/api/stock")
public class StockController {@Autowiredprivate StockService stockService;@GetMapping("/deduct")public String deduct(@RequestParam String productId) {boolean success = stockService.deductStock(productId);return success ? "扣减成功" : "库存不足";}
}
六、总结与扩展
模块 | Redis 技术点 | 适用场景 |
---|---|---|
防重下单 | SETNX + TTL | 幂等请求 |
接口限流 | Lua 脚本 + 令牌桶算法 | 防止接口过载 |
库存扣减 | Lua + 原子操作 | 秒杀库存一致性 |
Redis 不仅仅是一个缓存,更是一个轻量级高性能并发控制中心。
在实际项目中,这些功能可以与 Redisson、Spring AOP 或 分布式锁 结合,构建更健壮的高并发架构。