Redis总结

发布于:2025-09-10 ⋅ 阅读:(17) ⋅ 点赞:(0)

一、Redis概述

Redis(Remote Dictionary Server)是一款开源的高性能键值数据库,以其出色的性能和丰富特性,在缓存、数据存储、消息中间件等场景广泛应用。

核心特性

  • 基于内存,极速读写:数据主要存储在内存中,读写操作绕开磁盘 IO,单实例能轻松支撑每秒数万次操作,是高并发场景的性能利器。
  • 丰富数据结构:不仅支持基础的字符串(String),还提供哈希(Hash)、列表(List)、集合(Set)、有序集合(Sorted Set)等复杂结构,甚至有位图(Bitmap)、地理信息(GEO)等拓展类型,能灵活适配多样业务需求。
  • 原子性操作:对数据结构的操作(如字符串追加、列表元素推入等)都是原子性的,由单线程负责执行,天然避免并发竞争问题,保证操作的一致性。
  • 持久化保障:支持 RDB(快照持久化,定期将内存数据生成磁盘快照)和 AOF(日志持久化,记录每一条写命令)两种方式,可根据场景选择,防止内存数据意外丢失。
  • 高可用与分布式:提供主从复制、哨兵(Sentinel)、集群(Cluster)等方案,实现故障自动转移、数据分片与水平扩展,保障服务稳定可靠。
  • 附加能力丰富:具备发布 / 订阅(Pub/Sub)、事务、Lua 脚本、过期策略(如 LRU 淘汰)等功能,还能支持生命周期有限的密钥、自动故障转移等,满足更多业务场景,像消息队列、分布式锁等都能基于 Redis 实现。

二、Redis数据类型

1、常见的五种数据类型及底层实现原理

String(字符串)

适用场景:缓存对象(如将用户信息序列化为字符串缓存)、常规计数(如文章阅读量计数)、分布式锁、共享 Session 信息等。

底层实现:主要基于SDS简单动态字符串)。SDS相比C语言原生字符串,具备诸多优势:可存储二进制数据;通过记录长度实现O(1)时间复杂度获取字符串长度;具备空间预分配和惰性空间释放机制,减少内存重分配次数API是安全的,拼接字符串前会检查空间,避免缓冲区溢出。

List(列表)

适用场景:消息队列(需注意自行实现消息唯一ID等问题)、最新消息排行等。

底层实现:

  • 列表的元素个数小于512个(默认值,可由list-max-ziplist-entries配置),且每个元素都小于64字节(默认值,可由list-max-ziplist-value配置)时,Redis会使用压缩列表(ziplist)作为List类型的底层数据结构。
  • 若不满足上述条件,Redis会使用双向链表作为List类型的底层数据结构。

不过在Redis 3.2 版本之后,List数据类型底层数据结构由 quicklist 实现,它整合了双向链表和压缩列表的优点。

Hash(哈希)

适用场景:缓存对象(如存储购物车信息,键为用户ID,值为商品ID和数量的哈希表)、购物车等。

底层实现:

  • 哈希类型元素个数小于512个(默认值,可由hash-max-ziplist-entries配置),且所有键值对都小于64字节(默认值,可由hash-max-ziplist-value配置)时,Redis会使用压缩列表作为Hash类型的底层数据结构。
  • 若不满足上述条件,Redis会使用哈希表为Hash类型的底层数据结构。

在Redis 7.0中,压缩列表数据结构已废弃,交由Hashpack数据结构来实现。

Set(集合)

适用场景:聚合计算(如交集、并集、差集)场景,比如点赞统计、共同关注、抽奖活动等。

底层实现:

  • 如果集合中的元素都是整数且元素个数小于512个(默认值,可由set-maxintset-entries),Redis会使用整数集合作为Set类型的底层数据结构。
  • 如果集合中的元素不满足上述条件,则Redis使用哈希表作为Set类型的底层数据结构。

ZSet(有序集合)

适用场景:排行榜(如排行榜单、电话和姓名排序等)。

底层实现:

  • 如果有序集合的元素个数小于128个,并且每个元素的值小于64字节时,Redis会使用压缩列表作为ZSet类型的底层数据结构。
  • 如果有序集合的元素不满足上面的条件,Redis会使用跳表作为ZSet类型的底层数据结构。

在Redis 7.0中,压缩列表数据结构已废弃,交由Hashpack数据结构来实现。

2、后续版本新增数据类型

BitMap(2.2版新增)

适用场景:二值状态统计的场景,比如签到、判断用户登录状态、连续签到用户总数等。

底层实现:基于字符串类型实现,通过对字符串的位操作来进行二值状态的存储与统计。

HyperLogLog(2.8版新增)

适用场景:海量数据基数统计的场景,比如百万级网页UV(独立访客)计数等。

底层实现:利用概率算法,以极小的内存空间来统计大规模数据的基数(不重复元素的数量)。

GEO(3.2版新增)

适用场景:存储地理位置信息的场景,比如滴滴打车等。

底层实现:基于ZSet实现,将地理位置的经纬度等信息进行编码后存储在ZSet中,从而实现地理位置的相关操作,如计算两点距离、查找附近地点等。

Stream(5.0版新增)

适用场景:消息队列,相比基于List类型实现的消息队列,它能自动生成全局唯一消息ID,支持以消费者组形式消费数据。

底层实现:有专门的底层数据结构,支持消息的持久化,多消费者组,消息确认等功能,为消息队列场景提供更完善的支持。

三、Redis持久化

Redis是基于内存的数据库,为防止进程退出、机器故障导致内存数据丢失,提供了RDB和AOF两种持久化方式。

持久化过程主要保存两方面内容:

  • RDB方式对当前数据的状态进行保存,以快照的形式留存最终的数据结果。它的存储格式比较简洁,核心关注点在于数据本身。
  • AOF方式把数据的操作过程记录下来,采用日志的形式存储操作流程,核心关注点在于数据是如何被操作的。

3.1 RDB(Redis Database):快照持久化

概念与原理:

RDB是在指定时间间隔内,把内存中的数据集快照写入磁盘,也就是“Snapshot”(快照)。恢复时,直接将快照文件加载到内存里。

手动触发

save指令:手动执行一次保存操作。但它会阻塞当前Redis服务器,直到RDB过程完成,,有可能造成长时间阻塞,线上环境不建议使用。

bgsave指令:手动启动后台保存操作,并非立即执行。它是对save阻塞问题的优化,Redis内部设计RDB操作基本都用 bgsave。

其工作原理是,当执行bgsave指令时,Redis会调用 fork函数生成子进程,由子进程负责创建RDB文件,父进程可继续处理客户端请求,最后子进程完成后返回消息。

注意:

Fork函数的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,是一个全新的进程,并作为原进程的子进程。

自动触发

通过配置save second changes来实现,当满足指定时间范围内 key 的变化数量达到设定数值时,就会进行持久化。其中,second是监控时间范围,changes是监控 key 的变化量,配置在conf文件中。需要注意的是,save 配置要根据实际业务情况设定,频率过高或过低都可能引发性能问题,而且 save 配置启动后执行的是 bgsave 操作。

优点:

  • RDB 是紧凑压缩的二进制文件,存储效率高,节省磁盘空间。
  • 内部存储的是 Redis 在某个时间点的数据快照,非常适合数据备份、全量复制等场景。
  • 恢复数据的速度比 AOF 快很多。

缺点:

  • 调用fork函数生成子进程时 ,内存中的数据会被克隆一份,大致 2 倍的膨胀性需考虑,且若数据庞大,即便用了写时拷贝技术,仍较消耗性能。
  • 数据一致性低(若宕机,会丢失 “上次快照到宕机” 的所有数据)
  • Redis 众多版本中 RDB 文件格式未统一,不同版本服务间可能出现数据格式不兼容现象。

3.2 AOF(Append Only File):日志持久化

概念与执行过程

AOF 以独立日志的方式,记录每次写命令。Redis 重启时,重新执行 AOF 文件中的命令,从而恢复数据。它的主要作用是解决数据持久化的实时性问题,目前是 Redis 持久化的主流方式。

执行过程为:客户端的请求写命令会被 append 追加到 AOF 缓冲区内;AOF 缓冲区根据 AOF 持久化策略(always、everysec、no),将操作 sync 同步到磁盘的 AOF 文件中;当 AOF 文件大小超过重写策略或手动重写时,会对 AOF 文件进行 rewrite 重写,压缩文件容量;Redis 服务重启时,重新 load 加载 AOF 文件中的写操作以恢复数据。

AOF 持久化策略(appendfsync)

  • always(每次):每次写入操作都同步到 AOF 文件中,数据零误差,但性能较低。
  • everysec(每秒):每秒将缓冲区中的指令同步到 AOF 文件中,数据准确性较高,性能也不错,不过系统突然宕机时,会丢失 1 秒内的数据。
  • no(系统控制):由操作系统控制每次同步到 AOF 文件的周期,整体过程不可控。

AOF写数据可能遇到的问题: 随着 AOF 不断写入,文件会越来越大。

为解决此问题,Redis 引入 AOF 重写机制

以图片里的例子来说,对于 name 这个键,先后执行了 set name zs、set name ls、set name ww 等多次设置操作;对于 num 这个键,有 incr num(多次自增)和 set num 3 操作。
在 AOF 重写时,会忽略之前对 name 和 num 的中间操作指令,只保留最终能得到当前数据状态的指令。也就是对于 name,只保留最终的 set name ww;对于 num,只保留 set num 3

这样做的原因是,AOF 重写的核心是将对同一个数据的若干条命令执行结果,转化为最终结果数据对应的命令进行记录。通过这种方式,能去除冗余的中间指令,压缩 AOF 文件体积,同时也能让后续基于 AOF 文件恢复数据时,更高效、快速地得到最终的数据状态,还能降低磁盘使用率、提高持久化和数据恢复效率等

重写规则包括:

  • 忽略进程内超时的数据,不再写入文件。
  • 忽略无效指令,重写时使用进程内数据直接生成,新 AOF 文件只保留最终数据的写入命令,像对del key1、hdel key2等删除类指令,以及对同一数据的多条写命令,会合并为一条命令。
  • 对于lpush、sadd等类型指令,每条指令最多写入 64 个元素,防止数据量过大造成客户端缓冲区溢出。

AOF 重写有手动和自动两种方式。

手动重写用 bgrewriteaof 指令

自动重写由触发机制决定何时重写,Redis 会记录上次重写时的 AOF 大小,当 AOF 文件大小是上次 rewrite 后大小的一倍且大于auto - aof - rewrite - min - size(重写的基准值,最小文件 4MB),同时达到auto - aof - rewrite - percentage(重写的基准值,文件达到 100%(相对于原来)开始重写)时,就会进行重写。

优点:

  • 数据持久化的实时性较好,能最大程度减少数据丢失,根据策略不同,最多丢失 1 秒数据(everysec 策略)。
  • AOF 文件是按顺序追加的,写入性能较好,即使文件很大,重写也能优化文件,不会影响读写性能。

缺点:

  • AOF 文件通常比 RDB 文件大,占用磁盘空间多。
  • 恢复数据的速度比 RDB 慢。
  • 写操作的记录会产生一定的资源消耗,不过相较于数据安全性,这种消耗在可接受范围内。

四、Redis删除策略

        Redis作为高性能的内存数据库,其数据删除策略在设计上需要在内存占用与CPU消耗之间寻找最佳平衡点。不当的删除策略可能导致内存溢出或CPU过载,进而影响Redis整体性能甚至引发服务宕机。
Redis采用了两种主要的数据删除策略:基于过期时间的删除策略基于内存淘汰的删除策略

基于过期时间的删除策略

Redis作为内存数据库,所有数据都存储在内存中。通过TTL(生存时间)指令可以查看键的状态:

  • XX:具有时效性的数据
  • -1:永久有效的数据
  • -2:已过期、被删除或未定义的数据

但是过期的数据并不是立即被删除,Redis采用特定的策略来管理过期数据的删除。

1、定时删除

执行机制:为每个设置了过期时间的key创建独立的定时器,当key的过期时间到达时,定时器立即触发删除操作

优点:

  • 内存管理高效:过期数据立即被删除,快速释放内存空间
  • 无内存浪费:确保过期数据不会长时间占用内存

缺点:

  • CPU资源消耗大:无论当前系统负载如何,都会占用CPU资源
  • 影响性能:可能降低Redis服务器的响应速度和指令吞吐量

用处理器性能换取存储空间(时间换空间

2、惰性删除

执行机制:

数据到达过期时间后,不会立即被删除;

只有当客户端访问该数据是才会检查:

  • 如果数据未过期,正常返回数据
  • 如果数据已过期,立即删除并返回空值

优点:仅在必要时(如数据被访问)执行删除,避免了不必要的CPU开销,对CPU友好且性能影响小。

缺点:会面临较大的内存压力,大量已经过期却还没被访问的数据,会长期停留在内存里,持续占用宝贵的内存空间;如果某些过期key永远不被访问,将造成永久性的内存浪费。

以存储空间换取处理器性能(空间换时间)

3、定期删除

执行框架:

Redis服务器初始化时读取配置参数 server.hz(默认值为10);

每秒执行 server.hz 次 serverCron()中的方法调用链:

                                                                databasesCron()→ activeExpireCycle();

时间控制:
  • 每次  activeExpireCycle()执行时间限制为:250ms / server.hz;
  • 确保删除操作不会过度占用CPU资源;
执行过程:

1.分库检测:Redis将数据库分为16个分区(expires[0-15]),每次检测一个分区

2.随机抽样:对每个分区的检测中,随机选择W个key进行检查(W=ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)

3.删除判断:

  • 如果key已过期,立即删除
  • 统计本轮删除的key数量

4.循环条件:

  • 如果删除数量 < W*25%,继续在当前分区循环检测;
  • 如果删除数量 > W*25%,转向下一个分区检测;

5.进度记录:使用 current_col 变量记录当前检测的分区位置,确保下次执行是继续从该为止开始。

优点:

  • 自适应调整:当过期key较多时,会在当前分区多执行几轮;当过期key较少时,快速转向下一个分区
  • 资源可控:严格限制执行时间,避免过度占用CPU
  • 全面覆盖:通过循环检测确保所有分区都能被处理到

基于内存淘汰的删除策略

内存不足时的处理机制:

当Redis使用内存存储数据是,在执行每个命令前都会调用 freeMemoryIfNeeded()函数来检测内存是否充足。如果内存无法满足新数据的最低存储要求。Redis会启动数据清理过程,这种清理策略被称为逐出算法

补充:

  • 逐出过程可能无法一次性清理出足够的内存空间,如果不成功则会反复执行。
  • 如果对所有数据尝试清理后仍无法满足内存要求,将返回错误:(error)OOM command not allowed when used memory > 'maxmemory'。

关键配置参数

1. maxmemory - 最大内存限制

  • 作用:设置Redis可使用的最大内存容量
  • 默认值:0(表示无限制)
  • 生产环境建议:通常设置为物理内存的50%以上,根据实际需求调整

2. maxmemory-samples - 采样数量

  • 作用:指定每次检测师随机选取的key数量
  • 为什么需要:避免全库扫描带来的性能损耗,通过随机采样保证性能
  • 建议值:通常设置为5-10,平衡精度和性能

3. maxmemory-policy - 淘汰策略(核心配置)

淘汰策略详解

针对设置过期时间的数据(易失数据集)

volatile-lru:从已设置过期时间的key中,淘汰最近最少使用的键

适用场景:热点数据分布明显的业务

volatile-lfu:从已设置过期时间的key中,淘汰使用频率最低的键

适用场景:需要根据访问频率淘汰数据的场景

volatile-ttl:从已设置过期时间的key中,淘汰剩余寿命最短的键

适用场景:希望尽快释放即将过期数据的内存

volatile-random:从已设置过期时间的key中,随机选择键进行淘汰

适用场景:数据访问模式无规律的情况

针对所有数据(全库数据)

allkeys-lru:从所有key中,淘汰最近最少使用的键

适用场景:所有数据都可能需要被访问,但访问频次不同

allkeys-lfu:从所有key中,淘汰使用频率最低的键

适用场景:需要根据全局访问频率进行淘汰

allkeys-random:从所有key中,随机选择键进行淘汰

适用场景:所有数据重要性相同,无特定访问模式

特殊策略

no-enviction:禁止数据驱逐,直接返回错误(Redis 4.0+的默认策略)

适用场景:数据绝对不能丢失的关键业务

五、企业级解决方案

缓存预热

问题本质

系统启动或缓存失效后,大量请求会直接穿透到数据库(“冷启动”),导致数据库压力骤增,甚至宕机。缓存预热的核心是在流量到来前,提前将热点数据加载到缓存,避免冷启动。

冷启动:是指系统启动初期、缓存完全未加载数据(或缓存大面积失效后),所有用户请求无法从缓存获取数据,只能直接穿透到数据库的状态,会导致数据库瞬时压力骤增,甚至引发服务不可用。

解决方案:

定时任务预热:
  • 编写独立的脚本或任务,在系统启动前或低峰期(如凌晨),模拟用户访问行为,从数据库中查询出热点数据(如近期热门商品、首页配置信息等),并将其批量写入缓存。

优点:实现简单,可控性强。

缺点:无法覆盖所有热点,热点数据可能动态变化。

日志分析预热:
  • 通过分析历史流量日志(如Nginx访问日志),统计出最频繁访问的数据Key。
  • 基于这些Key到数据库中进行查询并预热到缓存中。

优点:数据更精准,更贴近真实业务热点。

消息队列异步预热:
  • 系统启动后,先通过一个“开关”将部分非核心页面的读请求引流到数据库,同时将查询到的数据Key发送到消息队列(Kafka/RocketMQ)。
  • 再由专门的消息消费者异步地从数据库拉取数据并写入缓存。

优点:不影响主流启动速度,平滑预热。

企业级实践:

通常采用 “定时任务 + 日志分析” 相结合的方式。在系统发布前,通过定时任务完成大部分热点数据的预热。系统上线后,通过实时分析日志微调预热内容。

缓存雪崩

问题描述:

缓存雪崩是指在同一时段内,大量缓存Key同时失效,或者缓存服务集群宕机,导致所有原本应该访问这些失效缓存的请求,全部涌向数据库,引起数据库压力骤增甚至崩溃。

场景一(同时失效):很多Key的过期时间(TTL)设置相同,比如都在午夜零点失效,零点一过,所有请求瞬间打向数据库。

场景二(服务宕机):Redis集群某个主节点宕机,导致大量请求无法得到缓存响应。

解决方案:

差异化过期时间:
  • 为缓存Key设置过期时间时,增加一个随机值(例如:基础时间+随机时间)。
  • 公式:TTL = base_time + random(0,300)s
  • 这样能保证Key不会在同一时间点大面积失效,而是均匀分布在不同的时间点。
构建高可用缓存集群:

引入Redis Sentinel(哨兵)或 Redis Cluster(集群)模式,实现主从复制和故障自动转移。即使个别节点宕机,集群仍能提供服务,防止“全军覆没”。

服务降级与熔断:
  • 引入熔断器组件(如Sentinel、Hystrix)。当检测到数据库压力过大或响应过慢时,主动熔断对数据库的访问。
  • 对于非核心数据,直接返回降级内容(如默认值、友好提示);对于核心数据,可尝试继续访问,但必须保护数据库不被拖垮。
永不过期与异步更新(策略性使用):
  • 某些极热点Key可以设置为永不过期(-1),然后由后台任务或定时任务异步地更新缓存。这彻底避免了失效问题,但需要额外的更新逻辑。

企业级实践:

差异化TTL+高可用集群”是标配。再结合“服务熔断降级”作为最后防线,形成一个立体的防护系统。

缓存击穿

问题描述:

缓存击穿是指某个极端热点Key(如天安门阅兵新闻、秒杀商品)在失效的瞬间,有大量的并发请求同时到来。这些请求发现缓存失效后,同时去数据库查询数据并试图回写缓存,导致数据库瞬间压力过大。

  • 与缓存雪崩的区别:雪崩是大量Key失效,击穿是单个(或极少数)热点Key失效。

解决方案:

互斥锁(Mutex Lock):
  1. 当第一个请求发现缓存失效时,它并不立即去查询数据库,而是先获取一个分布式锁(如使用Redis的SETNX命令)。
  2. 获取到锁的请求再去数据库查询数据并回写缓存。
  3. 在此期间,其他并发请求要么等待,要么直接返回默认值。
  4. 一旦缓存被重建,后续请求就能直接从缓存中获取数据。

优点:强一致性,能有效保护数据库。

缺点:存在死锁风险和性能开销。

逻辑过期:
  • 不设置Redis的物理TTL,而是在Value中存储一个逻辑的过期时间(expire_time)。
  • 当请求发现当前时间大于逻辑过期时间时,说明数据已旧。
  • 此时,一个请求获取锁进行异步更新,其他请求则直接返回旧的缓存数据,而不会等待。

优点:性能极好,用户体验无感知。

缺点:有一段时间的数据不一致,实现复杂。

企业级实践:

  • 对于金融、交易等对一致性要求高的场景,推荐使用“互斥锁”。
  • 对于资讯、社交等对性能和要求最终一致性的场景,“逻辑过期”是更优的选择。

缓存穿透

问题描述:

缓存穿透是指查询一个数据库中根本不存在的数据。由于缓存中也不会有该数据,导致每次请求都会穿透缓存,直接查询数据库。如果有人恶意构造大量此类不存在的Key发起攻击,数据库很容易被拖垮。

  • 场景:查询一个不存在的商品ID(如负数或非常大的ID)、不存在的用户ID。

解决方案:

参数校验:

在API网关或业务层对请求参数进行初步的合法性校验(如ID是否为负数、字段长度是否合规)。这是最简单有效的第一道屏障。

布隆过滤器(Bloom Filter):

在缓存之前,设置一个布隆过滤器。

工作原理: 布隆过滤器是一个高效的数据结构,用于判断“某元素一定不存在”或“可能存在”与某个集合。

流程:

将所有可能存在的Key(如所有有效的商品ID)哈希后存入布隆过滤器。

当一个请求到来时,先用布隆过滤器判断Key是否存在。

  • 如果不存在,直接返回空,无需访问缓存和数据库。
  • 如果存在,则继续后续的缓存和数据库查询流程。

优点:内存占用极小,效率极高。

缺点:有误判率(“可能存在”),但缓存穿透场景下,这个缺点可以接受(极少数合法请求可能被误判,但最终仍能从数据库拿到数据)。数据删除困难。

企业级实践:

“参数校验 + 布隆过滤器” 是防御恶意攻击的黄金组合。对于系统内部可能产生的无效请求(如用户输错ID),可以辅以 “缓存空对象” 策略。

六、Redis 采用单线程为何速度极快?

Redis 的核心网络 I/O 和数据操作采用单线程模型,却能实现极高的吞吐量(可达 10W/秒),主要原因如下:

  • 内存操作与高效数据结构:Redis 数据完全存储在内存中,操作速度极快,同时采用了哈希表、跳表等高效数据结构,确保了每个操作本身的高性能。
  • 避免多线程开销:单线程模型消除了多线程的上下文切换、锁竞争等开销,避免了死锁问题,使得代码更简单、高效。
  • I/O 多路复用:通过 select/epoll 机制,Redis 使用一个线程监听多个客户端连接请求(监听 Socket)和已连接请求(已连接 Socket),当任意 Socket 有数据到达时,内核就会通知 Redis 线程处理,高效管理了大量网络连接。

综上,Redis 的瓶颈通常在于内存或网络带宽而非 CPU,因此单线程模型在避免复杂性的同时,充分发挥了性能潜力。

下面通过一个场景来解释I/O多路复用:

餐厅服务员的故事:从手忙脚乱到气定神闲
想象一下,Redis 是一个服务员,而每一个客户端的请求就是一个来吃饭的客人。

方式一:阻塞 I/O(最笨的服务员)
最早的服务员工作方式是:

  1. 服务员站在餐厅门口,等一个客人来。
  2. 客人A来了,服务员把他领到座位上,然后站在旁边等客人A点餐。
  3. 客人A看菜单很慢,服务员就一直在那干等,什么事也做不了
  4. 终于点完餐,服务员把菜单交给厨房,然后又站在厨房门口干等厨师做完菜。
  5. 期间客人B、客人C来了,没人接待,他们只能在那傻等甚至离开。
  6. 直到把菜上给客人A,服务员才回去接待下一个客人。

结论:这种模式效率极低!服务员大部分时间都在“等待”,资源被严重浪费。这就像最古老的网络编程,一个线程处理一个连接,全程阻塞。

方式二:非阻塞 I/O(忙碌的服务员)
服务员学聪明了一点:

  1. 服务员不再干等。接待客人A后,问他:“点好没?”
  2. 如果客人A说“没好”,服务员就立刻离开,去问客人B“点好没?”,问完客人B又去问客人C,然后再轮回来问客人A。
  3. 他不停地在整个大厅里跑来跑去,循环地问每一个人“好了没?好了没?”。

结论:效率比第一种高,因为没人干等了。但服务员非常累(CPU 消耗高),大部分时间都花在了“跑来跑去”和“询问”这个过程本身上,而不是实际“端菜”这个有价值的工作上。

方式三:I/O 多路复用(高效的服务员) - Redis采用的方式
现在,服务员用上了高科技点餐系统(这就是 select/epoll):

客人入座后,每人给一个按铃这个铃就是文件描述符 Socket)。

服务员不再主动询问客人,而是悠闲地坐在前台,看着一个中央控制大屏幕。

这个屏幕会显示所有客人的按铃状态。服务员只需要关注谁亮了铃

突然,屏幕显示“客人A铃响了”,服务员就知道客人A点好餐了,于是过去取菜单。

之后,他又回到屏幕前等待。这时“客人C铃响了”和“厨房铃响了”(客人B的菜做好了),服务员再去处理。

在这个比喻中:

服务员:就是 Redis 的单个线程。

客人按铃:每个客户端的网络连接(Socket)有数据到来(例如发送了命令)时,会发出一个可读的信号。

厨房铃响:网络连接准备好可以写入数据(例如可以返回结果了),会发出一个可写的信号。

中央控制屏幕(epoll):就是操作系统内核提供的多路复用机制。它帮服务员监视着所有“铃”(连接),并高效、准确地通知哪些铃响了(哪些连接上有事件就绪)。

结论:服务员(Redis 线程)不再需要盲目地跑来跑去,也无需等待。他大部分时间都在“等待通知”,一旦有事件发生(铃响),他就去处理。这样,一个服务员就能高效地应对整个餐厅的客人,最大化了工作效率。

这就是 I/O 多路复用的核心思想:一个线程(进程)通过一种机制,可以监视多个文件描述符(Socket),一旦其中某个描述符就绪(读就绪或写就绪),就能够通知程序进行相应的读写操作。 Redis 正是依靠这项技术,用单线程就能处理数万甚至数十万的并发连接。


网站公告

今日签到

点亮在社区的每一天
去签到