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

相关推荐
憧憬成为web高手18 分钟前
皮卡丘靶场速通--sql 2
数据库·sql·mybatis
段一凡-华北理工大学31 分钟前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
二宝哥1 小时前
CentOS 7.9 系统下 Redis 5.0.4 单机安装与源码编译安装指南
redis·centos
oradh2 小时前
Oracle XTTS实现跨版本迁移和升级(Oracle 11g单库升级至19C RAC集群)
数据库·oracle·11g升级19c·xtts跨版本迁移和升级
z123456789863 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
程序猿DD3 小时前
一个 API Key,统一调用大模型、生图和联网搜索
数据库·网关
TlSfoward4 小时前
抓包代理链路下的 TLS 指纹变化分析 TLSFOWARD抓包工具
数据库·爬虫·网络协议·搜索引擎·php
数智化管理手记5 小时前
账龄分析手工统计易遗漏?自动账龄分析工具怎么搭建
大数据·网络·数据库·人工智能·数据挖掘
LabVIEW开发5 小时前
NI Package Manager安装LabVIEW时InternalServerError错误
网络·数据库·labview·labview知识·labview功能·labview程序
SelectDB5 小时前
PostgreSQL 实时同步到 Apache Doris:一站式 CDC 解决方案
数据库