(3) Redis高性能分析

一、从哪些点分析Redis高性能

1.1 网络通讯(网络IO)

我们都知道Redis是单线程的,那么单线程的Redis为什么能那么快?

要回答这个问题我们需要了解,中间涉及到的环节有哪些:

  1. Redis启动服务,等待客户端连接
  2. Redis收到客户端连接,等待客户端发送操作命令
  3. Redis解析并执行命令
  4. Redis操作并返回结果数据
  5. 客户端收到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表、压缩列表、双向链表、跳表、数组

  1. String : 简单动态字符串
  2. List : 压缩列表、双向链表
  3. Hash : Hash表、压缩列表
  4. ZSet(Sorted Set) : 压缩列表、跳表
  5. Set :Hash表、整数数组

看到上边的对应关系,相信了解数据结构的同学已经心里有数了。明白底层数据结构的特点,我们才能更好的使用,更高效的使用Redis,也能在遇到问题时尽快的想到关键点。

1.2.1.2 Redis键值存储数据结构

在细说数据结构之前,先想一下:Redis基于Key/Value存储数据,那么K/V是用什么结构存储的呢?

为了实现键到值的快速访问,Redis使用了Hash表来保存所有的键值对。

一个Hash表,其实就是一个数组,数组中每个元素称为一个Hash桶,Hash桶中有多个Entry组成,Entry中才是我们真正需要的Key/Value。

Hash.png

这里做一个简单的说明:

  1. 我们在存储数据时,首先需要申请一块儿内存空间,假设是一个长度为16的数组。
  2. 然后要存储一个K、V数据,首先会将K进行Hash取模操作,获得将要存储的数组下标位置。
  3. 此时会创建一个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,过程可以分为三步:

  1. 给hash2分配更大的空间,比如是当前hash1的2倍。
  2. 把hash1中的数据重新映射并拷贝到hash表2中;
  3. 释放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+的大集合... 所以我们在选择数据类型时要谨慎,更要了解其特点(优势、劣势)。

压缩列表

image.png

压缩列表实际上类似于一个数组,数组中每一个元素都对应保存一个数据。和数组不同的是,压缩列表头部有三个字段,zlbytes、zltail和zllen,分别表示列长度、列尾部的偏移量和列表中entry个数,压缩列表在尾部还有一个zlend,表示列表结束。

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头的三个字段直接定位,复杂度为O(1)。而查找其他元素时复杂度就是O(n)了。

思考:压缩列表和双向链表对比,有什么不同?

压缩链表,有长度、尾部偏移量、以及个数统计,在设计到统计的场景比较适合。

跳表

image.png

有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。

具体来说就是在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。

可以通过上图看到,这个查找过程就是在多级索引上跳来跳去,最后定位到元素。当数据量很大时跳表的复杂度就是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时我们还要考虑两个关键问题:

  1. 对哪些数据做快照。这关系到执行的效率。
  2. 做快照时,数据还能被操作么?这个关系到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那样做到秒级呢?

首先我们需要了解,全量快照会造成哪些开销。

  1. 快照会将数据写入磁盘,如果频繁写入,会给磁盘造成压力,如果前一个快照还没有执行完,后一个快照就开始做了,那么两个快照竞争有限的磁盘带宽,速度会更慢,很容易就会形成恶性循环,同时执行3个4个不断的增加...
  2. 虽然快照时bgsave执行,不会影响主线程,但是bgsave是通过主线程fork出来的,主线程fork操作本身会阻塞主线程,而且主线程内存越大,阻塞时间越长,如果频繁fork出bgsave那么主线程就不用玩了,大部分时间都用来生成bgsave了。

因此我们在做快照时不宜太过频繁。

可能有人会说,我们不做全量快照,做增量快照不行么?

在想到这一点的时候,心理咯噔一下,是呀,增量不就快了嘛!!! 但是我们还是要分析一下。

增量的特点是什么,需要有一个状态标记这个数据已经被记录过了,没有新的变动就不需要再记录了。因此我们需要一个状态标记,简单来说就是我们需要一个额外的空间,来存储状态信息,需要一个额外的操作来操作状态信息。

如果我们对每一个键都记录一个状态,那么如果有1W个键,就需要1W调额外的记录,而且键越多,额外的记录也就越多,对于有限的、宝贵的内存资源来说,是否会有点得不偿失。

到这里我们就发现,虽然跟AOF相比,RDB恢复数据速度很快,但是频率又不太好把握,太低很多的数据容易丢失,太高又影响性能。

Redis 4.0中提出了,混合使用RDB和AOF的方法。简单来说就是以一定的频次执行RDB,在两次快照之间使用AOF日志记录所有操作指令。这样一来,快照不用很频繁的执行,AOF也只需要记录快照之间的操作,不会出现文件过大,也能避免重写开销。而且第二次做全量快照时,就可以清空AOF日志。

image.png

默认频率。

3600秒任何一个数据发生一个变化。300秒内,100个数据发生变化,60秒内1W个数据发生变化。

总结

最后简单做一下总结。

  1. 如果数据不能丢失,RDB和AOF混合使用效果最佳。性能一般。
  2. 如果允许分钟级别数据丢失,仅使用RDB即可,效率要高很多。
  3. 如果只用AOF,优先使用everySenc配置选项。

不过建议还是使用RDB+AOF混合使用。虽然恢复上要慢于RDB,但是数据不容易丢,而且又避免了只使用AOF时遇到的AOF文件过大,AOF重写问题。

在AOF于RDB混合使用的前提下,AOF单独使用已经变得比较鸡肋了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • 前言 Redis作为一款高性能数据库,表现在:它接收到一个键值对操作后, 能以微秒级别的速度找到数据,并快速完成操...
    贪睡的企鹅阅读 813评论 0 0
  • 全书的重点在四五六章:如何建表、如何建索引、如何查询。第一章讲解了一些基本概念:锁与事物隔离 重中之重:4.1数据...
    AbrahamW阅读 982评论 0 0
  • 1、基于内存读写 内存的读写速度很快 2、采用的多路复用 Epoll模型 3、高效率的数据结构 常用的五大Redi...
    chanyi阅读 850评论 0 0
  • 点赞再看,养成习惯,搜一搜【一角钱技术[https://upload-images.jianshu.io/uplo...
    一角钱技术阅读 722评论 0 10
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,041评论 0 4