Redis 面试题大全
一、基础概念
Q1: Redis 的版本是多少?
Redis 目前主流版本是 Redis 7.0(2022年发布),生产环境中也常用 5.0、6.0 版本。Redis 7.0 引入了函数(Functions)、ACL 改进、命令分区等新特性。
Q2: Redis 的默认端口是多少?
6379。这个端口号是 Redis 作者 Antirez(Salvatore Sanfilippo)随手选的,没有任何特殊含义。
Q3: 为什么要用 Redis?
| 优势 | 说明 |
|---|---|
| 高性能 | 基于内存操作,读写速度极快(QPS 可达 10万+) |
| 丰富数据类型 | String、List、Hash、Set、ZSet 等,满足多样化需求 |
| 高可用 | 支持主从复制、哨兵模式、集群模式 |
| 支持持久化 | RDB 和 AOF 两种持久化机制 |
| 天然支持分布式 | 可轻松实现分布式锁、计数器、限流等 |
| 应用场景广泛 | 缓存、Session共享、消息队列、排行榜、分布式锁等 |
Q4: Redis 为什么快?
- 基于内存存储:数据存储在内存中,避免了磁盘 I/O 开销
- 单线程模型:避免了上下文切换和锁竞争(6.0前确实是单线程)
- 多路复用 I/O:使用 epoll/select/kqueue 等机制实现高并发
- 高效数据结构:底层使用 SDS、跳表、压缩列表等高效数据结构
- C 语言实现:贴近系统层,执行效率高
⚠️ Redis 6.0 引入了多线程,但主命令执行仍是单线程,多线程仅用于网络 I/O 处理。
Q5: Redis 的应用场景有哪些?
| 场景 | 说明 |
|---|---|
| 缓存 | 热数据缓存,减轻数据库压力 |
| Session共享 | 多节点 Web 应用 Session 共享 |
| 分布式锁 | 基于 SET NX EX 实现分布式锁 |
| 排行榜/计数器 | ZSet 实现有序集合,List 实现点赞计数 |
| 消息队列 | List 实现轻量级队列,Pub/Sub 实现发布订阅 |
| 限流 | 基于 INCR 实现滑动窗口限流 |
| 验证码/Token | 存储短信验证码、JWT Token |
| 热点数据发现 | 存储热点 Key,便于监控和预加载 |
二、数据类型
Q6: Redis 的基本数据类型有哪些?区别和应用场景?
| 类型 | 结构 | 特点 | 应用场景 |
|---|---|---|---|
| String | 字符串 | 最基础类型,可存储任何数据 | 缓存、计数器、分布式Session、Token |
| List | 双向链表 | 有序、可重复、支持两端操作 | 消息队列、最新列表、关注列表 |
| Hash | HashMap | 字段值对,适合存储对象 | 商品信息、用户画像、配置表 |
| Set | HashSet | 无序、不可重复 | 标签系统、好友关系、抽奖、去重 |
| ZSet | 有序集合 | 不可重复、有分数排序 | 排行榜、权重排序、带权重的任务队列 |
| Bitmap | 位图 | 适合大数据量布尔统计 | 签到统计、用户在线状态、布隆过滤器 |
| HyperLogLog | 基数统计 | 极低内存统计去重数量 | UV统计、日活统计 |
| Geospatial | 地理坐标 | 存储地理位置信息 | 附近的人、距离计算 |
| Stream | 日志流 | 支持消费组的消息队列 | 日志收集、消息队列替代方案 |
Q7: 各数据类型的命令(添加/查询)
| 类型 | 添加命令 | 查询命令 |
|---|---|---|
| String | SET key value / SETNX key value | GET key / MGET key1 key2 |
| List | LPUSH key value / RPUSH key value | LRANGE key 0 -1 / LINDEX key index |
| Hash | HSET key field value | HGET key field / HGETALL key |
| Set | SADD key member | SMEMBERS key / SISMEMBER key member |
| ZSet | ZADD key score member | ZRANGE key 0 -1 / ZSCORE key member |
三、持久化机制
Q8: Redis 的持久化机制有什么区别?
| 方式 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| RDB | 定时生成数据快照,fork子进程dump为.rdb文件 | 文件紧凑、恢复快、适合备份 | 可能丢失最近数据,fork时阻塞 |
| AOF | 记录每个写操作命令到.aof文件 | 数据安全性高,最多丢失1秒数据 | 文件较大,恢复慢 |
配置策略建议:
# RDB 配置
save 900 1 # 900秒内1次修改触发
save 300 10 # 300秒内10次修改触发
save 60 10000 # 60秒内10000次修改触发
# AOF 配置
appendonly yes
appendfsync everysec # 推荐:每秒同步
推荐组合使用:RDB + AOF,RDB 做快速备份,AOF 做数据安全。
Q9: Redis 的 AOF 日志文件中会有查询的命令吗?
不会。AOF 只记录写操作命令(如 SET、INCR、HSET),不记录读操作命令(如 GET、HGET、SMEMBERS)。这是 Redis 设计的有意为之,原因:
- 避免记录大量冗余的读命令导致文件膨胀
- 读命令不会修改数据,不需要记录
- 纯读操作在重启恢复时无实际意义
四、过期删除与内存淘汰
Q10: Redis 的过期删除策略?
Redis 采用 惰性删除 + 定期删除 组合策略:
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 惰性删除 | 访问Key时检查是否过期,过期则删除 | 节省CPU资源 | 过期Key占用内存 |
| 定期删除 | 每隔一段时间随机检查部分Key,删除过期的 | 解决内存占用问题 | 影响CPU性能 |
主动清理流程:Redis 每 100ms 从数据库中随机抽取 CHECKED_KEYS 数量检查,过期则删除。
Q11: Redis 的内存淘汰策略?
当内存达到 maxmemory 时的淘汰策略:
| 策略 | 说明 |
|---|---|
noeviction | 不淘汰,返回错误(默认) |
volatile-lru | 从设置了过期时间的Key中淘汰最近最少使用的 |
allkeys-lru | 从所有Key中淘汰最近最少使用的 |
volatile-lfu | 从设置了过期时间的Key中淘汰使用频率最低的 |
allkeys-lfu | 从所有Key中淘汰使用频率最低的 |
volatile-random | 从设置了过期时间的Key中随机淘汰 |
allkeys-random | 从所有Key中随机淘汰 |
volatile-ttl | 淘汰TTL最短的Key |
推荐使用:allkeys-lru(通用场景)或 volatile-lru(缓存层与持久层分离时)。
五、高可用架构
Q12: Redis 主从模式和哨兵模式有什么区别?
| 对比项 | 主从模式 | 哨兵模式 |
|---|---|---|
| 核心功能 | 数据复制、读写分离 | 自动故障转移、监控 |
| 故障恢复 | 需手动切换 | 自动切换 |
| 复杂度 | 简单 | 相对复杂 |
| 主库挂了 | 需人工干预 | 哨兵自动选举新主库 |
| 适用场景 | 数据备份、读写分离 | 生产环境高可用 |
本质区别:哨兵模式 = 主从复制 + 自动故障转移 + 监控
Q13: Redis 哨兵模式的工作原理?
┌─────────┐
│ 客户端 │
└────┬────┘
│ 询问主库地址
▼
┌─────────┐
│ 哨兵组 │ ←── 监控 + 故障检测 + 选举
│(1+3个) │
└────┬────┘
│ 监控
▼
┌─────────┐ 复制 ┌─────────┐
│ 主库 │ ──────────▶ │ 从库 │
└─────────┘ └─────────┘
工作流程:
- 监控:哨兵周期性 ping 主库、从库、哨兵节点
- 通知:将主从信息通知给订阅的客户端
- 自动故障转移:
- 主库超时 → 标记为主观下线
- 多数哨兵认为下线 → 标记为客观下线
- 投票选举 → 选出一个从库升级为主库
- 通知其他从库切换主库
- 告知客户端新主库地址
Q14: Redis 分片集群有什么特点?哈希槽有什么用?总共有多少个?
分片集群特点:
- 数据分布在多个主节点上,每个节点存储部分数据
- 支持横向扩展,可添加节点分担压力
- 无中心架构,客户端直连任意节点
- 每个节点可配置从节点实现高可用
哈希槽(Hash Slot):
- 总共 16384 个(2^14)槽位
- 通过 CRC16(key) % 16384 决定 Key 存储位置
- 槽位均匀分配给各主节点,如3节点:0-5460、5461-10922、10923-16383
为什么是16384个?
- 槽位信息需要广播到所有节点,节点间通过心跳传输
- 节点间每次心跳包包含 n 个槽位状态(大约2KB)
- 16384 × 2Byte ≈ 32KB,合理范围
- 65536 个槽位则每次心跳约64KB,过大
六、数据一致性
Q15: Redis 和 MySQL 的数据如何保证一致性?
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Cache Aside | 读:先缓存再DB;写:先DB再删缓存 | 最常用 | 可能短暂不一致 |
| Read Through | 缓存不存在时,缓存层自动加载 | 应用层代码简单 | 实现复杂 |
| Write Through | 写数据时同步写缓存和DB | 强一致 | 性能差 |
| Write Behind | 异步写,先更新缓存再批量写DB | 性能高 | 可能丢数据 |
最常用方案:Cache Aside
// 读
String data = redis.get(key);
if (data == null) {
data = mysql.query(key);
redis.setex(key, ttl, data);
}
// 写
mysql.update(key, value);
redis.del(key); // 注意:是删除不是更新
七、缓存三兄弟
Q16: Redis 的缓存三兄弟是什么?
缓存三兄弟:缓存击穿、缓存穿透、缓存雪崩
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 缓存击穿 | 热点Key过期,瞬间大量请求击穿到DB | 互斥锁 / 永不过期 + 异步更新 / 热点数据不过期 |
| 缓存穿透 | 查询不存在的数据,每次都到DB | 布隆过滤器 / 空值缓存 / 参数校验 |
| 缓存雪崩 | 大量Key同时过期 | 过期时间加随机值 / 多级缓存 / 服务降级限流 |
代码示例:
// 互斥锁解决击穿
String lockKey = "lock:" + key;
if (redis.setnx(lockKey, "1", 10)) {
// 获取到锁,查询DB
String data = mysql.query(key);
redis.setex(key, ttl, data);
redis.del(lockKey);
} else {
// 没获取到锁,短暂等待后重试
Thread.sleep(50);
return getData(key);
}
// 布隆过滤器解决穿透
if (!bloomFilter.contains(key)) {
return null; // 一定不存在
}
八、Java 客户端
Q17: 常用的 Redis 的 JavaAPI 类有哪些?
| 客户端 | 特点 |
|---|---|
| Jedis | 轻量级,连接池,支持所有命令 |
| Lettuce | Spring Boot 2.x 默认,支持响应式,线程安全 |
| Redisson | 分布式扩展,提供锁、Map等高级数据结构 |
// Jedis 示例
Jedis jedis = new Jedis("localhost", 6379);
jedis.set("name", "张三");
String value = jedis.get("name");
// Redisson 示例
RedissonClient client = Redisson.create();
RLock lock = client.getLock("myLock");
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock();
}
九、实际业务设计
Q18: 你负责的这块用的是哪个 Redis 类型?Key 和 Value 是怎么设计的?
示例回答:
我们系统使用 Redis 主要存储两类数据:
- 用户 Session(String)
- Key:
session:userId:{userId}- Value: 用户信息 JSON 字符串
- TTL: 30分钟
- 接口限流(String + Lua 脚本)
- Key:
rate:api:{apiId}:{userId}:{时间窗口}- Value: 计数器
- 热点商品缓存(Hash)
- Key:
product:{productId}- Field:
name,price,stock- Value: 对应值
- 商品排行榜(ZSet)
- Key:
ranking:sales:daily- Member: 商品ID
- Score: 销量
十、分布式锁
Q19: Redis 的分布式锁了解吗?还知道其他分布式锁吗?
Redis 分布式锁:基于 SET NX PX 命令实现
其他分布式锁方案:
| 方案 | 特点 |
|---|---|
| Redis Redisson | 支持重入、看门狗续期 |
| ZooKeeper | 有序节点,临时顺序节点实现锁 |
| etcd | 基于 Raft 协议,CP 模型 |
| 数据库 | 表锁行锁,性能差 |
| Consul | KV 存储,支持 TTL |
对比:Redis 最常用(性能高),ZooKeeper/etcd 更可靠(CP),根据场景选择。
Q20: 分布式锁底层原理?
# 加锁(SET NX EX 原子操作)
SET lock_key unique_value NX PX 30000
# 解锁(Lua 脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
关键点:
- NX:保证互斥,只有不存在时才能加锁
- PX/EX:设置过期时间,防止死锁
- Value 唯一性:解锁时验证是否为同一客户端(防止误删他人锁)
- Lua 脚本:解锁操作原子性
Q21: Redis 设置分布式锁的命令是什么?
# 基础版(可能死锁)
SET lock_key unique_value NX EX 30
# 完整版
SET lock_key unique_value NX PX 30000
注意:SET NX EX 是原子命令,不要分两步执行:
# ❌ 错误:非原子,可能死锁
SETNX lock_key unique_value
EXPIRE lock_key 30
# ✅ 正确:原子操作
SET lock_key unique_value NX EX 30
Q22: Redis 设置过期时间的命令是什么?
# 设置 TTL(秒)
EXPIRE key 30
# 设置过期时间点(时间戳)
EXPIREAT key 1700000000
# 毫秒级 TTL
PEXPIRE key 30000
# 设置值的同时指定过期时间
SET key value EX 30
SET key value PX 30000
SETEX key 30 value # 等价于 SET + EXPIRE
# 查询剩余 TTL
TTL key # 秒
PTTL key # 毫秒
十一、缓存预热
Q23: Redis 的缓存预热知道吗?
缓存预热:系统启动或高峰期前,将热点数据预先加载到 Redis 缓存中。
为什么需要?
- 避免冷启动时大量请求击穿到数据库
- 减少首次访问的响应延迟
实现方式:
// 1. 项目启动时
@PostConstruct
public void cacheWarmup() {
// 加载首页数据
List<Product> hotProducts = productService.getHotProducts();
for (Product p : hotProducts) {
redisTemplate.opsForValue().set(
"product:" + p.getId(),
JSON.toJSONString(p),
1, TimeUnit.HOURS
);
}
}
// 2. 定时任务预热
@Scheduled(cron = "0 0 6 * * ?") // 每天6点
public void scheduledWarmup() {
loadHotData();
}
// 3. 异步预热
public void getData(String key) {
String data = redis.get(key);
if (data == null) {
data = db.query(key);
redis.setex(key, ttl, data);
// 异步预热相关数据
asyncWarmupRelatedKeys(key);
}
return data;
}
十二、Redisson 专题
Q24: Redisson 的执行流程以及底层原理?
┌──────────┐ 1.获取连接 ┌──────────┐
│ 客户端 │ ──────────────▶ │ Redisson │
└──────────┘ │ Client │
│ └────┬─────┘
│ 2.获取锁 │ 3.发送命令
▼ ▼
┌──────────┐ 4.维护租约 ┌──────────┐
│ Lua脚本 │ ◀───────────────▶ │ Redis │
└──────────┘ │ Server │
└──────────┘
核心流程:
- 客户端调用
redissonClient.getLock("key") - 获取连接,发送 Lua 命令到 Redis
- Redis 执行
SET lock_key unique_value NX PX timeout - 启动看门狗(如果没有指定 leaseTime)
- 客户端获取到锁,开始执行业务
- 业务完成后调用
unlock() - 解锁时执行 Lua 脚本验证并删除
Q25: Redisson 分布式锁是可重入的吗?
是的,Redisson 锁是可重入的。
RLock lock = redisson.getLock("myLock");
lock.lock(); // 第一次获取
lock.lock(); // 重入,同一线程再次获取
try {
// 业务逻辑
} finally {
lock.unlock(); // 重入次数-1,仍持有锁
lock.unlock(); // 重入次数归0,释放锁
}
原理:可重入次数存储在 Hash 结构中
-- 存疑:实际 key 存储格式
lock {
threadId: 重入次数
}
Q26: Redisson 加锁解锁是怎么保证原子性的?
加锁原子性:使用单个 Lua 脚本
-- 加锁脚本
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
redis.call('pexpire', KEYS[1], ARGV[2])
return 1
else
-- 处理重入
if redis.call('hexists', KEYS[1], ARGV[1]) == 1 then
redis.call('hincrby', KEYS[1], ARGV[1], 1)
redis.call('pexpire', KEYS[1], ARGV[2])
return 1
end
return 0
end
解锁原子性:Lua 脚本验证 + 删除
-- 解锁脚本
if redis.call('get', KEYS[1]) == ARGV[1] then
if redis.call('del', KEYS[1]) == 1 then
return 1
end
end
return 0
Q27: Redisson 锁的类型?
| 锁类型 | 说明 | 使用 |
|---|---|---|
| RLock | 可重入锁 | client.getLock("key") |
| RReadWriteLock | 读写锁 | client.getReadWriteLock("key") |
| RFairLock | 公平锁(先到先得) | client.getFairLock("key") |
| RMultiLock | 联锁(全部获取才生效) | client.getMultiLock(lock1, lock2) |
| RRedLock | 红锁(多节点多数生效) | client.getRedLock(lock1, lock2) |
| RSemaphore | 信号量 | client.getSemaphore("key") |
| RCountDownLatch | 倒计数器 | client.getCountDownLatch("key") |
Q28: 看门狗机制续约是无限的吗?
不是无限的。
- 看门狗默认每
10秒检查一次 - 每次续期
30秒(lockWatchdogTimeout) - 最多续期到
lockLeaseTime(默认30秒) - 如果设置了 leaseTime,看门狗不启动
- 看门狗在锁持有期间持续续期,直到手动释放
Q29: 看门狗机制什么时候会决定续期?
触发条件:锁持有者仍在持有锁且未设置 leaseTime
启动加锁
│
▼
设置了 leaseTime? ───是───▶ 不启动看门狗
│
否
│
▼
启动看门狗线程
│
▼
每 10 秒检查一次:
│
├── 锁仍被当前线程持有? ───否───▶ 停止续期
│
是
│
▼
续期 30 秒
│
▼
重复检查...
Q30: 死循环看门狗机制怎么续期?
// 伪代码
while (isLocked) {
if (Thread.currentThread() == getHoldThread()) {
renewExpiration(); // 续期
}
Thread.sleep(10 * 1000); // 每10秒
}
private void renewExpiration() {
// 发送 Lua 脚本续期
redis.eval(`
if redis.call('pexpire', KEYS[1], ARGV[1]) == 1 then
return 1
end
return 0
`, 30 * 1000); // 续期30秒
}
特点:
- 后台线程异步执行,不阻塞业务
- 每次续期
lockWatchdogTimeout / 3= 10秒 - 锁释放时自动停止看门狗线程
Q31: Redisson 的使用步骤?
// 1. 引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.24.0</version>
</dependency>
// 2. 配置
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("password")
.setConnectionPoolSize(10);
return Redisson.create(config);
}
}
// 3. 使用
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
public void createOrder(Order order) {
RLock lock = redissonClient.getLock("order:create:" + order.getUserId());
try {
// 尝试获取锁,等待10秒,锁自动30秒过期
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
// 业务逻辑
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
附录:常用配置
# Redis 配置文件关键项
bind 0.0.0.0 # 绑定地址
port 6379 # 端口
daemonize no # 守护进程
maxmemory 2gb # 最大内存
maxmemory-policy allkeys-lru # 内存淘汰策略
appendonly yes # 开启 AOF
appendfsync everysec # AOF 同步策略
requirepass password # 密码