CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Redis 缓存入门/课时/String 与计数
RawBlame
课时目录12 节

Redis 缓存入门

  1. 01Redis 是什么
  2. 02CLI 与键命名
  3. 03String 与计数
  4. 04Hash / List / Set / ZSet
  5. 05过期与淘汰
  6. 06缓存模式
  7. 07Spring Data Redis
  8. 08会话与 Sa-Token
  9. 09Docker 运行 Redis
  10. 10Pub/Sub 入门
  11. 11测试与监控
  12. 12综合练习

String 与计数

SET/GET、INCR、过期时间。

25 分钟

String:不只是字符串,还是计数器和锁

这一课你将能做到

  1. 熟练使用 SET 的四个关键选项:EX/PX(过期)、NX(不存在才设)、XX(存在才设),并写出「分布式锁雏形」命令 SET key val NX EX 30
  2. 用 INCR/INCRBY/DECR 做原子计数,解释清楚为什么高并发下它不会数错而「先 GET 再 SET」会
  3. 读懂本站 RedisRequestRateLimiter 的完整 Java 实现,并用 StringRedisTemplate 写出带过期时间的标准缓存写入

为什么要学

String 是 Redis 使用率最高的类型,但多数初学者只会 SET key value 三个词。真实工程里的全部价值藏在选项和原子性里:

  • 接口限流要「计数 + 60 秒过期」,两条命令分开写就有竞态
  • 缓存写入要「存 JSON + 5 分钟过期」,Java 里对应一次 set(key, json, Duration) 调用
  • 「只允许一个实例执行定时任务」需要 SET lockkey uuid NX EX 30 一条命令

本站 RedisRequestRateLimiter(backend/src/main/java/com/codeforge/config/RedisRequestRateLimiter.java)就是用 String 的 INCR + EXPIRE 实现的固定窗口限流。学完这一课,你能逐行读懂它,还能解释它为什么并发安全。

先建立图像

String 在 Redis 里是一块字节数组,但不只是「文本」——它是四种角色的合体:

SET name "xiaolin"          → 文本
SET json '{"a":1}'          → JSON 缓存(本站 tracks 列表就是这么存的)
INCR visits                 → 数字(内部按整数优化存储)
SET lock "uuid" NX EX 30    → 带自动过期的互斥标记

可以把 String 想成一张「便签」,写的时候带可选动作:

  • 顺手定个闹钟(EX 秒 / PX 毫秒)
  • 写之前检查柜子空不空(NX:空才写)
  • 写之前检查柜子有没有东西(XX:有才写)

跟着做

步骤 1:SET 的选项组合实验

docker exec -it redis-dev redis-cli

SET cache:demo "v1" EX 60
TTL cache:demo                # (integer) 60 左右,倒计时
SET cache:demo "v2" NX EX 60  # (nil) —— key 已存在,NX 拒绝写入
GET cache:demo                # "v2" —— 上一条的值没被覆盖
SET cache:demo "v3" XX        # OK —— XX:存在才写,成功
SET cache:nope "v" XX         # (nil) —— 不存在,XX 拒绝

步骤 2:INCR 原子计数

DEL pv
INCR pv        # (integer) 1 —— key 不存在时先当 0 再 +1
INCR pv        # (integer) 2
INCRBY pv 10   # (integer) 12
DECR pv        # (integer) 11

注意:INCR 只能作用于「能解析成整数」的值,对 "xiaolin" INCR 会报 value is not an integer or out of range。

步骤 3:并发安全小实验(可选进阶)

docker exec redis-dev sh -c 'for i in 1 2 3 4 5; do redis-cli INCR concurrent:test > /dev/null & done; wait'
docker exec redis-dev redis-cli GET concurrent:test   # 一定是 "5"

对比 MySQL 里「读出来 +1 再写回去」的两个事务互相覆盖,INCR 是在服务端一条命令内完成读-改-写,没有插队的机会。

步骤 4:读本站限流器源码

打开 backend/src/main/java/com/codeforge/config/RedisRequestRateLimiter.java,核心逻辑四行:

String redisKey = "rl:" + key;                                   // ① 前缀隔离
Long n = stringRedisTemplate.opsForValue().increment(redisKey);  // ② 原子 +1
if (n != null && n == 1L) {                                      // ③ 窗口内第一次
    stringRedisTemplate.expire(redisKey, window);                //    给计数器上闹钟
}
return n == null || n <= max;                                    // ④ 超过配额就拒绝

这就是「固定窗口限流」:同一个 key(如客户端 IP)在 window 时间内最多放行 max 次,第 max+1 次被拒。

步骤 5:Java 写缓存的标准姿势

// 带 TTL 写入 JSON(综合实战课你会写完整版)
stringRedisTemplate.opsForValue()
    .set("coderoadmap:tracks:list", json, Duration.ofMinutes(5));

带 Duration 的重载一步完成「写值 + 过期」,等价于 SET key value EX 300,避免「写了值忘了过期」的经典事故。

步骤 6:MSET/MGET 批量操作

MSET a:1 "x" a:2 "y" a:3 "z"
MGET a:1 a:2 a:3      # 一次网络往返取回 3 个值

Java 侧对应 opsForValue().multiGet(keys)。批量命令省的是网络往返次数,不是内存。

讲清楚要点

  1. INCR 的原子性来自「命令在服务端单线程执行」:不是靠加锁,而是根本没有第二个执行者能插队。多实例共用同一个 Redis 计数,天然全局一致——这正是本站限流用 Redis 而不用 JVM 内存 Map 的原因(内存 Map 在多实例下各算各的,配额形同虚设)。
  2. SET NX EX 是一切分布式锁的地基:NX 保证只有一个客户端写成功,EX 保证持有者崩溃后锁自动释放。完整的生产锁还要考虑「误删别人的锁」(值存唯一 ID + Lua 脚本校验再删),这是进阶话题,本课先建立直觉。
  3. 值最大 512MB,但大值是性能杀手:10MB 的 JSON 每次 GET 都要网络传输和内存复制。本站约定缓存只存查询结果,单 key 控制在几百 KB 内。
  4. increment + expire 分两步有极小概率窗口:如果 n==1 时连接恰好断开、expire 没执行,计数器就永生了。严谨实现用 Lua 脚本把两步原子化;本站用「只在 n==1 时 expire」把风险压缩到窗口起点这一个点。
  5. String 存 JSON 是本站缓存的基本形态:读时 get → 反序列化;miss 时查 MySQL → 序列化 → set + TTL。第 6 课的 Cache-Aside 和第 12 课的实战都围绕它。

你可能会懵的地方

  • INCR 为什么不等于「GET 再 SET」? 两步之间别的客户端可以插进来执行自己的 INCR,你的 SET 会覆盖它的结果,计数丢失。INCR 是服务端一条命令内完成读-改-写,中间不可分割。

  • NX 和 XX 有什么实际用武之地? NX:分布式锁、防止重复初始化、幂等标记。XX:只更新已存在的缓存(避免 miss 时误写脏数据)。日常最常用的还是不带任何选项的普通 SET。

  • 限流器里为什么 n == 1 才 expire,第 2~N 次不续期吗? 固定窗口的定义就是「窗口起点 + 固定长度」。第一次 INCR 即窗口起点,上一次闹钟即可;后续每次都 expire 会把窗口往后推,语义变成滑动窗口,配额就失真了。

  • SET 会返回 (nil) 吗? 会:带 NX/XX 且条件不满足时返回 (nil) 而不是 OK。Java 里对应返回 null,业务判断要区分「写入成功」和「条件不满足被拒」。

  • opsForValue().increment() 返回 Long 还是 long? 返回包装类型 Long(管道/事务场景下可能为 null)。本站限流代码里 n != null && 的判空就是防它。

错法对照

错法问题正解
SET 完再单独 EXPIRE两步之间崩溃 → key 永生SET key val EX 60 或 Java 的 Duration 重载
计数用 GET +1 再 SET并发下丢更新INCR / INCRBY
分布式锁只有 NX 没有 EX持有者崩溃后死锁SET lock uuid NX EX 30
缓存值塞几 MB 的大 JSON传输、内存、网络全面放大只缓存查询结果,控制单 key 大小
对非整数值 INCR报 value is not an integer先确认值类型;浮点用 INCRBYFLOAT
限流 key 先 SET "0" 再 INCR白白多一次写,还引入竞态直接 INCR(不存在时自动当 0)

卡点急救

  1. (error) ERR value is not an integer or out of range:对 "abc" INCR 了。先 DEL 后重设数字,并排查是谁写脏了这个 key。
  2. TTL 返回 -1:key 存在但没过期时间——大概率 SET 时漏了 EX,或更新时把 TTL 覆盖没了。补 EXPIRE key 60。
  3. 限流计数器永不过期:历史上 n==1 那次 expire 调用失败了。TTL rl:xxx 确认后手工 EXPIRE 补上。
  4. Java set 报 SerializationException:StringRedisTemplate 只存字符串,对象要先经 ObjectMapper 转成 JSON(第 7 课详讲序列化)。
  5. MGET 返回列表里有 null:对应下标的 key 不存在。Java 里逐个判空,别直接解引用。

面试常问

  • Redis 怎么做接口限流? 固定窗口:INCR rl:{key} 原子计数,首次(返回 1)时 EXPIRE window 设置窗口,比较计数与 max 决定放行。结合本站代码讲:RedisRequestRateLimiter,key 前缀 rl:,多实例共享同一计数。进阶可提 Lua 原子化两步、ZSet 滑动窗口方案。

  • 为什么 INCR 是原子的?单线程怎么理解? Redis 命令在服务端由单线程串行执行,一条 INCR 内部完成读-改-写,中间不可能插入其他命令,因此无需锁也不会丢更新。多实例部署时所有实例打到同一个 Redis,计数天然全局一致。

  • SETNX 和 SET NX EX 的区别? SETNX 是老命令,等价于 SET key value NX,不能带过期时间——做锁时持有者一崩溃就死锁。正确姿势是一条 SET key value NX EX seconds 原子完成「抢锁 + 自动释放」。

自测三问

  1. 写出「key 不存在时才设置、60 秒过期」的一条命令,并说明它返回 OK 和 (nil) 分别代表什么。
  2. 本站限流器为什么在 n == 1 时才 EXPIRE?如果每次都 EXPIRE 会怎样?
  3. 用 StringRedisTemplate 写一个 5 分钟过期的 JSON 缓存,代码是什么?等价的 redis-cli 命令是什么?

小结一句话

String 的工程价值在选项与原子命令:NX/EX 组合出锁与幂等,INCR 出全局一致的计数器,本站限流器就是这两个特性的直接应用。

延伸

  • SET 命令
  • INCR 命令
  • 下一课:五种数据结构——Hash、List、Set、ZSet 登场:收藏、排行、队列、去重各该用什么结构,以及本站 tracks 列表为什么坚持用 String 存 JSON。
加载本节练习…
← 上一课CLI 与键命名下一课 →Hash / List / Set / ZSet