Redis 后端面试知识点梳理(中高级 + 高阶补强)

3 阅读27分钟

面向 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 每种数据类型至少有两种底层编码,根据数据规模自动切换。

类型小数据编码大数据编码切换阈值(默认)
Stringint / embstrrawembstr ≤ 44 字节
Listlistpack(7.0)/ ziplistquicklistlistpack 元素 ≤128、值 ≤64 字节
Hashlistpackhashtable字段 ≤128、值 ≤64 字节
Setintset / listpackhashtable全整数走 intset
ZSetlistpackskiplist + 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 过期键删除策略

三种结合使用:

  1. 惰性删除:访问时检查,过期则删并返回 nil。省 CPU、可能浪费内存。
  2. 定期删除:每 100ms 抽样部分有过期时间的 key,过期则删。hz 控制频率。
  3. 主动驱逐:内存达 maxmemory 时按淘汰策略删。

3.2 maxmemory-policy(八种淘汰策略)

策略作用范围算法
noeviction(默认)不淘汰,写入直接报错
allkeys-lru所有 keyLRU
volatile-lru设置过期的 keyLRU
allkeys-lfu所有 keyLFU(4.0+)
volatile-lfu设置过期的 keyLFU
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 变慢甚至卡死:

  1. 大 key 操作KEYS *HGETALL 大 hash、DEL 大 key(用 UNLINK)。
  2. 复杂命令SORTSUNIONSTORE 大集合、ZRANGEBYSCORE 大范围。
  3. 持久化 fork:大内存实例 fork 几百毫秒。
  4. AOF fsync always:每条命令磁盘 IO。
  5. swap:内存不够走交换分区,性能崩塌。
  6. 主从全量同步:fork RDB + 网络传输。
  7. 过期集中爆发:大量 key 同时到期触发集中删除。
  8. 网络抖动:连接重建风暴。

五、主从复制

5.1 复制流程

  1. 建立连接:从库 REPLICAOF host port
  2. 数据同步
    • 全量:从库第一次连或断连过久 → 主库 BGSAVE 生成 RDB → 网络传输 → 从库加载 → 期间增量进 repl_backlog
    • 部分:从库带 replid + offset 重连,主库判断 offset 还在 backlog 内 → 只发增量。
  3. 命令传播:之后主库每条写命令异步发给从库。

5.2 PSYNC 与复制偏移量

  • replid:主库的复制 ID(每次启动或角色变化时变)。
  • offset:复制流的字节偏移。
  • repl_backlog_size:环形缓冲区大小(默认 1MB,生产调到 100MB+)。

replid + offset 命中 backlog → 部分同步;否则全量。

5.3 复制延迟

主从异步复制 → 从库可能落后。

  • INFO replicationmaster_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 故障转移流程

  1. 哨兵检测到 ODOWN。
  2. 选举一个 leader 哨兵执行 failover(Raft 算法)。
  3. leader 从从库中选新主:
    • 优先级 replica-priority 高的。
    • 复制 offset 大的(数据最新)。
    • run_id 字典序小的。
  4. 把新主提升、其它从重新 REPLICAOF 新主。
  5. 通知客户端。

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

维度SentinelClusterCodis
分片不支持16384 slot1024 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 选
简单/老项目Jedisgo-redis
Spring BootLettuce + RedisTemplatego-redis
需要分布式锁/限流/布隆Redissonredsync + go-redis
响应式编程Lettuce Reactivego-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
  • SORTSUNIONSTORE 在大集合上 → 业务侧改造。

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:1hot:1:{0..9},读时随机选一份。
  • 客户端缓存(6.0+ Tracking):服务端通知失效。
  • 限流降级:到不了 Redis 的请求直接拒。

12.4 内存碎片

INFO memorymem_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-timeouttcp-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。

方案

  1. 缓存空值(短 TTL,比如 60 秒)。
  2. 布隆过滤器:所有合法 ID 预热进 BF,请求先过 BF。
  3. 接口层参数校验 + 风控。

13.2 缓存击穿

热点 key 突然过期,瞬间大量请求打 DB。

方案

  1. 互斥锁重建:cache miss 时 SETNX 抢锁,抢到的查 DB 回填,其它等。
  2. 逻辑过期:value 内带 expire_time,永不物理过期;过期则异步刷新。
  3. 永不过期 + 预热刷新。

13.3 缓存雪崩

大量 key 同时过期,或 Redis 宕机。

方案

  1. TTL 加随机扰动(expire ± rand(300s))。
  2. 多级缓存(本地 Caffeine + Redis)。
  3. 限流熔断(Sentinel/Hystrix),保护 DB。
  4. 高可用(Cluster + 多副本)。
  5. 提前预热。

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

四要素:

  1. NX:互斥。
  2. PX/EX:超时兜底。
  3. 唯一 token:避免误删别人锁(自己超时后被别人拿走,自己又 DEL)。
  4. 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 锁的常见错误

  1. 没设过期 → 持锁宕机死锁。
  2. value 不唯一 → 误删。
  3. 释放不用 Lua → 判断和删之间被打断。
  4. 业务超过锁 TTL → 锁过期被别人拿走,自己仍以为持锁。
  5. 主从切换丢锁 → 主写完 → 主挂 → 从无锁数据 → 别人拿到锁。

十五、高阶补强:架构 Case Study

15.1 Case 1:秒杀系统

核心问题:超卖、雪崩、刷子。

链路

  1. 限流:网关层限 IP/UID,Sentinel。
  2. 预扣库存:Redis Lua 原子扣减;扣成功才进下游。
  3. MQ 削峰:扣成功消息进 Kafka,下游异步落 DB 创单。
  4. 防超卖:DB 用乐观锁 UPDATE stock SET n=n-1 WHERE id=? AND n>0
  5. 防刷:BloomFilter 黑名单、风控前置。
  6. 数据回写:兜底脚本对账 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 没限流。

整改

  1. TTL 加随机:base + rand(0, 600s)
  2. 全部走 getCache():cache miss 走 SETNX 互斥重建。
  3. 双 cache:本地 30 秒 + Redis 1 小时。
  4. DB 加 Sentinel 流控。
  5. 监控:大批量 SET 同 TTL 报警。

15.4 Case 4:大 key 引发主从延迟

现象:从库延迟突增到 5 分钟,主库 CPU 也飙。

定位redis-cli --bigkeys 找到一个 500MB 的 hash(消息中心累积未清理)。每次写入触发主从大流量同步。

处理

  1. UNLINK 拆掉,按用户 ID 哈希到 1024 个小 hash。
  2. 加定期清理:每条消息带 TTL,业务侧异步删除。
  3. 监控:单 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 / HashSET/GET / HSET/HGET
计数器StringINCR/INCRBY
限流String / ZSetINCR + EXPIRE / ZADD + ZRANGEBYSCORE
队列List / StreamLPUSH+BRPOP / XADD+XREADGROUP
排行榜ZSetZADD/ZREVRANGE/ZREVRANK
去重 / 共同好友SetSADD/SINTER
标签 / 用户画像Set / HashSADD / HSET
签到BitmapSETBIT/BITCOUNT
UV 估算HyperLogLogPFADD/PFCOUNT
附近的人GeoGEOADD/GEOSEARCH
分布式锁StringSET NX EX + Lua
延时任务ZSet / StreamZADD score=ts / XADD
发布订阅Pub/Sub / StreamSUBSCRIBE / XREAD