随着互联网时代对网站访问速度的要求越来越高,直接使用关系型数据库的方案在性能上逐渐出现瓶颈。为此,在客户端与数据存储层之间引入一个缓存层来分担请求压力,成为提升系统性能的关键。作为一款优秀的缓存中间件,Redis在企业级架构中占据着举足轻重的地位,也因此成为了技术面试中的必问项。

为了帮助大家在技术考核中更好地展现自己,本文将通过30个问题,由浅入深地梳理Redis的核心知识体系。


1、什么是Redis?
Redis(Remote Dictionary Server)是一个开源的、键值对型的内存数据存储系统。它使用C语言编写,遵守BSD协议,既可作为基于内存的数据库,也支持数据持久化。Redis提供了多种语言的API,被广泛用于缓存、数据库和消息中间件等场景。它支持多种数据结构,可以存储不同类型值之间的映射,并具备事务、持久化、LUA脚本及多种集群方案等特性。

2、Redis的优缺点
优点:
- 完全基于内存操作,性能极高,读写速度快,支持超过 100KB/s 的读写速率。
- 支持高并发,可达10万级别的并发读写。
- 支持主从模式,具备读写分离与分布式能力。
- 拥有丰富的数据类型(如String、Hash、List、Set、ZSet)及特性(如发布订阅)。
- 支持持久化操作,保证数据不会因进程退出而丢失。
缺点:
- 数据库容量受物理内存限制,难以实现海量数据的高性能读写。
- 相比关系型数据库,不支持复杂的逻辑查询,存储结构相对简单。
- 虽然提供持久化能力,但更多是一种 disk-backed 功能,与传统意义上的持久化有所区别。

3、Memcache与Redis的区别
Memcache 同样是一个开源、高性能、分布式内存对象缓存系统。所有数据存储在内存中,服务器重启后数据丢失,采用哈希表和LRU算法进行数据管理。
两者的主要区别如下:
- 数据类型:Memcache 仅支持字符串类型,而 Redis 支持5种主要数据类型及多种高级数据结构。
- 数据持久化:Memcache 不支持持久化;Redis 支持 RDB 快照和 AOF 日志两种持久化策略。
- 分布式:Memcache 本身不支持分布式,需在客户端使用一致性哈希实现;Redis 3.0 之后可在服务端构建无中心节点的集群,具备线性可伸缩能力。
- 内存管理机制:Memcache 数据量不能超出系统内存;Redis 有 VM 机制,可管理物理内存。两者的底层实现和通信协议不同。
- 数据大小限制:Memcache 单个 Value 最大容量为 1MB;Redis 最大容量为 512MB。

4、Redis 支持哪些数据类型?
基本数据类型:
- String(字符串)
- Hash(哈希)
- List(列表)
- Set(集合)
- ZSet(Sorted Set,有序集合)
高级数据类型:
- HyperLogLog:用于基数统计的算法。在输入元素数量极大时,计算基数所需空间固定且很小,且不存储元素本身。
- Geo:用于存储和计算地理位置信息。其原理是通过 Geohash 算法将经纬度转换成一个哈希字符串,距离越近的地点,哈希值的前缀越相似。
- BitMap:本质上是一种对字符串进行的位操作,常用于统计日活跃用户等场景。

5、Redis中String类型的实现原理
Redis 并未直接使用C语言传统的字符串表示,而是自己构建了一种名为 简单动态字符串(Simple Dynamic String,SDS) 的抽象类型。
SDS 相较于C字符串的提升:
- 避免缓冲区溢出:修改字符串时,可根据SDS的
len 属性检查空间是否满足。
- 获取长度复杂度低:直接读取
len 属性即可,复杂度为O(1)。
- 减少内存重分配次数:通过空间预分配和惰性空间释放策略优化。
- 二进制安全:可以保存文本或任意二进制数据。
- 兼容部分C字符串函数:可重用一部分C语言字符串库的函数。

6、Redis 为何直接以内存存储?
将数据完全存储在内存中可以实现最快的读写速度。如果开启了持久化功能,数据会以异步方式写入磁盘,从而兼具了高速访问和数据持久化的特性。
内存操作本身就比磁盘I/O快得多,且不受磁盘I/O速度的制约。反之,若数据存于磁盘,I/O速度将成为严重的性能瓶颈。当数据集大小达到内存上限时,便无法插入新值。此时如果开启了虚拟内存功能,不常访问的数据会被换出到磁盘;若未开启,则会使用操作系统的交换内存,但这将导致性能急剧下降。若配置了内存淘汰策略,则会根据策略淘汰旧数据。

7、Redis 如何进行内存优化?
- 优先使用哈希表:对于字段数少于100的Hash结构,Redis的存储效率很高。在不需集合操作或列表的push/pop操作时,应优先考虑使用Hash。
- 考虑使用 BitMap:在合适的场景(如布尔状态统计)下,使用BitMap能极大节省内存。
- 利用共享对象池:Redis启动时会自动创建0-9999的整数对象池。对于这个范围内的整数值,Redis会直接引用池中的对象,因此尽量使用这些整数可以节省内存。
- 合理使用内存回收策略:利用
expire 设置键的过期时间,并配合合适的过期删除与内存淘汰策略。

8、Redis 如何实现分布式锁?
可以利用 INCR、SETNX、SET 命令,并结合 expire 来辅助实现。
方式1:利用 INCR
如果 key 不存在,则初始化为0,然后使用 INCR 加1。后续线程如果获取到的值大于等于1,说明锁已被占用。持有锁的线程执行完任务后,使用 DECR 将值减1以释放锁。
方式2:利用 SETNX
使用 setnx 争抢锁,抢到后再用 expire 设置一个过期时间,防止锁无法释放。setnx 在 key 不存在时设置值并返回1;key已存在时,不做任何操作并返回0。
方式3:利用 SET
set 指令通过参数可以合并 setnx 和 expire 的功能,其命令格式如下:
set($Key, $value, array('nx', 'ex'=>$ttl))

9、Redis 高性能的原因
- 完全基于内存操作,读写速度极快。
- 数据结构简单高效,针对不同场景有专门优化的数据结构。
- 核心网络请求模块采用单线程,避免了不必要的上下文切换和竞争条件,也无需考虑多线程环境下的锁问题(其他模块如持久化仍使用多线程)。
- 使用多路I/O复用模型,实现非阻塞I/O。
- 自身实现了VM机制,而非使用操作系统的Swap,可实现冷热数据分离,避免因内存不足导致的访问速度下降。

10、Redis 持久化的方式
1、RDB(Redis DataBase)持久化
这是Redis默认的持久化方式,按照配置的时间周期,将内存中的数据以快照形式保存到磁盘的 .rdb 文件中。
其核心是 fork 和 cow(Copy-On-Write)机制。执行 bgsave 时,Redis会 fork 出一个子进程共享主进程内存数据来写临时RDB文件,写完后替换旧文件。主进程进行读操作时访问共享内存,进行写操作时则会拷贝一份数据执行(cow)。
优点:
- 只有一个
dump.rdb 文件,便于备份和灾难恢复。
- 最大化性能,由子进程负责持久化,主进程继续处理命令,无I/O阻塞。
- 紧凑的二进制格式,重启时加载效率比AOF高,尤其在数据量大时。
缺点:
- 可能丢失最后一次快照之后的数据。
fork 过程在数据集很大时可能导致服务短暂停顿。
2、AOF(Append Only File)持久化
将每一个写命令追加到日志文件中。需要配置同步选项(appendfsync):
always:同步刷盘,可靠性最高,性能影响大。
everysec:每秒刷盘(默认),性能与可靠性折中,最多丢失1秒数据。
no:由操作系统控制,性能最好,可靠性最差。
AOF文件会不断增长,可通过 bgrewriteaof 命令或配置自动触发重写以压缩文件体积。
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
优点:
- 数据安全性更高,配置为
always 时最多丢失一次命令。
rewrite 机制可压缩文件体积。
缺点:
- AOF文件通常比RDB文件大。
- 重启时加载效率可能低于RDB。
- 在高并发下,AOF文件体积增长较快,需定期重写。

11、Redis 过期删除策略
方式1:定时删除
为每个设置过期时间的key创建一个定时器,到期立即删除。
特点: 对内存友好,对CPU不友好,会占用大量CPU时间处理过期键。
方式2:惰性删除
只有在获取key时才检查其是否过期,过期则删除。
特点: 对CPU友好,对内存不友好,可能导致大量过期key占用内存不被释放。
方式3:定期删除
每隔一段时间,主动检查并删除一批过期key(检查的数据库和数量由算法决定)。
特点: 上述两种策略的折衷,通过控制执行的时长和频率来平衡CPU和内存。
Redis实际采用的是 惰性删除 + 定期删除 的组合策略。

12、Redis 的同步机制
Redis主从同步分为全量同步和增量同步。从节点会先尝试增量同步,失败则进行全量同步。
增量同步:
从节点初始化完成后,主服务器执行的写命令会实时同步给从服务器。
全量同步:
从节点初次连接主节点时,会发送 psync 命令。主节点执行一次 bgsave 生成RDB文件并发送给从节点,同时将后续的写命令记录到内存缓冲区。从节点加载RDB文件后,主节点再将缓冲区中的命令发送给从节点重放,完成同步。

13、Redis 的内存淘汰策略
当内存使用达到 maxmemory 限制时,Redis会根据配置的策略淘汰数据。
对设置了过期时间的key:
volatile-lru:淘汰最近最少使用的。
volatile-random:随机淘汰。
volatile-ttl:淘汰存活时间更短的。
对所有key:
allkeys-lru:淘汰最近最少使用的。
allkeys-random:随机淘汰。
noeviction:不淘汰,新写入操作会报错(默认)。
策略选择参考:
- 数据访问呈幂律分布(部分高频,部分低频):用
allkeys-lru。
- 数据访问呈均匀分布:用
allkeys-random。
注意: Redis的LRU并非精确算法,而是随机采样一定数量(默认为5,通过 maxmemory-samples 配置)的key,淘汰其中最近最少使用的。

14、Redis 常见的问题
问题1:缓存穿透
指查询一个缓存和数据库中都不存在的数据,导致所有请求直达数据库。
解决:
- 布隆过滤器:将所有可能查询的参数哈希到足够大的位图中,查询前先校验位图,不存在则拦截。
- 缓存空对象:数据库查询为空时,仍将空结果缓存并设置较短过期时间。需注意数据库有数据后更新缓存。
问题2:缓存击穿
指某个热点key在缓存过期瞬间,大量请求同时击穿缓存直达数据库。
解决:
- 永不过期:对极热点数据设置逻辑上的永不过期。
- 互斥锁:缓存失效时,只允许一个线程去查询数据库并回填缓存,其他线程等待。
- 随机退避:缓存失效时,线程随机sleep很短时间后再重试缓存。
问题3:缓存雪崩
指大量缓存key在同一时间大面积失效,导致所有请求落向数据库。
解决:
- 差异化过期时间:给缓存失效时间加上一个随机值,让失效时间点分散。
- 二级缓存:使用本地缓存(如Ehcache)作为一级缓存,Redis作为二级缓存。
- 高可用架构:采用Redis集群,避免单点故障。
- 限流降级:在缓存失效后,通过加锁或队列控制访问数据库的线程数,或对非核心服务进行降级。

15、Redis 如何进行缓存降级?
缓存降级本质上是服务降级。在系统面临巨大流量压力或部分服务出现问题时,为了保证核心业务链路的可用性,有策略地暂时降低非核心服务的质量或直接关闭。
降级需要事先规划,明确哪些服务可降级。策略包括:
- 服务方式:直接拒绝服务、延迟服务、返回兜底数据。
- 服务范围:关闭非核心功能、只读服务、简化流程。
降级的核心目标是:有损服务,但保障核心。

16、Redis 如何进行缓存更新?
- 数据实时同步更新:数据库更新后,同步或异步通知缓存更新。保证强一致性,但对数据库有压力。
- 异步消息队列更新:数据库更新后,发送消息到队列,由消费者异步更新缓存。保证最终一致性。
- 定时任务更新:通过定时任务按频率全量或增量刷新缓存。一致性最弱,可能有延迟。
- 读时更新(Cache Aside Pattern):读缓存,没有则读数据库并写入缓存;写时直接更新数据库,并删除缓存。这是最常用的模式。

17、Redis 如何缓存热点Key?
热点Key指访问频率极高的key。
解决思路:
- 本地缓存:在应用层使用本地缓存(如Guava Cache),缓存极热点数据,减少对Redis的访问。
- Key拆分:将一个热点Key的数据拆分成多个子Key,分布到不同节点,减轻单点压力。
- 永不过期:对热点Key设置合理的逻辑永不过期,通过后台任务异步更新。
- 限流与熔断:对热点Key的访问实施限流,保护下游系统。

18、Redis 的哨兵模式
Sentinel(哨兵)是Redis官方提供的高可用性解决方案,用于监控主从节点状态。
主要作用:
- 监控:持续检查主节点和从节点是否正常运行。
- 通知:当被监控的Redis实例出现故障时,可以通过API通知管理员。
- 自动故障转移:主节点故障时,自动将一个从节点提升为新主节点,并让其他从节点复制新主节点。同时,通知客户端连接新的主节点。

19、Redis 的Pipeline管道
问题背景:
Redis基于TCP请求/响应模型,每条命令都需经历网络往返。频繁的请求会带来巨大的网络延迟开销。
Pipeline解决:
Pipeline允许客户端一次性发送多个命令,而无需等待每个命令的响应,服务器会按顺序处理并一次性返回所有结果。这能将多次网络往返时间缩减为一次,极大提升批量操作的性能。
注意:
- Pipeline基于队列实现,保证命令顺序性。
- 一次性打包过多命令会占用大量客户端内存。Redis客户端通常有默认提交大小(如53条)。
- Pipeline不保证原子性,只是批量发送。在Redis集群模式下无法直接使用跨slot的Pipeline。

20、Redis 如何进行性能优化?
- 持久化优化:主节点避免做耗时长的
bgsave,可在从节点开启AOF并配置为 everysec。
- 网络优化:主从节点尽量部署在同一局域网,保证复制速度和连接稳定性。
- 架构优化:主从采用链式结构(Master -> Slave1 -> Slave2),而非星型,便于故障切换。避免在压力大的主节点上直接添加过多从节点。
- 命令优化:使用批量操作命令(如
mget、mset)、Pipeline,避免大量循环单条命令。
- 键名优化:在保证可读性前提下,使用简短的键名。
- 内存优化:合理选择数据结构,使用整数对象池,设置过期时间。

21、Redis 的数据一致性问题
指缓存与数据库之间的数据不一致。常用更新策略:
方式1:先更新数据库,再更新缓存
问题:并发下可能因更新顺序导致缓存脏数据;且更新缓存可能失败。
方式2:先删除缓存,再更新数据库
问题:在“删缓存”后、“更新数据库”前,若有读请求将旧数据再次加载到缓存,会导致不一致。(可通过“延时双删”缓解)
方式3:延时双删策略
- 删除缓存。
- 更新数据库。
- 休眠一段时间(如几百毫秒)。
- 再次删除缓存。
目的:清除在第二步期间可能被读请求加载的旧缓存。
方式4:异步订阅Binlog
通过 canal 等工具订阅数据库的Binlog变化,异步更新或删除缓存。对业务代码无侵入,最终一致性。

22、Redis 如何实现事务?
Redis事务并非严格意义上的ACID事务(如执行EXEC时宕机,部分命令可能未执行),但提供了命令打包顺序执行的能力。
事务相关四个原语:
multi:开启事务,后续命令放入队列。
exec:执行事务队列中的所有命令。
discard:取消事务,清空队列。
watch:监视一个或多个key,如果在 exec 前被其他客户端修改,则事务失败(乐观锁)。
特性:
- 事务中的命令按顺序串行执行,不会被其他客户端命令打断。
- Redis事务不支持回滚。如果某条命令失败,后续命令仍会继续执行。

23、Redis 如何实现集群?
方式1:Redis Cluster(官方方案,>=3.0)
采用去中心化架构,数据分片存储在多个节点上。分布式算法是哈希槽,而非一致性哈希。整个集群有16384个槽,每个节点负责一部分。支持主从复制和自动故障转移。
方式2:Twemproxy(代理方案)
Twitter开源的一个轻量级代理,可管理后端多个Redis实例。客户端连接Twemproxy,由它通过一致性哈希等算法将请求转发到具体节点。具备节点故障自动剔除与重试机制。
方式3:Codis
一个分布式Redis解决方案,在客户端与Redis服务器之间引入了Codis Proxy和Codis Dashboard等组件,支持平滑扩缩容和数据迁移。

24、Redis 的脑裂问题
什么是脑裂?
当主节点与从节点、哨兵因网络分区被隔离时,哨兵可能在新分区中选举出新的主节点。此时集群中出现两个“主节点”,客户端可能向旧主节点写入数据,导致数据丢失和不一致。
如何解决?
通过配置以下参数,要求主节点必须有足够多、延迟足够低的从节点连接时才能接收写请求。
min-slaves-to-write 3 # 主节点至少需要3个从节点连接 (新版本参数名:min-replicas-to-write)
min-slaves-max-lag 10 # 从节点最长滞后时间10秒 (新版本参数名:min-replicas-max-lag)
配置后,如果发生脑裂,原主节点因连接从节点数不足会拒绝写请求,从而减少数据丢失。

25、Redis 如何实现异步队列?
-
使用 List 结构:
- 生产者:
RPUSH key value
- 消费者:
LPOP key 或 BLPOP key timeout (阻塞版本,无消息时等待)
使用 BLPOP 可避免消费者轮询带来的CPU消耗。
-
使用 Pub/Sub 模式:
实现一对多的消息广播。但它是非持久化的,如果消费者中途下线,将丢失其离线期间的消息。

26、Redis 如何实现延迟队列?
使用 ZSet 实现。将消息内容作为 member,投递的时间戳作为 score。
- 生产者:
ZADD delay_queue <timestamp> message
- 消费者:轮询调用
ZRANGEBYSCORE delay_queue 0 <current_timestamp> WITHSCORES 获取到期的消息进行处理,处理完后用 ZREM 移除。

27、Redis 中哈希槽的概念
Redis Cluster 采用哈希槽进行数据分片。整个键空间被划分为 16384 个槽。每个键通过 CRC16 校验后,对 16384 取模来决定归属于哪个槽。集群中的每个节点负责处理一部分哈希槽,从而实现了数据的自动分片和定位。

28、Redis 的应用场景
- 数据缓存:最经典场景,加速数据访问。
- 排行榜:利用ZSet的有序特性实现。
- 计数器:利用
INCR 命令的原子性实现文章阅读数、点赞数等。
- 分布式Session:在集群环境中集中管理用户Session。
- 分布式锁:利用
SETNX 或 Redlock 算法实现。
- 最新列表:使用 List 的
LPUSH、LTRIM 命令维护固定长度的最新动态。
- 位统计:使用 BitMap 统计用户签到、活跃状态等。
- 消息队列:使用 List 或 Stream 类型实现简单的消息队列。
- 社交关系:使用 Set 求交集、并集实现共同关注、好友推荐。
- 地理位置:使用 Geo 类型存储和计算附近的人、店铺。

29、Redis 实现签到表功能
方式1:使用 Set 结构
以日期为key,用户ID集合为value。
- 签到:
SADD sign:20240517 user_id
- 查询是否签到:
SISMEMBER sign:20240517 user_id
- 统计签到人数:
SCARD sign:20240517
方式2:使用 BitMap 结构
以 u:sign:user_id:yyyyMM 为key,每月一个位图,位的位置代表日期(0-30)。
# 用户ID 1000 在 2月17日签到(偏移量从0开始,故为16)
SETBIT u:sign:1000:201902 16 1
# 检查2月17日是否签到
GETBIT u:sign:1000:201902 16
# 统计2月份总签到次数
BITCOUNT u:sign:1000:201902
# 获取首次签到的日期(返回偏移量,+1即日期)
BITPOS u:sign:1000:201902 1
两者对比:
- Set:内存占用与签到用户数成正比。
- BitMap:内存占用与最大用户ID有关(最坏情况)。它非常节省空间,但仅适合存储布尔状态,且最大可存储2^32位(512MB)数据。

30、Redis 中 ZSet 的底层实现
Redis的ZSet内部使用跳表(Skip List) 而非红黑树实现。
什么是跳表?
跳表是一种随机化的数据结构,本质上是可以支持二分查找的有序链表。它在原有链表的基础上,构建了多级索引,通过索引实现快速查找、插入和删除,时间复杂度为O(log n)。

跳表特点总结:
- 可进行二分查找的有序链表。
- 插入元素时,随机生成其索引层数。
- 最底层(L0)包含所有元素。
- 时间复杂度接近平衡二叉树。
为何选择跳表而非红黑树?
红黑树能高效完成ZSet的大部分操作(插入、删除、查找、有序遍历),时间复杂度也是O(log n)。然而,对于 “查找区间内所有元素” 这个操作,在红黑树上需要进行中序遍历,效率不如跳表。在跳表中,只需定位到区间两端在底层的位置,然后顺序遍历即可,非常高效。此外,跳表的实现相对更简单。
掌握这些核心知识点,能让你在技术面试中更加游刃有余。技术的深度源于持续的积累与思考。如果你对更多系统设计、分布式系统或数据库相关话题感兴趣,欢迎在云栈社区与我们进一步交流探讨。