Redis入门
Redis 数据类型和数据结构
常见的数据类型
String类型
- 底层数据结构:SDS
List类型
- 底层数据结构:双向链表或者压缩列表
- 当列表元素小于512个,列表的每个元素的值都小于64字节,Redis会使用压缩列表
- 否则Redis会使用双向链表
- 在Redis 3.2之后,List的底层数据结构由
quickList实现
- 底层数据结构:双向链表或者压缩列表
Hash类型
- 底层数据结构:压缩列表或者哈希表实现
- 如果哈希类型元素小于512个且所有值小于64字节,Redis会使用压缩列表作为Hash类型的底层数据结构
- 否则使用哈希表
- 在Redis 7.0之后,由
listpack数据结构实现
- 底层数据结构:压缩列表或者哈希表实现
Set类型
- 底层数据结构:哈希表或者整数集合
- 如果集合中的元素都是整数且个数小于512,Redis会使用整数集合作为Set类型的底层数据结构
- 否则,使用哈希表作为Set类型的底层数据结构
- 底层数据结构:哈希表或者整数集合
ZSet类型
- 底层数据结构:压缩列表或者跳表
- 如果有序集合的元素个数小于128个,并且每个元素的值小于64字节,Redis会使用压缩列表作为ZSet类型的底层数据结构
- 否则使用跳表
- 在Redis 7.0中,压缩列表的数据结构已经废弃,使用
listpack数据结构实现
- 底层数据结构:压缩列表或者跳表
Redis数据结构(8种)
SDS
- 结构设计:
Len:记录字符串长度Alloc:记录分配的空间长度Flags:记录SDS的类型Buf[]:存放数据的字节数组
- 相比于原本C的字符串的好处:
- 可以以O(1)的时间复杂度获取字符串的长度,直接返回
Len - 二进制安全,可以存储
\0 - 不会发生缓冲区溢出,会自动扩容
- 扩容机制:
- 如果所需的SDS长度小于1MB,扩容策略是翻倍
- 如果所需的SDS长度超过1MB,扩容策略是所需SDS长度+1MB
- 好处:
- 可以有效减少内存的分配次数
- 扩容机制:
- 节省内存空间
- 使用不同的SDS类型:数据结构中的
Len和Alloc变量的数据类型不同,可以灵活地保存不同大小的字符串,从而节省内存空间 - 使用编译优化:要求编译器在编译过程中取消优化对齐,按照实际占用的字节数进行对齐
- 使用不同的SDS类型:数据结构中的
- 可以以O(1)的时间复杂度获取字符串的长度,直接返回
- 结构设计:
双向链表
- 结构设计:
head头节点- 节点设计:
prev前置节点next后置节点value节点值
- 节点设计:
tail尾节点dup节点负责函数free节点释放函数match节点比较函数len节点数量
- 优点:
- 获取前置节点和后置节点的时间复杂度为O(1),前后置节点都可以指向NULL,所以无环
- 获取头节点和尾节点的时间复杂度是O(1)
- 获取节点数量的时间复杂度是O(1)
- 可以保持不同类型的值
- 缺点:
- 每个节点的内存并不连续,所以无法很好地利用CPU缓存
- 每个节点要存储额外的信息,内存开销大
- 结构设计:
压缩列表
- 设计思想:一种内存紧凑型的数据结构,占用一块连续的内存空间,便于利用CPU缓存,根据不同长度的数据,进行相应的编码
- 缺陷:
- 不能保存过多的元素,否则查询效率会降低
- 新增或者修改某个元素的时候,压缩列表占用的内存空间需要重新分配,可能引发连锁更新的问题
- 结构设计:
- 表头:
zlbytes:记录整个压缩列表占用内存字节数zltail: 记录尾部节点距离起始地址多少节点,也就是列表尾的偏移量zllen:记录压缩列表包含的节点数量
- 表尾:
zlend:记录压缩列表的结束点,固定值0xFF
- 节点:
Prevlen:记录前一个节点的长度,便于实现从后向前遍历- 如果前一个节点的长度小于254字节,需要1字节的空间
- 如果前一个节点的长度大于等于254字节,需要5字节的空间
encoding:记录当前节点实际数据的类型(字符串和整数)和长度- 如果节点数据为整数,那么
encoding需要1字节的空间 - 如果是字符串,根据字符串的大小,使用1字节/2字节/5字节空间进行编码
- 前两个bit表示数据的类型,后续表示数据的实际长度
- 如果节点数据为整数,那么
data:记录当前节点的实际数据
- 表头:
- 优点:
- 查询第一个节点,最后一个节点,节点数的实际复杂度是O(1),查找其他元素的时间复杂度是O(N),因此压缩列表不适合保存过多的元素
- 缺陷:
- 连锁更新:
- 更新一个节点导致
Prevlen发生变化,进而导致后续所有节点的Prevlen发生变化
- 更新一个节点导致
- 连锁更新:
哈希表
- 结构设计:
dictEntry table:哈希表数组key键值对中的键v键值对中的值next:指向下一个节点,形成链表
size:哈希表大小sizemask:哈希表大小的掩码,用于计算索引值used:哈希表已有的节点数量
- 哈希冲突的解决:
- 使用链式哈希,被分配到同一个哈希桶的多个节点,使用单向链表连接起来
- Rehash:如果一个桶的数据过多,就需要Rehash
- 步骤:
- 给哈希表2分配空间,一般比哈希表1大一倍
- 将哈希表1的数据迁移到哈希表2中
- 迁移完成后,将哈希表2设置为哈希表1,新创建一个哈希表2,为下次rehash做准备
- 渐进式Rehash:
- 在Rehash期间,每次对哈希表进行新增、删除、查找、更新操作时,除了执行对应的操作之外,还会按照顺序将哈希表1上索引位置所有的key-value迁移到哈希表2上
- Rehash的触发条件:
- 负载因子:
- 计算:哈希表已保存节点数量 / 哈希表的大小
- 当负载因子大于等于1,并且Redis没有执行bgsave或者bgrewriteaof,也就是没有执行RDB快照或者进行AOF重写的时候,就会进行Rehash操作
- 当负载因子大于5的时候,无论有没有执行RDB快照或者AOF重写,都会强制进行Rehash操作
- 负载因子:
- 步骤:
- 结构设计:
整数集合
- 结构设计:
encoding:编码方式length:元素数量contents:保存元素的数组,元素的类型取决于encoding的值
- 整数集合的升级操作:
- 当新元素的类型比现在所有元素的类型都要长时,整数集合要先按照新元素的类型扩展
contents数组的大小,再将新元素加入到整数集合 - 好处:节省内存资源
- 当新元素的类型比现在所有元素的类型都要长时,整数集合要先按照新元素的类型扩展
- 结构设计:
跳表
- 结构设计:
header,tail:跳表的头尾节点ele对象的元素值score元素的权重值backward后向指针level[]:- 保存每一层的前向指针和跨度
length:跳表的长度level:跳表的最大层数
- 查询过程:
- 从头节点的最高点开始,逐一遍历每一层
- 如果当前节点的权重小于要查找的权重,跳表就会访问该层的下一个节点
- 如果当前节点的权重等于要查找的权重,并且当前节点的SDS类型数据小于要查找的数据,跳表就会访问该层的下一个节点
- 如果以上条件不满足,或者下一个节点为空,跳表会使用当前遍历节点
level数组里的下一层指针,沿着下一层指针继续查找,相当于跳到了下一层继续查找
- 从头节点的最高点开始,逐一遍历每一层
- 跳表相邻两层的节点数量最理想的比例是2:1,查找复杂度可以降低到O(logN)
- 跳表在创建节点的时候,会生成范围(0 - 1)的一个随机数,如果这个随机数小于0.25,层数就增加一层,然后继续生成下一个随机数,直到结果大于0.25
- 层高的限制是64,在创建头节点的时候,就会直接创建64层高的头节点
- 为什么要使用跳表而不使用平衡树:
- 从内存占用来说,跳表每个节点只有1.33个指针,调整参数后,跳表比平衡树更灵活一些
- 在做范围查找的时候,跳表比平衡树操作要简单
- 从算法实现难度上来说,跳表比平衡树要简单
- 结构设计:
Quicklist
Quicklist取代了List的双向链表或者压缩列表的底层数据结构- 本质上是 双向链表 + 压缩列表
- 结构设计:
head,tail:quicklist链表头prev,next:前置,后置节点zl,节点对应的压缩列表- 压缩列表的字节大小
- 压缩列表的元素个数
count压缩列表元素总数lenquicklist节点总数
Listpack
- 目的:用于替换压缩列表,解决连锁更新的问题
- 结构设计:
- 头部:
listpack总字节数listpack元素数量
- 节点:
encoding:元素的编码类型,会对不同长度的整数和字符串进行编码data,数据存放len:encoding+data的总长度
- 尾部:
- 结尾标识
- 头部:
listpack没有压缩列表中记录前一个节点长度的字段了,listpack只记录了当前节点的长度,当我们向listpack加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题- 如何向前遍历:
- 从当前列表项起始位置的指针开始,向左逐个字节解析,得到前一项的
entry-len值
- 从当前列表项起始位置的指针开始,向左逐个字节解析,得到前一项的
Redis 线程模型
Redis并非单线程:
- 接收客户端请求 -> 解析请求 -> 进行数据的读写操作 -> 发送数据给客户端这个过程是由一个线程,主线程来完成的
- 在2.6版本,会启动2个后台线程,分别用来处理关闭文件、AOF刷盘
- 在4.0之后,新增了一个后台线程,用来异步释放Redis内存,也就是
lazyfree线程
Redis单线程模式是怎么样的:
- 使用了
epoll,IO多路复用来监听连接事件,读事件,写事件,在监听到事件后,调用对应的事件处理函数
为什么Redis是单线程,但还是这么快
- Redis大部分操作都在内存中完成,并且采用了高效的数据结构
- Redis采用单线程模型避免了多线程之间的竞争
- Redis采用了I/O多路复用机制来处理大量的客户端的Socket请求
为什么Redis要使用单线程
- CPU并不是制约Redis性能表现的瓶颈所在
- 引入多线程,会增加系统复杂度,同时可能存在线程切换、加锁解锁、死锁造成的性能损耗
为什么Redis6.0之后引入了多线程
- 在Redis6.0之后,采用了多个I/O线程来处理网络请求,因为随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络I/O的处理上
Redis 持久化
Redis如何实现数据不丢失
- AOF日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里
- RDB快照:将某一时刻的内存数据,以二进制的方式写入磁盘
- 混合持久化方式:Redis 4.0新增的方式,集合了AOF和RDB的优点
AOF日志实现方式:
- Redis在执行完一条命令后,会把命令以追加的方式,写入到一个文件里,然后Redis重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复
- 为什么先执行命令,再把数据写入日志呢?
- 好处:
- 避免额外的检查开销
- 不会阻塞当前写操作命令的执行
- 弊端:
- 数据可能会丢失
- 可能会阻塞其他操作
- 好处:
- 写入日志的实现过程:
- 命令追加到
server.aof_buf缓冲区(用户态) - I/O系统调用(
write())切换到内核态 - 写入到内核缓冲区
- 由内核发起写操作,写入磁盘
- 什么时候写入磁盘,由操作系统内核决定
- Redis.conf提供了3种写入磁盘的参数:
Always:每次写操作完毕后,同步将AOF日志数据写回磁盘EverySec:每次写操作命令执行完后,先将命令写入到AOF文件的内核缓冲区,然后每隔1秒将缓冲区的内容写回磁盘No:不由Redis控制写回磁盘的时机,由操作系统控制
- 命令追加到
- AOF日志过大,触发重写机制:
- 重写机制会读取所有key最新的value,然后用一条命令记录到新的AOF文件,之前的一个命令就没有必要记录了,重写完成后,会将新的AOF文件覆盖现在的AOF文件,这相当于压缩了AOF文件,使得AOF文件的体积变小了
- 重写的过程:
- 启动一个重写子进程,共享物理内存
- 子进程对内存只读,重写子进程读取内存所有键值对,逐一转换成命令,再将命令写到新的日志
- 重写过程中,主进程依然可以正常处理命令,但主进程修改已经存在的key-value会导致写时复制,此时这个key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了
- 为了解决这种数据不一致问题,Redis设置了一个AOF重写缓冲区
- 重写AOF期间,当Redis执行完一个写命令之后,它会同时将这个写命令写入到「AOF缓冲区」和「AOF重写缓冲区」
- 当子进程完成AOF重写工作会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。
- 调用一个信号处理函数,该函数主要做以下工作:
- 将AOF重写缓冲区中的所有内容追加到新的AOF的文件中,使得新旧两个AOF文件所保存的数据库状态一致
- 新的AOF的文件进行改名,覆盖现有的AOF文件。
RDB快照
- RDB快照是以二进制的形式直接保存Redis数据区域的所有数据
- 如果使用
SAVE则会阻塞主线程 - 如果使用
BGSAVE则不会
- 如果使用
- 在执行RDB的期间,数据可以修改吗?
- 可以,RDB会使用一种
Copy-On-Write技术,在主线程修改数据的时候,会产生一个原数据的备份,RDB快照会使用这个备份进行,也就是保证RDB快照保存的是启动时候的数据
- 可以,RDB会使用一种
混合持久化
- 在持久化时,fork出的子进程,会首先将共享内存区域的内存数据以RDB快照的方式写入AOF文件,期间主线程的操作命令会被放入AOF重写缓冲区,重写缓冲区的增量命令会以AOF格式写入到AOF文件,之后会使用含有RDB和AOF的AOF文件替换原文件
- 好处:
- 由于前部分是RDB,所以数据加载恢复会很快
- 加载完RDB内容后,才会加载AOF的内容,减少了大量数据的丢失风险
- 缺点:
- 由于AOF文件添加了RDB的内容,让AOF的文件可读性变得很差
- 兼容性差,如果开启了混合持久化,那么混合持久化的AOF文件是不能用在Redis 4.0之前的版本的
Redis 集群-主从复制
Redis的高可用,要从Redis的多服务节点来考虑,比如Redis的主从复制,哨兵模式,切片集群等。
当 Redis 的 key 分散在多个机器上,某些机器存有大量的热点 key,导致访问请求不均衡时,可以通过以下几种方法来均衡访问请求:
1. 哈希槽(Hash Slot)均衡
Redis 集群模式使用一致性哈希将 key 映射到不同的节点上,但某些热点 key 可能集中在一个节点。可以通过以下策略来均衡:
- 预分配哈希槽:手动调整集群的哈希槽分配,使得负载较高的节点占用更多的哈希槽,分散负载。
- 再平衡节点:Redis 集群支持在线调整哈希槽,可以根据热点 key 分布和流量情况动态调整节点的哈希槽分配,平衡负载。
2. 缓存分片(Sharding)
通过将数据进行水平分片,均衡地将不同的 key 存储到多个节点上。每个 key 根据某种规则(如一致性哈希或范围分片)被分配到不同的 Redis 实例。这样可以分散热点 key 的请求。
- 一致性哈希分片:在一致性哈希环上增加虚拟节点,可以帮助更细致地分散 key,避免部分节点负载过高。
- 平衡数据分布:在现有的分片机制上进行再分配,将热点 key 迁移到负载较低的节点上。
3. 使用代理层(如 Twemproxy、Codis)
通过使用代理层来对 key 的访问进行均衡管理。代理层可以智能地将请求路由到不同的 Redis 节点,减少对某个节点的集中访问压力。例如:
- Twemproxy:可以作为 Redis 访问的中间层,通过哈希算法将请求分发到不同的 Redis 实例。
- Codis:支持分布式 Redis 集群,帮助实现动态扩容和均衡访问。
4. 热点 key 削峰(Hot Key Mitigation)
对于一些热点 key,可以通过一些削峰手段来减少对某个 Redis 实例的压力:
- 缓存副本:为热点 key 设置多个副本,将读取流量分发到不同的 Redis 实例中,减少对单个节点的压力。
- 多级缓存:在 Redis 之前增加一级本地缓存,或使用更高级别的缓存系统(如 CDN),减少对 Redis 的直接访问。
- 请求合并:对于多个访问同一个热点 key 的请求,合并为一个请求,减少 Redis 的压力。可以通过消息队列或异步队列实现。
5. 批量请求
将多个小请求合并成一个批量请求,减少对 Redis 节点的请求频率,缓解热点问题。例如,利用 Redis 的
MGET或MSET来批量处理多个 key。6. 分布式锁和限流
对于高频写操作,可以使用分布式锁控制对热点 key 的并发写入,或对请求进行限流,避免某个 key 被过度访问。
一致性哈希(Consistent Hashing) 是一种用于分布式系统中的负载均衡算法,广泛应用于分布式缓存、数据库和分布式存储等场景。它通过将数据和节点映射到哈希环上,从而在数据节点的增加或减少时尽可能减少数据的重新分布,以提高系统的扩展性和负载均衡能力。
一致性哈希的核心思想
一致性哈希的基本思想是将所有的节点和数据映射到一个哈希环(Hash Ring)上,当系统中的节点发生变化(比如增加或减少节点)时,只需要重新分配少部分的数据,而不是对所有数据进行重新映射。
哈希环
- 一致性哈希的环通常被表示为一个 0 到 2³² - 1 的整数范围,首尾相接形成一个圆环。系统中的每个节点(服务器、缓存、数据库等)通过哈希函数映射到环上的某个位置。
- 同时,数据的 key 也会通过相同的哈希函数映射到哈希环上。
数据分配
- 数据的 key 会按照顺时针方向存储在第一个大于或等于该 key 哈希值的节点上。如果环中没有更大的节点,则数据存储在第一个节点上。
- 当一个节点失效或新节点加入时,只需要重新分配该节点的前一个节点或后一个节点之间的数据,其他数据不需要重新分配。
一致性哈希的优点
- 减小节点变化时的数据迁移量:与传统哈希算法(如简单取模)不同,增加或删除节点时,一致性哈希只影响少部分节点上的数据,而不是所有节点上的数据。例如,当节点数量从
n增加到n+1时,取模算法需要重新分配几乎所有数据,而一致性哈希只需重新分配少部分数据。- 负载均衡性:如果节点均匀分布在哈希环上,一致性哈希能够使数据在各个节点之间尽可能平衡地分布。
- 动态扩展性:一致性哈希的扩展性非常好,支持节点的动态增加和减少,适用于节点频繁变动的分布式系统。
虚拟节点(Virtual Nodes)
由于一致性哈希在节点不均匀分布时可能导致负载不均衡,通常使用“虚拟节点”来进一步优化负载均衡性。
- 虚拟节点的概念:每个物理节点可以对应多个虚拟节点,这些虚拟节点通过哈希函数映射到环上的不同位置,实际上是对物理节点的多重表示。
- 优势:虚拟节点通过增加节点的分布密度,使得数据的分布更加均匀,避免某些物理节点承受过多的数据请求。
使用场景
一致性哈希主要用于分布式系统中的负载均衡和数据分布场景,常见的使用场景包括:
- 分布式缓存:如 Memcached、Redis 集群中,保证缓存节点的均匀分布和动态扩展。
- 分布式存储:如 Cassandra、Amazon DynamoDB,保证数据分布在不同的存储节点中。
- 负载均衡:在高可用的服务架构中,均衡请求在不同的服务实例之间的分布
主从复制:
全量复制
- 一般采用一主多从的模式,主节点负责读写操作,从节点只负责读操作,同时主节点会将写操作同步到从节点
- 主节点和从节点的写操作同步是异步进行的,难免会出现数据不一致的情况,是无法保证强一致性的。
- 过程:
- 第一阶段:建立链接,协调同步
- 从服务器会执行
replicaof命令和主服务器建立主从关系,接着从服务器会向主服务器发送psync命令,表示要进行数据同步,主服务器收到psync后,会以FULLRESYNC响应回应对方,表示采用的是全量复制的方式,也就是主服务器会把所有的数据都同步给从服务器
- 从服务器会执行
- 第二阶段:主服务器同步数据给从服务器
- 主服务器执行
BGSAVE命令来生成RDB文件,然后把文件发送给从服务器,从服务器收到RDB文件后,会清空数据库,然后载入RDB文件 - 在生成RDB文件后执行的命令,无法写入到生成的RDB文件中,这就导致了主从不一致
- 为了保证主从一致性,主服务器会将三个时间间隙中收到的写操作命令,写入
replication buffer中:- 主服务器生成RDB文件期间
- 主服务器发送RDB文件给从服务器期间
- 从服务器加载RDB文件期间
- 主服务器执行
- 第三阶段:主服务器发送新的写操作命令给从服务器
- 在从服务器同步RDB文件完成后,会向主服务器发送确认消息,接着主服务器会将
replication buffer中的所有命令发送给从服务器,这时,主从服务器的数据就一致了
- 在从服务器同步RDB文件完成后,会向主服务器发送确认消息,接着主服务器会将
- 第一阶段:建立链接,协调同步
命令传播
- 主服务器在完成第一次同步后,双方会维护一个TCP链接,后续主服务器可以通过这个链接将写操作命令传播给从服务器
增量复制:
- 如果在网络链接断开又恢复后,Redis主从节点之间会采用增量复制
- 过程:
- 在命令传播的过程中,主服务器的命令会写入到
repl-backlog-buffer中,这是一个环形缓冲区,用于主从服务器断开后,从中找到差异数据,主服务器使用master_repl_offset记录写的位置,从服务器使用slave_repl_offset记录读的位置 - 从服务器连上主服务器后,从服务器会将自己的复制偏移量
slave_repl_offset发送给主服务器,主服务器根据自己的偏移量和从服务器偏移量的差值,判断要执行的操作:- 如果从服务器要读取的数据还在缓冲区中,就采取增量同步
- 如果不在缓冲区中,就采取全量同步
- 为了减少全量同步,可以将
repl-backlog-buffer设置的大一些
- 在命令传播的过程中,主服务器的命令会写入到
如何判断Redis某个节点是否正常工作?
- 通过基本的Ping-Pong心跳检测机制:
- 主节点每10秒发送一次
ping对从节点,判断从节点的存活性和连接状态 - 从节点每隔1秒发送一次
replconf ack,给主节点上报自己的复制偏移量- 实时检测主从节点网络状态
- 上报自身复制偏移量,检查复制数据是否丢失,如果数据丢失,在从主节点缓冲区中拉取数据
- 主节点每10秒发送一次
主从架构中,过期Key如何处理?
- 主节点会模拟一个
DEL命令,发送给从节点
Redis是同步复制还是异步复制?
- Redis每次收到写命令后,先写到内部缓冲区,然后异步发送给从节点
两个Buffer (replication buffer 和 repl-backup-buffer) 有什么区别?
- 出现阶段:
Replication buffer在全量复制和增量复制都会出现,主节点会给每个连接的从节点都分配一个replication bufferRepl-backup-buffer只在增量复制节点出现,一个主节点只分配一个
- 当缓冲区满后,发送的事情不一样:
- 当
repl-backup-buffer满了,会直接覆盖起始数据位置 - 当
replication buffer满了,会导致连接断开,删除缓存,从节点重新连接,重新开始全量复制
- 当
如何应对主从数据不一致?
- 尽量保证主从节点状态良好
- 使用一个外部程序来监控主从节点的复制进度:
- 监控主节点
offset和从节点offset的差值,如果差值超出了我们的阈值,让从节点不再对外提供数据读取服务
- 监控主节点
主从切换如何减少数据丢失?
- 异步复制同步丢失:
- 对于服务端,配置一个
min-slaves-max-lag,一旦所有的从节点数据复制和同步的延迟都超过了min-salves-max-lag的值,主节点就拒绝接收请求 - 对于客户端,当发现主节点不可写后,采取降级措施,将数据暂时写入本地缓存和磁盘中或者写入
kafka等消息队列中,等主节点恢复正常,再将数据重新写入主节点
- 对于服务端,配置一个
- 集群产生脑裂数据丢失:
- 出现原因:当出现网络问题,哨兵节点认为主节点挂了,就会选举出一个新的主节点,但如果原来的主节点又重新连接,但因为选举了新的主节点,新的主节点就会发送数据的清空和全量同步,那么在主节点失联这个过程中的数据就会全部丢失
- 解决方案:
- 要求主节点必须有
x个从节点连接 - 要求主从复制同步的延迟不能超过
x秒 - 这样,如果主节点假掉线,因为无法保证和其他从节点连接和同步,就无法再接收新的消息,主从切换后,只有新的主节点能处理读请求,即便原主节点数据被清空,也不会导致数据丢失
- 要求主节点必须有
如何做到故障自动切换?
- 使用哨兵模式
Redis 过期删除和内存淘汰
Redis存储设置了过期事件的key的时候,会将key存入到一个过期字典中,Redis首先会检查key是否存在于过期字典中,如果不在就正常读取,否则判断key是否过期。
Redis使用的过期删除策略是 惰性删除 + 定期删除。
惰性删除
- 不主动删除过期键,每次从数据库访问key时,都检测key是否过期,如果过期则删除该key
- 优点:每次访问时,才会检查key是否过期,该策略对CPU时间友好
- 缺点:如果一个过期key一直没被访问,就会一直占用内存,导致浪费
定期删除
- 每隔一段时间「随机」从数据库中取出一定数量的key进行检查,并删除其中的过期key,如果过期key的数量占比超过选取key的数量的25%,就继续选取,否则停止,等待下一轮
- 优点 :通过限制删除执行的时长和频率,减少删除操作对CPU的影响,同时也能减少内存的占用
- 缺点:难以确定删除操作执行的时长和频率,频率高,对CPU不好,频率低,就和惰性删除类似
Redis持久化时,对过期键如何处理?
- RDB
- RDB文件生成阶段,过期的键不会被保存到新的RDB文件中
- RDB加载阶段:
- 主服务器:主服务器在加载RDB文件的时候,会对其中的键是否过期进行检查,如果过期,则不会被载入到内存中
- 从服务器:从服务器在加载RDB文件的时候,无论键是否过期,都会全部加载到内存中,不过由于从服务器在主从同步的过程中一般会清空数据库,所以几乎没有影响
- AOF
- AOF写入阶段:
- 如果某个键还没过期,AOF会将该键值对写入AOF文件,当过期键值被删除后,AOF会向AOF文件追加一条
DEL命令来显式的删除该键值对
- 如果某个键还没过期,AOF会将该键值对写入AOF文件,当过期键值被删除后,AOF会向AOF文件追加一条
- AOF重写阶段:
- 已过期的键不会被写入到新的AOF文件中
- AOF写入阶段:
Redis主从模式中,对过期键会如何处理?
- 从库不会进行过期扫描,从库在查询过期数据的时候,会直接返回,从库的过期依靠主库在过期时对AOF日志文件写入的追加
DEL命令同步
Redis的内存淘汰策略有哪些(内存占用满后)?
- 不进行数据淘汰策略,而是直接不再提供服务
- 进行数据淘汰策略
- 在设置了过期时间的数据中淘汰:
- 随机淘汰设置了过期策略的值
- 优先淘汰更早过期的值
- 淘汰设置了过期时间的键值中,最久未使用的键值
- 淘汰设置了过期时间的键值对中,最少使用的键值
- 在全部数据中淘汰:
- 随机淘汰键值对
- 淘汰整个键值对中最久未使用的键值
- 淘汰整个键值对中最少使用的键值
- 在设置了过期时间的数据中淘汰:
Redis是如何实现LRU算法的?
- 在Redis对象结构体中添加一个额外的字段
lru,记录该数据的最后一次访问时间- 24bit用于记录访问时间
- 进行内存淘汰时,随机取一部分值,淘汰最久未使用的那个
- 问题:无法解决缓存污染问题
Redis是如何实现LFU算法的?
- 可以解决缓存污染问题
- 在Redis对象的结构体中,有一个字段
lru记录key的访问频次- 前16bit记录访问时间,后8bit记录访问频次
Redis 缓存设计
如何避免缓存雪崩,缓存穿透,缓存击穿
缓存雪崩
- 定义:当同一时间有大量key同时过期,而这时又有大量的用户请求,导致Redis无法完全处理,请求完全打在数据库上,导致数据库无法处理
- 解决方法:
- 将缓存失效的时间打散
- 设置缓存不过期,而是手动更新缓存
缓存击穿
- 定义:某一时间,有一个key过期了,但同时针对这个key有大量的请求访问,而无法读取缓存,直接访问数据库,导致数据库宕机
- 解决方法:
- 使用互斥锁,Redis中使用
SETNX设置一个状态位,表示这是一种锁定状态,保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放,要么返回空值或者默认值 - 不给热点数据设置过期时间,由后台异步更新缓存
- 在热点数据要过期前,提前通知后台进程,更新缓存以及重新设置过期时间
- 使用互斥锁,Redis中使用
缓存穿透
- 定义:当用户访问的数据既不在缓存中,也不在数据库中,导致请求无法构建缓存,当大量这种请求到来时,数据库压力骤增
- 解决方法:
- 限制非法的请求,验证请求的参数是否合理,避免恶意请求进一步访问缓存和数据库
- 设置空值或者默认值,当发现缓存穿透现象,针对要查询的数据,在缓存中设置一个空值或者默认值
- 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在
- 在写入数据的时候,用布隆过滤器做个标记,在缓存失效后,可以通过查询布隆过滤器来快速判断数据是否存在
如何设计一个缓存策略,可以动态缓存热点数据?
- 思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据。只留下经常访问的数据
- 实现:可以使用
ZADD和ZRANGE来完成排序和获取数据的操作
常见的缓存更新策略
- Cache Aside(旁路缓存策略) (实际使用)
- 写策略:
- 先更新数据库的值,再删除缓存中的数据
- 顺序不能颠倒,否则会出现数据库和缓存的数据不一致问题:
- 先删除缓存,读策略时客户端无法读取到缓存,查询数据库,回写缓存,之后写策略更新数据库,导致数据库缓存不一致
- 顺序不能颠倒,否则会出现数据库和缓存的数据不一致问题:
- 先更新数据库的值,再删除缓存中的数据
- 读策略:
- 缓存未命中,客户端查询数据库,回写缓存
- 缓存命中,直接返回给用户
- 实际上先修改数据库,再删除缓存同样会出现数据不一致的情况:
- 请求未命中,查询数据库,写回缓存之前,用户修改数据库,导致数据库和写回缓存的数据不一致
- 但出现概率不高,因为缓存的写入要远远快于数据库的写入
- 写策略:
- Read/Write Through (读穿/写穿策略)
- Write Back (写回策略)
Redis 实战
Redis如何实现延迟队列
- 在Redis中可以使用ZSet的方式来实现延迟消息队列,使用
Score来存储延迟执行的时间 - 使用
ZADD一直向内存中生产消息,再利用ZRANGEBYSCORE查询符合条件的所有待处理任务,通过循环执行队列任务即可
Redis的大key如何处理
- 大key定义:指的是Value很大
- String类型的值大于10KB
- Hash、List、Set、ZSet的元素的个数超过5000个
- 大Key带来的影响:
- 客户端超时阻塞,Redis执行命令是单线程,操作大Key的时候会比较耗时。就会阻塞Redis
- 引发网络阻塞。每次获取大Key产生的网络流量较大
- 阻塞工作线程。使用
DEL删除大Key的时候,会阻塞工作线程,导致无法执行后续命令 - 内存分布不均。集群在分片均匀的情况下,会出现数据和查询倾斜的情况。部分有大Key的Redis节点占用内存多,QPS也比较大
如何查找大Key
- 使用
redis-cli --bigkeys- 注意事项:
- 最好在从节点上使用,否则会阻塞主节点,或者在业务压力的低峰阶段进行扫描查询,
- 不足:
- 只能返回每种类型中最大的Key,无法得到大小排在前N位的
bigkey - 对于集合来说,只能统计元素的个数大小,而不是实际占用的内存量
- 只能返回每种类型中最大的Key,无法得到大小排在前N位的
- 注意事项:
- 使用
SCAN命令- 使用
SCAN扫描,然后使用TYPE获取数据类型- 如果是String,使用
STRLEN获取的字符串长度就是内存占用的字节数 - 如果是集合:
- 预先在业务层获取,集合元素的平均大小,使用
LLEN,HLEN,SCARD,ZCARD获取集合的元素个数 - 在Redis 4.0以上,使用
MEMORY USAGE查询一个键值对占用的内存空间
- 预先在业务层获取,集合元素的平均大小,使用
- 如果是String,使用
- 使用
- 使用RDBTools查询大Key
如何删除大key
- 要点:本质上是内存释放的过程,内存释放后,会将空闲内存写入到空闲内存链表中,如果一次性释放太多内存,操作空闲内存链表的时间会增加,会导致Redis阻塞
- 方法:
- 分批次删除:
- 对于大Hash,使用
HSCAN命令,每次获取100个,再使用HDEL每次删除1个 - 对于大List,使用
LTRIM每次删除一部分 - 对于大Set,使用
SSCAN获取100个,再用SREM每次删除1个 - 对于大ZSet,使用
ZREMRANGEBYRANK,每次删除Top 100的元素
- 对于大Hash,使用
- 异步删除:
- 在Redis 4.0之后,使用
UNLINK命令调用异步线程删除
- 在Redis 4.0之后,使用
- 分批次删除:
Redis管道有什么用
- 管道是Redis提供的批处理技术,用于解决多个命令执行时的网络等待
Redis支持事务回滚吗?
- 使用命令开启:
MULTI提交:EXEC抛弃:DISCARD - Redis事务不一定保证原子性,提交的命令即便出错,正确的依然会执行
Redis实现分布式锁
- 分布式锁是分布式环境下的一种并发控制手段,用于控制某个资源在同一时刻只能被同一应用使用
- 实现方式:
- 加锁:
- 使用
SET lock_key unique_value(不同应用的唯一标识) NX(保证操作原子性) PX/EX(设置过期时间,避免异常导致锁无法释放)
- 使用
- 解锁:
- 要保证解锁的是上锁的应用
- 使用Lua脚本保证解锁操作的原子性
- 加锁:
- 优点:
- 性能高效
- 实现方便
- 避免单点故障
- 缺点:
- 超时时间不好设置
- 可以写一个守护线程,在锁快到期的时候,对锁进行续约,当执行完成后,销毁锁
- 主从模式下,数据的复制是异步的,这导致了分布式锁的不可靠性
- 如果主节点设置好锁后,宕机了,没有及时同步,那么新的主节点依然可以获取锁,导致多个应用服务同时获取到锁
- 超时时间不好设置
Redis 如何解决集群情况下分布式锁的可靠性?
- 使用
RedLock,本质上是基于多个Redis节点的分布式锁, - 思路:客户端和多个独立的Redis节点依次请求申请加锁,如果客户端可以和半数以上的节点成功完成加锁,则认为加锁成功,否则失败





