Redis 练习题(12 题)
学了那么多 Redis 命令,得动手敲才算真的会。这一页给 12 道精选练习题,按难度分三级——基础 4 题(string + 过期 + hash)、中级 3 题(list / set / zset 三大结构)、高级 5 题(事务 / 发布订阅 / SCAN / 持久化 / 分布式锁)。每题含题目描述、提示、可折叠的参考答案——答案里的命令可以直接在 redis-cli 里敲。
练习环境:本地 redis-cli(装好 Redis 服务后终端敲 redis-cli 即可)。所有命令都能直接复制到交互式命令行里跑,看 OK / (integer) / (nil) 这些返回值。
使用建议:先别急着展开答案。读题后自己敲,卡住再看提示,实在写不出再对照参考答案。Redis 命令简单,关键在"什么场景用哪种数据结构"的判断。
基础(4 题):string + 过期 + hash
这一组覆盖 Redis 最常用的基础——string 读写、原子计数、过期管理、hash 存对象。日常 80% 的 Redis 用法就是它们。
第 1 题:SET / GET / DEL 字符串
题目:练习 string 的基本操作——设值、取值、删除、判断存在、查类型。再写一个带 EX + NX 选项的 SET(设置 key 同时带过期且只在不存在时设)。
提示:SET k v / GET k / DEL k / EXISTS k / TYPE k;SET k v EX 3600 NX 是一条命令完成"设值+过期+条件"的原子操作。
参考答案
127.0.0.1:6379> SET user:1:name "小明"
OK
127.0.0.1:6379> GET user:1:name
"小明"
127.0.0.1:6379> DEL user:1:name
(integer) 1 # 返回删除的 key 数量
127.0.0.1:6379> EXISTS user:1:name
(integer) 0 # 0 不存在,1 存在
127.0.0.1:6379> TYPE user:1:name
(error) ERR no such key
# SET 带选项(一条命令完成,原子操作):
# EX 秒级过期 PX 毫秒级过期
# NX 不存在才设(常用于分布式锁)
# XX 存在才设
127.0.0.1:6379> SET token "abc" EX 3600 NX
OK
# string 是 Redis 最基础类型;value 最大 512MB,可存文本、数字、序列化 JSON第 2 题:INCR / DECR 原子计数器
题目:练习原子计数——INCR 自增、INCRBY 加指定值、DECR 自减、INCRBYFLOAT 浮点自增。再想想阅读量、点赞数、库存、限流这些场景为什么用 Redis 的 INCR 而不是数据库。
提示:Redis 单线程,INCR 是原子的,多连接并发也安全;INCRBYFLOAT 支持浮点。限流常用 INCR + EXPIRE 实现计数窗口。
参考答案
127.0.0.1:6379> SET views 0
OK
127.0.0.1:6379> INCR views # 原子 +1
(integer) 1
127.0.0.1:6379> INCR views
(integer) 2
127.0.0.1:6379> INCRBY views 10 # 加指定数
(integer) 12
127.0.0.1:6379> DECR views # -1
(integer) 11
127.0.0.1:6379> INCRBYFLOAT price 2.5 # 浮点自增
"2.5"
# Redis 单线程,INCR 是原子的——多连接同时 INCR 也不会出错
# 实战用途:
# 文章阅读量、点赞数:INCR
# 库存扣减:INCRBY -1 后判断结果是否 < 0
# API 限流:INCR + EXPIRE 实现滑动窗口计数器第 3 题:EXPIRE / TTL / PERSIST 过期管理
题目:给一个 key 设过期、查剩余时间、转回永久、用 SET ... EX 一步到位。再解释 TTL 返回值的含义(-1、-2、正数)。
提示:EXPIRE k 秒 / PEXPIRE k 毫秒 / TTL k / PERSIST k;SET k v EX n 比 SET + EXPIRE 更高效(单次往返)。-1 永久,-2 不存在。
参考答案
127.0.0.1:6379> SET session:abc "user_data"
OK
127.0.0.1:6379> EXPIRE session:abc 60 # 60 秒后过期
(integer) 1
127.0.0.1:6379> TTL session:abc # 剩余秒数
(integer) 58
127.0.0.1:6379> PEXPIRE session:abc 60000 # 毫秒级
(integer) 1
127.0.0.1:6379> PERSIST session:abc # 转为永久
(integer) 1
127.0.0.1:6379> TTL session:abc
(integer) -1 # -1 永不过期;-2 key 不存在
# 也可以 SET 时直接带过期:SET k v EX 60(更高效,单次往返)
127.0.0.1:6379> SET code:13800001234 "123456" EX 300
OK
# 过期机制:惰性删除(访问时检查)+ 定期删除(抽样巡检)组合
# 实战:验证码 5 分钟过期、会话 token、缓存自动失效第 4 题:Hash HSET / HGET / HGETALL
题目:用一个 hash 存用户对象(name / age / city)——一次设多字段、取单字段、取多字段、取全部、字段自增、删字段。再说为什么用 hash 比每个属性一个 string 好。
提示:HSET k f v [f v...] / HGET / HMGET / HGETALL / HINCRBY / HDEL;hash 改单字段不用反序列化整个 JSON,内存也更省。
参考答案
127.0.0.1:6379> HSET user:1 name "小明" age 30 city "北京"
(integer) 3 # 一次设多个字段
127.0.0.1:6379> HGET user:1 name
"小明"
127.0.0.1:6379> HMGET user:1 name age # 取多个字段
1) "小明"
2) "30"
127.0.0.1:6379> HGETALL user:1
1) "name"
2) "小明"
3) "age"
4) "30"
5) "city"
6) "北京"
127.0.0.1:6379> HINCRBY user:1 age 1 # 字段原子自增
(integer) 31
127.0.0.1:6379> HDEL user:1 city
(integer) 1
# Hash 适合存对象:比 string+JSON 改单字段高效(不用反序列化全量)
# 内存占用也比存多个独立 string 小(ziplist 编码时)中级(3 题):list / set / zset 三大结构
这一组覆盖 Redis 区别于"key-value 存储"的精髓——队列(list)、集合运算(set)、排行榜(zset)。掌握它们,你就懂了 Redis 为什么这么强。
第 5 题:List 队列(LPUSH / RPOP / BLPOP)
题目:用 list 实现一个任务队列——入队、查全部、出队、查长度、阻塞等待。再说"LPUSH+RPOP"和"LPUSH+LPOP"分别是什么数据结构。
提示:LPUSH 头入 / RPUSH 尾入 / LPOP 头出 / RPOP 尾出 / LRANGE 0 -1 全取 / BLPOP k 秒 阻塞等待。FIFO 队列用 RPUSH+LPOP。
参考答案
127.0.0.1:6379> LPUSH tasks "发邮件" "导报表" # 左(头)入队
(integer) 2 # 注意:多个值会依次左插,顺序反转
127.0.0.1:6379> RPUSH tasks "对账" # 右(尾)入队
(integer) 3
127.0.0.1:6379> LRANGE tasks 0 -1 # 取全部
1) "导报表"
2) "发邮件"
3) "对账"
127.0.0.1:6379> LPOP tasks # 头部出
"导报表"
127.0.0.1:6379> RPOP tasks # 尾部出
"对账"
127.0.0.1:6379> LLEN tasks
(integer) 1
127.0.0.1:6379> BLPOP delayed 30 # 阻塞 30 秒等数据
# 经典用法:
# FIFO 队列:RPUSH 入 + LPOP 出(先进先出)
# 栈:LPUSH + LPOP(后进先出)
# 简单消息队列:RPUSH + BLPOP(消费者阻塞等待)第 6 题:Set 集合运算(SINTER / SUNION / SDIFF)
题目:给两个 set(two 篇文章的标签),练习——加成员、查全部、判成员、计数、交集(共同标签)、并集、差集。再想一个"共同好友"该怎么实现。
提示:SADD / SMEMBERS / SISMEMBER / SCARD / SINTER / SUNION / SDIFF;共同好友 = 两人各自 set 的 SINTER。
参考答案
127.0.0.1:6379> SADD tags:post1 "MongoDB" "Redis" "MySQL"
(integer) 3
127.0.0.1:6379> SADD tags:post2 "Redis" "PostgreSQL"
(integer) 2
127.0.0.1:6379> SMEMBERS tags:post1 # 取全部成员
1) "MongoDB"
2) "Redis"
3) "MySQL"
127.0.0.1:6379> SISMEMBER tags:post1 "Redis" # 是否成员
(integer) 1
127.0.0.1:6379> SCARD tags:post1 # 成员数
(integer) 3
127.0.0.1:6379> SINTER tags:post1 tags:post2 # 交集
1) "Redis"
127.0.0.1:6379> SUNION tags:post1 tags:post2 # 并集
1) "MongoDB"
2) "Redis"
3) "MySQL"
4) "PostgreSQL"
127.0.0.1:6379> SDIFF tags:post1 tags:post2 # 差集(post1 有 post2 无)
1) "MongoDB"
2) "MySQL"
# 用途:标签、点赞用户、去重、共同好友(SINTER)、随机抽样(SRANDMEMBER)第 7 题:ZSet 排行榜(ZADD / ZRANGE / ZINCRBY)
题目:用 zset 实现游戏积分排行榜——加成员(带分数)、更新分数、按分数降序取全部、加指定分、查排名、按分数区间查。再说 zset 还能做延时队列(score 存什么)。
提示:ZADD k score member / ZRANGE k 0 -1 REV WITHSCORES / ZINCRBY / ZREVRANK / ZRANGEBYSCORE;延时队列 score 存执行时间戳,定期查到期的取出来执行。
参考答案
127.0.0.1:6379> ZADD leaderboard 100 "小明" 85 "小红" 95 "小刚"
(integer) 3
127.0.0.1:6379> ZADD leaderboard 120 "小明" # 已存在则更新分数
(integer) 0 # 返回 0 表示新增 0 个(只是更新)
127.0.0.1:6379> ZRANGE leaderboard 0 -1 REV WITHSCORES # 降序全取
1) "小明"
2) "120"
3) "小刚"
4) "95"
5) "小红"
6) "85"
127.0.0.1:6379> ZINCRBY leaderboard 5 "小红" # 加分
"90"
127.0.0.1:6379> ZREVRANK leaderboard "小明" # 降序排名(从 0)
(integer) 0
127.0.0.1:6379> ZRANGEBYSCORE leaderboard 90 120 # 按分数区间
1) "小红"
2) "小刚"
3) "小明"
# ZSet = 带 score 的 set,自动按 score 排序
# 用途:排行榜、热度排序、延时队列(score=执行时间戳,定期 ZRANGEBYSCORE 取到期任务)
# 注:Redis 7+ 支持 ZRANGE ... REV 选项;旧版用 ZREVRANGE / ZREVRANGEBYSCORE高级(5 题):事务 / 发布订阅 / SCAN / 持久化 / 分布式锁
这一组把 Redis 推向生产级——并发控制、消息系统、安全运维、数据可靠、经典面试题。
第 8 题:事务 MULTI / EXEC / WATCH
题目:开一个事务,放两条命令进队列,EXEC 一次性执行。再用 WATCH 实现乐观锁——监视一个 key,EXEC 前若被改过则事务取消。解释 Redis 事务为什么没有回滚。
提示:MULTI 开事务,后续命令 QUEUED 入队,EXEC 一次性执行,DISCARD 取消;WATCH k 监视,EXEC 时若 k 被改过返回 nil;复杂原子操作用 Lua(EVAL)。
参考答案
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET k1 "v1"
QUEUED # 入队,未执行
127.0.0.1:6379(TX)> INCR counter
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK # 一次性顺序执行所有命令,返回每条结果
2) (integer) 1
127.0.0.1:6379> DISCARD # 取消事务(放弃队列)—— 另开事务时演示
# WATCH:乐观锁,监视某 key;EXEC 前该 key 被其他连接改过,整个事务取消
127.0.0.1:6379> WATCH counter
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR counter
QUEUED
127.0.0.1:6379(TX)> EXEC
(nil) # 期间 counter 被改,返回 nil,事务未执行
# Redis 事务没有回滚(命令语法错会进队列但 EXEC 时报错;运行时错继续执行后续)
# 真要"原子复杂操作",用 Lua 脚本(EVAL)更合适第 9 题:发布订阅 PUBLISH / SUBSCRIBE
题目:开两个 redis-cli 终端——一个 SUBSCRIBE news 订阅,另一个 PUBLISH news "Hello" 发布,观察订阅端收到的消息。再练习按模式订阅 PSUBSCRIBE。说明 Pub/Sub 的可靠性缺点和替代方案。
提示:SUBSCRIBE / PUBLISH / PSUBSCRIBE;Pub/Sub 是即发即弃,离线丢消息;需要可靠消息用 Redis Stream(XADD / XREAD)。
参考答案
# 终端 A:订阅频道(阻塞等待消息)
127.0.0.1:6379> SUBSCRIBE news
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "news"
3) (integer) 1 # 订阅成功
# 终端 B:发布消息
127.0.0.1:6379> PUBLISH news "Hello"
(integer) 1 # 返回收到消息的订阅者数
# 终端 A 立刻收到:
1) "message"
2) "news"
3) "Hello"
# 按模式订阅(通配符)
127.0.0.1:6379> PSUBSCRIBE news.*
# 注意:Pub/Sub 是"即发即弃",订阅者离线就丢消息;
# 需要可靠消息(持久化、消费确认、回溯)请用 Redis Stream:
127.0.0.1:6379> XADD stream1 '*' msg "first" # 生产
127.0.0.1:6379> XREAD COUNT 5 STREAMS stream1 0 # 消费第 10 题:SCAN 安全遍历 key(替代 KEYS)
题目:说明为什么生产环境严禁 KEYS *。用 SCAN 游标式遍历所有以 user: 开头的 key,讲解游标返回值的含义,以及大集合遍历的专用命令。
提示:KEYS 阻塞 Redis 单线程,大库会卡服务;SCAN cursor MATCH ... COUNT n 非阻塞分批,返回"下一游标 + 本次 key";游标为 0 表示遍历完。专用:HSCAN / SSCAN / ZSCAN。
参考答案
# 危险!KEYS * 会阻塞 Redis 单线程,生产环境严禁
127.0.0.1:6379> KEYS user:* # 数据量大时会让服务卡顿几秒
# SCAN 是游标式遍历,非阻塞,生产可用
127.0.0.1:6379> SCAN 0 MATCH user:* COUNT 10
1) "168" # 下一页游标(下次 SCAN 用它);"0" 表示遍历完
2) 1) "user:1"
2) "user:2"
3) "user:3"
127.0.0.1:6379> SCAN 168 MATCH user:* COUNT 10
1) "0"
2) 1) "user:4"
2) "user:5"
# Python/Node 客户端通常封装成迭代器:
# for key in r.scan_iter(match="user:*", count=100): ...
# 大集合的专用 SCAN:
# HSCAN hash(遍历 hash 字段)
# SSCAN set(遍历 set 成员)
# ZSCAN zset(遍历 zset 成员)
# COUNT 只是"建议",实际返回数可能更多或更少;遍历期间数据可能变化,结果不保证唯一第 11 题:持久化 RDB / AOF
题目:练习手动触发 RDB(SAVE / BGSAVE)、查看 AOF 配置(CONFIG GET appendonly)、解释 RDB 和 AOF 的区别、说明三种 appendfsync 策略的取舍,并推荐生产配置。
提示:RDB 全量快照(紧凑、恢复快,宕机丢数据多);AOF 命令日志(更耐丢,文件大);appendfsync:always 最安全慢 / everysec 折中(默认)/ no 最快;混合持久化(4+)兼顾两者。
参考答案
# === RDB:全量内存快照,二进制紧凑,恢复快 ===
127.0.0.1:6379> SAVE # 同步阻塞保存(生产慎用,会卡服务)
OK
127.0.0.1:6379> BGSAVE # 后台 fork 子进程保存(推荐)
Background saving started
# 配置文件触发条件:
# save 900 1 # 900 秒内有 ≥ 1 个改动 → 自动 BGSAVE
# save 300 10
# save 60 10000
# === AOF:记录所有写命令到日志,更耐丢失 ===
127.0.0.1:6379> CONFIG GET appendonly
1) "appendonly"
2) "no"
# 配置 appendonly yes 开启;刷盘策略 appendfsync:
# always 每条命令都刷盘(最安全,最慢)
# everysec 每秒刷盘(默认推荐,宕机最多丢 1 秒)
# no 让 OS 决定(最快,可能丢几十秒)
# AOF 文件会越来越大,需定期 BGREWRITEAOF 重写压缩
# === Redis 4+ 混合持久化(推荐):AOF 文件前段 RDB 二进制 + 后续增量命令 ===
127.0.0.1:6379> CONFIG GET aof-use-rdb-preamble
1) "aof-use-rdb-preamble"
2) "yes"
# 选择建议:仅缓存 → RDB 即可;重要数据 → 开 AOF(everysec)+ 混合持久化第 12 题:分布式锁(SET NX EX + Lua)
题目:这是 Redis 最经典的高频面试题。用 SET k v NX EX 实现分布式锁的加锁;写一段 Lua 脚本实现安全释放(先验证 token 再删,防止误删别人的锁)。解释为什么释放要 Lua、为什么要设过期、生产环境该用什么。
提示;SET k token NX EX 10 加锁;token 必须唯一(防误删);释放用 Lua(GET 比较 + DEL 原子);EX 防死锁;生产用 Redisson(自动续期 + Redlock)。
参考答案
# 经典场景:分布式锁。SET 的 NX(不存在才设)+ EX(过期)是原子操作
127.0.0.1:6379> SET lock:order:100 "client-1-token" NX EX 10
OK # 拿到锁
127.0.0.1:6379> SET lock:order:100 "client-2-token" NX EX 10
(nil) # client-2 抢锁失败(锁已被占用)
# 业务处理完后,释放锁 —— ⚠️ 必须验证 token,防误删别人的锁
# (设想:client-1 因 GC 暂停,锁过期,client-2 拿到锁;client-1 醒来直接 DEL,
# 把 client-2 的锁删了。)用 Lua 脚本保证"判断+删除"原子:
127.0.0.1:6379> EVAL 'if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end' 1 lock:order:100 "client-1-token"
(integer) 1 # 删除成功
# 关键点:
# 1. token 用唯一值(UUID),防误删
# 2. EX 设合理过期:既防持有者宕机死锁,也要够业务跑完
# 3. 释放用 Lua,保证"GET 比较 + DEL"原子
# 4. 生产环境用 Redisson(自动续期、Redlock 算法),别自己造轮子练习建议
真敲一遍:Redis 命令的反馈是即时的(OK / integer / nil),亲手敲过印象才深。把答案复制到 redis-cli 跑,改改参数看返回变化。
试着扩展:做完第 5 题,能不能用 BRPOP 实现一个"多消费者轮询"的任务队列?做完第 7 题,能不能实现"实时刷新 Top 10 排行榜并去重"?做完第 12 题,能不能思考"锁过期但业务没做完"该怎么办(看门狗续期)?每个扩展都是一次实战。
场景驱动:Redis 的关键不是"背命令",而是"什么场景用哪种数据结构"。缓存 → string,对象 → hash,队列 → list,标签/共同好友 → set,排行榜 → zset。把这个对应表刻在脑子里,看到需求就知道选型。
← 上一篇 Redis 实战场景
返回 Redis 教程目录