1.7 java面试题:Redis 基本篇
好的,我按顺序逐题回答,对高频重点配上能直接落地的 Java 代码案例,让你面试时既能讲原理,又能亮代码。
1. 什么是 Redis?
Redis 是基于内存的高性能键值存储系统,C 语言编写,单机 QPS 可达 10 万+。它不只是缓存,更是一个数据结构服务器,支持 String、Hash、List、Set、Sorted Set 等丰富类型,并提供持久化、主从、集群、事务、Lua 脚本等高级功能。在银行核心系统中,我们用它做热点数据缓存、分布式锁、实时排行榜和消息队列。
2. Redis 的数据类型?
- String:缓存对象、计数器、分布式锁值。
- Hash:存对象(如客户信息),可部分更新。
- List:消息队列、最新 N 条记录。
- Set:去重、标签、共同好友。
- Sorted Set:排行榜、延迟队列。
- 高级:Bitmaps(签到)、HyperLogLog(UV 统计)、Geo(附近的人)、Stream(5.0+ 消息队列)。
好的,面试官问数据类型的底层数据结构,是在考察你对 Redis 内部实现的理解深度。老练的回答不能只罗列概念,要讲清楚为什么用这种结构、在什么条件下切换、以及演进历史。
以下是每种数据类型的底层编码详解,结合 Redis 6.x/7.x 版本。
一、String(字符串)
底层结构:SDS(简单动态字符串)
Redis 自己实现的字符串,不是 C 的 char*。优点:
- O(1) 获取长度(字段
len) - 杜绝缓冲区溢出(
alloc记录剩余空间) - 二进制安全(可存任意字节)
- 内存预分配和惰性释放,减少重分配次数
内部编码:
- int:当值可解析为整数且不超过
long范围时,直接用整数存储,空间最省。 - embstr:值长度 ≤ 44 字节(Redis 3.0 前为 39),分配一次内存,对象头和 SDS 连续存储,性能高。
- raw:值长度 > 44 字节,对象头和 SDS 分开分配,修改时不用重新分配整个对象。
切换规则:int 和 embstr 在值发生变化(如 append)后,会转为 raw;int 转为 embstr 或 raw 取决于新长度。
二、Hash(哈希)
底层结构:ziplist(压缩列表) 或 listpack(紧凑列表,7.0+) → dict(哈希表)
- ziplist/listpack:当字段少且每个字段值较小时使用,一块连续内存,无指针开销,但复杂度 O(n)。
- dict:真正的哈希表,数组+链表,查找 O(1),但内存碎片多。
切换条件(可在配置中调整):
- 字段数 <
hash-max-ziplist-entries(默认 512) - 每个字段值的长度 <
hash-max-ziplist-value(默认 64 字节)
满足以上条件使用 ziplist/listpack,否则转为 dict。
Redis 7.0 变化:ziplist 被 listpack 替代,解决了连锁更新问题。
三、List(列表)
底层结构:quicklist(快速列表)
Redis 3.2 前:linkedlist(双向链表) 或 ziplist,根据元素大小和数量切换。
3.2 后统一用 quicklist,它是 linkedlist 和 ziplist 的混合体:
- 一个 quicklist 是一个双向链表,但每个节点里是一个 ziplist(或 listpack),存储多个元素。
- 这样既保留了链表的插入删除灵活性,又通过压缩列表减少指针内存开销。
配置参数:
list-max-ziplist-size:控制每个 ziplist 节点的大小(正数表示元素个数,负数表示字节大小)。list-compress-depth:压缩深度,不活跃的端点节点会被压缩,节省内存。
四、Set(集合)
底层结构:intset(整数集合) 或 dict(哈希表)
- intset:当集合元素全部是整数且数量 ≤ 512 时使用,有序整数数组,二分查找 O(log n),无重复。
- dict:否则用哈希表,value 为 NULL。
切换条件:set-max-intset-entries(默认 512),超过后转为 dict。
五、Sorted Set(有序集合)
底层结构:ziplist/listpack → skiplist(跳表)+ dict(哈希表)
- ziplist/listpack:当元素少且值小时使用,按分值从小到大排列,查询 O(n)。
- skiplist + dict:复杂场景下使用。
- skiplist:按分值排序,支持范围查询 O(log n)。
- dict:从成员到分值的映射,支持 O(1) 查询分值。
两者共享元素节点,内存开销小。
切换条件:
- 元素数 <
zset-max-ziplist-entries(默认 128) - 每个元素长度 <
zset-max-ziplist-value(默认 64 字节)
总结表
| 数据类型 | 底层编码 | 切换条件 |
|---|---|---|
| String | int / embstr / raw | 值变化(长度、类型) |
| Hash | ziplist/listpack → dict | 字段数 > 512 或字段长度 > 64 |
| List | quicklist(内含 ziplist/listpack) | 无需切换,统一为 quicklist |
| Set | intset → dict | 元素数 > 512 或存在非整数元素 |
| Sorted Set | ziplist/listpack → skiplist+dict | 元素数 > 128 或值长度 > 64 |
面试时这样说:
“Redis 的每种数据类型都根据数据量大小和元素特性动态选择底层结构。String 用 SDS,有三种编码 int、embstr、raw;Hash 和 Sorted Set 在数据量小时用压缩列表或 listpack,省内存,数据量大时切为哈希表或跳表加字典;List 从 3.2 起统一用 quicklist,把链表和压缩列表结合起来,既高效又省内存;Set 数据全是整数且量少时用整数集合,否则用字典。我理解这套编码切换机制,是 Redis 能在各种场景下保持高性能和小内存占用的关键。”
这样回答既有细节,又体现了对整个内存优化策略的理解。
代码示例:Hash 存对象 + 部分更新
// 存入客户信息
redisTemplate.opsForHash().put("customer:1
01", "name", "张三");
redisTemplate.opsForHash().put("customer:1001", "balance", "50000");
// 只更新余额
redisTemplate.opsForHash().increment("customer:1001", "balance", -1000);
理解跳表不能死记硬背,作为一名老练的 Java 工程师,你要能讲出它为了解决什么问题、怎么实现、复杂度是多少、与红黑树/二叉树的对比、以及为什么 Redis 要用它。
下面我分五个层次帮你彻底吃透。
一、为什么需要跳表?
假设 Sorted Set 底层直接用有序链表,查找一个元素的时间复杂度是 O(n)。即使换成有序数组,二分查找是 O(log n),但插入删除又是 O(n)。
跳表的核心思想:给有序链表“加索引”,让它可以跳着找,实现查找、插入、删除都是 O(log n),同时保持链表的灵活性。
一句话:跳表 = 多层索引的有序链表,用空间换时间,类似于一个支持折半查找算法。
二、跳表的结构是怎样的?
Level 2: 1 ----------------> 9
Level 1: 1 ------> 5 ------> 9 ------> 13
Level 0: 1 -> 3 -> 5 -> 7 -> 9 -> 11 -> 13 -> 15
- 最底层(Level 0):包含所有元素的有序双向链表。
- 上层:是下层的“索引”,节点随机“晋升”。Level 1 是 Level 0 的子集,Level 2 是 Level 1 的子集,以此类推。
- 查找过程:从最高层开始,用当前层指针向右走,如果下一个节点值大于目标,则向下走一层,继续向右,直到最底层找到目标。比如找 7,会走
1(2层)→9(2层,大于7)→下到1层→1→5→9(大于7)→下到0层→5→7。
复杂度分析:
- 查找、插入、删除均为平均 O(log n)。
- 空间复杂度 O(n)(实际上约 1.33n,比红黑树的额外指针还省一些)。
三、Redis 中的跳表实现细节
在 server.h 中定义了两个结构:
typedef struct zskiplistNode {
sds ele; // 成员对象(member)
double score; // 分值
struct zskiplistNode *backward; // 后退指针(最底层使用)
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned long span; // 跨度(两个节点间跨越了几个元素)
} level[]; // 柔性数组,每个节点的层数随机
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length; // 节点总数
int level; // 当前最大层数
} zskiplist;
关键设计:
- 层数随机化:插入新节点时,用“抛硬币”算法随机决定层数(每次有 1/4 概率加一层,最大 32 层)。这保证了上层节点指数递减,维护了平衡。
- span 字段:记录当前层向前跨过了几个节点,排名操作时直接累加,不用遍历,计算
ZRANK的时间就是 O(log n)。 - 双向链表:最底层 backward 指针支持逆序查找。
四、为什么 Redis ZSet 用跳表而不用红黑树或 B+树?
| 对比维度 | 跳表 | 红黑树 | B+树 |
|---|---|---|---|
| 实现复杂度 | 简单,代码少 | 复杂,需旋转/变色 | 较复杂 |
| 范围查找 | O(log n + m),找到起点后沿链表遍历 | 需中序遍历,不直接 | 叶子链表支持范围查 |
| 并发友好 | 可锁局部,无全局旋转 | 旋转时需锁大片节点 | 页分裂合并 |
| 内存占用 | 额外指针少,平均约 1.33 倍 | 左右孩子 + 颜色位 | 页内大量空间 |
Redis 选择跳表的核心原因:
- 实现简单,易调试。
- 天然支持范围查询(ZRANGE、ZRANGEBYSCORE),找到起点后顺序遍历即可。
- 无全局平衡操作,写入时仅影响局部,性能平稳。
- 配合 dict 后,可按成员名 O(1) 查分值,再走跳表做范围或排序操作。
五、面试这样说,展现深度
“跳表本质是给有序链表加了多层索引。查找时从高层开始跳着找,实现 O(log n) 复杂度。Redis 的 ZSet 底层用跳表 + 字典,跳表按分值排序,支持范围查询;字典按成员名查分值,O(1) 锁定起点。
Redis 实现跳表时引入了
span字段,这样算排名时累加跨度就行,不用逐个遍历。层数是随机化的,平均时间复杂度 O(log n)。相比红黑树,跳表实现简单、范围查找直接、无全局平衡操作,非常契合 Redis 追求简洁和高性能的哲学。”
可补充的进阶亮点(面试官爱追问):
- ZRANK 复杂度:也是 O(log n),因为
span字段在查找过程中累计。 - ZRANGEBYSCORE:跳表先找起点 O(log n),然后顺序遍历 O(m)。
- 内存开销:跳表比红黑树的指针更少,这是因为节点平均层数约 1.33,红黑树每个节点需要左右子指针和颜色标记。
这样回答既有算法原理解析,又有 Redis 源码级别的实现细节,还有与其他结构的对比,面试官会认为你对数据结构是真正吃透了的。
3. 使用 Redis 有哪些好处?
- 极致性能:内存读写微秒级,缓解 DB 压力。
- 数据结构丰富:天然匹配缓存、队列、排行榜等业务。
- 分布式能力:分布式锁、Pub/Sub、Stream 支撑微服务。
- 持久化:RDB + AOF 保证数据安全。
- 原子操作:Lua 脚本打包多个命令,避免并发问题。
- 高可用:哨兵/集群模式,故障转移,水平扩展。
4. Redis 相比 Memcached 有哪些优势?
- 数据结构:Memcached 仅 String,Redis 支持多种复杂结构。
- 持久化:Redis 支持 RDB/AOF,Memcached 不支持,重启全丢。
- 高可用:Redis 原生支持哨兵、集群,Memcached 需客户端一致性哈希。
- 功能丰富:Redis 支持事务、Lua 脚本、发布订阅、过期策略等。
5. Memcache 与 Redis 的区别都有哪些?
| 维度 | Redis | Memcached |
|---|---|---|
| 数据结构 | 丰富 | 仅 String |
| 持久化 | 支持 | 不支持 |
| 线程模型 | 单线程 (命令执行) | 多线程 |
| 内存管理 | 多种淘汰策略 | 仅 LRU |
| 集群 | 原生支持 | 客户端实现 |
| 功能 | 事务、Lua、Pub/Sub | 简单 |
6. Redis 是单进程单线程的?
命令执行是单线程的,但 4.0 后引入多线程处理持久化、异步删除;6.0 后支持多线程 I/O(网络读写),但命令执行依然是单线程。这种设计避免了锁竞争,简化了数据一致性。
7. 一个字符串类型的值能存储最大容量是多少?
512MB。但生产中建议单个 value 不超过 10KB,大对象应拆分或使用 Hash 存储。
8. Redis 的持久化机制是什么?各自的优缺点?
- RDB(快照):定时全量备份。优点:恢复快,文件小。缺点:可能丢最后几分钟数据。
- AOF(追加日志):记录每个写命令。优点:最多丢 1 秒(
appendfsync everysec)。缺点:文件大,恢复慢。 - 混合持久化(4.0+):RDB 全量 + AOF 增量,推荐。银行场景开启 AOF
everysec。
好的,我们把这三种持久化机制彻底拆解清楚,让你在面试时能把原理、配置、优缺点和银行场景实践一气呵成地讲出来。
一、RDB(Redis DataBase)快照持久化
1. 原理
RDB 是某个时间点的全量内存数据快照。执行时,Redis 会 fork 一个子进程,子进程将当前内存中的数据写入一个临时的 .rdb 文件,写完后原子地替换旧的 RDB 文件。整个过程利用操作系统的**写时复制(Copy-On-Write)**机制,父进程可以继续处理写请求,只有在父进程修改内存页时才会复制该页给子进程。
2. 触发方式
-
自动触发:通过配置文件中的
save指令。save 900 1 # 900秒内至少1个key变化 save 300 10 # 300秒内至少10个key变化 save 60 10000 # 60秒内至少10000个key变化注意多个条件是“或”的关系,任一满足即触发。
-
手动触发:
SAVE:在主进程中执行,阻塞所有请求,直到快照完成。线上禁用。BGSAVE:fork 子进程后台执行,不阻塞主进程,线上唯一允许的手动方式。SHUTDOWN:安全关闭时自动执行BGSAVE。- 主从复制的全量同步时,主节点也会执行
BGSAVE。
3. 配置参数
dbfilename dump.rdb # RDB文件名
dir /data/redis # 文件存放目录
rdbcompression yes # 开启LZF压缩,节省磁盘空间
rdbchecksum yes # 文件末尾写入CRC64校验,防损坏
stop-writes-on-bgsave-error yes # BGSAVE失败时拒绝写入,保证数据安全
4. 优点
- 恢复极快:RDB 文件是紧凑的二进制数据,直接加载到内存即可,适合灾备恢复。
- 文件小:经过压缩,节省磁盘空间。
- 主进程不参与 I/O:fork 子进程处理,对主进程性能影响小。
5. 缺点
- 数据丢失风险较高:两次快照之间宕机,这期间的所有修改会丢失。比如配置
save 60 10000,最坏可能丢 60 秒的数据。 - fork 开销:如果内存数据很大(比如 10GB+),fork 操作本身耗时,可能造成毫秒级阻塞。同时写时复制可能导致内存翻倍。
- 不适合实时持久化。
6. 银行场景适用性
- 不推荐单独作为核心持久化手段,因为即使是几秒的丢失在金融业务中也可能造成账务不一致。
- 常用于冷备和历史数据归档。比如每天深夜自动生成一份 RDB,上传到异地灾备中心。
二、AOF(Append Only File)日志持久化
1. 原理
AOF 以独立日志的方式记录每一次写命令(以 Redis 协议格式),重启时通过回放所有命令来恢复数据。它是增量持久化,数据安全性远高于 RDB。
2. 工作流程
- 命令追加:服务端执行完写命令后,将命令追加到 AOF 缓冲区。
- 文件写入与刷盘:根据
appendfsync策略将缓冲区写入 AOF 文件并刷盘。 - AOF 重写:当 AOF 文件过大时,fork 子进程进行重写,生成一个最小的命令集合,用
BGREWRITEAOF触发。
3. 刷盘策略(核心)
| 策略 | 配置值 | 行为 | 丢失量 |
|---|---|---|---|
| 不刷盘 | no |
由操作系统决定何时刷盘 | 可能丢最后一整段 |
| 每秒刷盘 | everysec |
异步线程每秒刷一次 | 最多丢1秒数据 |
| 每次都刷 | always |
每个写命令都刷盘 | 不丢,但性能极低 |
银行推荐 everysec,在性能和安全间取得平衡。
4. AOF 重写机制
- 为什么需要重写?
随着时间推移,AOF 文件会膨胀,里面有很多无效命令(如多次修改同一个 key,或已删除的 key)。重写就是根据当前内存状态生成等效的最小命令集。 - 重写流程(
BGREWRITEAOF):- fork 子进程,将当前内存数据写入一个新的 AOF 文件。
- 在此期间,主进程的写命令同时追加到旧的 AOF 缓冲区和重写缓冲区。
- 子进程写完新 AOF 后,主进程将重写缓冲区的命令追加到新 AOF,然后原子的替换旧文件。
5. 配置参数
appendonly yes # 开启AOF
appendfilename "appendonly.aof" # AOF文件名
appendfsync everysec # 每秒刷盘
auto-aof-rewrite-percentage 100 # 文件增长率达到100%时重写
auto-aof-rewrite-min-size 64mb # 文件至少64MB才考虑重写
no-appendfsync-on-rewrite yes # 重写期间暂停刷盘,避免磁盘竞争
6. 优点
- 数据安全性高:最多丢失 1 秒数据(
everysec)。 - 可读性好:AOF 文件可以直接理解命令,便于人工修复。
- 可后台重写:不影响主进程。
7. 缺点
- 文件体积大:相同数据量,AOF 文件通常比 RDB 大数倍。
- 恢复速度慢:需要逐条命令回放,重启耗时较长。
- 写 QPS 受限:受磁盘 I/O 影响,
always策略下性能下降明显。
8. 银行场景适用性
- 核心持久化手段:保证数据安全,通常配置
everysec。 - 结合监控:监控 AOF 重写时的磁盘 I/O 和延迟,避免重写影响在线业务。
三、混合持久化(Redis 4.0+)
1. 原理
混合持久化将 RDB 的全量快照和 AOF 的增量日志整合到一个文件中。在 AOF 重写 时,生成的新的 AOF 文件前半部分是一个 RDB 格式的全量数据,后半部分是此后新增的 AOF 命令。
2. 工作流程
- 触发
BGREWRITEAOF时:- fork 子进程,将当前内存数据以 RDB 格式写入新 AOF 文件的头部。
- 父进程同时将新命令写入重写缓冲区。
- 子进程写完 RDB 部分后,父进程将重写缓冲区的命令以 AOF 格式追加到文件尾部。
- 原子替换旧 AOF 文件。
- 这样生成的文件既有 RDB 的紧凑和快速加载特性,又有 AOF 的完整性。
3. 配置参数
aof-use-rdb-preamble yes # 开启混合持久化
4. 优点
- 集两者之长:恢复速度快(头部 RDB 直接加载),数据安全性高(尾部 AOF 记录后续写操作)。
- 文件体积适中:大部分数据以紧凑 RDB 格式存储。
5. 缺点
- 兼容性:只支持 4.0 以上版本。
- 不可读:文件头部为二进制 RDB 格式,人工阅读不便。
6. 银行场景实践
- 强烈推荐:我们所有核心 Redis 实例都开启混合持久化,
appendfsync everysec。 - 恢复演练:定期在灾备环境加载生产 AOF 文件,验证恢复时间和数据完整性。
- 监控指标:
aof_current_size、aof_rewrite_in_progress、aof_last_bgrewrite_status,确保重写成功。
四、面试模板话术
“RDB 是定时全量快照,利用 fork + 写时复制,恢复快、文件小,但可能丢几分钟数据。AOF 是增量日志,记录每个写命令,通常配置
everysec最多丢 1 秒,但文件大、恢复慢。混合持久化在 AOF 重写时生成 RDB 头部 + AOF 尾部,兼顾恢复速度和数据安全性。在银行核心系统中,我绝对不用单独的 RDB,而是使用混合持久化 + AOF
everysec。这样既能保证数据最多丢 1 秒,又能在重启时快速加载 RDB 部分,恢复速度远快于纯 AOF。同时我们定期备份 RDB 到灾备中心,并做恢复演练,确保万无一失。”
这样既详细又结合了银行实战,面试官很难找到破绽。
9. Redis 常见性能问题和解决方案:
- 慢查询:用
SLOWLOG GET 10分析,优化大 key 或复杂命令。 - 热 key:加随机前缀打散或本地缓存(Caffeine)。
- 大 key:
redis-cli --bigkeys扫描,拆分。 - 内存打满:设置合理的
maxmemory-policy。 - RDB fork 阻塞:控制实例内存大小,避开高峰。
10. Redis 过期键的删除策略?
- 惰性删除:访问 key 时检查是否过期,过期则删。
- 定期删除:每 100ms 随机抽取一部分 key 检查删除。
- 内存淘汰:内存达上限时,按
maxmemory-policy淘汰。
11. Redis 的回收策略(淘汰策略)?
noeviction:不淘汰,写操作报错(适合金融记录)。allkeys-lru:所有 key 参与 LRU 淘汰(推荐缓存场景)。volatile-lru:仅设置了过期时间的 key 参与。allkeys-random:随机淘汰。volatile-ttl:优先淘汰剩余 TTL 最短的 key。
12. 为什么 Redis 需要把所有数据放到内存中?
为了达到微秒级延迟,必须避免磁盘 I/O。内存读写比磁盘快 1000 倍以上。持久化只是数据安全的补充,日常操作完全基于内存。
13. Redis 的同步机制了解么?
主从复制:从节点发送 PSYNC,主节点生成 RDB 快照 + 增量缓冲区。断线重连后根据 replication backlog 尝试增量同步,否则全量同步。
14. Pipeline 有什么好处,为什么要用 pipeline?
将多个命令批量发送,减少网络往返(RTT)。
代码示例:
// 无 pipeline:1000 次网络往返
for (int i = 0; i < 1000; i++) {
redisTemplate.opsForValue().set("key:" + i, "value" + i);
}
// 有 pipeline:1 次网络往返
redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations) {
for (int i = 0; i < 1000; i++) {
operations.opsForValue().set("key:" + i, "value" + i);
}
return null;
}
});
批量导入时性能提升数十倍。
15. 是否使用过 Redis 集群,集群的原理是什么?
使用过。Redis Cluster 采用虚拟哈希槽,CRC16(key) % 16384 映射到 16384 个 slot,每个节点负责部分 slot。客户端直连节点,若 key 不在该节点,返回 MOVED 重定向。每个节点有主从,保证高可用。
代码示例(Spring Boot + Lettuce 连接集群):
spring:
redis:
cluster:
nodes: 10.0.1.1:6379,10.0.1.2:6379,10.0.1.3:6379
max-redirects: 3
16. Redis 集群方案什么情况下会导致整个集群不可用?
- 所有主节点宕机,且从节点无法提升为主。
- slot 不完整:某个 slot 所在的主从全部不可用,如果配置
cluster-require-full-coverage yes(默认),集群整体停止服务。
17. Redis 支持的 Java 客户端都有哪些?官方推荐用哪个?
- Jedis:同步、直连、简单。
- Lettuce:基于 Netty,异步、线程安全,官方推荐。
- Redisson:高级封装,提供分布式锁、队列、对象等。
- 官方推荐 Lettuce,Spring Boot 2.x 默认集成。
18. Jedis 与 Redisson 对比有什么优缺点?
- Jedis:轻量,直接操作 Redis 命令,性能高,但实例非线程安全,需连接池。
- Redisson:高层抽象,像操作 Java 对象一样使用 Redis,提供分布式锁、队列、读写锁等,但额外序列化开销略高。
- 银行项目:一般用 Lettuce 做底层连接,Redisson 做分布式锁。
19. Redis 如何设置密码及验证密码?
服务端(redis.conf):
requirepass your_password
客户端连接:
spring:
redis:
password: your_password
命令验证:
AUTH your_password
20. 说说 Redis 哈希槽的概念?
Redis Cluster 将数据空间划分为 16384 个 slot,每个 key 通过 CRC16(key) & 16383 计算 slot,每个节点负责一部分 slot。移动 slot 即可实现数据迁移和扩缩容。
21. Redis 集群的主从复制模型是怎样的?
每个主节点可以有多个从节点,数据从主异步复制到从。主节点宕机,从节点可被选举提升为新主,保证可用性。
22. Redis 集群会有写操作丢失吗?为什么?
可能会。因为主从复制是异步的,如果主节点宕机时数据还未同步到从,新主节点将丢失这部分数据。可通过 wait 命令强制等待同步,但会降低性能。银行项目中,我们通过业务幂等和最终一致性兜底。
23. Redis 集群之间是如何复制的?
从节点启动时发送 PSYNC,主节点发送快照和命令流。正常运行期间,主节点将写命令异步发送给所有从节点。
24. Redis 集群最大节点个数是多少?
理论上 16384 个主节点,但官方建议不超过 1000 个节点,实际生产中一个集群几十个节点已经很大。
25. Redis 集群如何选择数据库?
集群模式不支持多数据库,只有一个 DB0。SELECT 命令被禁用。
26. 怎么测试 Redis 的连通性?
使用 PING 命令,返回 PONG 即连通。
String result = redisTemplate.getConnectionFactory().getConnection().ping();
System.out.println(result); // "PONG"
27. 怎么理解 Redis 事务?
Redis 事务(MULTI/EXEC)保证命令一次性、顺序执行,不会被打断,但不保证原子性:某条命令执行失败不会回滚其他命令。
代码示例(WATCH 乐观锁):
redisTemplate.watch("product:stock:1001");
String stock = redisTemplate.opsForValue().get("product:stock:1001");
if (Integer.parseInt(stock) > 0) {
redisTemplate.multi();
redisTemplate.opsForValue().decrement("product:stock:1001");
List<Object> result = redisTemplate.exec();
if (result.isEmpty()) {
// 事务被取消,重试
}
}
28. Redis 事务相关的命令有哪几个?
MULTI:开启事务EXEC:执行事务DISCARD:取消事务WATCH:监视 key,乐观锁UNWATCH:取消监视
29. Redis key 的过期时间和永久有效分别怎么设置?
- 过期时间:
EXPIRE key 60(秒)、SETEX key 60 value。 - 永久有效:不设置过期时间,或
PERSIST key移除已有过期时间。
30. Redis 如何做内存优化?
- 使用短 key。
- 使用压缩列表或紧凑编码(默认)。
- 使用 Hash 存储对象,比 String 存 JSON 节省内存。
- 开启内存淘汰策略,及时清理过期 key。
- 对较大值使用 Stream 或分片存储。
31. Redis 回收进程如何工作的?
当内存达到 maxmemory 时,根据 maxmemory-policy 淘汰 key。淘汰过程阻塞命令执行,直到释放出足够内存,但通常很快。
32. 都有哪些办法可以降低 Redis 的内存使用情况呢?
- 设置
maxmemory-policy allkeys-lru。 - 使用
hash-max-ziplist-entries等参数开启紧凑编码。 - 缩短 key 长度。
- 将大对象拆分为多个小 key。
- 使用
volatile-ttl淘汰即将过期的 key。 - 使用连接池减少连接开销。
33. Redis 的内存用完了会发生什么?
- 如果
maxmemory-policy不是noeviction,会淘汰 key 释放空间。 - 如果是
noeviction,所有写入操作返回错误,读操作不受影响。
34. 一个 Redis 实例最多能存放多少的 keys?List、Set、Sorted Set 他们最多能存放多少元素?
- 单实例 key 数量上限:2^32-1(约 42 亿)。
- List、Set、Sorted Set 每个集合内元素数量也是 2^32-1。
- 受限于内存大小,实际远小于理论值。
35. MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis 中的数据都是热点数据?
配置 maxmemory-policy allkeys-lru,内存满时自动淘汰最少使用的数据,保留热点数据。配合 maxmemory 设置合适的内存容量。
36. Redis 最适合的场景?
- 缓存:热点数据,减少 DB 压力。
- 分布式锁:跨进程互斥。
- 计数器:
INCR原子自增。 - 排行榜:Sorted Set。
- 消息队列:List 阻塞弹出、Stream。
- 实时系统:发布订阅。
37. 假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
不能用 KEYS prefix*,会阻塞 Redis。
使用 SCAN cursor MATCH prefix* COUNT 1000 游标迭代,每次返回部分 key,不阻塞主线程。
// Spring 中使用 scan
Set<String> keys = redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
Set<String> set = new HashSet<>();
Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions()
.match("prefix*").count(1000).build());
while (cursor.hasNext()) {
set.add(new String(cursor.next()));
}
return set;
});
38. 如果有大量的 key 需要设置同一时间过期,一般需要注意什么?
需要避免同一时刻大量 key 同时过期引发缓存雪崩。解决方案:在过期时间上加上一个随机值,例如 5~15 分钟的随机浮动。
39. 使用过 Redis 做异步队列么,你是怎么用的?
使用过。基于 List 的阻塞弹出 实现:
// 生产者
redisTemplate.opsForList().leftPush("task:queue", task);
// 消费者(阻塞等待)
while (true) {
Object task = redisTemplate.opsForList().rightPop("task:queue",
0, TimeUnit.SECONDS);
// 处理任务
}
或使用 Redis Stream(更可靠,支持消费者组):
// 发送消息
Map<String, Object> msg = new HashMap<>();
msg.put("orderId", "1001");
redisTemplate.opsForStream().add("order:stream", msg);
// 消费
List<MapRecord<String, Object, Object>> records =
redisTemplate.opsForStream().read(Consumer.from("group", "consumer1"),
StreamReadOptions.empty().count(1),
StreamOffset.create("order:stream", ReadOffset.lastConsumed()));
40. 使用过 Redis 分布式锁么,它是什么回事?
使用过。分布式锁是保证多个服务实例在操作共享资源时互斥访问的一种机制。基于 Redis 的 SETNX + Lua 或 Redisson 实现。
原理:利用 Redis 单线程命令 SET key value NX EX 30 原子加锁,解锁时用 Lua 脚本校验持有者删除,避免误删。
Redisson 代码案例:
RLock lock = redissonClient.getLock("lock:order:1001");
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 处理业务,看门狗自动续期
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
注意事项:锁超时问题要用看门狗或业务幂等兜底;主从切换可能丢失锁,需用红锁或幂等防线。
以上 40 个问题环环相扣,既有理论又有可落地的代码,足以应对绝大多数 Redis 面试。
更多推荐
所有评论(0)