Redis
前言
本文章是我学习过程中,不断总结而成,篇幅较长,可以根据选段阅读。
全篇17000
字,图片 十三
张,预计用时1
小时。
认识Redis
什么是Redis?
要使用一门技术,首先要知道这门技术是什么?其次就是他的作用是什么?如何实现这种作用的?
我们先来看看官方是如何介绍Redis的:图中数据来自Redis官网。
翻译:
Redis 是一个开源(BSD 许可)的内存中数据结构存储,用作数据库、缓存、消息代理和流引擎。Redis 提供数据结构,例如字符串、散列、列表、集合、带有范围查询的排序集、位图、超日志、地理空间索引和流。Redis 具有内置复制、 Lua 脚本编写、 LRU 驱逐、事务处理和不同级别的磁盘持久性,并通过 Redis Sentinel
提供高可用性和使用 Redis Cluster
自动分区。
您可以对这些类型运行原子操作,比如附加到字符串; 增加散列中的值; 将元素推入列表; 计算集的交集、联合和差异; 或者获得排序集中排名最高的成员。
简单来说
简单来说Redis就是一个将数据存储在内存中的数据库。因为对数据的操作都是在内存中完成的,因此读写速度非常快。常用于缓存、消息队列、分布式锁等场景。
Redis提供了多种数据结构用于支持不同业务场景。比如:字符串(string)、哈希(hash)、列表(list)、无序集合(set)、有序集合(zset)。还有一些特殊的数据类型,比如:位图(bitmaps)、基数统计(HyperLogLog)、地理信息(GEO)、流(Stream)。
除了这些之外,Redis还支持事务、发布/订阅模式、持久化、lua脚本、多种集群方式(主从复制、哨兵等)、内存淘汰、过期删除机制。
Redis和Memcached有什么区别?
听说过 Redis
的人大概率也听说过 Memcached
。那么作为可以在内存中存储数据的数据库,为什么不使用 Memcached
,而选择 Redis
呢?
Redis和Memcached的相同点:
- 他们都是基于内存进行数据存储的数据库,一般都用来当作缓存使用。
- 他们都有过期删除策略。
- 两者的性能都很高
Redis和Memcached的不同点:
- Redis支持的数据类型非常多样化,像字符串、哈希、列表、集合等数据结构。而Memcached只支持一个K-V键值对。
- Redis支持持久化,因此它可以将数据进行永久的存储在磁盘中,而非内存。Memcached一旦遇到特殊情况,数据将会全部丢失。
- Redis原生支持集群,Memcached只能通过客户端来实现向集群中分片写入数据。
- Redis支持发布/订阅模式、lua脚本、事务等功能,而Memcached不支持。
为什么Redis可以作为Mysql的缓存来使用?
Mysql是一种关系型数据库,而Redis是一种缓存型数据库。Redis要想作为Mysql的缓存数据库来使用,第一个需要保证的点就是速度。缓存速度一定要远大于实际存储速度,否则也没什么提升。
Redis支持事务,持久化,并且Redis在单机测试下的QPS轻松可以破10W,但是Mysql在单机测试下最高性能为1W QPS。因此Redis能够保证在高并发下也能正常工作。
总结:Redis用于高性能和高并发,能够完美的作为Mysql的缓存来使用。
Redis数据结构
Redis 数据类型以及使用场景分别是什么?
Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
随着 Redis 版本的更新,后面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。 Redis 五种数据类型的应用场景:
- String 类型的应用场景:签到打卡、缓存对象、常规计数、分布式锁、共享 session 信息等。
- List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
- Hash 类型:缓存对象、购物车等。
- Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
- Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
Redis 后续版本又支持四种数据类型,它们的应用场景如下:
- BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
- HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
- GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
- Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
五种常见的 Redis 数据类型是怎么实现?
String [字符串]
String 字符串的底层数据结构是SDS(简单动态字符串)。包含一个len字段,表示字符串长度。还有一个buf底层数组,用来动态的存储字符串。为什么要采用SDS呢?下面来比对一下SDS和C语言字符串:
- SDS不仅可以保存文本信息,还可以保存二进制数据。
- SDS获取字符串长度的复杂度是O(1)。它拥有一个维护字符串长度的len字段。
- Redis的SDS是安全的,不会出现拼接字符串导致溢出的问题。因为buf数组是可以动态进行扩容的,只需要在追加字符串的时候去判断一下是否会溢出,来选择申请新的内存空间来扩容即可。
List [列表]
List 列表在 Redis3.2 版本之前是由一个双向链表或者压缩列表组成的。具体选择什么数据结构根据存储元素个数是否超过512个元素或者存储字长是否超过64字节来决定。
在 Redis3.2 版本之后统一被替换为 quicklist 。
Hash [哈希/散列]
Hash 的底层是由压缩列表或者哈希表实现的。
如果存储元素个数是否超过512个元素或者存储字长是否超过64字节就使用哈希表,否则使用压缩列表。
在 Redis7.0 版本之后压缩列表被废弃,改为 listpack 数据结构来实现。
Set [集合]
Set 集合的底层是由整数集合或哈希表实现的。
如果存储的元素为整型且元素个数小于512就采用整数集合,否则采用哈希表实现。
Zset [有序集合]
Zset 有序集合底层采用的数据结构为压缩列表或跳表实现的。
如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构。否则使用跳表作为底层数据结构。
在 Redis7.0 版本以后压缩列表已经被废弃,改用 listpack 数据结构来实现。
什么是跳表?
跳表这个数据结构应该很多人都听说过,奈何很多书本上都没有讲到这个数据结构。这里我来简单介绍一下这个数据结构。
如果想要了解更多可以在参考中找到详细资料。
跳表是一种和红黑树一样,在查找、增加、删除操作上的时间复杂度都是 O(log n)。Redis用它作为 Zset 数据结构的底层实现。
跳表先知
这是一个普通的链表,每一个结点的尾结点指向下一个结点。
在普通链表中,对于查找 5 这个元素,我们需要进行 5 次判断操作。很显然他的查找复杂度为O(n)。
看图思考
那么我们突然有个神奇的发现。这个链表中的数据他是递增的,有序的。那对于有序的数组来说,一般都会采用二分查找的办法去将他的时间复杂度优化为O(log n)。那么链表进行二分查找都需要什么条件呢?
- 要知道整个链表中的元素个数。
- 要能够快速的跳到指定地点进行判断。
很显然,了解整个链表中元素个数很容易,但能够随意的跳到指定的地点进行判断是无法通过很小的代价实现的。
模拟操作
那么我们就换种做法,走另一条路实现二分查找。我们让这个普通链表变得不再普通,给他加上一层索引。
如果我们要找到 5 这个元素,下面是查找步骤:
- 找到二级索引中的第一个元素,发现 1 比 5 小,向右走。
- 找到第二个元素 3,判断之后继续向右走。
- 找到第三个元素 7 发现小于这个元素,退回去第二个元素走下边。
- 找到一级索引的元素 3。向右遍历,找到元素5。
只需要进行四次查找就能找到目标值。这里因为数据量小,可能效果不是很明显。但是我们已经知道添加索引,可以缩短我们查找的效率。使其呈现二分的效果。
如果数据量大的情况下,我们可以继续增加索引。假如图中的数据都扩大十倍,元素总数为 80。那么查找 50 这个元素,按照之前的查找逻辑,就只需要 22 次查找。由此可以看出,当元素数量较多时,索引提高的效率比较大,近似于二分查找。
那么是不是索引越多越好呢?
空间复杂度
根据上文可以得知模拟的结果大概是一个二分查找,因此他的时间复杂度平均应该为O(log n)。
空间复杂度也会随着索引层级的提升而成 log 级提升。假设第一层八个元素,那么第二层基本就是n/2个元素,第三层就是n/4个元素。也是log n级别的提升。
因此索引层级并不是越多越好,选择合适的数据进行索引的构建,不仅能大幅提升效率,还能节约大量的空间。
维护索引
因为跳表的插入和删除都是以查找为先驱条件,因此时间复杂度也自然就是O(logn)。但是随着插入和删除的操作的进行,索引必然会出现失效的问题。因此就需要时常去维护索引的有效性。具体的实现逻辑可以自行去看Redis源码,这里就不阐述了。
参考资料
一文彻底搞懂跳表的各种时间复杂度、适用场景以及实现原理 - 掘金 (juejin.cn)
Redis线程
Redis是不是单线程?
网上到处都在流传着Redis单线程的传说,因为一个能达到 10W QPS的数据库,你能想象到他仅仅依靠单线程就能够完成?
其实,Redis是单线程,也不完全是单线程。
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
其实Redis在启动的时候,会启动后台线程(BIO)的。后台线程主要处理一些对响应数据不会造成影响的操作。比如**关闭文件,AOF日志刷盘、释放内存(lazy free)**这三个操作。
Redis处理流程
这点推荐去看小林大佬的图解,我这里只作简单赘述
Redis采用多路复用的技术,能够同时监听并且建立多个C-S连接。仅仅靠一个连接一个连接的处理方式Redis就不可能达到10W QPS。
Redis会在初始化时创建一个epoll
对象,并通过socket函数
创建一个服务端 socket。在创建完毕后会去绑定监听端口号,然后注册等待监听的服务。
在初始化完毕之后,主线程会进入到一个事件循环函数。首先会调用处理发送队列函数,看一下发送队列中是否为空,如果不为空就通过 write
函数将缓冲区中的数据发送出去,如果数据没有发送完,那么就注册一个事件处理函数,等待 epoll_wait
发现可以继续发送的时候再去发送数据。
紧接着,调用epoll_wait
函数轮询请求。
-
如果是连接请求出现,则会调用连接事件处理函数。该函数会调用accept函数接收到socket中的请求,然后将这个请求注册到读事件函数中去。等待请求发送信息,然后进行读取。
-
如果是读请求出现,则会调用读请求处理函数。该函数会对接收到的命令进行解析,判断是否非法,然后处理命令。接着将客户端对象添加到发送队列中去,并注册写事件函数等待数据的发送。
-
如果是写请求出现,则会调用写请求处理函数。该函数会将客户端缓存区中的数据进行发送,如果没有发送完就继续注册写事件函数等待下一轮继续发送。
Redis速度快的秘密
虽然Redis采用单线程(网络I/O和执行命令)进行操作,但是这也恰恰是他快速的原因之一:
- Redis采用单线程操作,节省了多线程操作中上下文切换的资源开销和时间浪费,同时也避免了死锁情况的发生。
- Redis采用多路复用技术处理大量的连接请求,避免了一个连接一个连接处理的尴尬场景。
- Redis大部分操作都在内存中完成,内存的读写速率可是要比储存快上几十倍的。并且Redis采用了效率极高的数据结构。
Redis持久化
什么是持久化?
学过Mysql的同学应该知道Mysql可以依赖于redo log
日志来实现持久化的。
所谓持久化就是事务一旦提交,会造成不可逆转的后果,无论发生什么事情都不会改变其结果。
那么Redis这种基于内存进行操作的数据库,是依靠于什么实现的持久化呢?
Redis如何保证持久化?
因为Redis的大部分操作都是在内存中进行的,因为Redis的性能才会如此之好。但是一旦出现断电故障,所有的操作将会挥之一炬。为了避免这一情况的发生,Redis会将数据持久化到磁盘中去,这样下次启动Redis的时候,数据也会随之出现。
Redis实现持久化的方法有三种:
- 通过
AOF
日志文件实现持久化。Redis会将每次执行完的操作追加到AOF
日志中。 - 通过
RDB
快照实现持久化。将某一时刻的内存数据,以二进制的形式写入磁盘中去。 - 混合持久化。在Redis4.0之后实现,利用
AOF
和RDB
的优点完成。
AOF日志是如何实现持久化的
Redis在执行完一条命令之后,就会将这条命令追加到AOF日志中去。如果遇到断电事故出现的时候,只需要重新启动Redis。Redis在启动之前会对AOF进行检测,看看是否有没有执行的任务,如果有就去执行一遍。
AOF日志命令格式
以下是一条set语句追加到aof日志中的格式:
*3
$3
set
$4
name
$8
zhangsan
这里来专门解释一下这条语句的含义:
set name zhangsan
*3 表示接下来将会有三个部分完成这条指令。每个部分都是由 [$+数字] 组成的,并且这个数字表示接下来输入的字节数。
为什么先执行命令,然后才追加AOF日志?
这个问题很简单,可以从两方面回答:
- 避免错误检查:如果先追加 AOF 日志,而命令是非法的(错误的)亦或者命令没有执行成功,那么这个AOF日志中就会出现错误命令。
- 不会阻塞当前命令:因为 Redis 执行命令是单线程的,所以无论是追加 AOF 日志,还是执行命令,都有一个操作会被阻塞。但是先执行命令后追加日志就能保证当前的命令不会被阻塞。
安全隐患
先执行命令可能会导致数据不一致的问题出现:比如在执行完命令的同时出现断电故障,命令来不及书写到 AOF 日志中去,因此也就造成了数据丢失的风险。
虽然先执行命令不会阻塞当前执行的命令,但如果紧接着还有一条命令等待执行,就会阻塞下一条命令的执行。
如果是pipeline(管道)命令,AOF是如何追加的?
当我在看到每当执行完一条命令后,AOF 就会追加这条命令到日志中去。我就突然想到,如果是一个管道命令呢?到底是一条一条的执行,一条一条的追加日志。还是等全部命令执行完毕后再全部一起追加呢?
根据 chatgpt 给出的答案可以看出来。Pipeline 管道命令全部执行完毕之后,统一的进行日志的追加工作。这是为了保证数据的一致性。如果中间有一条错误,或者造成回滚操作,那么不至于日志中出现大量的脏数据。
但是这样也会有一个问题,假如一个 Pipeline 有大量请求,耗时很久完成,还没有来得及写入到 AOF 日志文件中,就遇到了断电故障。并且大量的Pipeline也会造成堵塞的问题出现,一旦事务回滚会造成资源损失。因此一个 Pipeline 中的请求命令条数一般不要超过 100/200 条。
AOF日志的写回策略
AOF日志写入流程:
- 执行完一条命令,将这条命令追加到 server.aof_buf 缓存中。
- 等待系统的 write I/O 调用,从用户态切换到内核态,将缓存进行拷贝至 page cache 中。
- 等待内核发起写操作到硬盘中去。
Redis提供了三种写回硬盘的策略,并且这三种策略是可以通过配置 Redis.conf 来改变的。
- Always:总是,意味着无论什么情况,只要执行完一条语句,直接将语句写入到硬盘中去。
- Everysec:每秒,意味着每秒进行一次。每隔一秒就将缓存区中的数据写回到磁盘中。
- No:从不,意味着从不写回到磁盘。
这三种回写测率各有优劣
写回策略 | 写回时机 | 优点 | 缺点 |
---|---|---|---|
Always | 同步写回 | 可靠性高、最大程度保证数不丢失 | 每个写命令都要写回硬盘,性能开销大 |
Everysec | 每秒写回 | 性能适中 | 宕机时会丢失1秒内的数据 |
No | 由操作系统控制写回 | 性能好 | 宕机时会丢失大量的数据 |
当AOF日志量过大时会发生什么?
当 AOF 日志量过大时,Redis 重启后的I/O开销会很大。因此 Redis 设置了一个阈值,一旦达到了这个阈值就会开启 AOF 重写机制。那么如何执行重写机制呢?
当Redis检测到AOF日志需要进行重写时,Redis会打开一个新的 AOF 文件。将当前数据库中所有的数据转为命令,并且记录在新的AOF日志中去。命令记录完毕后将新的AOF日志替换掉旧的AOF日志,这样就完成了AOF日志压缩。
那么如果在压缩过程中,数据发生了改变会怎么样呢?
要知道AOF日志重写机制是交给 Redis子进程 来执行的。这样不会阻塞到主线程的操作。那么在新的AOF日志进行读入的时候,前方的数据发生改变了又当如何呢?其实Redis会在 AOF重写 期间创建一个新的buf缓冲,称为重写缓冲。当一条命令被执行后,旧的buf缓冲和重写缓冲一起进行追加。
当 新的AOF日志 重写完毕后,再将重写期间的缓冲区追加到新的AOF日志中去,这样就保证了数据一致性。
RDB快照是如何实现的呢?
RDB快照和AOF日志是两种不同的实现Redis持久化的方式。
AOF日志会将每条执行语句分为几个部分然后追加到AOF日志中去,用于保证异常情况下重启时的数据一致性。
RDB快照会将某个时刻下该内存数据情况进行存储到一个文件中。用于异常情况下重启的数据恢复。对比AOF日志和RDB快照来说,有以下几个方面:
- 执行效率:AOF日志将所有的执行语句都进行存储。RDB会将当前内存数据进行存储,并且RDB存储的是实际命令。恢复效率要远高于AOF日志。
- 数据完整性:AOF日志会记录从开启到结束的所有执行命令。而RDB只会隔一段时间记录一次,具体时间可以自行配置。因此隔离时间越长,遇到异常情况恢复起来数据完整性越差。
RDB是如何工作的?
RDB是通过Redis的 Save、bgSave 工作的。
- Save:在主进程下进行RDB复制工作,会阻塞主进程命令的执行。
- bgSave:在子进程下进行RDB复制工作,不会阻塞主进程命令的执行。
Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的**「所有数据」**都记录到磁盘中。所以执行快照是一个比较重的操作。如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。
RDB执行快照的过程中,数据可以被修改吗?
答案是可以的。在执行bgsave的过程中,Redis开启子进程对RDB进行快照。因为子进程和父进程之间共用一个页表(指针指向同一块内存)。因此即使在执行快照过程中,数据被修改也不会造成影响。
混合持久化是什么?
混合持久化是指AOF日志和RDB快照共同生效的一种方式。
他既能保证AOF日志的数据强完整性,又能保证RDB快照恢复数据的高效。那么他是如何做的呢?
我们知道AOF回写机制是利用了子进程,我们也知道RDB快照利用的也是子进程,那么能不能同时双线操作呢?
实现
- 在AOF回写机制开启的时候,Redis将当前所有的内存数据以RDB 实际命令的格式记录在新AOF日志中。
- 在记录过程中,主进程进行的所有操作依旧按照AOF回写机制存储在回写buf缓存中。
- 当记录完毕后,将回写buf缓存内容按照AOF格式追加到新AOF日志中去。
- 最终该日志会替换原有的 AOF 日志,他的格式如下所示:
这样就完成了混合持久化操作,既保留了 RDB 的高效,也保证了 AOF 的完整性。
没有完美,只有更完美
虽然混合持久化效果很好,但是他也有缺点:
-
AOF文件可读性很差,一会 RDB 格式,一会 AOF 格式,花样挺多呀!
-
兼容性很差,因为该操作是诞生于 Redis4.0 版本之后的,因此生成的新AOF日志无法用于 Redis4.0 版本之前。
Redis过期删除与内存淘汰
Redis如何过期删除?过期删除的策略?
Redis如何进行过期删除?
Redis对于每个命令的执行,都可以为其设置一个过期时间。因此就需要有一些操作来管理这些过期的key。
这些被设置了过期时间的key,会被统一存储在过期字典中去。过期字典中存储了key-time,每一个key对应一个过期时间。
当一个获取请求命令执行时,会先判断该 key 是否在过期字典中。
如果 key 在过期字典中,就去判断该 key 是否过期。如果 key 不在过期字典中,就直接获取数据即可。
过期删除的策略有哪些?
Redis有两种过期删除的策略,分别是:
- 惰性删除:不会主动的去删除过期的key,只有在一个请求 key 命令执行时,经过判断该key已经失效,才会将其从内存中删除掉。这样做虽然会省去主进程用于删除key的成本,但也同时大量消耗了内存空间。
- 定时删除:每隔一段时间执行一次删除工作。每次从过期字典中随机挑选出 20 条 key,逐个判断其是否过期。如果挑选的 20 个 key 中,有超过 25%的key已经过期,会再次从过期字典中随机挑选出 20 条 key,循环操作。直到没有超过 25%的 key。如果循环时间超过25ms时也会被终止。这样做的好处就是不会有大量的过期key同时出现,并且也不会占据主进程太多的时间。缺点就是时间不好把握,如果间隔过短,频率过高,CPU吃不消。如果间隔过长,会跟惰性删除一样,出现大量的过期key。
因此,可以选择使用 [惰性删除+定时删除] 两种策略的配合使用,在一个合理的CPU时间和避免内存浪费时间做一个平衡。
Redis持久化过程中,对过期键如何处理?
我们知道Redis持久化方式有两种:AOF日志和RDB文件。那么在Redis进行持久化的时候,遇到**过期 key **该如何处理呢?
RDB文件分为两个阶段,分别是生成阶段和加载阶段。
- RDB文件生成阶段:在生成RDB文件过程中,需要对当前时刻内存数据进行记录。如果遇到key就会去检查他是否过期,如果没有过期就进行记录,否则不记录。
- RDB文件加载阶段:RDB文件加载阶段,要看当前服务器是主服务器还是从服务器。
- 如果服务器是主服务器:需要去判断key是否过期,从而选择是否被加载到数据库中。
- 如果服务器是从服务器:不需要去判断key是否过期,因为在主服务器进行数据同步的时候,从服务器中的所有数据都会被删除掉。因此对从服务器不会造成影响。
AOF文件也分为两个阶段:AOF写入阶段和AOF重写阶段
- AOF写入阶段:如果在AOF进行写入的阶段遇到了过期key,但是这个key还没有被删除掉。仍然会被写入到AOF文件中。如果这个key已经被删除掉了,那么就会在这条命令之后添加一个Del的删除key命令。
- AOF重写阶段:服务器会对AOF日志进行检查,如果该key已经过期,则不会去在数据库中生成数据。
Redis主从模式下,对过期键如何处理?
Redis主从模式指的是:多台服务器之间建立主服务器,和从服务器的关系。主服务器主要负责数据的增删改,从服务器主要负责数据的查询工作。当主服务器数据发生变化时,从服务器也要跟着变化。这种变化是通过异步日志的方式实现的,想要了解的同学可以自行查询一下,这里就不过多阐述了,我们主要来讲一下这种模式对过期键如何处理的问题。
当从服务器中的key过期时,不会进行删除工作。当主服务器中的key过期时,会执行Del删除该key,连带也会对从服务器中的数据造成影响,因此从服务器不需要主动的去做删除工作。
Redis内存满了会发生什么?
如果Redis内存满了,
-
会先触发内存淘汰机制。这个阀值可以通过我们来设置,配置项为 maxmemory。
-
如果Redis配置了持久化机制,数据会被写入磁盘,以避免数据丢失。
-
如果Redis没有配置持久化机制,数据会被清空,导致数据丢失。
-
Redis会停止接受新的写入请求,直到有足够的内存空间。
因此,需要我们设置合理的最大内存大小,并且合理运用内存淘汰机制。
Redis内存淘汰策略有哪些?
Redis内存淘汰策略一共有八种。这八种可以分为 不进行数据淘汰和进行数据淘汰两部分。
-
不进行数据淘汰的策略:
noeviction(Redis3.0版本之后默认的淘汰策略):他表示当前运行内存超过设置的最大内存之后,不进行内存淘汰。直接不提供服务,返回错误。
-
进行数据淘汰的策略:
对于数据淘汰的策略,又可以分为对设置了过期时间的数据进行淘汰和对所有数据进行淘汰两种
- volatile-random:在设置了过期时间的数据中进行随机的淘汰。
- volatile-ttl:优先淘汰过期时间更早的数据。
- volatile-lru(Redis3.0版本之前默认的淘汰方式):淘汰设置了过期时间的键中最不常使用的键。
- volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
在所有数据范围内进行淘汰:
- allkeys-random:随机淘汰任意键值;
- allkeys-lru:淘汰整个键值中最久未使用的键值;
- allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。
Redis缓存设计
在了解Redis缓存设计之前,我们需要先了解一些Redis缓存相关知识点:缓存击穿、缓存穿透、缓存雪崩。
只有了解了这些知识点,你才能更深刻的认识到缓存设计的重要性与关键作用。
缓存击穿
缓存击穿是缓存在一个时间段内,正在正常的承受请求,突然因为某些原因失效,同时伴随有大量并发请求打入数据库,最终导致了数据库的崩溃。注意一点,这些请求全部都是有效请求,只不过并发量太大。
这里结合一张图并用示例来说明一下:
有一天一个微博里的一位不那么火热的明星被爆出了大瓜,大家都来围观,然后呢,有些人就去翻阅其以往的记录想要调查出一点”蛛丝马迹“。
这时,Redis缓存突然收到了很多冷缓存请求,一开始Redis不以为然,从数据库中拿到数据并且将这些数据都返回就行了。但是天有不测风云,好景不长,有那么一瞬间Redis的缓存突然过期了。这时这位明星的大瓜还没有散去,甚至达到了高峰期。
同一时间,大量的请求进入Redis缓存,结果答案就是对DB数据库的定点输出,直接把数据库搞崩溃了。
缓存击穿例子解释
假设微博热搜特别火,导致很多用户都去查询一个点,对这个点一直进行查询。如果此时缓存内容设置的过期时间为 60s ,60.1s 重新恢复缓存内容,那么在这 0.1s 的时间内就可能会将服务器宕机,造成击穿现象。
对于这种正常请求导致的数据库崩溃,我们就叫他缓存击穿,那么应该如何防范呢?
缓存击穿如何防范
- 我们可以对 Redis 中的缓存数据进行缓存时间重置。在每次请求该缓存,都将他的过期时间进行重置,避免热点期间缓存过期的情况发生。
- 使用分布式锁。每次Redis缓存需要从DB数据库中获取数据时,都需要经过分布式锁,这样就阻止了大量并发请求同时打在数据库上的情况发生了。
- 也可以采用限流、降级、熔断等手段,避免大量并发请求的同时出现。
缓存穿透
缓存穿透是指,在一段时间内,大量的非正常请求进入Redis缓存,然后发现找不到缓存数据,就去请求DB数据库,然后发现数据库也找不到这个数据。数据库心想:好奇怪,我都说了我没有,你还一直问我要!
在软磨硬泡的强烈攻势下,数据库终于缴械了:我真服了你个老六了。这就是缓存穿透。
缓存穿透与缓存击穿的概念很相似,为了避免大家弄混,我会着重介绍这两者的关键区别。
下面用一张图和例子来解释一下缓存穿透:
某些奇奇怪怪的程序员,总想着进行一些不友好的访问。比如:爬虫,恶意请求
他们模仿正常的请求,做出一些不正常的操作,疯狂的进行“友好”访问。而这些请求在数据库中根本就找不到对应的数据,但是大量的请求一直袭来,Redis缓存这时候跟葛优大爷一样,直接躺平了,无可奈何之下,数据库顶不住也宕机了。
要正确认识缓存穿透和缓存击穿这两个概念,就必须先知道他们各自发生的场景。也要了解穿透和击穿两个词汇。
- 击穿:指的是结结实实的打在了上边,然后给他破防了。比如说你打游戏出了穿甲弓打坦克,刀刀砍在护甲上,硬是给他磨死了。
- 穿透:指的是你通过某些”手法”,给他来了一个措手不及。比如你打游戏出了混伤,本来对面以为你是个ADC,全出了护甲,没想到你小子还有当法师的潜质。这没办法啊,刀刀穿透护甲深入骨髓。
如何防范缓存穿透呢?
- 布隆过滤器:布隆过滤器是一种非常高效的,多个hash值确定一个key的数据结构。而且hash是根据进制来进行确定的,显然非常的快速。它可以将每个key进行存储,然后等到下一个key来的时候就给他返回一个值。用于判断请求是否合格,如果不合格,不用去数据库获取元素,直接返回空值(这个值可以自己设定)即可。具体实现原理可以自行查看资料。
- 为请求设置一个cache。当这个请求访问数据库之后返回来的数据为空,说明他没找到时,可以在redis中为这个key设置一个空缓存。当下次这个key来临的时候,直接返回该空值。
PS:
这里提一嘴布隆过滤器。他的优点很明显:高效便捷,但他也有缺点。比如无法进行删除操作。听说现在常用的有一个布谷鸟过滤器,解决了这个问题,并且更加的高效。感兴趣可以去了解一下。
缓存雪崩
缓存雪崩是指某一时刻大量的 key 通过无效的 Redis 访问到数据库中,导致数据库崩溃。
这乍一听怎么这么像缓存击穿呢?我写的时候差点都觉得写了两遍:)。
但是他们两个的前提条件是不同滴,缓存雪崩的前提条件是某一时刻,大量的key失效,这可不是单个key失效!
而缓存击穿的前提条件是某一时刻,单个key失效。
具体看我的图和例子进行解释,相信你就会恍然大悟了!
在双十一期间,一个晚上的某一时刻往往能卖掉大量的商品,而这些商品都会产生订单。
在这个时刻下单的所有商品,他们可能在Redis中都会留有一个缓存数据。当 0 点时这个数据正火热的通过 Redis 返回给客户端。这时也不会发生什么意外,一切都能撑得住。
但随着时间的流逝,凌晨两点,第一批同时下单的商品统一的一起过期,大量的商品请求被访问,数据库将第一批商品数据返回给Redis继续支撑。然后紧接着,第二批来到,第三批…等等,直到数据库崩溃宕机。
一个数据库宕机了不要紧,多台数据库可能会紧随脚步挨个宕机,然后整个系统如同雪崩式的崩溃。
如何防范缓存雪崩
- redis高可用
- 既然redis可能挂掉,那我多设置几台redis服务器,这样挂掉一台其他的还可以继续工作。其实就是搭建集群。
- 限流降级
- 把部分业务停掉,来保证其他服务的正常进行。如淘宝双十一停掉退款服务。
- 当缓存失效之后,通过加速或者队列来控制读数据库写缓存的线程数量。比如对于某个key只允许一个线程查询数据和写缓存,其他线程进行等待。
- 数据预热,过期时间打散
- 数据加热就是在正式部署之前,先把可能的数据全部预先访问一遍。这样大量的访问数据就会被加载到缓存中。在即将发生大并发访问之前手动触发加载缓存不同的key,设置不同梯度的过期时间。这样可以让缓存失效的时间均匀分布,不会造成全部集中失效!
如何设计一个缓存策略?
由于数据存储受限,系统并不是将所有数据都需要存放到缓存中的,而只是将其中一部分热点数据缓存起来,所以我们要设计一个热点数据动态缓存的策略。
热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
可以简单的描述为一个队列来实现:
- 对这个队列的前 100 热点 key 进行热点处理。每隔一段时间,可以根据单位时间内访问次数来对队列中的key进行排序。重新规定前 100 名热点key。
- 同时,可以将排名靠后的一些热点key丢弃,重新从数据库中拿出一部分key放在队列中。
如何保证缓存一致性?
提起Redis,总会碰到这个问题——缓存一致性。它意味着你要保证 Redis 缓存中的数据和数据库中实际的数据保持一致。
下面我会分别讲一下四种种缓存一致性的方法,并逐个介绍他们的优势和不足。
- 先修改数据库再更新缓存
- 先更新缓存再修改数据库
- 先删除缓存再修改数据库
- 先修改数据库再删除缓存
先更新缓存再修改数据库
先来看一下正常的流程图:
如果一切正常,网络也没问题,对大家都没影响。但有时候巧就巧在事与愿违,偏偏屋漏恰逢连夜雨,更新数据库的操作失败了。
如果这个时候数据库更新失败会造成什么后果呢?
- 缓存中是已经修改过的最新数据,只要在此期间有新的请求来临,就会返回该数据。
- 数据库中是旧数据,缓存收到数据库更新失败后需要将缓存删除掉。
- 在缓存修改完毕后到删除缓存的这个期间内,只要有新的请求全部收到的都是脏数据。
更严重的后果是,数据库当场宕机,缓存没有收到删除的命令,那么在请求过期之前,全部为脏数据。
先修改数据库再更新缓存
如果先修改数据库再更新缓存,和先修改缓存再更新数据库会发生一样的问题。这次bu是数据库发生了问题,而是缓存更新失败,那么缓存中数据在未过期之前都是脏数据。甚至如果缓存有自动续期的话,脏数据将无法解决。
从上面两张图中,大家也能看出,无论咋样,只要执行第二步时失败了,就必然会产生脏数据。这就是先修改的问题。那么来看看删除怎么样。
先删除缓存再修改数据库
可以根据下图看出,如果先删除缓存数据,即使数据库数据没有更新成功。下次缓存不存在时就会去查找数据库数据,然后更新缓存,这样就解决了数据不一致性的问题。那么事实真的如此吗?
这里假设一个场景:在高并发下,有两个请求进行访问,一个是读请求,一个是写请求。
当第一个写请求访问服务器时,会先将缓存进行删除,然后去更新数据库中的数据。与此同时一个读请求进来,访问缓存之后发现没有数据。怎么办?去数据库中查找,然后呢?他比修改数据库的操作稍微快了那么一点,脏数据又又又诞生了。
此时此刻,缓里存放着旧的数据,数据库里存储着新的数据,又回到了第一个场景。经典白忙活。
先修改数据库再删除缓存
此时仍会发生一二场景的问题,如果更新数据库成功了,突然就宕机了,删除缓存数据没执行或者失败了怎么办?
这里先不考虑会不会失败的问题,仍然假设高并发下,两个请求的处理。
- 首先读请求访问缓存,发现没有拿到数据,去数据库中查找数据。这时他的网络突然卡了一下。
- 由于读请求网络卡了,给到了写请求机会。写请求修改完数据库,准备去删除缓存,发现没缓存,有缓存也删除完毕了。这时读请求的网络恢复了。
- 读请求将拿到的旧数据放在了缓存中,悲剧再次发生。
到这里就把四种方案简单介绍了一下,可以看出每种方案都没有完全解决掉数据一致性的问题。
那怎么办?
我记得我看过的第一篇介绍Redis缓存一致性的文章,答主给出了答案:
对于缓存,加锁会降低其效率,本来缓存的作用就是为了提升性能,加锁不可取。
其次鱼和熊掌不可兼得。你又想要高效率,又要求完美无瑕,这必然不可能的。因此可以采取适当的措施尽量减少风险的诞生以及后果的危害性。
-
对于一些对实时数据不是那么严格的请求,偶尔返回脏数据也不会造成很大的后果,比如点赞数。
-
对于一些要求比较严格的数据,可以采用失败后多次尝试的策略。
推荐阅读
面试篇
Redis 常见面试题 | 小林coding (xiaolincoding.com)
刨根问底 Redis, 面试过程真好使 - 掘金 (juejin.cn)
实战篇
Redis只能做缓存?太out了! - 掘金 (juejin.cn)
聊一聊安全且正确使用缓存的那些事 —— 关于缓存可靠性、关乎数据一致性 - 掘金 (juejin.cn)
聊一聊缓存和数据库不一致性问题的产生及主流解决方案以及扩展的思考 - 掘金 (juejin.cn)