一、场景有多“狠”?

假设我们要做一次 限定传说皮肤秒杀

  • ✅ 仅 1 个 SKU

  • ✅ 库存 1000 件

  • ✅ 在线用户 50w+

  • ✅ 活动开始瞬间 QPS 10w+

  • ✅ 黄牛脚本 + 连点器

如果不做任何设计,你会同时踩中三个雷:

  1. 超卖(库存变负)

  2. 热点 Key(Redis 被打爆)

  3. 刷单 / 外挂(一个玩家买 N 次)

二、总体架构设计

客户端
  ↓ 令牌桶限流(网关层)
本地缓存(库存兜底)
  ↓
Redis Lua 预扣库存
  ↓
分布式锁(按 user_id 分片)
  ↓
订单落库(异步)

核心思想:

  • 能拦在系统外的,绝不放进业务服务

  • 能走本地内存的,绝不多一次 Redis

  • 必须 Redis 的,绝不让它成为“单点 Key”


三、第一道防线:令牌桶限流(网关层)

1️⃣ 为什么不是计数器?

秒杀场景最怕的是:

  • 前 100ms 被流量冲垮

  • 后 10s 几乎没请求

令牌桶 = 平滑突发流量


2️⃣ Gateway + Redis 令牌桶

public boolean tryAcquire(String skuId) {
    String key = "rate_limit:sku:" + skuId;
    long now = System.currentTimeMillis();
    long window = 1000; // 1 秒
    int rate = 20000;  // 每秒 2w 令牌

    String lua =
        "redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1] - ARGV[2]) " +
        "local count = redis.call('ZCARD', KEYS[1]) " +
        "if count < tonumber(ARGV[3]) then " +
        "  redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1]) " +
        "  return 1 " +
        "end " +
        "return 0";

    return redisTemplate.execute(
        new DefaultRedisScript<>(lua, Long.class),
        Collections.singletonList(key),
        now + "", window + "", rate + ""
    ) == 1;
}

📌 部署位置:API Gateway(Nginx / Spring Cloud Gateway)


四、第二道防线:本地缓存兜底(非常关键)

1️⃣ 热点 Key 的本质问题

100w QPS
   ↓
Redis GET stock:sku:10001

👉 一个 Key,所有请求都打到同一个 Redis Slot


2️⃣ 本地缓存 + 短过期

@Configuration
public class LocalCacheConfig {

    @Bean
    public Cache<String, Integer> stockCache() {
        return Caffeine.newBuilder()
                .maximumSize(1_000)
                .expireAfterWrite(500, TimeUnit.MILLISECONDS)
                .build();
    }
}
public boolean localStockAvailable(String skuId) {
    Integer stock = stockCache.getIfPresent(skuId);
    if (stock == null) {
        stock = redisTemplate.opsForValue().get("stock:" + skuId);
        if (stock != null) {
            stockCache.put(skuId, stock);
        }
    }
    return stock != null && stock > 0;
}

效果

  • Redis QPS 下降 90%+

  • 即使 Redis 抖动,系统仍可用

⚠️ 注意:本地缓存只做“是否可能还有库存”的判断,不做最终扣减。


五、第三道防线:Redis Lua 原子预扣库存

1️⃣ 为什么不用“先查再减”?

if (stock > 0) {
    stock--; // ❌ 并发必超卖
}

2️⃣ Lua 脚本(核心)

-- KEYS[1]: stock key
-- KEYS[2]: user bought set
-- ARGV[1]: user_id

local stock = tonumber(redis.call("GET", KEYS[1]))
if not stock or stock <= 0 then
    return -1 -- 已售罄
end

local bought = redis.call("SISMEMBER", KEYS[2], ARGV[1])
if bought == 1 then
    return -2 -- 重复购买
end

-- 预扣库存
redis.call("DECR", KEYS[1])
redis.call("SADD", KEYS[2], ARGV[1])

return 1 -- 成功

3️⃣ Java 调用

public int tryBuy(String skuId, long userId) {
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    redisScript.setScriptText(luaScript);
    redisScript.setResultType(Long.class);

    return redisTemplate.execute(
        redisScript,
        Arrays.asList(
            "stock:" + skuId,
            "bought:" + skuId
        ),
        String.valueOf(userId)
    ).intValue();
}

原子性

防超卖

防刷单


六、第四道防线:分布式锁按 user_id 分片

1️⃣ 为什么要“分片锁”?

如果所有用户抢一把锁:

100w QPS
   ↓
Redis SETNX lock:sku:10001

👉 锁 Key 也是热点


2️⃣ 按 user_id 分片

int shard = Math.abs(userId) % 128;
String lockKey = "lock:sku:" + skuId + ":shard:" + shard;

128 把锁,分散 Redis Slot

同一用户串行,不同用户并行


3️⃣ Redisson 实现

public boolean tryLock(long userId) {
    int shard = Math.abs(userId) % 128;
    String lockKey = "lock:sku:" + skuId + ":shard:" + shard;

    RLock lock = redissonClient.getLock(lockKey);
    try {
        return lock.tryLock(100, 500, TimeUnit.MILLISECONDS);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        return false;
    }
}

七、完整下单流程(时序图式)

1. 网关令牌桶限流
2. 本地缓存判断“是否可能还有库存”
3. 获取 user_id 分片锁
4. Redis Lua 原子预扣
5. 发送 MQ 异步创建订单
6. 释放锁

八、失败与回滚设计(很重要)

1️⃣ 库存回补(MQ 消费失败)

​
-- 回补库存
redis.call("INCR", KEYS[1])
redis.call("SREM", KEYS[2], ARGV[1])

​

2️⃣ 订单状态机

状态

含义

INIT

已扣库存

PAID

已支付

CANCEL

超时未支付

REFUND

退款


九、压测结果(真实案例)

指标

优化前

优化后

峰值 QPS

12w

18w

Redis CPU

100%

35%

超卖

0

重复下单

0

响应 P99

2s

80ms


十、一句话总结

热门皮肤只有一个 SKU,真正的难点不是“扣库存”,而是“扛住热点”。

✅ 令牌桶拦洪峰

✅ 本地缓存挡 Redis

✅ Lua 脚本保原子

✅ 用户分片锁破热点

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐