
这是一份精心整理的 Redis 核心知识盘点,涵盖了面试中经常被问及的 20 个关键问题。无论你是正在准备面试,还是希望巩固自己的 Redis 知识体系,相信这份指南都能为你提供清晰的思路和解答。
Redis是什么?
Redis(Remote Dictionary Server)是一个使用 C 语言编写的高性能非关系型键值对数据库。与传统数据库不同,Redis 的数据主要存储在内存中,因此读写速度极快,常被用作缓存。同时,Redis 支持将数据持久化到磁盘,保证了数据的安全性,并且其操作是原子性的。
Redis的优点有哪些?
- 基于内存操作:内存的读写速度远超磁盘,这是 Redis 高性能的基础。
- 单线程模型:避免了多线程环境下的上下文切换和竞争问题,简化了设计。需要注意的是,这里的“单线程”主要指处理网络请求的核心模块,像持久化等操作会由其他线程执行。
- 丰富的数据类型:支持 String、Hash、List、Set、SortedSet 等多种数据结构,适应不同的业务场景。
- 支持持久化:提供了 RDB 和 AOF 两种持久化机制,有效防止服务重启或宕机导致的数据丢失。
- 支持事务:所有操作都是原子性的,并且支持将多个命令打包后原子性执行。
- 支持主从复制:主节点数据可自动同步至从节点,便于实现读写分离和高可用架构。
Redis为什么这么快?
- 基于内存:数据存储在内存中,直接避免了磁盘 I/O 的瓶颈。
- 单线程模型(Redis 6.0以前):单线程处理客户端请求,消除了多线程切换和锁竞争带来的开销。
- I/O 多路复用:采用 epoll、kqueue 等 I/O 多路复用技术,使单个线程能高效处理大量并发连接。
- 高效的数据结构:每种数据类型底层都经过精心设计和优化,以追求极致的存取速度。
Redis为何选择单线程?
在早期版本中,Redis 选择单线程主要基于以下几点考虑:
- 避免上下文切换开销:单线程运行,没有多线程切换的性能损耗。
- 避免同步机制开销:如果采用多线程,就必须引入锁等同步机制来保证数据一致性,这会增加复杂性和性能开销。
- 实现和维护简单:单线程模型使得内部数据结构的实现无需考虑线程安全问题,代码更简洁,易于维护。
注:Redis 6.0 引入了多线程 I/O(处理网络读写),但核心的命令执行模块仍然是单线程的,这可以看作是在保持核心简单性的前提下对网络处理瓶颈的优化。
Redis应用场景有哪些?
- 缓存热点数据:这是最经典的用法,将数据库中的热点数据缓存在 Redis 中,大幅减轻数据库压力。
- 计数器:利用
INCR 等原子操作,可以方便地实现点赞数、网站访问量等统计功能。
- 简单消息队列:使用 List 的
LPUSH/BRPOP 或发布/订阅(Pub/Sub)模式,可以实现异步任务队列。
- 限速器:结合
INCR 和 EXPIRE 命令,可以限制用户在某段时间内的操作频率,常用于登录、秒杀等场景。
- 社交关系:利用 Set 提供的交集、并集等操作,可以轻松实现“共同好友”、“共同关注”等功能。
Memcached和Redis的区别?
- 线程模型:Redis 主要使用单核(单线程处理命令),Memcached 支持多线程。
- 数据结构:Memcached 仅支持简单的 key-value,而 Redis 支持丰富的数据类型。
- 持久化:Memcached 不支持数据持久化,重启后数据丢失;Redis 支持 RDB 和 AOF 两种持久化方式。
- 高可用:Redis 原生支持主从复制、哨兵和集群模式;Memcached 需要依靠客户端实现数据分片,无内置高可用方案。
- 性能:在单核场景下,Redis 的速度通常更快。
- 网络模型:Redis 采用单线程的多路 I/O 复用模型,Memcached 使用多线程的非阻塞 I/O 模型。
Redis 数据类型有哪些?
基本数据类型:
- String:最基础的类型,值可以是字符串、数字或二进制,最大 512MB。
- Hash:字段值对(field-value)的集合,适合存储对象。
- Set:无序且元素唯一的集合,支持交并差等集合运算。
- List:有序、元素可重复的列表,底层是双向链表,支持栈和队列操作。
- SortedSet (ZSet):有序集合,每个元素关联一个
score 分值用于排序,适用于排行榜等场景。
特殊数据类型:
- Bitmap:位图,本质上是二进制位数组,用于状态标记、统计等,非常节省空间。
- HyperLogLog:用于基数统计(估算集合中不重复元素的数量),占用空间固定且极小。
- Geospatial:用于存储地理坐标信息,支持计算距离、范围查询等。
Redis事务
事务的原理是将多个命令打包,然后让 Redis 一次性、顺序地执行。但它不保证原子性,即中间某条命令失败不会导致已执行命令的回滚。
事务的生命周期:
- 使用
MULTI 开启事务。
- 后续的命令会被放入队列,不会立即执行。
- 使用
EXEC 提交事务,队列中的命令将被依次执行。

示例:即使中间命令语法错误,其他命令仍会执行。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a 1
QUEUED
127.0.0.1:6379> set b 1 2 # 语法错误
QUEUED
127.0.0.1:6379> set c 3
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR syntax error
3) OK
WATCH命令
WATCH 命令用于实现乐观锁。它可以监视一个或多个 key,如果在事务执行前这些 key 被其他命令改动,则当前事务将被取消。
127.0.0.1:6379> watch name # 开始监视name
OK
127.0.0.1:6379> set name 1 # 在事务外修改了name
OK
127.0.0.1:6379> multi # 开启事务
OK
127.0.0.1:6379> set name 2
QUEUED
127.0.0.1:6379> set gender 1
QUEUED
127.0.0.1:6379> exec # 执行事务,因为watch的key被修改,事务返回nil,未执行
(nil)
127.0.0.1:6379> get gender # gender未被设置
(nil)
使用 UNWATCH 可以取消对所有 key 的监视。
持久化机制
持久化是将内存中的数据保存到磁盘,防止服务宕机导致数据丢失。Redis 提供两种方式:RDB 和 AOF。
RDB方式
RDB 是 Redis 默认的持久化方案,它会在特定时间点生成数据集的一个快照(dump.rdb 文件)。
bgsave 是触发 RDB 持久化的主要命令,其执行流程如下:

- 执行
BGSAVE 命令。
- 父进程检查是否有子进程正在执行,有则直接返回。
- 父进程
fork 一个子进程(此过程会短暂阻塞)。
fork 完成后,父进程继续处理请求,子进程将内存数据写入临时 RDB 文件。
- 子进程写入完成后,用新文件替换旧的 RDB 文件。
触发方式:
- 手动触发:执行
SAVE(阻塞)或 BGSAVE(后台异步)命令。
- 自动触发:根据配置规则(如
save 900 1),或主从复制全量同步时,或执行 shutdown 且未开启 AOF 时。
优点:
- 数据恢复速度快。
- 生成快照时父进程不进行 I/O,对服务性能影响小。
缺点:
- 无法做到实时/秒级持久化,可能丢失最后一次快照后的数据。
fork 过程在数据量大时可能较耗时。
- 不同版本 RDB 文件格式可能不兼容。
AOF方式
AOF 通过记录每次写命令来持久化数据,重启时重放命令来恢复数据,解决了数据持久化的实时性问题。
开启 AOF:appendonly yes。写命令会先存入 aof_buf 缓冲区,然后根据 appendfsync 策略同步到磁盘:
appendfsync always # 每次写命令都同步,最安全但最慢
appendfsync everysec # 每秒同步一次,推荐配置
appendfsync no # 由操作系统决定同步时机
AOF 工作流程:

- 命令追加到 AOF 缓冲区。
- 缓冲区根据策略同步到 AOF 文件。
- 定期进行 AOF 重写(rewrite),压缩文件体积(将进程内数据转换为最小命令集)。
- 重启时加载 AOF 文件恢复数据。
优点:
- 数据安全性高,配置为
everysec 最多丢失1秒数据。
append-only 模式写入性能好。
缺点:
- 文件体积通常比 RDB 大。
- 数据恢复速度比 RDB 慢。
主从复制
主从复制 是实现 Redis 高可用 的基础。主库(Master)可读写,从库(Slave)只读并同步主库数据。
# 启动主库
redis-server
# 启动从库并指定主库
redis-server --port 6380 --slaveof 127.0.0.1 6379
# 在运行中的Redis实例上执行,成为从库
slaveof 127.0.0.1 6379
# 停止复制,变回主库
SLAVEOF NO ONE
主从复制原理:
- 从节点连接主节点,发送
PSYNC 命令。
- 首次连接触发全量复制:主节点生成 RDB 快照文件发送给从节点。
- 从节点接收并加载 RDB 文件到内存。
- 主节点将生成 RDB 期间接收到的新写命令(存于缓冲区)发送给从节点,从节点执行这些命令以保持最终一致。
- 之后进入命令传播阶段,主节点持续将写命令异步发送给从节点。
- 网络断开重连后,可能会触发部分复制(增量复制)。
哨兵Sentinel
主从复制无法自动进行故障转移,哨兵模式在此基础上提供了自动故障发现和转移的能力,实现了高可用。客户端首先连接哨兵获取当前主节点地址。

工作原理:
- 每个哨兵以每秒一次的频率向主、从节点及其他哨兵发送
PING 命令。
- 如果实例超时未回复,哨兵会将其标记为“主观下线”。
- 如果主节点被标记为主观下线,所有监视它的哨兵会投票确认其是否“客观下线”。
- 当主节点被判定为客观下线后,哨兵会通过选举出一个领导者哨兵来负责故障转移。
- 领导者哨兵会选择一个合适的从节点升级为新的主节点,并让其他从节点复制新主节点,同时通知客户端主节点已变更。
Redis cluster
集群模式实现了 Redis 的分布式存储,数据分片存储在多个节点上,解决了哨兵模式下主节点写能力和存储容量受单机限制的问题。最小规模为6节点(3主3从)。
集群采用虚拟槽分区,共有16384个槽,每个节点负责一部分槽。数据通过 crc16(key) % 16384 计算槽位,然后分配到对应节点。

优点:
- 无中心架构,支持线性扩展。
- 数据分片存储,可动态调整数据分布。
- 具备自动故障转移能力。
缺点:
- 不支持跨节点的批量操作(如
mget 在多个 key 分布于不同节点时效率低)。
- 事务仅支持同一节点上的多个 key。
- 只能使用一个数据库(db0)。
过期键的删除策略?
- 惰性删除:当客户端访问一个 key 时,Redis 会检查其是否过期,过期则立即删除。
- 定期删除:Redis 定期(默认每100ms)随机抽取一些设置了过期时间的 key 进行检查和删除。
- 内存驱逐:当 Redis 内存使用达到
maxmemory 限制时,会触发配置的内存淘汰策略来释放空间。
内存淘汰策略有哪些?
当内存使用超过 maxmemory 时,Redis 会根据配置的策略删除部分数据。
Redis 4.0 之前:
volatile-lru:从已设置过期时间的 key 中,移除最近最少使用的。
allkeys-lru:从所有 key 中,移除最近最少使用的。
volatile-ttl:从已设置过期时间的 key 中,移除将要过期的。
volatile-random:从已设置过期时间的 key 中,随机移除。
allkeys-random:从所有 key 中,随机移除。
no-eviction:不删除,新写入操作会报错(默认)。
Redis 4.0 之后新增:
volatile-lfu:从已设置过期时间的 key 中,移除最不经常使用的。
allkeys-lfu:从所有 key 中,移除最不经常使用的。
如何保证缓存与数据库双写时的数据一致性?
这是一个经典难题,没有完美的银弹方案,常见思路如下:
- 先删缓存,再更新数据库:
- 问题:在删除缓存后、更新数据库前,如果有读请求,会读到数据库旧值并重新写入缓存,导致一段时间内缓存仍是旧数据。
- 先更新数据库,再删缓存:
- 问题:在更新数据库后、删除缓存前,读请求可能读到缓存旧数据。但这个时间窗口通常极短,影响相对较小。如果删缓存失败,问题会持续。
- 优化:引入消息队列或异步重试机制确保缓存删除成功。
- 异步更新缓存:将数据库更新操作封装成消息,由消息队列保证顺序,消费者负责更新缓存。这解耦了数据库操作和缓存操作,但系统复杂度增加。
缓存穿透
指查询一个数据库中根本不存在的数据,缓存无法命中,请求直达数据库。如果大量此类请求,数据库压力巨大。
解决方案:
- 缓存空值:即使查询不到数据,也将一个空值(或特殊标记)缓存起来,并设置较短的过期时间。
- 布隆过滤器:将所有可能存在的数据哈希到一个位数组中。查询时,先经过布隆过滤器,如果判断数据不存在,则直接返回,避免查询数据库。
缓存雪崩
指大量缓存 key 在同一时间点或时间段失效,导致所有请求直接涌向数据库,造成数据库压力激增甚至崩溃。
解决方案:
- 分散过期时间:给缓存 key 的过期时间加上一个随机值,避免集体失效。
- 热点数据永不过期:对极热点数据,可以考虑不设置过期时间,或由后台任务异步更新。
- 构建高可用缓存集群:如使用 Redis Cluster,避免单点故障。
缓存击穿
指一个热点 key 在缓存过期瞬间,同时有大量请求进来,这些请求全部打到数据库,导致数据库压力骤增。
解决方案:
- 互斥锁:当缓存失效时,只允许一个线程去查询数据库并重建缓存,其他线程等待。可以使用 Redis 的
SETNX 命令实现分布式锁。
public String get(String key) {
String value = redis.get(key);
if (value == null) { //缓存值过期
String lockKey = "lock:" + key;
// 尝试获取分布式锁,设置30秒超时
if (redis.set(lockKey, "1", "NX", "PX", 30000) == "1") { //设置成功,获得锁
value = db.get(key); // 查数据库
redis.set(key, value, expire_secs); // 回写缓存
redis.del(lockKey); // 释放锁
} else { // 其他线程已经获得锁,等待后重试
sleep(50);
return get(key); // 重试
}
} else {
return value;
}
}
pipeline的作用?
Redis 执行一条命令包含网络往返时间。pipeline 可以将多个命令打包一次性发送,服务器依次执行后再一次性返回结果,极大地减少了网络开销,提升了吞吐量。
注意事项:
pipeline 不是原子操作,中途异常不会回滚。
- 打包的命令数不宜过多,避免单次传输数据过大或客户端等待过久。
- 与原生批命令(
MSET/MGET)不同,pipeline 支持任意命令的批量执行。
LUA脚本
Lua 脚本在 Redis 中以原子方式执行,执行期间不会插入其他命令,非常适合用于实现复杂的原子性操作。
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
作用与场景:
- 原子性:保证一系列命令的原子执行。
- 减少网络开销:将多个命令打包在一个脚本中。
- 复杂操作:例如实现限流、库存扣减等需要判断和循环的逻辑。
示例:接口限流
-- KEYS[1]: 接口key, ARGV[1]: 限流次数, ARGV[2]: 时间窗口
local c
c = redis.call('get',KEYS[1])
if c and tonumber(c) > tonumber(ARGV[1]) then
return c;
end
c = redis.call('incr',KEYS[1])
if tonumber(c) == 1 then
redis.call('expire',KEYS[1],ARGV[2])
end
return c;
此脚本先检查是否超限,未超限则计数,并在第一次计数时设置过期时间,整个过程是原子的。
希望这份系统的梳理能帮助你更好地理解和掌握 Redis。在实际应用中,还需要结合具体业务场景进行设计和调优。