Redis 常被笼统地叫成”缓存”,但它的数据结构远不止做缓存这一种用法。String 能计数,List 能排队,ZSet 能排行,Stream 能做消息流,再加上过期时间和 Lua 脚本,Redis 实际上是一个内存数据结构瑞士军刀。选对场景用对结构,能省掉一堆中间件;选错了,要么把简单问题复杂化,要么把 Redis 当数据库用出数据丢失事故。
本文把 Redis 的常见使用场景过一遍,每个场景说清楚”用什么数据结构、怎么用、什么场景适用、什么时候不该用”。涉及缓存、布隆过滤器、库存预扣、排行榜的深度实战已有专文,这里只做场景串联,不重复展开。
一、缓存:Redis 的本职工作
缓存是 Redis 最高频的用法,核心是 Cache-Aside 模式:读先查 Redis,miss 再查数据库并回写;写先更新数据库再删缓存。Redis 适合做缓存,靠的是全内存读写(微秒级延迟)和丰富的过期策略。
缓存场景的难点不在”怎么存”,而在三个经典问题:
- 缓存穿透:查询不存在的数据,请求穿透到数据库
- 缓存击穿:热点 key 过期瞬间,大量并发打到数据库
- 缓存雪崩:大量 key 同时过期,数据库被压垮
这三个问题的根因与对策(缓存空值、布隆过滤器、互斥锁、逻辑过期、随机过期时间、多级缓存)在 Redis 缓存三大问题与实战 里有完整展开,本文不重复。缓存场景下记住一条边界:Redis 是缓存不是数据库,缓存可以丢、可以重建,但绝不能当唯一存储。
二、计数器:PV/UV/点赞
INCR 是 Redis 最被低估的命令。它对 String 类型的整数做原子自增,单线程保证不会有并发覆盖,天然适合做计数器。
# 文章浏览量(PV)INCR article:1001:views # 返回自增后的值INCRBY article:1001:views 5 # 一次加 5
# 限制每天计数的 key,按天分桶INCR article:1001:views:20260712EXPIRE article:1001:views:20260712 86400 # 次日自动过期点赞数同理,INCR 加、DECR 取消。关键优势是原子性:哪怕一万个用户同时点赞,INCR 串行执行,不会丢计数,也不需要数据库行锁。如果还要存”谁点过赞”防重复,配合 Set 用 SADD 记录用户 ID 即可。
UV(独立访客)要的是去重计数,朴素做法是用 Set 存所有访客 ID 再 SCARD,但用户量大时 Set 占内存惊人。生产环境用 HyperLogLog,PFADD 记录、PFCOUNT 估算,每个 key 固定 12KB,能估算上亿 UV,代价是约 0.81% 的误差。对 UV 这种统计场景,这点误差完全可接受。
# UV 统计:HyperLogLog,固定 12KB 内存PFADD page:home:uv:20260712 user1 user2 user3PFCOUNT page:home:uv:20260712 # 返回去重后的估算值PFMERGE page:home:uv:202607 page:home:uv:20260712 page:home:uv:20260711 # 合并多天| 计数场景 | 数据结构 | 命令 | 是否精确 |
|---|---|---|---|
| PV/点赞数 | String | INCR/INCRBY | 精确 |
| 点赞用户集合 | Set | SADD/SISMEMBER | 精确 |
| UV 去重计数 | HyperLogLog | PFADD/PFCOUNT | 约 0.81% 误差 |
三、排行榜:ZSet 的主场
排行榜是 Sorted Set 最经典的应用。ZSet 里每个元素带一个分数,按分数排序,ZADD 更新分数、ZREVRANGE 取 Top N、ZREVRANK 查排名,复杂度都在 O(log n) 量级(ZREVRANGE 取 N 个元素时为 O(log n + N))。
# 实时积分排行榜ZADD leaderboard:week 1500 "user:1001" 1200 "user:1002"ZINCRBY leaderboard:week 100 "user:1001" # user:1001 加 100 分ZREVRANGE leaderboard:week 0 9 WITHSCORES # Top 10ZREVRANK leaderboard:week "user:1001" # 查排名(0 开始)同分时怎么排?常见技巧是把时间戳编码进分数,分数 = 主分数 × 大基数 + (上限 − 时间戳),让先达到该分数的人排前面。排行榜的分页、同分处理、每日清理在 Redis 缓存三大问题与实战 的排行榜章节有完整实现。
四、消息队列:List、Pub/Sub、Stream
Redis 做消息队列有三种形态,能力递增,复杂度也递增。
List 队列是最朴素的方案:生产者 LPUSH 入队,消费者 BRPOP 阻塞出队。BRPOP 没有数据时会阻塞等待,避免空轮询。
# 生产者LPUSH task:queue '{"task":"send_email","to":"a@b.com"}'
# 消费者(阻塞等待最多 30 秒)BRPOP task:queue 30List 队列简单,但有硬伤:BRPOP 取走消息即删除,消费者处理失败消息就丢了,没有确认机制;也不能多个消费者各消费一份(只能分摊)。它适合”丢一两条无所谓”的轻量任务分发。
Pub/Sub 是发布订阅,PUBLISH 发消息,SUBSCRIBE 订阅。它的特点是实时广播,所有订阅者都收到,但消息不持久化,订阅者离线期间发的消息直接丢失。适合做实时通知、配置变更广播,不适合做可靠队列。
# 发布者PUBLISH events:user "user:1001 logged in"
# 订阅者SUBSCRIBE events:userStream 是 Redis 5.0 引入的专业消息流,弥补了前两者的短板:消息持久化、支持消费者组、能回溯历史。底层用 radix tree + listpack,按 ID 单调递增组织。
# 生产者:XADD 写入,* 表示让 Redis 生成 IDXADD orders:stream * order_id 1001 amount 99.9
# 消费者组:创建组,从最旧开始消费XGROUP CREATE orders:stream order_group 0
# 消费者:读取并加入 PEL(未确认列表)XREADGROUP GROUP order_group consumer_1 COUNT 10 STREAMS orders:stream >
# 处理完后确认XACK orders:stream order_group <message_id>Stream 的消费者组机制(last-delivered-id + PEL)让多个消费者分摊消息,未确认的消息能被 XCLAIM 转给别的消费者,实现至少一次投递和故障转移。Stream 的存储原理见 Redis 数据结构与持久化原理。
| 队列形态 | 持久化 | 多消费组 | 消息确认 | 适用场景 |
|---|---|---|---|---|
| List | 否(取即删) | 否(只能分摊) | 无 | 轻量任务分发,可丢消息 |
| Pub/Sub | 否 | 是(广播) | 无 | 实时通知、配置广播 |
| Stream | 是 | 是 | 是(XACK) | 可靠投递、回溯、多消费组 |
Redis 做消息队列的边界要认清:它的持久化是异步刷盘(RDB/AOF),极端故障下仍可能丢消息,且 Stream 数据放在内存,积压过多会撑爆内存。对消息可靠性要求高、积压量大的场景,应该用 Kafka/RabbitMQ/RocketMQ 这类专业 MQ。Redis 队列适合”量不大、延迟敏感、可以接受偶发丢失”的场景。
五、会话存储:Session 共享
多机部署时,Session 存在哪台机器就成了问题:用户请求被负载均衡分到不同服务器,第二次请求可能落在没有 Session 的机器上,导致反复登录。把 Session 统一放 Redis,所有服务器共享,问题就解决了。
# 登录时写 Session,设过期时间(如 30 分钟)SETEX session:abc123token 1800 '{"user_id":1001,"role":"admin"}'
# 每次请求校验并续期GET session:abc123tokenEXPIRE session:abc123token 1800 # 滑动过期:活跃就续期
# 登出删除DEL session:abc123tokenSETEX(或 SET ... EX)一步设置值和过期时间,过期时间就是 Session 超时。滑动过期靠每次访问时 EXPIRE 续期,让活跃用户不掉线、非活跃用户自动清理。Spring Session、express-session 等框架都内置了 Redis 适配,开箱即用。
会话存储用 Redis 的优势是高吞吐和天然过期,但要注意 Session 是热数据,单 key 不要塞太多(避免大 key),敏感信息(密码、令牌密钥)不要明文存。
六、限流:固定窗口、滑动窗口、令牌桶
限流是保护下游的标配,Redis 凭借原子操作和过期机制,能实现多种限流算法。
固定窗口最简单:每个时间窗口(如每分钟)一个计数器,INCR 后判断是否超阈值,超了就拒绝,首次进入窗口时设过期。
-- 固定窗口限流:每分钟 100 次local count = redis.call('INCR', KEYS[1])if count == 1 then redis.call('EXPIRE', KEYS[1], 60)endif count > tonumber(ARGV[1]) then return 0 -- 限流endreturn 1 -- 放行固定窗口的缺陷是边界突刺:窗口切换的瞬间(比如 59 秒和下一分钟的 1 秒内各打满 100 次),实际通过 200 次。滑动窗口能消除这个突刺。
滑动窗口用 ZSet 实现:每个请求作为一个 member,时间戳作为 score,每次请求先清理窗口外的旧记录,再统计窗口内请求数。
-- 滑动窗口限流:每 60 秒 100 次local now = tonumber(ARGV[1])redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, now - 60000) -- 清理 60 秒前的local seq = redis.call('INCR', KEYS[2]) -- 全局自增序号,保证 member 唯一redis.call('ZADD', KEYS[1], now, seq) -- 加入当前请求(member 用序号而非时间戳)redis.call('EXPIRE', KEYS[1], 60)if redis.call('ZCARD', KEYS[1]) > tonumber(ARGV[2]) then return 0endreturn 1member 用自增序号而非时间戳,是为了避免同一毫秒内多个请求因 member 相同而被 ZADD 当作”更新分数”而非”新增元素”,导致 ZCARD 少算、限流放行超量。
令牌桶更灵活,允许突发流量:桶里以固定速率放令牌,请求消耗令牌,桶满则丢弃新令牌。Redis 实现令牌桶通常用 Lua 脚本算”距上次放了多少令牌、当前剩多少”,原子地更新和判断。
限流算法的选型:固定窗口简单但边界有突刺,适合精度要求不高的场景;滑动窗口平滑,适合多数接口限流;令牌桶允许突发,适合需要容忍短时高峰的场景(如对外 API)。三种都能用 Lua 脚本在 Redis 单线程里原子执行,避免并发下的判断与更新错位。
七、分布式锁
分布式锁是 Redis 另一个高频场景,用 SET NX PX 加锁、Lua 脚本释放、看门狗续期。它的实现细节和陷阱(两步竞态、误删他人锁、锁续期、Redlock、fencing token、GC 停顿失效)在 Redis 分布式锁:从 SETNX 到 Redlock 的实现与陷阱 里专门展开。
这里只强调场景边界:Redis 分布式锁适合”偶尔冲突代价可接受”的场景(防重复提交、限流互斥、缓存重建),不适合对互斥性零容忍的金融扣款。后者要用基于共识算法的锁服务。
八、布隆过滤器:去重与防穿透
布隆过滤器用极小内存判断”某个元素一定不存在或可能存在”,适合做去重和缓存穿透防护。RedisBloom 模块提供 BF.ADD/BF.EXISTS,没有模块时也可以用 bitmap 手搓一个简易版。
# 布隆过滤器防缓存穿透:启动时把存在的 ID 全部灌入BF.RESERVE user:exists 0.001 1000000 # 误判率 0.1%,容量 100 万BF.ADD user:exists 1001
# 查询时先过布隆过滤器BF.EXISTS user:exists 999999# 返回 0:一定不存在,直接返回空,不查 DB# 返回 1:可能存在,再查缓存和 DB布隆过滤器的原理(位数组 + 多哈希、误判率与容量的权衡、Counting Bloom Filter 支持删除)在 Redis 缓存三大问题与实战 的布隆过滤器章节有展开。场景上,它适合”判断不存在就能短路”的防穿透、爬虫 URL 去重、海量数据存在性判断。
九、场景选型速查
把八个场景的数据结构和适用边界汇总成一张表,做选型时对照:
| 场景 | 数据结构 | 核心命令 | 关键优势 | 不适用边界 |
|---|---|---|---|---|
| 缓存 | String/Hash | SET/GET/HSET | 微秒级读写 | 不能当唯一存储 |
| 计数器 | String | INCR/INCRBY | 原子自增 | 需持久化精确计数时回写 DB |
| UV 去重 | HyperLogLog | PFADD/PFCOUNT | 固定 12KB 内存 | 需要精确计数时用 Set |
| 排行榜 | Sorted Set | ZADD/ZREVRANGE | O(log n) 排序 | 全量排序场景用专业方案 |
| 消息队列 | List/Pub-Sub/Stream | LPUSH/XADD | 轻量、低延迟 | 高可靠大积压用专业 MQ |
| 会话存储 | String | SETEX/EXPIRE | 共享、自动过期 | 敏感信息勿明文存 |
| 限流 | String/ZSet | INCR/ZADD + Lua | 原子计数 | 分布式限流需考虑时钟一致 |
| 分布式锁 | String + Lua | SET NX PX | 跨进程互斥 | 金融级互斥用 etcd/ZK |
| 布隆过滤器 | Bitmap/RedisBloom | BF.ADD/BF.EXISTS | 极小内存判存在 | 有误判、不支持删除(除非 Counting) |
选型的核心原则是:先用对数据结构(O(1) 的别用 O(n) 的,能 HyperLogLog 的别用 Set),再认清 Redis 的边界(内存型、异步持久化、单线程),量超出 Redis 承受能力或可靠性要求超过 Redis 能保证的,就换专业中间件。Redis 强在”快”和”结构丰富”,不在”可靠”和”海量”。
参考资料
- Redis Commands - Redis 命令官方文档
- Redis 数据结构简介 - 各数据类型与适用场景
- Using Redis as a message queue - List/Pub-Sub/Stream 队列模式
- HyperLogLog - UV 去重计数原理
- Redis Streams - Stream 消费者组与确认机制
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






