mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3001 字
8 分钟
Redis 常见使用场景与实现
2024-08-21

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:20260712
EXPIRE 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 user3
PFCOUNT page:home:uv:20260712 # 返回去重后的估算值
PFMERGE page:home:uv:202607 page:home:uv:20260712 page:home:uv:20260711 # 合并多天
计数场景数据结构命令是否精确
PV/点赞数StringINCR/INCRBY精确
点赞用户集合SetSADD/SISMEMBER精确
UV 去重计数HyperLogLogPFADD/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 10
ZREVRANK 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 30

List 队列简单,但有硬伤:BRPOP 取走消息即删除,消费者处理失败消息就丢了,没有确认机制;也不能多个消费者各消费一份(只能分摊)。它适合”丢一两条无所谓”的轻量任务分发。

Pub/Sub 是发布订阅,PUBLISH 发消息,SUBSCRIBE 订阅。它的特点是实时广播,所有订阅者都收到,但消息不持久化,订阅者离线期间发的消息直接丢失。适合做实时通知、配置变更广播,不适合做可靠队列。

# 发布者
PUBLISH events:user "user:1001 logged in"
# 订阅者
SUBSCRIBE events:user

Stream 是 Redis 5.0 引入的专业消息流,弥补了前两者的短板:消息持久化、支持消费者组、能回溯历史。底层用 radix tree + listpack,按 ID 单调递增组织。

# 生产者:XADD 写入,* 表示让 Redis 生成 ID
XADD 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)可靠投递、回溯、多消费组
Warning

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:abc123token
EXPIRE session:abc123token 1800 # 滑动过期:活跃就续期
# 登出删除
DEL session:abc123token

SETEX(或 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)
end
if count > tonumber(ARGV[1]) then
return 0 -- 限流
end
return 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 0
end
return 1

member 用自增序号而非时间戳,是为了避免同一毫秒内多个请求因 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/HashSET/GET/HSET微秒级读写不能当唯一存储
计数器StringINCR/INCRBY原子自增需持久化精确计数时回写 DB
UV 去重HyperLogLogPFADD/PFCOUNT固定 12KB 内存需要精确计数时用 Set
排行榜Sorted SetZADD/ZREVRANGEO(log n) 排序全量排序场景用专业方案
消息队列List/Pub-Sub/StreamLPUSH/XADD轻量、低延迟高可靠大积压用专业 MQ
会话存储StringSETEX/EXPIRE共享、自动过期敏感信息勿明文存
限流String/ZSetINCR/ZADD + Lua原子计数分布式限流需考虑时钟一致
分布式锁String + LuaSET NX PX跨进程互斥金融级互斥用 etcd/ZK
布隆过滤器Bitmap/RedisBloomBF.ADD/BF.EXISTS极小内存判存在有误判、不支持删除(除非 Counting)

选型的核心原则是:先用对数据结构(O(1) 的别用 O(n) 的,能 HyperLogLog 的别用 Set),再认清 Redis 的边界(内存型、异步持久化、单线程),量超出 Redis 承受能力或可靠性要求超过 Redis 能保证的,就换专业中间件。Redis 强在”快”和”结构丰富”,不在”可靠”和”海量”。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Redis 常见使用场景与实现
https://blog.souloss.cn/posts/middleware/cache/redis-use-cases/
作者
Souloss
发布于
2024-08-21
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时