5407 words
27 minutes
Redis 使用案例与相关知识笔记

最近在复习和整理 Redis 的常见用法,顺手把缓存、限流、分布式锁、排行榜、延迟任务这些高频场景归到一篇笔记里,方便后续复习时速查。

一、Redis 是什么#

Redis 是一个基于内存的高性能 Key-Value 数据库。它常被用作缓存、分布式锁、排行榜、计数器、限流器、消息队列、延迟任务等场景。

Redis 的核心特点可以概括为:

  1. 数据存储在内存中,读写速度快
  2. 支持多种数据结构
  3. 单线程执行命令,避免复杂的线程竞争
  4. 支持持久化机制
  5. 支持主从复制、哨兵、集群等高可用方案
  6. 提供 Lua 脚本、事务、Pipeline 等能力

Redis 不是只能存字符串的缓存工具,它更像是一个“内存数据结构服务器”。很多业务问题,本质上可以转换成对 Redis 某种数据结构的使用。


二、Redis 常见数据类型#

Redis 常用的数据类型包括:

类型典型命令常见场景
StringSETGETINCR缓存、计数器、分布式锁
HashHSETHGETHGETALL用户信息、对象字段缓存
ListLPUSHRPUSHLPOPBRPOP简单队列、消息列表
SetSADDSISMEMBERSINTER标签、去重、共同关注
ZSetZADDZRANGEZRANK排行榜、延迟任务
BitmapSETBITGETBITBITCOUNT用户签到、在线状态
HyperLogLogPFADDPFCOUNTUV 统计
GeoGEOADDGEODIST附近的人、附近门店
StreamXADDXREADXGROUP消息队列、事件流

三、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;
}

基本流程是:

  1. 先查 Redis
  2. Redis 命中则直接返回
  3. Redis 未命中则查询数据库
  4. 查询到数据后写入 Redis
  5. 下次请求直接走 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 0
end

这样可以保证“判断锁是不是自己的”和“删除锁”这两个操作具备原子性。

分布式锁需要注意:

  1. 锁必须设置过期时间,避免死锁
  2. value 要有唯一标识,避免误删
  3. 释放锁建议使用 Lua 脚本
  4. 业务执行时间可能超过锁过期时间,需要考虑锁续期
  5. 对可靠性要求很高的场景,需要谨慎评估 Redis 锁的适用性

如果是 Java 项目,实际开发中经常会直接使用 Redisson。它已经封装了加锁、释放锁、看门狗续期等逻辑,比自己手写 SET NX EX 更稳妥。不过即使用成熟框架,也要明确锁保护的临界区范围,避免把耗时很长、不可控的外部调用放进锁里。

还需要注意,Redis 分布式锁适合解决大多数业务互斥问题,但它不是强一致分布式协调系统。单 Redis 主节点加锁后,如果锁还没同步到从节点主节点就宕机,极端情况下可能出现锁丢失。对一致性要求极高的场景,应考虑数据库唯一约束、事务、ZooKeeper、etcd 等更强约束的方案。


3. 计数器#

Redis 的 String 类型支持自增操作,可以用来做计数器。

例如文章浏览量:

INCR article:1001:view

点赞数:

INCR article:1001:like

取消点赞:

DECR article:1001:like

INCRDECR 是原子操作,适合处理高并发计数场景。

常见应用包括:

  • 浏览量统计
  • 点赞数统计
  • 下载次数统计
  • 接口调用次数统计
  • 用户每日操作次数统计

如果是按天统计,可以把日期放到 key 中:

INCR user:1001:login:2026-06-16
EXPIRE user:1001:login:2026-06-16 86400

4. 限流#

Redis 可以用来实现接口限流。

固定窗口限流#

例如限制某个用户一分钟内最多请求 100 次:

INCR rate:user:1001:api
EXPIRE rate:user:1001:api 60

当计数超过 100 时拒绝请求。

这种方式简单,但存在窗口边界问题。例如用户可能在上一分钟最后一秒请求 100 次,在下一分钟第一秒又请求 100 次,短时间内实际请求量翻倍。

另外,INCREXPIRE 如果分成两条命令执行,还要注意两个细节:

  1. 如果每次请求都执行 EXPIRE,这个窗口可能被不断续期,变成“距离最后一次请求 60 秒后过期”。
  2. 如果 INCR 成功但 EXPIRE 因异常没有执行,这个限流 key 可能长期留在 Redis 中。

更稳妥的做法是只在计数第一次变成 1 时设置过期时间,生产项目中通常会用 Lua 脚本把 INCR、判断返回值、设置过期时间、判断是否超过阈值这些操作放在一起执行。

滑动窗口限流#

可以使用 ZSet 实现滑动窗口。

把请求时间戳作为 score:

ZADD rate:user:1001 currentTimestamp requestId

每次请求时:

  1. 删除窗口外的数据
  2. 统计窗口内请求数量
  3. 判断是否超过限制
  4. 没超过则写入当前请求

核心命令大致如下:

ZREMRANGEBYSCORE rate:user:1001 0 当前时间-窗口大小
ZCARD rate:user:1001
ZADD rate:user:1001 当前时间 请求ID
EXPIRE rate:user:1001 60

为了保证原子性,实际项目中通常会用 Lua 脚本把这些命令组合起来。


5. 排行榜#

排行榜非常适合用 ZSet 实现。

ZSet 的每个元素都有一个 score,Redis 会根据 score 自动排序。

例如游戏积分排行榜:

ZADD game:rank 1200 user:1001
ZADD game:rank 1500 user:1002
ZADD game:rank 900 user:1003

查询前 10 名:

ZREVRANGE game:rank 0 9 WITHSCORES

查询某个用户排名:

ZREVRANK game:rank user:1001

给用户增加积分:

ZINCRBY game:rank 100 user:1001

ZSet 的典型应用包括:

  • 游戏排行榜
  • 热门文章榜
  • 视频播放榜
  • 商品销量榜
  • 用户贡献榜

如果需要按时间维度统计,可以设计不同的 key:

rank:daily:2026-06-16
rank:weekly:2026-W25
rank:monthly:2026-06

6. 延迟任务:订单超时自动取消#

订单超时自动取消是 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 延迟任务还没删除的情况。

完整流程大致是:

  1. 用户创建订单
  2. 订单写入数据库,状态为待支付
  3. Redis ZSet 写入订单超时时间
  4. 定时任务扫描到期订单
  5. 尝试从 ZSet 删除订单 ID
  6. 删除成功后查询数据库订单状态
  7. 如果仍然是待支付,则取消订单
  8. 如果已经支付,则不处理

这种方案实现简单,但定时扫描会有一定延迟,也需要补偿机制保证最终一致。如果业务对延迟和可靠性要求更严格,可以考虑消息队列的延迟消息、时间轮、任务表扫描等方案。


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-06

Bitmap 的优点是节省空间。一个用户一个月的签到记录只需要几十个 bit,非常适合布尔状态类数据。

常见场景:

  • 用户签到
  • 用户在线状态
  • 活跃用户统计
  • 功能是否开启
  • 打卡记录

8. UV 统计:HyperLogLog#

HyperLogLog 可以用来统计不重复元素数量,例如网站 UV。

普通 Set 也可以去重统计,但如果访问用户数量非常大,Set 会占用较多内存。

HyperLogLog 的特点是:

  • 占用内存很小
  • 适合大规模去重计数
  • 结果是近似值,有一定误差

例如统计某篇文章的独立访问用户:

PFADD article:1001:uv user:1001
PFADD article:1001:uv user:1002
PFADD article:1001:uv user:1001

统计 UV:

PFCOUNT article:1001:uv

HyperLogLog 适合用于:

  • 网站 UV
  • 页面 UV
  • 活动访问人数
  • 搜索关键词去重统计

如果业务要求绝对精确,就不适合使用 HyperLogLog。


9. 社交关系:Set#

Set 是无序且不重复的集合,适合做去重和集合运算。

例如用户关注列表:

SADD user:1001:follows user:2001
SADD user:1001:follows user:2002
SADD user:1001:follows user:2003

判断是否关注:

SISMEMBER user:1001:follows user:2001

共同关注:

SINTER user:1001:follows user:1002:follows

可能认识的人:

SDIFF user:1002:follows user:1001:follows

Set 常见应用包括:

  • 标签系统
  • 用户关注
  • 好友关系
  • 黑名单
  • 抽奖去重
  • 共同好友
  • 共同关注

10. 消息队列#

Redis 可以通过 List、Pub/Sub、Stream 实现不同形式的消息功能。

List 实现简单队列#

生产者:

LPUSH queue:task task1

消费者:

BRPOP queue:task 0

BRPOP 是阻塞式弹出,如果队列为空,消费者会等待。

这种方式简单,但功能有限,例如消息确认、消费组、失败重试等能力需要自己实现。

Pub/Sub 发布订阅#

发布消息:

PUBLISH channel:notice hello

订阅消息:

SUBSCRIBE channel:notice

Pub/Sub 适合实时广播,但消息不会持久化。如果消费者不在线,可能收不到消息。

Stream#

Stream 是 Redis 提供的更完整的消息流数据结构,支持:

  • 消息持久化
  • 消费组
  • 消费确认
  • 历史消息读取
  • 多消费者协作消费

写入消息:

XADD stream:order * orderId 1001 status created

读取消息:

XREAD COUNT 10 STREAMS stream:order 0

使用消费组时,还可以实现类似消息队列的消费模型。


五、Redis 事务和 Lua 脚本#

1. Redis 事务#

Redis 事务使用 MULTIEXECDISCARDWATCH 等命令。

示例:

MULTI
SET name redis
INCR count
EXEC

Redis 事务的特点是:

  1. 命令会先进入队列
  2. EXEC 时统一执行
  3. 单条命令执行具备原子性
  4. 事务中间不会被其他客户端命令插入
  5. Redis 事务不支持传统关系型数据库那种自动回滚

需要注意,如果事务中的某条命令运行时报错,其他命令仍然可能继续执行。

例如:

MULTI
SET count hello
INCR count
SET name redis
EXEC

其中 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 0
end

Lua 脚本的优点:

  1. 原子执行
  2. 减少网络往返
  3. 适合封装复杂判断逻辑
  4. 常用于分布式锁、限流、库存扣减等场景

需要注意,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 3600
SETEX user:1001 3600 value

Redis 删除过期 key 主要依赖:

  1. 惰性删除:访问 key 时检查是否过期,过期则删除
  2. 定期删除:Redis 定期抽样检查部分 key,删除其中过期的数据

如果只用惰性删除,很多过期 key 可能长期留在内存中;如果全量扫描删除,又会影响性能。所以 Redis 使用定期删除和惰性删除结合的方式。

2. 内存淘汰#

当 Redis 内存达到限制后,会根据配置的淘汰策略删除部分 key。

常见策略包括:

  • noeviction:不淘汰,写入时报错
  • allkeys-lru:从所有 key 中淘汰最近最少使用的 key
  • volatile-lru:从设置了过期时间的 key 中淘汰最近最少使用的 key
  • allkeys-random:从所有 key 中随机淘汰
  • volatile-random:从设置了过期时间的 key 中随机淘汰
  • volatile-ttl:优先淘汰剩余时间短的 key
  • allkeys-lfu:从所有 key 中淘汰使用频率低的 key
  • volatile-lfu:从设置了过期时间的 key 中淘汰使用频率低的 key

缓存场景中比较常见的是 allkeys-lruallkeys-lfu


八、Redis 高可用#

1. 主从复制#

Redis 支持主从复制。主节点负责写,从节点复制主节点数据,可以用于读扩展和数据备份。

基本结构:

Client -> Master -> Replica

主从复制的问题是:如果主节点宕机,需要有机制完成故障转移。

2. 哨兵模式#

Sentinel 可以监控 Redis 主从节点状态。当主节点宕机时,Sentinel 可以从从节点中选举新的主节点。

哨兵主要负责:

  1. 监控 Redis 节点
  2. 判断主节点是否下线
  3. 选举新的主节点
  4. 通知客户端新的主节点地址

3. Redis Cluster#

Redis Cluster 用于数据分片和集群高可用。

Redis Cluster 把数据划分到 16384 个 hash slot 中,不同节点负责不同的 slot。

例如:

key -> CRC16(key) % 16384 -> slot -> Redis node

Cluster 的特点:

  • 支持水平扩展
  • 支持自动分片
  • 支持故障转移
  • 客户端需要支持重定向

如果数据量和并发量较大,单机 Redis 无法支撑时,可以考虑 Redis Cluster。

使用 Cluster 时要额外注意多 key 操作。Redis Cluster 会把不同 key 分布到不同 slot 上,如果一次命令涉及多个 key,而这些 key 不在同一个 slot,命令可能无法直接执行。需要让相关 key 使用相同的 hash tag,例如 order:{1001}:infoorder:{1001}:lock,让它们落在同一个 slot 中。


九、Redis 使用中的注意事项#

1. Key 设计#

Redis key 应该具备清晰的业务含义。

推荐格式:

业务名:对象类型:对象ID:字段

例如:

user:1001:profile
article:1001:view
order:timeout
rank:daily:2026-06-16

注意事项:

  1. key 不要太长
  2. 命名要统一
  3. 避免不同业务使用相同 key
  4. 可以用冒号分层
  5. 批量操作时注意 key 的数量

2. 避免大 key#

大 key 指 value 特别大,或者集合元素特别多的 key。

例如:

  • 一个 String 存储几 MB 的 JSON
  • 一个 List 有几百万个元素
  • 一个 Hash 有几十万个 field
  • 一个 ZSet 有大量 member

大 key 的问题:

  1. 读写耗时长
  2. 删除可能阻塞 Redis
  3. 网络传输成本高
  4. 持久化和复制压力大
  5. 容易造成集群数据倾斜

解决思路:

  • 拆分 key
  • 控制集合大小
  • 分页读取
  • 使用异步删除
  • 避免一次性 HGETALLSMEMBERS 读取大量数据

3. 避免热 key#

热 key 指某个 key 被极高频率访问。

热 key 的问题是流量集中在单个 Redis 节点上,可能导致节点压力过大。

解决思路:

  • 本地缓存
  • 多副本读
  • key 拆分
  • 热点数据预加载
  • 限流降级

4. 谨慎使用复杂命令#

一些命令在数据量大时可能比较危险,例如:

KEYS *
HGETALL big_hash
SMEMBERS big_set
LRANGE big_list 0 -1
ZRANGE big_zset 0 -1

生产环境中不建议使用 KEYS * 扫描全部 key,可以使用 SCAN 分批扫描。

例如:

SCAN 0 MATCH user:* COUNT 100

SCAN 是渐进式遍历,不会像 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 时,可以从三个角度理解:

  1. 业务角度:Redis 能解决什么问题
  2. 数据结构角度:每种类型适合什么场景
  3. 系统设计角度:如何处理缓存一致性、高可用、大 key、热 key、持久化等问题

在实际项目中,Redis 用得好可以显著提升系统性能;用得不合理也可能引入缓存雪崩、数据不一致、大 key 阻塞、锁误删等问题。所以 Redis 的重点不只是会用命令,还要理解每个场景背后的设计取舍。

Redis 使用案例与相关知识笔记
https://contrue.top/posts/redis-use-cases-notes/
Author
contrueCT
Published at
2026-04-06
License
CC BY-NC-SA 4.0