什么是Redis
redis是一种基于内存的数据库,因为对数据的读写是在内存当中完成的,所以读写速度非常快,常用于缓存、消息队列、分布式锁等场景
redis提供了多种数据类型来支持不同的业务场景,比如说String、Hash、List、Set(集合)等等,并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题
除此之外,Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等
那么有人会问了,同样的,memcached也是基于内存的数据库,为什么不选择它作为缓存呢?
那么我们要弄清楚redis和memcached的区别和共同点:
共同点:1、都是基于内存的数据库,一般都用来当作缓存
2、都有过期策略
3、两者的性能都非常高
区别: 1、redis支持的数据类型更丰富,而memcached只支持key-value数据类型
2、redis支持数据的持久化,可以将数据从内存转移到磁盘当中,下次重启的时候再次加载使用,而memcached则没有,重启之后数据消失
3、redis原生支持集群模式,memcached没有原生的集群模式,需要依靠客户端来实现集群中分片写入数据
4、redis支持事务等功能,memcached不支持
为什么用redis作为mysql的缓存?
主要因为redis的高性能和高并发特性(
高并发(High Concurrency)是指在同一时间内,有大量的用户或客户端请求被发送到一个系统或服务,并且系统能够同时处理这些请求,保证稳定和响应速度的能力。)
虽然用户第一次读取MySQL中的某些数据,因为是从磁盘上读取的,这个过程会比较慢,但是这些数据会存储到redis缓存当中,这样下一次再访问的时候就可以直接从缓存中获取了,操作redis缓存就是直接操作内存,速度相当快,那么这里存在一个问题,就是假如说mysql的数据改变了,那么redis缓存还会存在么?答案是如果 MySQL 中的对应数据改变的之后,同步改变 Redis 缓存中相应的数据即可,不过这里会有 Redis 和 MySQL 双写一致性的问题
单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。
所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
redis的数据结构:
redis的数据结构以及使用场景分别是什么?
redis提供了丰富的数据类型,常见的数据类型有:string、hash、list、set、Zset等
这五种数据类型的应用场景:
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类型的数据结构实现主要是SDS(简单动态字符串)。SDS相比于C的字符串来说:
SDS不仅可以保存文本数据,而且可以保存二进制数据。
SDS获取字符串长度的时间复杂度为O(1),因为它专门设置了一个长度属性
redis的SDS API是安全的,拼接字符串不会造成缓冲区溢出。因为 SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。
Hash类型的底层数据结构是由listpack或者哈希表实现的
Set类型的数据结构是由哈希表或整数集合实现的,如果集合中的元素都是整数且元素小于512个,redis会使用整数集合作为set类型的底层数据结构
Zset类型的底层数据结构由listpack或者跳表实现的
Redis 线程模型
Redis 是单线程吗?
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的:
现版本会启动三个后台进程,分别处理关闭文件、AOF刷盘、异步释放redis内存,着重说一下第三个后台进程,因为使用del命令进行删除的时候,会推给主线程处理,这样的话会导致redis主线程卡顿,因此我们使用unlink命令来异步删除大key,这样就会交给后台进程处理
之所以 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。
关闭文件、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) 释放跳表对象;
但是在redis6.0版本之前都是单线程模式,按照连接事件、读事件、写事件的顺序执行,但是之前的redis既然是单线程为什么还那么快,三个原因:
1、redis的大部分操作都在内存中完成,并且采用了高效的数据结构,因此redis瓶颈可能是机器的内存或者网络带宽,而不是CPU,既然CPU不是瓶颈,那么自然就采用单线程的解决方案了
2、redis采用单线程可以避免多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题
3、redis采用了I/O多路复用机制处理大量的客户端Socket请求,IO多路复用是指一个线程处理多个IO流,就是我们经常听到的select/epoll机制。简单来说,在redis只运行单线程的情况下,该禁止允许内核同时存在多个监听Socket和已连接Socket。内核会一直监听这些 Socket 上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果
所以为什么Redis 6.0 之前为什么使用单线程?
因为首先redis之前的瓶颈不是因为性能的问题,而是受限于内存和网络I/O的限制,所以redis核心网络模型使用单线程没什么问题,如果你想要使用服务的多核CPU,可以在一台服务器上启动多个节点或者采用分片集群的方式,另外,多线程虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,比如说增加了系统复杂度、同时存在线程切换。加锁/解锁/死锁造成的性能损耗
Redis 6.0 之后为什么引入了多线程?
虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。
redis 6.0 版本支持的 I/O 多线程特性,默认情况下 I/O 多线程只针对发送响应数据(write client socket),并不会以多线程的方式处理读请求(read client socket)。要想开启多线程处理客户端读请求,就需要把 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes。
Redis 持久化
redis通过将数据存储到磁盘当中实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样在redis重启之后就能够从磁盘中恢复原有的数据
redis共有三种数据持久化的方式:
AOF日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点;
AOF其实就是先执行一条命令,再保存一条,恢复的时候也是一条一条命令的恢复
为什么先执行命令,再写呢?
这样做有两个好处:1、避免额外的检查开销(命令可能会出现拼写错误等等),2、不会堵塞当前写命令的执行
但是这样也会造成风险:1、数据可能会丢失。当执行完命令服务器直接宕机了,会造成redis写命令的丢失 2、可能会堵塞其他进程
AOF 日志过大,会触发什么机制?
随着时间的累积,AOF内部的写命令会越来越多,这就导致恢复的速度可能会越来越慢,这就出现了AOF重写机制,就是当AOF文件大小超过了某个阈值的时候,redis会读取当前数据库的所有键值对,然后将每一个键值对用一条命令记录到新的AOF文件当中,等到全部记录完后,就将新的AOF文件替换掉现有的AOF文件
Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,这么做可以达到两个好处:
1、它不打扰主进程执行的命令 2、线程之间可以共享内存,父子进程之间无需加锁来保证数据的安全
但是重写过程中,主进程依然可以正常处理命令,那问题来了,重写 AOF 日志过程中,如果主进程修改了已经存在 key-value,那么会发生写时复制,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了,这时要怎么办呢?
为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。
在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。
也就是说,在 bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作:
- 执行客户端发来的命令;
- 将执行后的写命令追加到 「AOF 缓冲区」;
- 将执行后的写命令追加到 「AOF 重写缓冲区」;
当子进程完成 AOF 重写工作(扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。
主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:
- 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
- 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。
ROB快照是如何实现的?
RDB快照是在某一时刻进行内存数据的记录,因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。
RDB 做快照时会阻塞线程吗?
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行:
- 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
- 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;
Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令,默认会提供以下配置:
别看选项名叫 save,实际上执行的是 bgsave 命令,也就是会创建子进程来生成 RDB 快照文件。 只要满足上面条件的任意一个,就会执行 bgsave,它们的意思分别是:
- 900 秒之内,对数据库进行了至少 1 次修改;
- 300 秒之内,对数据库进行了至少 10 次修改;
- 60 秒之内,对数据库进行了至少 10000 次修改。
这里提一点,Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。所以执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。
RDB 在执行快照的时候,数据能修改吗?
可以的,执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,关键的技术就在于写时复制技术(Copy-On-Write, COW)。执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响。如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。
为什么会有混合持久化?
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,但是数据恢复不快。
为了集成了两者的优点, Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。
混合持久化工作在 AOF 日志重写过程,当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。
加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。
混合持久化优点:
- 混合持久化结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。
混合持久化缺点:
- AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;
- 兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了
Redis 集群
Redis 如何实现服务高可用?
主从复制:
主从复制是 Redis 高可用服务的最基础的保证,实现方案就是将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,且主从服务器之间采用的是「读写分离」的方式。
主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。
也就是说,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的。
注意,主从服务器之间的命令复制是异步进行的。
具体来说,在主从服务器命令传播阶段,主服务器收到新的写命令后,会发送给从服务器。但是,主服务器并不会等到从服务器实际执行完命令后,再把结果返回给客户端,而是主服务器自己在本地执行完命令后,就会向客户端返回结果了。如果从服务器还没有执行主服务器同步过来的命令,主从服务器间的数据就不一致了。
所以,无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。
哨兵模式:
在使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复。
为了解决这个问题,Redis 增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。
切片集群模式:
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
集群脑裂导致数据丢失怎么办?
什么是脑裂:脑裂就是当主从节点之间的联系全部断开了,但这时候客户端还与主节点连接,依然发送者写数据请求,主节点也在响应这些请求,这时候哨兵发现主从节点失联了,所以它选出了一个leader重新作为主节点,然而,这时候,主从节点的联系又好了,这就出现了两个主节点,也叫脑裂,但是哨兵推选出来的主节点会将原来的主节点作为附庸,并让其清空本地数据,和它数据同步,这就导致了在这个时间段内与客户端的联系数据丢失
总结一句话就是:由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。
解决方案就是:当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。
在 Redis 的配置文件中有两个参数我们可以设置:
- min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
- min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。
我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。这两个配置项组合后的要求是,主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK 消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的写请求了。
Redis过期删除与内存淘汰
Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。
当我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:
- 如果不在,则正常读取键值;
- 如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。
Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。
什么是惰性删除策略:惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
惰性删除策略的优点:
- 因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
惰性删除策略的缺点:
- 如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。
什么是定期删除策略?定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
Redis 的定期删除的流程:
- 从过期字典中随机抽取 20 个 key;
- 检查这 20 个 key 是否过期,并删除已过期的 key;
- 如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。
可以看到,定期删除是一个循环的流程。那 Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。
定期删除策略的优点:
- 通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
定期删除策略的缺点:
- 难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对 CPU 不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。
可以看到,惰性删除策略和定期删除策略都有各自的优点,所以 Redis 选择「惰性删除+定期删除」这两种策略配和使用,以求在合理使用 CPU 时间和避免内存浪费之间取得平衡。
Redis 持久化时,对过期键会如何处理的?
我们之前提到,redis持久化有两种方法LRDB和AOF
在RDB当中,:在RDB文件生成阶段的时候,从内存状态持久化成RDB文件的时候,会对key进行过期检查,如果key过期了则不会被保存到新的RDB文件当中,因此redis的过期键不会对生成的RDB文件产生影响
在RDB加载阶段:如果是主服务器中,在加载RDB的时候,会对key值进行是否过期判断,如果过期了,则不会进行加载到数据库中,如果是从服务器,在载入RDB文件时,不管key是否过期都会载入到数据库当中,但是由于主从服务器在进行数据同步时,从服务器会数据清空,所以,过期key对载入RDB文件也不会产生影响
在AOF中:AOF写入阶段,当redis以AOF来进行持久化时,若数据库中的某个过期键还没删除,那么AOF文件会保留此过期键,当此过期键被删除后,redis会向AOF文件追加一条DEL命令来显式的删除键值
AOF加载阶段:在重写阶段中,redis会对键值进行检查,如果发现已过期的键值则不会保存到重写后的AOF文件中,因此不会对持久化产生影响
Redis 主从模式中,对过期键会如何处理?
当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的。也就是即使从库中的 key 过期了,如果有客户端访问从库时,依然可以得到 key 对应的值,像未过期的键值对一样返回。
从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
Redis 内存满了,会发生什么?
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory。
Redis 内存淘汰策略有哪些?
Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。
不进行数据淘汰:当内存达到某个阈值的时候,redis将不再提供功能,直接返回错误
进行数据淘汰:
对设置了过期时间的数据淘汰:1、随机淘汰设置了过期时间的任意键值 2、优先淘汰更早过期的键值 3、优先淘汰所有设置了过期时间的键值中,最久未使用的键值 4、优先淘汰设置了过期时间的使用次数最少的键值
未设置过期时间的数据淘汰:1、随机淘汰任意键值 2、优先淘汰最久未使用的键值 3、优先淘汰使用次数最少的键值
LRU算法和LFU算法:
在平常的LRU(最近最久未使用算法)算法实现中,我们通常使用链表对元素进行排序,最新使用的元素会被移动到表头,所以我们只需要删除链表尾部的元素即可,但是在redis中并没有使用这种方式,
因为传统的 LRU 算法存在两个问题:
- 需要用链表管理所有的缓存数据,这会带来额外的空间开销;
- 当有数据被访问时,需要在链表上把该数据移动到头端,如果有大量数据被访问,就会带来很多链表移动操作,会很耗时,进而会降低 Redis 缓存性能。
所以,redis实现的是一种近似LRU的算法,它在redis的对象结构体当中设置了一个额外字段,记录数据的最近访问时间,当redis进行内存淘汰的时候,会使用随机采样的方式来淘汰数据,随机挑选五个值,淘汰最久未使用的哪个
但是 LRU 算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。
因此,在 Redis 4.0 之后引入了 LFU 算法来解决这个问题。
LFU(最近最不常使用)算法
相较于LRU,LFU又多了一个数据访问频次字段
Redis 缓存设计
#如何避免缓存雪崩、缓存击穿、缓存穿透?
如何避免缓存雪崩
缓存雪崩就是当大量的缓存数据在同一时间全部过期,同时又有大量的用户请求这些数据,就会导致全部去数据库当中访问,导致数据库的压力骤增,甚至可能会造成数据库宕机,从而发生一系列的反应,造成系统崩溃
解决方式:我们可以在过期时间上加上一个随机时间,这样就不会出现大量的缓存数据同时过期
我们可以设置缓存不过期,这也是一种解决办法
如何避免缓存击穿?
我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。 应对缓存击穿可以采取前面说到两种方案:
- 互斥锁方案(Redis 中使用 setNX 方法设置一个状态位,表示这是一种锁定状态),保证同一时间只有一个业务线程请求缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
如何避免缓存穿透?
当发生缓存雪崩或击穿时,数据库中还是保存了应用要访问的数据,一旦缓存恢复相对应的数据,就可以减轻数据库的压力,而缓存穿透就不一样了。
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
解决方法:1、对非法请求进行限制,我们需要判断请求参数是否合理,如果判断出是恶意请求则直接返回错误
2、设置空值或者默认值,通过将空值返回给未访问到数据的请求
3、通过布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在
如何设计一个缓存策略,可以动态缓存热点数据呢?
由于数据存储受限,系统并不是将所有数据都需要存放到缓存中的,而只是将其中一部分热点数据缓存起来,所以我们要设计一个热点数据动态缓存的策略。
热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
以电商平台场景中的例子,现在要求只缓存用户经常访问的 Top 1000 的商品。具体细节如下:
- 先通过缓存系统做一个排序队列(比如存放 1000 个商品),系统会根据商品的访问时间,更新队列信息,越是最近访问的商品排名越靠前;
- 同时系统会定期过滤掉队列中排名最后的 200 个商品,然后再从数据库中随机读取出 200 个商品加入队列中;
- 这样当请求每次到达的时候,会先从队列中获取商品 ID,如果命中,就根据 ID 再从另一个缓存数据结构中读取实际的商品信息,并返回。
说说常见的缓存更新策略?
实际开发中,Redis 和 MySQL 的更新策略用的是 Cache Aside,简单来说
写策略的步骤:
- 先更新数据库中的数据,再删除缓存中的数据。
读策略的步骤:
- 如果读取的数据命中了缓存,则直接返回数据;
- 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。
缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。而一旦请求 A 早于请求 B 删除缓存之前更新了缓存,那么接下来的请求就会因为缓存不命中而从数据库中重新读取数据,所以不会出现这种不一致的情况。
Cache Aside 策略适合读多写少的场景,不适合写多的场景,因为当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。如果业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:
- 一种做法是在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁,因为这样在同一时间只允许一个线程更新缓存,就不会产生并发问题了。当然这么做对于写入的性能会有一些影响;
- 另一种做法同样也是在更新数据时更新缓存,只是给缓存加一个较短的过期时间,这样即使出现缓存不一致的情况,缓存的数据也会很快过期,对业务的影响也是可以接受。
Redis 实战
Redis 如何实现延迟队列?
延迟队列是指把当前要做的事情,往后推迟一段时间再做。延迟队列的常见使用场景有以下几种:
- 在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消;
- 打车的时候,在规定时间没有车主接单,平台会取消你的单并提醒你暂时没有车主接单;
- 点外卖的时候,如果商家在10分钟还没接单,就会自动取消订单;
在 Redis 可以使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。
使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。
Redis 的大 key 如何处理?
大 key 并不是指 key 的值很大,而是 key 对应的 value 很大。
一般而言,下面这两种情况被称为大 key:
- String 类型的值大于 10 KB;
- Hash、List、Set、ZSet 类型的元素的个数超过 5000个;
大 key 会造成什么问题?
大 key 会带来以下四种影响:
- 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
- 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
- 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
- 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较大。
如何删除大key
如果一下子释放了大量内存,空暇内存块链表操作时间就会增加,相应的会造成redis主线程的阻塞,如果主线程发生了阻塞,其他所有请求可能都会超时,超时越来越多,会造成 Redis 连接耗尽,产生各种异常。
因此,删除大 key 这一个动作,我们要小心。具体要怎么做呢?这里给出两种方法:
- 分批次删除
- 异步删除(Redis 4.0版本以上)
在异步删除中,就和我们之前说过的redis的多线程一样,通过unlink将删除擦欧总交给后台线程进行,这样就不会堵塞主线程
Redis 管道有什么用?
使用管道技术可以解决多个命令执行时的网络等待,它是把多个命令整合到一起发送给服务器端处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。
但使用管道技术也要注意避免发送的命令过大,或管道内的数据太多而导致的网络阻塞。
要注意的是,管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能
Redis 事务支持回滚吗?
Redis 中并没有提供回滚机制,虽然 Redis 提供了 DISCARD 命令,但是这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果。
如何用 Redis 实现分布式锁的?
我们可以使用redis库来实现,比如说redsync.Redsync库,通过创建一个特定名字的互斥锁,并对其进行上锁来实现
- 多个 Redis 实例:Redsync 并不依赖单个 Redis 实例来维护锁的状态,而是可以利用多个 Redis 实例来增加锁机制的容错性。
- 锁算法:
- 当请求锁时,Redsync 会尝试在多个 Redis 实例上创建锁。
- 锁由一个随机生成的值(锁标识)和一个有有效期的键组成。
- 只有当大多数(通常是超过一半的)Redis 实例都成功设置了键时,锁才被认为是成功获取的。
- 自动解锁:每个锁都有一个过期时间,以防止死锁和长时间的锁占用。锁定操作必须在超时前完成,否则锁会自动释放。
- 解锁过程:解锁时,Redsync 会在所有 Redis 实例上尝试删除锁。只有当大多数实例上的锁被成功删除时,锁才被认为是成功释放的。
在这个例子中,我们首先创建了一个 Redis 客户端池,然后利用这些客户端初始化了一个 Redsync 实例。接着,我们创建了一个互斥锁,尝试获取锁,并在操作完成后释放锁。
适用场景
Redsync 非常适合于需要跨多个应用或服务确保资源互斥访问的场景,特别是在微服务架构或分布式系统中。使用 Redsync 可以有效地防止资源冲突和状态不一致的问题。
标签:AOF,缓存,过期,redis,Redis,key,数据 From: https://www.cnblogs.com/guanyifan/p/18072768