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)正好适合这种场景。


示例代码

java 复制代码
@Service
public class OrderService {

    @Autowired
    private 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 令牌桶原理

令牌桶算法的核心思想是:

  1. 初始化‌:设置一个桶(bucket),里面有一定数量的令牌(tokens)。
  2. 令牌生成‌:每隔一定的时间(例如每秒),向桶中添加一定数量的令牌。
  3. 请求处理‌:每次处理请求时,从桶中取出一个令牌。如果没有足够的令牌,则请求被阻塞或延迟处理。
复制代码
       +---------------------+
       |  Redis Key: rate:api |
       |---------------------|
       | tokens = 10         |
       | last_time = 1690000 |
       +---------------------+
                  |
      ┌────────────────────────────┐
      │ 每次请求到来:              │
      │  1. 计算可新增令牌数量       │
      │  2. 判断是否有足够令牌       │
      │  3. 减少令牌 or 拒绝请求     │
      └────────────────────────────┘

3. 示例代码(Redis + Lua 实现令牌桶)

java 复制代码
@Component
public class RedisRateLimiter {

    @Autowired
    private 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 then
                tokens = capacity
                timestamp = now
            end

            local delta = math.max(0, now - timestamp)
            local new_tokens = math.min(capacity, tokens + delta * rate)
            local allowed = new_tokens >= requested
            if allowed then
                new_tokens = new_tokens - requested
            end

            redis.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;
    }
}

✅ 使用示例

java 复制代码
@RestController
@RequestMapping("/api/order")
public class OrderController {

    @Autowired
    private 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 脚本实现

java 复制代码
@Service
public class StockService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String STOCK_LUA = """
        local stock = tonumber(redis.call('GET', KEYS[1]))
        if stock <= 0 then
            return -1
        end
        redis.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;
    }
}

✅ 使用效果

java 复制代码
@RestController
@RequestMapping("/api/stock")
public class StockController {

    @Autowired
    private StockService stockService;

    @GetMapping("/deduct")
    public String deduct(@RequestParam String productId) {
        boolean success = stockService.deductStock(productId);
        return success ? "扣减成功" : "库存不足";
    }
}

六、总结与扩展

模块 Redis 技术点 适用场景
防重下单 SETNX + TTL 幂等请求
接口限流 Lua 脚本 + 令牌桶算法 防止接口过载
库存扣减 Lua + 原子操作 秒杀库存一致性

Redis 不仅仅是一个缓存,更是一个轻量级高性能并发控制中心

在实际项目中,这些功能可以与 RedissonSpring AOP分布式锁 结合,构建更健壮的高并发架构。

相关推荐
木易2.03 小时前
从零构建RAG知识库管理系统(二)
数据库·oracle
程序新视界4 小时前
什么是MySQL JOIN查询的驱动表和被驱动表?
数据库·后端·mysql
lingggggaaaa4 小时前
小迪安全v2023学习笔记(一百三十四讲)—— Windows权限提升篇&数据库篇&MySQL&MSSQL&Oracle&自动化项目
java·数据库·windows·笔记·学习·安全·网络安全
小光学长4 小时前
基于Vue的保护动物信息管理系统r7zl6b88 (程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
前端·数据库·vue.js
一匹电信狗4 小时前
【MySQL】数据库的相关操作
linux·运维·服务器·数据库·mysql·ubuntu·小程序
TDengine (老段)6 小时前
连接 TDengine 遇到报错 “failed to connect to server, reason: Connection refused” 怎么办?
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
李慕婉学姐7 小时前
Springboot黄河文化科普网站5q37v(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
Southern Wind7 小时前
Vue 3 多实例 + 缓存复用:理念及实践
前端·javascript·vue.js·缓存·html
Cabbage_acmer7 小时前
MySQL期中考试突击!
数据库·mysql