目录
Redis线程模型
Redis是单线程吗?
Redis单线程是指接收客户端---解析请求---进行数据读写请求操作---发送数据到客户顿这个过程是由一个线程(主线程)来完成的。
但Redis程序并不是单线程的,Redis在启动的时候,是会启动后台程序(BIO)的:
1.Redis2.6版本,会启动2个线程,分别处理文件关闭、aof刷盘这两个任务
2.Redis4.0版本后,新增了一个新的后台程序,实现数据的异步惰性删除,解决删除数据效率比较低的问题
3.Redis6版本线程模型,网络 I/O 和命令处理都是单线程
4.Redis7,采用多个线程来处理网络请求,提高网络请求处理的并行度,对于读写操作命令依然使用单线程处理。
Redis采用单线程为什么那么快?
1.Redis大部分操作都在内存中完成,并且采用了高效的数据结构,因此Redis瓶颈并非CPU,而是机器内存或网络单宽。
2.Redis采用单线程避免多线程间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
2.Redis采用了I/O多路复用处理大量客户端socket请求,I/O多路复用是指一个或一组线程处理多个tcp连接,使用单线程就能实现同时处理多个客户端的连接,无需创建或维护过多线程/进程。
I/O多路复用模型
将用户socket对应的文件描述符(FileDescriptor)注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor反应模式。
如上图对Redis的I/O多路复用模型进行一下描述说明:
(1)一个socket客户端与服务端连接时,会生成对应一个套接字描述符(套接字描述符是文件描述符的一种),每一个socket网络连接其实都对应一个文件描述符。
(2)多个客户端与服务端连接时,Redis使用 I/O多路复用程序 将客户端socket对应的FD注册到监听列表(一个队列)中,并同时监控多个文件描述符(fd)的读写情况。当客服端执行accept、read、write、close等操作命令时,I/O多路复用程序会将命令封装成一个事件,并绑定到对应的FD上。
(3)当socket有文件事件产生时,I/O 多路复用模块就会将那些产生了事件的套接字fd传送给文件事件分派器。
(4)文件事件分派器接收到I/O多路复用程序传来的套接字fd后,并根据套接字产生的事件类型,将套接字派发给相应的事件处理器来进行处理相关命令操作。
Redis持久化
Redis如何保证数据不丢失?
Redis的读写操作都是在内存中的,所以Redis性能才会高,但是当Redis重启后,内存中的数据就会丢失,为了保证内存中的数据不会丢失,Redis实现了数据持久化机制,这个机制会把数据存储到磁盘中,这样Redis重启就能够从磁盘中恢复原有的数据。
1.AOF日志:每执行一条操作命令,就把该命令以追加写的方式写入到一个文件里。
2.RDB快照:将某一时刻的内存数据以二进制的方式写入到磁盘。
3.混合持久方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点。
AOF日志
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
默认情况下,redis是没有开启AOF(append only file)的。开启AOF功能需要设置配置:appendonly yes。
Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的。这么做其实有两个好处。
- 避免额外的检查开销:因为如果先将写操作命令记录到 AOF 日志里,再执行该命令的话,如果当前的命令语法有问题,那么如果不进行命令语法检查,该错误的命令记录到 AOF 日志里后,Redis 在使用日志恢复数据时,就可能会出错。
- 不会阻塞当前写操作命令的执行:因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。
当然,这样做也会带来风险:
- 数据可能会丢失: 执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险。
- 可能阻塞其他操作: 由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。
AOF三种写回策略
Redis 写入 AOF 日志的过程:
- Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区;
- 然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘;
- 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。
Redis 提供了 3 种写回硬盘的策略,控制的就是上面说的第三步的过程。 在 Redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填:
- Always:同步写回,每个写命令执行完立即同步的将日志写回磁盘
- everysec:每秒写回,每个命令执行完,只是先将日志写到AOF文件的内存缓冲区,每隔1秒把缓冲区中的内容写入磁盘
- no:操作系统控制的写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
AOF重写机制
由于AOF持久化是Redis不断将写命令记录到 AOF 文件中,随着Redis不断的进行,AOF 的文件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间越长。为了解决这个问题,Redis新增了重写机制,当AOF文件的大小超过所设定的峰值时,Redis就会自动启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集或者可以手动使用命令 bgrewriteaof 来重写。
AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
触发机制
重写原理
1:在重写开始前,redis会创建一个"重写子进程",这个子进程会读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。
2:与此同时,主进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可用性,避免在重写过程中出现意外。
3:当"重写子进程"完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中
4:当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中
5:重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似
RDB快照
实现类似照片记录效果的方式,就是把某一时刻的数据和状态以文件的形式写到磁盘上,也就是
快照。这样一来即使故障宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。
这个快照文件就称为RDB文件(dump.rdb)
Redis 提供了两个命令来生成 RDB 文件,分别是 save
和 bgsave
,他们的区别就在于是否在「主线程」里执行:
- 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
- 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;
RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。
Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令,默认会提供以下配置:
save 900 1
save 300 10
save 60 10000
别看选项名叫 save,实际上执行的是 bgsave 命令,也就是会创建子进程来生成 RDB 快照文件。
只要满足上面条件的任意一个,就会执行 bgsave,它们的意思分别是:
- 900 秒之内,对数据库进行了至少 1 次修改;
- 300 秒之内,对数据库进行了至少 10 次修改;
- 60 秒之内,对数据库进行了至少 10000 次修改。
这里提一点,Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。
执行快照时,数据能被修改吗?
可以的,执行 bgsave 过程中,Redis 依然可以继续处理操作命令 的,也就是数据是能被修改的,关键的技术就在于写时复制技术(Copy-On-Write, COW)
混合持久化
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,但是数据恢复不快。
为了集成了两者的优点, Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。
先使用RDB进行快照存储,然后使用AOF持久化记录所有的写操作,当重写策略满足或手动触发重写的时候,将最新的数据存储为新的RDB记录。这样的话,重启服务的时候会从RDB和AOF两部分恢复数据,既保证了数据完整性,又提高了恢复数据的性能。简单来说:混合持久化方式产生的文件一部分是RDB格式,一部分是AOF格式。----》AOF包括了RDB头部+AOF混写
Redis大key对数据持久化的影响?
大key对AOF日志的影响
在使用 Always 策略的时候,主线程在执行完命令后,会把数据写入到 AOF 日志文件,然后会调用 fsync() 函数,将内核缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回。
当使用 Always 策略的时候,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。
当使用 Everysec 策略的时候,由于是异步执行 fsync() 函数,所以大 Key 持久化的过程(数据同步磁盘)不会影响主线程。
当使用 No 策略的时候,由于永不执行 fsync() 函数,所以大 Key 持久化的过程不会影响主线程。
大key对AOF重写和RDB的影响
AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork()
函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程):
- 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
- 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。
Redis事务
Redis事务允许依次执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序的串行化执行而不会被其他命令插入,不允许加塞。
Redis事务和数据库事务的区别
Redis事务命令
事务执行过程是这样的:
- 开始事务(
MULTI
); - 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行);
- 执行事务(
EXEC
)。
情况一:正常执行
情况二:放弃事务
情况三:全体连坐
当事务中有任何一个事务语法出现错误,Redis会直接返回错误,所有命令都不会执行
情况四:冤头债主
当事务前期语法正确,编译通过,但执行exec后报错:事务中对的命令执行,错的命令停止执行
Redis过期删除策略和内存淘汰策略有什么不同?
Redis过期删除策略
Redis 是可以对 key 设置过期时间的,因此需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。
如何判断Rediskey已经过期了?
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。
当我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:
- 如果不在,则正常读取键值;
- 如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。
过期删除策略有哪些?
定时删除
定时删除策略的做法是,**在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。**这样对内存友好,但在过期key较多的情况下回占用相当一部分CPU,对CPU不友好。
定期删除
定期删除策略的做法是,**每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。**并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
惰性删除
惰性删除策略的做法是,**不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。**这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
Redis选择【惰性删除+定期删除】两种策略配合使用,以求在合理使用CPU时间和避免内存浪费之间取得平衡。
Redis持久化时,对过期键如何处理?
AOF
AOF 文件分为两个阶段,AOF 文件写入阶段和 AOF 重写阶段。
1.写入阶段:当Redis以AOF模式持久化是,如果数据库中某个过期键还没被删除,那么AOF文件会保留此过期键,当此过期键被删除后,Redis会像AOF中AOF文件追加一条del命令来显式的删除该键值。
2.重写阶段:执行 AOF 重写时,会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中,因此不会对 AOF 重写造成任何影响。
RDB
RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。
1.生成阶段:从内存中持久化生成RDB文件时,会对key进行过期检查,过期的key不会保存到新的RDB文件中。因此Redis中的过期键不会对新的RDB文件产生影响。
2.加载阶段:RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:
- 主库加载阶段:在加载时会对文件中保存的key进行检查,过期的键不会被载入到数据库中。所以过期键不会对载入 RDB 文件的主服务器造成影响;
- 从库加载阶段:在载入RDB文件时,不论键是否过期都会被 载入到从库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。所以一般来说,过期键对载入 RDB 文件的从服务器也不会造成影响。
Redis内存淘汰策略
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory。
Redis 内存淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。
不进行数据淘汰
- noeviction :它表示当运行内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,会报错通知禁止写入,不淘汰任何数据,但是如果没用数据写入的话,只是单纯的查询或者删除操作的话,还是可以正常工作。
进行数据淘汰
针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。
在设置了过期时间的数据中进行淘汰
- volatile-random :从已设置过期时间的数据集(
server.db[i].expires
)中任意选择数据淘汰。 - volatile-ttl :从已设置过期时间的数据集(
server.db[i].expires
)中挑选将要过期的数据淘汰 - volatile-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
- volatile-lfu(least frequently used):淘汰所有设置了过期时间的键值中,最少使用的键值;
在所有数据范围内进行淘汰
- allkeys-random:随机淘汰任意键值;
- allkeys-lru(least recently used):淘汰整个键值中最久未使用的键值;
- allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。
LRU算法和LFU算法有什么区别?
LRU 全称是 Least Recently Used 翻译为最近最少使用,会选择淘汰最近最少使用的数据。
LFU 全称是 Least Frequently Used 翻译为最近最不常用,LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是"如果数据过去被访问多次,那么将来被访问的频率也更高"。
所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。
文章参考:
1.Redis之I/O多路复用模型实现原理_redis io多路复用_有盐先生的博客-CSDN博客
2.小林coding:图解Redis介绍 | 小林coding (xiaolincoding.com)
4.javaguide:Redis常见面试题总结(下) | JavaGuide(Java面试 + 学习指南)