redis 5种数据类型
string
字符串是 Redis 里最基础的数据类型,一个键对应一个值。
设置值
SET key value
例如:
SET name "John"
获取值
GET key
例如:
GET name
list
列表是简单的字符串列表,按插入顺序排序。
在列表头部添加元素
LPUSH key value1 [value2 ...]
例如:
LPUSH mylist "apple" "banana"
从列表头部移除并返回元素
LPOP key
例如:
LPOP mylist
set
集合是字符串的无序集合,且每个元素唯一。
添加元素到集合
SADD key member1 [member2 ...]
例如:
SADD myset "red" "green"
获取集合中的所有元素
SMEMBERS key
例如:
SMEMBERS myset
hash
哈希类型是键值对的集合,特别适合用于存储对象。
设置字段值
HSET key field value
例如:
HSET user:1 name "John" age 30
获取字段值
HGET key field
例如:
HGET user:1 name
zset
有序集合和集合类似,不过每个成员都关联了一个分数,分数用于对成员进行排序。
添加成员到有序集合
ZADD key score1 member1 [score2 member2 ...]
例如:
ZADD myzset 1 "one" 2 "two"
获取有序集合中指定范围的成员
ZRANGE key start stop [WITHSCORES]
例如:
ZRANGE myzset 0 -1 WITHSCORES
redis 5种基本数据类型使用场景
- 字符串(String)
- 缓存数据:常用于缓存各种类型的数据,如数据库查询结果、页面片段、JSON 数据等。例如,将一个复杂的数据库查询结果缓存为字符串,下次请求相同数据时可以直接从 Redis 中获取,减少数据库查询压力。
计数器:利用字符串类型的原子递增 / 递减操作,可以轻松实现计数器功能。比如,记录网站的访问量、文章的点赞数、视频的播放量等。 - 分布式锁:通过设置一个具有特定过期时间的字符串键来实现分布式锁。当一个客户端成功设置了这个键,就表示它获取了锁,其他客户端在锁过期之前无法获取锁,以此来保证在分布式环境下的资源互斥访问。
- 缓存数据:常用于缓存各种类型的数据,如数据库查询结果、页面片段、JSON 数据等。例如,将一个复杂的数据库查询结果缓存为字符串,下次请求相同数据时可以直接从 Redis 中获取,减少数据库查询压力。
- 哈希(Hash)
- 存储对象:非常适合存储对象的相关信息,如用户信息、商品信息等。可以将对象的各个属性作为哈希的字段,属性值作为字段的值,这样可以方便地对对象的属性进行单独的查询、更新等操作,而不需要像在关系型数据库中那样进行复杂的表连接操作。
- 配置信息管理:用于存储应用的配置信息,将配置项作为字段,配置值作为字段值。这样可以方便地在运行时动态修改配置信息,而不需要重新启动应用程序。
- 列表(List)
- 消息队列:可以作为简单的消息队列使用,生产者将消息通过 LPUSH 命令从列表头部插入,消费者通过 RPOP 命令从列表尾部取出消息,实现异步消息处理。例如,在一个电商系统中,订单生成后可以将订单信息放入一个列表中,由后台的订单处理服务从列表中取出订单进行处理。
- 任务队列:与消息队列类似,用于存储待处理的任务。不同的是,任务队列可能会根据任务的优先级、类型等进行分类存储,消费者可以根据自身的能力和需求从不同的列表中获取任务进行处理。
历史记录和日志:用于记录用户的操作历史、系统的日志信息等。可以按照时间顺序将相关信息通过 RPUSH 命令插入到列表中,然后根据需要可以通过 LRANGE 命令获取指定范围的历史记录或日志。
- 集合(Set)
- 标签和分类:用于存储对象的标签或分类信息。例如,在一个文章系统中,每篇文章可以有多个标签,将文章的标签存储在一个集合中,方便进行标签的添加、删除和查询操作,也可以轻松实现根据标签进行文章的筛选和分类。
- 去重:由于集合中的元素是唯一的,因此可以用于对数据进行去重处理。例如,在处理用户注册信息时,可以将用户的邮箱或手机号码存储在一个集合中,当有新用户注册时,只需检查集合中是否已存在该邮箱或手机号码,就可以快速判断用户是否重复注册。
- 交集、并集和差集运算:可以方便地对多个集合进行交集、并集和差集等集合运算。例如,在社交网络中,可以通过集合运算找出两个用户共同关注的人(交集),或者某个用户关注的所有人与另一个用户关注的所有人的总和(并集),以及某个用户关注但另一个用户未关注的人(差集)。
- 有序集合(Sorted Set)
- 排行榜:常用于实现各种排行榜功能,如游戏排行榜、商品销量排行榜、用户积分排行榜等。将排行榜的对象(如用户 ID、商品 ID 等)作为有序集合的成员,将对应的排名依据(如积分、销量等)作为分数,通过 ZRANK、ZREVRANK 等命令可以快速获取某个成员的排名,通过 ZRANGE、ZREVRANGE 等命令可以获取排行榜的前几名或后几名。
- 带权重的任务队列:与普通的列表任务队列不同,有序集合可以根据任务的优先级或权重来进行排序。将任务的权重作为分数,任务的标识作为成员,这样消费者在获取任务时,可以优先获取权重高的任务进行处理。
- 地理位置信息存储和查询:可以将地理位置信息(如经纬度)存储在有序集合中,通过计算成员之间的距离来实现附近地点查询等功能。例如,在一个外卖应用中,可以通过有序集合存储商家的地理位置信息,当用户查询附近的商家时,可以根据用户的位置计算与各个商家的距离,并按照距离进行排序返回。
redis 5种基本数据类型实现原理
Redis 的 5 种基本数据类型分别是字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set),下面为详细介绍它们的实现原理。
- 字符串(String)
- 简单动态字符串(SDS):Redis 并没有直接使用 C 语言的字符串,而是采用简单动态字符串(SDS)来实现。SDS 结构包含字符串长度、剩余空间和实际存储的字符串内容。这种结构让 Redis 能高效地执行字符串操作。
- 优势:获取字符串长度的时间复杂度为 O(1);能避免缓冲区溢出;可以二进制安全地存储任意数据;并且在修改字符串时,通过预分配和惰性释放机制减少内存重新分配的次数。
struct sdshdr {//记录buf数组中已使用字节的数量//等于SDS所保存字符串的长度int len;//记录buf数组中未使用字节的数量,表示声音可用空间int free;//字节数组,用于存放字符串char buf[];
};
- 哈希(Hash)
- 实现方式:Redis 的哈希类型有两种实现方式,分别是压缩列表(ziplist)和哈希表(hashtable)。
- 压缩列表:当哈希元素数量较少且每个元素的键值长度较短时,Redis 会使用压缩列表来存储哈希。压缩列表是一种连续内存块组成的顺序数据结构,能有效节省内存。
- 哈希表:当哈希元素数量增多或者键值长度变长时,Redis 会将存储方式转换为哈希表。哈希表使用链地址法来解决哈希冲突,每个哈希桶是一个链表,当链表过长时会进行 rehash 操作,将数据重新分配到新的哈希表中。
- 压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序性(sequential)数据结构。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值
下图是压缩列表的各个组成部分:
下图是一个压缩列表示例:
- 列表(List)
- 实现方式:Redis 的列表类型也有两种实现方式,即压缩列表和双向链表。
- 压缩列表:与哈希类型类似,在元素数量少且元素长度短时,使用压缩列表存储,节省内存。
- 双向链表:当元素数量增多或元素长度变长时,使用双向链表存储。双向链表的每个节点包含指向前一个节点和后一个节点的指针,方便进行插入、删除和遍历操作。
- 集合(Set)
- 实现方式:集合类型的实现方式有整数集合(intset)和哈希表。
- 整数集合:当集合中的元素都是整数,并且元素数量较少时,Redis 使用整数集合存储。整数集合是一个有序的整数数组,能高效地存储和查找整数元素。
- 哈希表:当集合中的元素包含非整数或者元素数量增多时,Redis 会使用哈希表存储。哈希表的键就是集合的元素,值统一为 null,通过哈希表的特性保证元素的唯一性。
- 整数集合(intset)可以保存类型为int16_t、int32_t、int64_t的整数值,并且保证集合中不会出现重复元素。整数集合有序、无重复
- 有序集合(Sorted Set)
- 实现方式:有序集合采用跳跃表(skiplist)和哈希表的组合来实现。
- 跳跃表:跳跃表是一种有序的数据结构,它在链表的基础上增加了多层索引,使得查找、插入和删除操作的平均时间复杂度O(logN)。在有序集合中,跳跃表根据元素的分数进行排序,方便进行范围查找。
- 哈希表:哈希表用于存储元素和分数的映射关系,使得可以在 O(1)时间复杂度内根据元素获取其分数。
Redis 中的有序集合zset是一种非常重要的数据结构,它通过巧妙地结合哈希表和跳表来实现高效的存储和操作。以下是zset对哈希表和跳表的具体使用方式:
- 哈希表的使用
- 实现成员到分数的映射:zset中的哈希表用于存储成员到分数的映射关系。这样,通过成员可以快速查找对应的分数,时间复杂度为 (O(1))。例如,在一个存储学生成绩的zset中,学生的姓名作为成员,成绩作为分数,通过哈希表可以迅速根据学生姓名找到其成绩。
- 实现快速存在性检查:利用哈希表的特性,可以快速判断一个成员是否存在于zset中。这在一些应用场景中非常有用,比如在实时统计在线用户的活跃时间时,能够快速判断某个用户是否已经在zset中,以便进行相应的操作。
- 跳表的使用
- 实现按分数排序:跳表是一种多层的有序链表结构,zset中的跳表按照分数对成员进行排序。在跳表中,每个节点包含成员、分数以及指向其他节点的指针。通过这种结构,可以在 (O(log n)) 的时间复杂度内完成对有序集合的插入、删除和查找操作。例如,在一个存储商品销量排名的zset中,通过跳表可以快速找到销量最高的商品,或者根据销量范围查找相应的商品列表。
- 支持范围查询:跳表的结构使得zset能够高效地支持范围查询。例如,可以快速查找分数在某个区间内的所有成员,或者按照排名范围获取相应的成员列表。这在很多实际应用中非常常见,比如查找成绩排名在班级前 10% 的学生,或者查找热度排名在某个范围内的文章等。
redis持久化
Redis 持久化是指将 Redis 内存中的数据保存到硬盘等持久化存储介质中,以便在 Redis 服务器重启或故障时能够恢复数据,保证数据的安全性和可靠性。Redis 提供了两种主要的持久化方式:RDB(Redis Database)持久化和 AOF(Append Only File)持久化。
RDB 持久化
工作原理
RDB 持久化是通过将 Redis 在内存中的数据库状态保存到磁盘上的 RDB 文件来实现的。它可以在指定的时间间隔内,对内存中的数据进行快照,将数据以二进制的格式写入到 RDB 文件中。当 Redis 服务器重启时,它会自动加载 RDB 文件,将数据恢复到内存中。
触发方式:
- 自动触发:可以通过配置 save 命令来设置自动触发 RDB 持久化的条件。例如,save 900 1 表示在 900 秒内如果至少有 1 个键发生了修改,就会触发 RDB 持久化;save 300 10 表示在 300 秒内如果至少有 10 个键发生了修改,就会触发 RDB 持久化。可以配置多个 save 条件,只要满足其中一个条件,就会触发 RDB 持久化。
- 手动触发:可以使用 SAVE 或 BGSAVE 命令手动触发 RDB 持久化。SAVE 命令会阻塞 Redis 服务器进程,直到 RDB 文件生成完毕;BGSAVE 命令则会在后台 fork 一个子进程来执行 RDB 持久化操作,不会阻塞服务器进程。
优点:
- 数据恢复快:RDB 文件是一个紧凑的二进制文件,在恢复数据时,Redis 可以快速地将 RDB 文件中的数据加载到内存中,相比 AOF 方式,恢复速度通常更快。
- 适合大规模数据备份:RDB 文件可以用于数据的定期备份,例如可以将 RDB 文件复制到其他存储设备或服务器上,以防止数据丢失。由于 RDB 文件是一个整体,因此在进行大规模数据备份和恢复时非常方便。
缺点:
- 数据可能丢失:由于 RDB 是按照一定的时间间隔进行快照的,如果在两次快照之间发生了服务器故障,那么这期间的数据将会丢失。例如,如果设置了每 5 分钟进行一次 RDB 快照,而在第 4 分钟时服务器发生故障,那么这 4 分钟内的数据将无法恢复。
- fork 子进程开销:在执行 BGSAVE 命令时,Redis 需要 fork 一个子进程来进行 RDB 文件的生成。fork 操作会消耗一定的系统资源,并且如果内存中的数据量很大,fork 过程可能会比较耗时,甚至会导致服务器短暂的阻塞。
AOF 持久化
- 工作原理:AOF 持久化是将 Redis 服务器执行的写命令以追加的方式记录到 AOF 文件中。当 Redis 服务器重启时,它会重新执行 AOF 文件中的命令,将数据恢复到内存中。
- 触发方式:AOF 持久化是在每次执行写命令时,将命令追加到 AOF 文件中。不过,实际的写入磁盘操作并不是立即执行的,而是根据配置的 appendfsync 参数来决定何时将数据真正写入磁盘。
优点:
- 数据完整性高:由于 AOF 是记录每一个写命令,因此可以保证数据的完整性。只要 AOF 文件没有损坏,就可以通过重新执行 AOF 文件中的命令来恢复到故障前的状态,相比 RDB 方式,数据丢失的风险更小。
- 可读性好:AOF 文件是一个文本文件,其中记录了 Redis 执行的所有写命令,因此可以很容易地查看和分析 AOF 文件中的内容,对于故障排查和数据审计非常有帮助。
缺点:
- 文件体积大:随着 Redis 服务器的运行,AOF 文件会不断增大,因为它记录了所有的写命令。这可能会导致磁盘空间不足,并且在恢复数据时,由于需要执行大量的命令,恢复速度相对较慢。
- 恢复速度慢:相比 RDB,AOF 在恢复数据时需要重新执行大量的写命令,因此恢复速度会比较慢。特别是当 AOF 文件非常大时,恢复过程可能会消耗很长时间。
AOF重写是什么
- 背景:由于AOF文件里记录的是写命令,那命令越来越多,AOF文件就会越来越大,数据还原时间就会越来越长。比如我连续rpush key v1-6,执行6次,AOF就需要保存6条命令。
- AOF文件重写:读取Redis数据库中的数据,而不是读取现有的AOF文件。比如旧AOF文件记录了6条rpush命令,那么新AOF文件会从数据库中读取键现在的值,然后用一条命令去记录键值对,这样6条命令变成了一条。重新完后新的AOF文件会替换旧AOF文件。但是新AOF文件不会包含浪费空间的冗余命令,所以体积较小,这就解决了AOF文件体积膨胀的问题
- 特殊情况,比如一个集合键有100个元素,按理说AOF文件中会记录SADD key + 100个元素,实际上,这个命令太长了,执行时会造成客户端缓冲区溢出,所以重写程序在处理列表、哈希表、集合和有序集合这4种键时,会见检查元素量,如果元素超过64个,就会用多条命令记录键的值,比如SADD100个元素,那么分2条命令,一条是SADD 64个,另一条是SADD36个。
AOF有哪几种日志策略
- AOF_FSYNC_ALWAYS:每执行一次命令就保存一下数据,性能低
- AOF_FSYNC_NO:不主动将AOF文件的内存从缓存保存到磁盘中,交给操作系统去决定何时保存到磁盘(比如Redis被关闭,AOF功能被关闭的时候)
- AOF_FSYNC_EVERYSE:每秒保存一次
混合持久化
从 Redis 4.0 开始,支持混合持久化方式。这种方式结合了 RDB 和 AOF 的优点,在进行持久化时,首先会使用 RDB 方式将内存中的数据进行快照,然后将 RDB 数据和后续的写命令以 AOF 的格式追加到同一个文件中。在恢复数据时,先加载 RDB 部分的数据,然后再执行 AOF 部分的命令,这样既可以提高数据恢复的速度,又可以保证数据的完整性。
持久化配置
在 Redis 的配置文件中,可以通过以下参数来配置持久化方式和相关参数:
- save:用于配置 RDB 持久化的触发条件,如 save 900 1 save 300 10 等。
- appendonly:用于开启或关闭 AOF 持久化,appendonly yes 表示开启 AOF 持久化,appendonly no 表示关闭 AOF 持久化。
- appendfsync:用于配置 AOF 文件的同步策略,有 always、everysec 和 no 三个可选值。always 表示每次写命令都立即同步到磁盘,数据安全性最高,但性能最低;everysec 表示每秒将 AOF 缓冲区中的数据同步到磁盘,这是默认的配置,兼顾了数据安全性和性能;no 表示由操作系统来决定何时将数据同步到磁盘,性能最高,但数据安全性最低。
可以根据实际应用场景和对数据安全性、性能的要求,选择合适的持久化方式和配置参数。通常情况下,可以同时开启 RDB 和 AOF 持久化,以提供更可靠的数据保护。
redis发布订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。微信、微博、关注系统!
Redis客户端可以订阅任意数量的频道
订阅/发布消息图:
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
命令
这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。
命令
PSUBSCRIBE pattern [pattern…] 订阅一个或多个符合给定模式的频道。
PUNSUBSCRIBE pattern [pattern…] 退订一个或多个符合给定模式的频道。
PUBSUB subcommand [argument[argument]] 查看订阅与发布系统状态。
PUBLISH channel message 向指定频道发布消息
SUBSCRIBE channel [channel…] 订阅给定的一个或多个频道。
SUBSCRIBE channel [channel…] 退订一个或多个频道
127.0.0.1:6379> subscribe blog # 订阅频道
Reading messages... (press Ctrl-C to quit) # 等待推送信息
1) "subscribe"
2) "blog"
3) (integer) 1
1) "message" # 消息
2) "blog" # 消息来自频道
3) "hello world!" # 消息内容
127.0.0.1:6379> publish blog "hello world!" # 发送消息到频道
(integer) 1
127.0.0.1:6379>
原理
每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。
客户端订阅,就被链接到对应频道的链表的尾部,退订则就是将客户端节点从链表中移除。
使用场景:
实时消息系统!
实时聊天!(频道当作聊天室,将信息回显给所有人)
订阅,关注系统都是可以
复杂的情况,使用专业的消息中间件
做订阅的缺点
如果一个客户端订阅了频道,但自己读取消息的速度却不够快的话,那么不断积压的消息会使redis输出缓冲区的体积变得越来越大,这可能使得redis本身的速度变慢,甚至直接崩溃。
这和数据传输可靠性有关,如果在订阅方断线,那么他将会丢失所有在短线期间发布者发布的消息。
redis事务
Redis 事务允许在一个步骤中执行一组命令,并且保证这些命令作为一个单独的、不可分割的操作序列执行。
事务的基本概念
Redis 事务通过 MULTI、EXEC、DISCARD 和 WATCH 这几个命令来实现。其基本特性包括:
- 原子性:事务中的所有命令要么全部执行,要么全部不执行。不过,Redis 事务的原子性是有限的,在执行过程中如果某条命令执行失败,后续命令仍会继续执行。
- 隔离性:在事务执行期间,不会有其他客户端的命令插入到事务执行序列中,保证事务的操作是相互隔离的。
事务的基本命令及使用示例
- MULTI
用于开启一个事务块,后续输入的命令会被放入事务队列中,而不会立即执行。
MULTI
- EXEC
执行事务队列中的所有命令。如果在执行 EXEC 之前没有使用 DISCARD 取消事务,那么队列中的命令将按顺序依次执行。
EXEC
- DISCARD
取消当前事务,清空事务队列,之前放入队列中的命令不会被执行。
DISCARD
- WATCH
用于监视一个或多个键,在事务执行之前,如果被监视的键被其他客户端修改,那么事务将被取消,EXEC 命令会返回 nil。
WATCH key1 key2 ...
事务使用示例
下面是一个简单的 Redis 事务使用示例,展示了如何使用 MULTI 和 EXEC 来执行一组命令:
# 开启事务
MULTI
# 设置键值对
SET key1 "value1"
SET key2 "value2"
# 获取键的值
GET key1
GET key2
# 执行事务
EXEC
在这个示例中,SET 和 GET 命令会被依次放入事务队列中,当执行 EXEC 命令时,这些命令会按顺序执行,并返回每个命令的执行结果。
Redis 事务的局限性
- 错误处理:在事务队列中,如果某条命令存在语法错误,整个事务在执行 EXEC 时会失败;但如果是在执行过程中某个命令执行失败(例如对一个非哈希类型的键执行哈希操作),其他命令仍会继续执行,不会回滚已经执行的命令。
- 不支持嵌套事务:Redis 不支持在一个事务中嵌套另一个事务。
结合 WATCH 实现乐观锁
WATCH 命令可以用于实现乐观锁机制,保证在并发环境下数据的一致性。以下是一个使用 WATCH 的示例:
# 监视键
WATCH key
# 开启事务
MULTI
# 获取键的值
GET key
# 修改键的值
SET key "new_value"
# 执行事务
EXEC
在这个示例中,如果在执行 EXEC 之前,key 被其他客户端修改,那么事务将被取消,EXEC 会返回 nil。客户端可以根据返回结果来决定是否重新尝试执行事务
测试多线程修改值,使用watch可以当作redis的乐观锁操作
①开启两个客户端,模拟多线程情况
②左边支出20元(但是不执行事务),然后右边修改money的数值
③左边执行事务,发现执行操作返回nil,查看money和out,发现事务并没有被执行(确实有乐观锁的效果)
如果修改失败获取最新的值就好(exec、unwatch、discard都可以清除连接时所有的监视)
小结
使用Redis实现乐观锁(watch监听某一个key,获取其最新的value)
在提交事务时,如果key的value没有发生变化,则成功执行
在提交事务时,如果key的value发生了变化,则无法成功执行
redis部署模式
Redis 有多种部署模式,以满足不同应用场景下的性能、可靠性和可扩展性需求。
单机模式
- 部署方式:将 Redis 服务器安装在一台独立的服务器上,所有数据都存储在这台服务器的内存中。这是最简单的部署方式,适用于小型应用或开发测试环境。
- 优点:部署简单,易于管理和维护。成本较低,只需要一台服务器。适用于数据量较小、并发访问量不高的场景。
- 缺点:单点故障问题,如果服务器出现故障,整个系统将无法正常运行。内存容量有限,受限于服务器的内存大小。性能也受限于单台服务器的处理能力,无法满足高并发、大规模数据处理的需求。
主从复制模式
- 部署方式:由一个主节点(Master)和多个从节点(Slave)组成。主节点负责处理所有的写操作,并将数据异步复制到从节点。从节点可以接受读请求,分担主节点的读压力。
- 优点:提高了系统的读性能,通过增加从节点可以水平扩展读能力。实现了数据的冗余备份,从节点可以在主节点出现故障时提供数据恢复。
- 缺点:主节点仍然是单点故障,如果主节点出现故障,需要手动或通过其他机制将从节点提升为主节点,否则整个系统的写操作将无法进行。数据复制存在一定的延迟,特别是在网络环境较差的情况下,可能会导致从节点的数据与主节点不一致。
Sentinel(哨兵)模式
- 部署方式:在主从复制模式的基础上,引入了 Sentinel 进程。Sentinel 用于监控 Redis 主节点和从节点的状态,当主节点出现故障时,自动进行故障转移,将一个从节点提升为主节点,并通知其他从节点和客户端新的主节点地址。
- 优点:实现了自动故障转移,提高了系统的可用性。多个 Sentinel 节点可以组成集群,避免了 Sentinel 单点故障问题。
- 缺点:Sentinel 本身也需要一定的资源来运行,增加了系统的复杂性和运维成本。在故障转移过程中,可能会有短暂的服务中断,并且可能会丢失部分未及时复制到从节点的数据。
集群模式
- 部署方式:将数据分布在多个节点上,每个节点负责存储一部分数据。节点之间通过 gossip 协议进行通信,以维护集群的状态。客户端可以连接到集群中的任意节点,通过请求重定向机制来访问所需的数据。
- 优点:可以水平扩展系统的存储和处理能力,通过增加节点来应对大规模数据和高并发访问。数据在多个节点上分布存储,提高了数据的可靠性和容错能力。
- 缺点:集群的部署和管理相对复杂,需要考虑数据的分片、节点的故障处理、数据迁移等问题。对客户端的支持要求较高,需要客户端能够感知集群的拓扑结构并进行请求的正确路由。
云托管模式
- 部署方式:将 Redis 部署在云服务提供商提供的托管环境中,如阿里云的 Redis 云数据库、腾讯云的 Redis 数据库等。用户无需关心底层的服务器硬件和运维管理,由云服务提供商负责服务器的部署、维护、监控和故障处理等工作。
- 优点:用户可以快速创建和部署 Redis 实例,无需进行复杂的安装和配置工作。云服务提供商通常提供了高可用性、自动备份、性能监控等一系列的增值服务,降低了用户的运维成本。可以根据业务需求灵活调整实例的规格和配置,实现资源的弹性伸缩。
- 缺点:用户对底层服务器的控制能力有限,无法进行一些底层的优化和定制化操作。费用相对较高,尤其是对于大规模、高性能的 Redis 实例,使用云托管模式的成本可能会比较高。
在实际应用中,需要根据业务的需求、数据量、并发量、预算等因素综合考虑,选择合适的 Redis 部署模式。同时,还需要注意数据的备份、安全防护等方面的问题,以确保 Redis 系统的稳定运行和数据的安全可靠。
面试题
redis实现分布式锁的原理
基本原理
Redis 分布式锁的核心思想是利用 Redis 的原子性操作,在多个客户端尝试获取锁时,只有一个客户端能够成功设置特定的键值对,这个客户端就被认为获取到了锁。其他客户端在锁被释放之前无法再次设置该键值对,从而实现了资源的互斥访问。
实现步骤与原理细节
- 获取锁
原子操作:客户端使用SET命令来尝试获取锁,SET命令需要带上特定的参数,格式为SET key value NX PX timeout。
key:作为锁的唯一标识,通常可以使用业务相关的名称,例如order🔒123表示订单 ID 为 123 的锁。
value:可以是一个唯一的随机字符串,用于在释放锁时进行验证,防止误释放其他客户端的锁。
NX:表示只有当键不存在时才进行设置操作,如果键已经存在则设置失败,这保证了同一时间只有一个客户端能够设置成功。
PX timeout:设置键的过期时间,单位为毫秒。这样做是为了防止客户端在获取锁后由于某些原因(如崩溃)未能正常释放锁,导致其他客户端永远无法获取该锁,造成死锁。
示例代码:
SET order:lock:123 "unique_value_123" NX PX 5000
若命令返回OK,则表示客户端成功获取到了锁;若返回nil,则表示锁已被其他客户端持有,当前客户端获取锁失败。
2. 执行业务逻辑
当客户端成功获取到锁后,就可以执行需要互斥访问的业务逻辑,例如对共享资源进行读写操作。
3. 释放锁
验证与删除:释放锁时,客户端需要先验证锁的value是否与自己设置的一致,以避免误释放其他客户端的锁。这可以通过执行 Lua 脚本来保证操作的原子性。
Lua 脚本示例:
if redis.call("GET", KEYS[1]) == ARGV[1] thenreturn redis.call("DEL", KEYS[1])
elsereturn 0
end
执行 Lua 脚本:在 Redis 客户端中可以使用EVAL命令来执行上述 Lua 脚本,示例如下:
EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 order:lock:123 "unique_value_123"
分布式锁的安全性和可靠性考虑
- 锁的过期时间:需要合理设置锁的过期时间,若过期时间过短,可能会导致业务逻辑还未执行完锁就已经过期,从而引发并发问题;若过期时间过长,可能会在客户端异常时导致其他客户端长时间无法获取锁。
- 锁的可重入性:在某些场景下,可能需要支持锁的可重入性,即同一个客户端在持有锁的情况下可以再次获取该锁,这可以通过在锁的value中记录客户端的标识和重入次数来实现。
- Redis 集群环境:在 Redis 集群环境中,需要考虑锁的一致性问题。可以使用 Redlock 算法来提高锁的可靠性,该算法通过在多个 Redis 节点上获取锁,只有当在大多数节点上都成功获取到锁时,才认为客户端获取锁成功。
redis缓存雪崩、缓存穿透、缓存击穿
Redis 缓存击穿、穿透和雪崩是在使用 Redis 作为缓存时常见的问题,下面将详细描述它们的概念、产生原因、带来的影响以及解决方案。
缓存击穿
- 概念:缓存击穿是指热点数据在缓存中过期的瞬间,大量并发请求同时访问该数据,这些请求会绕过缓存直接访问数据库,导致数据库压力瞬间增大。
- 产生原因:一般是由于某个热点数据的缓存过期时间设置不合理,或者在高并发场景下,缓存过期的瞬间有大量请求同时到达。
- 影响:可能会使数据库的负载瞬间过高,甚至可能导致数据库响应缓慢或崩溃,影响整个系统的性能和稳定性。
- 解决方案:
- 设置热点数据永不过期:对于一些经常被访问的热点数据,可以设置其在缓存中永不过期,但需要注意内存的使用情况,避免内存溢出。
- 使用互斥锁:在缓存过期时,只允许一个请求去查询数据库并更新缓存,其他请求则等待该请求完成后从缓存中获取数据。可以使用 Redis 的分布式锁来实现。
缓存穿透
- 概念:缓存穿透是指客户端请求的数据在缓存中不存在,并且在数据库中也不存在,导致请求直接穿透缓存访问数据库。如果有大量这样的请求,会使数据库压力增大。
- 产生原因:可能是由于恶意攻击,如黑客故意发送不存在的 key 进行大量请求;也可能是业务逻辑问题,导致某些数据在缓存和数据库中都没有被正确存储。
- 影响:大量请求直接访问数据库,可能导致数据库性能下降,甚至引发数据库故障,影响系统的正常运行。
- 解决方案:
- 布隆过滤器:在缓存之前使用布隆过滤器来判断数据是否存在。布隆过滤器是一种概率型数据结构,它可以快速判断一个元素是否在一个集合中,具有空间效率高、查询速度快的特点。当布隆过滤器判断数据不存在时,直接返回,不再访问数据库。
- 缓存空值:当查询数据库发现数据不存在时,也将空值缓存起来,并设置一个较短的过期时间,这样后续相同的请求就可以直接从缓存中获取空值,而不会再穿透到数据库。
缓存雪崩
- 概念:缓存雪崩是指在某一时刻,缓存中大量的数据同时过期,导致大量请求同时访问数据库,使数据库压力骤增,甚至可能导致数据库崩溃,进而影响整个系统的可用性。
- 产生原因:通常是因为缓存数据的过期时间设置过于集中,或者 Redis 服务器出现故障,导致缓存中的数据全部丢失。
- 影响:数据库可能会因为无法承受巨大的并发压力而崩溃,从而使整个系统无法正常提供服务,造成严重的业务影响。
- 解决方案:
- 分散过期时间:避免将大量缓存数据的过期时间设置为同一时刻,而是将过期时间分散在一个时间段内,例如可以在原来的过期时间基础上加上一个随机的时间偏移量,这样可以使缓存数据的过期时间更加均匀地分布,减少同时过期的可能性。
- 使用多级缓存:采用多级缓存架构,如本地缓存(如 Ehcache)和分布式缓存(如 Redis)相结合。当分布式缓存中的数据过期或不可用时,可以先从本地缓存中获取数据,减轻数据库的压力。
- 数据预热:在系统启动时,提前将一些热点数据加载到缓存中,避免在系统运行过程中突然出现大量数据同时过期的情况。
搭建 Redis 集群:通过搭建 Redis 集群来提高 Redis 的可用性和可靠性,避免因单个 Redis 节点故障导致缓存数据全部丢失。同时,可以使用 Redis 的持久化功能,如 RDB 和 AOF,在服务器重启后能够快速恢复数据。
redis 3种特殊数据类型
redis一致性hash
一致性哈希的基本原理是:把所有的哈希值空间组织成一个虚拟的圆环(哈希环),整个空间按照顺时针方向组织,其中0和最高位2^32-1是重叠的。
假设我们有四台机器要哈希环来实现映射(分布数据),我们先根据机器的名称或者ip计算哈希值,然后分布到环中,也就是图中红色标记。
现在有4条数据或者4个访问请求,对key计算后,得到哈希环中的位置(绿色位置)。沿哈希环顺时针找到的一个Node,就是数据存储的结点。
在这种情况下,新增一个Node5结点 ,只影响一部分数据的分布,如下:
如果我们删除一个结点Node4,只影响相邻的一个结点。
一致性哈希解决了动态增减结点时,所有数据都需要重新分布的问题,它只会影响到下一个相邻的结点,对其他结点没有影响。
但是这样的一致性哈希有一个缺点,因为结点不一定是均匀分布的,特别是在节点数比较少的情况下,所以数据不能得到均匀分布。解决这个问题的办法是引入虚拟结点
比如,2个节点,5条数据 ,只有1条分布到Node2,4条分布到Node1,不均匀。
Node1设置了两个虚拟结点,Node2也设置了两个虚拟结点(虚线圆圈),这时候有3条数据分不到Node1,1条分布到Node2。
一致性Hash在分布式系统中很多场景都有应用,例如负载均衡、分库分表等等,是一个非常重要的基础算法。
来自豆包
Redis 本身并不直接实现一致性哈希算法,但在 Redis 集群和分布式缓存系统中,一致性哈希算法有着重要的应用,下面为你详细介绍。
一致性哈希的基本概念
一致性哈希是一种特殊的哈希算法,在传统哈希算法里,若哈希空间大小改变(如增加或减少服务器节点),几乎所有的键都会被重新映射,这会带来大量的数据迁移。而一致性哈希算法能够尽可能减少键的重新映射,在节点数量变化时,只有一小部分键需要重新映射到新的节点,从而提高系统的可扩展性和稳定性。
一致性哈希的原理
- 构建哈希环
首先,确定一个哈希空间,通常是一个 2^32 大小的整数环(范围从 0 到 2**32 - 1)。
接着,使用哈希函数(如 MD5、SHA - 1 等)将服务器节点的标识(如 IP 地址、主机名)映射到这个哈希环上的某个点。 - 键的映射
同样运用哈希函数,把数据的键映射到哈希环上的某个点。
沿着哈希环顺时针查找,找到第一个遇到的服务器节点,就把该键存储到这个节点上。 - 节点的添加与删除
添加节点:当新增一个服务器节点时,只需将哈希环上从新增节点位置开始,顺时针方向到下一个节点之间的键重新映射到新节点,其他键的映射关系保持不变。
删除节点:若删除一个服务器节点,将该节点上的键重新映射到哈希环上顺时针方向的下一个节点,其余键的映射不受影响。
一致性哈希在 Redis 中的应用场景
- Redis 集群
在 Redis 集群环境中,需要把数据均匀地分布到多个节点上。一致性哈希算法可以让数据在节点数量变化时,只有少量数据需要迁移,减少了数据迁移的开销,保证了集群的稳定性和可用性。 - 分布式缓存
在分布式缓存系统里,使用一致性哈希算法能把缓存数据均匀地分布到多个缓存节点上。当某个缓存节点出现故障或需要添加新的缓存节点时,只有部分缓存数据需要重新分布,降低了缓存失效的范围。
一致性哈希的优缺点
优点
节点增减时数据迁移少:在服务器节点数量发生变化时,只有少数数据的映射关系需要调整,大大减少了数据迁移的工作量。
负载均衡:能够将数据均匀地分布到各个节点上,避免了部分节点负载过重的问题。
缺点
数据分布不均:若服务器节点在哈希环上分布不均匀,可能会导致部分节点负载过高,而部分节点负载过低。
虚拟节点开销:为了解决数据分布不均的问题,通常会引入虚拟节点,但这会增加额外的计算和存储开销。
简单的 Python 示例代码
import hashlibclass ConsistentHashing:def __init__(self, replicas=3):self.replicas = replicasself.ring = {}self.sorted_keys = []def _hash(self, key):return int(hashlib.md5(str(key).encode()).hexdigest(), 16)def add_node(self, node):for i in range(self.replicas):virtual_node = f"{node}-{i}"hash_value = self._hash(virtual_node)self.ring[hash_value] = nodeself.sorted_keys.append(hash_value)self.sorted_keys.sort()def remove_node(self, node):for i in range(self.replicas):virtual_node = f"{node}-{i}"hash_value = self._hash(virtual_node)del self.ring[hash_value]self.sorted_keys.remove(hash_value)def get_node(self, key):if not self.ring:return Nonehash_value = self._hash(key)for node_hash in self.sorted_keys:if hash_value <= node_hash:return self.ring[node_hash]return self.ring[self.sorted_keys[0]]# 使用示例
ch = ConsistentHashing()
ch.add_node("node1")
ch.add_node("node2")
ch.add_node("node3")key = "data_key"
node = ch.get_node(key)
print(f"Key {key} is mapped to node {node}")
这个示例实现了一个简单的一致性哈希类,包含添加节点、删除节点和查找键对应的节点等功能。
布隆过滤器
布隆过滤器 (Bloom Filter)是由 Burton Howard Bloom 于 1970 年提出,它是一种 space efficient 的概率型数据结构,用于判断一个元素是否在集合中。
当布隆过滤器说,某个数据存在时,这个数据可能不存在;当布隆过滤器说,某个数据不存在时,那么这个数据一定不存在。
哈希表也能用于判断元素是否在集合中,但是布隆过滤器只需要哈希表的 1/8 或 1/4 的空间复杂度就能完成同样的问题。
布隆过滤器可以插入元素,但不可以删除已有元素。
其中的元素越多,false positive rate(误报率)越大,但是 false negative (漏报)是不可能的。
布隆过滤器原理
BloomFilter 的算法是,首先分配一块内存空间做 bit 数组,数组的 bit 位初始值全部设为 0。
加入元素时,采用 k 个相互独立的 Hash 函数计算,然后将元素 Hash 映射的 K 个位置全部设置为 1。
检测 key 是否存在,仍然用这 k 个 Hash 函数计算出 k 个位置,如果位置全部为 1,则表明 key 存在,否则不存在。
如下图所示:
布隆过滤器原理
哈希函数会出现碰撞,所以布隆过滤器会存在误判。
这里的误判率是指,BloomFilter 判断某个 key 存在,但它实际不存在的概率,因为它存的是 key 的 Hash 值,而非 key 的值。
所以有概率存在这样的 key,它们内容不同,但多次 Hash 后的 Hash 值都相同。
对于 BloomFilter 判断不存在的 key ,则是 100% 不存在的,反证法,如果这个 key 存在,那它每次 Hash 后对应的 Hash 值位置肯定是 1,而不会是 0。布隆过滤器判断存在不一定真的存在。
为什么不允许删除元素呢?
删除意味着需要将对应的 k 个 bits 位置设置为 0,其中有可能是其他元素对应的位。
因此 remove 会引入 false negative,这是绝对不被允许的。
redlock
来自豆包
Redlock 是一种分布式锁的实现算法,由 Redis 官方提出,旨在解决在分布式环境中实现锁的可靠性和可用性问题。以下是关于 Redlock 的详细介绍:
工作原理
Redlock 算法基于多个独立的 Redis 节点来实现分布式锁。与传统的基于单个 Redis 节点的锁不同,它通过与多个节点进行交互,确保在大多数节点都成功获取锁的情况下,才认为锁获取成功。这样可以避免单点故障导致的锁失效问题,提高了锁的可靠性。
具体来说,当客户端尝试获取锁时,它会依次向多个 Redis 节点发送获取锁的请求。每个节点会独立地处理请求,如果节点成功地将锁设置(例如,通过设置一个具有特定过期时间的键),则会返回成功响应。客户端在收到一定数量(超过半数)的节点的成功响应后,才认为锁获取成功。如果客户端在规定的时间内没有收到足够数量的成功响应,或者在获取锁的过程中出现错误,它会认为锁获取失败,并自动释放已经在部分节点上获取到的锁。
优势
- 高可用性:由于依赖多个 Redis 节点,即使部分节点出现故障,只要大多数节点正常工作,锁仍然可以正常获取和释放,不会影响系统的正常运行。
- 容错性强:能够容忍一定数量的节点故障,只要剩余的正常节点数量足够多,就可以保证分布式锁的正确性和可靠性。
- 性能较好:在大多数情况下,Redlock 的性能表现良好。虽然需要与多个节点进行交互,但由于 Redis 本身的高性能,以及 Redlock 算法的优化,使得获取和释放锁的操作能够在较短的时间内完成。
应用场景
- 分布式系统中的资源竞争控制:在分布式系统中,多个节点可能同时访问共享资源,如数据库记录、文件等。Redlock 可以用于确保在同一时间只有一个节点能够访问这些资源,避免数据冲突和不一致性。
- 任务调度:在分布式任务调度系统中,保证同一任务在集群中只被一个节点执行。例如,定时任务、数据同步任务等,通过 Redlock 可以避免任务被重复执行,确保任务的准确性和一致性。
- 分布式缓存的更新控制:当多个应用节点共享一个分布式缓存时,可能会出现多个节点同时更新缓存的情况。使用 Redlock 可以保证在同一时间只有一个节点能够更新缓存,避免缓存数据的不一致性。
redis内存淘汰策略
- noeviction:不会驱逐任何key
- allkeys-lru:对所有key使用LRU算法进行删除
- volatile-lru:对所有设置了过期时间的key使用LRU算法进行删除
- allkeys-random:对所有key随机删除
- volatile-random:对所有设置了过期时间的key随机删除
- volatile-ttl:删除马上要过期的key
- allkeys-lfu:对所有key使用LFU算法进行删除
- volatile-lfy:对所有设置了过期时间的key使用LFU算法进行删除
一般采用第2种,LRU算法思想:删除最久的那些没有被使用到的key,注意强调的是时间上最久
LFU:删除使用次数最少得key,注意强调的是使用次数最少
redis键的删除策略
redis主从复制
主从复制的作用
- 读写分离:master写,slave读,提高服务器的读写负载能力
- 负载均衡:基于主从复制、读写分离,由slave分担master负载,通过多个从节点分担数据读取负载,提高服务器的并发量和吞吐量
- 故障恢复:mater出现问题时,由slave提供服务,实现快速的故障恢复
- 高可用基石:基于主从复制,构建哨兵与集群,实现Redis的高可用方案
Redis主从复制原理
- slave成功连接到maser后,会向master发送SYNC命令。
- mater收到SYNC命令后,执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令。
- mater会将BGSAVE命令生成的RDB文件发送给slave,slave接收并载入RDB文件,将自己的数据库状态更新至master执行BGSAVE命令时的数据库状态。
- mater将记录在缓冲区的所有写命令发送给从slave,slave执行这些写命令,将自己的数据库状态更新至master数据库当前所处的状态。
全量复制和增量复制
全量复制:slave在接受到master的RDB文件后,将其存盘并载入到内存中
增量复制:master将写命令发送给从slave执行,避免每次都是全量复制。比如同步完成后,master删除了key,slave没删除,这时主从不一致,master就需要将写操作发送给salve并执行。这就是命令传播。
redis IO多路复用
Redis 是一个高性能的键值对存储数据库,它单线程却能处理大量并发客户端连接,这主要得益于其使用的 I/O 多路复用技术。下面将详细介绍 Redis I/O 多路复用的相关内容。
基本概念
I/O 多路复用是一种让单个线程高效处理多个 I/O 流的技术。在传统的阻塞 I/O 模型中,一个线程在处理一个 I/O 操作时会被阻塞,直到该操作完成,这使得线程无法同时处理其他 I/O 操作。而 I/O 多路复用通过一个机制可以监视多个文件描述符(如套接字),当其中任何一个文件描述符就绪(有数据可读、可写或者发生异常)时,就会通知程序进行相应的处理。
Redis 中使用 I/O 多路复用的原因
Redis 是单线程的,为了处理大量的并发客户端连接,使用 I/O 多路复用技术可以让 Redis 在一个线程内高效地处理多个客户端的 I/O 操作,避免了为每个客户端连接创建一个线程带来的线程切换开销和内存消耗问题,从而提高了系统的性能和可扩展性。
Redis 支持的 I/O 多路复用模型
Redis 支持多种 I/O 多路复用模型,不同的操作系统支持的模型有所不同,主要包括以下几种:
- select:这是一种最早的 I/O 多路复用模型,几乎所有操作系统都支持。它通过一个fd_set数据结构来存储需要监视的文件描述符,每次调用select函数时,会将fd_set从用户空间复制到内核空间,在内核中遍历所有的文件描述符,检查是否有就绪的文件描述符。select的缺点是支持的文件描述符数量有限(通常为 1024),并且每次调用都需要进行用户空间和内核空间的复制操作,效率较低。
- poll:poll是select的改进版本,它使用一个pollfd数组来存储需要监视的文件描述符,解决了select中文件描述符数量有限的问题。与select类似,poll也需要进行用户空间和内核空间的复制操作,但由于使用了数组,在处理大量文件描述符时性能相对较好。
- epoll:epoll是 Linux 系统特有的 I/O 多路复用模型,它使用事件驱动的方式,通过epoll_ctl函数注册需要监视的文件描述符和事件类型,当有事件发生时,内核会主动通知程序。epoll避免了select和poll中每次调用都需要遍历所有文件描述符的问题,性能更高,尤其在处理大量并发连接时表现出色。
- kqueue:kqueue是 FreeBSD 系统特有的 I/O 多路复用模型,类似于epoll,它也是基于事件驱动的方式,性能较高。
Redis 选择 I/O 多路复用模型的策略
Redis 在启动时会根据当前操作系统的支持情况,自动选择最合适的 I/O 多路复用模型。一般来说,会优先选择性能最高的模型,如在 Linux 系统上会优先选择epoll,在 FreeBSD 系统上会优先选择kqueue。
示例代码理解(伪代码)
下面是一个简单的伪代码示例,展示了 Redis 如何使用 I/O 多路复用处理客户端连接:
# 初始化 I/O 多路复用器,这里以 epoll 为例
epoll = epoll_create()# 创建监听套接字并绑定地址
listen_socket = socket_create()
socket_bind(listen_socket, address)
socket_listen(listen_socket)# 注册监听套接字到 epoll 中,监听读事件
epoll_ctl(epoll, EPOLL_CTL_ADD, listen_socket, EPOLLIN)while True:# 等待事件发生events = epoll_wait(epoll)for event in events:if event.fd == listen_socket:# 有新的客户端连接client_socket = socket_accept(listen_socket)# 注册客户端套接字到 epoll 中,监听读事件epoll_ctl(epoll, EPOLL_CTL_ADD, client_socket, EPOLLIN)else:# 客户端套接字有数据可读data = socket_read(event.fd)if data:# 处理客户端请求response = process_request(data)socket_write(event.fd, response)else:# 客户端关闭连接epoll_ctl(epoll, EPOLL_CTL_DEL, event.fd, 0)socket_close(event.fd)
这个伪代码示例展示了 Redis 使用epoll进行 I/O 多路复用的基本流程:
初始化epoll实例。
创建监听套接字并绑定地址,将监听套接字注册到epoll中,监听读事件。
进入一个无限循环,调用epoll_wait等待事件发生。
当有事件发生时,根据事件类型进行相应的处理:如果是监听套接字有事件,表示有新的客户端连接,接受连接并将客户端套接字注册到epoll中;如果是客户端套接字有事件,表示有数据可读,读取数据并处理请求,然后将响应发送给客户端;如果客户端关闭连接,从epoll中删除该套接字并关闭。
通过这种方式,Redis 可以在一个线程内高效地处理多个客户端的 I/O 操作。
IO多路复用与异步IO的关系
I/O 多路复用和异步是两个不同但又有一定关联的概念,它们在处理 I/O 操作时有着不同的作用和特点。以下是它们的关系分析:
区别
概念定义
- I/O 多路复用:是一种同步 I/O 模型,通过一个线程监视多个文件描述符,当其中有文件描述符就绪时,通知应用程序进行处理。应用程序在收到通知后,需要主动进行读写操作,操作过程中线程仍然可能会阻塞。例如在使用select、poll或epoll等 I/O 多路复用机制时,线程会阻塞在select、poll或epoll_wait函数上等待事件发生,当有事件发生后,线程需要自行处理 I/O 操作。
- 异步:是一种更高级的 I/O 模型,在异步 I/O 中,应用程序发起 I/O 操作后,无需等待操作完成,而是继续执行其他任务。当 I/O 操作完成后,系统会通过回调函数或信号等方式通知应用程序。在整个过程中,应用程序不需要主动去查询 I/O 操作的状态,也不会因为 I/O 操作而阻塞线程。
数据处理流程
- I/O 多路复用:应用程序需要不断地轮询或等待 I/O 多路复用机制返回的就绪事件,然后根据事件类型来处理相应的 I/O 操作。例如,在使用epoll的情况下,应用程序通过epoll_wait获取就绪的文件描述符列表,然后对每个就绪的文件描述符进行数据读取或写入操作。
- 异步:应用程序发起 I/O 请求后,继续执行其他代码。当 I/O 操作完成时,系统会自动调用事先注册的回调函数来处理数据。例如,在使用异步 I/O 库时,应用程序可以在发起文件读取请求后,继续执行其他任务,当文件读取完成后,系统会调用回调函数将读取的数据传递给应用程序。
联系
- 配合使用:在实际应用中,I/O 多路复用常常与异步操作配合使用,以提高系统的性能和响应能力。例如,在一些高性能的网络服务器中,会使用 I/O 多路复用机制来监听多个客户端连接的事件,当有数据可读事件发生时,通过异步的方式将数据读取到内存中,然后再进行处理。这样可以避免在读取数据时阻塞线程,提高线程的利用率。
- 都用于提高 I/O 效率:I/O 多路复用和异步的目的都是为了更高效地处理 I/O 操作,减少线程阻塞的时间,提高系统的并发性和吞吐量。它们都可以让一个线程同时处理多个 I/O 流,避免了为每个 I/O 操作创建一个线程所带来的开销。
参考链接
- Redis详解
- 2025全网最硬核Redis面试题(大厂必备)
- Redis数据结构之Zset
- 【征服redis14】认真理解一致性Hash与Redis的三种集群
- 硬核 | Redis 布隆(Bloom Filter)过滤器原理与实战