Redis 高级用法 12 例:分布式锁 / 排行榜 / 限流
很多团队把 Redis 当作简单的 key-value 缓存用,但这样浪费了它至少 80% 的能力。Redis 的强大之处在于它提供了丰富的高级数据结构与原子命令,让原本需要复杂数据库设计的场景可以用一两行命令搞定。本文整理 12 个 Redis 高级用法的工程实例,覆盖分布式锁(SETNX 与 Redlock)、排行榜(Sorted Set)、限流(令牌桶与滑动窗口)、消息队列(List 与 Stream)、广播(Pub/Sub)、概率统计(HyperLogLog 与 Bloom Filter)、地理信息(Geo)等场景,每一个都给出关键命令与典型陷阱,帮你充分发挥 Redis 的能力。
一、分布式锁的三种实现演进
分布式锁是 Redis 最经典的应用之一。最朴素的实现是 SETNX:客户端用 SETNX lock_key value 尝试设置,如果返回 1 说明拿到锁,做完业务用 DEL 释放。这个方案有致命缺陷:客户端拿锁后崩溃就永远释放不了,造成死锁。
第二代是 SET 加 EX 加 NX 一条命令:SET lock_key value NX EX 30。原子地完成"不存在则设置 + 设置过期",避免了死锁,是绝大多数业务场景的最佳选择。但仍有两个隐患:客户端 GC 停顿超过 30 秒后,自己的锁已过期但仍以为持锁;释放时如果直接 DEL,可能删掉别人新加的锁。第三代修复方法是:SET 时 value 用唯一标识(UUID),释放时用 Lua 脚本原子地"GET 比对 + DEL"。生产环境推荐 Java 用 Redisson、Go 用 redislock 等成熟客户端,不要自己造轮子。本站 UUID 生成器可生成锁标识。
二、Redlock 与强一致争论
对单 Redis 实例的锁,主从切换瞬间可能丢锁——主刚拿到锁还没同步给从就挂了,从被晋升为主,新客户端可以再拿同一把锁,违反互斥。antirez 提出 Redlock 算法:部署 5 个独立 Redis 实例(不互为主从),客户端依次向 5 个发 SET NX,多数(3 个)成功就算获得锁,否则向所有节点 DEL 释放。
Redlock 在工程上能解决主从切换丢锁问题,但 Martin Kleppmann 在著名博文中指出:在 GC 停顿、时钟回拨等极端场景下,Redlock 仍可能违反互斥。如果业务对强一致要求极高(金融转账、库存扣减且不能超卖),应该用 etcd 或 ZooKeeper 这种基于共识算法的协调服务。一般业务用 Redlock 已经足够,配合业务幂等做最后兜底。
三、排行榜与 Sorted Set
游戏积分榜、热搜榜、电商热销榜——所有需要"按某个数值排序的 top N"的场景,都是 Redis Sorted Set 的舞台。ZADD leaderboard 100 user_a 把用户加入榜单并设置分数;ZINCRBY leaderboard 1 user_a 给用户加分;ZREVRANGE leaderboard 0 9 取前 10 名;ZRANK leaderboard user_a 查询用户排名;ZSCORE leaderboard user_a 查询用户分数。
实战中常见需求:周榜与日榜——用 leaderboard:2026-04-29 与 leaderboard:week:17 这种带时间维度的 key,过期由 EXPIRE 控制;维度榜——按地区、职业等维度建多个 ZSet;持久化到 MySQL——定时把 ZSet dump 到数据库做归档;分页查询排名附近——查 ZRANK 后再 ZRANGE rank-5 rank+5 即可。亿级元素的 ZSet 内存占用约 6-9GB,超过单机内存就分片或按时间切表。
四、限流之令牌桶
令牌桶是经典限流算法:桶以恒定速率往里加令牌,请求来时消耗一个令牌,没令牌就拒绝。它允许短时突发(桶满时可以一次性消耗),适合 API 网关、登录接口等场景。Redis 实现的核心是用 Lua 脚本保证原子性。
核心逻辑用文字描述:每个用户维护两个字段——last_refill_time(上次补充时间)与 tokens(当前令牌数);请求到来时计算应补充的令牌数(按速率乘以经过时间),更新 tokens;如果 tokens 大于等于 1,扣 1 后返回成功,否则返回拒绝。整段逻辑必须在 Lua 中原子完成,否则并发下会算错。Spring Cloud Gateway、Kong 等网关内置了 Redis 限流插件,开箱即用。
五、限流之滑动窗口
令牌桶允许突发,但有些场景需要"严格限制每秒/每分钟最多 N 次",比如防刷验证码。滑动窗口算法用 Sorted Set 实现:score 是请求时间戳,member 是请求 ID(UUID 或时间戳+随机)。
每次请求执行:ZREMRANGEBYSCORE 清理窗口外的旧请求;ZCARD 查询当前窗口请求数;如果未超阈值,ZADD 加入新请求并 EXPIRE 续期;否则拒绝。这种方式精确度极高,比固定窗口好(避免边界双倍流量),但内存占用稍大(每个请求一条记录)。低精度需求可以用固定窗口 + INCR 替代,每个时间桶一个 key,过期自动清理。可以结合本站 时间戳转换工具调试限流逻辑。
六、消息队列:List 与 Stream
用 List 做简单消息队列:生产者 LPUSH 入队,消费者 BRPOP 阻塞出队。BRPOP 在队列空时阻塞等待,新消息一来立即唤醒,比轮询高效得多。这个方案适合任务队列、邮件发送、异步通知等场景。但有个致命问题:消费者拿到消息后崩溃,消息丢失。
更可靠的是 Redis Stream(5.0 引入)。XADD stream * field value 入队;消费者组 XREADGROUP 读取消息;处理完用 XACK 确认;XPENDING 查询未确认消息可重新分发。Stream 支持消息持久化、消费者组、自动重试、按 ID 范围回溯,几乎对标 Kafka 的能力,单机 QPS 可达数十万。本站的 Kafka vs RabbitMQ 一文对比了不同消息系统选型。
七、Pub/Sub 广播与缓存失效
Pub/Sub 是 Redis 的另一种消息机制:PUBLISH channel message 发布,SUBSCRIBE channel 订阅。消息不持久化,订阅者必须在线才能收到,适合实时性要求高、丢失可接受的场景:聊天室、实时通知、缓存失效广播。
典型用法是缓存失效广播。多个应用节点都缓存了某条数据,数据更新时需要通知所有节点失效本地缓存。数据库写入后向 Redis PUBLISH cache:invalidate user_123,所有节点 SUBSCRIBE 该频道,收到消息删除本地缓存。这个模式比直接清 Redis 缓存更灵活,因为应用层缓存可能有多层(堆内 + Redis)。Pub/Sub 不能替代消息队列——一旦订阅者下线就丢消息。需要可靠投递用 Stream。
八、HyperLogLog 与 Bloom Filter 概率结构
HyperLogLog 解决基数统计问题——比如统计今天 UV(独立访客数)。朴素方案用 Set 存 user_id,1000 万 UV 需要数百 MB 内存。HyperLogLog 用固定 12KB 内存就能估算 1000 万级别的基数,误差约 0.81%。命令简单:PFADD uv:2026-04-29 user_id 加入;PFCOUNT uv:2026-04-29 查询;PFMERGE 合并多天 UV 算月活。代价是不精确、不能反查具体元素。
Bloom Filter 解决"某元素是否在集合中"的判断,假阳性可控、假阴性为零。Redis 4.0+ 的 RedisBloom 模块提供 BF.ADD 与 BF.EXISTS。典型场景:缓存穿透防护——用户查询商品前先问 Bloom Filter "这个商品 ID 存在吗",不存在直接拒绝,避免恶意构造的不存在 ID 打穿缓存到数据库。爬虫去重也常用,1 亿 URL 用 100MB 即可,远小于 Set。
九、地理信息(Geo)与会话状态
Redis 5.0+ 内置 Geo 命令,把经纬度编码为 GeoHash 存进 Sorted Set。GEOADD users 121.4737 31.2304 user_a 添加;GEOSEARCH users FROMMEMBER user_a BYRADIUS 5 km 查询附近 5km 内的用户。性能极佳,亿级元素下亚毫秒响应,是外卖、打车、社交"附近的人"的标配。
会话状态存储是另一个经典用法。HSET session:abc user_id 123 token xyz;HGETALL session:abc 取出整个会话;EXPIRE session:abc 1800 设置 30 分钟过期。比传统 sticky session 更灵活,应用层可以无状态横向扩容。注意敏感数据加密、定期清理、做好高可用——会话存储挂了等于全员下线。我们的 Redis vs Memcached 一文从存储角度做了详细对比。
常见问题
SETNX 实现的分布式锁有什么坑?
常见三个陷阱:忘记设置过期时间导致死锁、释放锁时误删别人的锁、客户端 GC 暂停超过锁过期时间导致两个客户端同时持锁。修复方法分别是 SET key value NX EX seconds 一条命令、释放时 Lua 脚本校验 value 一致再删、业务上做幂等。生产环境推荐用 Redisson、redislock 等成熟客户端封装好的实现。
Redlock 算法是什么?真的可靠吗?
Redlock 是 antirez 提出的多 Redis 实例分布式锁算法,要求客户端在多数节点上获取锁才算成功。Martin Kleppmann 与 antirez 之间有过激烈争论,结论是 Redlock 在严格的分布式系统理论下并不安全(时钟漂移、GC 暂停可能违反互斥),但在实际工程中可接受。需要强一致建议用 etcd 或 ZooKeeper。
Redis 排行榜可以支持多少用户?
单个 ZSet 在亿级元素下依然能保持毫秒级 ZADD 与 ZRANGE。瓶颈通常在内存:每个元素约 60 到 90 字节(key 名加 score 加 skiplist 结构),1 亿元素约占 6 到 9GB。如果数据量超过单机内存,按区间或按维度(日榜、周榜、月榜)拆分,或者用 Redis Cluster 分片。读多写少的场景可以加本地缓存。
令牌桶和滑动窗口限流怎么选?
令牌桶允许突发流量(桶满时一次性放出),适合 API 限流场景;滑动窗口严格限制单位时间内请求数,适合反爬、防刷场景。固定窗口最简单但有边界双倍流量问题。Redis 实现:令牌桶用 Lua 脚本维护时间戳与令牌数;滑动窗口用 ZSet 存请求时间戳,每次清理过期再 ZCARD。两者也可以组合使用。
Redis Pub/Sub 与 Stream 该用哪个?
Pub/Sub 是 fire-and-forget 广播:消息不持久化,订阅者掉线消息丢失,适合实时通知、缓存失效广播。Stream 是日志型数据结构:消息持久化,支持消费者组、ACK、重试、消息回溯,是更可靠的消息队列。新项目用 Stream,老的简单广播场景可继续用 Pub/Sub。Stream 的 QPS 与可靠性已经接近专门的消息中间件。
相关工具
- UUID 生成器 — 生成分布式锁标识
- 时间戳转换 — 调试限流时间窗口
- Redis vs Memcached 对比 — 缓存方案选型