跳转到内容

第 25 章 · Redis

一句话:把数据放在内存里的键值数据库,读写快,自带过期,常用作缓存、会话、排行榜、简单队列。 用它还是用别的:要快、允许丢、数据量放得进内存就用它;要持久可靠保存的业务数据用 PostgreSQL 或 MySQL;复杂队列用 RabbitMQ 或 Kafka(第 26 章选型表)。 核对版本:Redis 8,2026-10

本书用 Docker 跑 Redis。在随书的 compose/ 目录下启动,Compose 的用法详见第 13 章:

终端窗口
docker compose --profile cache up -d

宿主机端口是 6379,不设口令,程序里连 localhost:6379 即可。数据存在命名卷 redisdata 里,docker compose --profile cache down 不会删。

进 Redis 的命令行客户端 redis-cli,出现 127.0.0.1:6379> 提示符后输入 PING,返回 PONG 就是通了:

终端窗口
docker compose exec redis redis-cli

本章 25.3 和 25.4 的命令都在这个提示符下输入,按 Ctrl+C 或输入 exit 退出。只跑一条命令时可以直接跟在后面,如 docker compose exec redis redis-cli DBSIZE。

查版本:

终端窗口
docker compose exec redis redis-cli INFO server | grep redis_version
终端窗口
docker compose exec redis redis-cli INFO server | Select-String redis_version

不想用 Docker、要在本机安装的,见 25.7 的“下载与本机安装”。

  • 键值(Key-Value):每条数据是一个键加一个值。键是字符串,习惯用冒号分层,如 user:1、session:abc;值有多种结构,见下一条。没有表、没有 SQL,按键存取。
  • 五种核心数据结构:String 存一段文本、JSON 或计数器;Hash 存一个对象的多个字段,如用户的姓名和年龄;List 是有序列表,两头进出,做简单队列;Set 是不重复的集合,做标签、去重、求共同好友;Sorted Set(有序集合)每个成员带一个分数,按分数排序,做排行榜。
  • 过期(TTL,Time To Live):给键设一个存活时间,到点 Redis 自动删掉它。缓存、验证码、会话都靠它,不用自己写清理任务。
  • 单线程执行命令:Redis 一次只执行一条命令,执行完再轮到下一条。所以普通命令极快,但一条慢命令(如对百万个键的 KEYS *)会让所有客户端一起等,见 25.6。
  • 持久化(Persistence):内存里的数据断电就没,Redis 用两种方式落盘。RDB(Redis Database,快照文件)定时把全部数据拍成一个快照文件 dump.rdb,重启丢最后一次快照之后的写入;AOF(Append Only File)把每条写命令追加到日志,默认每秒刷一次盘,最多丢一秒。随书 compose 已开 AOF。
  • 数据库编号:一个 Redis 实例里有 16 个库,编号 0 到 15,默认用 0 号,用 SELECT 1 切换。各库的键互不可见,但共用内存和线程,一般只用 0 号,要隔离就再起一个实例。

命令名大小写都行,键名区分大小写。表里的 k、q、tags、board 都是示例键名;返回 (nil) 表示键不存在,(integer) 1 表示成功或真,0 表示没做或假。

任务 命令 说明
连接与查看
测试连接 PING 返回 PONG 就是通的
看内存用量 INFO memory 看 used_memory_human 一行;不带参数输出全部信息
当前库有多少键 DBSIZE 只数当前库
实时看所有命令 MONITOR 调试用,吞吐量会明显下降,生产别开;Ctrl+C 退出
String
写一个值 SET k v 已有就覆盖,原来的过期时间也会清掉
读一个值 GET k 不存在返回 (nil)
写入并设过期 SET k v EX 60 60 秒后自动删;毫秒用 PX
不存在才写 SET k v NX 写成返回 OK,已存在返回 (nil),做锁用
计数加一 INCR k 原子操作,多个客户端同时加也不会少算;加任意数用 INCRBY k 10
一次读多个 MGET k1 k2 按顺序返回,不存在的位置是 (nil)
Hash
写对象的字段 HSET user:1 name Tom age 20 一次可写多个字段,只改给出的字段
读一个字段 HGET user:1 name 返回 "Tom"
读全部字段 HGETALL user:1 字段和值交替列出;字段上万时别用
删字段 HDEL user:1 age 字段数字加减用 HINCRBY user:1 age 1
List
从左边放入 LPUSH q a 返回放入后的长度
从右边取出 RPOP q 和 LPUSH 配合就是先进先出的队列
看全部元素 LRANGE q 0 -1 -1 表示最后一个,不会删除元素
阻塞地取 BRPOP q 5 队列空就等最多 5 秒,等不到返回 (nil),消费者用它不用轮询
Set
加成员 SADD tags a b 已有的成员不重复加
列出全部成员 SMEMBERS tags 顺序不固定
是否包含 SISMEMBER tags a 返回 1 或 0
求交集 SINTER s1 s2 两个集合都有的成员,如共同好友
Sorted Set
写分数 ZADD board 100 tom 已有成员就改分数
加分 ZINCRBY board 5 tom 返回加完后的分数
前十名 ZRANGE board 0 9 REV WITHSCORES 高分在前,成员和分数交替列出
查名次 ZREVRANK board tom 高分在前的名次,从 0 数;ZRANK 是低分在前
查分数 ZSCORE board tom 不在榜上返回 (nil)
过期
设过期 EXPIRE k 60 对已有的键设 60 秒;取消过期用 PERSIST k
还剩多少秒 TTL k -1 是永不过期,-2 是键不存在
键管理
是否存在 EXISTS k 返回 1 或 0
删键 DEL k 立即删,大键会卡住整个 Redis
异步删键 UNLINK k 后台释放内存,删大键用它
按模式找键 SCAN 0 MATCH user:* COUNT 100 返回下一个游标和一批键,拿游标再查,直到游标为 0;别用 KEYS *
发布订阅
订阅频道 SUBSCRIBE ch 进入等待状态,收到一条打印一条,Ctrl+C 退出
发消息 PUBLISH ch msg 另开一个 redis-cli 发,返回收到的订阅者数;没人订阅消息就丢
运维
看慢命令 SLOWLOG GET 10 最近 10 条超过 10 毫秒的命令,含耗时和参数
一个键占多少内存 MEMORY USAGE k 单位字节
看配置项 CONFIG GET maxmemory 返回字节数,0 表示不限
手动做快照 BGSAVE 后台写 dump.rdb,基本不阻塞其他命令(数据量大时 fork 那一下可能卡)
清空当前库 FLUSHDB 生产慎用:当前库全部键立即删除,无法恢复

查用户信息时先看 Redis,没有再查数据库并写回。下面用 redis-cli 把程序要做的读、写回、改库、删缓存四步手动走一遍:

  1. 进命令行:docker compose exec redis redis-cli。
  2. 读缓存,返回 (nil) 表示没有缓存:GET user:1。
  3. 程序查数据库,把结果写进缓存,5 分钟后过期:SET user:1 '{"id":1,"name":"Tom"}' EX 300。
  4. 再读一次,这次直接拿到 JSON,不用查库:GET user:1。
  5. 看缓存还剩几秒:TTL user:1。
  6. 用户改了名字,程序先更新数据库,再删缓存:DEL user:1。
  7. 下次读又是 (nil),回到第 3 步,写回的就是新数据:GET user:1。

顺序必须是先改库再删缓存。反过来,删缓存和改库之间如果有请求读到旧数据并写回缓存,旧数据要等 5 分钟过期后才会消失。

缓存旁路的读写路径:读先查 Redis,未命中查库并写回;写先改库再删缓存

多台机器上的定时任务,同一时刻只允许一台执行:

  1. 抢锁,值用每次随机生成的字符串(如 UUID),30 秒后自动释放:SET lock:job1 8f14e45f NX PX 30000。

  2. 返回 OK 是抢到了;返回 (nil) 说明别人持有,看还剩多少毫秒再决定等不等:PTTL lock:job1。

  3. 抢到后干活。干活时间要明显短于 30 秒,否则锁会在活没干完时过期。

  4. 干完释放锁,用下面这段 Lua 脚本:值等于自己加锁时的随机值才删,不等就不动。

    -- KEYS[1] 是锁的键,ARGV[1] 是加锁时用的随机值
    if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
    else
    return 0
    end
  5. 在 redis-cli 里执行这段脚本,1 表示后面跟一个键,返回 (integer) 1 是删掉了,0 是锁已经不是你的:EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:job1 8f14e45f。

为什么不能干完直接 DEL lock:job1:如果活干了 35 秒,锁在第 30 秒已经过期,另一台机器抢到了新锁,这时你的 DEL 删掉的是别人的锁,第三台机器又能抢到,同时有两台在跑。先 GET 比对再 DEL 分两条命令也不行,两条之间锁可能刚好过期易主;Lua 脚本在 Redis 里一次执行完,中间不会插进别的命令。

游戏或积分榜,按分数实时排名:

  1. 写入三个人的分数:ZADD board 100 tom 90 jerry 120 alice。
  2. tom 赢了一局加 5 分,返回新分数 "105":ZINCRBY board 5 tom。
  3. 取前十名,高分在前,带分数:ZRANGE board 0 9 REV WITHSCORES。
  4. 查 tom 排第几,返回 (integer) 1,从 0 数所以是第二名:ZREVRANK board tom。
  5. 查 tom 的分数:ZSCORE board tom。

分数相同的成员按名字的字典序倒序排列。

配置文件的位置:

范围 路径
配置文件(Linux) /etc/redis/redis.conf
配置文件(Windows) —
配置文件(Docker 镜像内) 默认无配置文件,用启动参数
数据与日志 /data(RDB、AOF)

Redis 官方不出 Windows 版,Windows 上用 Docker 跑。Docker 镜像默认不带配置文件,用启动参数:随书 compose.yaml 里的 command: ["redis-server", "--appendonly", "yes", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"] 就是例子,每个 --配置项 值 相当于配置文件里的一行,要加配置就往这一行里加参数,如 "--requirepass", "<密码>"。

运行中查看用 CONFIG GET <配置项>,临时修改用 CONFIG SET <配置项> <值>,立即生效,但重启后恢复为文件或启动参数里的值。

值得改的项:

配置项 建议值 效果
bind 127.0.0.1 本机安装时只接受本机连接;Docker 里别改,见表下
requirepass 一串长随机密码 不登录的连接执行任何命令都报 NOAUTH
maxmemory 机器内存的一半左右,如 2gb 内存用到上限后按下一项淘汰;不设就一直涨
maxmemory-policy allkeys-lru 满了淘汰最久没用的键;默认 noeviction 是直接拒绝写入
appendonly yes 开 AOF,重启后数据还在
save 3600 1 300 100 RDB 快照频率:3600 秒内有 1 次写入,或 300 秒内有 100 次写入,就拍一次
timeout 300 客户端空闲 300 秒断开,0 是永不断开

在 Docker 里让容器继续监听所有网卡,用 compose 的端口映射 "127.0.0.1:6379:6379" 限制只有本机能连,再加 requirepass;随书 compose 的端口映射已经这样写。容器里写 bind 127.0.0.1,宿主机就连不上了,因为那是容器自己的回环地址。

Linux 本机安装的最小配置,写进 /etc/redis/redis.conf。注意 redis.conf 不支持行尾注释,注释必须单独一行写在上面,写在同一行会被当成参数而启动失败:

# 只接受本机连接
bind 127.0.0.1
# 连接密码,换成自己的长随机串
requirepass <强密码>
# 内存上限
maxmemory 256mb
# 满了淘汰最久没用的键
maxmemory-policy allkeys-lru
# 开 AOF 日志,重启不丢数据
appendonly yes
# AOF 每秒刷一次盘,最多丢 1 秒
appendfsync everysec
# 3600 秒内 1 次写或 300 秒内 100 次写就拍 RDB 快照
save 3600 1 300 100
# 客户端空闲 300 秒断开
timeout 300
  • 线上 Redis 突然卡几秒,所有请求一起超时。 原因:有人跑了 KEYS * 或 DEL 了一个大键,单线程被堵住。处理:用 SCAN 代替 KEYS,用 UNLINK 代替 DEL;SLOWLOG GET 10 找出是哪条命令。
  • 内存一直涨,最后进程被系统杀掉,或写入报 OOM command not allowed when used memory > 'maxmemory'. 原因:没设 maxmemory,或设了但淘汰策略是默认的 noeviction,键又不过期。处理:设 maxmemory 和 maxmemory-policy allkeys-lru;缓存写入都带 EX。
  • 一个键几百 MB(大键),读它或删它时 Redis 卡顿。 原因:把整张表塞进了一个 Hash 或 List。处理:MEMORY USAGE <键> 找出来,按用户或日期拆成多个键,如 order:2026-10-07。
  • Redis 里的数据被清空,只剩一个陌生的键,内容是勒索信息。 原因:监听 0.0.0.0 又没设密码,端口暴露在公网;Docker 镜像默认就是这样。处理:本机安装用 bind 127.0.0.1 加 requirepass;Docker 把端口映射写成 "127.0.0.1:6379:6379" 加 requirepass;云上用安全组只放内网。
  • 重启或 docker compose --profile cache down 后再起,DBSIZE 返回 (integer) 0。 原因:没开持久化,或容器没挂数据卷,或用了 --profile cache down -v 把卷删了。处理:appendonly yes,compose 里把卷挂到 /data(随书文件已挂)。
  • 缓存过期的那一刻,数据库 CPU 冲到 100%(缓存击穿)。 原因:一个热键过期,成千上万个请求同时没命中,一起去查库。处理:热键不设过期、由后台定时刷新;或用 SET <锁键> 1 NX EX 10 让只有一个请求去查库,其余稍等再读缓存。
  • 查一个不存在的 ID,每次都打到数据库(缓存穿透)。 原因:数据库查不到时没写缓存,下次照样没命中。处理:查不到也缓存一个空值,TTL 设短些,如 SET user:999 "" EX 60。
  • (error) NOAUTH Authentication required. 原因:服务端设了 requirepass,客户端连上后没登录。处理:在 redis-cli 里先 AUTH <密码>,或启动时 redis-cli -a <密码>;程序里在连接串写上密码。