## redis过期删除策略 Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。 在说 Redis 过期删除策略之前,先跟大家介绍下,常见的三种过期删除策略: ### 定时删除 在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作** 定时删除策略的**优点**: - 可以保证过期 key 会被尽快删除,也就是内存可以被尽快地释放。因此,定时删除对内存是最友好的。 定时删除策略的**缺点**: - 在过期 key 比较多的情况下,删除过期 key 可能会占用相当一部分 CPU 时间,在内存不紧张但 CPU 时间紧张的情况下,将 CPU 时间用于删除和当前任务无关的过期键上,无疑会对服务器的响应时间和吞吐量造成影响。所以,定时删除策略对 CPU 不友好。 ### 惰性删除 不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。 惰性删除策略的**优点**: - 因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。 惰性删除策略的**缺点**: - 如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。 ### 定期删除: 每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。 定期删除策略的**优点**: - 通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。 定期删除策略的**缺点**: - 内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。 - 难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。 **Redis 选择「惰性删除+定期删除」这两种策略配和使用** ## redis内存淘汰策略 *1、不进行数据淘汰的策略* **noeviction**(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,则会触发 OOM,但是如果没用数据写入的话,只是单纯的查询或者删除操作的话,还是可以正常工作。 *2、进行数据淘汰的策略* 针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。 在设置了过期时间的数据中进行淘汰: - **volatile-random**:随机淘汰设置了过期时间的任意键值; - **volatile-ttl**:优先淘汰更早过期的键值。 - **volatile-lru**(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值; - **volatile-lfu**(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值 ## Redis 持久化 Redis 共有三种数据持久化的方式: - **AOF 日志**:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里; - **RDB 快照**:将某一时刻的内存数据,以二进制的方式写入磁盘; - **混合持久化方式**:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点; ### AOF ![img](assets/98987d9417b2bab43087f45fc959d32a-20230309232253633.png) > AOF 日志过大,会触发什么机制? Redis 为了避免 AOF 文件越写越大,提供了 **AOF 重写机制**,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。 AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。 Redis 的**重写 AOF 过程是由后台子进程 \*bgrewriteaof\* 来完成的**,bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作:(**写时复制(\*Copy On Write\*)**) - 执行客户端发来的命令; - 将执行后的写命令追加到 「AOF 缓冲区」; - 将执行后的写命令追加到 「AOF 重写缓冲区」; ### RDB RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据, Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行: - 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,**会阻塞主线程**; - 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以**避免主线程的阻塞**; Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令,默认会提供以下配置: ```c save 900 1 save 300 10 save 60 10000 ``` 执行 bgsave 过程中,Redis 依然**可以继续处理操作命令**的。关键的技术就在于**写时复制技术** 如果主线程(父进程)要修改共享数据里的某一块数据(比如键值对 A)时,就会发生写时复制,于是这块数据的物理内存就会被复制一份(键值对 A'),然后主线程在这个数据副本(键值对 A')进行修改操作。与此同时,bgsave 子进程可以继续把原来的数据(键值对 A)写入到 RDB 文件。 ### 为什么会有混合持久化? Redis 4.0 提出了**混合使用 AOF 日志和内存快照** AOF 文件的**前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据**。 ![img](assets/f67379b60d151262753fec3b817b8617-20230309232312657.png) ## Redis安全控制 ### 缓存穿透--没有key 产生的背景: 缓存穿透是指使用不存在的key进行大量的高并发查询,导致缓存无法命中,每次请求都要都要穿透到后端数据库查询(同时也查不到),使得数据库的压力非常大,甚至导致数据库服务压死;**既不在缓存中,也不在数据库中** 解决方案: 1. 接口层实现api限流、用户授权、id检查等; 2. 从缓存和数据库都取不到数据的话,一样将数据库空值放入缓存中,设置30s有效期避免使用同一个id对数据库攻击压力大; 3. 布隆过滤器 ### 缓存击穿--单个key 产生背景: 在高并发的情况下,当一个缓存key过期时,因为访问该key请求较大,多个请求同时发现缓存过期,因此对多个请求同时数据库查询、同时向Redis写入缓存数据,这样会导致数据库的压力非常大; 解决方案: 1. 使用分布式锁 保证在分布式情况下,使用分布式锁保证对于每个key同时只允许只有一个线程查询到后端服务,其他没有获取到锁的权限,只需要等待即可;这种高并发压力直接转移到分布式锁上,对分布式锁的压力非常大。 2. 使用本地锁 使用本地锁与分布式锁机制一样,只不过分布式锁适应于服务集群、本地锁仅限于单个服务使用。 3. 软过过期 设置热点数据永不过期或者异步延长过期时间; 4. 布隆过滤器 ### 缓存雪崩--多个key 缓存雪崩指缓存服务器重启或者大量的缓存集中在某个时间段失效,突然给数据库产生了巨大的压力,甚至击垮数据库的情况。 解决思路:对不用的数据使用不同的失效时间,加上随机数 ## 数据库和缓存如何保证一致性? ### 先更新数据库,再更新缓存 ![图片](assets/8febac10b14bed16cb96d1d944cd08da.png) **出现了缓存和数据库中的数据不一致的现象**。 ### 先更新缓存,再更新数据库 ![图片](assets/454a8228a6549176ad7e0484fba3c92b.png) **出现了缓存和数据库中的数据不一致的现象**。 ### 先删除缓存,再更新数据库 ![图片](assets/cc208c2931b4e889d1a58cb655537767.png) **先删除缓存,再更新数据库,在「读 + 写」并发的时候,还是会出现缓存和数据库的数据不一致的问题**。 ### 先更新数据库,再删除缓存 ![图片](assets/1cc7401143e79383ead96582ac11b615.png) ## 说说常见的缓存更新策略? ### Cache Aside(旁路缓存)策略 **写策略的步骤:** - 先更新数据库中的数据,再删除缓存中的数据。 **读策略的步骤:** - 如果读取的数据命中了缓存,则直接返回数据; - 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。 ### Read/Write Through(读穿 / 写穿)策略 ***1、Read Through 策略*** 先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。 ***2、Write Through 策略*** 当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在: - 如果缓存中数据已经存在,则更新缓存中的数据,并且由**缓存组件同步**更新到数据库中,然后缓存组件告知应用程序更新完成。 - 如果缓存中数据不存在,直接更新数据库,然后返回; ### Write Back(写回)策略; Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量**异步更新**的方式进行。 ## 布隆过滤器 ### 什么是布隆过滤器? 布隆过滤器它实际上是一个很长的二进制向量和一系列随机映射函数。以Redis中的布隆过滤器实现为例,Redis中的布隆过滤器底层是**一个大型位数组(二进制数组)+多个无偏hash函数。** **一个大型位数组(二进制数组)**: ![位数组.png](assets/e94e504adc5a75a2d7f562dc44166511.png) **多个无偏hash函数:** 无偏hash函数就是能把元素的hash值计算的比较均匀的hash函数,能使得计算后的元素下标比较均匀的映射到位数组中。 如下就是一个简单的布隆过滤器示意图,其中k1、k2代表增加的元素,a、b、c即为无偏hash函数,最下层则为二进制数组。 ![布隆过滤器.png](assets/9ebde5c11ad69447314c216acf188fc8.png) ### 增删查改 * 增加元素 往布隆过滤器增加元素,添加的key需要根据k个无偏hash函数计算得到多个hash值,然后对数组长度进行取模得到数组下标的位置,然后将对应数组下标的位置的值置为1 - 通过k个无偏hash函数计算得到k个hash值 - 依次取模数组长度,得到数组索引 - 将计算得到的数组索引下标位置数据修改为1 例如,key = Liziba,无偏hash函数的个数k=3,分别为hash1、hash2、hash3。三个hash函数计算后得到三个数组下标值,并将其值修改为1. 如图所示: ![增加元素.png](assets/a3e7d217ecb825e94bdc577a467eb29d.png) * 查询元素 布隆过滤器最大的用处就在于判断某样东西一定不存在或者可能存在,而这个就是查询元素的结果。其查询元素的过程如下: - 通过k个无偏hash函数计算得到k个hash值 - 依次取模数组长度,得到数组索引 - 判断索引处的值是否全部为1,如果全部为1则存在(这种存在可能是误判),如果存在一个0则必定不存在 关于误判,其实非常好理解,hash函数在怎么好,也无法完全避免hash冲突,也就是说可能会存在多个元素计算的hash值是相同的,那么它们取模数组长度后的到的数组索引也是相同的,这就是误判的原因。例如李子捌和李子柒的hash值取模后得到的数组索引都是1,但其实这里只有李子捌,如果此时判断李子柒在不在这里,误判就出现啦!因此布隆过滤器最大的缺点误判只要知道其判断元素是否存在的原理就很容易明白了! * 删除元素 布隆过滤器对元素的删除不太支持,目前有一些变形的特定布隆过滤器支持元素的删除!关于为什么对删除不太支持,其实也非常好理解,hash冲突必然存在,删除肯定是很苦难的! * 修改元素 无 ## Redis为什么快 - 内存存储:Redis是一种基于内存的数据存储系统,所有数据都存储在内存中,这使得它可以快速地读取和写入数据,因为内存访问速度比磁盘快得多。 - 精简的数据结构:Redis支持多种数据结构,如字符串、列表、哈希表、集合和有序集合等。这些数据结构经过精心设计,具有高效的操作复杂度,使得Redis在执行常见的数据操作时非常快速。 - 单线程模型:Redis采用单线程模型来处理所有客户端请求。这意味着在任何给定时间点,Redis只会执行一个操作,避免了多线程之间的竞争条件和锁操作,从而减少了上下文切换的开销。单线程模型使得Redis非常高效,并且能够在现代计算机系统中充分利用CPU缓存。 - 非阻塞I/O:Redis使用了非阻塞的I/O多路复用机制,通常使用epoll或kqueue等,这使得它能够同时处理多个客户端请求而不会导致线程阻塞。这样一来,在I/O等待的过程中,Redis可以继续处理其他请求,提高了系统的吞吐量。 ## Redis 线程模型 ### Redis 是单线程吗? **Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的**,这也是我们常说 Redis 是单线程的原因。 但是,**Redis 程序并不是单线程的**,Redis 在启动的时候,是会**启动后台线程**(BIO)的: - **Redis 在 2.6 版本**,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务; - **Redis 在 4.0 版本之后**,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。 ![img](assets/后台线程.jpg) 关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列: - BIO_CLOSE_FILE,关闭文件任务队列:当队列有任务后,后台线程会调用 close(fd) ,将文件关闭; - BIO_AOF_FSYNC,AOF刷盘任务队列:当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘, - BIO_LAZY_FREE,lazy free 任务队列:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象; ### Redis 单线程模式是怎样的? ![img](assets/redis单线程模型.drawio.png) 图中的蓝色部分是一个事件循环,是由主线程负责的,可以看到网络 I/O 和命令处理都是单线程。 Redis 初始化的时候,会做下面这几件事情: - 首先,调用 epoll_create() 创建一个 epoll 对象和调用 socket() 创建一个服务端 socket - 然后,调用 bind() 绑定端口和调用 listen() 监听该 socket; - 然后,将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。 初始化完后,主线程就进入到一个**事件循环函数**,主要会做以下事情: - 首先,先调用**处理发送队列函数**,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。 - 接着,调用 epoll_wait 函数等待事件的到来: - 如果是**连接事件**到来,则会调用**连接事件处理函数**,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数; - 如果是**读事件**到来,则会调用**读事件处理函数**,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送; - 如果是**写事件**到来,则会调用**写事件处理函数**,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。 ### Redis 采用单线程为什么还这么快? 之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因: - Redis 的大部分操作**都在内存中完成**,并且采用了高效的数据结构,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案了; - Redis 采用单线程模型可以**避免了多线程之间的竞争**,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。 - Redis 采用了 **I/O 多路复用机制**处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听 Socket 和已连接 Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。 ### Redis 6.0 之前为什么使用单线程? 使用了单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,**增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗**。 ### Redis 6.0 之后为什么引入了多线程? 随着硬件性能提升,Redis 的性能瓶颈可能出现网络 IO 的读写,也就是:**单个线程处理网络读写的速度跟不上底层网络硬件的速度**。 **Redis 多 IO 线程模型只用来处理网络读写请求,对于 Redis 的读写命令,依然是单线程处理**。 ![图片来源:后端研究所](assets/c55029a32a6448d9aa35bb3de6efcecatplv-k3u1fbpfcp-zoom-in-crop-mark4536000.awebp) ![Redis多线程与IO线程](assets/f109a45fe4904726b11fd36422f4abactplv-k3u1fbpfcp-zoom-in-crop-mark4536000.awebp) **主要流程**: 1. 主线程负责接收建立连接请求,获取 `socket` 放入全局等待读处理队列; 2. 主线程通过轮询将可读 `socket` 分配给 IO 线程; 3. 主线程阻塞等待 IO 线程读取 `socket` 完成; 4. 主线程执行 IO 线程读取和解析出来的 Redis 请求命令; 5. 主线程阻塞等待 IO 线程将指令执行结果回写回 `socket`完毕; 6. 主线程清空全局队列,等待客户端后续的请求。 思路:**将主线程 IO 读写任务拆分出来给一组独立的线程处理,使得多个 socket 读写可以并行化,但是 Redis 命令还是主线程串行执行。** ## Redis 大 Key 对持久化有什么影响? 当 AOF 写回策略配置了 Always 策略,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。 AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 `fork()` 函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程): - 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长; - 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。 大 key 除了会影响持久化之外,还会有以下的影响。 - 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。 - 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。 - 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。 - 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。 ### 如何删除大KEY - 分批次删除 每次分批删除,每次删除100个 - 异步删除(Redis 4.0版本以上) 从 Redis 4.0 版本开始,可以采用**异步删除**法,**用 unlink 命令代替 del 来删除**。这样 Redis 会将这个 key 放入到一个异步线程中进行删除,这样不会阻塞主线程。