最近在复习和整理 Redis 的常见用法,顺手把缓存、限流、分布式锁、排行榜、延迟任务这些高频场景归到一篇笔记里,方便后续复习时速查。
一、Redis 是什么
Redis 是一个基于内存的高性能 Key-Value 数据库。它常被用作缓存、分布式锁、排行榜、计数器、限流器、消息队列、延迟任务等场景。
Redis 的核心特点可以概括为:
- 数据存储在内存中,读写速度快
- 支持多种数据结构
- 单线程执行命令,避免复杂的线程竞争
- 支持持久化机制
- 支持主从复制、哨兵、集群等高可用方案
- 提供 Lua 脚本、事务、Pipeline 等能力
Redis 不是只能存字符串的缓存工具,它更像是一个“内存数据结构服务器”。很多业务问题,本质上可以转换成对 Redis 某种数据结构的使用。
二、Redis 常见数据类型
Redis 常用的数据类型包括:
| 类型 | 典型命令 | 常见场景 |
|---|---|---|
| String | SET、GET、INCR | 缓存、计数器、分布式锁 |
| Hash | HSET、HGET、HGETALL | 用户信息、对象字段缓存 |
| List | LPUSH、RPUSH、LPOP、BRPOP | 简单队列、消息列表 |
| Set | SADD、SISMEMBER、SINTER | 标签、去重、共同关注 |
| ZSet | ZADD、ZRANGE、ZRANK | 排行榜、延迟任务 |
| Bitmap | SETBIT、GETBIT、BITCOUNT | 用户签到、在线状态 |
| HyperLogLog | PFADD、PFCOUNT | UV 统计 |
| Geo | GEOADD、GEODIST | 附近的人、附近门店 |
| Stream | XADD、XREAD、XGROUP | 消息队列、事件流 |
三、Redis 为什么快
Redis 快主要有以下几个原因:
1. 基于内存操作
Redis 的数据主要存储在内存中,避免了频繁的磁盘 IO。相比传统数据库从磁盘读取数据,内存访问速度要快很多。
2. 单线程命令执行模型
Redis 的核心命令执行通常是单线程的。单线程模型减少了线程切换、锁竞争、上下文切换带来的开销。
虽然 Redis 命令执行是单线程模型,但它处理网络连接时使用了 IO 多路复用,可以同时管理大量客户端连接。
3. IO 多路复用
Redis 使用 IO 多路复用机制,比如 Linux 下常见的 epoll。它可以让一个线程同时监听多个 socket,当某个 socket 有读写事件时再进行处理。
简单理解就是:Redis 不需要为每个客户端连接都创建一个线程,而是用一个事件循环统一管理大量连接。
4. 高效的数据结构
Redis 的底层数据结构经过了很多优化。例如:
- String 底层使用 SDS,支持动态扩容
- List 在新版本中主要使用 quicklist
- Hash、ZSet 在元素较少时可能使用 listpack 压缩存储
- ZSet 在数据量较大时通常由跳表和哈希表共同支持
- Set 在整数元素较少时可以使用 intset
这些设计让 Redis 在性能和内存占用之间取得了比较好的平衡。
四、Redis 常见使用案例
1. 缓存
缓存是 Redis 最常见的使用场景。
例如用户信息查询:
public User getUserById(Long userId) { String key = "user:" + userId;
String json = redis.get(key); if (json != null) { return JSON.parseObject(json, User.class); }
User user = userMapper.selectById(userId); if (user != null) { redis.setex(key, 3600, JSON.toJSONString(user)); }
return user;}基本流程是:
- 先查 Redis
- Redis 命中则直接返回
- Redis 未命中则查询数据库
- 查询到数据后写入 Redis
- 下次请求直接走 Redis
缓存的关键点是:Redis 通常只是加速访问的数据副本,真正的数据源仍然是数据库。所以设计缓存时,不能只关注“查得快”,还要考虑缓存失效、数据一致性和异常流量下数据库是否能扛住。
缓存常见问题
缓存穿透
缓存穿透指查询一个数据库中也不存在的数据,导致请求每次都打到数据库。
解决方案:
- 缓存空值
- 使用布隆过滤器
- 对非法参数提前校验
缓存击穿
缓存击穿指某个热点 key 过期的一瞬间,大量请求同时访问数据库。
解决方案:
- 互斥锁
- 热点 key 不设置过期时间
- 逻辑过期
- 提前异步刷新缓存
缓存雪崩
缓存雪崩指大量 key 在同一时间过期,导致数据库压力瞬间升高。
解决方案:
- 过期时间加随机值
- 分批预热缓存
- 限流降级
- 多级缓存
2. 分布式锁
Redis 可以通过 SET key value NX EX seconds 实现分布式锁。
示例:
SET lock:order:1001 requestId NX EX 10含义是:
NX:只有 key 不存在时才设置成功EX 10:设置 10 秒过期时间requestId:用于标识当前加锁客户端
释放锁时不能直接 DEL key,因为可能误删别人的锁。
推荐用 Lua 脚本判断 value 后再删除:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1])else return 0end这样可以保证“判断锁是不是自己的”和“删除锁”这两个操作具备原子性。
分布式锁需要注意:
- 锁必须设置过期时间,避免死锁
- value 要有唯一标识,避免误删
- 释放锁建议使用 Lua 脚本
- 业务执行时间可能超过锁过期时间,需要考虑锁续期
- 对可靠性要求很高的场景,需要谨慎评估 Redis 锁的适用性
如果是 Java 项目,实际开发中经常会直接使用 Redisson。它已经封装了加锁、释放锁、看门狗续期等逻辑,比自己手写 SET NX EX 更稳妥。不过即使用成熟框架,也要明确锁保护的临界区范围,避免把耗时很长、不可控的外部调用放进锁里。
还需要注意,Redis 分布式锁适合解决大多数业务互斥问题,但它不是强一致分布式协调系统。单 Redis 主节点加锁后,如果锁还没同步到从节点主节点就宕机,极端情况下可能出现锁丢失。对一致性要求极高的场景,应考虑数据库唯一约束、事务、ZooKeeper、etcd 等更强约束的方案。
3. 计数器
Redis 的 String 类型支持自增操作,可以用来做计数器。
例如文章浏览量:
INCR article:1001:view点赞数:
INCR article:1001:like取消点赞:
DECR article:1001:likeINCR 和 DECR 是原子操作,适合处理高并发计数场景。
常见应用包括:
- 浏览量统计
- 点赞数统计
- 下载次数统计
- 接口调用次数统计
- 用户每日操作次数统计
如果是按天统计,可以把日期放到 key 中:
INCR user:1001:login:2026-06-16EXPIRE user:1001:login:2026-06-16 864004. 限流
Redis 可以用来实现接口限流。
固定窗口限流
例如限制某个用户一分钟内最多请求 100 次:
INCR rate:user:1001:apiEXPIRE rate:user:1001:api 60当计数超过 100 时拒绝请求。
这种方式简单,但存在窗口边界问题。例如用户可能在上一分钟最后一秒请求 100 次,在下一分钟第一秒又请求 100 次,短时间内实际请求量翻倍。
另外,INCR 和 EXPIRE 如果分成两条命令执行,还要注意两个细节:
- 如果每次请求都执行
EXPIRE,这个窗口可能被不断续期,变成“距离最后一次请求 60 秒后过期”。 - 如果
INCR成功但EXPIRE因异常没有执行,这个限流 key 可能长期留在 Redis 中。
更稳妥的做法是只在计数第一次变成 1 时设置过期时间,生产项目中通常会用 Lua 脚本把 INCR、判断返回值、设置过期时间、判断是否超过阈值这些操作放在一起执行。
滑动窗口限流
可以使用 ZSet 实现滑动窗口。
把请求时间戳作为 score:
ZADD rate:user:1001 currentTimestamp requestId每次请求时:
- 删除窗口外的数据
- 统计窗口内请求数量
- 判断是否超过限制
- 没超过则写入当前请求
核心命令大致如下:
ZREMRANGEBYSCORE rate:user:1001 0 当前时间-窗口大小ZCARD rate:user:1001ZADD rate:user:1001 当前时间 请求IDEXPIRE rate:user:1001 60为了保证原子性,实际项目中通常会用 Lua 脚本把这些命令组合起来。
5. 排行榜
排行榜非常适合用 ZSet 实现。
ZSet 的每个元素都有一个 score,Redis 会根据 score 自动排序。
例如游戏积分排行榜:
ZADD game:rank 1200 user:1001ZADD game:rank 1500 user:1002ZADD game:rank 900 user:1003查询前 10 名:
ZREVRANGE game:rank 0 9 WITHSCORES查询某个用户排名:
ZREVRANK game:rank user:1001给用户增加积分:
ZINCRBY game:rank 100 user:1001ZSet 的典型应用包括:
- 游戏排行榜
- 热门文章榜
- 视频播放榜
- 商品销量榜
- 用户贡献榜
如果需要按时间维度统计,可以设计不同的 key:
rank:daily:2026-06-16rank:weekly:2026-W25rank:monthly:2026-066. 延迟任务:订单超时自动取消
订单超时自动取消是 Redis ZSet 的经典使用场景。
思路是:
- 把订单 ID 作为 member
- 把订单过期时间戳作为 score
- 后台任务定时扫描已经到期的订单
创建订单时:
ZADD order:timeout 过期时间戳 orderId后台定时任务执行:
ZRANGEBYSCORE order:timeout 0 当前时间戳 LIMIT 0 100查出已经超时的订单后,执行业务取消逻辑。
处理完成后删除:
ZREM order:timeout orderId为了避免多个服务实例重复处理同一个订单,可以在处理时先尝试删除:
ZREM order:timeout orderId如果删除成功,说明当前实例抢到了这个任务;如果删除失败,说明已经被其他实例处理。
这里有一个可靠性细节:如果某个实例 ZREM 成功后还没来得及执行业务取消逻辑就宕机,这个任务可能会丢失。因此这类方案通常需要让数据库状态兜底,或者额外设计补偿扫描。比如定期从数据库中扫描长时间处于“待支付”的订单,再重新触发取消逻辑。
订单取消逻辑中还要再次查询数据库中的订单状态,因为可能出现用户已经支付但 Redis 延迟任务还没删除的情况。
完整流程大致是:
- 用户创建订单
- 订单写入数据库,状态为待支付
- Redis ZSet 写入订单超时时间
- 定时任务扫描到期订单
- 尝试从 ZSet 删除订单 ID
- 删除成功后查询数据库订单状态
- 如果仍然是待支付,则取消订单
- 如果已经支付,则不处理
这种方案实现简单,但定时扫描会有一定延迟,也需要补偿机制保证最终一致。如果业务对延迟和可靠性要求更严格,可以考虑消息队列的延迟消息、时间轮、任务表扫描等方案。
7. 用户签到:Bitmap
Bitmap 可以用来记录用户每天是否签到。
例如用一个 key 表示某个用户某个月的签到情况:
sign:user:1001:2026-06第 1 天签到:
SETBIT sign:user:1001:2026-06 0 1第 2 天签到:
SETBIT sign:user:1001:2026-06 1 1判断某一天是否签到:
GETBIT sign:user:1001:2026-06 0统计本月签到次数:
BITCOUNT sign:user:1001:2026-06Bitmap 的优点是节省空间。一个用户一个月的签到记录只需要几十个 bit,非常适合布尔状态类数据。
常见场景:
- 用户签到
- 用户在线状态
- 活跃用户统计
- 功能是否开启
- 打卡记录
8. UV 统计:HyperLogLog
HyperLogLog 可以用来统计不重复元素数量,例如网站 UV。
普通 Set 也可以去重统计,但如果访问用户数量非常大,Set 会占用较多内存。
HyperLogLog 的特点是:
- 占用内存很小
- 适合大规模去重计数
- 结果是近似值,有一定误差
例如统计某篇文章的独立访问用户:
PFADD article:1001:uv user:1001PFADD article:1001:uv user:1002PFADD article:1001:uv user:1001统计 UV:
PFCOUNT article:1001:uvHyperLogLog 适合用于:
- 网站 UV
- 页面 UV
- 活动访问人数
- 搜索关键词去重统计
如果业务要求绝对精确,就不适合使用 HyperLogLog。
9. 社交关系:Set
Set 是无序且不重复的集合,适合做去重和集合运算。
例如用户关注列表:
SADD user:1001:follows user:2001SADD user:1001:follows user:2002SADD user:1001:follows user:2003判断是否关注:
SISMEMBER user:1001:follows user:2001共同关注:
SINTER user:1001:follows user:1002:follows可能认识的人:
SDIFF user:1002:follows user:1001:followsSet 常见应用包括:
- 标签系统
- 用户关注
- 好友关系
- 黑名单
- 抽奖去重
- 共同好友
- 共同关注
10. 消息队列
Redis 可以通过 List、Pub/Sub、Stream 实现不同形式的消息功能。
List 实现简单队列
生产者:
LPUSH queue:task task1消费者:
BRPOP queue:task 0BRPOP 是阻塞式弹出,如果队列为空,消费者会等待。
这种方式简单,但功能有限,例如消息确认、消费组、失败重试等能力需要自己实现。
Pub/Sub 发布订阅
发布消息:
PUBLISH channel:notice hello订阅消息:
SUBSCRIBE channel:noticePub/Sub 适合实时广播,但消息不会持久化。如果消费者不在线,可能收不到消息。
Stream
Stream 是 Redis 提供的更完整的消息流数据结构,支持:
- 消息持久化
- 消费组
- 消费确认
- 历史消息读取
- 多消费者协作消费
写入消息:
XADD stream:order * orderId 1001 status created读取消息:
XREAD COUNT 10 STREAMS stream:order 0使用消费组时,还可以实现类似消息队列的消费模型。
五、Redis 事务和 Lua 脚本
1. Redis 事务
Redis 事务使用 MULTI、EXEC、DISCARD、WATCH 等命令。
示例:
MULTISET name redisINCR countEXECRedis 事务的特点是:
- 命令会先进入队列
EXEC时统一执行- 单条命令执行具备原子性
- 事务中间不会被其他客户端命令插入
- Redis 事务不支持传统关系型数据库那种自动回滚
需要注意,如果事务中的某条命令运行时报错,其他命令仍然可能继续执行。
例如:
MULTISET count helloINCR countSET name redisEXEC其中 INCR count 会报错,因为 count 不是数字。但其他命令可能已经执行。
所以 Redis 事务更适合理解成“命令批量排队后按顺序执行”,它能保证事务执行期间不会插入其他客户端命令,但不提供关系型数据库那种完整回滚能力。如果业务需要先读后写并防止并发修改,可以配合 WATCH 做乐观锁;如果需要更复杂的原子判断逻辑,Lua 脚本通常更直观。
2. Lua 脚本
Lua 脚本可以把多条 Redis 命令组合成一个整体执行。
例如释放分布式锁:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1])else return 0endLua 脚本的优点:
- 原子执行
- 减少网络往返
- 适合封装复杂判断逻辑
- 常用于分布式锁、限流、库存扣减等场景
需要注意,Lua 脚本执行时间不应该太长。因为 Redis 执行 Lua 脚本时,会阻塞其他命令执行。如果脚本逻辑复杂或者循环处理大量数据,可能影响 Redis 响应。
六、Redis 持久化
Redis 是内存数据库,但它也提供持久化能力,避免服务重启后数据完全丢失。
主要持久化方式有两种:
1. RDB
RDB 是快照持久化,会在某个时间点生成 Redis 数据快照。
优点:
- 文件紧凑
- 适合备份
- 恢复速度较快
缺点:
- 两次快照之间的数据可能丢失
- 生成快照时可能带来额外开销
2. AOF
AOF 会记录 Redis 执行过的写命令。
优点:
- 数据安全性通常比 RDB 更高
- 可以配置不同的刷盘策略
- 文件可读性相对更好
缺点:
- 文件可能比 RDB 更大
- 恢复时需要重放命令
- 需要 AOF 重写来压缩文件
常见配置策略:
- 对数据安全要求不高:只开 RDB
- 对数据安全要求较高:开启 AOF
- 兼顾恢复速度和安全性:RDB + AOF 混合使用
七、Redis 过期删除和内存淘汰
1. 过期删除
Redis key 可以设置过期时间:
EXPIRE user:1001 3600SETEX user:1001 3600 valueRedis 删除过期 key 主要依赖:
- 惰性删除:访问 key 时检查是否过期,过期则删除
- 定期删除:Redis 定期抽样检查部分 key,删除其中过期的数据
如果只用惰性删除,很多过期 key 可能长期留在内存中;如果全量扫描删除,又会影响性能。所以 Redis 使用定期删除和惰性删除结合的方式。
2. 内存淘汰
当 Redis 内存达到限制后,会根据配置的淘汰策略删除部分 key。
常见策略包括:
noeviction:不淘汰,写入时报错allkeys-lru:从所有 key 中淘汰最近最少使用的 keyvolatile-lru:从设置了过期时间的 key 中淘汰最近最少使用的 keyallkeys-random:从所有 key 中随机淘汰volatile-random:从设置了过期时间的 key 中随机淘汰volatile-ttl:优先淘汰剩余时间短的 keyallkeys-lfu:从所有 key 中淘汰使用频率低的 keyvolatile-lfu:从设置了过期时间的 key 中淘汰使用频率低的 key
缓存场景中比较常见的是 allkeys-lru 或 allkeys-lfu。
八、Redis 高可用
1. 主从复制
Redis 支持主从复制。主节点负责写,从节点复制主节点数据,可以用于读扩展和数据备份。
基本结构:
Client -> Master -> Replica主从复制的问题是:如果主节点宕机,需要有机制完成故障转移。
2. 哨兵模式
Sentinel 可以监控 Redis 主从节点状态。当主节点宕机时,Sentinel 可以从从节点中选举新的主节点。
哨兵主要负责:
- 监控 Redis 节点
- 判断主节点是否下线
- 选举新的主节点
- 通知客户端新的主节点地址
3. Redis Cluster
Redis Cluster 用于数据分片和集群高可用。
Redis Cluster 把数据划分到 16384 个 hash slot 中,不同节点负责不同的 slot。
例如:
key -> CRC16(key) % 16384 -> slot -> Redis nodeCluster 的特点:
- 支持水平扩展
- 支持自动分片
- 支持故障转移
- 客户端需要支持重定向
如果数据量和并发量较大,单机 Redis 无法支撑时,可以考虑 Redis Cluster。
使用 Cluster 时要额外注意多 key 操作。Redis Cluster 会把不同 key 分布到不同 slot 上,如果一次命令涉及多个 key,而这些 key 不在同一个 slot,命令可能无法直接执行。需要让相关 key 使用相同的 hash tag,例如 order:{1001}:info 和 order:{1001}:lock,让它们落在同一个 slot 中。
九、Redis 使用中的注意事项
1. Key 设计
Redis key 应该具备清晰的业务含义。
推荐格式:
业务名:对象类型:对象ID:字段例如:
user:1001:profilearticle:1001:vieworder:timeoutrank:daily:2026-06-16注意事项:
- key 不要太长
- 命名要统一
- 避免不同业务使用相同 key
- 可以用冒号分层
- 批量操作时注意 key 的数量
2. 避免大 key
大 key 指 value 特别大,或者集合元素特别多的 key。
例如:
- 一个 String 存储几 MB 的 JSON
- 一个 List 有几百万个元素
- 一个 Hash 有几十万个 field
- 一个 ZSet 有大量 member
大 key 的问题:
- 读写耗时长
- 删除可能阻塞 Redis
- 网络传输成本高
- 持久化和复制压力大
- 容易造成集群数据倾斜
解决思路:
- 拆分 key
- 控制集合大小
- 分页读取
- 使用异步删除
- 避免一次性
HGETALL、SMEMBERS读取大量数据
3. 避免热 key
热 key 指某个 key 被极高频率访问。
热 key 的问题是流量集中在单个 Redis 节点上,可能导致节点压力过大。
解决思路:
- 本地缓存
- 多副本读
- key 拆分
- 热点数据预加载
- 限流降级
4. 谨慎使用复杂命令
一些命令在数据量大时可能比较危险,例如:
KEYS *HGETALL big_hashSMEMBERS big_setLRANGE big_list 0 -1ZRANGE big_zset 0 -1生产环境中不建议使用 KEYS * 扫描全部 key,可以使用 SCAN 分批扫描。
例如:
SCAN 0 MATCH user:* COUNT 100SCAN 是渐进式遍历,不会像 KEYS 那样一次性阻塞 Redis 很久。
5. 设置合理的过期时间
缓存类 key 通常应该设置过期时间,避免无用数据长期占用内存。
同时,为了避免大量 key 同时过期,可以给过期时间增加随机值:
过期时间 = 基础时间 + 随机时间例如:
3600 + random(0, 300)十、总结
Redis 的价值不只是“快”,更重要的是它提供了丰富的数据结构和原子操作能力。很多业务场景都可以通过合适的数据结构建模,从而得到简单、高效的实现。
常见对应关系可以总结为:
| 业务场景 | 推荐数据结构 |
|---|---|
| 缓存 | String / Hash |
| 计数器 | String |
| 分布式锁 | String + Lua |
| 排行榜 | ZSet |
| 延迟任务 | ZSet |
| 用户签到 | Bitmap |
| UV 统计 | HyperLogLog |
| 标签、关注、共同好友 | Set |
| 简单队列 | List |
| 消息流 | Stream |
| 附近的人 | Geo |
学习 Redis 时,可以从三个角度理解:
- 业务角度:Redis 能解决什么问题
- 数据结构角度:每种类型适合什么场景
- 系统设计角度:如何处理缓存一致性、高可用、大 key、热 key、持久化等问题
在实际项目中,Redis 用得好可以显著提升系统性能;用得不合理也可能引入缓存雪崩、数据不一致、大 key 阻塞、锁误删等问题。所以 Redis 的重点不只是会用命令,还要理解每个场景背后的设计取舍。