Redis 之所以性能卓越,主要得益于以下几个核心设计:
基于内存的数据存储:Redis 将所有数据保存在内存中进行读写操作。由于内存的访问速度远高于磁盘,这使得 Redis 能够实现极高的吞吐量和低延迟响应。
高效的 I/O 多路复用机制:在单线程模型下,Redis 使用 I/O 多路复用技术(如 epoll),允许一个线程同时监听多个客户端连接。当任意连接有数据可读时,系统会通知主线程处理,从而高效地支持大量并发连接。
select、poll
精心优化的数据结构:Redis 内部采用了多种高性能数据结构,例如哈希表、有序集合、列表等。这些结构经过深度优化,能快速完成插入、查找、删除等常见操作,进一步提升了执行效率。
单线程处理核心逻辑:Redis 的网络 I/O 和键值对读写操作由同一个线程完成。这种设计避免了多线程环境下的上下文切换开销以及锁竞争带来的性能损耗,保证了命令执行的原子性和高效性。
read
需要说明的是,“Redis 是单线程”这一说法仅指其核心的数据操作与网络请求处理模块。实际上,Redis 并非完全意义上的单线程系统——像持久化(如 RDB 快照、AOF 日志重写)和集群通信等功能是由独立的子进程或多线程模块来完成的。
那么为何选择单线程?原因如下:
Socket
然而随着业务规模扩大,某些高 QPS 场景下,Redis 的性能瓶颈逐渐转移到网络 I/O 上。尽管使用了 I/O 多路复用,但在数据就绪后将内容从内核空间复制到用户空间的过程仍是同步阻塞的,此时主线程无法处理其他请求。
为此,Redis 6.0 引入了多线程能力,但仅限于网络请求的接收与解析阶段。具体来说:
这一改进有效缓解了高并发下的网络 I/O 压力,同时保留了原有单线程模型的优点。
同步阻塞 I/O(Blocking IO):用户线程发起 read 调用后会一直阻塞,直到网卡接收到数据,并完成从内核空间到用户空间的数据拷贝。该模型实现简单,但每个连接需占用一个独立线程,资源消耗大,不适合高并发场景。典型的例子如 Java 中的传统 BIO。
同步非阻塞 I/O(Non-blocking IO):应用程序设置文件描述符为非阻塞模式后,调用 read 时若数据未就绪则立即返回错误,不会阻塞线程。之后可通过轮询不断尝试读取。虽然避免了长时间等待,但频繁的系统调用导致 CPU 占用较高,适用于少量连接或实时性要求较高的场景。
I/O 多路复用(IO Multiplexing):这是目前主流高性能服务所采用的技术。通过 select、poll 或更高效的 epoll 等系统调用,使单个线程能够监视多个文件描述符的状态变化。一旦某个连接有数据可读,系统即通知应用进行处理。
尽管数据拷贝阶段仍需用户线程参与(属于同步 I/O),但由于能用极少线程管理成千上万连接,极大减少了资源开销,被广泛应用于 Redis、Netty 等框架中。
epoll
异步 I/O(Asynchronous IO, AIO):真正的非阻塞模型。用户发起 aio_read 请求后,内核负责等待数据就绪并将数据自动从内核空间拷贝至用户空间,完成后通过回调函数通知应用程序。整个过程无需用户线程干预,适合超高并发场景。
不过 AIO 编程模型复杂,且不同操作系统支持程度不一,因此在实际开发中更多采用 I/O 多路复用来模拟异步行为。
select 是最早的 I/O 多路复用实现,最大缺点是最多只能监控 1024 个文件描述符,且每次都需要传入整个描述符集合,效率低下。
poll 改进了 select 的数量限制,支持更多连接,但仍需线性遍历所有注册的描述符来检查状态,时间复杂度为 O(n),在连接数较多时性能下降明显。
epoll 基于事件驱动机制,内部使用红黑树管理文件描述符,只关注“活跃”的连接。当某连接有数据到达时,内核主动将其加入就绪队列,无需遍历全部描述符,性能接近 O(1),特别适合大规模并发场景。
Redis 在 Linux 系统中正是基于 epoll 实现高效的事件循环。
操作系统为了保护系统安全和稳定运行,将程序运行空间划分为用户态(User Space)和内核态(Kernel Space)。用户态程序不能直接访问硬件资源或关键内存区域,必须通过系统调用进入内核态来完成特定操作(如读写文件、网络通信等)。这个切换过程有一定开销,也是理解 I/O 性能差异的基础。
操作系统的核心是内核,它负责统筹硬件资源的管理与调度,并为上层应用提供基础服务支持。如果允许所有程序直接操作硬件,将极易引发系统崩溃或数据损坏等问题。为此,系统设计了两种运行模式:用户态和内核态。
用户态是指程序在受限权限下运行的状态,只能访问被授权的资源,无法直接操控硬件,从而保障系统的稳定性和安全性。
内核态则拥有最高权限,能够直接与硬件交互,执行诸如内存分配、进程调度等底层操作。
当用户程序需要进行I/O操作时,就必须从用户态切换到内核态。此时CPU会暂停当前进程,保存其上下文信息(如程序计数器、寄存器状态、栈指针等),转入内核态执行相应的硬件操作。待操作完成后,再切回用户态并恢复进程继续执行。
Redis中常用的数据类型主要包括五种:String、List、Hash、Set以及Zset。
String 是最基本的数据类型,可以存储字符串、整数或浮点数,适用于缓存、计数器等场景。
Hash 由多个键值对组成,适合用于表示对象的属性集合,例如用户信息或商品详情。
List 是一个有序的字符串列表,支持从头部或尾部插入和删除元素,底层采用双向链表实现,常用于消息队列等结构。
Set 是无序且不包含重复元素的集合,基于哈希表实现,具备高效的查找与去重能力。
Zset(有序集合)与Set类似,但每个成员都关联一个分数(score),用于排序。其底层使用跳表实现,支持快速的范围查询操作,如ZRANGEBYSCORE。
在Redis中,ZSet同时利用跳表和哈希表来实现功能:跳表维护元素的排序顺序,而哈希表则用于存储member到score的映射关系,即key为member,value为score。
Redis提供了两种持久化机制——RDB和AOF,用以将内存中的数据写入磁盘,防止意外丢失。
RDB 通过生成内存快照的方式定期将数据保存至磁盘文件,适用于灾难恢复和备份。
RDB的生成方式有两种:
AOF(Append Only File)则是记录每一个写操作命令,并追加到日志文件末尾。Redis重启时可通过重放这些命令来重建数据状态。虽然在极端情况下(如刚写完内存但未落盘即断电)仍可能丢数据,但相比RDB具有更高的数据安全性。
AOF的工作流程如下:
持久化频率可设置为三种模式:always(每次写都刷盘)、everysec(每秒一次)、no(由系统决定)。
随着写操作不断累积,AOF文件会变得越来越大,因此引入了AOF重写机制:
此外,Redis还支持混合持久化模式,结合RDB和AOF的优势,在AOF重写时以RDB格式保存基础数据,后续增量操作仍以AOF形式追加,既提升了恢复效率,又保证了数据安全。
关于事务与Lua脚本的区别:
Redis的事务机制允许将多个命令打包发送,按顺序执行。但它并不具备严格的原子性——命令只是被放入队列等待执行,一旦执行开始,即使中间某条命令出错,后续命令依然会继续执行,不会自动回滚。此外,事务中的命令彼此独立,不能依赖前一条命令的结果,只有在调用EXEC之后才会真正执行。
相比之下,Lua脚本在Redis中以原子方式运行。整个脚本在单一线程中执行,期间不会被其他命令打断,天然具备隔离性。如果脚本内部出现错误,执行会立即终止,不会部分生效。更重要的是,Lua脚本支持逻辑控制和变量传递,可以基于前一条命令的结果进行条件判断或后续处理,灵活性远高于原生事务。
至于ZSet为何选择跳表而非B+树作为底层数据结构,主要原因在于Redis是内存数据库,而B+树主要针对磁盘存储优化。B+树通过高扇出和页式结构减少磁盘I/O次数,适合MySQL这类依赖磁盘的系统。而Redis运行于内存中,随机访问成本极低,无需复杂结构来优化I/O。跳表实现简单、性能良好、易于维护,且在插入和删除时仅需调整局部指针,无需像B+树那样进行节点分裂、合并和平衡操作,更适合Redis的设计目标。
在启用混合持久化机制后,Redis的AOF重写过程会将当前的数据状态以RDB格式写入AOF文件的起始部分,随后的增量操作则继续以AOF格式追加至文件末尾。这种结构结合了RDB快速恢复的优势与AOF日志的高数据安全性,既提升了重启时的加载速度,也有效降低了因宕机导致大量数据丢失的风险。
生成RDB快照期间,主进程中的数据是否可以被修改?
答案是可以。这依赖于操作系统提供的“写时复制”(Copy-on-Write)机制。当主进程通过fork创建子进程时,并不会立即复制全部内存数据,而是让父子进程共享同一份内存页,并将这些页面设置为只读。只有当主进程尝试修改某个内存页中的数据时,系统才会复制该页,主进程操作副本,而子进程仍保留原始页的内容,从而确保其生成的快照反映的是fork时刻的数据状态。这种方式显著减少了fork带来的性能开销。

若在子进程执行RDB持久化过程中,主线程持续执行大量写操作,会导致越来越多的内存页被复制,进而增加整体内存消耗。极端情况下可能触发OOM(Out of Memory),造成服务中断。
为应对这一问题,生产环境中建议采取以下措施:

指某个热点key在缓存过期的瞬间,大量并发请求直接穿透到数据库,导致数据库瞬时压力剧增,甚至崩溃。
解决方案:
互斥锁方式:当缓存未命中时,线程需先获取分布式锁。例如线程1获得锁后从数据库加载数据并回填缓存;线程2在此期间无法获取锁,则短暂休眠等待,待锁释放后再重新查询缓存获取最新结果。
逻辑过期方案:将缓存本身设置为永不过期,但在数据内部维护一个过期时间字段。请求到来时判断是否已“逻辑过期”,若是,则尝试获取锁并异步更新缓存,其他请求继续返回旧数据。此方法适用于允许短暂不一致的高并发场景。
指查询一个在数据库和缓存中都不存在的数据,每次请求都会直达数据库,造成不必要的资源浪费和潜在风险。
解决方案:
缓存空值:对确认不存在的数据,将其key对应的值设为null并设置较短过期时间存入缓存,后续相同请求可直接返回null,减少数据库压力。
布隆过滤器:在访问缓存前,利用布隆过滤器预先判断该key是否存在。若判定不存在则直接拒绝请求。但因其基于哈希算法,存在一定的误判率(即可能将存在的key误判为不存在,或反之)。
当大量缓存项在同一时间点集中失效,或Redis服务发生故障,所有请求都将转向数据库,引发数据库负载过高。
解决方案:

完全实时的一致性难以实现,但可通过以下策略尽可能维持两者同步:
策略一:先更新数据库,再删除缓存
流程为:更新数据库 → 删除缓存 → 后续读请求触发缓存重建。该方案简单高效,适用于一致性要求不高的多数业务场景。
可能出现的异常情况:请求A查询缓存未命中,从数据库读取旧值(如10);此时请求B更新数据库为20并删除缓存;接着请求A将之前读取的10写回缓存,导致缓存中存入错误数据。尽管概率较低(需缓存恰好失效且操作顺序交错),但仍可能发生。由于缓存操作通常远快于数据库更新,此类冲突较少见,因此该策略被广泛采用。
策略二:延迟双删
步骤为:先删除缓存 → 更新数据库 → 延迟一段时间后再次删除缓存。适用于对一致性要求较高、并发较大的环境。
缺点在于延迟时间难以精确控制——过短可能导致中间状态残留,过长则延长不一致窗口。
策略三:基于Binlog异步更新缓存
适用于高并发且强一致性要求的场景。通过监听MySQL的Binlog变化(如使用Canal工具),捕获数据变更事件后主动清除对应Redis缓存,失败则进行重试。也可通过MQ发布更新消息,由消费者异步处理缓存删除或刷新。优势在于不侵入业务代码,解耦性强。

主要原因是更新缓存的逻辑更为复杂,容易引发一致性问题。
举例说明:若采用“先更新缓存,再更新数据库”的模式,假设请求A欲将数据改为10,首先将缓存更新为10;此时请求B执行完整流程,将数据库和缓存均更新为20;随后请求A完成数据库更新为10。最终结果是数据库为10,缓存也为10,看似正确。但如果请求顺序颠倒或涉及多个字段,极易出现脏写或覆盖问题。
相比之下,删除缓存策略更轻量、安全,由下一次读请求按需重建,能有效规避并发更新带来的混乱。
在数据更新过程中,如果选择先更新数据库再同步缓存,同样可能引发数据不一致的问题。例如:请求A将数据库更新为10,但尚未更新缓存;此时请求B到来,先将数据库和缓存都更新为20;随后请求A继续执行,将其缓存也更新为10,最终导致缓存与数据库状态不一致。虽然这种情况发生的概率较低,但在每次更新缓存时均存在潜在风险,相比“先更新数据库后删除缓存”的策略,其不一致的窗口更大——因为后者仅在缓存未命中时才可能出现问题。
当 Redis 占用的内存达到设定上限时,系统会根据配置的淘汰策略决定如何释放空间。该策略通过配置项 maxmemory-policy 进行设置,共支持 8 种不同的方式,主要可分为两大类:开启数据淘汰 和 不开启数据淘汰。
noeviction:当内存使用超出限制时,不再接受写操作,直接返回错误。适用于对数据完整性要求高、不允许丢失任何键值的场景。
volatile-random:从已设置过期时间的 key 中随机选择一个进行删除。(原理类似于图示)
allkeys-random
volatile-ttl:优先淘汰剩余生存时间(TTL)最短的 key,适合用于需要快速清理即将过期数据的场景,如用户会话管理。
volatile-lru:基于 LRU(最近最少使用)算法,在设置了过期时间的 key 中淘汰最久未被访问的数据。(与下图机制相同,仅作用于带过期时间的键)
allkeys-lru
volatile-lfu:采用 LFU(最不经常使用)算法,从设置了过期时间的 key 中移除访问频率最低的一个。(同理见下图,限定范围为有过期时间的键)
allkeys-lfu
allkeys-random:在所有 key 中随机挑选并删除,适用于没有明确优先级区分的缓存环境。
allkeys-lru:在整个键空间中应用 LRU 算法,保留最近频繁使用的数据,适用于电商等热点商品集中访问的业务场景。
allkeys-lfu:在整个数据集中使用 LFU 算法,确保高频访问的内容得以保留,常见于社交媒体中热门内容的缓存优化。
Redis 的主从复制机制允许一个或多个从节点(slave/replica)从主节点(master)同步数据,从而实现数据冗余与读写分离。写操作由主节点处理,而读请求可分发至从节点,有效减轻主节点负载。
复制流程如下:
PSYNC
Redis 提供三种主流的集群部署方案:主从复制、哨兵模式(Sentinel)、以及原生集群模式(Redis Cluster)。
主从模式:这是最基础的集群结构,旨在解决单点故障问题。系统包含一个主节点和一个或多个从节点。主节点负责处理所有写操作及部分读操作,从节点则通过复制主节点数据来提供读服务。当主节点宕机时,可通过手动方式将某个从节点提升为主节点,实现故障转移。
优点:架构简单,适用于读多写少的应用场景。
缺点:缺乏自动容灾能力,主节点失效需人工干预切换。
哨兵模式:为弥补主从模式中无法自动故障转移的缺陷,Redis 引入了哨兵系统。哨兵是一组独立运行的监控进程,持续检测主从节点健康状态。一旦发现主节点异常,哨兵集群会自动选举新的主节点,并通知其余从节点和客户端更新连接信息,实现高可用。
集群模式(Redis Cluster):这是官方推荐的分布式解决方案,能够应对大规模数据存储需求。它通过哈希槽(hash slot)机制将数据自动分片到多个节点,每个节点仅管理一部分数据。同时内置主从架构以提高可用性——每个分片由一个主节点和若干从节点组成,主节点处理写请求,从节点负责数据复制和读取。
Redis Cluster 具备自动故障检测与转移能力,无需外部工具介入,因其内部集成了类似哨兵的逻辑,能自我协调完成节点替换与负载均衡。
Redis Cluster 构成了 Redis 的集群环境,具备自动分片能力,能够将数据分散存储在多个节点上。同时,其内部集成了类似哨兵的机制,支持故障自动转移,无需依赖外部组件即可实现高可用。(换句话说,Cluster 模式本身就包含了 Sentinel 功能,并实现了多节点间的数据分片)
Sentinel 被称为“哨兵”,主要用于监控和管理多个 Redis 实例,提升系统的可用性。但它本身并不提供数据分片功能,仅专注于主从状态监控与故障切换。
MOVED
在 Redis 集群架构中,数据是如何定位到具体节点的?这涉及到集群的核心实现原理:
整个集群由多个 Redis 节点组成,每个节点负责存储一部分互不重复的数据。系统将所有数据划分为 16384 个哈希槽(hash slots),并将这些槽分配给不同的节点。当客户端需要访问某个 key 时,会先计算该 key 所属的哈希槽,再连接对应节点进行操作。
那么,如何确定一个 key 对应哪个节点呢?
在存取 key 时,系统使用 CRC16 算法对该 key 进行计算,然后将结果对 16384 取模,从而得到对应的哈希槽编号。这样每一个 key 都能唯一映射到一个槽位。
通常情况下,客户端在启动阶段会从集群获取当前哈希槽到节点的映射表,以此来决定请求应发送至哪个节点。
若访问的 key 不在当前连接的节点上,Redis 服务器会返回一个重定向响应:
ASK
当客户端接收到
MOVED
类型的响应时,说明该 key 所属的哈希槽已经被迁移到另一个节点,客户端需更新本地的槽位映射信息并重新尝试请求。
而当收到
ASK
响应时,则表示集群正处于伸缩过程中(如扩容或缩容),迁移尚未完成。此时客户端只需临时重定向请求而不必立即更新映射关系。待迁移完成后,再次访问会触发
MOVED
响应,此时才应持久化更新槽位映射。
客户端依据
MOVE/ASK
指令的引导,自动跳转至正确的 Redis 节点完成操作。
关于 Redis 的哨兵机制:
哨兵机制是一种保障 Redis 高可用性的解决方案。一般建议以奇数个节点组成哨兵集群(至少三个),用于监控主从结构中的 Redis 实例,能够在主节点异常时自动执行故障转移,实现服务的自我恢复。
其主要职责包括以下几个方面:
那么,哨兵是如何判断主节点已经宕机的呢?这依赖于两个关键机制:
主观下线
每个哨兵每秒都会向其所监控的所有 Redis 节点发送 PING 请求。如果在预设时间内未收到 PONG 回复,该哨兵就会将该节点标记为“主观下线”。需要注意的是,这种判断是局部的,可能存在误判(例如网络抖动或哨兵自身问题)。
客观下线
只有主节点才会进入客观下线状态,从节点不会。当某个哨兵认为主节点主观下线后,它会向其他哨兵发起询问,确认它们是否也认为该主节点异常。如果超过半数的哨兵都达成一致,则判定为主观下线升级为“客观下线”,即正式确认故障,随即触发故障转移流程。
接下来是哨兵 leader 的选举过程:
当出现主节点客观下线情况时,参与判断的哨兵会成为候选者。每位候选者首先为自己投一票,并向其他哨兵发送拉票请求。每个哨兵仅有一次投票权,一旦投出便不能再更改。最终获得超过半数选票的哨兵将成为 leader,负责主导后续的主从切换操作。
在选出哨兵 leader 后,下一步是从现有从节点中挑选出新的主节点,选择过程遵循以下优先级顺序:
新主节点确定后,哨兵 leader 会命令其余从节点与新主节点建立复制关系。若原主节点恢复上线,也会被降级为从节点加入集群。最后,通过 Redis 的发布订阅机制,将新主节点的 IP 地址和端口号广播给客户端,完成整个主从切换流程。
关于 Redis 中热点 Key 的问题如何应对?
所谓热点 Key,是指在短时间内被频繁访问的某个特定 key,导致大量请求集中落在某一个节点上,造成 CPU 资源紧张、负载不均,影响整体性能和吞吐能力。
在处理Redis性能问题时,热点Key和Big Key是两个常见的瓶颈。针对这些问题,可以通过多种策略进行优化。
对于热点Key的处理,可以采用以下几种方式:
除了热点Key外,Big Key也是影响Redis性能的重要因素。所谓Big Key,指的是那些占用较大内存空间的Key或其对应的Value值过大,可能带来如下问题:
为解决Big Key问题,可采取以下措施:
扫码加好友,拉您进群



收藏
