面向 Java/Go 后端 3-5 年社招及高阶岗位,默认版本 Redis 7.x。
〇、Redis 命令大全(按数据结构分类)
0.1 通用命令(Key 维度)
# 增删查改
SET key value # 写入(覆盖)
GET key
DEL key [key ...]
UNLINK key [key ...] # 异步删除,大 key 必用
EXISTS key [key ...] # 返回存在数量
TYPE key # string/list/hash/set/zset/stream
# 过期
EXPIRE key seconds [NX|XX|GT|LT] # 7.0 加修饰
PEXPIRE key milliseconds
EXPIREAT key unix-ts
TTL key # -1 永久 / -2 不存在
PTTL key
PERSIST key # 移除过期
# 键扫描(生产严禁 KEYS *,必用 SCAN)
KEYS pattern # ⚠️ 阻塞,禁用
SCAN cursor [MATCH pat] [COUNT n] [TYPE t] # 游标式,非阻塞
DBSIZE
RANDOMKEY
# 重命名 / 移动
RENAME key newkey
RENAMENX key newkey # 仅当 newkey 不存在
COPY src dst [DB n] [REPLACE]
MOVE key db # 跨 DB
# 序列化
DUMP key
RESTORE key ttl serialized-value [REPLACE]
# 元信息
OBJECT ENCODING key # 看底层编码(embstr/raw/ziplist/...)
OBJECT IDLETIME key # 空闲秒数
OBJECT FREQ key # LFU 频次(需 maxmemory-policy=allkeys-lfu)
DEBUG SLEEP 5 # 测试用,阻塞
0.2 String
# 基本
SET k v [EX sec | PX ms | EXAT ts | PXAT ts] [NX|XX] [KEEPTTL] [GET]
GET k
GETSET k v # 已废弃,用 SET ... GET
SETNX k v # 等价 SET k v NX
SETEX k sec v # 等价 SET k v EX sec
MSET k1 v1 k2 v2 ...
MGET k1 k2 ...
MSETNX k1 v1 k2 v2 # 全部 NX,原子
# 数值
INCR k / DECR k # +1 / -1
INCRBY k n / DECRBY k n
INCRBYFLOAT k 1.5
# 字符串/位
APPEND k v
STRLEN k
GETRANGE k start end
SETRANGE k offset v
# 7.0+ 一次设置过期与值
SET k v EXAT 1735660800
# 位操作(见 Bitmap 节)
0.3 List(双端链表 / quicklist)
LPUSH k v [v ...] # 头插
RPUSH k v [v ...] # 尾插
LPOP k [count] # 头弹(6.2+ 支持 count)
RPOP k [count]
LRANGE k start stop # 0 -1 = 全部
LINDEX k i
LLEN k
LINSERT k BEFORE|AFTER pivot v
LSET k i v
LREM k count v # count>0 从头删,<0 从尾删,=0 全删
LTRIM k start stop # 保留区间,列表裁剪
# 阻塞版(消息队列原始形态)
BLPOP k [k...] timeout # 阻塞等待,0 = 永久
BRPOP k [k...] timeout
BRPOPLPUSH src dst timeout # 已废弃 → BLMOVE
BLMOVE src dst LEFT|RIGHT LEFT|RIGHT timeout
LMPOP numkeys k... LEFT|RIGHT [COUNT n] # 7.0+
0.4 Hash
HSET k f v [f v ...] # 4.0+ 支持多字段
HGET k f
HMSET k f v ... # 已废弃,用 HSET
HMGET k f1 f2 ...
HGETALL k # ⚠️ 大 hash 慎用
HDEL k f [f ...]
HEXISTS k f
HKEYS k / HVALS k
HLEN k
HSTRLEN k f
HINCRBY k f n
HINCRBYFLOAT k f 1.5
HSCAN k cursor [MATCH] [COUNT] # 大 hash 必用
HRANDFIELD k [count [WITHVALUES]] # 6.2+
0.5 Set
SADD k v [v ...]
SREM k v [v ...]
SMEMBERS k # ⚠️ 大 set 慎用
SISMEMBER k v
SMISMEMBER k v [v ...] # 6.2+ 批量
SCARD k # 元素个数
SRANDMEMBER k [count] # 随机抽样
SPOP k [count] # 随机弹出
SMOVE src dst v
SSCAN k cursor [MATCH] [COUNT]
# 集合运算
SINTER k1 k2 ... # 交集
SUNION k1 k2 ... # 并集
SDIFF k1 k2 ... # 差集
SINTERSTORE dst k1 k2 ... # 结果存到 dst(避免大结果集传输)
SUNIONSTORE / SDIFFSTORE
SINTERCARD numkeys k1 k2 [LIMIT n] # 7.0+ 只返回交集大小
0.6 ZSet(有序集合 / 跳表 + 字典)
ZADD k [NX|XX|GT|LT] [CH] [INCR] score member [score member ...]
ZREM k m [m ...]
ZSCORE k m
ZMSCORE k m [m ...]
ZINCRBY k inc m
ZCARD k
ZCOUNT k min max # 分数范围内的数量
ZRANK k m [WITHSCORE] # 升序排名(0 起)
ZREVRANK k m
# 范围查询(7.0 推荐统一用 ZRANGE)
ZRANGE k start stop [BYSCORE|BYLEX] [REV] [LIMIT off cnt] [WITHSCORES]
ZRANGEBYSCORE k min max [WITHSCORES] [LIMIT off cnt] # 老命令
ZRANGEBYLEX k min max # 字典序
ZREVRANGE k start stop # 老命令
ZRANGESTORE dst src ... # 7.0 结果存储
# 删除
ZREMRANGEBYRANK k start stop
ZREMRANGEBYSCORE k min max
ZREMRANGEBYLEX k min max
# 阻塞 / 弹出
ZPOPMIN k [count] / ZPOPMAX k [count]
BZPOPMIN k [k...] timeout / BZPOPMAX
ZMPOP numkeys k... MIN|MAX [COUNT n] # 7.0+
# 集合运算
ZUNIONSTORE dst n k1 k2 [WEIGHTS] [AGGREGATE SUM|MIN|MAX]
ZINTERSTORE dst n k1 k2 ...
ZDIFFSTORE dst n k1 k2 ...
ZUNION / ZINTER / ZDIFF # 6.2+ 不存储版
0.7 Bitmap(建立在 String 之上)
SETBIT k offset 0|1
GETBIT k offset
BITCOUNT k [start end [BYTE|BIT]]
BITPOS k 0|1 [start [end [BYTE|BIT]]]
BITOP AND|OR|XOR|NOT dst k1 [k2 ...]
BITCOUNT user:sign:202504 0 -1 # 月签到天数
BITFIELD k GET|SET|INCRBY type offset [val] # 子整型操作
典型场景:签到、活跃用户统计、布隆过滤器底层。
0.8 HyperLogLog(基数估算,0.81% 误差,固定 12KB)
PFADD k v [v ...]
PFCOUNT k [k ...] # 多 key 求并集基数
PFMERGE dst src [src ...]
场景:UV 统计,亿级用户用 12KB 一个 key。
0.9 Geo(基于 ZSet 实现)
GEOADD k [NX|XX|CH] lon lat member [...]
GEOPOS k m [m ...]
GEODIST k m1 m2 [m|km|ft|mi]
GEOSEARCH k FROMMEMBER m | FROMLONLAT lon lat
BYRADIUS r unit | BYBOX w h unit
[ASC|DESC] [COUNT n [ANY]] [WITHCOORD] [WITHDIST] [WITHHASH]
GEOSEARCHSTORE dst src ... # 6.2+
场景:附近的人 / 商家。
0.10 Stream(消息队列,5.0+)
# 写入
XADD key [NOMKSTREAM] [MAXLEN|MINID [~|=] threshold] * field value [...]
XADD orders * user 1 amount 99 # * 自动生成 ID
# 读
XLEN key
XRANGE key start end [COUNT n] # - + 表示最小/最大
XREVRANGE key end start [COUNT n]
XREAD [COUNT n] [BLOCK ms] STREAMS k1 k2 id1 id2
# 消费组(核心)
XGROUP CREATE key group $ [MKSTREAM] # $ 表示从最新开始
XREADGROUP GROUP g consumer [COUNT n] [BLOCK ms] [NOACK]
STREAMS key > / id # > 拉新消息
XACK key group id [id ...]
XPENDING key group [...] # 未 ACK 的消息
XCLAIM key group consumer min-idle id [...] # 转移消费者
XAUTOCLAIM key group consumer min-idle start [COUNT n]
# 修剪
XTRIM key MAXLEN|MINID [~|=] threshold
XDEL key id [...]
XINFO STREAM/GROUPS/CONSUMERS key
0.11 Pub/Sub(发布订阅,无持久化)
SUBSCRIBE ch [ch ...]
UNSUBSCRIBE [ch ...]
PSUBSCRIBE pattern [...] # 模式订阅
PUBLISH ch msg
PUBSUB CHANNELS [pattern]
PUBSUB NUMSUB [ch ...]
PUBSUB NUMPAT
Pub/Sub 不持久化、订阅者断开就丢消息。可靠消息用 Stream。
0.12 事务(MULTI / EXEC)
MULTI
SET k1 v1
INCR counter
EXEC # 一次性原子执行
DISCARD # 放弃事务
WATCH k [k ...] # 乐观锁,被改则 EXEC 返回 nil
UNWATCH
0.13 Lua 脚本
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
SCRIPT LOAD "..."
SCRIPT EXISTS sha1 [...]
SCRIPT FLUSH
0.14 服务器 / 运维
INFO [section] # server/clients/memory/persistence/stats/replication/cpu/cluster/keyspace
CONFIG GET pattern
CONFIG SET param value
CONFIG REWRITE # 写回 redis.conf
CLIENT LIST
CLIENT KILL ID <id> | ADDR ip:port
CLIENT NO-EVICT ON|OFF # 7.0+ 防止此连接被驱逐
CLIENT PAUSE ms
DBSIZE
FLUSHDB [ASYNC|SYNC] # 清当前库
FLUSHALL [ASYNC|SYNC]
SELECT db # 选库(集群模式不支持)
SAVE # ⚠️ 同步阻塞
BGSAVE
BGREWRITEAOF
LASTSAVE
DEBUG SEGFAULT # 别用
SLOWLOG GET [n] / RESET / LEN
LATENCY DOCTOR / LATEST / HISTORY event
MEMORY USAGE key [SAMPLES n]
MEMORY STATS
MEMORY DOCTOR
0.15 集群 / 复制
CLUSTER INFO
CLUSTER NODES
CLUSTER SLOTS / CLUSTER SHARDS # 7.0+ 推荐 SHARDS
CLUSTER KEYSLOT key
CLUSTER COUNTKEYSINSLOT slot
CLUSTER GETKEYSINSLOT slot count
CLUSTER MEET ip port
CLUSTER FORGET node-id
CLUSTER FAILOVER [FORCE|TAKEOVER]
REPLICAOF host port / REPLICAOF NO ONE # 5.0+,老命令 SLAVEOF
ROLE
WAIT numreplicas timeout # 等待写入复制到 N 个从
一、Redis 数据结构与底层编码
1.1 五大基础结构 + 编码选择
Redis 每种数据类型至少有两种底层编码,根据数据规模自动切换。
| 类型 | 小数据编码 | 大数据编码 | 切换阈值(默认) |
|---|---|---|---|
| String | int / embstr | raw | embstr ≤ 44 字节 |
| List | listpack(7.0)/ ziplist | quicklist | listpack 元素 ≤128、值 ≤64 字节 |
| Hash | listpack | hashtable | 字段 ≤128、值 ≤64 字节 |
| Set | intset / listpack | hashtable | 全整数走 intset |
| ZSet | listpack | skiplist + dict | 元素 ≤128、值 ≤64 字节 |
7.0 用
listpack取代了ziplist,避免连锁更新。
OBJECT ENCODING key 可以看实际编码。
1.2 SDS(Simple Dynamic String)
Redis 自己的字符串实现,不用 C 字符串。
struct sdshdr {
int len; // 已用长度
int alloc; // 总分配
char flags; // 类型标识
char buf[]; // 数据
};
优势:
- O(1) 取长度。
- 二进制安全(可存
\0)。 - 预分配 + 惰性释放,减少 realloc。
- 杜绝缓冲区溢出(先检查 alloc)。
1.3 跳表(Skip List)
ZSet 的核心结构。多层链表,上层稀疏作为索引。
- 平均 O(log N),最坏 O(N)。
- 比红黑树:实现简单、范围查询友好(底层就是链表)、不需要旋转。
- 比 B 树:内存友好(每节点更小),但磁盘场景输给 B+ 树。
1.4 quicklist(List 默认结构)
双向链表,每个节点是一段 listpack。
- 兼顾链表(增删头尾 O(1))和压缩(连续内存)。
list-max-listpack-size:每段最大字节数(负值表示按 KB)。list-compress-depth:除头尾几段外其它段压缩存储。
1.5 listpack(替代 ziplist)
紧凑的连续内存数组,每个元素自带长度。比 ziplist 改进:
- 每个 entry 不再保存"前一个 entry 长度",避免连锁更新(cascade update)。
- 内存连续,CPU cache 友好,小数据量遍历比 hashtable 快。
1.6 dict(哈希字典)
渐进式 rehash:
- 持有两个 hash 表 ht[0] / ht[1]
- 触发扩容时分配 ht[1],每次操作搬一部分桶
- rehashidx 记录进度
- 完成后 ht[1] 顶替 ht[0]
避免一次性 rehash 长时间阻塞。
二、持久化(RDB / AOF / 混合)
2.1 RDB(快照)
二进制全量快照。触发方式:
SAVE:主线程同步,阻塞,禁用。BGSAVE:fork 子进程做快照,主线程继续服务。- 自动:
save 900 1/save 300 10/save 60 10000(任一条件触发)。
优点:文件小、加载快、适合备份和灾备。 缺点:宕机会丢上一次快照后的数据;fork 在大内存实例上耗时。
fork 写时复制(COW):父子共享物理页,谁写谁复制。所以 RDB 期间内存可能翻倍,写入热的实例尤甚。
2.2 AOF(日志追加)
记录每条写命令。
appendfsync 三种策略:
always:每条命令 fsync,最安全最慢。everysec(默认):每秒 fsync 一次,宕机最多丢 1 秒。no:交给 OS,性能最好但丢数据多。
AOF 重写:日志膨胀后用最少命令表达当前数据。
BGREWRITEAOF手动触发。auto-aof-rewrite-percentage 100+auto-aof-rewrite-min-size 64mb自动触发。- 重写期间新写入进 AOF 重写缓冲区,重写完合并。
2.3 混合持久化(4.0+,推荐)
aof-use-rdb-preamble yes(默认开)。
- AOF 重写时,先把当前数据集以 RDB 格式写到 AOF 头部。
- 之后的新增命令以 AOF 格式追加。
- 加载:先按 RDB 加载头部、再回放 AOF 增量。
兼顾启动速度和数据完整性。
2.4 选型建议
- 缓存场景:可纯 RDB 或不开持久化,重启后从 DB 回填。
- 重要数据:AOF everysec + 混合持久化。
- 主备分离:主开 AOF、从开 RDB 做备份。
三、过期与内存管理
3.1 过期键删除策略
三种结合使用:
- 惰性删除:访问时检查,过期则删并返回 nil。省 CPU、可能浪费内存。
- 定期删除:每 100ms 抽样部分有过期时间的 key,过期则删。
hz控制频率。 - 主动驱逐:内存达
maxmemory时按淘汰策略删。
3.2 maxmemory-policy(八种淘汰策略)
| 策略 | 作用范围 | 算法 |
|---|---|---|
| noeviction(默认) | — | 不淘汰,写入直接报错 |
| allkeys-lru | 所有 key | LRU |
| volatile-lru | 设置过期的 key | LRU |
| allkeys-lfu | 所有 key | LFU(4.0+) |
| volatile-lfu | 设置过期的 key | LFU |
| allkeys-random | 所有 key | 随机 |
| volatile-random | 设置过期的 key | 随机 |
| volatile-ttl | 设置过期的 key | 优先删快过期的 |
LRU vs LFU 选型:
- 访问模式有时序局部性(最近访问的还会被访问) → LRU。
- 存在长期热点(几个 key 一直被访问) → LFU。
Redis 的近似 LRU/LFU:不维护全局链表,每次随机抽 maxmemory-samples(默认 5)个 key,淘汰其中最差的。samples=10 已接近精确 LRU。
3.3 过期对主从复制的影响
主库过期一个 key 后,会显式发 DEL 到从库。从库自己不会主动删过期 key(避免不一致)。所以从库读到过期 key 是有可能的(旧版本),3.2+ 已修复(从库读时也判断 TTL)。
四、单线程模型与多线程演进
4.1 为什么"单线程"还快
- 纯内存操作。
- 命令执行单线程 → 无锁、无上下文切换。
- 多路 IO 复用(epoll/kqueue/io_uring)。
- 高效数据结构。
4.2 真实的"线程模型"
Redis 不是完全单线程:
- 主线程:处理命令、IO 解析、响应。
- 后台线程(bio):异步任务,如
UNLINK大 key 释放、BGSAVE之外的关闭文件、AOF fsync。 - IO 线程(6.0+,默认关闭):处理读写网络数据,命令执行仍单线程。
io-threads 4 + io-threads-do-reads yes 可开启多 IO 线程。仅在带宽是瓶颈时开。
4.3 阻塞场景(高阶必问)
什么会让 Redis 变慢甚至卡死:
- 大 key 操作:
KEYS *、HGETALL大 hash、DEL大 key(用UNLINK)。 - 复杂命令:
SORT、SUNIONSTORE大集合、ZRANGEBYSCORE大范围。 - 持久化 fork:大内存实例 fork 几百毫秒。
- AOF fsync
always:每条命令磁盘 IO。 - swap:内存不够走交换分区,性能崩塌。
- 主从全量同步:fork RDB + 网络传输。
- 过期集中爆发:大量 key 同时到期触发集中删除。
- 网络抖动:连接重建风暴。
五、主从复制
5.1 复制流程
- 建立连接:从库
REPLICAOF host port。 - 数据同步:
- 全量:从库第一次连或断连过久 → 主库
BGSAVE生成 RDB → 网络传输 → 从库加载 → 期间增量进repl_backlog。 - 部分:从库带
replid + offset重连,主库判断 offset 还在 backlog 内 → 只发增量。
- 全量:从库第一次连或断连过久 → 主库
- 命令传播:之后主库每条写命令异步发给从库。
5.2 PSYNC 与复制偏移量
replid:主库的复制 ID(每次启动或角色变化时变)。offset:复制流的字节偏移。repl_backlog_size:环形缓冲区大小(默认 1MB,生产调到 100MB+)。
replid + offset 命中 backlog → 部分同步;否则全量。
5.3 复制延迟
主从异步复制 → 从库可能落后。
INFO replication看master_repl_offset与从库slave_repl_offset差。- 业务上:写后立即读走主、强一致用
WAIT。
5.4 从库只读
replica-read-only yes(默认)。从库写入会报错。
5.5 无盘复制
repl-diskless-sync yes:主库直接把 RDB 流写到 socket,不落盘,对慢盘环境友好。
六、高可用:哨兵(Sentinel)
6.1 角色
哨兵是独立进程,监控主库、自动故障转移。
职责:
- 监控:定期 ping 主从。
- 通知:状态变化通知客户端。
- 故障转移:主库挂了选一个从升主。
- 配置中心:客户端从哨兵拿主库地址。
6.2 主观下线 vs 客观下线
- SDOWN(主观):单个哨兵 ping 不通超过
down-after-milliseconds。 - ODOWN(客观):超过
quorum数量的哨兵都认为下线。
6.3 故障转移流程
- 哨兵检测到 ODOWN。
- 选举一个 leader 哨兵执行 failover(Raft 算法)。
- leader 从从库中选新主:
- 优先级
replica-priority高的。 - 复制 offset 大的(数据最新)。
- run_id 字典序小的。
- 优先级
- 把新主提升、其它从重新
REPLICAOF新主。 - 通知客户端。
6.4 哨兵自身高可用
哨兵至少 3 个节点,quorum >= n/2 + 1,否则选举不出 leader。
七、Redis Cluster(分片集群)
7.1 架构
- 数据分片:16384 个 slot,每个 key 通过 CRC16(key) % 16384 落到某个 slot。
- 节点:每个主节点负责一部分 slot,配若干从。
- 去中心化:节点之间用 Gossip 协议互相感知。
- 至少 6 节点(3 主 3 从)。
7.2 客户端路由
- 客户端发命令到任意节点。
- 节点判断 slot 是否归自己 → 是则执行;否则返回
MOVED ip:port,客户端重定向并缓存映射。 - 迁移中:返回
ASK ip:port,客户端临时跳转,不更新缓存。
7.3 多 key 操作的限制
跨 slot 的 mget / mset / 事务 / Lua 都会报 CROSSSLOT。
Hash Tag 解决:用 {} 强制部分参与 hash。
{user:1000}:profile
{user:1000}:orders
两个 key 必落同一 slot。
7.4 故障转移
主挂了 → 从节点发起选举,过半主同意即升级 → 接管 slot。
cluster-node-timeout 控制判定时间(默认 15s,生产调短)。
7.5 扩容缩容
# 加新节点
redis-cli --cluster add-node new-ip:port any-existing-ip:port
# 重新分片
redis-cli --cluster reshard ip:port
# 删节点(先迁走 slot)
redis-cli --cluster del-node ip:port node-id
迁移过程中:源节点 MIGRATING、目标节点 IMPORTING,客户端按 ASK 重定向。
7.6 Cluster vs Sentinel vs Codis
| 维度 | Sentinel | Cluster | Codis |
|---|---|---|---|
| 分片 | 不支持 | 16384 slot | 1024 slot(Proxy) |
| 高可用 | 是 | 是 | 是(依赖 ZK) |
| 客户端 | 普通 | 需支持 cluster 协议 | 普通(透明) |
| 多 key | 支持 | 同 slot 才支持 | 不支持 |
| 运维 | 简单 | 中 | 重 |
生产首选:Cluster(官方)。Codis 多用于历史项目。
八、事务与 Lua
8.1 Redis 事务(弱事务)
MULTI → 命令入队 → EXEC 一次性顺序执行。
特点:
- 不保证回滚。语法错误整个事务不执行;运行时错误(类型不对)只该条失败,其它继续。
- 隔离性:执行期间不会被其它命令插入(单线程)。
- WATCH 乐观锁:监控的 key 在 EXEC 前被改,事务整体不执行(返回 nil)。
WATCH balance
val = GET balance
MULTI
SET balance new_val
EXEC # 如果别人改了 balance,返回 nil
8.2 Lua 脚本(强原子)
执行期间不被打断(注意:会阻塞主线程)。
-- 原子扣减库存
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
redis.call('DECR', KEYS[1])
return 1
else
return 0
end
调用:
EVAL "..." 1 stock:1001
注意:
- 脚本要短小(默认 5 秒超时,超过
SCRIPT KILL不一定有效)。 - 集群下脚本访问的所有 key 必须在同一 slot(用 hash tag)。
- 用
EVALSHA复用,节省网络。
8.3 函数(Functions,7.0+)
FUNCTION LOAD 把 Lua 函数持久化到服务端,FCALL 调用。比 EVAL 更适合常驻业务。
九、应用场景
9.1 缓存
- Cache Aside 旁路缓存(最常用)。
- Read/Write Through、Write Behind(少用)。
- 三连击防护:穿透 / 击穿 / 雪崩(详见 13.x)。
9.2 分布式锁
最简:
SET lock:order:1 token NX EX 30
# 释放(必须用 Lua 保证原子)
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else return 0 end
要点:
NX:互斥。EX:避免持锁宕机死锁。value用唯一 token:避免误删别人的锁。- 释放用 Lua:判断 + 删除原子。
- 看门狗:业务可能跑超时,用 Redisson 自动续期。
RedLock(多实例锁,Martin Kleppmann 与作者 antirez 有过著名争论):
- 5 个独立 Redis 节点,向 N/2+1 申请,多数成功才算获锁。
- 工程代价大,多数场景不需要,主从异步丢锁的概率工程上可接受。
9.3 计数器 / 限流
# 计数
INCR article:1001:view
# 滑动窗口限流(ZSet)
ZADD limiter:user:1 1700000000 1700000000
ZREMRANGEBYSCORE limiter:user:1 0 (now-60000)
ZCARD limiter:user:1 # > 阈值则拒绝
# 令牌桶(Lua + INCR)
# 漏桶算法
9.4 排行榜
ZSet 天然支持。
ZADD rank:202504 100 user1 200 user2
ZREVRANGE rank:202504 0 9 WITHSCORES # Top10
ZREVRANK rank:202504 user1 # 我的名次
9.5 延时队列
ZSet:score = 执行时间戳。
ZADD delay_queue 1700000000 task1
# 消费者轮询
ZRANGEBYSCORE delay_queue 0 now LIMIT 0 10
或 Stream + 消费组实现可靠队列;专业场景用 RocketMQ/Pulsar 延时队列。
9.6 会话存储 / Token
SET session:xxx data EX 1800,访问时刷新 TTL。
9.7 布隆过滤器
防穿透。RedisBloom 模块(BF.ADD/BF.EXISTS)或自己用 Bitmap 实现。
BF.RESERVE bf:user 0.001 1000000
BF.ADD bf:user 1001
BF.EXISTS bf:user 1001
误判率:可能"在"实际不在;不会"不在"实际在。
9.8 地理位置
GEOADD + GEOSEARCH,附近的人/商家。
9.9 消息队列
- 简单:List
LPUSH/BRPOP。 - 发布订阅:Pub/Sub(不可靠)。
- 可靠:Stream + 消费组(持久化、ACK、重试)。
十、客户端 SDK 速查
10.1 Java:Jedis
阻塞 IO,每个连接一线程,JedisPool 管理。简单直接。
JedisPoolConfig cfg = new JedisPoolConfig();
cfg.setMaxTotal(200);
cfg.setMaxIdle(50);
cfg.setMinIdle(10);
cfg.setTestOnBorrow(true);
JedisPool pool = new JedisPool(cfg, "127.0.0.1", 6379, 2000, "pwd");
try (Jedis jedis = pool.getResource()) {
jedis.set("k", "v");
String v = jedis.get("k");
jedis.setex("k", 60, "v");
// pipeline 批量
Pipeline p = jedis.pipelined();
for (int i=0;i<1000;i++) p.set("k"+i, "v");
p.sync();
// 事务
Transaction tx = jedis.multi();
tx.set("a","1"); tx.incr("a");
tx.exec();
// Lua
Object r = jedis.eval(script, 1, "k1", "arg1");
}
集群:
Set<HostAndPort> nodes = Set.of(new HostAndPort("h1",7000), ...);
JedisCluster jc = new JedisCluster(nodes, 2000, 2000, 5, "pwd", cfg);
jc.set("k","v");
10.2 Java:Lettuce
Netty 异步、线程安全连接(一个连接给所有线程用)。Spring Boot 默认。
RedisClient client = RedisClient.create("redis://pwd@127.0.0.1:6379/0");
StatefulRedisConnection<String,String> conn = client.connect();
RedisCommands<String,String> sync = conn.sync();
RedisAsyncCommands<String,String> async = conn.async();
RedisReactiveCommands<String,String> reactive = conn.reactive();
sync.set("k","v");
async.get("k").thenAccept(System.out::println);
// 集群
RedisClusterClient clusterClient = RedisClusterClient.create(
List.of(RedisURI.create("h1",7000), RedisURI.create("h2",7000)));
StatefulRedisClusterConnection<String,String> cc = clusterClient.connect();
Spring Boot:
spring.data.redis.host: 127.0.0.1
spring.data.redis.lettuce.pool.max-active: 200
@Resource StringRedisTemplate redis;
redis.opsForValue().set("k","v", Duration.ofSeconds(60));
redis.opsForHash().put("h","f","v");
redis.opsForZSet().add("z","m", 1.0);
10.3 Java:Redisson
封装高级特性:分布式锁(带看门狗)、限流、布隆、延时队列、RxJava/Reactive。
Config cfg = new Config();
cfg.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("pwd");
RedissonClient r = Redisson.create(cfg);
// 分布式锁(自动续期 30 秒)
RLock lock = r.getLock("order:1");
lock.lock(10, TimeUnit.SECONDS); // 显式过期
try { ... } finally { lock.unlock(); }
// 公平锁、读写锁、信号量、CountDownLatch
RReadWriteLock rwLock = r.getReadWriteLock("rw");
RSemaphore sem = r.getSemaphore("sem");
// 限流
RRateLimiter rl = r.getRateLimiter("rl");
rl.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.SECONDS);
rl.tryAcquire();
// 布隆
RBloomFilter<Long> bf = r.getBloomFilter("bf");
bf.tryInit(1_000_000, 0.001);
bf.add(1001L);
// 集合直接当 Java 集合用
RMap<String,String> map = r.getMap("m");
map.put("k","v");
10.4 Go:go-redis(推荐)
import "github.com/redis/go-redis/v9"
rdb := redis.NewClient(&redis.Options{
Addr: "127.0.0.1:6379",
Password: "pwd",
DB: 0,
PoolSize: 200,
MinIdleConns: 10,
})
ctx := context.Background()
rdb.Set(ctx, "k", "v", time.Minute)
val, err := rdb.Get(ctx, "k").Result()
if err == redis.Nil { /* not exist */ }
// pipeline
pipe := rdb.Pipeline()
for i:=0;i<1000;i++ { pipe.Set(ctx, fmt.Sprintf("k%d", i), "v", 0) }
pipe.Exec(ctx)
// 事务
rdb.Watch(ctx, func(tx *redis.Tx) error {
n, _ := tx.Get(ctx, "balance").Int()
if n < 100 { return errors.New("insufficient") }
_, err := tx.TxPipelined(ctx, func(p redis.Pipeliner) error {
p.DecrBy(ctx, "balance", 100)
return nil
})
return err
}, "balance")
// Lua
script := redis.NewScript(`
if tonumber(redis.call("GET", KEYS[1])) > 0 then
return redis.call("DECR", KEYS[1])
end
return -1
`)
script.Run(ctx, rdb, []string{"stock:1"})
// 集群
cc := redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{"h1:7000","h2:7000"},
})
10.5 Go:redsync(分布式锁)
import "github.com/go-redsync/redsync/v4"
pool := goredis.NewPool(rdb)
rs := redsync.New(pool)
mutex := rs.NewMutex("lock:order:1",
redsync.WithExpiry(30*time.Second),
redsync.WithTries(3))
if err := mutex.Lock(); err == nil {
defer mutex.Unlock()
// ...
}
10.6 客户端选型
| 场景 | Java 选 | Go 选 |
|---|---|---|
| 简单/老项目 | Jedis | go-redis |
| Spring Boot | Lettuce + RedisTemplate | go-redis |
| 需要分布式锁/限流/布隆 | Redisson | redsync + go-redis |
| 响应式编程 | Lettuce Reactive | go-redis(context 自带) |
十一、Redis 在 Spring Boot 中的常用姿势
11.1 RedisTemplate vs StringRedisTemplate
StringRedisTemplate:key/value 都是 String,序列化简单。默认推荐。RedisTemplate<Object, Object>:要手动配序列化器,否则用 JDK 序列化(不可读)。
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory cf) {
RedisTemplate<String, Object> t = new RedisTemplate<>();
t.setConnectionFactory(cf);
t.setKeySerializer(new StringRedisSerializer());
t.setValueSerializer(new GenericJackson2JsonRedisSerializer());
t.setHashKeySerializer(new StringRedisSerializer());
t.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return t;
}
11.2 Spring Cache 注解
@Cacheable(value="user", key="#id", unless="#result == null")
public User get(Long id) { ... }
@CachePut(value="user", key="#u.id")
public User update(User u) { ... }
@CacheEvict(value="user", key="#id")
public void delete(Long id) { ... }
配合 RedisCacheManager。注意:
@Cacheable默认不缓存 null(防穿透要unless="#result==null"反向逻辑或自己实现)。- 不支持手动指定 TTL 不同 key 不同 → 用
RedisCacheConfiguration按 cacheName 配。
11.3 自定义注解 + AOP
复杂场景常自实现:双删、防击穿(互斥)、防穿透(空值缓存)。
十二、高阶补强:性能与排查决策树
12.1 慢查询排查
CONFIG SET slowlog-log-slower-than 10000 # 微秒,10ms
CONFIG SET slowlog-max-len 1024
SLOWLOG GET 10
SLOWLOG RESET
# 7.0+ commandstats
INFO commandstats
典型慢命令:
KEYS *、HGETALL大 hash、SMEMBERS大 set、LRANGE 0 -1→ 改 SCAN/分页。DEL大 key →UNLINK。SORT、SUNIONSTORE在大集合上 → 业务侧改造。
12.2 大 key 排查
# 在线上 SCAN 模式扫描
redis-cli --bigkeys
redis-cli --memkeys # 6.0+
# 单独看
MEMORY USAGE key SAMPLES 0
DEBUG OBJECT key # 老命令
# 离线分析 RDB
rdb -c memory dump.rdb > out.csv # rdb-tools
大 key 危害:
- 操作慢(读写、删除、迁移)。
- 内存倾斜(集群分布不均)。
- 持久化时 fork 内存翻倍风险。
处理:
- 拆分(hash/list 切片,按业务维度分桶)。
- 删除用
UNLINK。 - 设计上避免无界增长(加 TTL、定期裁剪)。
12.3 热 key 排查与处理
# 4.0+
redis-cli --hotkeys # 需 maxmemory-policy=*-lfu
MONITOR # ⚠️ 性能影响大,不要长跑
# 客户端侧统计:开 trace、Sentinel/Hystrix 的 key 维度埋点
处理:
- 本地缓存:Caffeine/HashMap 兜底,TTL 短(几秒)。
- 多副本:
hot:1→hot:1:{0..9},读时随机选一份。 - 客户端缓存(6.0+ Tracking):服务端通知失效。
- 限流降级:到不了 Redis 的请求直接拒。
12.4 内存碎片
INFO memory 看 mem_fragmentation_ratio。
-
1.5:碎片严重。
- < 1:用了 swap,已经性能崩了。
处理:
- 4.0+
activedefrag yes开启自动整理。 - 重启实例(最有效但有抖动)。
- jemalloc 是默认分配器,不建议换。
12.5 阻塞排查
LATENCY DOCTOR # 总体诊断
LATENCY LATEST
LATENCY HISTORY event-name
LATENCY GRAPH event-name
INFO clients # blocked_clients
INFO commandstats # 哪个命令耗时多
Latency 事件:fork、aof-fsync、expire-cycle、eviction-cycle、command 等。
12.6 主从同步异常
INFO replication
# master_link_status:up?
# master_last_io_seconds_ago
# master_sync_in_progress
# repl_backlog_size / repl_backlog_histlen
全量同步频繁:backlog 太小 → 调大 repl_backlog_size(推荐 100MB+)。
网络断连:检查 repl-timeout、tcp-keepalive。
12.7 集群问题
- slot 迁移卡住:
CLUSTER SETSLOT卡,先看CLUSTER NODES状态,必要时FIX。 - 节点失联但没切:
cluster-node-timeout设太大;过小则误判。 - 键分布倾斜:用 hash tag 时一定要小心,热 tag 会把一个节点打爆。
12.8 监控关键指标
| 类别 | 指标 |
|---|---|
| 容量 | used_memory、used_memory_rss、mem_fragmentation_ratio、maxmemory |
| 性能 | ops/sec、commandstats、slowlog 数量、latency events |
| 连接 | connected_clients、blocked_clients、rejected_connections |
| 持久化 | rdb_last_bgsave_status、aof_last_rewrite_time、aof_pending_bio_fsync |
| 复制 | master_link_status、master_repl_offset、slave_lag |
| 集群 | cluster_state、cluster_slots_assigned、cluster_slots_ok |
十三、高阶补强:缓存设计专题
13.1 缓存穿透
查不存在的数据,每次绕过 cache 打 DB。
方案
- 缓存空值(短 TTL,比如 60 秒)。
- 布隆过滤器:所有合法 ID 预热进 BF,请求先过 BF。
- 接口层参数校验 + 风控。
13.2 缓存击穿
热点 key 突然过期,瞬间大量请求打 DB。
方案
- 互斥锁重建:cache miss 时
SETNX抢锁,抢到的查 DB 回填,其它等。 - 逻辑过期:value 内带 expire_time,永不物理过期;过期则异步刷新。
- 永不过期 + 预热刷新。
13.3 缓存雪崩
大量 key 同时过期,或 Redis 宕机。
方案
- TTL 加随机扰动(
expire ± rand(300s))。 - 多级缓存(本地 Caffeine + Redis)。
- 限流熔断(Sentinel/Hystrix),保护 DB。
- 高可用(Cluster + 多副本)。
- 提前预热。
13.4 一致性方案对比
| 方案 | 一致性 | 性能 | 实现 |
|---|---|---|---|
| 先 DB 后删 cache(Cache Aside) | 最终 | 高 | 简单 |
| 先 DB 后更新 cache | 最终(弱) | 中 | 简单但易乱序 |
| 延迟双删 | 最终(强一些) | 中 | 中 |
| Canal 订阅 binlog 删 cache | 最终 | 高 | 中(需 Canal) |
| 分布式锁强同步 | 强 | 低 | 复杂 |
首选:Cache Aside(先 DB 后删 cache)+ Canal 兜底。
为什么不是"先删 cache 后写 DB":删 cache 后写 DB 前,另一线程读 DB 旧值回填 cache → 长期脏数据。
延迟双删:先删 → 写 DB → sleep(覆盖主从延迟)→ 再删。
13.5 客户端缓存(Tracking,6.0+)
服务端记录"哪个客户端缓存了哪个 key",key 变更时主动通知客户端失效。
CLIENT TRACKING ON
适合读多写少的强一致缓存。Lettuce/Redisson 都支持。
十四、高阶补强:分布式锁与一致性
14.1 单实例锁的正确姿势
SET lock:res:1 unique-token NX PX 30000
# 释放(Lua 原子)
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
四要素:
- NX:互斥。
- PX/EX:超时兜底。
- 唯一 token:避免误删别人锁(自己超时后被别人拿走,自己又 DEL)。
- Lua 释放:判断 + 删除原子化。
14.2 锁续期(看门狗)
业务可能跑超时,手动估计时间难。Redisson 的看门狗:
lock()不传 timeout,默认 30 秒锁,每 10 秒续期一次。- 客户端宕机停止续期,30 秒后自动释放。
自实现要起守护协程定时 EXPIRE。
14.3 RedLock 争议
5 个独立 Redis,向 N/2+1 申请锁,多数成功才算。
Martin 的批评:
- GC pause 等导致客户端晚执行,锁已被别人持,仍以为自己有锁 → 用 fencing token 兜底。
- 时钟漂移破坏 TTL 假设。
作者的回应:工程足够。
实务:
- 普通业务:单 Redis + 主从异步丢锁概率可接受(极小)。
- 金融强一致:用 ZK / etcd(Raft 强一致,性能低于 Redis)。
- 应用层 fencing token + 业务幂等 兜底所有锁失效场景。
14.4 锁选型
| 方案 | 一致性 | 性能 | 复杂度 |
|---|---|---|---|
| Redis 单实例 + Lua | 最终 | 极高 | 低 |
| Redisson(推荐) | 最终(带看门狗) | 高 | 低 |
| RedLock | 略强 | 高 | 中 |
| ZK 临时节点 | 强 | 低 | 中 |
| etcd lease | 强 | 低 | 中 |
| 数据库唯一键 | 强 | 低 | 极低 |
14.5 锁的常见错误
- 没设过期 → 持锁宕机死锁。
- value 不唯一 → 误删。
- 释放不用 Lua → 判断和删之间被打断。
- 业务超过锁 TTL → 锁过期被别人拿走,自己仍以为持锁。
- 主从切换丢锁 → 主写完 → 主挂 → 从无锁数据 → 别人拿到锁。
十五、高阶补强:架构 Case Study
15.1 Case 1:秒杀系统
核心问题:超卖、雪崩、刷子。
链路:
- 限流:网关层限 IP/UID,Sentinel。
- 预扣库存:Redis Lua 原子扣减;扣成功才进下游。
- MQ 削峰:扣成功消息进 Kafka,下游异步落 DB 创单。
- 防超卖:DB 用乐观锁
UPDATE stock SET n=n-1 WHERE id=? AND n>0。 - 防刷:BloomFilter 黑名单、风控前置。
- 数据回写:兜底脚本对账 Redis 与 DB。
Redis 关键技术:
- 库存 key 预热到本地 + Redis。
- Lua 保证扣减原子。
- 用 hash tag 把同 SKU 路由到同节点。
15.2 Case 2:Feed 流
两种模型
- 推(写扩散):发布时把内容 ID 推到所有粉丝的 timeline ZSet。读快,写慢,名人不友好。
- 拉(读扩散):读时合并所有关注用户的 ZSet。写快,读慢。
- 推拉结合:普通用户推、大 V 拉,登录时合并。
ZSet:score = 时间戳,member = 内容 ID。ZREVRANGE 取最新 N 条 + 业务侧 MGET 内容详情。
15.3 Case 3:缓存雪崩复盘
事故:promotion 大促预热脚本一次性给 10 万 SKU 设了相同 TTL=1 小时。整点全部失效,DB QPS 瞬间 100x,全站 502。
根因
- 批量同 TTL → 集中失效。
- 没有互斥重建保护。
- DB 没限流。
整改
- TTL 加随机:
base + rand(0, 600s)。 - 全部走
getCache():cache miss 走 SETNX 互斥重建。 - 双 cache:本地 30 秒 + Redis 1 小时。
- DB 加 Sentinel 流控。
- 监控:大批量 SET 同 TTL 报警。
15.4 Case 4:大 key 引发主从延迟
现象:从库延迟突增到 5 分钟,主库 CPU 也飙。
定位:redis-cli --bigkeys 找到一个 500MB 的 hash(消息中心累积未清理)。每次写入触发主从大流量同步。
处理
UNLINK拆掉,按用户 ID 哈希到 1024 个小 hash。- 加定期清理:每条消息带 TTL,业务侧异步删除。
- 监控:单 key > 10MB 报警。
十六、面试高频问答骨架
16.1 必背组合题
Redis 为什么快? 内存 + 单线程无锁 + 多路 IO 复用 + 高效数据结构(SDS/跳表/listpack/hashtable)+ 渐进 rehash。
Redis 单线程,6.0 多线程是怎么回事?
命令执行始终单线程。6.0 引入 IO 多线程(io-threads)只处理网络读写解析;执行还是主线程串行。
ZSet 为什么用跳表不用红黑树? 范围查询友好(底层链表)、实现简单(无需旋转)、内存可控。
RDB 和 AOF 怎么选? 重要数据 AOF + 混合持久化。 缓存场景可纯 RDB 或不开。
主从复制全量 vs 部分? 首次/断连过久 → 全量(BGSAVE + 增量缓冲)。 重连且 offset 在 backlog 内 → 部分。
集群为什么是 16384 个 slot? 作者解释:心跳包带 slot bitmap,16384 bit = 2KB 可接受;集群规模设计上限 1000 节点,每节点平均 16 个 slot 够用。
Redis 分布式锁的全部坑
- 没 NX → 不互斥
- 没 EX → 死锁
- value 不唯一 → 误删
- 释放不原子 → 误删
- 业务超 TTL → 锁失效仍执行
- 主从异步丢锁 → 用 RedLock 或 fencing token
缓存与 DB 一致性怎么保证? Cache Aside(先 DB 后删 cache) + 延迟双删 + Canal 订阅 binlog 兜底。强一致用分布式锁但性能差,多数业务最终一致即可。
热 key 怎么处理? 本地缓存 + 多副本(key 加随机后缀)+ 客户端 Tracking + 限流降级。
大 key 怎么处理? 拆分(按业务维度分桶哈希) + UNLINK 删除 + TTL 控制 + 监控告警。
Redis 持久化时为什么会变慢? fork 阻塞主线程几十 ms 到几百 ms(写时复制 + 拷贝页表)。 AOF fsync 抖动。 解决:错峰、调小实例(< 10GB)、SSD、关 THP。
16.2 真实场景题
- 给你 1 亿 UV 怎么统计?→ HyperLogLog(12KB 一个 key,误差 0.81%)。
- 千万级用户签到 + 月活统计?→ Bitmap(每用户每天 1 bit,月 31 bit)。
- 微博热搜实时排行?→ ZSet。
- 附近 1km 的商家?→ Geo / GEOSEARCH。
- 延时 30 分钟的订单关闭?→ ZSet score=ts 或 Stream + 消费组 + 定时扫描,专业用 RocketMQ。
- 100 万人在线长连接的消息推送?→ Pub/Sub 不可靠,要用 Stream + 消费组,或 MQ。
16.3 反向提问
- 你们 Redis 部署形态?Cluster 还是哨兵?规模多大?
- 缓存一致性怎么保证?有没有 Canal?
- 大 key/热 key 治理流程?怎么发现?
- Redis 监控关键指标看哪些?
- 出过哪些线上事故?怎么复盘的?
附录:Redis 数据结构与命令对照表
| 业务诉求 | 选 | 关键命令 |
|---|---|---|
| 缓存对象 | String / Hash | SET/GET / HSET/HGET |
| 计数器 | String | INCR/INCRBY |
| 限流 | String / ZSet | INCR + EXPIRE / ZADD + ZRANGEBYSCORE |
| 队列 | List / Stream | LPUSH+BRPOP / XADD+XREADGROUP |
| 排行榜 | ZSet | ZADD/ZREVRANGE/ZREVRANK |
| 去重 / 共同好友 | Set | SADD/SINTER |
| 标签 / 用户画像 | Set / Hash | SADD / HSET |
| 签到 | Bitmap | SETBIT/BITCOUNT |
| UV 估算 | HyperLogLog | PFADD/PFCOUNT |
| 附近的人 | Geo | GEOADD/GEOSEARCH |
| 分布式锁 | String | SET NX EX + Lua |
| 延时任务 | ZSet / Stream | ZADD score=ts / XADD |
| 发布订阅 | Pub/Sub / Stream | SUBSCRIBE / XREAD |