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分布式锁 结合,构建更健壮的高并发架构。

相关推荐
IT 小阿姨(数据库)11 小时前
PostgreSQL 之上的开源时序数据库 TimescaleDB 详解
运维·数据库·sql·postgresql·开源·centos·时序数据库
JanelSirry11 小时前
缓存击穿,缓存穿透,缓存雪崩的原因和解决方案(或者说使用缓存的过程中有没有遇到什么问题,怎么解决的)
缓存
熊文豪12 小时前
openEuler 云原生实战:部署高性能 Redis 集群与压测分析
数据库·redis·云原生·openeuler
GTgiantech12 小时前
科普SFP 封装光模块教程
服务器·网络·数据库
深圳市恒讯科技12 小时前
如何在服务器上安装和配置数据库(如MySQL)?
服务器·数据库·mysql
言之。12 小时前
TiDB分布式数据库技术架构概述
数据库·分布式·tidb
万事大吉CC13 小时前
SQL表设计与约束教程
数据库·sql
员大头硬花生13 小时前
七、InnoDB引擎-架构-后台线程
java·数据库·mysql
Ryan ZX13 小时前
etcd 高可用分布式键值存储
数据库·分布式·etcd
研究司马懿13 小时前
【ETCD】ETCD——confd配置管理
数据库·golang·自动化·运维开发·etcd·argocd·gitops