这是《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
解释:
KEYS[1]:库存 key;KEYS[2]:抢购成功队列;ARGV[1]:购买数量;ARGV[2]:订单请求 ID。
完整链路:
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);
}
连续签到可:
- 从今天向前逐位
GETBIT; - 使用
BITCOUNT判断月内次数; - 复杂奖励规则落 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 场景通用注意事项
- 所有写操作考虑幂等;
- Redis 数据要明确可丢失等级;
- 高并发读要有本地缓存或多级缓存;
- 高并发写要评估单 key 热点;
- 定时任务要有分布式防重;
- 关键业务必须与数据库对账。
本章小结
- 计数用 String,点赞去重用 Set,排行用 ZSet;
- 固定窗口简单但边界突刺,滑动窗口和令牌桶更平滑;
- 库存扣减必须把检查与扣减放入 Lua;
- 秒杀要前端削峰、Redis 原子扣减、异步建单、数据库兜底;
- Bitmap/HyperLogLog 分别解决活跃标记与海量 UV 估算。
思考题
- 为什么
DECR后再判断小于 0 并回滚仍可能有问题? - 分布式限流在 Redis 故障时应该拒绝所有请求还是放行?如何设计?
- 设计一个“活动排行榜 + 实时排名 + 历史月榜”的完整 key 结构。