中间件 - 缓存 - 架构师面试题库

侧重Redis核心原理、缓存架构设计、一致性方案、高可用集群、性能优化,考察候选人在缓存领域的实战深度。


一、Redis核心原理(1-25题)

1. 🔵 Redis为什么这么快?单线程模型是如何实现高性能的?

答:Redis的高性能来源于多个因素的叠加:

  1. 纯内存操作:数据存储在内存中,读写速度是磁盘的10万倍以上。
  2. 单线程模型:避免了多线程的锁竞争、上下文切换开销。单线程顺序执行命令,没有并发问题。
  3. IO多路复用:使用epoll/kqueue实现非阻塞IO,单线程可以同时处理数万个客户端连接。
  4. 高效的数据结构:SDS(Simple Dynamic String)、ziplist、quicklist、skiplist、intset等,针对不同场景优化。
  5. 单线程避免CPU缓存失效:数据在同一个CPU核心的缓存中,命中率高。

单线程的含义(Redis 6.0+):

  • 命令执行是单线程的(保证原子性)。
  • 网络IO可以是多线程的(io-threads配置,默认关闭)。Redis 6.0+支持多线程IO,网络读写由多个IO线程处理,命令执行仍然是单线程。
  • 后台任务是多线程的:持久化(RDB/AOF)、惰性删除(lazyfree)、集群通信等由后台线程处理。

性能数据:单实例Redis可以达到10万+ QPS(简单命令),实际生产中通常在5-8万QPS。

2. 🔴 Redis的数据结构底层实现是怎样的?String、List、Hash、Set、ZSet分别用了什么编码?

答:Redis的每种数据类型都有多种底层编码,根据数据量和元素大小自动切换。

String:

  • int:值是整数且在long范围内,直接存储整数值(不分配SDS)。
  • embstr:字符串长度≤44字节,SDS和RedisObject在一块连续内存中分配(一次内存分配,缓存友好)。
  • raw:字符串长度>44字节,SDS和RedisObject分开分配(两次内存分配)。

List:

  • listpack(Redis 7.0+,替代ziplist):元素少且小时使用。连续内存,紧凑存储。
  • quicklist:元素多时使用。双向链表,每个节点是一个listpack(压缩列表)。兼顾了链表的灵活性和紧凑存储的内存效率。

Hash:

  • listpack:field数量≤hash-max-listpack-entries(默认128)且每个field/value长度≤hash-max-listpack-value(默认64字节)。
  • hashtable:超过阈值后转为哈希表。

Set:

  • intset:所有元素都是整数且数量≤set-max-intset-entries(默认512)。有序整数数组,二分查找。
  • listpack(Redis 7.2+):非整数元素但数量少时使用。
  • hashtable:超过阈值后转为哈希表。

ZSet(Sorted Set):

  • listpack:元素数量≤zset-max-listpack-entries(默认128)且每个元素长度≤zset-max-listpack-value(默认64字节)。
  • skiplist + hashtable:超过阈值后使用跳表(按score排序,支持范围查询)+ 哈希表(按member查找score,O(1))。

3. 🔵 什么是Redis的SDS(Simple Dynamic String)?它和C语言的char*有什么区别?

答:SDS是Redis自己实现的字符串类型,解决了C字符串的多个问题。

SDS结构(Redis 3.2+有多种类型:sdshdr5/8/16/32/64):

1
2
3
4
5
6
struct sdshdr8 {
uint8_t len; // 已使用长度
uint8_t alloc; // 分配的总长度(不含头部和\0)
unsigned char flags; // 类型标识(sdshdr5/8/16/32/64)
char buf[]; // 实际数据(柔性数组)
};

与C字符串的区别:

特性 C字符串(char*) SDS
获取长度 O(n)遍历到\0 O(1)直接读len
缓冲区溢出 不检查,可能溢出 自动扩容,安全
修改时内存分配 每次修改都可能重新分配 预分配+惰性释放
二进制安全 不安全(\0截断) 安全(按len读取)
兼容C函数 是(buf末尾有\0)

内存预分配策略:

  • 修改后len < 1MB:分配2*len+1字节(翻倍)。
  • 修改后len >= 1MB:分配len+1MB+1字节(每次多分配1MB)。
  • 减少频繁修改时的内存重新分配次数。

惰性空间释放:缩短字符串时不立即释放多余空间(alloc不变),后续追加时可以直接使用。通过sdsRemoveFreeSpace()手动释放。

4. 🔴 Redis的跳表(Skip List)是如何实现的?为什么ZSet用跳表而不用红黑树?

答:跳表是ZSet的核心数据结构,支持O(log n)的查找、插入、删除和范围查询。

跳表结构:

  • 多层有序链表。最底层包含所有元素,每上一层是下一层的”快速通道”。
  • 每个节点随机决定层数(概率p=0.25,最大层数32)。
  • 查找:从最高层开始,向右查找直到下一个节点大于目标值,然后下降一层继续查找。平均时间复杂度O(log n)。

Redis跳表的实现细节:

1
2
3
4
5
6
7
8
9
typedef struct zskiplistNode {
sds ele; // 成员(member)
double score; // 分值
struct zskiplistNode *backward; // 后退指针(只有第一层有)
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned long span; // 跨度(用于计算排名)
} level[]; // 层级数组
} zskiplistNode;

为什么用跳表而不用红黑树:

  1. 范围查询高效:ZRANGEBYSCORE等范围操作,跳表找到起始节点后沿链表顺序遍历即可。红黑树需要中序遍历,实现复杂。
  2. 实现简单:跳表的插入删除比红黑树简单得多(不需要旋转和变色)。Redis作者antirez明确表示选择跳表是因为实现简单。
  3. 内存局部性:跳表的节点在内存中可以更紧凑(虽然不如数组,但比红黑树的指针跳转好)。
  4. 并发友好:跳表更容易实现无锁并发(虽然Redis是单线程,但这是跳表的通用优势)。
  5. 排名计算:Redis跳表的span字段可以O(log n)计算元素排名(ZRANK),红黑树需要额外维护子树大小。

代价:跳表的空间开销比红黑树大(每个节点平均1.33个指针,红黑树是2个指针但节点更紧凑)。

5. 🔵 Redis的内存淘汰策略有哪些?LRU和LFU的区别是什么?

答:当Redis内存达到maxmemory限制时,根据淘汰策略决定如何处理新写入。

淘汰策略(maxmemory-policy):

  1. noeviction(默认):不淘汰,写入操作返回错误(OOM)。读操作正常。
  2. allkeys-lru:从所有key中淘汰最近最少使用的key。最常用。
  3. volatile-lru:只从设置了过期时间的key中淘汰LRU key。
  4. allkeys-lfu(Redis 4.0+):从所有key中淘汰最不经常使用的key。
  5. volatile-lfu:只从设置了过期时间的key中淘汰LFU key。
  6. allkeys-random:从所有key中随机淘汰。
  7. volatile-random:从设置了过期时间的key中随机淘汰。
  8. volatile-ttl:淘汰剩余TTL最短的key。

Redis的近似LRU(非精确LRU):

  • 精确LRU需要维护全局链表,内存和CPU开销大。
  • Redis采用采样淘汰:随机采样maxmemory-samples个key(默认5),淘汰其中最久未访问的。
  • 每个key用24bit记录最后访问时间(lru字段),精度约为10秒。
  • 采样数越大越接近精确LRU,但CPU开销越大。

LRU vs LFU:

  • LRU(Least Recently Used):淘汰最久未访问的key。问题:偶尔被访问一次的冷数据不会被淘汰(如全表扫描导致大量冷数据被访问一次)。
  • LFU(Least Frequently Used):淘汰访问频率最低的key。Redis的LFU使用Morris计数器(概率计数,8bit存储频率)+ 衰减机制(lfu-decay-time,默认1分钟衰减一半)。更适合区分热数据和冷数据。

生产建议:大多数场景用allkeys-lru。如果有明显的热点数据,用allkeys-lfu更优。

6. 🔴 Redis的持久化机制有哪些?RDB和AOF的原理和区别是什么?

答:Redis提供RDB和AOF两种持久化机制,以及混合持久化。

RDB(Redis Database):

  • 原理:将内存中的数据快照保存为二进制文件(dump.rdb)。
  • 触发方式:
    • 手动:SAVE(阻塞主线程)、BGSAVE(fork子进程,不阻塞)。
    • 自动:配置save规则(如save 900 1,900秒内至少1次修改则触发BGSAVE)。
  • fork + Copy-on-Write:BGSAVE时fork子进程,子进程共享父进程的内存页。父进程继续处理请求,修改数据时触发COW(只复制被修改的内存页)。
  • 优点:文件紧凑、恢复速度快、对性能影响小(子进程处理)。
  • 缺点:可能丢失最后一次快照后的数据(分钟级数据丢失)。fork大内存实例时可能短暂阻塞。

AOF(Append Only File):

  • 原理:将每个写命令追加到AOF文件末尾。恢复时重放所有命令。
  • 写入策略(appendfsync):
    • always:每个命令都fsync到磁盘。最安全但最慢。
    • everysec(默认):每秒fsync一次。最多丢失1秒数据。推荐。
    • no:由OS决定何时fsync。最快但可能丢失较多数据。
  • AOF重写(BGREWRITEAOF):AOF文件过大时,fork子进程重写AOF(只保留最终状态的命令,去除冗余)。重写期间新命令写入AOF缓冲区,重写完成后追加到新AOF文件。
  • 优点:数据安全性高(最多丢失1秒)。
  • 缺点:文件比RDB大、恢复速度比RDB慢。

混合持久化(Redis 4.0+,aof-use-rdb-preamble=yes):

  • AOF重写时,先写入RDB格式的快照,再追加增量的AOF命令。
  • 恢复时先加载RDB部分(快),再重放AOF部分(少量命令)。
  • 兼顾了RDB的恢复速度和AOF的数据安全性。推荐使用。

7. 🔵 Redis的过期键删除策略是怎样的?惰性删除和定期删除是如何配合的?

答:Redis使用惰性删除 + 定期删除两种策略配合处理过期键。

惰性删除(Lazy Expiration):

  • 访问key时检查是否过期,过期则删除并返回nil。
  • 优点:不浪费CPU(只在访问时检查)。
  • 缺点:如果过期key一直不被访问,永远不会被删除,浪费内存。

定期删除(Active Expiration):

  • Redis每100ms执行一次(hz配置,默认10,即每秒10次):
    1. 从设置了过期时间的key中随机采样20个。
    2. 删除其中已过期的key。
    3. 如果过期key比例>25%,重复步骤1(说明过期key很多,需要继续清理)。
    4. 每次执行时间不超过25ms(避免阻塞主线程太久)。
  • 优点:主动清理过期key,避免内存浪费。
  • 缺点:可能有延迟(过期key不会立即被删除)。

内存淘汰兜底:

  • 如果惰性删除和定期删除都没有及时清理过期key,当内存达到maxmemory时,内存淘汰策略会清理key(包括已过期但未被删除的key)。

注意事项:

  1. 大量key同时过期可能导致Redis短暂卡顿(定期删除集中执行)。建议给过期时间加随机偏移(如TTL + random(0, 300)秒)。
  2. 从库不主动删除过期key(等待主库的DEL命令同步)。Redis 3.2之前从库可能返回已过期的key,3.2+从库会检查过期时间返回nil(但不删除)。

8. 🔴 Redis的主从复制原理是什么?全量复制和增量复制的流程是怎样的?

答:Redis主从复制实现数据冗余和读写分离。

全量复制(首次同步或复制积压缓冲区不足时):

  1. 从库发送PSYNC ? -1(首次)或PSYNC runid offset(断线重连)。
  2. 主库判断需要全量复制,返回FULLRESYNC runid offset。
  3. 主库执行BGSAVE生成RDB快照,同时将新的写命令缓存到复制缓冲区(replication buffer)。
  4. 主库将RDB文件发送给从库。
  5. 从库清空旧数据,加载RDB文件。
  6. 主库将复制缓冲区中的命令发送给从库执行。
  7. 同步完成,进入增量复制阶段。

增量复制(正常运行时):

  • 主库执行写命令后,将命令传播给所有从库。
  • 从库执行收到的命令,保持与主库数据一致。

断线重连的增量复制(PSYNC 2.0,Redis 4.0+):

  • 主库维护复制积压缓冲区(repl-backlog-size,默认1MB),环形缓冲区存储最近的写命令。
  • 从库断线重连时发送PSYNC runid offset。
  • 如果offset在积压缓冲区范围内,主库发送缺失的命令(增量复制)。
  • 如果offset不在范围内(断线太久),触发全量复制。

PSYNC 2.0改进:

  • 支持从库重启后的增量复制(从库将runid和offset持久化到RDB文件)。
  • 支持主库故障转移后的增量复制(从库记录主库和自己的replid,新主库可以识别)。

关键配置:

  • repl-backlog-size:积压缓冲区大小。建议根据写入速率和可能的断线时间计算:size = 写入速率(MB/s) × 断线时间(s) × 2。
  • repl-diskless-sync:无盘复制。主库直接将RDB通过Socket发送给从库,不写入磁盘。适合磁盘慢但网络快的场景。

9. 🔵 Redis Sentinel(哨兵)的工作原理是什么?如何实现自动故障转移?

答:Redis Sentinel是Redis的高可用方案,监控主从节点,自动故障转移。

Sentinel架构:

  • 多个Sentinel节点(建议3个或5个,奇数个)组成Sentinel集群。
  • Sentinel监控主库和从库的健康状态。
  • 主库故障时自动选举新主库并通知客户端。

故障检测:

  1. 主观下线(SDOWN):单个Sentinel认为节点不可用(超过down-after-milliseconds未响应PING)。
  2. 客观下线(ODOWN):多个Sentinel(≥quorum个)都认为主库不可用。只有主库才有客观下线。

故障转移流程:

  1. Sentinel集群通过Raft协议选举一个Leader Sentinel执行故障转移。
  2. Leader Sentinel从从库中选择新主库:
    • 排除不健康的从库(断线、响应慢)。
    • 优先级最高的(replica-priority最小的)。
    • 复制偏移量最大的(数据最新的)。
    • runid最小的(兜底)。
  3. Leader Sentinel向选中的从库发送SLAVEOF NO ONE,使其成为新主库。
  4. 向其他从库发送SLAVEOF new-master,让它们复制新主库。
  5. 将旧主库标记为从库(旧主库恢复后自动成为新主库的从库)。
  6. 通过Pub/Sub通知客户端主库地址变更。

客户端连接:

  • 客户端连接Sentinel获取主库地址(SENTINEL get-master-addr-by-name)。
  • 客户端订阅Sentinel的+switch-master频道,感知主库切换。
  • Jedis/Lettuce等客户端库内置Sentinel支持,自动处理故障转移。

10. 🔴 Redis Cluster的原理是什么?数据分片和节点通信是如何实现的?

答:Redis Cluster是Redis的分布式方案,支持数据分片和高可用。

数据分片:

  • 哈希槽(Hash Slot):共16384个槽(0-16383),每个key通过CRC16(key) % 16384映射到一个槽。
  • 每个主节点负责一部分槽。如3个主节点:Node1(0-5460)、Node2(5461-10922)、Node3(10923-16383)。
  • Hash Tag:key中用{}包裹的部分作为哈希计算的输入。如{user:1}.name和{user:1}.age映射到同一个槽,可以在同一个节点上执行多key操作。

节点通信(Gossip协议):

  • 每个节点维护集群状态(节点列表、槽分配、节点状态)。
  • 节点间通过Gossip协议交换信息:每秒随机选择几个节点发送PING消息,携带自己知道的部分节点信息。
  • 消息类型:PING/PONG(心跳和信息交换)、MEET(新节点加入)、FAIL(节点故障通知)。
  • 最终一致:通过Gossip协议,所有节点最终会收敛到一致的集群状态。

请求路由:

  • 客户端发送命令到任意节点。
  • 如果key在该节点负责的槽中,直接处理。
  • 如果不在,返回MOVED重定向:MOVED 3999 127.0.0.1:6381,客户端重新发送到正确节点。
  • 智能客户端(如Jedis Cluster、Lettuce)缓存槽→节点的映射,直接发送到正确节点,避免重定向。

故障转移:

  • 节点间通过PING/PONG检测故障。超过cluster-node-timeout未响应则标记为PFAIL(疑似故障)。
  • 多数主节点都标记某节点为PFAIL时,标记为FAIL(确认故障)。
  • 故障主节点的从节点自动发起选举(需要多数主节点投票),成为新主节点接管槽。

11. 🔵 Redis Cluster的槽迁移是如何实现的?迁移过程中如何保证数据一致性?

答:槽迁移用于集群扩缩容时重新分配槽。

迁移流程:

  1. 目标节点设置槽状态为IMPORTING:CLUSTER SETSLOT <slot> IMPORTING <source-node-id>
  2. 源节点设置槽状态为MIGRATING:CLUSTER SETSLOT <slot> MIGRATING <target-node-id>
  3. 逐个迁移key:
    • CLUSTER GETKEYSINSLOT <slot> <count>获取源节点该槽的key列表。
    • MIGRATE <target-ip> <target-port> <key> 0 <timeout>将key从源节点迁移到目标节点(原子操作:源节点发送key到目标节点,目标节点确认后源节点删除key)。
  4. 所有key迁移完成后,通知所有节点更新槽分配:CLUSTER SETSLOT <slot> NODE <target-node-id>

迁移过程中的请求处理:

  • 客户端请求源节点:
    • key还在源节点:正常处理。
    • key已迁移到目标节点:返回ASK重定向(ASK <slot> <target-ip>:<target-port>)。客户端先发送ASKING命令到目标节点,再发送实际命令。
  • ASK vs MOVED:ASK是临时重定向(只对当前请求),MOVED是永久重定向(更新客户端的槽映射缓存)。

注意事项:

  1. 迁移是逐key进行的,大key迁移可能阻塞源节点(MIGRATE是同步操作)。
  2. 迁移过程中不要执行涉及多个槽的命令(如MGET跨槽key)。
  3. 使用redis-cli –cluster reshard自动化迁移过程。
  4. 迁移速度取决于网络带宽和key大小,大规模迁移建议在低峰期执行。

12. 🔴 什么是Redis的大Key问题?如何检测和处理大Key?

答:大Key:单个key的value占用内存过大(如String > 10KB,集合类型元素 > 1万个)。

大Key的危害:

  1. 阻塞主线程:读取/删除大Key耗时长,阻塞其他命令。如DEL一个包含百万元素的Hash可能阻塞数秒。
  2. 网络带宽:大Key的读取占用大量网络带宽,影响其他请求。
  3. 内存不均:Cluster模式下大Key导致节点内存不均衡。
  4. 持久化影响:大Key的序列化/反序列化耗时长,影响RDB和AOF。
  5. 过期删除:大Key过期时的删除操作阻塞主线程。

检测方法:

  1. redis-cli –bigkeys:扫描所有key,找出每种类型最大的key。使用SCAN命令,不阻塞。
  2. redis-cli –memkeys:扫描所有key,按内存占用排序(Redis 4.0+,需要开启memory-usage)。
  3. MEMORY USAGE key:查看单个key的内存占用。
  4. RDB分析工具:redis-rdb-tools解析RDB文件,离线分析所有key的大小。
  5. 监控:通过slowlog发现耗时长的命令,排查是否涉及大Key。

处理方案:

  1. 拆分大Key:将大Hash拆分为多个小Hash(如user:1:basic、user:1:detail)。将大List拆分为多个小List(按时间或ID范围分段)。
  2. 压缩value:对String类型的value进行压缩(gzip/snappy),读取时解压。
  3. 异步删除:使用UNLINK代替DEL(Redis 4.0+),UNLINK在后台线程异步删除。或开启lazyfree-lazy-expire=yes,过期key自动异步删除。
  4. 渐进式删除:对集合类型,使用HSCAN/SSCAN/ZSCAN逐批删除元素,最后删除key。
  5. 避免产生大Key:设计时控制集合大小,超过阈值时分片存储。

13. 🔵 什么是Redis的热Key问题?如何检测和解决?

答:热Key:某个key被大量请求访问,导致该key所在的Redis节点成为瓶颈。

热Key的危害:

  1. 单节点CPU和网络带宽打满。
  2. Cluster模式下负载不均(热Key所在节点过载,其他节点空闲)。
  3. 极端情况下导致节点宕机,引发雪崩。

检测方法:

  1. redis-cli –hotkeys:使用LFU淘汰策略时可用,扫描找出访问频率最高的key。
  2. MONITOR命令:实时监控所有命令(生产环境慎用,性能影响大)。采样分析高频key。
  3. 代理层统计:如果使用Redis代理(如Twemproxy、Codis),在代理层统计key的访问频率。
  4. 客户端统计:在Redis客户端SDK中埋点,统计key的访问频率。
  5. Redis slowlog + 业务日志分析

解决方案:

  1. 本地缓存(L1 Cache):在应用层增加本地缓存(Caffeine/Guava Cache),热Key优先从本地缓存读取。减少对Redis的请求。注意:本地缓存的一致性问题(TTL短一些,或通过MQ通知失效)。
  2. 读写分离:热Key的读请求分散到多个从库。
  3. Key分片:将热Key拆分为多个子Key(如hotkey:1、hotkey:2…hotkey:N),读取时随机选择一个子Key。写入时更新所有子Key。
  4. Cluster模式下的副本读取READONLY命令允许从Cluster的从节点读取数据。
  5. 代理层缓存:在Redis代理层缓存热Key的数据。

14. 🔴 Redis的内存碎片是什么?如何检测和解决?

答:内存碎片:Redis实际使用的物理内存大于存储数据所需的逻辑内存。

产生原因:

  1. 频繁修改不同大小的value:Redis使用jemalloc内存分配器,按固定大小的内存块分配(8/16/32/64/…字节)。如果value从100字节修改为200字节,原来的128字节块释放,分配256字节块。释放的128字节块可能无法被其他数据使用。
  2. 大量删除key:删除key后释放的内存块可能不连续,无法合并为大块。
  3. 数据类型转换:如Hash从listpack转为hashtable,内存布局变化。

检测:

  • INFO memory命令:
    • used_memory:Redis数据占用的逻辑内存。
    • used_memory_rss:Redis进程占用的物理内存(包含碎片)。
    • mem_fragmentation_ratio = used_memory_rss / used_memory。
    • ratio > 1.5:碎片严重,需要处理。
    • ratio < 1:说明使用了swap,性能严重下降,需要立即处理。

解决方案:

  1. 自动碎片整理(Redis 4.0+)
    • activedefrag yes:开启自动碎片整理。
    • Redis在后台扫描内存,将数据从碎片化的内存块移动到连续的内存块,释放碎片。
    • 控制参数:active-defrag-threshold-lower 10(碎片率>10%时开始整理)、active-defrag-cycle-min 1(最少使用1%的CPU)、active-defrag-cycle-max 25(最多使用25%的CPU)。
  2. 重启Redis:重启后从RDB/AOF恢复数据,内存重新分配,碎片消除。简单粗暴但有效。
  3. 预防:尽量使用固定大小的value,减少频繁修改value大小的操作。

15. 🔵 Redis的Pipeline是什么?它和事务(MULTI/EXEC)有什么区别?

答:Pipeline:将多个命令打包一次性发送给Redis,减少网络往返次数(RTT)。

Pipeline原理:

  • 正常模式:每个命令一次RTT(发送命令→等待响应→发送下一个命令)。
  • Pipeline模式:一次性发送N个命令,Redis依次执行并将N个响应一次性返回。N个命令只需要1次RTT。
  • 性能提升:如果RTT是1ms,100个命令正常需要100ms,Pipeline只需要1ms+执行时间。

Pipeline vs 事务(MULTI/EXEC):

维度 Pipeline 事务(MULTI/EXEC)
原子性 不保证(命令之间可能插入其他客户端的命令) 保证(EXEC时所有命令连续执行)
网络优化 是(减少RTT) 是(命令缓存到EXEC时一起执行)
错误处理 每个命令独立,部分失败不影响其他 语法错误全部不执行,运行时错误不回滚
回滚 不支持 不支持(Redis事务不支持回滚)
WATCH 不支持 支持(乐观锁)

Pipeline + 事务:可以在Pipeline中使用MULTI/EXEC,既减少RTT又保证原子性。

注意事项:

  1. Pipeline的命令数量不宜过多(建议100-1000个),过多会占用大量内存(Redis需要缓存所有响应)。
  2. Pipeline中的命令不能依赖前一个命令的结果(因为是批量发送的)。
  3. Cluster模式下,Pipeline中的命令必须在同一个节点上(使用Hash Tag保证)。

16. 🔴 Redis的Lua脚本有什么作用?它和事务相比有什么优势?

答:Redis支持在服务端执行Lua脚本,保证多个命令的原子性执行。

Lua脚本的优势(相比事务):

  1. 真正的原子性:Lua脚本执行期间不会被其他命令打断(单线程保证)。事务虽然也是原子执行,但不支持在命令之间做逻辑判断。
  2. 支持逻辑控制:Lua脚本可以使用if/else、循环等逻辑,根据中间结果决定后续操作。事务不支持。
  3. 减少网络往返:多个命令在服务端一次执行,只需要一次RTT。
  4. 复用:EVALSHA缓存脚本,多次调用只传SHA1摘要。

常见使用场景:

  1. 分布式锁:加锁和设置过期时间原子执行。
  2. 限流:检查计数器并递增原子执行。
  3. 库存扣减:检查库存并扣减原子执行。
  4. 复杂的条件更新:根据当前值决定是否更新。

示例(限流脚本):

1
2
3
4
5
6
7
8
9
10
-- KEYS[1]: 限流key, ARGV[1]: 限流阈值, ARGV[2]: 窗口时间(秒)
local current = redis.call('GET', KEYS[1])
if current and tonumber(current) >= tonumber(ARGV[1]) then
return 0 -- 超过限制
end
current = redis.call('INCR', KEYS[1])
if tonumber(current) == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[2])
end
return 1 -- 允许通过

注意事项:

  1. Lua脚本执行时间不宜过长(阻塞主线程)。lua-time-limit默认5秒,超时后其他客户端的命令会返回BUSY错误。
  2. Lua脚本中不要使用随机函数或时间函数(影响主从一致性)。Redis 7.0+的Function支持更灵活的脚本管理。
  3. Cluster模式下,Lua脚本中访问的所有key必须在同一个槽(使用Hash Tag)。

17. 🔴 什么是Redis的慢查询?如何排查和优化Redis的性能问题?

答:慢查询:执行时间超过阈值的命令。

慢查询配置:

  • slowlog-log-slower-than:慢查询阈值(微秒),默认10000(10ms)。生产建议设为1000(1ms)。
  • slowlog-max-len:慢查询日志最大条数,默认128。建议设为1000+。
  • SLOWLOG GET [count]:查看慢查询日志。
  • SLOWLOG LEN:慢查询日志条数。
  • SLOWLOG RESET:清空慢查询日志。

常见性能问题和优化:

  1. O(n)命令:KEYS *、HGETALL(大Hash)、SMEMBERS(大Set)、LRANGE 0 -1(大List)。

    • 优化:用SCAN代替KEYS,用HSCAN代替HGETALL,限制集合大小。
  2. 大Key操作:读取/删除大Key阻塞主线程。

    • 优化:拆分大Key,使用UNLINK异步删除。
  3. 频繁的全量持久化:BGSAVE的fork操作在大内存实例上可能阻塞数百毫秒。

    • 优化:减少BGSAVE频率,使用AOF+混合持久化。控制实例内存大小(建议<10GB)。
  4. 内存不足导致swap:Redis使用swap后性能下降100倍以上。

    • 优化:监控used_memory_rss,确保物理内存充足。设置maxmemory。
  5. 网络延迟:客户端和Redis之间的网络延迟。

    • 优化:同机房部署,使用Pipeline减少RTT。
  6. CPU竞争:Redis与其他进程竞争CPU。

    • 优化:Redis绑定CPU核心(taskset),避免与CPU密集型进程共存。
  7. AOF重写:AOF重写期间fork和写入磁盘可能影响性能。

    • 优化:no-appendfsync-on-rewrite yes,重写期间不fsync(可能丢失少量数据)。

排查工具:

  • INFO命令:查看Redis各项指标。
  • LATENCY命令(Redis 2.8.13+):延迟监控和分析。
  • redis-cli --latency:测量客户端到Redis的延迟。
  • redis-cli --intrinsic-latency:测量系统固有延迟。

18. 🔵 什么是Redis的发布订阅(Pub/Sub)?它有什么局限性?

答:Redis Pub/Sub:消息发布订阅机制,Publisher发送消息到Channel,所有订阅该Channel的Subscriber收到消息。

基本使用:

1
2
3
4
5
6
# 订阅
SUBSCRIBE channel1 channel2
PSUBSCRIBE pattern* # 模式订阅

# 发布
PUBLISH channel1 "hello"

局限性:

  1. 消息不持久化:消息发送后如果没有订阅者在线,消息丢失。不像MQ有消息存储。
  2. 不支持消息确认:Publisher不知道消息是否被Subscriber成功处理。
  3. 不支持消费者组:所有Subscriber收到所有消息(广播模式),不支持负载均衡。
  4. 不支持消息回溯:无法消费历史消息。
  5. Cluster模式下的问题:PUBLISH命令会在所有节点间广播,消耗集群内部带宽。
  6. 客户端断线丢消息:Subscriber断线期间的消息全部丢失。

替代方案:Redis Stream(Redis 5.0+)解决了Pub/Sub的大部分局限性。

19. 🔴 什么是Redis Stream?它和Pub/Sub以及Kafka有什么区别?

答:Redis Stream(5.0+)是Redis内置的消息队列数据结构,支持消息持久化、消费者组、消息确认。

核心特性:

  • 消息持久化:消息存储在Stream中,不会因为没有消费者而丢失。
  • 消费者组(Consumer Group):多个消费者分摊消费消息(负载均衡)。
  • 消息确认(ACK):消费者处理完消息后ACK,未ACK的消息可以被重新消费。
  • 消息ID:每条消息有唯一ID(时间戳-序号),支持范围查询。
  • 消息回溯:可以从任意ID开始消费历史消息。

与Pub/Sub对比:

维度 Pub/Sub Stream
持久化 不持久化 持久化
消费者组 不支持 支持
消息确认 不支持 支持(XACK)
历史消息 不支持 支持(XRANGE)
消息丢失 断线丢失 不丢失

与Kafka对比:

维度 Redis Stream Kafka
定位 轻量级消息队列 分布式流平台
吞吐量 万级QPS 百万级QPS
持久化 内存+RDB/AOF 磁盘(顺序写)
分区 不支持(单Stream单节点) 支持(Partition)
消费者组 支持 支持
适用场景 轻量级异步任务、简单消息队列 大数据、日志、流处理

适用场景:不想引入Kafka/RocketMQ等重量级MQ,只需要简单的消息队列功能(如异步任务、事件通知)。

20. 🔴 Redis的事务为什么不支持回滚?WATCH的乐观锁机制是怎样的?

答:Redis事务(MULTI/EXEC)不支持回滚,这是Redis作者的设计决策。

不支持回滚的原因(antirez的解释):

  1. Redis命令只会因为语法错误或类型错误失败:如对String执行LPUSH。这类错误是编程错误,不应该在生产环境出现。
  2. 回滚增加复杂度:支持回滚需要记录undo日志,增加内存和CPU开销,与Redis的简单高效理念矛盾。
  3. Redis的定位:Redis不是关系数据库,不需要ACID事务。

Redis事务的行为:

  • MULTI:开始事务,后续命令入队(不立即执行)。
  • EXEC:执行所有入队的命令。
  • 语法错误(编译期):如命令拼写错误,EXEC时所有命令都不执行。
  • 运行时错误:如对String执行LPUSH,该命令失败但其他命令正常执行(不回滚)。

WATCH乐观锁:

  • WATCH key1 key2…:监视一个或多个key。
  • 在EXEC之前,如果被WATCH的key被其他客户端修改,EXEC返回nil(事务取消)。
  • 实现原理:每个被WATCH的key关联一个版本号(或修改标记)。EXEC时检查版本号是否变化。

使用示例(CAS操作):

1
2
3
4
5
6
7
8
9
WATCH balance
val = GET balance
if val >= 100:
MULTI
DECRBY balance 100
INCRBY target 100
EXEC # 如果balance在WATCH后被修改,EXEC返回nil,需要重试
else:
UNWATCH

注意:WATCH + MULTI/EXEC在高并发下可能频繁失败重试。对于复杂的原子操作,推荐使用Lua脚本。

21. 🔵 什么是Redis的内存优化?有哪些减少Redis内存使用的技巧?

答:Redis内存优化从数据结构选择、编码优化、配置调整三个层面入手。

数据结构优化:

  1. 使用Hash代替多个String:存储对象时,用一个Hash存储所有字段,比多个String节省内存(Hash在元素少时使用listpack,非常紧凑)。
  2. 使用intset/listpack编码:控制集合元素数量和大小在阈值内,保持紧凑编码。
  3. 短key名:key名越短越省内存。如用u:1:n代替user:1:name(但要平衡可读性)。
  4. 使用位操作:BITMAP适合存储大量布尔值(如用户签到、在线状态)。每个用户只占1bit。
  5. 使用HyperLogLog:统计UV等基数估算场景,固定占用12KB,无论数据量多大。

编码优化:

  1. 控制Hash/Set/ZSet的元素数量:保持在listpack阈值内(hash-max-listpack-entries等配置)。listpack比hashtable/skiplist节省50%+内存。
  2. 使用整数值:整数可以使用int编码(不分配SDS),比字符串省内存。
  3. 压缩value:对大value进行gzip/snappy压缩后存储。

配置优化:

  1. maxmemory:设置内存上限,配合淘汰策略。
  2. activedefrag:开启自动碎片整理。
  3. lazyfree-lazy-expire/eviction/server-del:开启异步删除,减少主线程阻塞。

内存分析工具:

  • INFO memory:整体内存使用情况。
  • MEMORY USAGE key:单个key的内存占用。
  • MEMORY DOCTOR:内存问题诊断建议。
  • redis-rdb-tools:离线分析RDB文件中每个key的内存占用。

22. 🔴 Redis 7.0有哪些重要的新特性?Function和Lua Script有什么区别?

答:Redis 7.0的重要新特性:

  1. Redis Function:替代EVAL/EVALSHA的新脚本机制。

    • 函数持久化:函数存储在Redis中(随RDB/AOF持久化),不需要每次启动时重新加载。
    • 库管理:函数组织在Library中,支持版本管理。
    • 与Lua Script区别:
      维度 Lua Script (EVAL) Function
      持久化 不持久化(重启后丢失) 持久化(随数据持久化)
      管理 通过SHA1引用 通过函数名引用
      组织 无组织结构 Library→Function层级
      主从复制 脚本本身不复制 函数定义复制到从库
      引擎 只支持Lua 可扩展(目前只有Lua)
  2. Multi-part AOF:AOF文件拆分为多个文件(base文件 + 增量文件 + manifest文件)。重写时只替换base文件,增量文件追加。避免了单个大AOF文件的问题。

  3. listpack全面替代ziplist:所有使用ziplist的地方改为listpack。listpack解决了ziplist的级联更新问题(ziplist修改一个元素可能导致后续所有元素重新分配)。

  4. Client-side Caching改进:支持广播模式(Broadcasting),服务端主动推送key失效通知。

  5. Sharded Pub/Sub:Cluster模式下的分片Pub/Sub,消息只在负责该Channel的节点间传播(不再全集群广播),大幅减少集群内部带宽。

  6. ACL v2:更细粒度的权限控制,支持Selector(多组权限规则)。

23. 🔵 什么是Redis的Client-side Caching?它是如何实现的?

答:Client-side Caching:客户端在本地缓存Redis数据,减少网络请求。Redis服务端在数据变更时通知客户端失效。

实现方式(Redis 6.0+):

  1. Tracking模式(默认)

    • 客户端开启Tracking:CLIENT TRACKING ON
    • 客户端读取key时,Redis记录”这个客户端缓存了这个key”。
    • 当key被修改时,Redis发送失效通知(Invalidation Message)给客户端。
    • 客户端收到通知后删除本地缓存。
  2. 广播模式(Broadcasting)

    • CLIENT TRACKING ON BCAST PREFIX user:
    • 客户端订阅key前缀,任何匹配前缀的key变更都会通知。
    • 不需要Redis记录每个客户端缓存了哪些key(节省服务端内存)。
    • 可能收到不需要的通知(客户端没缓存但前缀匹配的key)。
  3. OPTIN模式

    • CLIENT TRACKING ON OPTIN
    • 客户端在读取key前发送CLIENT CACHING YES,只有显式标记的key才被追踪。
    • 更精确,减少不必要的追踪开销。

通知机制:

  • 使用RESP3协议的Push消息(Redis 6.0+)。
  • 或通过Pub/Sub的__redis__:invalidate频道(兼容RESP2)。

Lettuce/Jedis支持:Lettuce 6.0+原生支持Client-side Caching。

适用场景:读多写少的热数据(如配置信息、用户Profile),本地缓存命中率高时效果显著。

24. 🔴 Redis的IO多线程模型(Redis 6.0+)是怎样的?它和Memcached的多线程有什么区别?

答:Redis 6.0引入多线程IO,但命令执行仍然是单线程。

Redis多线程IO模型:

  1. 主线程接收客户端连接,将连接分配给IO线程。
  2. IO线程负责读取客户端请求数据(网络读)。
  3. 主线程单线程执行所有命令(保证原子性和线程安全)。
  4. IO线程负责将响应数据写回客户端(网络写)。

配置:

1
2
io-threads 4          # IO线程数(建议CPU核心数的一半,不超过8)
io-threads-do-reads yes # IO线程也处理读操作(默认只处理写)

为什么命令执行仍然是单线程:

  • Redis的数据结构不是线程安全的,多线程执行需要加锁,锁的开销可能抵消多线程的收益。
  • 单线程执行保证了命令的原子性,简化了编程模型。
  • Redis的瓶颈通常在网络IO而非CPU(命令执行很快),多线程IO已经足够。

与Memcached多线程对比:

维度 Redis 6.0+ Memcached
线程模型 IO多线程 + 命令单线程 完全多线程(IO和命令都多线程)
数据结构 不需要锁 需要锁(CAS等)
原子性 天然保证 需要CAS保证
性能 单实例10-20万QPS 单实例数十万QPS
复杂度 简单 较复杂(锁竞争)

性能提升:开启多线程IO后,Redis的吞吐量提升约1倍(从10万QPS到20万QPS),主要收益在网络IO密集的场景。

25. 🔵 什么是Redis的ACL(Access Control List)?如何实现细粒度的权限控制?

答:Redis ACL(6.0+):细粒度的用户权限控制,替代旧版本的单一密码认证(requirepass)。

ACL功能:

  1. 用户管理:创建多个用户,每个用户独立的密码和权限。
  2. 命令权限:控制用户可以执行哪些命令。如只允许读命令,禁止FLUSHALL等危险命令。
  3. Key权限:控制用户可以访问哪些key(按模式匹配)。如只允许访问user:*的key。
  4. Channel权限(Redis 7.0+):控制用户可以订阅哪些Pub/Sub Channel。

配置示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 创建用户:只能执行GET/SET命令,只能访问app:*的key
ACL SETUSER appuser on >password123 ~app:* +get +set

# 创建只读用户
ACL SETUSER readonly on >readpass ~* +@read

# 创建管理员
ACL SETUSER admin on >adminpass ~* +@all

# 查看用户列表
ACL LIST

# 查看当前用户
ACL WHOAMI

命令分类(@category):

  • @read:所有读命令(GET、HGET、SMEMBERS等)。
  • @write:所有写命令(SET、HSET、SADD等)。
  • @admin:管理命令(CONFIG、SHUTDOWN等)。
  • @dangerous:危险命令(KEYS、FLUSHALL、DEBUG等)。
  • @slow:可能阻塞的命令。

生产最佳实践:

  1. 禁用default用户或设置强密码。
  2. 为不同应用创建独立用户,最小权限原则。
  3. 禁止所有用户执行KEYS、FLUSHALL、FLUSHDB、CONFIG等危险命令。
  4. ACL规则持久化到aclfile(aclfile /etc/redis/users.acl)。

二、缓存架构设计(26-50题)

26. 🔵 什么是缓存穿透、缓存击穿、缓存雪崩?分别如何解决?

答:这是缓存架构中最经典的三个问题。

缓存穿透:查询不存在的数据,缓存和数据库都没有,每次请求都打到数据库。

  • 原因:恶意攻击(大量随机ID查询)、业务bug。
  • 解决方案:
    1. 缓存空值:查询数据库为空时,缓存一个空值(TTL短,如5分钟)。防止同一个不存在的key反复查询数据库。
    2. 布隆过滤器:在缓存前加一层布隆过滤器,存储所有合法的key。不在布隆过滤器中的key直接拒绝。
    3. 参数校验:在入口层校验参数合法性(如ID必须为正整数)。

缓存击穿:热点key过期的瞬间,大量请求同时打到数据库。

  • 原因:热点key过期 + 高并发。
  • 解决方案:
    1. 互斥锁:缓存未命中时,用分布式锁(SETNX)保证只有一个线程查询数据库并回填缓存,其他线程等待。
    2. 逻辑过期:不设置TTL,在value中存储逻辑过期时间。发现逻辑过期后异步更新缓存,当前请求返回旧数据。
    3. 热点key永不过期:对确定的热点key不设置过期时间,通过后台任务定期更新。

缓存雪崩:大量key同时过期,或Redis宕机,大量请求打到数据库。

  • 解决方案:
    1. 过期时间加随机值:避免大量key同时过期。TTL = base + random(0, 300)。
    2. 多级缓存:L1(本地缓存)+ L2(Redis)+ L3(数据库)。Redis不可用时本地缓存兜底。
    3. 熔断降级:数据库压力过大时触发熔断,返回默认值或错误提示。
    4. Redis高可用:Sentinel或Cluster保证Redis不宕机。

27. 🔴 什么是缓存和数据库的一致性问题?有哪些保证一致性的方案?

答:缓存和数据库的数据不一致是分布式系统中的经典问题。

不一致的场景:

  • 先更新数据库,再删除缓存:删除缓存失败,缓存中是旧数据。
  • 先删除缓存,再更新数据库:删除缓存后、更新数据库前,另一个请求读取数据库旧数据写入缓存。

常见方案:

  1. Cache Aside Pattern(旁路缓存,最常用)

    • 读:先读缓存,未命中则读数据库,写入缓存。
    • 写:先更新数据库,再删除缓存(不是更新缓存)。
    • 为什么删除而非更新:避免并发写入时缓存和数据库不一致(两个线程同时更新,缓存可能存储先更新的旧值)。
    • 问题:极端情况下仍可能不一致(读请求在数据库更新前读到旧值,在缓存删除后写入缓存)。概率很低(需要读请求的数据库查询比写请求的数据库更新慢)。
  2. 延迟双删

    • 先删除缓存→更新数据库→延迟N毫秒→再次删除缓存。
    • 第二次删除清理可能被并发读请求写入的旧缓存。
    • 延迟时间 > 读请求的数据库查询时间。
    • 问题:延迟时间难以精确确定。
  3. 基于binlog的异步更新(推荐)

    • 写操作只更新数据库。
    • 通过Canal/Debezium监听binlog,异步删除/更新缓存。
    • 优点:业务代码不需要操作缓存(解耦),binlog保证不丢失。
    • 缺点:有短暂的不一致窗口(binlog传播延迟)。
  4. Read/Write Through

    • 缓存层封装数据库操作。读未命中时缓存层自动从数据库加载。写操作由缓存层同步写入数据库。
    • 优点:业务代码只操作缓存,逻辑简单。
    • 缺点:需要缓存中间件支持(如Redis本身不支持,需要自己封装)。
  5. Write Behind(异步写回)

    • 写操作只更新缓存,缓存层异步批量写入数据库。
    • 优点:写入性能极高。
    • 缺点:缓存宕机可能丢数据。适合允许少量数据丢失的场景(如计数器)。

生产建议:大多数场景用Cache Aside + 合理的TTL。对一致性要求高的场景加上binlog异步更新。

28. 🔵 什么是多级缓存架构?如何设计L1+L2+L3的缓存体系?

答:多级缓存:在不同层级设置缓存,逐级降低对下游的压力。

典型架构:

1
客户端 → CDN(静态资源) → Nginx本地缓存 → 应用本地缓存(L1) → Redis(L2) → 数据库(L3)

各层设计:

L1(应用本地缓存):

  • 技术:Caffeine(Java最快的本地缓存)、Guava Cache。
  • 特点:内存级访问,纳秒级延迟。容量有限(受JVM堆内存限制)。
  • 适用:热点数据、变化不频繁的数据(如配置、字典)。
  • TTL:较短(10秒-5分钟),避免数据不一致。
  • 一致性:通过Redis Pub/Sub或MQ通知其他实例失效。

L2(分布式缓存 - Redis):

  • 特点:毫秒级延迟,容量大(集群可达TB级)。
  • 适用:大部分业务数据。
  • TTL:中等(5分钟-24小时)。
  • 高可用:Sentinel或Cluster。

L3(数据库):

  • 兜底数据源,保证数据完整性。

多级缓存的挑战:

  1. 一致性:多级缓存的失效顺序和时机需要仔细设计。通常先失效L1再失效L2。
  2. 缓存穿透放大:L1未命中→L2未命中→数据库。需要在每一层都有防穿透措施。
  3. 监控复杂:需要监控每一层的命中率、延迟、容量。

开源方案:

  • J2Cache(红薯开源):L1(Caffeine/Ehcache)+ L2(Redis),内置缓存同步。
  • Jetcache(阿里开源):支持多级缓存、自动刷新、统计。

29. 🔴 什么是布隆过滤器(Bloom Filter)?Redis中如何使用?有什么局限性?

答:布隆过滤器:概率型数据结构,用于判断元素是否存在于集合中。可能误判(false positive),但不会漏判(no false negative)。

原理:

  • 一个m位的位数组(初始全0)和k个哈希函数。
  • 添加元素:用k个哈希函数计算k个位置,将这些位置设为1。
  • 查询元素:用k个哈希函数计算k个位置,如果所有位置都是1则”可能存在”,如果有任何位置是0则”一定不存在”。

Redis中使用布隆过滤器:

  1. RedisBloom模块(推荐):Redis官方模块,提供BF.ADD、BF.EXISTS等命令。
1
2
3
4
5
6
7
# 创建布隆过滤器(错误率0.01,预期容量100万)
BF.RESERVE user_filter 0.01 1000000
# 添加元素
BF.ADD user_filter "user:1001"
# 查询元素
BF.EXISTS user_filter "user:1001" # 返回1(存在)
BF.EXISTS user_filter "user:9999" # 返回0(不存在)
  1. 基于BITMAP手动实现:使用SETBIT/GETBIT命令操作位数组,应用层实现多个哈希函数。

应用场景:

  1. 缓存穿透防护:将所有合法key加入布隆过滤器,查询前先检查。
  2. 去重:爬虫URL去重、消息ID去重。
  3. 推荐系统:过滤用户已看过的内容。

局限性:

  1. 误判率:存在false positive(判断存在但实际不存在)。误判率取决于位数组大小和哈希函数数量。
  2. 不支持删除:标准布隆过滤器不支持删除元素(删除可能影响其他元素)。Counting Bloom Filter支持删除但占用更多空间。Cuckoo Filter支持删除且空间效率更高。
  3. 容量固定:创建时需要预估容量,超过容量后误判率急剧上升。Scalable Bloom Filter可以动态扩容。

30. 🔵 什么是Redis的分布式锁?如何正确实现?有哪些常见的坑?

答:分布式锁:在分布式系统中保证同一时刻只有一个进程执行某个操作。

基本实现:

1
2
3
4
5
6
7
8
9
# 加锁(原子操作:SET + NX + EX)
SET lock_key unique_value NX EX 30

# 解锁(Lua脚本保证原子性:检查value后删除)
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end

关键要素:

  1. 互斥性:NX保证只有一个客户端能加锁。
  2. 防死锁:EX设置过期时间,客户端崩溃后锁自动释放。
  3. 防误删:value使用唯一标识(UUID),解锁时检查value是否匹配,防止删除别人的锁。
  4. 原子性:加锁用SET NX EX(一条命令),解锁用Lua脚本。

常见的坑:

  1. 锁过期但业务未完成:业务执行时间超过锁的过期时间,锁被自动释放,其他客户端获取锁,导致并发问题。
    • 解决:Redisson的看门狗机制(WatchDog),后台线程定期续期锁(默认每10秒续期到30秒)。
  2. Redis主从切换丢锁:客户端在Master上加锁成功,Master宕机,Slave升级为Master但未同步到锁数据。另一个客户端在新Master上加锁成功。
    • 解决:RedLock算法(见下题)。
  3. 可重入性:同一个线程多次加锁需要支持重入。
    • 解决:Redisson的RLock支持可重入(内部用Hash记录加锁次数)。
  4. 非阻塞:SETNX失败立即返回,不等待。需要阻塞等待的场景需要自己实现重试。
    • 解决:Redisson的tryLock(waitTime, leaseTime, unit)支持等待。

31. 🔴 什么是RedLock算法?它真的能解决分布式锁的问题吗?Martin Kleppmann的批评是什么?

答:RedLock是Redis作者antirez提出的分布式锁算法,使用多个独立的Redis实例提高锁的可靠性。

RedLock算法:

  1. 获取当前时间T1。
  2. 依次向N个独立的Redis实例(建议5个)发送加锁请求(SET NX EX)。
  3. 如果在超过N/2+1个实例上加锁成功,且总耗时 < 锁的过期时间,则加锁成功。
  4. 锁的有效时间 = 过期时间 - 加锁耗时。
  5. 如果加锁失败,向所有实例发送解锁请求。

Martin Kleppmann的批评(”How to do distributed locking”):

  1. 时钟跳跃问题:RedLock依赖各节点的时钟大致同步。如果某个节点时钟跳跃(NTP调整),锁可能提前过期。
  2. GC停顿问题:客户端获取锁后发生长时间GC停顿,锁过期后其他客户端获取锁,GC恢复后两个客户端都认为自己持有锁。
  3. 本质问题:如果需要强一致的分布式锁,应该使用基于共识协议的系统(如ZooKeeper、etcd),而非Redis。

antirez的回应:

  1. 时钟跳跃可以通过配置NTP避免大幅跳跃。
  2. GC停顿问题在任何分布式锁实现中都存在(包括ZooKeeper)。
  3. RedLock在实际场景中足够可靠。

实际生产建议:

  • 效率型锁(防止重复执行,允许偶尔失败):单Redis实例的SETNX就够了,简单高效。
  • 正确性型锁(绝对不能并发执行):使用ZooKeeper或etcd的分布式锁(基于共识协议,更可靠)。
  • RedLock在大多数场景下是过度设计。Redisson的单实例锁 + 看门狗续期已经能满足99%的需求。

32. 🔵 Redisson是什么?它提供了哪些分布式数据结构?

答:Redisson是Redis的Java客户端,提供了丰富的分布式数据结构和服务。

核心功能:

  1. 分布式锁

    • RLock:可重入锁,支持看门狗自动续期。
    • RReadWriteLock:读写锁。
    • RFairLock:公平锁(按请求顺序获取)。
    • RMultiLock:联锁(多个锁同时获取)。
    • RRedLock:RedLock算法实现。
    • RSemaphore:分布式信号量。
    • RCountDownLatch:分布式CountDownLatch。
  2. 分布式集合

    • RMap:分布式Map(支持本地缓存、淘汰策略)。
    • RSet/RSortedSet:分布式Set。
    • RList/RQueue/RDeque:分布式List/Queue。
    • RBloomFilter:分布式布隆过滤器。
    • RHyperLogLog:分布式HyperLogLog。
  3. 分布式服务

    • RRemoteService:分布式远程服务调用。
    • RScheduledExecutorService:分布式定时任务。
    • RMapCache:带过期时间的分布式Map。
    • RTopic:分布式发布订阅。
  4. 其他

    • RateLimiter:分布式限流器。
    • RAtomicLong:分布式原子计数器。
    • RBitSet:分布式位图。

看门狗机制(WatchDog):

  • 加锁时不指定leaseTime,Redisson自动启动看门狗。
  • 看门狗每lockWatchdogTimeout/3(默认10秒)检查一次,如果锁还被持有则续期到lockWatchdogTimeout(默认30秒)。
  • 客户端宕机后看门狗停止,锁在30秒后自动释放。

33. 🔴 如何设计一个高并发的计数器系统?Redis在其中如何使用?

答:高并发计数器(如微博点赞数、文章阅读数、商品库存)的设计。

方案1:Redis INCR(简单场景)

  • 直接使用Redis的INCR/DECR命令,原子操作,单实例10万+ QPS。
  • 适合:精确计数、实时性要求高。
  • 问题:Redis宕机可能丢失数据(取决于持久化策略)。

方案2:Redis + 异步持久化(推荐)

  • 计数操作写入Redis(INCR),定时任务将Redis中的计数同步到数据库。
  • 读取优先从Redis读,Redis不可用时从数据库读。
  • 同步策略:每N秒或每N次变更同步一次。
  • 优点:高性能 + 数据不丢失(最多丢失一个同步周期的数据)。

方案3:Redis HyperLogLog(近似计数)

  • 适合UV统计等不需要精确值的场景。
  • 固定12KB内存,误差率0.81%。
  • PFADD添加元素,PFCOUNT获取基数估算。

方案4:Redis Bitmap(布尔计数)

  • 适合”用户是否已操作”的场景(如签到、点赞去重)。
  • SETBIT user:1:likes 1001 1(用户1001点赞)。
  • BITCOUNT user:1:likes(统计点赞数)。

库存扣减的特殊处理:

1
2
3
4
5
6
7
-- 原子扣减库存(Lua脚本)
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1 -- 扣减成功
end
return 0 -- 库存不足

注意:库存扣减需要保证Redis和数据库的一致性。推荐:Redis预扣减(快速响应)+ 异步同步数据库 + 对账机制兜底。

34. 🔵 如何设计一个基于Redis的排行榜系统?

答:排行榜是Redis ZSet(Sorted Set)的经典应用场景。

基本设计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 更新分数
ZADD leaderboard 1000 "user:1001"
ZINCRBY leaderboard 50 "user:1001" # 加50分

# 获取排名(从高到低,0-based)
ZREVRANK leaderboard "user:1001"

# 获取Top N
ZREVRANGE leaderboard 0 9 WITHSCORES # Top 10

# 获取分数
ZSCORE leaderboard "user:1001"

# 获取排名范围
ZREVRANGEBYSCORE leaderboard 1000 500 WITHSCORES LIMIT 0 10

复杂排行榜设计:

  1. 多维度排序:分数相同时按时间排序。

    • 方案:score = 主分数 * 10^10 + (MAX_TIMESTAMP - 时间戳)。分数相同时,时间早的排在前面。
  2. 分页查询

    • ZREVRANGEBYSCORE + LIMIT实现分页。
    • 或ZREVRANGE + offset实现(大offset性能差,O(offset+count))。
  3. 实时排行榜 + 历史排行榜

    • 实时:直接操作ZSet。
    • 历史:定时快照(ZUNIONSTORE或导出到数据库)。
    • 日榜/周榜/月榜:不同的ZSet key(leaderboard:daily:20240101)。
  4. 大规模排行榜(亿级用户)

    • 单个ZSet不宜过大(百万级以内)。
    • 分段排行榜:按分数段分片(0-1000分一个ZSet,1001-2000分一个ZSet)。
    • 或只维护Top N(如Top 10000),其他用户的排名通过分数估算。
  5. 附近排名:获取某用户前后N名的用户。

    • ZREVRANK获取排名 → ZREVRANGE获取排名范围内的用户。

35. 🔴 如何设计一个基于Redis的分布式限流系统?

答:限流是保护系统的重要手段,Redis适合实现分布式限流。

限流算法:

  1. 固定窗口计数器
1
2
3
4
# 每分钟限制100次
INCR rate:user:1001:202401011200
EXPIRE rate:user:1001:202401011200 60
# 检查是否超过100
  • 问题:窗口边界突发(如第59秒100次+第60秒100次=1秒内200次)。
  1. 滑动窗口计数器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 使用ZSet实现滑动窗口
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2]) -- 窗口大小(秒)
local now = tonumber(ARGV[3])
-- 移除窗口外的记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)
-- 统计窗口内的请求数
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. math.random())
redis.call('EXPIRE', key, window)
return 1 -- 允许
end
return 0 -- 拒绝
  1. 令牌桶(Token Bucket)
  • 以固定速率向桶中添加令牌,请求消耗令牌。桶满时令牌溢出。
  • 允许突发流量(桶中有积累的令牌)。
  • Redis实现:记录上次添加令牌的时间和当前令牌数,每次请求时计算应该添加的令牌数。
  1. 漏桶(Leaky Bucket)
  • 请求进入桶中,以固定速率流出处理。桶满时拒绝新请求。
  • 平滑流量,不允许突发。

Redisson限流器:

1
2
3
4
5
RRateLimiter limiter = redisson.getRateLimiter("myLimiter");
limiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.MINUTES);
if (limiter.tryAcquire()) {
// 允许通过
}

生产建议:

  • 简单场景用固定窗口(实现简单)。
  • 精确限流用滑动窗口或令牌桶。
  • 多维度限流:用户级 + 接口级 + 全局级,不同维度不同阈值。

36. 🔵 什么是缓存预热?有哪些缓存预热的策略?

答:缓存预热:系统启动或缓存失效后,提前将热点数据加载到缓存中,避免冷启动时大量请求打到数据库。

预热策略:

  1. 启动时预热

    • 应用启动时从数据库加载热点数据到Redis。
    • 适合:数据量不大、热点数据可预知的场景。
    • 实现:Spring的@PostConstruct或ApplicationRunner。
  2. 定时预热

    • 定时任务定期刷新缓存(如每5分钟刷新热点商品缓存)。
    • 适合:数据变化不频繁、可以接受短暂不一致的场景。
  3. 访问驱动预热(Lazy Loading)

    • 第一次访问时从数据库加载并缓存。
    • 适合:热点数据不可预知的场景。
    • 问题:冷启动时第一批请求延迟高。
  4. 基于日志的预热

    • 分析历史访问日志,找出热点数据,提前加载。
    • 适合:大促前的预热(分析历史大促的访问模式)。
  5. 渐进式预热

    • 新上线的缓存节点不立即承担全部流量。
    • 通过权重控制,逐步增加流量比例(如10%→30%→50%→100%)。
    • 适合:缓存集群扩容场景。
  6. CDC驱动预热

    • 通过数据库CDC(binlog)实时将数据变更同步到缓存。
    • 适合:需要缓存和数据库实时同步的场景。

注意事项:

  • 预热不要一次性加载太多数据,避免Redis内存突增和数据库压力。
  • 分批加载,控制加载速率。
  • 预热数据设置合理的TTL,避免缓存中存储过期数据。

37. 🔴 如何设计一个基于Redis的分布式Session管理系统?

答:分布式Session:将用户Session存储在Redis中,实现多实例共享Session。

基本设计:

  • key:session:{sessionId}(sessionId由服务端生成,通过Cookie传递给客户端)。
  • value:Hash类型,存储Session属性(userId、loginTime、权限等)。
  • TTL:Session过期时间(如30分钟),每次访问时续期。

Spring Session + Redis实现:

1
2
3
4
5
6
7
8
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory("redis-host", 6379);
}
}

设计要点:

  1. Session ID安全:使用安全随机数生成(SecureRandom),防止猜测。
  2. Session固定攻击防护:登录成功后重新生成Session ID。
  3. Session数据最小化:只存储必要信息(userId、角色),不存储大对象。
  4. 过期策略:绝对过期(创建后30分钟过期)+ 滑动过期(每次访问续期30分钟)。
  5. 序列化:使用JSON序列化(可读性好、跨语言)而非Java序列化(安全风险、不跨语言)。

高可用:

  • Redis Sentinel或Cluster保证Session存储的高可用。
  • 多数据中心:Session数据通过Redis复制同步到多个数据中心。

与JWT对比:

维度 Redis Session JWT
存储 服务端(Redis) 客户端(Token)
注销 删除Redis中的Session 需要黑名单机制
扩展性 依赖Redis 无状态,天然可扩展
安全性 Session ID泄露风险 Token泄露风险
数据大小 不限 Token不宜过大

38. 🔵 什么是Redis的GEO功能?如何实现附近的人/商家功能?

答:Redis GEO(3.2+):基于ZSet实现的地理位置功能,支持存储经纬度和距离计算。

底层实现:使用GeoHash算法将二维经纬度编码为一维整数,存储在ZSet的score中。GeoHash保证地理位置相近的点编码也相近。

基本命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 添加位置
GEOADD shops 116.397128 39.916527 "shop:1001"
GEOADD shops 116.405285 39.904989 "shop:1002"

# 查询两点距离
GEODIST shops "shop:1001" "shop:1002" km

# 查询附近的商家(以某点为中心,半径5km内)
GEOSEARCH shops FROMLONLAT 116.397128 39.916527 BYRADIUS 5 km ASC COUNT 10

# 查询某个商家附近的其他商家
GEOSEARCH shops FROMMEMBER "shop:1001" BYRADIUS 3 km ASC COUNT 10

# 获取位置的GeoHash值
GEOHASH shops "shop:1001"

# 获取经纬度
GEOPOS shops "shop:1001"

“附近的人”功能设计:

  1. 用户更新位置时:GEOADD user_locations lng lat userId
  2. 查询附近的人:GEOSEARCH user_locations FROMLONLAT lng lat BYRADIUS 5 km ASC COUNT 20
  3. 过滤:结合用户属性(性别、年龄)在应用层过滤。
  4. 位置更新频率:不需要实时更新,每30秒-1分钟更新一次即可。

注意事项:

  1. GEO数据存储在ZSet中,数据量大时(百万级)单个key可能成为大Key。按区域分片(如按城市)。
  2. GEOSEARCH的时间复杂度是O(N+log(M)),N是范围内的元素数,M是ZSet总元素数。
  3. GEO不支持多边形范围查询(只支持圆形和矩形),复杂地理围栏需要PostGIS或Elasticsearch。

39. 🔴 Redis和Memcached的区别是什么?什么场景下应该选择Memcached?

答:Redis和Memcached都是内存缓存,但定位和能力差异很大。

核心区别:

维度 Redis Memcached
数据结构 String/Hash/List/Set/ZSet/Stream等 只有String(key-value)
持久化 RDB/AOF 不支持
集群 Cluster原生支持 客户端分片(一致性哈希)
线程模型 单线程命令执行+多线程IO 完全多线程
内存管理 jemalloc slab分配器
最大value 512MB 1MB
事务 MULTI/EXEC 不支持
Lua脚本 支持 不支持
发布订阅 支持 不支持
过期策略 惰性+定期删除 惰性删除

选择Memcached的场景:

  1. 纯缓存场景:只需要简单的key-value缓存,不需要复杂数据结构。
  2. 多线程优势:Memcached的多线程模型在多核CPU上可以更好地利用资源。单实例吞吐量可能高于Redis。
  3. 内存效率:Memcached的slab分配器在存储大量相同大小的value时内存碎片更少。
  4. 已有Memcached基础设施:迁移成本高,且当前功能满足需求。

选择Redis的场景(大多数场景):

  1. 需要复杂数据结构(排行榜、计数器、分布式锁等)。
  2. 需要持久化。
  3. 需要发布订阅、Stream等功能。
  4. 需要原生集群支持。

实际趋势:Redis已经在大多数场景下替代了Memcached。除非有特殊的性能需求或历史原因,新项目推荐使用Redis。

40. 🔵 什么是一致性哈希(Consistent Hashing)?它在缓存分片中有什么作用?

答:一致性哈希:将节点和数据映射到同一个哈希环上,数据存储在顺时针方向的第一个节点上。

传统哈希分片的问题:

  • hash(key) % N(N是节点数)。
  • 增减节点时N变化,几乎所有key的映射都会改变,导致大量缓存失效(缓存雪崩)。

一致性哈希的优势:

  • 增减节点时,只有相邻节点的部分key需要迁移,其他key不受影响。
  • 增加一个节点:只影响该节点和逆时针方向前一个节点之间的key。
  • 删除一个节点:该节点的key迁移到顺时针方向的下一个节点。

虚拟节点:

  • 问题:节点少时,哈希环上的分布可能不均匀(数据倾斜)。
  • 解决:每个物理节点映射多个虚拟节点(如每个物理节点150个虚拟节点),虚拟节点均匀分布在哈希环上。
  • 数据先映射到虚拟节点,再映射到物理节点。

在缓存中的应用:

  • Memcached客户端(如Ketama)使用一致性哈希做客户端分片。
  • Redis Cluster不使用一致性哈希,而是使用哈希槽(16384个固定槽),手动分配槽到节点。哈希槽方案更灵活(可以精确控制数据分布),但需要集群管理。
  • Twemproxy/Codis等Redis代理使用一致性哈希或哈希槽做分片。

41. 🔴 如何设计一个支持秒杀场景的缓存架构?

答:秒杀场景的核心挑战:瞬时高并发、库存扣减的原子性和一致性、防止超卖。

架构设计:

1
用户 → CDN(静态页面) → 网关(限流+鉴权) → 应用层(业务逻辑) → Redis(库存扣减) → MQ(异步下单) → 数据库

关键设计:

  1. 前端

    • 静态页面CDN缓存,减少服务端压力。
    • 按钮防重复点击(点击后置灰)。
    • 前端随机延迟(分散请求到不同时间点)。
  2. 网关层

    • 限流:令牌桶限流,控制进入系统的请求量。
    • 黑名单:拦截恶意请求(频率异常、非法参数)。
    • 排队:超过处理能力的请求进入队列等待。
  3. 应用层

    • 本地缓存标记:库存为0后在本地缓存标记”已售罄”,后续请求直接返回,不再访问Redis。
    • 请求去重:同一用户短时间内的重复请求去重。
  4. Redis库存扣减(核心):

1
2
3
4
5
6
7
-- 原子扣减库存
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
redis.call('DECR', KEYS[1])
return 1 -- 扣减成功
end
return 0 -- 库存不足
  1. 异步下单

    • 库存扣减成功后,发送消息到MQ(Kafka/RocketMQ)。
    • 下游服务异步消费消息,创建订单、扣减数据库库存。
    • 用户看到”排队中”,异步通知结果。
  2. 防超卖

    • Redis Lua脚本保证扣减原子性。
    • 数据库层面用乐观锁兜底:UPDATE stock SET count=count-1 WHERE id=? AND count>0
    • 对账机制:定时对比Redis库存和数据库库存。

42. 🔴 什么是Redis的读写分离?如何实现?有什么注意事项?

答:读写分离:写操作发送到主库,读操作分散到从库,提高读吞吐量。

实现方式:

  1. 客户端实现:应用层根据操作类型选择连接主库或从库。Lettuce支持ReadFrom配置(MASTER/SLAVE/NEAREST等)。
  2. 代理实现:Redis代理(如Twemproxy、Codis、Redis Proxy)自动将读写请求路由到主从节点。
  3. Sentinel + 读写分离:通过Sentinel获取主从节点地址,读操作发送到从库。

注意事项:

  1. 主从延迟:从库的数据可能落后于主库(异步复制)。写入后立即读取可能读到旧数据。
    • 解决:关键读操作强制读主库。或使用WAIT命令等待从库同步完成(牺牲性能)。
  2. 从库过期key问题:Redis 3.2之前从库不主动删除过期key,可能返回已过期的数据。3.2+从库会检查过期时间返回nil。
  3. 从库数量:从库越多,主库的复制压力越大(每个从库都需要主库发送数据)。建议不超过3-5个从库。可以使用级联复制(从库的从库)减轻主库压力。
  4. 连接管理:从库故障时需要自动切换到其他从库或主库。Sentinel可以自动处理。

43. 🔵 什么是Redis的数据分片方案?客户端分片、代理分片、Cluster分片有什么区别?

答:Redis数据分片的三种方案:

  1. 客户端分片

    • 客户端根据key计算哈希值,决定发送到哪个Redis实例。
    • 算法:一致性哈希或哈希取模。
    • 优点:简单,无额外组件。
    • 缺点:客户端逻辑复杂,扩缩容需要客户端配合,不支持跨节点操作。
    • 代表:Jedis ShardedJedis。
  2. 代理分片

    • 在客户端和Redis之间加一层代理,代理负责路由。
    • 优点:客户端无感知,支持多语言。
    • 缺点:代理是额外的网络跳转(增加延迟),代理本身需要高可用。
    • 代表:Twemproxy(Twitter开源,不支持在线扩缩容)、Codis(豌豆荚开源,支持在线扩缩容和Dashboard)。
  3. Redis Cluster(官方方案)

    • 16384个哈希槽分配到多个主节点。
    • 优点:官方支持,无额外组件,支持在线扩缩容,自动故障转移。
    • 缺点:部分命令受限(跨槽操作需要Hash Tag),客户端需要支持Cluster协议。
    • 代表:Redis Cluster(Redis 3.0+)。

选型建议:

  • 新项目:直接用Redis Cluster(官方方案,生态最好)。
  • 已有Codis/Twemproxy:评估迁移成本,条件允许迁移到Cluster。
  • 特殊需求(如需要代理层的统一管理):考虑Codis或商业方案。

44. 🔴 Redis Cluster的限制有哪些?哪些操作在Cluster模式下不支持或受限?

答:Redis Cluster的限制:

  1. 跨槽操作受限

    • MGET/MSET/DEL等多key命令:所有key必须在同一个槽(使用Hash Tag)。
    • 事务(MULTI/EXEC):所有key必须在同一个槽。
    • Lua脚本:所有访问的key必须在同一个槽。
    • 集合操作(SUNION/SINTER等):所有key必须在同一个槽。
  2. 数据库限制

    • Cluster模式只支持db0,不支持SELECT切换数据库。
  3. 发布订阅

    • PUBLISH命令会在所有节点间广播(消耗集群内部带宽)。Redis 7.0+的Sharded Pub/Sub解决了这个问题。
  4. 大Key迁移

    • 槽迁移时大Key的MIGRATE操作是同步的,可能阻塞源节点。
  5. 节点数量

    • 官方建议不超过1000个节点(Gossip协议的通信开销随节点数增加)。
  6. 客户端复杂度

    • 客户端需要支持Cluster协议(MOVED/ASK重定向、槽映射缓存)。
    • 不是所有Redis客户端都完整支持Cluster。
  7. 运维复杂度

    • 扩缩容需要手动迁移槽(或使用redis-cli –cluster reshard)。
    • 故障转移可能导致短暂不可用(选举期间)。

解决方案:

  • 跨槽操作:使用Hash Tag将相关key映射到同一个槽。如{order:1001}.info和{order:1001}.items。
  • 大Key:拆分大Key,避免迁移问题。
  • 发布订阅:使用Sharded Pub/Sub(Redis 7.0+)或外部MQ。

45. 🔴 什么是Redis的脑裂问题?如何防止脑裂导致的数据丢失?

答:脑裂:网络分区导致主库和从库/Sentinel之间失去联系,Sentinel将从库提升为新主库,此时出现两个主库同时接受写入。

脑裂场景:

  1. 主库和Sentinel之间网络断开(但主库和客户端之间网络正常)。
  2. Sentinel认为主库下线,将从库提升为新主库。
  3. 客户端仍然向旧主库写入数据。
  4. 网络恢复后,旧主库变为从库,从新主库同步数据,旧主库上的新写入数据丢失。

防止脑裂的配置:

1
2
min-replicas-to-write 1    # 主库至少有1个从库连接才接受写入
min-replicas-max-lag 10 # 从库的复制延迟不超过10秒

工作原理:如果主库检测到连接的从库数量 < min-replicas-to-write,或所有从库的复制延迟 > min-replicas-max-lag,主库拒绝写入(返回错误)。这样在网络分区时,被隔离的旧主库无法接受写入,避免数据丢失。

代价:如果从库全部宕机或网络延迟过大,主库也无法写入(牺牲可用性保证一致性)。

Cluster模式的脑裂:

  • Cluster模式下,主节点如果无法与多数主节点通信,会拒绝写入(cluster-require-full-coverage配置)。
  • 从节点选举需要多数主节点投票,被隔离的少数派无法选举新主节点。

46. 🔵 什么是Redis的Bitmap?有哪些实际应用场景?

答:Bitmap:基于String类型的位操作,每个bit存储0或1。

基本命令:

1
2
3
4
5
SETBIT key offset value    # 设置指定位
GETBIT key offset # 获取指定位
BITCOUNT key [start end] # 统计值为1的位数
BITOP AND/OR/XOR/NOT destkey key1 key2 # 位运算
BITPOS key bit [start end] # 查找第一个0或1的位置

应用场景:

  1. 用户签到

    • key:sign:{userId}:{yyyyMM}
    • offset:日期(1-31)
    • SETBIT sign:1001:202401 15 1(用户1001在1月15日签到)
    • BITCOUNT sign:1001:202401(统计1月签到天数)
    • 内存:每个用户每月只需4字节。
  2. 用户在线状态

    • key:online:{yyyyMMdd}
    • offset:userId
    • SETBIT online:20240101 1001 1(用户1001在线)
    • BITCOUNT online:20240101(统计在线用户数)
    • 1亿用户只需12MB内存。
  3. 活跃用户统计

    • 每天一个Bitmap记录活跃用户。
    • BITOP AND result day1 day2 day3(连续3天都活跃的用户)。
    • BITOP OR result day1 day2 … day7(7天内活跃过的用户)。
  4. 特征标记

    • 用户是否完成新手引导、是否开启通知等布尔特征。
    • 每个特征一个bit,一个Bitmap存储所有特征。
  5. 布隆过滤器

    • 手动实现布隆过滤器的底层位数组。

内存效率:1亿个布尔值只需12MB(1亿bit ÷ 8 = 12.5MB)。如果用String存储(每个key约50字节),需要5GB。

47. 🔴 什么是Redis的HyperLogLog?它的原理是什么?误差率如何?

答:HyperLogLog:概率型数据结构,用于基数估算(统计不重复元素的数量)。

原理(简化版):

  • 基于概率统计:对元素进行哈希,观察哈希值的二进制表示中前导零的最大数量。
  • 直觉:如果观察到的最大前导零数量为k,则估算基数约为2^k。
  • HyperLogLog使用多个桶(Redis使用16384个桶)分别统计,取调和平均值,提高精度。

Redis实现:

  • 每个HyperLogLog使用12KB固定内存(16384个桶 × 6bit/桶)。
  • 小基数时使用稀疏编码(Sparse),内存更小。基数增大后自动转为密集编码(Dense,12KB)。

命令:

1
2
3
PFADD key element1 element2 ...  # 添加元素
PFCOUNT key # 获取基数估算
PFMERGE destkey key1 key2 # 合并多个HyperLogLog

误差率:标准误差0.81%。即实际基数100万时,估算值在99.19万-100.81万之间。

应用场景:

  1. UV统计:统计网站/页面的独立访客数。
  2. 搜索关键词去重计数:统计不同搜索关键词的数量。
  3. 社交网络:统计用户的独立好友数、独立互动用户数。

与Set对比:

  • Set存储100万个用户ID需要约50MB内存。
  • HyperLogLog只需要12KB,但只能获取近似基数,不能获取具体元素。

适用条件:只需要知道”有多少个不同的元素”,不需要知道”具体是哪些元素”,且可以接受0.81%的误差。

48. 🔴 如何设计一个基于Redis的延迟队列?

答:延迟队列:消息在指定时间后才被消费。Redis没有原生延迟队列,需要自行实现。

方案1:ZSet实现(最常用)

1
2
3
4
5
6
7
# 生产者:将消息加入ZSet,score为执行时间戳
ZADD delay_queue <execute_timestamp> <message_id>

# 消费者:轮询获取到期的消息
ZRANGEBYSCORE delay_queue 0 <current_timestamp> LIMIT 0 10
# 处理消息后删除
ZREM delay_queue <message_id>

Lua脚本保证原子性(获取+删除):

1
2
3
4
5
6
7
local messages = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1], 'LIMIT', 0, ARGV[2])
if #messages > 0 then
for i, msg in ipairs(messages) do
redis.call('ZREM', KEYS[1], msg)
end
end
return messages

方案2:Redis Stream + 消费者轮询

  • 消息写入Stream,消费者定时检查是否有到期消息。
  • 不如ZSet方案直观。

方案3:Redis Keyspace Notification

  • 利用key过期事件通知:设置一个key,TTL为延迟时间。key过期时Redis发送通知。
  • 配置:notify-keyspace-events Ex
  • 问题:通知不可靠(Pub/Sub语义,消费者不在线则丢失)。不推荐生产使用。

ZSet方案的优化:

  1. 多消费者竞争:多个消费者同时ZRANGEBYSCORE可能获取相同消息。用Lua脚本原子获取+删除,或用分布式锁。
  2. 消息持久化:消息详情存储在Hash中(HSET msg:{id} …),ZSet只存储消息ID和执行时间。
  3. 失败重试:消费失败的消息重新加入ZSet,score设为下次重试时间。
  4. 监控:监控ZSet大小(ZCARD),过大说明消费能力不足。

生产建议:简单场景用Redis ZSet延迟队列。复杂场景(大量延迟消息、高可靠性要求)用RocketMQ延迟消息或专门的延迟队列服务。

49. 🔵 什么是Redis的Cluster Bus?节点间通信的协议是怎样的?

答:Cluster Bus是Redis Cluster节点间通信的内部通道,使用独立的端口(数据端口+10000,如6379→16379)。

通信协议:

  • 基于TCP的二进制协议(不是RESP协议)。
  • 使用Gossip协议交换集群状态信息。

消息类型:

  1. PING/PONG:心跳消息。每个节点每秒随机选择几个节点发送PING,收到PING的节点回复PONG。PING/PONG消息携带:
    • 发送节点的信息(ID、IP、端口、负责的槽等)。
    • 发送节点知道的部分其他节点的信息(Gossip Section,随机选择几个节点的信息)。
  2. MEET:邀请新节点加入集群。CLUSTER MEET ip port
  3. FAIL:某个节点被标记为FAIL时,广播给所有节点。
  4. PUBLISH:Pub/Sub消息在集群内广播。
  5. FAILOVER_AUTH_REQUEST/ACK:从节点发起故障转移选举时的投票请求和响应。
  6. UPDATE:槽分配变更通知。

Gossip协议的特点:

  • 最终一致:信息通过多轮Gossip传播,最终所有节点收敛到一致状态。
  • 传播速度:O(log N)轮(N是节点数)。100个节点约7轮(每秒1轮)即可传播到所有节点。
  • 带宽开销:每个PING/PONG消息约2KB+(取决于携带的节点信息数量)。节点越多,Gossip消息越大。

cluster-node-timeout:节点超时时间(默认15秒)。超过此时间未收到某节点的PONG,标记为PFAIL。影响故障检测速度和Gossip频率。

50. ⚫ 如果让你设计一个分布式缓存系统,你会如何设计?需要考虑哪些核心问题?

答:这是一道开放性架构设计题。

核心设计决策:

  1. 数据模型

    • 只支持key-value(如Memcached,简单高效)?还是支持丰富的数据结构(如Redis)?
    • 数据结构越丰富,实现越复杂,但应用场景越广。
  2. 内存管理

    • 内存分配器:jemalloc(Redis)、slab(Memcached)、tcmalloc。
    • 淘汰策略:LRU/LFU/FIFO/Random。
    • 内存碎片处理:碎片整理或定期重启。
  3. 持久化

    • 是否需要持久化?纯缓存不需要,但Redis的持久化使其可以作为数据库使用。
    • 快照(RDB)vs 日志(AOF)vs 混合。
  4. 分布式

    • 数据分片:一致性哈希 vs 哈希槽。
    • 副本:主从复制,同步还是异步?
    • 故障转移:自动还是手动?
    • 一致性:强一致(Raft)还是最终一致(Gossip)?
  5. 网络模型

    • 单线程+IO多路复用(Redis)vs 多线程(Memcached)。
    • 协议:文本协议(简单调试)vs 二进制协议(高效)。
  6. 高可用

    • 主从复制 + 自动故障转移。
    • 跨数据中心复制。
  7. 可观测性

    • 慢查询日志、内存使用统计、命中率统计。
    • 监控指标暴露(Prometheus格式)。
  8. 安全

    • 认证(密码/ACL)。
    • 加密(TLS)。
    • 命令限制(禁止危险命令)。

好的回答应该展示对这些权衡的理解,以及根据具体场景做出合理选择的能力。


三、Redis高级特性与生产实践(51-75题)

51. 🔴 Redis的fork操作为什么可能导致延迟?如何优化?

答:Redis在BGSAVE(RDB持久化)和BGREWRITEAOF(AOF重写)时需要fork子进程。

fork的延迟来源:

  1. 页表复制:fork时OS需要复制父进程的页表(虚拟地址→物理地址的映射)。页表大小与进程使用的内存成正比。10GB内存的Redis实例,页表约20MB,fork耗时约20-50ms。
  2. Copy-on-Write(COW):fork后父子进程共享物理内存页。父进程修改数据时触发COW,复制被修改的内存页。写入密集时COW导致内存使用量翻倍。

优化方案:

  1. 控制实例内存大小:单实例内存建议不超过10GB。内存越大fork越慢。
  2. 使用大页内存(Huge Pages):Linux的Transparent Huge Pages(THP)会导致COW时复制2MB的大页(而非4KB的小页),内存开销和延迟都增大。建议关闭THP:echo never > /sys/kernel/mm/transparent_hugepage/enabled
  3. 减少fork频率:调整RDB的save规则,减少自动BGSAVE频率。使用AOF+混合持久化替代频繁的RDB。
  4. 无盘复制repl-diskless-sync yes,主从复制时不生成RDB文件,直接通过Socket发送。减少一次fork(但仍需要fork来序列化数据)。
  5. 监控fork耗时INFO persistence中的latest_fork_usec(最近一次fork的耗时,微秒)。超过100ms需要关注。

52. 🔵 什么是Redis的Lazy Free机制?它解决了什么问题?

答:Lazy Free(Redis 4.0+):将耗时的内存释放操作放到后台线程异步执行,避免阻塞主线程。

解决的问题:

  • DEL一个大Key(如包含百万元素的Hash)时,释放内存需要遍历所有元素,可能阻塞主线程数秒。
  • 过期key的删除、内存淘汰时删除大Key同样会阻塞。

Lazy Free命令:

  • UNLINK key:替代DEL,异步删除key。主线程只是将key从keyspace中移除(O(1)),实际内存释放由后台线程完成。
  • FLUSHALL ASYNC/FLUSHDB ASYNC:异步清空数据库。

自动Lazy Free配置:

1
2
3
4
5
lazyfree-lazy-eviction yes      # 内存淘汰时异步删除
lazyfree-lazy-expire yes # 过期key异步删除
lazyfree-lazy-server-del yes # 隐式删除(如RENAME覆盖旧key)异步执行
lazyfree-lazy-user-del yes # DEL命令也异步执行(等同于UNLINK)
lazyfree-lazy-user-flush yes # FLUSHALL/FLUSHDB默认异步

实现原理:

  • 主线程将待释放的对象放入一个队列(bio_lazy_free)。
  • 后台线程(bio线程)从队列中取出对象,执行实际的内存释放。
  • 后台线程不影响主线程的命令执行。

注意:Lazy Free只对大对象有意义。小对象(如简单String)的释放本身就很快,异步反而增加了队列操作的开销。Redis内部会判断对象大小,小对象仍然同步释放。

53. 🔴 Redis的RESP协议是什么?RESP2和RESP3有什么区别?

答:RESP(Redis Serialization Protocol):Redis客户端和服务端之间的通信协议。

RESP2(Redis 2.0-5.x默认):

  • 文本协议,简单易读。
  • 数据类型:
    • Simple String:+OK\r\n
    • Error:-ERR unknown command\r\n
    • Integer::1000\r\n
    • Bulk String:$6\r\nfoobar\r\n(长度前缀)
    • Array:*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
  • 限制:类型信息不足(如HGETALL返回的是Array,客户端需要自己解析为Map)。

RESP3(Redis 6.0+):

  • 新增数据类型:
    • Map:%2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n$3\r\nbaz\r\n$3\r\nqux\r\n(HGETALL直接返回Map)。
    • Set:~2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
    • Boolean:#t\r\n#f\r\n
    • Double:,3.14\r\n
    • Null:_\r\n
    • Push:>2\r\n...(服务端主动推送,用于Client-side Caching的失效通知)。
  • 优势:
    1. 类型信息更丰富,客户端不需要猜测返回值类型。
    2. 支持Push消息(Client-side Caching)。
    3. 支持Attribute(附加元数据)。

切换到RESP3:客户端发送HELLO 3命令切换到RESP3协议。默认仍然是RESP2(向后兼容)。

54. 🔵 什么是Redis的Keyspace Notification?有什么应用场景?

答:Keyspace Notification:Redis在key发生特定事件时发送通知(通过Pub/Sub)。

配置:notify-keyspace-events参数,指定监听的事件类型:

  • K:Keyspace事件(keyspace@:key,按key通知)
  • E:Keyevent事件(keyevent@:event,按事件类型通知)
  • g:通用命令(DEL、EXPIRE、RENAME等)
  • $:String命令
  • l:List命令
  • s:Set命令
  • h:Hash命令
  • z:ZSet命令
  • x:过期事件
  • e:驱逐事件(内存淘汰)
  • A:所有事件(g$lshzxe的别名)

示例:

1
2
3
4
5
# 配置监听过期事件
CONFIG SET notify-keyspace-events Ex

# 订阅过期事件
SUBSCRIBE __keyevent@0__:expired

应用场景:

  1. 订单超时取消:创建订单时设置key的TTL为30分钟。key过期时收到通知,触发订单取消逻辑。
  2. 缓存失效通知:key被删除或过期时通知应用层更新本地缓存。
  3. 数据变更监听:监听特定key的修改事件,触发业务逻辑。

局限性:

  1. 不可靠:基于Pub/Sub,如果订阅者不在线,通知丢失。
  2. 性能开销:开启通知会增加Redis的CPU和内存开销。
  3. Cluster模式:通知只在key所在的节点发送,订阅者需要连接所有节点。
  4. 过期事件不精确:Redis的过期删除是惰性+定期的,过期事件可能延迟触发。

生产建议:不要依赖Keyspace Notification做关键业务逻辑。订单超时等场景推荐使用延迟队列(ZSet方案或RocketMQ延迟消息)。

55. 🔴 什么是Redis的内存回收机制?jemalloc的工作原理是什么?

答:Redis使用jemalloc作为默认内存分配器(也支持libc malloc和tcmalloc)。

jemalloc的核心设计:

  1. Arena:jemalloc将内存分为多个Arena(默认CPU核心数×4个),每个线程绑定一个Arena,减少锁竞争。Redis是单线程,主要使用一个Arena。
  2. Size Class:内存按固定大小分类(8/16/32/48/64/80/96/112/128/…字节)。分配时向上取整到最近的Size Class。如请求100字节,分配112字节。
  3. Slab:相同Size Class的内存块组成Slab(连续内存页)。分配时从Slab中取一个空闲块,释放时归还到Slab。
  4. Page Run:大内存(>14KB)直接从Page Run分配(连续的内存页)。

内存碎片的产生:

  • 内部碎片:分配的Size Class大于实际需要(如需要100字节分配112字节,浪费12字节)。
  • 外部碎片:释放的内存块不连续,无法合并为大块。频繁的分配/释放不同大小的内存块会加剧外部碎片。

Redis的内存回收:

  • DEL/UNLINK删除key时,释放key和value占用的内存(归还给jemalloc)。
  • jemalloc不一定将内存归还给OS(保留在jemalloc的空闲列表中,供后续分配使用)。
  • MEMORY PURGE:强制jemalloc将空闲内存归还给OS(调用jemalloc的arena.purge)。
  • used_memory(jemalloc分配的内存)和used_memory_rss(OS分配给Redis的物理内存)的差值反映了jemalloc保留的空闲内存和碎片。

56. 🔵 什么是Redis的Sentinel和Cluster的区别?如何选择?

答:Sentinel和Cluster是Redis的两种高可用方案,定位不同。

维度 Sentinel Cluster
定位 高可用(主从故障转移) 高可用 + 数据分片
数据分片 不支持(单主库存储所有数据) 支持(16384个哈希槽)
容量 受单机内存限制 可水平扩展(多主节点)
写入能力 单主库写入 多主节点并行写入
架构 主从 + Sentinel监控 多主多从,自治
客户端 通过Sentinel获取主库地址 需要支持Cluster协议
运维复杂度 较低 较高
跨槽操作 无限制 受限(需要Hash Tag)

选择建议:

  • Sentinel:数据量不大(单机内存可以容纳)、不需要水平扩展写入能力、希望运维简单。典型场景:缓存、Session、小规模业务。
  • Cluster:数据量大(超过单机内存)、需要水平扩展写入能力、高吞吐量要求。典型场景:大规模缓存、排行榜、计数器。

混合方案:

  • 小规模:单实例Redis(不需要高可用)或Sentinel(需要高可用)。
  • 中规模:Cluster(3主3从)。
  • 大规模:多个Cluster集群(按业务拆分)。

57. 🔴 什么是Redis的WAIT命令?如何实现强一致性读?

答:WAIT命令:阻塞当前客户端,直到之前的写命令被指定数量的从库确认。

语法:WAIT numreplicas timeout

  • numreplicas:等待确认的从库数量。
  • timeout:超时时间(毫秒),0表示无限等待。
  • 返回值:在超时前确认的从库数量。

使用场景:写入后需要立即从从库读取最新数据(强一致性读)。

1
2
3
SET key value
WAIT 1 5000 # 等待至少1个从库确认,超时5秒
# 此时从从库读取key一定能读到最新值

注意事项:

  1. WAIT只保证数据已发送到从库,不保证从库已执行(从库可能还在执行队列中)。但实际上从库执行速度很快,几乎等同于已执行。
  2. WAIT会阻塞客户端,增加延迟。不适合高频使用。
  3. 如果从库全部宕机,WAIT会阻塞到超时。
  4. WAIT不是事务性的,只保证WAIT之前的命令已同步。

强一致性读的其他方案:

  1. 读主库:关键读操作直接读主库。最简单但增加主库压力。
  2. WAIT + 读从库:写入后WAIT确认,再从从库读取。
  3. 版本号方案:写入时记录版本号,读取时检查从库的版本号是否最新。

58. 🔵 什么是Redis的Module(模块)?有哪些常用的Redis模块?

答:Redis Module(4.0+):允许通过C语言编写扩展模块,为Redis添加新的数据类型和命令。

常用模块:

  1. RedisJSON:原生JSON数据类型,支持JSON路径查询和部分更新。
1
2
3
JSON.SET user:1001 $ '{"name":"张三","age":30,"address":{"city":"北京"}}'
JSON.GET user:1001 $.name # 获取name字段
JSON.NUMINCRBY user:1001 $.age 1 # age加1
  1. RediSearch:全文搜索引擎,支持索引、查询、聚合。
1
2
FT.CREATE idx:users ON HASH PREFIX 1 user: SCHEMA name TEXT age NUMERIC
FT.SEARCH idx:users "@name:张三 @age:[25 35]"
  1. RedisBloom:概率型数据结构(布隆过滤器、Cuckoo过滤器、Count-Min Sketch、Top-K)。

  2. RedisTimeSeries:时间序列数据类型,支持自动降采样、聚合查询。适合IoT、监控数据。

  3. RedisGraph:图数据库模块,支持Cypher查询语言。

  4. RedisAI:在Redis中运行机器学习模型(TensorFlow/PyTorch/ONNX)。

Redis Stack:Redis官方将常用模块打包为Redis Stack(包含RedisJSON、RediSearch、RedisBloom、RedisTimeSeries等),提供开箱即用的体验。

模块开发:使用Redis Module API(C语言),可以:

  • 定义新的数据类型(自定义编码和存储)。
  • 注册新的命令。
  • 访问Redis的内部数据结构。
  • 使用Redis的事件循环和IO。

59. 🔴 什么是Redis的Cluster Proxy?为什么需要它?

答:Redis Cluster Proxy:在客户端和Redis Cluster之间的代理层,让不支持Cluster协议的客户端也能使用Cluster。

为什么需要:

  1. 客户端兼容性:部分Redis客户端(尤其是非Java语言)不完整支持Cluster协议(MOVED/ASK重定向)。
  2. 简化客户端:客户端不需要维护槽映射缓存,不需要处理重定向。
  3. 跨槽操作:代理可以拆分跨槽的MGET/MSET等命令,分别发送到不同节点,合并结果返回。
  4. 连接管理:代理维护与所有Cluster节点的连接池,客户端只需要连接代理。

方案:

  1. redis-cluster-proxy(官方,已停止维护):Redis官方的Cluster代理,C语言实现。

  2. Twemproxy:Twitter开源,支持一致性哈希分片。不支持Cluster协议,但可以作为分片代理。不支持在线扩缩容。

  3. Codis:豌豆荚开源,支持在线扩缩容、Dashboard管理。基于ZooKeeper/etcd做元数据管理。国内使用较多。

  4. Predixy:高性能Redis代理,支持Sentinel和Cluster模式。C++实现,性能接近直连。

  5. Envoy/HAProxy:通用代理,支持Redis协议。适合Service Mesh场景。

选型建议:

  • 如果客户端支持Cluster协议(如Jedis、Lettuce、redis-py),直连Cluster(性能最好)。
  • 如果需要代理,Predixy性能好,Codis功能全(但已不太活跃)。
  • 云环境考虑云厂商提供的代理方案(如AWS ElastiCache Proxy)。

60. 🔵 什么是Redis的OBJECT命令?如何查看key的内部编码和内存使用?

答:OBJECT命令用于查看key的内部信息。

常用子命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看key的内部编码
OBJECT ENCODING key
# 返回值:int/embstr/raw/ziplist/listpack/quicklist/hashtable/intset/skiplist等

# 查看key的引用计数(用于调试)
OBJECT REFCOUNT key

# 查看key的空闲时间(最后一次访问距今的秒数)
OBJECT IDLETIME key
# 注意:只有maxmemory-policy为LRU相关策略时才有意义

# 查看key的访问频率(LFU策略下)
OBJECT FREQ key

# 查看key的帮助信息
OBJECT HELP

MEMORY命令(Redis 4.0+):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查看key的内存使用(字节,包含key本身、value、元数据的开销)
MEMORY USAGE key [SAMPLES count]
# SAMPLES:对集合类型采样计算(默认5),0表示精确计算(可能慢)

# 内存使用统计
MEMORY STATS

# 内存诊断建议
MEMORY DOCTOR

# 强制释放空闲内存给OS
MEMORY PURGE

# 设置内存分配器
MEMORY MALLOC-STATS

实际用途:

  1. 排查大Key:MEMORY USAGE key查看具体key的内存占用。
  2. 验证编码优化:OBJECT ENCODING key确认key使用了预期的紧凑编码。
  3. 分析热点:OBJECT FREQ key查看key的访问频率(需要LFU策略)。
  4. 排查内存问题:MEMORY DOCTOR获取内存优化建议。

61. 🔴 如何设计一个基于Redis的全局唯一ID生成器?

答:全局唯一ID是分布式系统的基础需求。Redis可以作为ID生成器的核心组件。

方案1:Redis INCR(最简单)

1
INCR global_id  # 返回递增的唯一ID
  • 优点:简单、有序、性能高(10万+ QPS)。
  • 缺点:单点依赖Redis,ID连续可能暴露业务量。

方案2:Redis + 号段模式(推荐)

  • 应用启动时从Redis获取一个号段(如INCRBY global_id 1000,获取1000个ID)。
  • 应用在本地内存中分配ID,号段用完再从Redis获取下一个号段。
  • 优点:减少Redis访问频率,本地分配性能极高。Redis宕机时本地号段仍可用。
  • 缺点:应用重启时可能浪费部分ID(号段未用完)。

方案3:Redis + 时间戳 + 序列号(类Snowflake)

1
2
3
4
5
6
-- Lua脚本:生成时间戳+序列号的ID
local timestamp = redis.call('TIME')
local sec = timestamp[1]
local seq = redis.call('INCR', 'seq:' .. sec)
redis.call('EXPIRE', 'seq:' .. sec, 2)
return sec * 10000 + seq -- 每秒最多10000个ID
  • 优点:ID包含时间信息,可以按时间排序。
  • 缺点:依赖Redis时钟。

方案4:Snowflake算法(不依赖Redis)

  • 64位ID = 1位符号位 + 41位时间戳 + 10位机器ID + 12位序列号。
  • 每毫秒每台机器可生成4096个ID。
  • 优点:不依赖外部存储,本地生成。
  • 缺点:需要解决机器ID分配问题(可以用Redis分配机器ID)。

生产建议:中小规模用Redis INCR或号段模式。大规模用Snowflake(本地生成,无网络开销)。美团Leaf、百度UidGenerator是成熟的开源方案。

62. 🔵 什么是Redis的SCAN命令?它和KEYS命令有什么区别?

答:SCAN是Redis的增量迭代命令,用于安全地遍历key。

KEYS的问题:

  • KEYS pattern:遍历所有key,时间复杂度O(n)。
  • 在key数量多时(百万级)阻塞主线程数秒,导致其他命令超时。
  • 生产环境禁止使用KEYS命令。

SCAN的优势:

  • 增量迭代,每次只返回少量key(默认约10个)。
  • 不阻塞主线程(每次迭代只扫描少量数据)。
  • 使用游标(cursor)记录迭代位置。

使用方式:

1
2
3
4
SCAN 0 MATCH user:* COUNT 100
# 返回:1) "17"(下一个游标) 2) ["user:1", "user:2", ...]
SCAN 17 MATCH user:* COUNT 100
# 返回:1) "0"(游标为0表示迭代完成) 2) [...]

SCAN家族:

  • SCAN:遍历所有key。
  • HSCAN:遍历Hash的field。
  • SSCAN:遍历Set的member。
  • ZSCAN:遍历ZSet的member。

注意事项:

  1. 不保证完整性:迭代过程中新增或删除的key可能被遗漏或重复返回。
  2. COUNT是建议值:实际返回数量可能多于或少于COUNT。
  3. MATCH在返回后过滤:SCAN先获取COUNT个key,再用MATCH过滤。如果匹配率低,可能返回空数组但游标不为0(需要继续迭代)。
  4. 时间复杂度:单次SCAN是O(COUNT),完整遍历是O(n)。但分散在多次调用中,不会阻塞。

63. 🔴 Redis在微服务架构中有哪些典型应用?

答:Redis在微服务架构中扮演多种角色。

  1. 分布式缓存:最核心的应用。缓存数据库查询结果、API响应、计算结果。减少数据库压力,降低响应延迟。

  2. 分布式Session:Spring Session + Redis,多实例共享用户Session。

  3. 分布式锁:Redisson实现的分布式锁,保证分布式环境下的互斥操作。

  4. 消息队列:Redis Stream或List实现轻量级消息队列。适合简单的异步任务,不需要引入Kafka/RocketMQ。

  5. 限流:基于Redis的分布式限流(令牌桶、滑动窗口),保护下游服务。

  6. 服务发现/配置中心:Redis Pub/Sub通知配置变更,Hash存储配置数据。(简单场景,复杂场景用Nacos/Consul)

  7. 幂等性保证:用SETNX记录已处理的请求ID,防止重复处理。

  8. 排行榜/计数器:ZSet实现排行榜,INCR实现计数器。

  9. 布隆过滤器:防止缓存穿透,去重。

  10. 地理位置:GEO功能实现附近的人/商家。

微服务中Redis的最佳实践:

  • 每个微服务使用独立的Redis实例或独立的key前缀(命名空间隔离)。
  • 统一的Redis客户端封装(连接池管理、序列化、异常处理)。
  • 监控每个微服务的Redis使用情况(QPS、内存、慢查询)。
  • 避免微服务之间通过Redis共享数据(违反微服务的独立性原则)。

64. 🔴 什么是Redis的连接池?如何正确配置Jedis/Lettuce的连接池?

答:连接池:预先创建一组Redis连接,复用连接避免频繁创建/销毁的开销。

Jedis连接池(JedisPool,基于Apache Commons Pool):

1
2
3
4
5
6
7
8
9
10
11
12
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(50); // 最大连接数
config.setMaxIdle(20); // 最大空闲连接数
config.setMinIdle(5); // 最小空闲连接数
config.setMaxWaitMillis(3000); // 获取连接最大等待时间
config.setTestOnBorrow(true); // 获取连接时检测有效性
config.setTestWhileIdle(true); // 空闲时检测有效性

JedisPool pool = new JedisPool(config, "redis-host", 6379, 2000, "password");
try (Jedis jedis = pool.getResource()) {
jedis.set("key", "value");
}

Lettuce连接(基于Netty,默认共享连接):

1
2
3
4
5
6
7
8
9
10
// Lettuce默认使用单连接+多路复用(不需要连接池)
RedisClient client = RedisClient.create("redis://password@host:6379");
StatefulRedisConnection<String, String> connection = client.connect();
RedisCommands<String, String> commands = connection.sync();
commands.set("key", "value");

// 如果需要连接池(高并发阻塞命令场景)
GenericObjectPoolConfig<StatefulRedisConnection<String, String>> poolConfig = new GenericObjectPoolConfig<>();
poolConfig.setMaxTotal(50);
GenericObjectPool<StatefulRedisConnection<String, String>> pool = ConnectionPoolSupport.createGenericObjectPool(client::connect, poolConfig);

Jedis vs Lettuce:

维度 Jedis Lettuce
线程安全 不安全(需要连接池) 安全(共享连接)
底层 BIO(阻塞IO) Netty(NIO)
连接模型 连接池 单连接多路复用
异步支持 不支持 支持(异步/响应式)
Cluster支持 支持 支持(更完善)
Spring Boot默认 2.0之前 2.0之后

连接池大小计算:

  • 公式:连接数 = 业务线程数 × 每个线程的Redis操作时间 / Redis命令平均响应时间。
  • 经验值:一般20-50个连接足够。过多的连接反而增加Redis的客户端管理开销。

65. 🔵 什么是Redis的慢查询日志和延迟监控?如何使用?

答:Redis提供慢查询日志和延迟监控两个工具来排查性能问题。

慢查询日志(Slowlog):

  • 记录执行时间超过阈值的命令。
  • 注意:只记录命令执行时间,不包括网络传输和排队等待时间。
1
2
3
4
5
6
CONFIG SET slowlog-log-slower-than 1000  # 阈值1ms(微秒)
CONFIG SET slowlog-max-len 1000 # 最多保留1000条

SLOWLOG GET 10 # 查看最近10条慢查询
SLOWLOG LEN # 慢查询日志条数
SLOWLOG RESET # 清空

延迟监控(Latency Monitoring,Redis 2.8.13+):

  • 记录各种延迟事件(命令执行、fork、AOF写入等)。
1
2
3
4
5
6
CONFIG SET latency-monitor-threshold 100  # 监控超过100ms的事件

LATENCY LATEST # 查看最近的延迟事件
LATENCY HISTORY event-name # 查看某类事件的历史
LATENCY RESET # 清空
LATENCY GRAPH event-name # ASCII图形展示延迟趋势

延迟事件类型:

  • command:命令执行延迟。
  • fast-command:O(1)/O(log n)命令的延迟。
  • fork:fork操作延迟。
  • aof-write:AOF写入延迟。
  • aof-fsync-always:AOF fsync延迟。
  • aof-rewrite:AOF重写延迟。
  • rdb-save:RDB保存延迟。
  • expire-cycle:过期key清理延迟。
  • eviction-cycle:内存淘汰延迟。

redis-cli延迟测试:

1
2
3
4
redis-cli --latency          # 持续测量延迟
redis-cli --latency-history # 每15秒输出一次延迟统计
redis-cli --latency-dist # 延迟分布图
redis-cli --intrinsic-latency 10 # 测量系统固有延迟(10秒)

66. 🔴 如何设计Redis的容灾方案?跨机房/跨地域的Redis架构如何设计?

答:Redis容灾需要考虑单机房故障和跨地域灾备。

单机房高可用:

  • Sentinel模式:1主2从+3 Sentinel,主库故障自动切换。
  • Cluster模式:3主3从,分布在不同机架。

跨机房方案:

  1. 主从跨机房

    • 主库在机房A,从库在机房B。
    • 正常时机房A读写,机房B只读。
    • 机房A故障时,手动或自动将机房B的从库提升为主库。
    • 问题:跨机房复制延迟(毫秒到秒级),切换时可能丢失少量数据。
  2. 双活方案(Active-Active)

    • 两个机房各有独立的Redis集群。
    • 应用层双写:写操作同时写入两个机房的Redis。
    • 问题:双写一致性难保证(网络延迟、部分失败)。
    • 适合:缓存场景(允许短暂不一致)。
  3. 基于复制的双活

    • 使用Redis Enterprise的Active-Active Geo-Distribution(CRDT技术)。
    • 两个机房的Redis集群自动双向同步,冲突通过CRDT(Conflict-free Replicated Data Types)自动解决。
    • 优点:自动冲突解决,应用无感知。
    • 缺点:需要Redis Enterprise商业版。
  4. 基于中间件的同步

    • 使用开源工具(如redis-shake、DTS)实现跨机房数据同步。
    • redis-shake支持全量+增量同步,支持单向和双向同步。

容灾切换流程:

  1. 检测故障(监控告警)。
  2. 评估数据丢失量(复制延迟)。
  3. 切换DNS/VIP指向备机房。
  4. 验证备机房Redis数据完整性。
  5. 通知应用层切换。
  6. 故障恢复后,反向同步数据。

67. 🔵 什么是Redis的RESP3协议中的Push消息?它有什么应用?

答:RESP3的Push消息:服务端主动向客户端推送消息,无需客户端请求。

Push消息格式:>count\r\n...(以>开头,区别于普通响应)。

应用场景:

  1. Client-side Caching失效通知

    • 客户端开启Tracking后,Redis在key变更时通过Push消息通知客户端。
    • 客户端收到通知后删除本地缓存。
    • 不需要客户端轮询或订阅Pub/Sub。
  2. Pub/Sub消息

    • RESP3中Pub/Sub消息以Push消息形式发送。
    • 客户端可以在同一个连接上同时执行命令和接收Pub/Sub消息(RESP2中订阅后连接只能接收消息)。
  3. 集群状态变更通知(未来可能):

    • 槽迁移、节点上下线等事件通过Push消息通知客户端。

RESP2的限制:

  • 没有Push消息机制。
  • Client-side Caching需要通过额外的Pub/Sub连接接收失效通知(浪费一个连接)。
  • Pub/Sub订阅后连接进入”订阅模式”,不能执行其他命令。

RESP3的优势:

  • 单连接同时支持命令执行和Push消息接收。
  • 减少连接数,简化客户端实现。

客户端支持:Lettuce 6.0+支持RESP3。Jedis目前不支持RESP3。

68. 🔴 什么是Redis的数据迁移?有哪些迁移工具和方案?

答:Redis数据迁移场景:版本升级、架构变更(单机→Cluster)、云迁移、跨机房迁移。

迁移工具:

  1. redis-shake(阿里开源,推荐)

    • 支持:单机→单机、单机→Cluster、Cluster→Cluster、RDB文件导入。
    • 模式:全量同步(RDB)+ 增量同步(AOF/复制流)。
    • 支持数据过滤(按key前缀、按数据库)。
    • 支持在线迁移(不停机)。
  2. redis-cli –rdb/–pipe

    • redis-cli --rdb dump.rdb:导出RDB文件。
    • redis-cli --pipe:批量导入数据(使用Redis协议格式)。
    • 适合离线迁移。
  3. MIGRATE命令

    • 将单个key从一个Redis实例迁移到另一个实例(原子操作)。
    • Cluster的槽迁移内部使用MIGRATE。
    • 不适合大规模迁移(逐key操作,效率低)。
  4. SLAVEOF/REPLICAOF

    • 将目标Redis设为源Redis的从库,全量+增量同步。
    • 同步完成后断开复制关系,目标Redis成为独立主库。
    • 简单但只支持单机→单机。
  5. 云厂商DTS(Data Transmission Service)

    • AWS DMS、阿里云DTS等,支持Redis迁移。
    • 托管服务,无需自己部署工具。

迁移注意事项:

  1. 数据一致性验证:迁移后对比源和目标的key数量、抽样对比value。
  2. 大Key处理:大Key迁移可能阻塞源Redis。提前拆分大Key。
  3. 带宽控制:迁移流量可能影响正常业务。控制迁移速率。
  4. 版本兼容:不同Redis版本的RDB格式可能不兼容。确认目标版本支持源版本的RDB。
  5. 切换方案:DNS切换或客户端配置切换。准备回滚方案。

69. 🔴 什么是Redis的CRDT(Conflict-free Replicated Data Types)?它如何解决多活场景的冲突?

答:CRDT是一种特殊的数据结构,保证在多个副本并发修改时自动收敛到一致状态,无需协调。

CRDT在Redis中的应用(Redis Enterprise Active-Active):

  • 多个地理分布的Redis集群可以同时接受写入。
  • 冲突通过CRDT规则自动解决,不需要人工干预。

CRDT类型和冲突解决规则:

  1. Counter(计数器):使用G-Counter(只增计数器)或PN-Counter(增减计数器)。每个副本维护自己的增量,合并时取各副本增量之和。

    • 例:副本A执行INCR 3,副本B执行INCR 5,合并后值为8。
  2. String(Last Write Wins):使用时间戳,最后写入的值胜出。

    • 例:副本A在T1设置value=”a”,副本B在T2设置value=”b”(T2>T1),合并后value=”b”。
  3. Set(OR-Set):添加操作总是生效,删除操作只删除已知的元素。

    • 例:副本A添加元素x,副本B删除元素x(但B不知道A添加了x),合并后x存在(添加优先)。
  4. Hash:每个field独立使用LWW(Last Write Wins)。

  5. Sorted Set:score使用LWW,member使用OR-Set语义。

局限性:

  1. CRDT只能解决特定类型的冲突,复杂业务逻辑的冲突仍需要应用层处理。
  2. LWW依赖时钟同步,时钟偏差可能导致非预期的结果。
  3. 只有Redis Enterprise支持,开源Redis不支持。

开源替代:

  • 应用层双写 + 冲突检测 + 人工/自动解决。
  • 使用消息队列保证写入顺序(牺牲多活的写入能力)。

70. 🔵 什么是Redis的DEBUG命令?生产环境为什么要禁用?

答:DEBUG命令是Redis的调试工具,提供多种内部调试功能。

常用DEBUG子命令:

  • DEBUG SLEEP seconds:让Redis主线程睡眠指定秒数(阻塞所有命令)。
  • DEBUG SET-ACTIVE-EXPIRE 0/1:开启/关闭主动过期。
  • DEBUG OBJECT key:查看key的内部信息(编码、引用计数、序列化长度等)。
  • DEBUG RELOAD:重新加载RDB文件(阻塞)。
  • DEBUG LOADAOF:重新加载AOF文件。
  • DEBUG CHANGE-REPL-ID:更改复制ID。
  • DEBUG QUICKLIST-PACKED-THRESHOLD:调整quicklist的压缩阈值。
  • DEBUG OOM:模拟OOM。
  • DEBUG SEGFAULT:触发段错误(崩溃Redis)。

生产环境禁用的原因:

  1. DEBUG SLEEP:阻塞主线程,导致所有客户端超时。
  2. DEBUG SEGFAULT:直接崩溃Redis进程。
  3. DEBUG RELOAD:阻塞式重新加载数据,期间不可用。
  4. 安全风险:恶意用户可以利用DEBUG命令破坏Redis。

禁用方式:

1
2
3
4
5
# ACL禁止DEBUG命令
ACL SETUSER default -debug

# 或在配置文件中
rename-command DEBUG "" # 重命名为空(完全禁用)

其他应该禁用的危险命令:

  • KEYS *:全量扫描,阻塞主线程。
  • FLUSHALL/FLUSHDB:清空数据。
  • CONFIG:修改运行时配置。
  • SHUTDOWN:关闭Redis。

71. 🔴 Redis在容器化环境(Docker/K8s)中有哪些注意事项?

答:Redis在容器化环境中面临一些特殊挑战。

内存管理:

  1. maxmemory设置:容器有内存限制(cgroup),Redis必须设置maxmemory小于容器内存限制。否则Redis可能被OOM Killer杀死。建议maxmemory = 容器内存 × 0.7(留30%给OS、fork、缓冲区等)。
  2. 禁用swap:容器中使用swap会严重影响Redis性能。确保容器不使用swap。
  3. Transparent Huge Pages:容器中THP可能导致fork延迟增大。在宿主机上关闭THP。

网络:

  1. 端口映射:Redis Cluster需要两个端口(数据端口和Cluster Bus端口=数据端口+10000)。Docker需要映射两个端口。
  2. IP地址:容器IP可能变化(重启后)。Cluster节点间通信使用IP,IP变化会导致集群异常。使用StatefulSet + Headless Service保证稳定的网络标识。
  3. NAT问题:Docker的NAT可能导致Cluster节点间通信失败。使用cluster-announce-ipcluster-announce-port配置外部可达的IP和端口。

持久化:

  1. 数据卷:RDB/AOF文件必须存储在持久化卷(PersistentVolume)中,不能存储在容器的临时文件系统中。
  2. 磁盘性能:网络存储(如EBS、NFS)的IO性能可能不如本地SSD。对延迟敏感的场景使用本地SSD(hostPath或local PV)。

K8s部署:

  • 使用StatefulSet部署Redis(保证有序部署、稳定网络标识、持久化存储)。
  • 使用Operator(如Spotahome Redis Operator、Redis Operator by OpsTree)自动化管理。
  • 资源限制:设置CPU和内存的request/limit。Redis是CPU密集型(单线程),建议不限制CPU(只设request)。

72. 🔴 什么是Redis的Cluster Resharding?如何安全地进行集群扩缩容?

答:Resharding:重新分配Redis Cluster的哈希槽,实现集群扩缩容。

扩容流程(添加新节点):

  1. 将新节点加入集群:redis-cli --cluster add-node new-host:port existing-host:port
  2. 新节点加入后没有分配槽(不存储数据)。
  3. 从现有节点迁移槽到新节点:redis-cli --cluster reshard existing-host:port
  4. 指定迁移的槽数量和源节点。工具自动执行槽迁移。
  5. 为新主节点添加从节点:redis-cli --cluster add-node new-slave:port new-master:port --cluster-slave

缩容流程(移除节点):

  1. 将待移除节点的槽迁移到其他节点:redis-cli --cluster reshard
  2. 确认待移除节点没有槽。
  3. 移除节点:redis-cli --cluster del-node host:port node-id
  4. 如果移除的是主节点,其从节点会自动成为其他主节点的从节点。

安全操作要点:

  1. 分批迁移:不要一次迁移太多槽。每次迁移一部分,观察集群状态后再继续。
  2. 限速:大量数据迁移时限制速率,避免影响正常业务。
  3. 监控:迁移过程中监控集群状态(cluster info)、节点负载、Consumer Lag。
  4. 避免高峰期:在业务低峰期执行。
  5. 备份:操作前备份RDB。
  6. 验证:迁移完成后验证槽分配(cluster slots)和数据完整性。

自动化工具:

  • redis-cli --cluster rebalance:自动均衡槽分配(根据节点权重)。
  • redis-cli --cluster check:检查集群状态和槽分配。
  • redis-cli --cluster fix:修复集群问题(如槽分配不一致)。

73. 🔵 什么是Redis的Pub/Sub在Cluster模式下的问题?Sharded Pub/Sub如何解决?

答:传统Pub/Sub在Cluster模式下的问题:

  • PUBLISH命令会将消息广播到集群中的所有节点(不管节点是否有该Channel的订阅者)。
  • 集群内部带宽消耗 = 消息大小 × 节点数量。
  • 节点越多,带宽浪费越严重。

Sharded Pub/Sub(Redis 7.0+):

  • Channel被映射到特定的槽(类似key的哈希槽映射)。
  • 消息只在负责该槽的节点上发布和订阅。
  • 订阅者只需要连接负责该Channel的节点。

命令:

1
2
3
4
5
6
7
8
# Sharded Pub/Sub(新命令)
SSUBSCRIBE channel1 # 分片订阅
SUNSUBSCRIBE channel1 # 分片取消订阅
SPUBLISH channel1 message # 分片发布

# 传统Pub/Sub(仍然可用,仍然全集群广播)
SUBSCRIBE channel1
PUBLISH channel1 message

区别:

维度 传统Pub/Sub Sharded Pub/Sub
消息传播 全集群广播 只在负责的节点
带宽消耗 O(节点数) O(1)
订阅方式 连接任意节点 连接负责该Channel的节点
模式订阅 支持(PSUBSCRIBE) 不支持
命令 SUBSCRIBE/PUBLISH SSUBSCRIBE/SPUBLISH

客户端支持:需要客户端支持Sharded Pub/Sub(Lettuce 6.2+、Jedis 4.3+)。

74. 🔴 如何排查Redis的内存泄漏问题?

答:Redis内存泄漏:内存持续增长,即使没有新数据写入。

排查步骤:

  1. 确认内存增长趋势

    • INFO memory定期采集used_memory,绘制趋势图。
    • 如果used_memory持续增长且没有对应的key增长,可能是内存泄漏。
  2. 检查key数量和大小

    • DBSIZE:key总数。
    • redis-cli --bigkeys:找出大Key。
    • redis-cli --memkeys:按内存排序的key。
  3. 检查过期key

    • 大量key设置了过期时间但未被及时清理(惰性删除+定期删除不够及时)。
    • INFO keyspace查看每个数据库的key数量和过期key数量。
  4. 检查客户端缓冲区

    • INFO clients:查看connected_clients和client_recent_max_output_buffer。
    • CLIENT LIST:查看每个客户端的输出缓冲区大小(omem)。
    • 大量Pub/Sub订阅者或慢消费者可能导致输出缓冲区膨胀。
    • 配置:client-output-buffer-limit限制缓冲区大小。
  5. 检查复制缓冲区

    • 主从复制的replication buffer和repl-backlog可能占用大量内存。
    • INFO replication查看复制状态。
  6. 检查Lua脚本

    • 长时间运行的Lua脚本可能持有大量临时数据。
    • SCRIPT EXISTS检查缓存的脚本数量。
  7. 内存碎片

    • mem_fragmentation_ratio过高说明碎片严重。
    • 开启activedefrag或重启Redis。
  8. RDB分析

    • 导出RDB文件,使用redis-rdb-tools离线分析每个key的内存占用。
    • 找出异常增长的key或命名空间。

75. ⚫ 你在生产环境中遇到过最严重的Redis故障是什么?你是如何处理的?

答:这是一道开放性经验题,考察候选人的实战经验和应急处理能力。

优秀回答应该包含:

  1. 故障描述:清晰描述故障现象、影响范围、持续时间。
  2. 应急处理:第一时间做了什么止血?如何减少业务影响?
  3. 根因分析:故障的根本原因是什么?
  4. 修复方案:如何彻底修复?
  5. 改进措施:事后做了哪些改进防止再次发生?

典型故障示例:

示例1:Redis主库OOM被Kill

  • 现象:Redis主库进程被OS的OOM Killer杀死,所有写入失败。
  • 应急:Sentinel自动将从库提升为主库,服务恢复。但丢失了最后几秒的数据。
  • 根因:未设置maxmemory,数据量持续增长超过物理内存。fork时COW导致内存翻倍,触发OOM。
  • 改进:设置maxmemory和淘汰策略。监控内存使用率,80%告警。关闭THP。控制实例大小<10GB。

示例2:缓存雪崩导致数据库崩溃

  • 现象:大量缓存key同时过期,请求全部打到数据库,数据库CPU 100%,服务不可用。
  • 应急:限流降级,返回默认值。手动预热热点缓存。
  • 根因:批量导入数据时设置了相同的TTL,导致同时过期。
  • 改进:TTL加随机偏移。多级缓存(本地缓存兜底)。熔断降级机制。

示例3:大Key删除导致Redis卡顿

  • 现象:业务反馈Redis响应超时,持续约5秒。
  • 排查:slowlog发现一个DEL命令耗时4.8秒,删除了一个包含200万元素的Hash。
  • 改进:开启lazyfree-lazy-expire。使用UNLINK替代DEL。定期扫描大Key并拆分。

四、缓存实战问题与故障处理(76-105题)

76. 🔵 什么是缓存的命中率?如何监控和提高缓存命中率?

答:缓存命中率 = 缓存命中次数 / (缓存命中次数 + 缓存未命中次数)。

监控:

  • INFO stats中的keyspace_hitskeyspace_misses
  • 命中率 = keyspace_hits / (keyspace_hits + keyspace_misses)。
  • 生产环境命中率应该>95%,<90%需要优化。

提高命中率的方法:

  1. 合理的TTL:TTL太短导致频繁失效,太长导致数据不一致。根据数据变化频率设置。
  2. 缓存预热:启动时加载热点数据,避免冷启动。
  3. 增大缓存容量:maxmemory调大,减少淘汰。
  4. 优化淘汰策略:使用LFU替代LRU(更好地保留热点数据)。
  5. 缓存空值:防止缓存穿透导致的无效查询。
  6. 合理的key设计:避免key过于细粒度(命中率低)或过于粗粒度(更新频繁)。
  7. 多级缓存:L1本地缓存拦截大部分请求。
  8. 分析未命中原因:是key不存在(穿透)?还是key过期(失效)?还是key被淘汰(容量不足)?针对性优化。

77. 🔴 如何设计一个基于Redis的分布式限流+熔断系统?

答:限流和熔断是微服务保护的两道防线。

限流(Rate Limiting):控制请求速率,防止过载。

  • Redis实现:滑动窗口或令牌桶(见第35题)。
  • 多维度:用户级、接口级、服务级、全局级。
  • 限流后的处理:返回429 Too Many Requests,或排队等待。

熔断(Circuit Breaker):检测下游故障,快速失败,防止级联故障。

  • 状态机:Closed(正常)→ Open(熔断,快速失败)→ Half-Open(试探恢复)。
  • Redis实现:
1
2
3
4
5
6
7
8
9
10
-- 记录失败次数
local failures = redis.call('INCR', KEYS[1]) -- key: circuit:{service}:failures
if failures == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1]) -- 统计窗口
end
-- 检查是否触发熔断
if tonumber(failures) >= tonumber(ARGV[2]) then -- 失败阈值
redis.call('SET', KEYS[2], '1', 'EX', ARGV[3]) -- 熔断标记,持续N秒
end
return failures

组合设计:

  1. 请求进入 → 检查熔断状态(Redis GET)→ 如果熔断则快速失败。
  2. 未熔断 → 检查限流(Redis限流脚本)→ 如果超限则拒绝。
  3. 通过限流 → 执行业务逻辑。
  4. 业务失败 → 记录失败次数(Redis INCR)→ 超过阈值则触发熔断。
  5. 熔断超时后 → Half-Open状态 → 放行少量请求试探 → 成功则关闭熔断。

生产建议:简单场景用Redis自行实现。复杂场景用Sentinel(阿里开源)或Resilience4j,它们内置了限流、熔断、降级等功能。

78. 🔵 什么是Redis的数据序列化?不同序列化方式的性能对比如何?

答:Redis存储的是字节数组,应用层需要将对象序列化为字节数组存储,读取时反序列化。

常见序列化方式:

方式 大小 速度 可读性 跨语言
JSON
Protobuf
Kryo 最快 差(Java)
Hessian
Java Serializable 差(Java)
MessagePack

选型建议:

  • JSON:调试方便、跨语言。适合数据量不大、需要可读性的场景。Spring Boot默认使用Jackson JSON。
  • Protobuf:性能好、体积小、跨语言。适合高性能要求的场景。需要定义.proto文件。
  • Kryo:Java生态中最快的序列化框架。适合纯Java环境、对性能要求极高的场景。
  • Java Serializable:不推荐。体积大、速度慢、安全风险(反序列化漏洞)。

Spring Data Redis配置:

1
2
3
4
5
6
7
8
9
10
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// key使用String序列化
template.setKeySerializer(new StringRedisSerializer());
// value使用JSON序列化
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}

注意:不同序列化方式存储的数据不兼容。切换序列化方式需要迁移数据或清空缓存。

79. 🔴 什么是Redis的Pipeline和Lua脚本的性能对比?各自适用什么场景?

答:Pipeline和Lua脚本都能减少网络往返,但适用场景不同。

性能对比:

维度 Pipeline Lua脚本
网络往返 1次RTT(批量发送) 1次RTT(脚本执行)
原子性 不保证 保证
命令间依赖 不支持 支持
服务端开销 低(逐条执行) 中(脚本解析+执行)
灵活性 低(固定命令序列) 高(支持逻辑控制)
调试 简单 较复杂

适用场景:

Pipeline适合:

  • 批量读取/写入(如批量GET、批量SET)。
  • 命令之间没有依赖关系。
  • 不需要原子性保证。
  • 示例:批量加载缓存、批量删除key。

Lua脚本适合:

  • 需要原子性(如分布式锁、库存扣减)。
  • 命令之间有依赖(如先GET再根据结果SET)。
  • 需要逻辑控制(if/else、循环)。
  • 示例:限流、CAS操作、复杂的条件更新。

性能数据(参考):

  • 单命令:1万QPS(受RTT限制)。
  • Pipeline(100条/批):10万+ QPS。
  • Lua脚本(等效100条命令):5-8万QPS(脚本解析有开销)。

最佳实践:简单批量操作用Pipeline,需要原子性或逻辑控制用Lua脚本。两者可以结合使用(Pipeline中包含EVALSHA调用)。

80. 🔵 什么是Redis的CLUSTER FAILOVER命令?手动故障转移有什么用?

答:CLUSTER FAILOVER:在Redis Cluster中手动触发从节点升级为主节点。

使用场景:

  1. 计划内维护:需要重启或升级某个主节点时,先手动将其从节点提升为主节点,避免服务中断。
  2. 负载均衡:某个主节点负载过高,将其部分槽的从节点提升为主节点。
  3. 测试:测试故障转移流程是否正常。

命令:

1
2
3
4
# 在从节点上执行
CLUSTER FAILOVER # 安全模式(等待数据同步完成后切换)
CLUSTER FAILOVER FORCE # 强制模式(不等待数据同步)
CLUSTER FAILOVER TAKEOVER # 接管模式(不需要主节点参与,用于主节点完全不可用时)

安全模式流程:

  1. 从节点向主节点发送FAILOVER请求。
  2. 主节点停止接受新的写入,将所有未同步的数据发送给从节点。
  3. 从节点确认数据同步完成。
  4. 从节点发起选举,获得多数主节点投票后成为新主节点。
  5. 旧主节点变为从节点。

与自动故障转移的区别:

  • 自动故障转移:主节点故障后触发,可能丢失少量未同步的数据。
  • 手动故障转移(安全模式):主节点正常运行,先同步数据再切换,不丢数据。

滚动升级中的应用:

  1. 对每个主节点执行:先在其从节点上执行CLUSTER FAILOVER。
  2. 从节点成为新主节点后,升级旧主节点(现在是从节点)。
  3. 升级完成后,可以再次CLUSTER FAILOVER切回(可选)。

81. 🔴 什么是Redis的内存分析?如何找出Redis中占用内存最多的数据?

答:Redis内存分析是排查内存问题的关键步骤。

在线分析(不影响服务):

  1. INFO memory:整体内存使用情况。
  2. MEMORY USAGE key:单个key的内存占用。
  3. redis-cli --bigkeys:扫描找出每种类型最大的key。
  4. redis-cli --memkeys:按内存占用排序的key(Redis 4.0+)。
  5. MEMORY STATS:详细的内存使用统计(数据、元数据、缓冲区等)。

离线分析(更全面):

  1. 导出RDBBGSAVE生成RDB文件。
  2. redis-rdb-tools分析
1
2
3
4
5
6
# 导出所有key的内存信息为CSV
rdb --command memory dump.rdb --bytes 1024 -f memory.csv
# 按内存排序
sort -t, -k4 -rn memory.csv | head -20
# 按前缀统计
rdb --command memory dump.rdb | awk -F, '{split($1,a,":"); print a[1]}' | sort | uniq -c | sort -rn
  1. redis-memory-analyzer:可视化分析工具。

内存组成分析(MEMORY STATS):

  • dataset.bytes:实际数据占用的内存。
  • overhead.total:Redis元数据开销(key的指针、过期时间、字典结构等)。
  • replication.backlog:复制积压缓冲区。
  • clients.normal:客户端输出缓冲区。
  • aof.buffer:AOF缓冲区。

常见内存占用大户:

  1. 大Key(大Hash、大List、大Set)。
  2. 大量小Key的元数据开销(每个key约70字节的元数据)。
  3. 客户端输出缓冲区(大量Pub/Sub订阅者或慢客户端)。
  4. 复制缓冲区(repl-backlog-size)。
  5. 内存碎片。

82. 🔵 什么是Redis的RESP协议中的Inline命令?它有什么用?

答:Redis支持两种命令格式:RESP格式和Inline格式。

RESP格式(标准):

1
*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n

Inline格式(简化):

1
SET key value\r\n

Inline格式的用途:

  1. telnet调试:直接用telnet连接Redis,输入命令。不需要构造RESP格式。
1
2
3
telnet localhost 6379
SET key value
GET key
  1. 简单客户端:不需要实现完整的RESP协议解析,直接发送文本命令。
  2. redis-cli:redis-cli内部使用RESP格式,但用户输入的是Inline格式。

限制:

  • Inline格式不支持二进制安全(value中不能包含空格和换行)。
  • 生产环境的客户端应该使用RESP格式(二进制安全、性能更好)。

83. 🔴 如何设计一个基于Redis的分布式配置中心?

答:分布式配置中心:集中管理应用配置,支持动态更新和实时推送。

设计方案:

存储:

  • 使用Hash存储配置:HSET config:{app}:{env} key1 value1 key2 value2
  • 支持多应用、多环境(dev/test/prod)。
  • 配置版本:INCR config:{app}:{env}:version,每次修改递增版本号。

读取:

  • 应用启动时从Redis加载配置:HGETALL config:{app}:{env}
  • 本地缓存配置,减少Redis访问。

动态更新:

  • 修改配置后,通过Redis Pub/Sub通知所有应用实例。
1
2
3
4
5
6
7
8
# 修改配置
HSET config:myapp:prod db.url "jdbc:mysql://new-host:3306/db"
INCR config:myapp:prod:version
PUBLISH config:myapp:prod "db.url" # 通知变更的key

# 应用订阅
SUBSCRIBE config:myapp:prod
# 收到通知后重新加载变更的配置

高可用:

  • Redis Sentinel或Cluster保证配置存储的高可用。
  • 应用本地缓存配置,Redis不可用时使用本地缓存。

与专业配置中心对比:

维度 Redis方案 Nacos/Apollo
复杂度 简单 功能丰富
版本管理 简单版本号 完整的版本历史和回滚
灰度发布 需要自行实现 原生支持
权限管理 Redis ACL 完整的权限体系
审计日志 需要自行实现 原生支持
适用场景 小规模、简单配置 大规模、复杂配置管理

生产建议:简单场景(几十个配置项、几个服务)用Redis方案。复杂场景用Nacos或Apollo。

84. 🔵 什么是Redis的OBJECT HELP命令?Redis 7.0+的命令文档有什么改进?

答:Redis 7.0+对命令文档和自省能力做了重大改进。

COMMAND DOCS(Redis 7.0+):

  • COMMAND DOCS [command-name]:获取命令的详细文档(描述、参数、复杂度、版本等)。
  • 替代了之前需要查阅外部文档的方式。

COMMAND LIST(Redis 7.0+):

  • COMMAND LIST:列出所有命令。
  • COMMAND LIST FILTERBY MODULE module-name:按模块过滤。
  • COMMAND LIST FILTERBY PATTERN get*:按模式过滤。

COMMAND GETKEYS(Redis 7.0+改进):

  • COMMAND GETKEYS command args...:获取命令涉及的key。
  • 用于代理层分析命令的key,实现正确的路由。

这些改进使得Redis客户端和工具可以在运行时自省Redis的能力,不需要硬编码命令信息。

85. 🔴 什么是Redis的Cluster模式下的Multi-key操作?如何处理跨槽操作?

答:Redis Cluster中,涉及多个key的命令要求所有key在同一个槽中。

受影响的命令:

  • MGET/MSET/DEL(多key)
  • SUNION/SINTER/SDIFF(集合操作)
  • ZUNIONSTORE/ZINTERSTORE(有序集合操作)
  • RENAME(源key和目标key必须同槽)
  • SORT … STORE(源和目标必须同槽)
  • MULTI/EXEC中的多key命令
  • Lua脚本中访问的多个key

解决方案:

  1. Hash Tag:在key中使用{}指定哈希计算的部分。
1
2
3
4
# 以下key都映射到同一个槽(基于"user:1001"计算哈希)
SET {user:1001}.name "张三"
SET {user:1001}.age "30"
MGET {user:1001}.name {user:1001}.age # 可以正常执行
  1. 客户端拆分:将跨槽的MGET拆分为多个单key GET,并行发送到不同节点,客户端合并结果。
1
2
3
// Lettuce自动处理跨槽MGET
List<KeyValue<String, String>> results = commands.mget("key1", "key2", "key3");
// 内部自动按槽分组,并行发送,合并结果
  1. 代理层处理:Redis代理(如Predixy、Codis)可以自动拆分跨槽命令。

  2. Lua脚本:所有key使用相同的Hash Tag,在Lua脚本中执行复杂的多key操作。

Hash Tag的注意事项:

  • 过度使用Hash Tag可能导致数据倾斜(大量key映射到同一个槽)。
  • Hash Tag应该基于业务实体(如用户ID、订单ID),而非随意设置。
  • 空的Hash Tag(如{})会导致整个key作为哈希输入(等同于没有Hash Tag)。

86. 🔴 什么是Redis的Sentinel选举算法?Raft协议在Sentinel中是如何应用的?

答:Sentinel使用类Raft协议选举Leader Sentinel来执行故障转移。

选举流程:

  1. 某个Sentinel检测到主库客观下线(ODOWN)。
  2. 该Sentinel将自己的epoch(纪元)+1,向其他Sentinel发送SENTINEL is-master-down-by-addr请求,请求投票。
  3. 每个Sentinel在同一个epoch中只能投票一次(先到先得)。
  4. 获得多数票(>= quorum且>= Sentinel总数/2+1)的Sentinel成为Leader。
  5. Leader Sentinel执行故障转移(选择新主库、通知从库、更新配置)。

与标准Raft的区别:

  • Sentinel的选举是事件驱动的(只在需要故障转移时选举),不是持续的Leader维护。
  • Sentinel不需要日志复制(不是状态机复制),只需要选出一个Leader执行一次性操作。
  • 选举失败后(没有获得多数票),等待一段时间后重新选举(failover-timeout的2倍)。

quorum参数:

  • sentinel monitor mymaster 127.0.0.1 6379 2中的2就是quorum。
  • quorum的作用:至少quorum个Sentinel认为主库下线才触发故障转移。
  • 选举Leader需要的票数:max(quorum, Sentinel总数/2+1)。
  • 建议:3个Sentinel设quorum=2,5个Sentinel设quorum=3。

87. 🔵 什么是Redis的CLUSTER RESET命令?什么时候需要使用?

答:CLUSTER RESET:重置Redis Cluster节点的集群状态。

两种模式:

  • CLUSTER RESET SOFT:保留数据,清除集群状态(槽分配、已知节点列表、epoch等)。节点变为独立节点。
  • CLUSTER RESET HARD:清除集群状态+清空数据+生成新的节点ID。完全重置。

使用场景:

  1. 重建集群:集群状态混乱无法修复时,RESET所有节点后重新组建集群。
  2. 移除节点:将节点从集群中移除后,RESET该节点使其成为独立节点。
  3. 测试:测试环境中快速重置集群。

注意事项:

  • RESET前确保节点上没有重要数据(HARD模式会清空数据)。
  • RESET后节点不再属于任何集群,需要重新CLUSTER MEET加入集群。
  • 不要在生产环境的正常节点上执行RESET。

88. 🔴 什么是Redis的AOF重写?重写过程中如何保证数据不丢失?

答:AOF重写:将AOF文件中的冗余命令合并,生成更紧凑的AOF文件。

为什么需要重写:

  • AOF文件记录所有写命令,文件会持续增长。
  • 同一个key被修改100次,AOF中有100条命令,但只需要最后一条。
  • 重写后只保留每个key的最终状态对应的命令。

重写流程(BGREWRITEAOF):

  1. 主进程fork子进程。
  2. 子进程遍历内存中的所有数据,将每个key的当前值转换为写命令,写入新的AOF文件。
  3. fork后主进程继续处理客户端请求。新的写命令同时写入:
    • 旧AOF文件(保证旧AOF的完整性)。
    • AOF重写缓冲区(aof_rewrite_buf)。
  4. 子进程完成重写后通知主进程。
  5. 主进程将AOF重写缓冲区中的命令追加到新AOF文件。
  6. 原子替换旧AOF文件为新AOF文件。

数据不丢失的保证:

  • 重写期间旧AOF文件仍然在追加新命令(步骤3),如果重写失败,旧AOF文件仍然完整。
  • 重写缓冲区记录了fork后的所有写命令(步骤3),追加到新AOF后数据完整。
  • 文件替换是原子操作(rename系统调用)。

Redis 7.0的Multi-part AOF:

  • AOF文件拆分为base文件(重写生成)+ incr文件(增量命令)+ manifest文件(索引)。
  • 重写时只替换base文件,incr文件持续追加。
  • 避免了单个大AOF文件的问题。

89. 🔵 什么是Redis的WAIT命令和WAITAOF命令?它们有什么区别?

答:WAIT和WAITAOF都是等待数据持久化的命令。

WAIT(Redis 3.0+):

  • WAIT numreplicas timeout
  • 等待之前的写命令被指定数量的从库确认(复制到从库的内存)。
  • 保证:数据已复制到从库,但不保证从库已持久化到磁盘。
  • 用途:强一致性读(写入后从从库读取最新数据)。

WAITAOF(Redis 7.2+):

  • WAITAOF numlocal numreplicas timeout
  • numlocal:等待本地AOF fsync完成的确认数(0或1)。
  • numreplicas:等待从库AOF fsync完成的确认数。
  • 保证:数据已持久化到磁盘(本地和/或从库)。
  • 用途:强持久化保证(数据不会因为进程崩溃或机器重启而丢失)。

区别:

维度 WAIT WAITAOF
保证级别 复制到从库内存 持久化到磁盘(AOF fsync)
数据安全 从库进程崩溃可能丢失 进程崩溃不丢失
延迟 较低(内存复制) 较高(磁盘fsync)
版本 3.0+ 7.2+

使用场景:

  • WAIT:需要从从库读取最新数据,但可以接受极端情况下的数据丢失。
  • WAITAOF:金融等对数据持久性要求极高的场景。

90. 🔴 什么是Redis的Cluster模式下的读写分离?READONLY命令的作用是什么?

答:Redis Cluster默认所有读写都发送到主节点。READONLY命令允许从从节点读取数据。

READONLY命令:

  • 在从节点连接上执行READONLY,该连接进入只读模式。
  • 只读模式下,从节点接受读命令(GET、HGET等),不再返回MOVED重定向。
  • READWRITE命令退出只读模式。

使用场景:

  1. 读写分离:读请求发送到从节点,减轻主节点压力。
  2. 就近读取:跨可用区部署时,客户端从同可用区的从节点读取(减少延迟和跨区流量)。

客户端支持:

  • Lettuce:ReadFrom.REPLICAReadFrom.NEAREST配置。
1
2
StatefulRedisClusterConnection<String, String> connection = clusterClient.connect();
connection.setReadFrom(ReadFrom.REPLICA_PREFERRED); // 优先从从节点读取
  • Jedis:需要手动管理从节点连接。

注意事项:

  1. 数据一致性:从节点的数据可能落后于主节点(异步复制)。读取可能返回旧数据。
  2. 从节点故障:从节点故障时需要自动切换到主节点读取。
  3. 写操作:写操作仍然必须发送到主节点。从节点收到写命令返回MOVED。
  4. 热点key:如果热点key的读请求集中在一个从节点,该从节点可能过载。需要多个从节点分担。

91. 🔵 什么是Redis的CONFIG REWRITE命令?它有什么用?

答:CONFIG REWRITE:将当前运行时的配置写回配置文件(redis.conf),使动态修改的配置持久化。

使用场景:

  • 通过CONFIG SET动态修改了配置(如maxmemory、slowlog-log-slower-than等)。
  • 这些修改只在内存中生效,Redis重启后丢失。
  • CONFIG REWRITE将当前配置写回redis.conf,重启后仍然生效。

工作原理:

  • 读取原始redis.conf文件。
  • 对于已存在的配置项,更新为当前值。
  • 对于新增的配置项(原文件中没有),追加到文件末尾。
  • 保留原文件的注释和格式。
  • 原子写入(先写临时文件,再rename)。

注意事项:

  1. 需要Redis进程对配置文件有写权限。
  2. 如果Redis启动时没有指定配置文件(redis-server不带参数),CONFIG REWRITE会失败。
  3. 建议在CONFIG SET后立即CONFIG REWRITE,避免遗忘。
  4. 配置文件的变更应该纳入版本管理(git)。

92. 🔴 什么是Redis的OBJECT ENCODING优化?如何通过调整编码阈值优化内存?

答:Redis的数据结构会根据数据量自动选择编码。通过调整编码阈值,可以在内存和性能之间权衡。

编码阈值配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Hash
hash-max-listpack-entries 128 # field数量阈值
hash-max-listpack-value 64 # field/value长度阈值(字节)

# List
list-max-listpack-size -2 # 每个quicklist节点的listpack大小(-2表示8KB)
list-compress-depth 0 # quicklist两端不压缩的节点数(0表示不压缩)

# Set
set-max-intset-entries 512 # intset的元素数量阈值
set-max-listpack-entries 128 # listpack的元素数量阈值

# ZSet
zset-max-listpack-entries 128 # listpack的元素数量阈值
zset-max-listpack-value 64 # 元素长度阈值(字节)

优化策略:

  1. 增大阈值节省内存:如将hash-max-listpack-entries从128调大到256,更多Hash使用listpack编码(比hashtable节省50%+内存)。代价:listpack的查找是O(n),元素多时性能下降。
  2. 减小阈值提升性能:如将阈值调小,更早切换到hashtable/skiplist编码。代价:内存占用增加。
  3. List压缩list-compress-depth 1,quicklist中间节点使用LZF压缩。节省内存但增加CPU开销。

实际优化案例:

  • 存储大量小Hash(如用户Profile,5-10个field):保持默认阈值,确保使用listpack编码。
  • 存储大Hash(如商品详情,50+个field):如果读写频繁,可以降低阈值提前切换到hashtable。
  • 存储大量整数Set(如用户ID集合):增大set-max-intset-entries,保持intset编码。

验证:OBJECT ENCODING key确认key使用了预期的编码。

93. 🔴 什么是Redis的Cluster模式下的数据迁移原子性?MIGRATE命令的实现原理是什么?

答:MIGRATE命令在Redis Cluster的槽迁移中负责将key从源节点迁移到目标节点。

MIGRATE命令:

1
MIGRATE target-host target-port key|"" destination-db timeout [COPY] [REPLACE] [AUTH password] [KEYS key1 key2 ...]

实现原理:

  1. 源节点将key序列化(DUMP格式,包含value和TTL)。
  2. 源节点通过Socket将序列化数据发送到目标节点。
  3. 目标节点接收数据,执行RESTORE命令恢复key。
  4. 目标节点返回OK。
  5. 源节点收到OK后删除本地的key。

原子性保证:

  • MIGRATE是同步阻塞操作。在整个过程中,源节点的主线程被阻塞。
  • 如果迁移失败(网络错误、目标节点拒绝),源节点保留key,不删除。
  • 使用COPY选项:迁移后源节点不删除key(复制而非移动)。
  • 使用REPLACE选项:如果目标节点已存在同名key,覆盖。

性能影响:

  • MIGRATE是阻塞操作,大Key迁移会阻塞源节点。
  • 批量迁移(KEYS选项)比逐个迁移效率高(减少网络往返)。
  • 建议:迁移前拆分大Key,使用批量MIGRATE。

94. 🔵 什么是Redis的SUBSCRIBE和PSUBSCRIBE的区别?模式订阅有什么性能问题?

答:

  • SUBSCRIBE channel1 channel2:精确订阅指定Channel。
  • PSUBSCRIBE pattern*:模式订阅,匹配模式的所有Channel。

区别:

维度 SUBSCRIBE PSUBSCRIBE
匹配方式 精确匹配 通配符匹配(*/?/[…])
性能 O(1)查找 O(N)遍历所有模式
消息接收 只收到订阅Channel的消息 收到所有匹配模式的Channel的消息
消息格式 [“message”, channel, data] [“pmessage”, pattern, channel, data]

性能问题:

  • PUBLISH时,Redis需要遍历所有PSUBSCRIBE的模式,逐个匹配。模式数量多时性能下降。
  • 如果有1000个模式订阅,每次PUBLISH需要匹配1000次。
  • 精确订阅(SUBSCRIBE)使用哈希表查找,O(1)。

最佳实践:

  • 尽量使用精确订阅(SUBSCRIBE),避免大量模式订阅。
  • 如果需要模式订阅,控制模式数量。
  • Redis 7.0+的Sharded Pub/Sub不支持模式订阅(PSUBSCRIBE),只支持精确订阅。

95. 🔴 什么是Redis的RDB文件格式?如何解析RDB文件?

答:RDB文件是Redis数据的二进制快照,格式紧凑高效。

RDB文件结构(简化):

1
2
3
4
5
6
7
8
9
10
11
[REDIS magic string "REDIS0011"]  -- 文件头(REDIS + 版本号)
[Auxiliary fields] -- 辅助信息(redis-ver, redis-bits, ctime, used-mem等)
[Database selector: FE 00] -- 数据库编号(db 0)
[Resize DB: FB db_size expires_size] -- 数据库大小提示
[Key-Value pairs] -- 数据(type + key + value)
[Expiry: FD timestamp_seconds | FC timestamp_ms] -- 过期时间(可选)
[Type byte] -- 数据类型(0=String, 1=List, 2=Set, ...)
[Key: length-prefixed string] -- key
[Value: type-specific encoding] -- value(根据类型不同编码不同)
[EOF: FF] -- 文件结束标记
[8-byte CRC64 checksum] -- 校验和

解析工具:

  1. redis-rdb-tools(Python):最常用的RDB解析工具。
1
2
3
4
5
6
7
pip install rdbtools
# 转为JSON
rdb --command json dump.rdb
# 内存分析
rdb --command memory dump.rdb -f memory.csv
# 转为Redis协议(可用于导入)
rdb --command protocol dump.rdb | redis-cli --pipe
  1. redis-cli –rdb:导出RDB文件。
  2. OBJECT ENCODING + DEBUG OBJECT:在线查看key的编码信息。

RDB版本演进:

  • RDB版本随Redis版本升级(如Redis 7.0使用RDB版本10)。
  • 高版本RDB不能被低版本Redis加载。
  • 低版本RDB可以被高版本Redis加载(向后兼容)。

96. 🔵 什么是Redis的LATENCY命令?如何使用延迟监控排查性能问题?

答:LATENCY命令(Redis 2.8.13+)提供延迟事件的监控和分析。

开启延迟监控:

1
CONFIG SET latency-monitor-threshold 50  # 监控超过50ms的事件

命令:

1
2
3
4
LATENCY LATEST          # 最近的延迟事件
LATENCY HISTORY command # 某类事件的历史记录
LATENCY RESET # 清空记录
LATENCY GRAPH command # ASCII图形展示

LATENCY LATEST输出示例:

1
2
3
4
1) 1) "command"           # 事件名称
2) (integer) 1706000000 # 最近发生时间(Unix时间戳)
3) (integer) 150 # 最近延迟(ms)
4) (integer) 300 # 最大延迟(ms)

延迟事件类型及排查方向:

  • command:命令执行慢 → 检查slowlog,排查大Key或O(n)命令。
  • fork:fork操作慢 → 实例内存过大,关闭THP。
  • aof-fsync-always:AOF每次fsync慢 → 磁盘IO问题,考虑改为everysec。
  • aof-write:AOF写入慢 → 磁盘IO问题。
  • rdb-unlink-temp-file:删除临时RDB文件慢 → 磁盘IO问题。
  • expire-cycle:过期key清理慢 → 大量key同时过期。
  • eviction-cycle:内存淘汰慢 → 淘汰的key太大。

排查流程:

  1. LATENCY LATEST查看有哪些延迟事件。
  2. LATENCY HISTORY event查看事件的历史趋势。
  3. LATENCY GRAPH event可视化延迟趋势。
  4. 根据事件类型针对性优化。

97. 🔴 如何设计一个基于Redis的实时推荐系统的缓存层?

答:实时推荐系统需要低延迟、高吞吐的缓存支持。

缓存层设计:

  1. 用户画像缓存

    • 数据结构:Hash(user:{userId}:profile)。
    • 内容:用户标签、偏好、历史行为统计。
    • TTL:较长(24小时),定时从数据库/数据仓库刷新。
    • 更新:用户行为触发异步更新(MQ消费后更新Redis)。
  2. 物品特征缓存

    • 数据结构:Hash(item:{itemId}:features)。
    • 内容:物品标签、类目、热度分数。
    • TTL:中等(1-6小时)。
  3. 推荐结果缓存

    • 数据结构:List或ZSet(recommend:{userId}:feed)。
    • 内容:预计算的推荐列表(物品ID + 分数)。
    • TTL:较短(5-30分钟),保证推荐的新鲜度。
    • 策略:用户请求时先查缓存,缓存未命中则实时计算并缓存。
  4. 热门物品缓存

    • 数据结构:ZSet(hot:items:daily)。
    • 内容:按热度排序的物品列表。
    • 更新:定时任务每分钟更新。
    • 用途:冷启动用户(无历史行为)的推荐兜底。
  5. 实时特征缓存

    • 数据结构:String或Hash。
    • 内容:实时计算的特征(如最近1小时的点击率、转化率)。
    • 更新:Flink实时计算后写入Redis。
    • TTL:很短(1-5分钟)。
  6. 布隆过滤器去重

    • 数据结构:RedisBloom。
    • 用途:过滤用户已看过的物品,避免重复推荐。
    • 每个用户一个布隆过滤器(seen:{userId})。

性能优化:

  • Pipeline批量读取用户画像和物品特征。
  • 本地缓存热门物品(L1 Cache)。
  • 预计算推荐结果,减少实时计算压力。

98. 🔵 什么是Redis的CONFIG SET和CONFIG GET?有哪些常用的运行时配置?

答:CONFIG SET/GET用于在运行时查看和修改Redis配置,无需重启。

常用运行时配置:

内存相关:

1
2
3
CONFIG SET maxmemory 4gb                    # 最大内存
CONFIG SET maxmemory-policy allkeys-lfu # 淘汰策略
CONFIG SET activedefrag yes # 自动碎片整理

持久化相关:

1
2
3
4
CONFIG SET save "900 1 300 10 60 10000"     # RDB触发规则
CONFIG SET appendonly yes # 开启AOF
CONFIG SET appendfsync everysec # AOF刷盘策略
CONFIG SET aof-use-rdb-preamble yes # 混合持久化

性能相关:

1
2
3
4
5
CONFIG SET slowlog-log-slower-than 1000     # 慢查询阈值(微秒)
CONFIG SET slowlog-max-len 1000 # 慢查询日志长度
CONFIG SET latency-monitor-threshold 50 # 延迟监控阈值(ms)
CONFIG SET io-threads 4 # IO线程数
CONFIG SET io-threads-do-reads yes # IO线程处理读

安全相关:

1
2
CONFIG SET requirepass "strong-password"     # 设置密码
CONFIG SET rename-command FLUSHALL "" # 禁用危险命令

复制相关:

1
2
3
CONFIG SET min-replicas-to-write 1          # 最少从库数
CONFIG SET min-replicas-max-lag 10 # 从库最大延迟
CONFIG SET repl-backlog-size 64mb # 复制积压缓冲区

注意:CONFIG SET的修改只在内存中生效,重启后丢失。使用CONFIG REWRITE持久化到配置文件。

99. 🔴 什么是Redis的CLUSTER SLOTS和CLUSTER SHARDS命令?它们有什么区别?

答:两个命令都用于获取Redis Cluster的槽分配信息,但格式不同。

CLUSTER SLOTS(旧命令):

1
2
3
CLUSTER SLOTS
# 返回:槽范围 → 节点列表
# [[start, end, [ip, port, id], [ip, port, id]], ...]
  • 返回格式:数组嵌套,按槽范围组织。
  • 每个槽范围包含:起始槽、结束槽、主节点信息、从节点信息。

CLUSTER SHARDS(Redis 7.0+,推荐):

1
2
3
CLUSTER SHARDS
# 返回:分片列表,每个分片包含槽范围和节点列表
# [{slots: [[start, end], ...], nodes: [{id, ip, port, role, ...}, ...]}, ...]
  • 返回格式:更结构化,使用Map格式(RESP3)。
  • 包含更多节点信息(health、replication-offset等)。
  • 更易解析和使用。

区别:

维度 CLUSTER SLOTS CLUSTER SHARDS
版本 3.0+ 7.0+
格式 数组嵌套 结构化Map
信息量 基本信息 更丰富(健康状态等)
解析难度 较难 较易

客户端使用:智能客户端(Jedis、Lettuce)使用这些命令获取槽→节点的映射,缓存在本地,直接将命令发送到正确的节点。

100. ⚫ 作为架构师,你如何评估一个系统是否需要引入Redis?引入Redis后需要关注哪些风险?

答:这是一道架构决策题,考察全局视角和风险意识。

引入Redis的评估标准:

需要引入的信号:

  1. 数据库查询成为性能瓶颈(QPS高、响应慢)。
  2. 存在热点数据(少量数据被大量访问)。
  3. 需要分布式锁、限流、排行榜等功能。
  4. 需要低延迟的数据访问(毫秒级)。
  5. 需要临时数据存储(Session、验证码、Token)。

不需要引入的信号:

  1. 数据库性能足够,没有瓶颈。
  2. 数据量小、访问量低。
  3. 数据一致性要求极高(Redis是最终一致)。
  4. 团队没有Redis运维经验(引入新组件有学习成本)。

引入后的风险:

  1. 数据一致性:缓存和数据库的数据不一致。需要设计一致性方案(Cache Aside + TTL + binlog异步更新)。

  2. 可用性依赖:系统对Redis产生依赖,Redis故障影响业务。需要:高可用部署(Sentinel/Cluster)、降级方案(Redis不可用时直接查数据库或返回默认值)。

  3. 内存成本:Redis是内存存储,成本高于磁盘。需要:合理的TTL和淘汰策略、控制缓存数据量、定期清理无用数据。

  4. 运维复杂度:增加了一个需要监控和维护的组件。需要:完善的监控告警、备份恢复方案、扩缩容方案。

  5. 缓存穿透/击穿/雪崩:缓存失效时的连锁反应。需要:布隆过滤器、互斥锁、随机TTL、多级缓存。

  6. 大Key/热Key:影响Redis性能和稳定性。需要:定期扫描、拆分大Key、本地缓存热Key。

  7. 安全风险:Redis默认无认证,暴露在公网可能被攻击。需要:ACL、网络隔离、TLS加密。

好的架构师不是盲目引入技术,而是在充分评估收益和风险后做出合理决策。


五、补充题(101-105题)

101. 🔴 什么是Redis的COPY命令(Redis 6.2+)?它有什么用?

答:COPY命令:将一个key的值复制到另一个key,支持跨数据库复制。

语法:COPY source destination [DB destination-db] [REPLACE]

使用场景:

  1. 数据备份:复制key作为备份,修改前保留原始数据。
  2. 数据迁移:跨数据库复制key(DB选项)。
  3. 原子复制:COPY是原子操作,比GET+SET更安全。

与DUMP+RESTORE对比:

  • DUMP+RESTORE:序列化→传输→反序列化,支持跨实例。
  • COPY:内存级复制,只支持同实例(或同Cluster节点)。更快。

注意:Cluster模式下,source和destination必须在同一个槽。

102. 🔵 什么是Redis的LPOS命令(Redis 6.0.6+)?它解决了什么问题?

答:LPOS:在List中查找元素的位置(索引)。

语法:LPOS key element [RANK rank] [COUNT count] [MAXLEN len]

之前的问题:Redis没有直接查找List中元素位置的命令。需要LRANGE获取所有元素后在客户端查找,效率低。

使用示例:

1
2
3
4
5
RPUSH mylist a b c d c b a
LPOS mylist c # 返回2(第一个c的位置)
LPOS mylist c RANK 2 # 返回4(第二个c的位置)
LPOS mylist c COUNT 0 # 返回[2, 4](所有c的位置)
LPOS mylist c MAXLEN 3 # 只搜索前3个元素

103. 🔴 什么是Redis的GETDEL和GETEX命令(Redis 6.2+)?

答:

  • GETDEL key:获取key的值并删除key。原子操作,等同于GET+DEL。
  • GETEX key [EX seconds] [PX ms] [EXAT timestamp] [PXAT ms-timestamp] [PERSIST]:获取key的值并设置/修改/移除过期时间。

使用场景:

  • GETDEL:一次性消费的数据(如验证码:获取后立即删除)。
  • GETEX:读取数据时顺便续期(如Session:每次访问时延长过期时间)。

之前需要两条命令(GET+DEL或GET+EXPIRE),现在一条命令完成,减少网络往返且保证原子性。

104. 🔵 什么是Redis的CLIENT NO-EVICT命令(Redis 7.0+)?

答:CLIENT NO-EVICT ON:标记当前客户端连接为”不可驱逐”。

背景:当Redis内存达到maxmemory时,如果客户端的输出缓冲区占用大量内存,Redis可能断开该客户端连接来释放内存。

NO-EVICT的作用:保护重要的客户端连接不被驱逐。适用于:

  • 监控客户端(持续订阅Pub/Sub)。
  • 管理客户端(执行运维命令)。
  • 关键业务客户端。

注意:过多的NO-EVICT客户端可能导致内存无法释放,需要谨慎使用。

105. ⚫ 如果Redis的QPS已经达到瓶颈,你有哪些扩展方案?

答:Redis单实例QPS瓶颈(通常10-20万QPS)的扩展方案:

  1. 读写分离:写操作走主库,读操作分散到多个从库。适合读多写少的场景。线性扩展读能力。

  2. Redis Cluster分片:数据分散到多个主节点,每个节点处理一部分请求。线性扩展读写能力。

  3. 本地缓存(L1 Cache):在应用层增加Caffeine等本地缓存,拦截大部分读请求。减少Redis访问量。

  4. 多线程IO(Redis 6.0+):开启io-threads,提升网络IO处理能力。单实例QPS可提升到20万+。

  5. Pipeline/Lua批量操作:减少网络往返,提高单次请求的效率。

  6. 业务优化:减少不必要的Redis访问(合并请求、缓存预计算结果)。优化数据结构(使用更高效的编码)。

  7. 多实例部署:在同一台机器上部署多个Redis实例(利用多核CPU)。每个实例绑定不同的CPU核心。

  8. 客户端优化:使用连接池、异步客户端(Lettuce)、批量操作。减少客户端到Redis的网络延迟。

选择顺序:先优化(Pipeline/本地缓存/业务优化)→ 再扩展(读写分离/Cluster)→ 最后换架构(多级缓存体系)。


四、多级缓存架构(106-125题)

106. 🔵 什么是多级缓存架构?为什么单一Redis缓存不够用?

答:多级缓存是在不同层级设置缓存,逐级降低对下游的访问压力。

单一Redis缓存的局限:

  1. 网络延迟:即使Redis在同机房,一次网络往返也需要0.5-1ms。高并发下累积延迟可观
  2. 带宽瓶颈:热点数据的大量读取可能打满Redis网络带宽(单实例通常10Gbps)
  3. 单点风险:Redis故障时所有请求直接打到数据库
  4. 序列化开销:每次从Redis读取都需要反序列化,CPU开销不可忽视

多级缓存架构:

1
2
3
4
5
请求 → CDN(静态资源)
→ Nginx本地缓存(OpenResty/Lua)
→ 应用本地缓存 L1(Caffeine)
→ 分布式缓存 L2(Redis)
→ 数据库 L3

各层的价值:

  • CDN:缓存静态资源和可缓存的API响应,拦截80%+的静态请求
  • Nginx缓存:在网关层缓存热点API响应,减少后端服务压力
  • L1本地缓存:纳秒级访问,零网络开销,拦截最热的数据请求
  • L2 Redis:毫秒级访问,容量大,所有实例共享
  • L3数据库:兜底数据源

性能对比:

  • L1(Caffeine):~100ns,亿级QPS/实例
  • L2(Redis):~1ms,10万级QPS/实例
  • L3(MySQL):~10ms,万级QPS/实例

每一层缓存命中都能避免访问下一层,整体性能提升是指数级的。

107. 🔴 Caffeine作为L1本地缓存的核心原理是什么?它为什么比Guava Cache快?

答:Caffeine是Java生态中性能最高的本地缓存库,被Spring Boot 2.x+作为默认缓存实现。

核心设计:

  1. W-TinyLFU淘汰算法

    • 结合了LRU和LFU的优点
    • 分为Window Cache(1%容量,LRU)和Main Cache(99%容量,分为Probation和Protected两段)
    • 新数据先进入Window Cache
    • Window Cache淘汰的数据与Main Cache Probation段的数据通过TinyLFU频率比较,频率高的留下
    • TinyLFU使用Count-Min Sketch(概率计数器)记录访问频率,只需要极少内存
  2. 高并发数据结构

    • 使用ConcurrentHashMap存储数据
    • 读写操作记录到Ring Buffer(环形缓冲区),由专门的维护线程异步处理淘汰和过期
    • 避免了Guava Cache在每次读写时同步执行淘汰检查的开销
  3. 异步维护

    • 缓存的淘汰、过期、统计等维护操作由后台线程异步执行
    • 读写操作几乎无额外开销

为什么比Guava Cache快:

维度 Guava Cache Caffeine
淘汰算法 LRU(Segmented LRU) W-TinyLFU(命中率更高)
并发机制 分段锁(Segment Lock) ConcurrentHashMap + Ring Buffer
维护操作 同步执行(读写时触发) 异步执行(后台线程)
过期检查 读写时惰性检查 定时轮(Timing Wheel)精确调度
性能 百万级ops/s 千万级ops/s

使用示例:

1
2
3
4
5
6
Cache<String, Object> cache = Caffeine.newBuilder()
.maximumSize(10_000) // 最大条目数
.expireAfterWrite(5, TimeUnit.MINUTES) // 写入后5分钟过期
.refreshAfterWrite(1, TimeUnit.MINUTES) // 写入后1分钟异步刷新
.recordStats() // 开启统计
.build();

108. 🔴 L1本地缓存和L2 Redis缓存如何协同工作?数据一致性如何保证?

答:L1+L2协同是多级缓存的核心难题。

读取流程:

1
2
3
1. 查L1(Caffeine)→ 命中则直接返回
2. L1未命中 → 查L2(Redis)→ 命中则写入L1并返回
3. L2未命中 → 查数据库 → 写入L2 → 写入L1 → 返回

写入/更新流程:

1
2
3
1. 更新数据库
2. 删除L2(Redis)缓存
3. 通知所有应用实例删除L1缓存(通过Redis Pub/Sub或MQ)

L1一致性问题:

  • 多个应用实例各自有独立的L1缓存
  • 数据更新后,只有处理更新请求的实例知道数据变了
  • 其他实例的L1缓存仍然是旧数据

L1缓存失效通知方案:

  1. Redis Pub/Sub通知
1
2
3
4
5
6
7
8
// 数据更新时发布失效消息
redisTemplate.convertAndSend("cache:invalidate", cacheKey);

// 所有实例订阅失效消息
@RedisListener(topics = "cache:invalidate")
public void onInvalidate(String cacheKey) {
localCache.invalidate(cacheKey);
}
  • 优点:实时性好,实现简单
  • 缺点:Redis Pub/Sub不持久化,实例重启期间的消息会丢失
  1. RocketMQ/Kafka广播消费
  • 使用广播模式消费失效消息,每个实例都能收到
  • 优点:消息不丢失,可靠性高
  • 缺点:引入MQ依赖,延迟略高
  1. 短TTL兜底
  • L1缓存设置较短的TTL(如10秒-1分钟)
  • 即使通知丢失,最多在TTL时间内数据不一致
  • 这是最简单也最实用的兜底方案

生产建议:Redis Pub/Sub通知 + 短TTL兜底,双重保障。

109. 🔵 CDN缓存在多级缓存体系中扮演什么角色?如何设计CDN缓存策略?

答:CDN是多级缓存的最外层,距离用户最近,效果最显著。

CDN缓存的适用内容:

  1. 静态资源:JS/CSS/图片/字体/视频,命中率可达95%+
  2. 可缓存的API响应:商品详情、文章内容等变化不频繁的数据
  3. 页面片段:SSR渲染的HTML页面

CDN缓存策略设计:

  1. 缓存时间(TTL)

    • 静态资源(带hash指纹):1年(文件名变了就是新URL)
    • 静态资源(无hash):1天-7天
    • API响应:10秒-5分钟(根据数据变化频率)
    • 用户个性化数据:不缓存
  2. 缓存Key设计

    • 默认按URL缓存
    • 需要区分的维度加入Key:设备类型(PC/Mobile)、语言、地域
    • 避免Cookie等用户相关信息进入Key(导致缓存命中率极低)
  3. 缓存失效

    • TTL自然过期
    • 主动Purge:数据更新时调用CDN API清除缓存
    • 版本化URL:/api/products/123?v=20240101,更新时改版本号
  4. 回源策略

    • 缓存未命中时回源到源站
    • 源站通过Cache-Control头控制CDN缓存行为
    • Cache-Control: public, max-age=300:CDN缓存5分钟
    • Cache-Control: private, no-cache:不缓存(用户个性化数据)
    • Stale-While-Revalidate:缓存过期后先返回旧数据,后台异步回源更新

CDN + 多级缓存的协同:

1
用户请求 → CDN(命中率90%)→ 网关Nginx缓存 → L1 Caffeine → L2 Redis → DB

CDN拦截了90%的请求后,到达后端的流量只有10%,后端的多级缓存压力大幅降低。

110. 🔴 Nginx层的缓存如何实现?OpenResty+Lua在缓存架构中有什么作用?

答:Nginx/OpenResty层缓存是多级缓存中的重要一环,位于CDN和应用之间。

Nginx原生缓存(proxy_cache):

1
2
3
4
5
6
7
8
9
10
11
12
13
proxy_cache_path /data/nginx/cache levels=1:2 keys_zone=my_cache:10m 
max_size=10g inactive=60m use_temp_path=off;

server {
location /api/ {
proxy_cache my_cache;
proxy_cache_valid 200 5m; # 200响应缓存5分钟
proxy_cache_valid 404 1m; # 404缓存1分钟
proxy_cache_key "$request_uri"; # 缓存Key
proxy_cache_use_stale error timeout updating; # 源站故障时返回旧缓存
add_header X-Cache-Status $upstream_cache_status; # 缓存命中状态
}
}

OpenResty + Lua的高级缓存:

  • OpenResty = Nginx + LuaJIT,可以在Nginx中执行Lua脚本
  • 使用lua-resty-lrucache实现Nginx Worker进程内的本地缓存
  • 使用lua-resty-redis直接从Nginx访问Redis

多级缓存在OpenResty中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
-- 三级缓存:Worker本地缓存 → 共享内存 → Redis
local lrucache = require "resty.lrucache"
local cache = lrucache.new(1000) -- Worker级LRU缓存

function get_data(key)
-- L1: Worker本地缓存
local val = cache:get(key)
if val then return val end

-- L2: Nginx共享内存(所有Worker共享)
val = ngx.shared.my_cache:get(key)
if val then
cache:set(key, val, 10) -- 回填L1,10秒TTL
return val
end

-- L3: Redis
local redis = require "resty.redis"
local red = redis:new()
red:connect("127.0.0.1", 6379)
val = red:get(key)
if val and val ~= ngx.null then
ngx.shared.my_cache:set(key, val, 60) -- 回填L2
cache:set(key, val, 10) -- 回填L1
return val
end

return nil -- 缓存未命中,转发到后端应用
end

优势:在Nginx层就拦截大量请求,后端应用服务器压力大幅降低。适合读多写少的热点数据。

111. 🔴 多级缓存架构下的缓存穿透、击穿、雪崩问题如何升级应对?

答:多级缓存下这三个经典问题的应对策略需要在每一层都考虑。

缓存穿透(查询不存在的数据):

  • L1层:本地布隆过滤器(Guava BloomFilter),拦截明显不存在的Key
  • L2层:Redis布隆过滤器(RedisBloom),所有实例共享
  • 兜底:缓存空值到L2(短TTL),L1也缓存空值
  • 多级防护:请求依次经过L1布隆过滤器 → L2布隆过滤器 → 空值缓存,穿透到数据库的概率极低

缓存击穿(热点Key过期瞬间):

  • L1层:使用Caffeine的refreshAfterWrite,过期前异步刷新,永远有数据可用
  • L2层:分布式锁(Redisson)保证只有一个线程回源数据库
  • 逻辑过期:L2中存储逻辑过期时间,过期后异步更新,当前请求返回旧数据
  • 多级防护:L1的异步刷新 + L2的互斥锁,双重保障

缓存雪崩(大量Key同时过期或Redis宕机):

  • L1层:Redis宕机时L1本地缓存兜底,保证核心功能可用
  • L2层:过期时间加随机偏移,避免同时过期
  • 熔断降级:Sentinel/Resilience4j熔断,数据库压力过大时返回默认值
  • Redis高可用:Cluster模式 + 多副本,避免整体宕机

关键原则:每一层缓存都是下一层的保护屏障。即使某一层完全失效,其他层仍然能提供服务。

112. 🔴 缓存预热在多级缓存架构中如何实现?大促前的预热策略是什么?

答:多级缓存的预热比单级缓存更复杂,需要逐层预热。

预热顺序(从下到上):

1
2
3
1. 预热L2(Redis):从数据库加载热点数据到Redis
2. 预热L1(Caffeine):应用启动时从Redis加载到本地缓存
3. 预热CDN:通过预热工具主动请求热点URL,让CDN缓存

大促前预热策略:

  1. 数据分析阶段(大促前3天):

    • 分析历史大促的访问日志,找出Top 1000热点商品
    • 分析搜索热词,预测热点品类
    • 与运营确认主推商品和活动页面
  2. L2预热(大促前1天):

    • 批量将热点商品数据加载到Redis
    • 设置较长的TTL(如24小时),避免大促期间过期
    • 预热商品详情、库存、价格等核心数据
  3. L1预热(大促前1小时):

    • 应用实例启动时通过@PostConstruct从Redis加载Top热点数据
    • 或通过预热接口触发:POST /admin/cache/warmup
  4. CDN预热(大促前2小时):

    • 调用CDN预热API,提交热点URL列表
    • CDN主动回源缓存这些URL的响应
    • 预热静态资源(商品图片、活动页面JS/CSS)
  5. 验证阶段(大促前30分钟):

    • 检查各层缓存命中率
    • 模拟请求验证热点数据是否已缓存
    • 监控Redis内存使用率和连接数

113. 🔵 Spring Cache如何集成多级缓存?有哪些开源的多级缓存框架?

答:Spring Cache原生只支持单级缓存,需要扩展或使用第三方框架实现多级缓存。

Spring Cache的局限:

  • @Cacheable只能指定一个CacheManager
  • 不支持L1+L2的级联查找和回填
  • 不支持缓存失效通知

开源多级缓存框架:

  1. J2Cache(红薯/OSChina开源)

    • L1:Caffeine/Ehcache + L2:Redis
    • 内置Redis Pub/Sub缓存失效通知
    • 与Spring Cache无缝集成
    • 配置简单,开箱即用
  2. Jetcache(阿里开源)

    • 支持多级缓存(Local + Remote)
    • 注解驱动:@Cached@CacheUpdate@CacheInvalidate
    • 支持自动刷新(@CacheRefresh
    • 内置缓存统计和监控
    1
    2
    3
    4
    @Cached(name = "userCache", expire = 3600, 
    cacheType = CacheType.BOTH, // LOCAL + REMOTE
    localLimit = 1000, localExpire = 60)
    public User getUserById(Long id) { ... }
  3. 自定义实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    @Component
    public class MultiLevelCache {
    private final Cache<String, Object> l1Cache; // Caffeine
    private final RedisTemplate<String, Object> redisTemplate; // Redis

    public Object get(String key) {
    // L1
    Object val = l1Cache.getIfPresent(key);
    if (val != null) return val;
    // L2
    val = redisTemplate.opsForValue().get(key);
    if (val != null) {
    l1Cache.put(key, val); // 回填L1
    return val;
    }
    return null;
    }

    public void put(String key, Object val, long ttlSeconds) {
    redisTemplate.opsForValue().set(key, val, ttlSeconds, TimeUnit.SECONDS);
    l1Cache.put(key, val);
    // 通知其他实例失效L1
    redisTemplate.convertAndSend("cache:invalidate", key);
    }
    }

推荐:中小项目用J2Cache(简单),大型项目用Jetcache(功能丰富)或自定义实现(灵活可控)。

114. 🔴 多级缓存的监控和可观测性如何设计?需要关注哪些指标?

答:多级缓存的监控比单级缓存复杂得多,需要监控每一层的状态。

核心监控指标:

  1. 命中率(Hit Rate):最重要的指标

    • L1命中率:Caffeine的stats().hitRate(),目标>80%
    • L2命中率:Redis的INFO stats中的keyspace_hits/(keyspace_hits+keyspace_misses),目标>95%
    • 总命中率:1 - (数据库查询次数 / 总请求次数),目标>99%
    • 命中率下降是缓存问题的第一信号
  2. 延迟(Latency)

    • L1访问延迟:通常<1ms,超过说明GC或锁竞争
    • L2访问延迟:通常1-5ms,超过说明网络或Redis负载问题
    • 回源延迟:数据库查询延迟,缓存未命中时的代价
  3. 容量和内存

    • L1:缓存条目数、内存占用、淘汰次数
    • L2:Redis内存使用率、Key数量、大Key检测
    • 淘汰频率过高说明容量不足
  4. 一致性

    • 失效通知的延迟和丢失率
    • L1和L2数据不一致的比例(采样对比)

监控实现:

1
2
3
4
5
6
7
8
9
// Caffeine统计
CacheStats stats = cache.stats();
metrics.gauge("cache.l1.hit_rate", stats.hitRate());
metrics.counter("cache.l1.eviction_count", stats.evictionCount());
metrics.gauge("cache.l1.size", cache.estimatedSize());

// Redis统计
RedisInfo info = redisTemplate.getConnectionFactory().getConnection().info("stats");
// 解析keyspace_hits, keyspace_misses等

告警规则:

  • L1命中率 < 70%:Warning
  • L2命中率 < 90%:Warning
  • 总命中率 < 95%:Critical
  • Redis内存使用率 > 80%:Warning
  • 缓存失效通知延迟 > 5s:Warning

115. 🔴 什么是缓存的”惊群效应”(Thundering Herd)?在多级缓存中如何避免?

答:惊群效应:缓存失效瞬间,大量请求同时穿透到下游(数据库),造成瞬间压力激增。

与缓存击穿的区别:

  • 缓存击穿:单个热点Key过期
  • 惊群效应:更广义,包括缓存重建、服务重启、批量Key过期等场景

多级缓存中的惊群场景:

  1. L1缓存重建:应用重启后L1为空,所有请求打到L2
  2. L2缓存重建:Redis故障恢复后缓存为空,所有请求打到数据库
  3. 批量L1过期:大量L1缓存同时过期,瞬间打到L2

解决方案:

  1. 互斥锁(Mutex Lock)

    • L1未命中时,只有一个线程去查L2/DB,其他线程等待或返回旧值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public Object getWithLock(String key) {
    Object val = l1Cache.getIfPresent(key);
    if (val != null) return val;

    Lock lock = lockMap.computeIfAbsent(key, k -> new ReentrantLock());
    if (lock.tryLock()) {
    try {
    val = loadFromL2OrDB(key);
    l1Cache.put(key, val);
    } finally {
    lock.unlock();
    }
    } else {
    // 等待或返回默认值
    Thread.sleep(50);
    return l1Cache.getIfPresent(key);
    }
    return val;
    }
  2. Caffeine的异步刷新

    • refreshAfterWrite:过期前异步刷新,始终有旧值可用
    • 只有一个线程执行刷新,其他线程返回旧值
    • 最优雅的方案,推荐使用
  3. 渐进式预热

    • 应用启动后不立即承担全部流量
    • 通过负载均衡权重控制,逐步增加流量(10%→30%→50%→100%)
    • 给L1缓存足够的预热时间
  4. L1过期时间加随机偏移

    • TTL = baseTTL + random(0, baseTTL * 0.2)
    • 避免大量L1缓存同时过期

116. 🔵 本地缓存的容量规划如何做?如何避免OOM?

答:本地缓存直接占用JVM堆内存,规划不当会导致OOM或频繁GC。

容量估算:

1
单条数据大小 × 最大缓存条目数 = 缓存总内存
  • 使用JOL(Java Object Layout)工具精确测量对象大小
  • 或通过Runtime.getRuntime().totalMemory()前后对比估算

Caffeine的容量控制:

1
2
3
4
5
6
Caffeine.newBuilder()
.maximumSize(10_000) // 按条目数限制
// 或
.maximumWeight(100_000_000) // 按权重限制(如字节数)
.weigher((key, value) -> ((byte[])value).length)
.build();

容量规划原则:

  1. 本地缓存总量不超过堆内存的20-30%:留足空间给业务对象和GC
  2. 单个缓存实例不超过1万条:条目太多会增加GC压力(每个对象都是GC Root的可达对象)
  3. 大对象不放本地缓存:超过10KB的对象放Redis,避免堆内存膨胀
  4. 监控缓存大小cache.estimatedSize()定期上报,接近上限时告警

避免OOM的措施:

  • 设置maximumSize硬上限,Caffeine会自动淘汰
  • 使用弱引用(weakKeys()/weakValues()),GC压力大时自动回收
  • 定期清理:cache.cleanUp()手动触发清理
  • JVM参数:-XX:+HeapDumpOnOutOfMemoryError,OOM时自动dump分析

117. 🔴 如何设计一个电商商品详情页的多级缓存方案?

答:商品详情页是多级缓存的经典应用场景,读多写少,热点集中。

数据分析:

  • 商品基本信息:变化不频繁(名称、描述、图片),强缓存
  • 商品价格:可能频繁变化(促销),短缓存
  • 库存状态:实时性要求高,极短缓存或不缓存
  • 用户评价:变化频繁但实时性要求不高,中等缓存

多级缓存设计:

  1. CDN层

    • 商品图片:CDN缓存1年(URL带hash指纹)
    • 商品详情页HTML(SSR):CDN缓存5分钟,Stale-While-Revalidate: 60
    • 商品API响应:CDN缓存1分钟(非登录态)
  2. Nginx/OpenResty层

    • 热门商品的API响应缓存在Nginx共享内存中
    • TTL:30秒
    • 使用Lua脚本实现缓存逻辑,命中后直接返回,不转发到后端
  3. L1本地缓存(Caffeine)

    • 商品基本信息:TTL 5分钟,最大1万条
    • 商品价格:TTL 30秒(价格变化需要快速感知)
    • 分类/品牌等字典数据:TTL 30分钟
  4. L2分布式缓存(Redis)

    • 商品完整信息:TTL 1小时,Hash结构存储各字段
    • 库存:TTL 10秒或不缓存(直接查库存服务)
    • 评价摘要:TTL 5分钟
  5. 数据库层

    • 兜底数据源
    • 读写分离:详情页查询走从库

缓存更新策略:

  • 商品信息变更 → Canal监听binlog → 删除L2缓存 → Redis Pub/Sub通知删除L1
  • 价格变更 → 直接删除L2 → 广播删除L1(价格敏感,不能有延迟)
  • 库存变更 → 不走缓存或极短TTL(库存准确性要求高)

118. 🔴 多级缓存架构下如何处理缓存与数据库的最终一致性?

答:多级缓存的一致性比单级缓存更复杂,因为需要保证多层缓存的一致性。

一致性挑战:

  • L1缓存分布在多个应用实例中,各自独立
  • L2缓存是共享的,但与数据库之间有延迟
  • CDN缓存在边缘节点,更新延迟更大

分层一致性策略:

  1. 数据库 → L2(Redis)一致性

    • 方案:Canal监听binlog + 异步删除Redis缓存
    • 延迟:通常100ms-1s
    • 兜底:Redis缓存设置TTL,即使Canal延迟也能在TTL后自动过期
  2. L2(Redis)→ L1(Caffeine)一致性

    • 方案:Redis Pub/Sub广播失效通知
    • 延迟:通常10ms-100ms
    • 兜底:L1设置短TTL(10秒-1分钟)
  3. L2(Redis)→ CDN一致性

    • 方案:数据变更时调用CDN Purge API
    • 延迟:通常1-5秒(CDN全球节点同步)
    • 兜底:CDN缓存设置较短的TTL

整体一致性保障:

1
2
3
4
5
数据库更新 
→ Canal捕获binlog(100ms)
→ 删除Redis缓存 + 发布Pub/Sub通知(10ms)
→ 各实例删除L1缓存(10ms)
→ 调用CDN Purge API(1-5s)

最坏情况下的不一致窗口:

  • L1:Pub/Sub通知延迟 + L1 TTL = 最多1分钟
  • L2:Canal延迟 + Redis TTL = 最多几秒到1小时
  • CDN:Purge延迟 + CDN TTL = 最多几秒到5分钟

对于大多数业务场景,秒级的最终一致性是可以接受的。对于价格、库存等敏感数据,应该绕过缓存直接查询。

119. 🔴 什么是缓存的”读写穿透”模式?Read-Through和Write-Through如何在多级缓存中实现?

答:Read-Through和Write-Through将缓存操作封装在缓存层内部,业务代码只操作缓存。

Read-Through(读穿透):

  • 缓存未命中时,缓存层自动从数据源加载数据并缓存
  • 业务代码不需要关心”缓存未命中→查数据库→回填缓存”的逻辑
  • Caffeine原生支持:
1
2
3
4
5
6
7
LoadingCache<String, User> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build(key -> userDao.findById(key)); // CacheLoader自动加载

// 使用时只需要get,未命中自动加载
User user = cache.get("user:1001");

Write-Through(写穿透):

  • 写操作时,缓存层同步写入数据源
  • 保证缓存和数据源的强一致性
  • 代价:写入延迟增加(需要等待数据源写入完成)

Write-Behind(异步写回):

  • 写操作只更新缓存,缓存层异步批量写入数据源
  • 优点:写入性能极高(只写内存)
  • 缺点:缓存故障可能丢数据
  • 适合:计数器、浏览量等允许少量丢失的场景

多级缓存中的Read-Through实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// L1 Read-Through:未命中自动从L2加载
LoadingCache<String, Object> l1Cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(1, TimeUnit.MINUTES)
.build(key -> {
// L2 Read-Through:未命中自动从DB加载
Object val = redisTemplate.opsForValue().get(key);
if (val == null) {
val = dbService.load(key);
if (val != null) {
redisTemplate.opsForValue().set(key, val, 1, TimeUnit.HOURS);
}
}
return val;
});

// 业务代码极简
User user = (User) l1Cache.get("user:1001");

这种模式让业务代码完全不感知缓存的存在,缓存逻辑集中在CacheLoader中管理。

120. 🔵 多级缓存架构的降级策略如何设计?各层故障时如何保证系统可用?

答:降级策略是多级缓存高可用的关键。

各层故障的降级方案:

  1. CDN故障

    • 降级:DNS切换到备用CDN或直接回源
    • 影响:延迟增加,源站压力增大
    • 预案:多CDN厂商备份,自动切换
  2. Nginx缓存层故障

    • 降级:请求直接到达应用层
    • 影响:L1/L2缓存仍然有效,影响较小
    • 预案:Nginx集群多实例,负载均衡自动摘除故障节点
  3. L1(Caffeine)故障(通常是应用重启):

    • 降级:所有请求打到L2(Redis)
    • 影响:延迟从纳秒级升到毫秒级,Redis压力增大
    • 预案:渐进式预热,启动后逐步增加流量
  4. L2(Redis)故障

    • 降级:L1本地缓存兜底 + 数据库直接查询
    • 影响:L1未命中的请求直接打到数据库,压力大
    • 预案:
      • Redis Sentinel/Cluster自动故障转移
      • 熔断器:Redis不可用时快速失败,不等待超时
      • L1延长TTL:Redis故障期间L1缓存不过期
      • 限流:保护数据库不被打垮
  5. 数据库故障

    • 降级:返回缓存中的旧数据(即使已过期)
    • 影响:数据可能不是最新的,但系统仍然可用
    • 预案:缓存设置stale-while-error策略,数据库不可用时返回过期缓存

降级代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object getWithDegradation(String key) {
try {
// 正常流程:L1 → L2 → DB
return multiLevelCache.get(key);
} catch (RedisException e) {
// Redis故障:L1兜底
Object val = l1Cache.getIfPresent(key);
if (val != null) return val;
// L1也没有:限流后查DB
if (rateLimiter.tryAcquire()) {
return dbService.load(key);
}
return defaultValue; // 返回默认值
} catch (Exception e) {
return defaultValue; // 兜底默认值
}
}

121. 🔴 Redis Client-side Caching与应用层L1缓存有什么区别?如何选择?

答:两者都是”本地缓存”,但实现机制和适用场景不同。

Redis Client-side Caching(Redis 6.0+):

  • 客户端缓存Redis中的数据,Redis服务端在数据变更时主动推送失效通知
  • 一致性由Redis服务端保证(Tracking机制)
  • 客户端不需要自己实现失效逻辑

应用层L1缓存(Caffeine等):

  • 应用自己管理的本地缓存,与Redis无直接关联
  • 一致性需要自己实现(Pub/Sub通知、TTL兜底等)
  • 更灵活,可以缓存任何数据源的数据(不限于Redis)

对比:

维度 Client-side Caching 应用层L1缓存
失效机制 Redis服务端推送 自己实现(Pub/Sub/MQ)
一致性 强(服务端保证) 最终一致(有延迟窗口)
数据来源 只能缓存Redis中的数据 任何数据源
实现复杂度 低(客户端库支持) 中(需要自己实现通知)
灵活性 低(受Redis Tracking限制) 高(完全自定义)
客户端支持 Lettuce 6.0+支持 任何语言/框架

选择建议:

  • 数据全部来自Redis且使用Lettuce客户端:优先用Client-side Caching,实现简单一致性好
  • 数据来源多样(Redis+DB+API):用应用层L1缓存,更灵活
  • 两者可以结合使用:Client-side Caching处理Redis数据,Caffeine处理其他数据源

122. 🔴 如何设计一个支持”读写分离”的多级缓存框架?

答:读写分离的多级缓存需要区分读路径和写路径,保证一致性的同时最大化性能。

读路径设计:

1
2
3
读请求 → L1(Caffeine,本地)
→ L2(Redis Slave,就近读取)
→ DB(从库)

写路径设计:

1
2
3
写请求 → DB(主库)
→ 删除L2(Redis Master)
→ 广播删除L1(所有实例)

框架核心接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface MultiLevelCacheManager {
// 读:逐级查找
<T> T get(String key, Class<T> type);

// 读:带加载器(Read-Through)
<T> T get(String key, Class<T> type, Function<String, T> loader);

// 写:更新DB后失效缓存
void evict(String key);

// 写:直接更新缓存(Write-Through场景)
void put(String key, Object value, Duration ttl);

// 批量操作
<T> Map<String, T> multiGet(Collection<String> keys, Class<T> type);
}

读写分离的关键点:

  1. Redis读写分离:写操作走Master,读操作走Slave(Lettuce原生支持ReadFrom.REPLICA
  2. DB读写分离:缓存未命中时查从库,写操作走主库
  3. 一致性保障:写操作完成后,短时间内的读请求强制走主库/Master(避免主从延迟导致读到旧数据)

实现技巧:

  • 使用ThreadLocal标记”刚写入”状态,短时间内(如2秒)读请求绕过缓存直接查主库
  • 或使用”写后读主”策略:写操作后设置一个短TTL的标记Key,有标记时读走主库

123. ⚫ 如果让你设计一个千万级QPS的缓存架构,你会怎么做?

答:千万级QPS意味着单一缓存层无法承载,必须多级缓存+水平扩展。

架构设计:

  1. 流量分层

    • CDN拦截80%的静态请求(图片、JS/CSS、可缓存API)
    • 到达后端的有效请求约200万QPS
  2. 网关层缓存(OpenResty)

    • 100台Nginx实例,每台2万QPS
    • Worker本地LRU缓存热点数据
    • 命中率目标50%,拦截后剩余100万QPS
  3. 应用层L1缓存(Caffeine)

    • 500台应用实例,每台2000QPS
    • Caffeine缓存Top 1万热点数据
    • 命中率目标80%,拦截后剩余20万QPS
  4. L2分布式缓存(Redis Cluster)

    • 50个主节点的Redis Cluster,每节点4万QPS
    • 总容量200万QPS,实际承载20万QPS(留足余量)
    • 命中率目标99%,穿透到DB的只有2000QPS
  5. 数据库层

    • 主从集群,从库承载2000QPS绰绰有余

关键设计决策:

  • 热点发现:实时统计访问频率,动态识别热点Key,推送到L1和网关层
  • 一致性:分层TTL(L1: 10s, L2: 5min, CDN: 1min),Pub/Sub通知失效
  • 容灾:任何一层故障,上下层自动承接流量。Redis故障时L1延长TTL兜底
  • 弹性扩缩:根据流量自动扩缩应用实例和Redis节点

成本估算:

  • CDN:按流量计费
  • Nginx:100台4核8G ≈ 中等成本
  • 应用服务器:500台8核16G ≈ 主要成本
  • Redis:50台16核64G ≈ 中等成本

124. 🔴 多级缓存中的序列化问题如何解决?不同层使用不同序列化格式的利弊?

答:序列化是多级缓存中容易被忽视但影响很大的问题。

各层的序列化选择:

L1(Caffeine):

  • 不需要序列化:直接存储Java对象引用
  • 优点:零序列化开销,访问速度最快
  • 注意:缓存的对象是引用,修改对象会影响缓存中的值。需要返回深拷贝或使用不可变对象

L2(Redis):

  • 必须序列化:数据需要通过网络传输和存储
  • 常见选择:
    • JSON(Jackson/Gson):可读性好,跨语言,但体积大、速度慢
    • Protobuf:体积小、速度快,但需要定义Schema
    • Kryo:Java生态最快的序列化框架,但不跨语言
    • MessagePack:类似JSON但二进制格式,体积小速度快

CDN层:

  • 通常是HTTP响应体:JSON或HTML
  • 使用Gzip/Brotli压缩减少传输体积

不同层使用不同格式的利弊:

  • 优点:每层使用最适合的格式(L1无序列化、L2用Protobuf、CDN用JSON+Gzip)
  • 缺点:L1和L2之间需要格式转换,增加复杂度

推荐方案:

  • L1:直接存储Java对象(零开销)
  • L2:统一使用JSON(兼容性好,调试方便)或Protobuf(性能优先)
  • 避免在L1和L2之间频繁转换格式

序列化的坑:

  1. 类变更兼容性:添加/删除字段后旧缓存反序列化失败。JSON天然兼容,Kryo需要注册类
  2. 泛型擦除List<User>反序列化后可能变成List<LinkedHashMap>。需要传入TypeReference
  3. 循环引用:对象间循环引用导致序列化死循环。Jackson需要@JsonIdentityInfo

125. ⚫ 你在生产中遇到过哪些多级缓存的坑?如何解决的?

答:这是一道开放性题目,考察真实的多级缓存实战经验。以下是几个典型案例:

案例1:L1缓存对象被意外修改

  • 场景:从Caffeine获取的User对象,业务代码修改了其中的字段,导致缓存中的数据也被改了
  • 原因:L1缓存存储的是对象引用,不是副本
  • 解决:返回深拷贝(user.clone()),或使用不可变对象(Lombok的@Value
  • 教训:L1缓存的对象必须是不可变的,或者返回时做深拷贝

案例2:Redis Pub/Sub通知丢失导致L1数据不一致

  • 场景:数据更新后L2已删除,但部分实例的L1仍然是旧数据
  • 原因:Redis Pub/Sub不持久化,网络抖动时消息丢失
  • 解决:L1设置短TTL(30秒)兜底 + 定期全量校验(每5分钟随机抽样对比L1和L2)
  • 教训:不能完全依赖Pub/Sub通知,必须有TTL兜底

案例3:缓存预热导致Redis瞬间OOM

  • 场景:应用重启后500个实例同时从DB加载数据写入Redis,Redis内存瞬间打满
  • 原因:预热没有限流,大量数据同时写入
  • 解决:预热时加限流(每秒最多写入1000条),分批预热,错峰启动
  • 教训:预热必须限流,避免对Redis和DB造成冲击

案例4:L1和L2的TTL设置不合理导致”缓存踏空”

  • 场景:L1 TTL=5分钟,L2 TTL=5分钟。L1过期后查L2,L2也刚好过期,直接打到DB
  • 原因:L1和L2的TTL相同,可能同时过期
  • 解决:L2的TTL必须大于L1(如L1=1分钟,L2=10分钟),保证L1过期时L2仍然有数据
  • 教训:多级缓存的TTL必须递增,下层TTL > 上层TTL