一、从哪些点分析Redis高性能
1.1 网络通讯(网络IO)
我们都知道Redis是单线程的,那么单线程的Redis为什么能那么快?
要回答这个问题我们需要了解,中间涉及到的环节有哪些:
- Redis启动服务,等待客户端连接
- Redis收到客户端连接,等待客户端发送操作命令
- Redis解析并执行命令
- Redis操作并返回结果数据
- 客户端收到Redis返回结果
...
这里需要注意两个需要“等待”的地方。试想,如果在单线程中,Redis启动服务后,需要阻塞等待客户端连接,因为不阻塞的话,后续的操作就没有意义,在客户端连接后,也需要等待客户端的指令(没有客户端指令不知道该干啥~)。这两个步骤都是需要阻塞的,那么试想,假如在单线程中,有两个客户端1,客户端2需要连接Redis,客户单1先建立连接,建立后并不发送指令,Redis收到客户端1的连接后又阻塞在等待客户端1发送指令的环节,这时客户端2建立连接,立刻发送指令,此时Redis服务是“单线程”,且阻塞在等待客户端1的发送指令位置处,无法及时的响应客户端2请求。
public static void main(String[] args) {
ServerSocket serverSocket=null;
try {
serverSocket=new ServerSocket(8080);
System.out.println("启动服务:监听端口:8080");
// 表示阻塞等待监听一个客户端连接,返回的socket表示连接的客户端信息
while(true) {
// 连接阻塞
Socket socket = serverSocket.accept();
System.out.println("客户端:" + socket.getPort());
// inputstream是阻塞的(***) 读取客户端指令报文
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String clientStr = bufferedReader.readLine();
System.out.println("收到客户端发送的消息:" + clientStr);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("receive a message:" + clientStr + "\n");
bufferedWriter.flush();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(serverSocket!=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
这种情况很显然不合理,这么搞就没人用Redis了。
这里先解释一下,Redis单线程是指Redis网络IO的单线程和键值对读写是由一个线程来完成的。其他的功能例如持久化、异步删除、集群数据同步等是由额外的线程执行的。
通常来说单线程的处理能力都要比多线程差很多,但是Redis缺能使用单线程达到每秒10W级别的处理能力,原因是什么呢?这是Redis多方面设计选择的一个综合结果。
一方面,Redis的主要操作都是基于内存完成(数据读写),再加上高效的数据结构hash、跳表操作起来更加的快速。试想,我们使用多线程提示处理能力的情况是什么呢?通常是由于某个点处理时间太久,且后续操作与本次操作并无太大关联,可以同时进行。
举个栗子:一个厨师做一个菜需要5分钟,此时中午高峰期客户点了10个菜,1个厨师做那就是50分钟,10个师傅做就是5分钟,100个师傅做可能也需要3~4分钟(不考虑其他问题,别扯炉子)。这里试想,100个师傅没有起到期望作用的原因在哪里呢?因为一个菜炒熟就是需要3分钟,任务已经没办法分解了,在加人的效果已经微乎其微了,那么为什么1个到10个效率变化这么大呢?因为有10个菜呀!!!有100个菜那就100个人了。想明白这两点,就能理解Redis为什么使用单线程了。
对于Redis而言,由于直接操作内存,没有寻址,写磁盘等一些耗时操作,而且使用高效的数据结构,已经很快了。假设操作一条数据是1毫秒(实际更快),单线程1秒就能搞1000次,多线程可能搞N*1000次,我已经不能再拆分了,而且我现在1秒就1000多一点的操作需求,再给我加线程没有意义呀,我还得考虑多线程并发线程安全问题,没有收益呀,我自己干就好了...于是Redis使用了单线程。
因此我们这里重点关注,网络IO的性能、以及数据读写操作的性能即可,这里先分析网络IO,IO决定了Redis能够接受多大的操作需求,下边数据结构会说读写操作的高性能。
1.1.1 网络IO模型
提到网络IO,可能有同学就知道,BIO(阻塞IO),NIO(非阻塞IO)。1.1部分的代码就是阻塞 IO,性能很低,如果只有BIO,Redis就更不需要多线程了。这部分会说明从BIO-NIO-多路复用的演进。
1.1代码看过后就知道,阻塞的地方主要是两个,抛开链接阻塞不谈(Soket内部已经处理了),在等待读取指令部分的阻塞,我们可以使用多线程的方式来解决。这样后连接的客户端便不会受限于先连接的客户端。
static ExecutorService executorService= Executors.newFixedThreadPool(10);
public static void main(String[] args) {
ServerSocket serverSocket=null;
try {
serverSocket=new ServerSocket(8080);
System.out.println("启动服务:监听端口:8080");
//表示阻塞等待监听一个客户端连接,返回的socket表示连接的客户端信息
while(true) {
Socket socket = serverSocket.accept(); //连接阻塞
System.out.println("客户端:" + socket.getPort());
//IO变成了异步执行
executorService.submit(new SocketThread(socket));
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(serverSocket!=null){
try {
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
class SocketThread implements Runnable{
private Socket socket;
public SocketThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
//inputstream是阻塞的(***) //表示获取客户端的请求报文
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String clientStr = bufferedReader.readLine();
System.out.println("收到客户端发送的消息:" + clientStr);
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
bufferedWriter.write("receive a message:" + clientStr + "\n");
bufferedWriter.flush();
}catch (Exception e){
e.printStackTrace();
}finally {
//TODO 关闭IO流
}
}
}
这时想必也看出来了,使用线程优化后,每个连接都需要启动一个线程,线程资源又是有限的,此时就受限于线程的数量。想要支撑10W的请求肯定不现实。
那么试想,我需要为每一个Socket连接创建一个线程么?我需要创建的这个线程什么都不干,就一直等着不知道隔多久才会发送一次的客户端指令么?很显然,这里就是一种资源的浪费,客户端指令并非一直在发送,这时我何不用有限的资源去处理呢?比如客户端建立连接后,为其申请建立一个Buffer空间,有指令发送过来时先存储在Buffer中,使用线程在N个链接的buffer中查看,发现有待处理的数据,便回调通知主线程接收处理。这样节省了不必要的线程资源。
多路复用便是采用这种思想,Redis网络框架调用epoll机制,让内核监听这些Socket。此时Redis线程不会阻塞在某一个特定的监听或者已经链接的Socket上,也就是说不会阻塞在客户端请求处理上。此外为了在请求到达时能够通知到Redis,Select/Epoll提供了基于事件的回调机制,针对不同事件的发送,调用相应的处理函数。
1.2 数据存储(缓存)
1.2.1 数据结构
1.2.1.1 Redis 数据类型和底层数据结构关系
Redis中的数据类型我们基本都知道,但是数据类型和底层数据结构的关系是什么样的呢?
不同的数据结构都有其特点,也都有优缺点,那么我们如何选择数据结构来存储数据?又如何在选择使用一种数据类型存储我们需要的数据时,避免一些慢操作的坑呢?
简单动态字符串、hash表、压缩列表、双向链表、跳表、数组
- String : 简单动态字符串
- List : 压缩列表、双向链表
- Hash : Hash表、压缩列表
- ZSet(Sorted Set) : 压缩列表、跳表
- Set :Hash表、整数数组
看到上边的对应关系,相信了解数据结构的同学已经心里有数了。明白底层数据结构的特点,我们才能更好的使用,更高效的使用Redis,也能在遇到问题时尽快的想到关键点。
1.2.1.2 Redis键值存储数据结构
在细说数据结构之前,先想一下:Redis基于Key/Value存储数据,那么K/V是用什么结构存储的呢?
为了实现键到值的快速访问,Redis使用了Hash表来保存所有的键值对。
一个Hash表,其实就是一个数组,数组中每个元素称为一个Hash桶,Hash桶中有多个Entry组成,Entry中才是我们真正需要的Key/Value。
这里做一个简单的说明:
- 我们在存储数据时,首先需要申请一块儿内存空间,假设是一个长度为16的数组。
- 然后要存储一个K、V数据,首先会将K进行Hash取模操作,获得将要存储的数组下标位置。
- 此时会创建一个Entry对象来进行存储K/V。注意这里存储的不是具体的K/V值,而是指针,或者说是对象值的引用,也可以称为索引。同时由于为了防止Hash冲突,所以都是采用数组+链表的形式,所以Entry中会有Next指针。
这里可以思考一下,为什么不直接存数据,而是要存指针?如果存的是数据,那么我们是不是可以立刻就能拿到并返回,存指针还需要再进行一次查找,才能获得数据。
1.2.1.3Hash冲突与Rehash
对于Redis来说,Hash表存储了全部的键值对,所以也叫作全局Hash表,他的时间复杂度为O(1),我们只需要计算一次Hash值便能轻松的获得数据,而且这种Hash计算与数据量的大小无关,不管Hash表中有10W或者100W个键,我们还是只需要1次就能找到存储的下标位置。
了解到这一步,相信对Redis大量数据依然可以快速查找的原因就明白了,但是,当你往Redis中写入大量数据后,就可能发现操作有时候会突然变慢,这时你可能就会一头雾水了。明明复杂度为O(1)为啥就突然慢了呢?是不是偶发的?这会是网络不好吧?相信有些同学会从外部找原因了~
但是实际上hash表有一个潜在的风险点,就是Hash冲突,和Rehash可能带来的操作阻塞。
上边我们说Hash表是由数组+链表组成,链表就是用来解决Hash冲突的,也叫作Hash冲突链,或者链式Hash。在发生冲突时使用链表的方式来进行存储,这样在Hash取模计算找到下标后,遍历链表也能快速的找到存储的数据。
但是如果链表中的数据非常的多,那么速度自然就会变慢。怎么解决这种问题呢?就是通过Rehash来解决。
试想,为什么Hash冲突会变多,在什么样的情况下回变多。如果Hash表数组长度为6,跟数组长度为16的情况下,哪种Hash冲突情况较多。很明显,是数组越大,越能避免冲突。
因此我们在往Redis写大量数据后,在Key达到一定数量时,Redis一定会扩容Hash表,进行Rehash操作,来保证Hash操作的O(1)复杂度。
对于Rehash来说,其实就是重新散列。增加现有的Hash桶的数量(数组长度),让逐渐增多的entry元素能够在更多的桶之间分散保存,减少单个桶中Entry元素的数量。
Redis中,为了使rehash的操作更高效,采用的两个hash表:hash1/hash2一开始,刚插入数据时默认使用hash1、此时hash2并未分配空间 ,随着数据的增多,Redis开始rehash,过程可以分为三步:
- 给hash2分配更大的空间,比如是当前hash1的2倍。
- 把hash1中的数据重新映射并拷贝到hash表2中;
- 释放hash1的空间
此时我们就可以从hash1,切换到hash2,将hash1留下作为下次的rehash扩容备用。
这个过程中第二步涉及到大量的数据拷贝,比较耗时,那么在拷贝数据的过程中,如果想要一次拷贝,那么势必要阻塞用户请求,等到Copy完成后,才能将hash2给用户使用。这种肯定会影响用户使用,那么应该采用什么样的方式比较合理呢?
Redis采用了渐进式的rehash。简单来说就是在第二部拷贝数据的时候,Redis正常处理客户端请求,每处理一个请求时,从hash表1中的第一个索引位置开始,顺带将索引位置上所有的entries拷贝到hash表2中,等处理下一个请求时,再顺带拷贝hash表中下一个索引位置的entries。这样就巧妙的将一次性的大量拷贝开销,分摊到多次处理请求中,避免了耗时操作,保证数据的快速访问。
1.2.1.4 思考
这里可以思考一下: hash1和hash2是怎么实现切换的?是等rehash执行完毕后切换?还是逐步的切换?
个人认为:是逐步的切换,拷贝一个索引位置,做一个已拷贝的标记。后续请求落在该索引位置时,自动去hash2中再次进行hash计算取值。 如果是执行完后切换,按照上一段说明的处理方式,在索引位置1拷贝完毕后,由于未全部拷贝,此时仍然使用hash1,后续有新增修改操作落在索引位置1上时,就会造成数据不一致问题(这个问题也可以解决,就是拷贝完后,对该索引位置数据的操作,再记录一份日志,等到所有的数据拷贝完后,再同步过程中间的数据变动,继续执行修改...在写操作多的情况下...很难想象其优点,以及执行时间)。
假设使用渐进式,那么在Copy完成之前,正式切换hash2之前每次请求都要在返回后,多加一步拷贝操作,虽然不影响本次请求,但会影响下次请求响应时间,类似于插队,而且 如果命中已拷贝的数据,还需要到hash2中进行操作。这种情况下,操作肯定要变慢。这里有没有其他的优化方法呢?
最后,由于Redis需要进行rehash,所以如果value不是指针而是真实的数据的话,那么势必也要进行数据的拷贝!!!这种更加耗时,因此entry中存的都是指针。
最最后,我们知道通过key获取值由于使用了全局hash表所以复杂度为O(1),那么对于String类型来说,查到之后就可以直接进行数据操作了。对于集合类型来说,查到之后还需要在集合中进行下一步的操作,比如一个List类型数据,如果是10W+的大集合... 所以我们在选择数据类型时要谨慎,更要了解其特点(优势、劣势)。
压缩列表
压缩列表实际上类似于一个数组,数组中每一个元素都对应保存一个数据。和数组不同的是,压缩列表头部有三个字段,zlbytes、zltail和zllen,分别表示列长度、列尾部的偏移量和列表中entry个数,压缩列表在尾部还有一个zlend,表示列表结束。
在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头的三个字段直接定位,复杂度为O(1)。而查找其他元素时复杂度就是O(n)了。
思考:压缩列表和双向链表对比,有什么不同?
压缩链表,有长度、尾部偏移量、以及个数统计,在设计到统计的场景比较适合。
跳表
有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。
具体来说就是在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。
可以通过上图看到,这个查找过程就是在多级索引上跳来跳去,最后定位到元素。当数据量很大时跳表的复杂度就是O(logN)。
总结
上边我们学习了,Redis底层的数据结构,包括了Redis用来保存键值的全局hash表结构。也包括了支持集合类型实现的双向链表、压缩列表、整数数组、hash表和跳表五大底层结构。
Redis之所以能够快速操作键值,一方面是因为O(1)复杂度的哈希表被广泛的使用,包括String、hash和set,他们的操作复杂度是由哈希表决定的,另一方面,Sorted Set也采用了O(logN)复杂度的跳表,不过集合类型的范围操作,因为要遍历数据结构,复杂度通常为O(N)。因此,应该因地制宜的使用List类型,例如主要使用POP/PUSH队列场景,而不是一个随机读写的集合。
Redis数据类型丰富,每个类型的操作繁多,通常无法一下就记住所有操作的复杂度,所以最好的方法就是掌握原理,你可以看到,一旦掌握了数据结构的基本原理,就可以推断出不同操作的复杂度。
数据持久化
Redis我们通常会将其当做缓存来适应,把数据库中的热点数据存储在内存中,然后直接从内存中读取数据,响应速度会非常快。但是服务器一旦宕机,内存的数据将全部丢失。这时我们的数据要怎么恢复呢?如果从后端数据库恢复数据,需要频繁的访问数据库,而且从数据库中读取性能肯定要慢Redis很多,导致程序响应变慢,进而影响用户使用,甚至在请求量大的时候还会造成更加严重的问题。所以对于Redis来说,实现数据的持久化,避免从后端数据库中进行恢复也是至关重要的
AOF (避免数据丢失)
AOF记录的是写后日志,Redis是先执行命令,把数据写入内存,然后才会记录日志。
思考:为什么要写后日志而不是写前日志?举一些写前日志的例子。
首先我们要先知道AOF日志中记录的到底是什么信息。传统数据库日志,例如redo log记录的是修改后的数据,而AOF中记录的是Redis收到的每一条命令,以文本方式保存。也就是说AOF中记录的是客户端所有的请求指令。
AOF回写策略
AOF的两个风险
既然AOF是写后日志,那么,肯定存在如果数据写入内存成功,没有来得及写AOF服务器宕机了,数据肯定是会丢失的,如果只是作为缓存,那么还可以从数据库进行读取,如果作为数据库,因为没有记录日志,所以就无法用日志进行恢复了。
前边我们说了Redis是单线程,网络IO和数据读写都由一个线程来执行,AOF作为一个写后日志,其实也是由主线程来执行的,所以AOF虽然不会对当前命令造成影响,但是会对下一个操作带来阻塞风险,如果在把日志文件写入磁盘时,磁盘读写压力大,就会导致写盘很慢,进而导致后续操作无法执行。
这两个风险其实都是跟写盘相关,这也就意味着,如果我们能够控制一个写命令执行完后AOF日志写回磁盘的时机,这两个风险就解除了。
AOF对回写的时机提供了三种策略选择。
- Always 同步写回。每个命令执行完,立刻同步写回磁盘;
- EverySec 每秒写回。每个命令执行完,先写AOF内存缓冲区,每隔1秒把缓冲区写入磁盘
- NO 操作系统控制,每个写命令执行完,只是先把日志写到AOF缓冲区,由操作系统决定何时写盘(30s)
第一种写回同步,可以做到基本不丢数据,但是他在写一个命令后都有一个慢速落盘的动作,不可避免的会影响主线程性能,与直接操作数据库类似了。
每秒写回,采用一秒写回一次的频率,性能高于同步写回,但也会造成一秒的数据丢失。
操作系统控制写回,虽然性能提升的很多,但是丢数据的风险也变得不可控。(配合集群使用,也可降低风险)
总之策略选择都是一种权衡。
在选择完合适的策略后,我们的AOF就开始工作了,随着运行时间,我们的AOF文件也会越来越大,这也就意味着,我们要小心AOF文件过大带来的性能问题。
这里的性能问题主要是三个方面:一是,文件系统本身对文件大小有限制,无法保存过大的文件。二是,如果文件太大,之后再往里面追加命令记录的话,效率也会降低。三是,如果发生宕机,AOF文件过大,恢复时间会很长,影响Redis正常使用。
可想,Redis想要通过AOF的方式来进行持久化,进行故障恢复,肯定是需要做一些优化的。那么我们可以想一下,是否每一条指令都需要执行呢?很显然不是,只需要对操作成功的指令进行记录即可,执行失败的没有必要记录。甚至,查询操作也没必要记录。这样文件大小肯定会小很多。因此AOF使用写后日志方式。
另外虽然这样已经简化了一部分大小,但是对于同一个key的三个不同set命令,会记录三遍AOF日志,而我们需要的只是最后一个结果,所以这里也有很大的优化空间。AOF针对这个问题,使用了重写机制,使用多转1的形式对文件进行压缩。
AOF重写会阻塞么
首先AOF重写肯定是不能阻塞的,否则主线程就没法玩了。和AOF的日志写回不同,重写的过程是由后台子进程bgrewriteAOF来完成的。
重写的过程可以总结为一个拷贝,两处日志。
一个拷贝是指,在每次进行重写时,主线程fork出后台的bgrewriteaif子进程。此时,fork会把主线程的内存,拷贝一份给bgrewriteaof子进程,这里面就包含了当前内存的最新数据。然后,子进程就在不影响主线程的情况下,逐一把拷贝的内存数据写成操作,记入重写日志。
两处日志,就是指因为主线程未阻塞,仍然可以处理新来的操作。此时如果有写操作,第一处日志就是正在使用的AOF日志,redis会把写操作写到他的缓冲区。这样原AOF的数据仍然是齐全的,即使宕机也可以用原AOF文件进行恢复。
第二处日志是指新的AOF重写日志,这个操作也会西重写到新的日志缓冲区,等到拷贝的数据操作记录重写完成后,缓冲区的这些后来的操作日志也会写入新的AOF文件,以保证数据库最新状态的记录,我们就可以用新的AOF文件代替旧的文件了。
RDB (数据快速恢复)
首先,在AOF部分我们提到了,AOF通过存储操作指令的方式来进行数据持久化,避免数据丢失,以及数据的故障恢复,但是由于记录的是操作指令,而不是实际的数据,所以AOF在进行故障恢复的时候,需要逐一把操作指令日志都执行一遍,如果日志非常多,Redis就会恢复的很慢,影响到正常使用。所以AOF在故障恢复场景不是理想的结果。
RDB就是为了实现快速恢复而出现的另外一种持久化方式,内存快照。所谓内存快照,就是指内存数据在某一时刻的状态记录。对于Redis来说,他把内存某一时刻的状态以文件的形式写到磁盘上。这样一来,及时宕机,快照也不会丢失,这个快照文件就是RDB文件,就是Redis DataBase的缩写。
和AOF相比,RDB记录的是某一时刻的数据,并不是操作,所以在数据恢复时,我们可以直接把RDB文件读入内存,很快的完成恢复。
为什么说RDB要比AOF快呢?还记得AOF重写是怎么做的么?需要fork一个子进程,然后Copy一份内存给子进程,也就说对内存的Copy是非常的快的。
那这么说的话,故障恢复是不是用RDB就可以了呢?其实RDB也并不是最优选项。
在使用RDB时我们还要考虑两个关键问题:
- 对哪些数据做快照。这关系到执行的效率。
- 做快照时,数据还能被操作么?这个关系到Redis是否需要阻塞,是否还可以正常处理请求。
对哪些数据做快照
Redis的数据都存在内存中,为了提供所有数据的可靠性保证,他执行的是全量快照,会把内存中所有数据记录到磁盘中。
Redis提供了两个命令来生成RDB文件,分别是save 和 bgsave。
- save 表示在主线程执行,会导致阻塞。
- bgsave 表示创建子进程执行,专门写RDB文件。
做快照时数据还能被操作么
在给别人拍照时,一旦对方动了,照片就会模糊了。所以我们当然希望对方保持不动。对于内存快照而言,肯定也希望内存数据不动。
但是对于快照执行期间数据不能修改,是会有潜在问题的,假如Redis中有两个G的数据,磁盘写入宽带是0.2GB/S,简单来说,至少需要10s才能完成,如果快照期间数据不能被修改,那么10秒钟用户无法正常使用Redis,数据再多的话,时间也会越久,而且RDB文件执行频率也需要保证,否则无法及时恢复,因此系统可用性将大大降低。
为了快照而暂停写操作肯定是不能接受的,所以这个时候,Redis就会借助操作系统提供的写时复制技术(Copy-On-Write,COW),在执行快照同时正常处理写操作。
简单来说就是在RDB执行期间,写操作修改一个数据,那么会对这个数据进行一个复制,生成一个副本。然后bgsave子线程会把这个副本数据写入RDB文件。相当于在RDB文件执行期间,针对写数据,多了一个生成副本的操作。虽然也会影响性能,但是相比于暂停写操作来说要好太多。
多久做一次快照呢?
这里再强调一下,RDB在故障恢复时效率高于AOF,因此比较适合做故障恢复。那么既然要做故障恢复,就不得不提一下,RDB快照的频次,如果1分钟一次,每次恢复要丢1分钟数据。如果每次修改就做一次快照,很明显也不现实,毕竟RDB是全量快照。那么能不能像AOF那样做到秒级呢?
首先我们需要了解,全量快照会造成哪些开销。
- 快照会将数据写入磁盘,如果频繁写入,会给磁盘造成压力,如果前一个快照还没有执行完,后一个快照就开始做了,那么两个快照竞争有限的磁盘带宽,速度会更慢,很容易就会形成恶性循环,同时执行3个4个不断的增加...
- 虽然快照时bgsave执行,不会影响主线程,但是bgsave是通过主线程fork出来的,主线程fork操作本身会阻塞主线程,而且主线程内存越大,阻塞时间越长,如果频繁fork出bgsave那么主线程就不用玩了,大部分时间都用来生成bgsave了。
因此我们在做快照时不宜太过频繁。
可能有人会说,我们不做全量快照,做增量快照不行么?
在想到这一点的时候,心理咯噔一下,是呀,增量不就快了嘛!!! 但是我们还是要分析一下。
增量的特点是什么,需要有一个状态标记这个数据已经被记录过了,没有新的变动就不需要再记录了。因此我们需要一个状态标记,简单来说就是我们需要一个额外的空间,来存储状态信息,需要一个额外的操作来操作状态信息。
如果我们对每一个键都记录一个状态,那么如果有1W个键,就需要1W调额外的记录,而且键越多,额外的记录也就越多,对于有限的、宝贵的内存资源来说,是否会有点得不偿失。
到这里我们就发现,虽然跟AOF相比,RDB恢复数据速度很快,但是频率又不太好把握,太低很多的数据容易丢失,太高又影响性能。
Redis 4.0中提出了,混合使用RDB和AOF的方法。简单来说就是以一定的频次执行RDB,在两次快照之间使用AOF日志记录所有操作指令。这样一来,快照不用很频繁的执行,AOF也只需要记录快照之间的操作,不会出现文件过大,也能避免重写开销。而且第二次做全量快照时,就可以清空AOF日志。
默认频率。
3600秒任何一个数据发生一个变化。300秒内,100个数据发生变化,60秒内1W个数据发生变化。
总结
最后简单做一下总结。
- 如果数据不能丢失,RDB和AOF混合使用效果最佳。性能一般。
- 如果允许分钟级别数据丢失,仅使用RDB即可,效率要高很多。
- 如果只用AOF,优先使用everySenc配置选项。
不过建议还是使用RDB+AOF混合使用。虽然恢复上要慢于RDB,但是数据不容易丢,而且又避免了只使用AOF时遇到的AOF文件过大,AOF重写问题。
在AOF于RDB混合使用的前提下,AOF单独使用已经变得比较鸡肋了。