RedisNotes

第 12 章:高并发场景实战

zjc 于 2026-01-12 发布

这是《Redis 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 本章把常见高并发场景写成可直接落地的 Redis 方案:计数、点赞、排行榜、限流、库存扣减、签到与 UV。

12.1 计数器

INCR read:article:1001
INCRBY like:comment:2001 10
DECR stock:sku:1001

服务:

public long increase(String key) {
    Long value = redis.opsForValue().increment(key);
    return value == null ? 0 : value;
}

分布式计数的一致性

Redis 计数是实时准确的(单实例内),但若需要审计和可追溯,不能只保留一个数字。推荐:

Redis: 高频计数缓存
DB: 定期落盘流水/快照
对账: 累计流水 vs Redis 计数

避免 Redis 故障后计数完全丢失。

12.2 点赞与取消点赞

用 Set 记录点赞用户,防止重复:

SADD like:article:1001 user:1
SREM like:article:1001 user:1
SISMEMBER like:article:1001 user:1
SCARD like:article:1001

原子点赞脚本:

local liked = redis.call('SISMEMBER', KEYS[1], ARGV[1])
if liked == 1 then
    return 0
end
redis.call('SADD', KEYS[1], ARGV[1])
redis.call('INCR', KEYS[2])
return 1

如果只需要数量、不要求精确名单,可用 HyperLogLog 或 Set + 定期归档。

12.3 排行榜

ZINCRBY rank:activity:2026 10 user:1001
ZREVRANGE rank:activity:2026 0 9 WITHSCORES
ZREVRANK rank:activity:2026 user:1001
ZRANGE rank:activity:2026 0 -1 WITHSCORES

服务层:

public List<RankItem> top10(String key) {
    Set<ZSetOperations.TypedTuple<String>> tuples =
            redis.opsForZSet().reverseRangeWithScores(key, 0, 9);

    List<RankItem> result = new ArrayList<>();
    int rank = 1;
    for (var tuple : tuples) {
        result.add(new RankItem(
                rank++,
                Objects.requireNonNull(tuple.getValue()),
                tuple.getScore()));
    }
    return result;
}

分页

ZREVRANGE rank:activity 0 9
ZREVRANGE rank:activity 10 19

排名并列需要业务规则。ZSet 分数相同时按 member 字典序排序,不一定符合“同分早到者靠前”。

12.4 固定窗口限流

key = rate:{subject}:{windowStart}
每个请求 INCR,首次设置 TTL
超过阈值拒绝

Lua:

local current = redis.call('INCR', KEYS[1])
if current == 1 then
    redis.call('PEXPIRE', KEYS[1], ARGV[1])
end
if current > tonumber(ARGV[2]) then
    return 0
end
return 1

Java 调用:

public boolean allowFixedWindow(String subject,
                                int limit,
                                long windowMillis) {
    long window = System.currentTimeMillis() / windowMillis;
    String key = "rate:" + subject + ":" + window;
    Long result = redis.execute(fixedWindowScript,
            List.of(key), String.valueOf(windowMillis), String.valueOf(limit));
    return result != null && result == 1;
}

优点:实现简单。缺点:窗口边界突刺,例如 59 秒和 61 秒可能集中双倍流量。

12.5 滑动窗口限流

用 ZSet 保存请求时间戳:

local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local member = ARGV[4]

redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - window)
local count = redis.call('ZCARD', KEYS[1])
if count >= limit then
    return 0
end
redis.call('ZADD', KEYS[1], now, member)
redis.call('PEXPIRE', KEYS[1], window)
return 1

优点:边界更平滑。缺点:每个请求多记录一个 member,内存和清理成本更高。

12.6 令牌桶限流

思想:

rate: 每秒生成令牌数
capacity: 桶容量
每次请求按当前时间补充令牌,尝试消费 1 个

适合允许短时突发但平均速率受限的接口。生产常用 Redisson RRateLimiter,或网关层限流。

选型:

算法 特点
固定窗口 简单,边界突刺
滑动窗口 平滑,成本略高
令牌桶 支持突发,参数清晰
漏桶 强制平滑,常用于整形

12.7 秒杀库存扣减

朴素 DECR 会超卖:

库存 0
A DECR -> -1
B DECR -> -2

Lua 保证检查和扣减原子:

local stock = tonumber(redis.call('GET', KEYS[1]))
if stock == nil then
    return -1
end

local num = tonumber(ARGV[1])
if stock < num then
    return 0
end

redis.call('DECRBY', KEYS[1], num)
redis.call('LPUSH', KEYS[2], ARGV[2])
return 1

解释:

完整链路:

1. 前端/网关削峰,防止恶意刷请求
2. Redis Lua 原子校验并扣减库存
3. 扣减成功写入抢购成功队列/Stream
4. 异步创建订单
5. 数据库乐观锁 + 唯一订单号兜底
6. 支付超时回补库存

12.8 签到与连续签到

按月 Bitmap:

SETBIT sign:user:1001:202608 24 1
GETBIT sign:user:1001:202608 24
BITCOUNT sign:user:1001:202608

Java:

public void sign(long userId, LocalDate date) {
    String key = "sign:user:%d:%s".formatted(
            userId, date.format(DateTimeFormatter.ofPattern("yyyyMM")));
    redis.opsForValue().setBit(key, date.getDayOfMonth() - 1, true);
}

连续签到可:

  1. 从今天向前逐位 GETBIT
  2. 使用 BITCOUNT 判断月内次数;
  3. 复杂奖励规则落 DB 或事件表。

12.9 UV 统计

精确去重:

SADD uv:page:1001 user:1001
SCARD uv:page:1001

估算去重:

PFADD uv:page:1001 user:1001
PFCOUNT uv:page:1001
PFMERGE uv:all uv:page:1 uv:page:2

选型:

需求 方案
精确名单 Set / DB
精确人数 Set / DB
大规模估算 HyperLogLog
签到/活跃位 Bitmap

12.10 场景通用注意事项

  1. 所有写操作考虑幂等;
  2. Redis 数据要明确可丢失等级;
  3. 高并发读要有本地缓存或多级缓存;
  4. 高并发写要评估单 key 热点;
  5. 定时任务要有分布式防重;
  6. 关键业务必须与数据库对账。

本章小结

思考题

  1. 为什么 DECR 后再判断小于 0 并回滚仍可能有问题?
  2. 分布式限流在 Redis 故障时应该拒绝所有请求还是放行?如何设计?
  3. 设计一个“活动排行榜 + 实时排名 + 历史月榜”的完整 key 结构。