第 25 章 · Redis
一句话:把数据放在内存里的键值数据库,读写快,自带过期,常用作缓存、会话、排行榜、简单队列。 用它还是用别的:要快、允许丢、数据量放得进内存就用它;要持久可靠保存的业务数据用 PostgreSQL 或 MySQL;复杂队列用 RabbitMQ 或 Kafka(第 26 章选型表)。 核对版本:Redis 8,2026-10
25.1 安装与版本
Section titled “25.1 安装与版本”本书用 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_versiondocker compose exec redis redis-cli INFO server | Select-String redis_version不想用 Docker、要在本机安装的,见 25.7 的“下载与本机安装”。
25.2 五分钟概念
Section titled “25.2 五分钟概念”- 键值(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 号,要隔离就再起一个实例。
25.3 常用命令
Section titled “25.3 常用命令”命令名大小写都行,键名区分大小写。表里的 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 |
生产慎用:当前库全部键立即删除,无法恢复 |
25.4 典型工作流
Section titled “25.4 典型工作流”给查询加缓存(Cache-Aside)
Section titled “给查询加缓存(Cache-Aside)”查用户信息时先看 Redis,没有再查数据库并写回。下面用 redis-cli 把程序要做的读、写回、改库、删缓存四步手动走一遍:
- 进命令行:
docker compose exec redis redis-cli。 - 读缓存,返回
(nil)表示没有缓存:GET user:1。 - 程序查数据库,把结果写进缓存,5 分钟后过期:
SET user:1 '{"id":1,"name":"Tom"}' EX 300。 - 再读一次,这次直接拿到 JSON,不用查库:
GET user:1。 - 看缓存还剩几秒:
TTL user:1。 - 用户改了名字,程序先更新数据库,再删缓存:
DEL user:1。 - 下次读又是
(nil),回到第 3 步,写回的就是新数据:GET user:1。
顺序必须是先改库再删缓存。反过来,删缓存和改库之间如果有请求读到旧数据并写回缓存,旧数据要等 5 分钟过期后才会消失。
做一把分布式锁
Section titled “做一把分布式锁”多台机器上的定时任务,同一时刻只允许一台执行:
-
抢锁,值用每次随机生成的字符串(如 UUID),30 秒后自动释放:
SET lock:job1 8f14e45f NX PX 30000。 -
返回
OK是抢到了;返回(nil)说明别人持有,看还剩多少毫秒再决定等不等:PTTL lock:job1。 -
抢到后干活。干活时间要明显短于 30 秒,否则锁会在活没干完时过期。
-
干完释放锁,用下面这段 Lua 脚本:值等于自己加锁时的随机值才删,不等就不动。
-- KEYS[1] 是锁的键,ARGV[1] 是加锁时用的随机值if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])elsereturn 0end -
在 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 里一次执行完,中间不会插进别的命令。
游戏或积分榜,按分数实时排名:
- 写入三个人的分数:
ZADD board 100 tom 90 jerry 120 alice。 - tom 赢了一局加 5 分,返回新分数
"105":ZINCRBY board 5 tom。 - 取前十名,高分在前,带分数:
ZRANGE board 0 9 REV WITHSCORES。 - 查 tom 排第几,返回
(integer) 1,从 0 数所以是第二名:ZREVRANK board tom。 - 查 tom 的分数:
ZSCORE board tom。
分数相同的成员按名字的字典序倒序排列。
25.5 配置要点
Section titled “25.5 配置要点”配置文件的位置:
| 范围 | 路径 |
|---|---|
| 配置文件(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 30025.6 注意事项
Section titled “25.6 注意事项”- 线上 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 <密码>;程序里在连接串写上密码。
25.7 深入阅读
Section titled “25.7 深入阅读”- Redis 官方文档:入门、数据结构、运维都在这里。
- 命令参考:每条命令的参数、复杂度和示例,查命令到这里。
- 数据结构详解:五种核心结构加 Stream、Bitmap 等的用法和适用场景。
- 持久化:RDB 和 AOF 怎么选、怎么配。
- 下载与本机安装:不想用 Docker 时各平台的安装方法。