Redis 面试题大全

6 阅读14分钟

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 为什么快?

  1. 基于内存存储:数据存储在内存中,避免了磁盘 I/O 开销
  2. 单线程模型:避免了上下文切换和锁竞争(6.0前确实是单线程)
  3. 多路复用 I/O:使用 epoll/select/kqueue 等机制实现高并发
  4. 高效数据结构:底层使用 SDS、跳表、压缩列表等高效数据结构
  5. 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双向链表有序、可重复、支持两端操作消息队列、最新列表、关注列表
HashHashMap字段值对,适合存储对象商品信息、用户画像、配置表
SetHashSet无序、不可重复标签系统、好友关系、抽奖、去重
ZSet有序集合不可重复、有分数排序排行榜、权重排序、带权重的任务队列
Bitmap位图适合大数据量布尔统计签到统计、用户在线状态、布隆过滤器
HyperLogLog基数统计极低内存统计去重数量UV统计、日活统计
Geospatial地理坐标存储地理位置信息附近的人、距离计算
Stream日志流支持消费组的消息队列日志收集、消息队列替代方案

Q7: 各数据类型的命令(添加/查询)

类型添加命令查询命令
StringSET key value / SETNX key valueGET key / MGET key1 key2
ListLPUSH key value / RPUSH key valueLRANGE key 0 -1 / LINDEX key index
HashHSET key field valueHGET key field / HGETALL key
SetSADD key memberSMEMBERS key / SISMEMBER key member
ZSetZADD key score memberZRANGE 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 只记录写操作命令(如 SETINCRHSET),不记录读操作命令(如 GETHGETSMEMBERS)。这是 Redis 设计的有意为之,原因:

  1. 避免记录大量冗余的读命令导致文件膨胀
  2. 读命令不会修改数据,不需要记录
  3. 纯读操作在重启恢复时无实际意义

四、过期删除与内存淘汰

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个)  │
└────┬────┘
     │ 监控
     ▼
┌─────────┐     复制     ┌─────────┐
│  主库   │ ──────────▶ │  从库   │
└─────────┘             └─────────┘

工作流程

  1. 监控:哨兵周期性 ping 主库、从库、哨兵节点
  2. 通知:将主从信息通知给订阅的客户端
  3. 自动故障转移
    • 主库超时 → 标记为主观下线
    • 多数哨兵认为下线 → 标记为客观下线
    • 投票选举 → 选出一个从库升级为主库
    • 通知其他从库切换主库
    • 告知客户端新主库地址

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轻量级,连接池,支持所有命令
LettuceSpring 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 主要存储两类数据:

  1. 用户 Session(String)
    • Key: session:userId:{userId}
    • Value: 用户信息 JSON 字符串
    • TTL: 30分钟
  2. 接口限流(String + Lua 脚本)
    • Key: rate:api:{apiId}:{userId}:{时间窗口}
    • Value: 计数器
  3. 热点商品缓存(Hash)
    • Key: product:{productId}
    • Field: name, price, stock
    • Value: 对应值
  4. 商品排行榜(ZSet)
    • Key: ranking:sales:daily
    • Member: 商品ID
    • Score: 销量

十、分布式锁

Q19: Redis 的分布式锁了解吗?还知道其他分布式锁吗?

Redis 分布式锁:基于 SET NX PX 命令实现

其他分布式锁方案

方案特点
Redis Redisson支持重入、看门狗续期
ZooKeeper有序节点,临时顺序节点实现锁
etcd基于 Raft 协议,CP 模型
数据库表锁行锁,性能差
ConsulKV 存储,支持 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

关键点

  1. NX:保证互斥,只有不存在时才能加锁
  2. PX/EX:设置过期时间,防止死锁
  3. Value 唯一性:解锁时验证是否为同一客户端(防止误删他人锁)
  4. 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  │
                               └──────────┘

核心流程

  1. 客户端调用 redissonClient.getLock("key")
  2. 获取连接,发送 Lua 命令到 Redis
  3. Redis 执行 SET lock_key unique_value NX PX timeout
  4. 启动看门狗(如果没有指定 leaseTime)
  5. 客户端获取到锁,开始执行业务
  6. 业务完成后调用 unlock()
  7. 解锁时执行 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      # 密码