Redis 深入原理
2026/4/18大约 9 分钟Java面试Redis缓存持久化分布式锁集群缓存穿透
Redis 深入原理
Redis 数据结构底层实现、RDB/AOF 持久化、哨兵/集群架构、缓存三大问题、分布式锁——腾讯/字节 Redis 必考全覆盖。
核心原理
Redis 数据结构及底层编码
Redis 对外暴露 5 种基本类型(+Stream、HyperLogLog 等),底层根据数据量自动选择紧凑编码:
| 类型 | 底层编码 | 切换条件 |
|---|---|---|
| String | int / embstr(<= 44字节)/ raw | 数值用 int,短字符串用 embstr(一次内存分配) |
| List | listpack(小数据)/ quicklist(大数据) | 元素数 > 128 或单元素 > 64 字节切 quicklist |
| Hash | listpack / hashtable | 字段数 > 128 或 value > 64 字节切 hashtable |
| Set | listpack / intset / hashtable | 全整数 <= 512 用 intset,否则 hashtable |
| ZSet | listpack / skiplist+hashtable | 元素数 > 128 或单元素 > 64 字节切 skiplist |
跳表(SkipList)原理(ZSet 底层,面试常考):
Level 4: 1 ────────────────────────────────── 9
Level 3: 1 ──────────── 5 ────────────────── 9
Level 2: 1 ──── 3 ──── 5 ──── 7 ────────── 9
Level 1: 1 ─ 2 ─ 3 ─ 4 ─ 5 ─ 6 ─ 7 ─ 8 ─ 9- 查找时从最高层开始,时间复杂度 O(log N)
- 比平衡树简单,插入删除无需旋转,实现更简洁
- ZSet 同时维护 skiplist(范围查询)+ hashtable(O(1) 按 key 查分值)
Redis 持久化:RDB vs AOF vs 混合持久化
RDB(Redis DataBase):
- 周期性生成内存快照,存储二进制文件(dump.rdb)
- 触发方式:
SAVE(同步阻塞)/BGSAVE(fork 子进程,推荐)/ 配置save 900 1 - 优点:文件紧凑,恢复速度快;缺点:可能丢失最近几分钟数据
AOF(Append Only File):
- 记录每条写命令(RESP 格式),追加到 aof 文件
- 刷盘策略:
always(每条都 fsync,最安全)/everysec(每秒 fsync,丢 1 秒)/no(OS 决定,风险最大) - AOF 重写(
BGREWRITEAOF):压缩 AOF 文件,去除冗余命令
混合持久化(Redis 4.0+,推荐):
AOF 重写时:
┌─────────────────────────────────────────────┐
│ RDB 格式快照(紧凑) │ 增量 AOF 命令(增量)│
└─────────────────────────────────────────────┘
重启时:先加载 RDB 快照,再回放增量 AOF,恢复快且数据全配置:aof-use-rdb-preamble yes
下图以时序图对比了 RDB 快照和 AOF 追加日志两种持久化机制的工作流程:
高频面试题
Q: 缓存穿透、缓存击穿、缓存雪崩如何区分和解决?(阿里/美团 必考三连)
缓存穿透: 查询不存在的 key,缓存和 DB 都没有,每次都打到 DB。
下图展示了缓存穿透的判断与解决方案决策:
// 解决方案1:缓存空值(简单,推荐) Object value = cache.get(key); if (value == null) { value = db.query(key); if (value == null) { cache.set(key, NULL_OBJECT, Duration.ofMinutes(5)); // 缓存空值,5分钟过期 } else { cache.set(key, value, Duration.ofHours(1)); } } // 解决方案2:布隆过滤器(BitMap,内存极小,准确率高) BloomFilter<String> filter = BloomFilter.create(Funnels.stringFunnel(UTF_8), 1_000_000, 0.001); // 初始化时将所有有效 key 加入布隆过滤器 if (!filter.mightContain(key)) { return null; // 布隆过滤器不存在,直接返回(误判率 0.1%) }缓存击穿: 热点 key 过期的瞬间,大量并发请求同时打到 DB(重建缓存风暴)。
下图展示了缓存击穿的互斥锁与逻辑过期两种解决方案的决策路径:
// 解决方案1:互斥锁(保证只有一个线程重建缓存) public Object getWithMutex(String key) { Object value = cache.get(key); if (value != null) return value; String lockKey = "lock:" + key; if (redisLock.tryLock(lockKey, 30, TimeUnit.SECONDS)) { try { value = cache.get(key); // 双重检查,避免已被其他线程重建 if (value == null) { value = db.query(key); cache.set(key, value, Duration.ofHours(1)); } } finally { redisLock.unlock(lockKey); } } else { Thread.sleep(50); // 等待重建完成,重试 return getWithMutex(key); } return value; } // 解决方案2:逻辑过期(不设 TTL,数据中存过期时间,异步更新) // 适合极热点 key,性能更好,但可能短暂返回旧数据缓存雪崩: 大量 key 同时过期 / Redis 宕机,全部流量涌入 DB。
下图展示了缓存雪崩的多层防御解决方案:
// 解决1:过期时间加随机抖动(防止同时过期) long baseExpiry = 3600; // 1小时 long jitter = ThreadLocalRandom.current().nextLong(0, 600); // 随机 0-10分钟 cache.set(key, value, Duration.ofSeconds(baseExpiry + jitter)); // 解决2:Redis 集群高可用(哨兵/Cluster 避免单点故障) // 解决3:熔断降级(Sentinel)+ 限流(令牌桶) // 解决4:多级缓存(本地 Caffeine + Redis)
Q: Redis 分布式锁的实现原理?Redisson 看门狗机制?(腾讯 P6 必问)
基础实现:
# 原子操作:不存在时设值+过期时间 SET lock_key unique_value NX PX 30000 # NX = 不存在时才设置(互斥),PX = 毫秒级过期(防死锁)释放锁必须用 Lua 脚本(保证判断+删除的原子性,防误删):
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 endRedisson 看门狗(Watchdog):
- 获取锁后启动后台线程,每隔
leaseTime/3(默认 10 秒)续期至 leaseTime(30 秒)- 客户端宕机后看门狗停止,锁在 leaseTime 后自动过期,防死锁
- 底层使用 Redis Hash:
Hash key = 锁名,Hash field = UUID:threadId,Hash value = 重入次数// Redisson 使用示例 RLock lock = redissonClient.getLock("order:lock:" + orderId); try { boolean locked = lock.tryLock(0, TimeUnit.SECONDS); // 不等待,立即失败 if (!locked) throw new BizException("操作频繁,请稍后重试"); // 业务逻辑(看门狗自动续期) processOrder(orderId); } finally { if (lock.isHeldByCurrentThread()) lock.unlock(); }
Q: Redis 哨兵模式和 Cluster 模式的区别?如何选择?(字节)
下图展示了哨兵模式(左)与 Cluster 模式(右)的拓扑结构对比:
哨兵模式(Sentinel):
- 一主多从 + 哨兵集群(>=3 个哨兵节点)
- 哨兵监控主节点,主节点宕机后自动选举新主(Raft 变体)
- 缺点: 所有数据在单机上,内存上限受限;写操作只能打主节点
- 适用: 数据量 < 单机内存(如 16-32GB),高可用要求,不需要水平扩展
Cluster 模式:
- 数据按 16384 个 slot 分片分布在多个主节点(hash slot = CRC16(key) % 16384)
- 每个主节点可有多个从节点(主从切换自动完成)
- 客户端持有 slot 路由表,直接路由到正确节点(MOVED 重定向)
- 缺点: 多 key 操作(mget/mset/pipeline)需要在同一 slot(用
{}指定 hash tag);不支持跨 slot 事务- 适用: 数据量大(> 单机内存),需要水平扩展写能力
# Cluster 模式的 hash tag(强制同一 slot) SET {user:1}:profile "..." SET {user:1}:orders "..." # 两个 key 都路由到 user:1 对应的 slot MGET {user:1}:profile {user:1}:orders # 可以在同一节点执行
Q: Redis 的 Pipeline、事务(MULTI/EXEC)、Lua 脚本的区别?(阿里)
特性 Pipeline MULTI/EXEC 事务 Lua 脚本 原子性 否(仅批量发送,减少 RTT) 弱原子(执行中命令出错不回滚) 强原子(脚本执行不可中断) 网络往返 1 次(多命令打包) 2 次(MULTI+EXEC) 1 次 错误处理 各命令独立 语法错误中止,运行时错误跳过 脚本内可处理 适用场景 批量读写加速 简单复合操作 原子复合操作(如分布式锁) // Pipeline 示例(Jedis) Pipeline pipeline = jedis.pipelined(); for (String key : keys) { pipeline.get(key); } List<Object> results = pipeline.syncAndReturnAll(); // Lua 脚本(原子扣减库存) String script = """ local stock = tonumber(redis.call('GET', KEYS[1])) if stock <= 0 then return -1 end redis.call('DECRBY', KEYS[1], ARGV[1]) return stock - tonumber(ARGV[1]) """; Long remaining = (Long) jedis.eval(script, Collections.singletonList("stock:" + productId), Collections.singletonList("1"));
Q: Redis 的内存淘汰策略有哪些?如何选择?(美团)
8 种淘汰策略(Redis 4.0+):
noeviction(默认):内存满时新写操作报错,不淘汰任何数据allkeys-lru:从所有 key 中淘汰最近最少使用的(推荐缓存场景)volatile-lru:只从设置了过期时间的 key 中淘汰最近最少使用的allkeys-random:随机淘汰volatile-random:从有过期时间的 key 中随机淘汰volatile-ttl:从有过期时间的 key 中淘汰 TTL 最小的(最快过期的)allkeys-lfu(Redis 4.0+):淘汰使用频率最低的(LFU 算法,比 LRU 更合理)volatile-lfu:从有过期时间的 key 中淘汰使用频率最低的推荐选择:
- 纯缓存(所有数据可重建):
allkeys-lru或allkeys-lfu- 缓存 + 持久化混用:
volatile-lru(只淘汰缓存 key,保留持久化 key)
知识星球深度内容
完整大厂面经实录(字节/阿里/腾讯/美团)、简历 1v1 修改、每周高频题精讲,扫码加入「AI 工程师加速社区」知识星球 👉 立即加入
