Appearance
Redis 面试题
Redis 基础
Q1: Redis 有哪些数据类型?
┌─────────────────────────────────────────────────────────────────┐
│ Redis 数据类型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ String - 字符串 SDS (Simple Dynamic String) │
│ Hash - 哈希表 Dict (Hash Table) │
│ List - 列表 QuickList (压缩列表+双向链表) │
│ Set - 集合 IntSet + Hash Table │
│ Sorted Set - 有序集合 ZipList + SkipList │
│ │
│ 高级类型: │
│ Bitmap - 位图 String 类型 │
│ HyperLogLog - 基数统计 String 类型 │
│ Geo - 地理坐标 Sorted Set │
│ Stream - 流数据 Radix Tree │
│ │
└─────────────────────────────────────────────────────────────────┘| 类型 | 应用场景 | 底层实现 |
|---|---|---|
| String | 缓存、计数器、分布式锁 | SDS |
| Hash | 对象存储 | Hash Table |
| List | 消息队列、排行榜 | QuickList |
| Set | 标签、好友关系 | IntSet + HT |
| ZSet | 有序集合、排行榜 | SkipList |
| Bitmap | 用户签到、活跃统计 | String |
| HyperLogLog | UV 统计 | String |
| Geo | 附近的人 | Sorted Set |
| Stream | 消息队列 | RadixTree |
Q2: Redis 的持久化机制?
RDB(Redis Database):
┌─────────────────────────────────────────────────────────────────┐
│ RDB 持久化 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 原理:定时生成数据快照 │
│ │
│ 触发方式: │
│ 1. 定时任务(redis.conf 配置) │
│ 2. BGSAVE 命令 │
│ 3. SHUTDOWN 时 │
│ │
│ 优点:恢复速度快,适合备份 │
│ 缺点:可能丢失最后一次快照后的数据 │
│ │
└─────────────────────────────────────────────────────────────────┘AOF(Append Only File):
┌─────────────────────────────────────────────────────────────────┐
│ AOF 持久化 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 原理:记录每个写命令 │
│ │
│ 同步策略: │
│ - always 每条命令同步(最安全,性能最差) │
│ - everysec 每秒同步(推荐) │
│ - no 由系统同步(性能最好,数据最不安全) │
│ │
│ 重写机制: │
│ BGREWRITEAOF - 压缩 AOF 文件 │
│ │
│ 优点:数据安全性高 │
│ 缺点:文件体积大,恢复速度慢 │
│ │
└─────────────────────────────────────────────────────────────────┘RDB 和 AOF 对比:
| 对比 | RDB | AOF |
|---|---|---|
| 文件大小 | 小 | 大 |
| 恢复速度 | 快 | 慢 |
| 数据安全性 | 可能丢失数据 | 可配置 |
| 性能影响 | 后台 fork 进程 | 每秒刷盘 |
| 场景 | 备份、恢复 | 持久化要求高 |
Q3: Redis 的过期策略?
三种过期策略:
bash
# 1. 定时删除(定时扫描)
# 设置过期时间时创建定时器,到期删除
# 优点:内存友好
# 缺点:CPU 压力大
# 2. 惰性删除(访问时检查)
# 访问 key 时检查是否过期
# 优点:CPU 友好
# 缺点:内存可能泄漏
# 3. 定期删除(折中方案)
# 周期性扫描,定期检查
# Redis 默认使用惰性删除 + 定期删除内存淘汰策略:
bash
# 当内存达到 maxmemory 时
maxmemory-policy volatile-lru
# 策略选项:
# - noeviction 不淘汰,返回错误
# - allkeys-lru 所有 key LRU 淘汰
# - volatile-lru 有过期时间的 key LRU 淘汰
# - allkeys-random 所有 key 随机淘汰
# - volatile-random 有过期时间的 key 随机淘汰
# - volatile-ttl 有过期时间的 key 优先淘汰 TTL 短的Q4: Redis 的线程模型?
┌─────────────────────────────────────────────────────────────────┐
│ Redis 单线程模型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ Client │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ I/O 多路复用│ │
│ │ (epoll/select)│ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 命令队列 │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ 单线程执行 │ ← Redis 6.0 之前是单线程 │
│ │ (SocketIO + 命令执行) │ │
│ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘注意:
- Redis 6.0 之前是单线程(I/O 和命令执行都是单线程)
- Redis 6.0 之后 I/O 变成多线程,但命令执行还是单线程
- 持久化、集群通信等由后台线程处理
Redis 集群
Q5: Redis 主从复制原理?
┌─────────────────────────────────────────────────────────────────┐
│ 主从复制架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ │
│ │ Master │ │
│ │ (主节点) │ │
│ └────┬─────┘ │
│ │ │
│ │ SYNC / PSYNC │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Slave 1 │ │Slave 2 │ │Slave 3 │ │
│ │ (从节点) │ │ (从节点) │ │ (从节点) │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
│ 主节点:写 + 读 │
│ 从节点:读(分担压力) │
│ │
└─────────────────────────────────────────────────────────────────┘复制过程:
- 从节点发送
SYNC命令 - 主节点执行
BGSAVE,生成 RDB 文件 - 主节点发送 RDB 文件给从节点
- 从节点加载 RDB 文件
- 主节点发送缓冲区中的写命令
- 后续主节点的写命令同步发送给从节点
Redis 4.0+ 支持 PSYNC:
- 完整同步(PSYNC ?-1):首次复制
- 部分同步(PSYNC <runid> <offset>):断线重连
Q6: Redis Sentinel(哨兵)原理?
┌─────────────────────────────────────────────────────────────────┐
│ Redis Sentinel 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ │
│ │ Sentinel │ │
│ │ (哨兵) │ │
│ └────┬─────┘ │
│ │ │
│ ┌──────────────┼──────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Master │ │ Slave1 │ │ Slave2 │ │
│ └────┬───┘ └────────┘ └────────┘ │
│ │ │
│ │ 故障检测 │
│ ▼ │
│ 自动故障转移 │
│ │
└─────────────────────────────────────────────────────────────────┘哨兵职责:
- 监控:定期检查主从节点是否存活
- 通知:故障时通知应用方
- 自动故障转移:主节点宕机时自动选举新主节点
- 配置中心:提供当前主节点地址
Q7: Redis Cluster 原理?
┌─────────────────────────────────────────────────────────────────┐
│ Redis Cluster 架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 16384 个槽位分片 │
│ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
│ │ 0-5460 │ │5461-10922│10923-16383│ │
│ └────┬───┘ └────┬───┘ └────┬───┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │Master 1│ │Master 2│ │Master 3│ │
│ └────────┘ └────────┘ └────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Slave 1 │ │ Slave 2 │ │ Slave 3 │ │
│ └────────┘ └────────┘ └────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘特点:
- 数据分片(16384 个槽)
- 去中心化(无单一主节点)
- 高可用(每个主节点至少一个从节点)
- 自动故障转移
Redis 分布式锁
Q8: Redis 分布式锁如何实现?
基本实现:
java
// 加锁
public boolean lock(String key, String value, long expireTime) {
String result = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
// 解锁(Lua 脚本保证原子性)
public boolean unlock(String key, String value) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
return redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
value
) == 1L;
}为什么要用 Lua 脚本?
- 解锁时需要先判断锁是否是自己加的,再删除
- 如果分两步执行,在判断和删除之间锁过期了,会误删别人的锁
- Lua 脚本保证这两步原子执行
Q9: Redisson 分布式锁?
java
// Redisson 分布式锁
RLock lock = redissonClient.getLock("order:lock:" + orderId);
try {
// 等待锁30秒,锁定后自动60秒释放
if (lock.tryLock(30, 60, TimeUnit.SECONDS)) {
// 业务逻辑
}
} finally {
lock.unlock();
}Redisson 特点:
- 自动续期(看门狗机制)
- 可重入锁
- 公平锁
- 读写锁
缓存问题
Q10: 什么是缓存穿透?如何解决?
缓存穿透:查询不存在的数据,每次都穿透到数据库
┌─────────────────────────────────────────────────────────────────┐
│ 缓存穿透示意 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 请求 ──→ Redis ──→ DB ──→ 返回 NULL │
│ ↑ ↑ │
│ │ │ │
│ 缓存不存在 数据库也不存在 │
│ │ │ │
│ ←─────────┘ │
│ 每次请求都打满数据库 │
│ │
└─────────────────────────────────────────────────────────────────┘解决方案:
1. 布隆过滤器
java
BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
1000000, // 预期元素数量
0.01 // 误判率
);
// 查询前先检查布隆过滤器
if (!bloomFilter.mightContain(key)) {
return null; // 一定不存在
}2. 缓存空值
java
// 缓存空值,设置较短过期时间
if (user == null) {
redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES);
} else {
redisTemplate.opsForValue().set(key, user, 1, TimeUnit.HOURS);
}Q11: 什么是缓存击穿?如何解决?
缓存击穿:热点 key 过期,瞬间大量请求打满数据库
┌─────────────────────────────────────────────────────────────────┐
│ 缓存击穿示意 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 热点 key 过期 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 瞬间大量请求同时发现缓存不存在 │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ DB │
│ │
└─────────────────────────────────────────────────────────────────┘解决方案:
1. 互斥锁
java
public User getUserMutex(Long id) {
String key = "user:" + id;
User user = redisTemplate.opsForValue().get(key);
if (user == null) {
String lockKey = "lock:" + key;
String lockValue = UUID.randomUUID().toString();
if (redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS)) {
try {
user = userDao.findById(id);
redisTemplate.opsForValue().set(key, user, 1, TimeUnit.HOURS);
} finally {
redisTemplate.delete(lockKey);
}
} else {
Thread.sleep(50);
return getUserMutex(id);
}
}
return user;
}2. 热点数据永不过期 + 异步更新
Q12: 什么是缓存雪崩?如何解决?
缓存雪崩:大量缓存同时过期,导致数据库压力过大
┌─────────────────────────────────────────────────────────────────┐
│ 缓存雪崩示意 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 大量 key 同时过期 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 大量请求同时发现缓存过期 │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ DB │
│ │
└─────────────────────────────────────────────────────────────────┘解决方案:
1. 过期时间加随机值
java
redisTemplate.opsForValue().set(key, value, 1 + random.nextInt(5), TimeUnit.HOURS);2. 热点数据永不过期
3. Redis Cluster 高可用
4. 服务降级/限流
Redis 实战
Q13: Redis 和 Memcached 的区别?
| 对比 | Redis | Memcached |
|---|---|---|
| 数据类型 | 丰富(5种+) | 只有 String |
| 持久化 | 支持 RDB/AOF | 不支持 |
| 集群 | 原生支持 Cluster | 需二次开发 |
| 线程模型 | 单线程 + 多线程(I/O) | 多线程 |
| 内存管理 | 自行实现 | slab allocation |
| 适用场景 | 复杂数据结构、持久化 | 简单缓存 |
Q14: Redis 的并发竞争问题?
问题:多个客户端同时修改同一个 key,导致数据不一致
解决方案:
1. 分布式锁(前面已讲)
2. WATCH + MULTI + EXEC(乐观锁)
java
// 监视 key
redisTemplate.watch(key);
// 开启事务
redisTemplate.multi();
// 修改操作
redisTemplate.opsForValue().increment(key);
// 执行事务
redisTemplate.exec();3. 队列串行化 将并发请求放入队列,串行处理
Q15: Redis 性能优化?
1. 避免大 key
bash
# 查看大 key
redis-cli --bigkeys
# 删除大 key 用 UNLINK(异步删除)
redis-cli UNLINK big_key2. 使用 Pipeline
java
// 批量命令,减少网络开销
List<Object> results = redisTemplate.executePipelined(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
for (int i = 0; i < 10000; i++) {
operations.opsForValue().increment("key:" + i);
}
return null;
}
});3. 合理选择数据结构
java
// 存储用户信息,用 Hash 而不是 String
redisTemplate.opsForHash().put("user:100", "name", "张三");
// 排行榜,用 ZSet
redisTemplate.opsForZSet().add("ranking", "user:100", 1000);总结
Redis 高频面试知识点:
| 知识点 | 面试频率 |
|---|---|
| 5 种数据结构 | ⭐⭐⭐⭐⭐ |
| 持久化 RDB/AOF | ⭐⭐⭐⭐ |
| 主从复制原理 | ⭐⭐⭐⭐ |
| Sentinel / Cluster | ⭐⭐⭐⭐ |
| 分布式锁 | ⭐⭐⭐⭐⭐ |
| 缓存穿透/击穿/雪崩 | ⭐⭐⭐⭐⭐ |
| Redis 线程模型 | ⭐⭐⭐⭐ |
| 内存淘汰策略 | ⭐⭐⭐ |