Redis学习专题

1.Redis内存模型

参考文章:https://www.cnblogs.com/kismetv/p/8654978.html

1.1 Redis数据存储

在这里插入图片描述

  • dictEntry:Redis是Key-Value数据库,因此对每个键值对都会有一个dictEntry,里面存储了指向k和v的指针;next指向下一个dictEntry,与本k-v无关

  • key:键值不是直接以字符串存储,而是存储在SDS结构中

  • redisObject:任何值对象都是以redisObject的数据结构存储的。redisObject中封装了值的类型,值的地址和引用次数以及空转时间。如果值的类型是字符串,仍然是需要通过SDS存储的。

  • jemalloc:无论是dictEntry对象,还是redisObject、SDS对象,都需要内存分配器(如jemalloc)分配内存进行存储。

1.2 jemalloc

redis在编译时便会指定内存分配器;内存分配器可以是libc、jemalloc或者tcmalloc,默认是jemalloc

jemalloc作为Redis的默认内存分配器,在减小内存碎片方面做的相对比较好。jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。

jemalloc划分的内存单元如下图所示:
在这里插入图片描述

1.3 redisObject

Redis对象有五种类型,这五种类型在redisObject对象的成员type中体现,无论哪种类型,Redis都不会直接存储,而是通过redisObject对象进行存储。

redisObject对象非常重要,redis对象的类型、内部编码、内存回收、共享对象等功能,都需要它支持,redis的定义如下:

typedef struct redisObject {
  unsigned type:4;
  unsigned encoding:4;
  unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
  int refcount;
  void *ptr;
} robj;

1.3.1 type

type字段表示对象的类型,占4个比特;目前包括REDIS_STRING(字符串)、REDIS_LIST (列表)、REDIS_HASH(哈希)、REDIS_SET(集合)、REDIS_ZSET(有序集合)。

当我们执行type命令时,便是通过读取RedisObject的type字段获得对象的类型;
在这里插入图片描述

1.3.2 encoding

encoding表示对象的内部编码(而不是数据的编码),占四个比特

对于Redis支持的每种类型,都有至少两种内部编码,例如对于字符串,有int、embstr、raw三种编码。通过encoding属性,Redis可以根据不同的使用场景来为对象设置不同的编码,大大提高了Redis的灵活性和效率。以列表对象为例,有压缩列表和双端链表两种编码方式;如果列表中的元素较少,Redis倾向于使用压缩列表进行存储,因为压缩列表占用内存更少,而且比双端链表可以更快载入;当列表对象元素较多时,压缩列表就会转化为更适合存储大量元素的双端链表。

通过object encoding命令,可以查看对象采用的编码方式
在这里插入图片描述

1.3.3 lru

lru记录的是对象最后一次被命令程序访问的时间,通过对比lru时间与当前时间,可以计算某个对象的空转时间;object idltime命令可以显示该空转时间,时间单位是秒,该指令不改变对象的lru值
在这里插入图片描述
可见set一个object会改变它的空转时间(根据redis版本会不同)
lru值除了通过object idletime命令打印之外,还与redis的内存回收有关系:如果redis打开了maxmemory选项,且内存回收算法是volatile-lru或者allkeys-lru,那么当redis内存占用超过了maxmemory指定的值时,redis会有限选择空转时间最长的对象进行释放。

1.3.4 refcount

refcount与共享对象
refcount记录的是该对象被引用的次数,类型为整型。refcount的作用,主要在于对象的引用计数和内存回收。当创建新对象时,refcount初始化为1;当有新程序使用该对象时,refcount加1;当对象不再被一个新程序使用时,refcount减1;当refcount变为0时,对象占用的内存会被释放。

Redis中被多次使用的对象(refcount>1),称为共享对象。Redis为了节省内存,当有一些对象重复出现时,新的程序不会创建新的对象,而是仍然使用原来的对象。这个被重复使用的对象,就是共享对象。目前共享对象仅支持整数值的字符串对象。

共享对象的具体实现
Redis的共享对象目前只支持整数值的字符串对象。之所以如此,实际上是对内存和CPU(时间)的平衡:共享对象虽然会降低内存消耗,但是判断两个对象是否相等却需要消耗额外的时间。对于整数值,判断操作复杂度为O(1);对于普通字符串,判断复杂度为O(n);而对于哈希、列表、集合和有序集合,判断的复杂度为O(n^2)。

虽然共享对象只能是整数值的字符串对象,但是5种类型都可能使用共享对象(如哈希、列表等的元素可以使用)。

就目前的实现来说,Redis服务器在初始化时,会创建10000个字符串对象,值分别是09999的整数值;当Redis需要使用值为09999的字符串对象时,可以直接使用这些共享对象。10000这个数字可以通过调整参数REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值进行改变。

共享对象的引用次数可以通过object refcount命令查看

1.3.5 ptr

ptr指针指向具体的数据,例如指向某个字符串的sds

1.4 SDS(simple dynamic string)

redis没有简单地使用c字符串作为默认的字符串实现,而是使用了sds。

1.4.1 sds结构

struct sdshdr {
    int len;
    int free;
    char buf[];
};

其中,buf表示字节数组,用来存储字符串;len表示buf已使用的长度,free表示buf未使用的长度。下面是两个例子。
在这里插入图片描述
通过sds结构可以看出来,buf数组的长度 = free+len+1(其中1表示字符串结尾的标识)

1.4.2

SDS在C字符串的基础上加入了free和len字段,带来了很多好处:

  • 获取字符串长度:SDS是O(1),C字符串是O(n)

  • 缓冲区溢出:使用C字符串的API时,如果字符串长度增加(如strcat操作)而忘记重新分配内存,很容易造成缓冲区的溢出;而SDS由于记录了长度,相应的API在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。

  • 修改字符串时内存的重分配:对于C字符串,如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。而对于SDS,由于可以记录len和free,因此解除了字符串长度和空间数组长度之间的关联,可以在此基础上进行优化:空间预分配策略(即分配内存时比实际需要的多)使得字符串长度增大时重新分配内存的概率大大减小;惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小。

  • 存取二进制数据:SDS可以,C字符串不可以。因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而SDS以字符串长度len来作为字符串结束标识,因此没有这个问题。

此外,由于SDS中的buf仍然使用了C字符串(即以’\0’结尾),因此SDS可以使用C字符串库中的部分函数;但是需要注意的是,只有当SDS用来存储文本数据时才可以这样使用,在存储二进制数据时则不行(’\0’不一定是结尾)。

2.Redis对象类型与内部编码

redis支持5种对象类型,每种对象类型都至少有2种编码;这样做的好处:一方面接口与实现分离,当需要增加或者改变内部编码时,用户使用不受影响,另一方面可以根据不同的应用场景切换内部编码,提高效率。

在这里插入图片描述

2.1 字符串

redis里的字符串的长度不能超过512MB ,其中字符串类型的内部编码有三种:

  • int:8个字节的长整型。字符串值是整型时,这个值用long整型表示。
  • embstr: <= 37 字节的字符串。embstr和raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因此redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。 因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。(版本不同,情况有区别)
    在这里插入图片描述
  • raw:大于39个字节的字符串(不同版本有区别的)
  • 在这里插入图片描述

2.2 列表

列表list用来存储多个有序的字符串,每个字符串称为元素;一个列表可以存储2^32-1个元素。Redis中的列表支持两端插入和弹出,并可以获得指定位置和范围的元素,可以充当数组、队列、栈等。

  • ziplist(压缩列表):https://blog.csdn.net/yellowriver007/article/details/79021049 参考文章
    压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构;具体结构相对比较复杂,略。与双端链表相比,压缩列表可以节省内存空间,但是进行修改或增删操作时,复杂度较高;因此当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算。
  • linklist(双端列表):
  • 在这里插入图片描述
    当列表中元素数量小雨512,且列表中所有字符串对象都不足64字节时才采用压缩列表,否则使用双端列表。

2.3 哈希

Redis的外层结构(hash dictEntry的部分)只使用了hashtable,而内层结构使用了ziplist和hashtable两种,压缩列表前面已介绍。与哈希表相比,压缩列表用于元素个数少、元素长度小的场景;其优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于哈希中元素数量较少,因此操作的时间并没有明显劣势。

在这里插入图片描述

  • dictEntry:用来保存键值对

    typedef struct dictEntry{
          void *key;
          union{
              void *val;
              uint64_tu64;
              int64_ts64;
          }v;
          struct dictEntry *next;
      }dictEntry;
    

其中key指向一个sds的引用;val是一个共用体,可以是一个指针 也可以是有符号或者无富豪的64位整型数;next指向下一个dictEntry,用来解决哈希冲突问题,在64位系统中,一个dictEntry占24个字节

bucket是一个数组,数组的每个元素都是指向dictEntry的指针

dictht结构如下:

typedef struct dictht{
    dictEntry *table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
}dictht;

table属性是一个指针,指向bucket;
size属性记录了哈希表的大小,即bucket的大小;
used记录了已使用的dictEntry的数量;
sizemask属性的值总是为size-1,这个属性和哈希值一起决定一个键在table中存储的位置。

当哈希表中的元素数量小于512值且每个键值字符串长度都小于64字节才使用压缩列表,否则用哈希表

2.4 集合

set中的元素是无序的,且不能重复,set还提供取交集,取并集、差集的操作

  • intset 其中,encoding代表contents中存储内容的类型,虽然contents(存储集合中的元素)是int8_t类型,但实际上其存储的值是int16_t、int32_t或int64_t,具体的类型便是由encoding决定的;length表示元素个数。

整数集合适用于集合所有元素都是整数且集合元素数量较小的时候,与哈希表相比,整数集合的优势在于集中存储,节省空间;同时,虽然对于元素的操作复杂度也由O(1)变为了O(n),但由于集合数量较少,因此操作的时间并没有明显劣势。

typedef struct intset{
    uint32_t encoding;
    uint32_t length;
    int8_t contents[];
} intset;

当集合中元素小于512且所有元素都是整数才会使用intset,否则转变位hashtable

2.5 有序集合

有序集合元素也不能重复,但是提供一个基于score元素作为排序一句

内部编码使用ziplist或者skiplist,跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。除了跳跃表,实现有序数据结构的另一种典型实现是平衡树;大多数情况下,跳跃表的效率可以和平衡树媲美,且跳跃表实现比平衡树简单很多,因此redis中选用跳跃表代替平衡树。跳跃表支持平均O(logN)、最坏O(N)的复杂点进行节点查找,并支持顺序操作。Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成:前者用于保存跳跃表信息(如头结点、尾节点、长度等),后者用于表示跳跃表节点。具体结构相对比较复杂,略。

跳表专题https://www.cnblogs.com/Elliott-Su-Faith-change-our-life/p/7545940.html
这个在后面再做详细介绍

当有序集合中元素数量小于128个且成员长度都不足64字节才会使用压缩列表,否则使用跳跃表。
在这里插入图片描述

3.Redis内存优化

  • 利用jemalloc特性进行优化:由于jemalloc分配内存时数值是不连续的,因此key/value字符串变化一个字节,可能会引起占用内存有很大的变动。
  • 使用整型/长整数:如果是整型/长整数,redis会使用int类型来代替字符串,可以节约更多的空间。因此在可以使用长整型/整型代替字符串的场景下,尽量使用长整型/整型。
  • 共享对象:利用共享对象,可以减少对象的创建(同时减少了redisObject的创建),节省内存空间。可以调整REDIS_SHARED_INTEGERS参数提高共享对象的个数。
  • 关注内存碎片率,如果redis内存碎片率过高(jemalloc在1.03左右比较正常),说明内存碎片多,内存浪费严重;这时便可以考虑重启redis服务,在内存中对数据进行重排,减少内存碎片。如果内存碎片率小于1,说明redis内存不足,部分数据使用了虚拟内存(swap);由于虚拟内存的存取速度要比物理内存速度查很多,此时redis的访问就会变得很慢 ,因此必须设法增大物理内存(可以增加服务器节点数量,或者提高单机内存),来减少redis里的数据。
  • 另外设置合理的数据回收策略也很重要,当内存达到一定量后,根据不同的优先级对内存进行回收。

4.Redis持久化

参考资料:https://www.cnblogs.com/kismetv/p/9137897.html

在Redis中,实现高可用的技术主要包括持久化、复制、哨兵和集群。

4.1 RDB

RDB持久化是将当前进程中的数据生成快照保存到硬盘,保存的文件后缀是.rdb;当redis重新启动时,可以读取快照文件恢复数据。

4.1.1 触发条件

1)手动触发

  • save命令和bgsave命令都可以生成RDB文件,但是save会阻塞redis主进程,直到rdb文件创建完毕,在redis服务器阻塞期间,不能处理任何命令,由于这一原因基本不用。而bgsave命令会创建一个子进程,由子进程来负责创建RDB文件,父进程则继续处理请求。bgsave命令执行过程中,只有fork子进程时会短暂阻塞服务器。

2)自动触发

  • save m n:当时间到m秒时 如果redis数据发生了至少n次变化 那么就触发
    其中时通过serverCron函数,dirty计数器和lastsave时间戳来实现的,save m n的原理如下:每隔100ms,执行serverCron函数;在serverCron函数中,遍历save m n配置的保存条件,只要有一个条件满足,就进行bgsave。对于每一个save m n条件,只有下面两条同时满足时才算满足:

(1)当前时间-lastsave > m

(2)dirty >= n

  • 主从复制场景下,如果从节点执行全量复制操作,则主节点会执行bgsave命令,并将rdb文件发送给从节点
  • 执行shutdown命令时,自动执行rdb持久化。

4.1.2 执行流程

在这里插入图片描述
图片中的5个步骤所进行的操作如下:

  1. Redis父进程首先判断:当前是否在执行save,或bgsave/bgrewriteaof(后面会详细介绍该命令)的子进程,如果在执行则bgsave命令直接返回。bgsave/bgrewriteaof 的子进程不能同时执行,主要是基于性能方面的考虑:两个并发的子进程同时执行大量的磁盘写操作,可能引起严重的性能问题。

  2. 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的,Redis不能执行来自客户端的任何命令

  3. 父进程fork后,bgsave命令返回”Background saving started”信息并不再阻塞父进程,并可以响应其他命令

  4. 子进程创建RDB文件,根据父进程内存快照生成临时快照文件,完成后对原有文件进行原子替换

  5. 子进程发送信号给父进程表示完成,父进程更新统计信息

4.1.3 其它

  • redis默认采用LZF算法对RDB文件进行压缩。虽然压缩耗时,但是可以大大减小RDB文件的体积,因此压缩默认开启;可以通过命令关闭。并且该压缩不是针对整个文件,而是对存储的字符串进行的,并且只有当字符串的长度到达20字节才会进行。

  • RDB文件的载入工作时在服务器启动时自动执行的,并没有专门的命令

  • 常用配置:

    • save m n:bgsave自动触发的条件;如果没有save m n配置,相当于自动的RDB持久化关闭,不过此时仍可以通过其他方式触发
    • stop-writes-on-bgsave-error yes:当bgsave出现错误时,Redis是否停止执行写命令;设置为yes,则当硬盘出现问题时,可以及时发现,避免数据的大量丢失;设置为no,则Redis无视bgsave的错误继续执行写命令,当对Redis服务器的系统(尤其是硬盘)使用了监控时,该选项考虑设置为no
    • rdbcompression yes:是否开启RDB文件压缩
    • rdbchecksum yes:是否开启RDB文件的校验,在写入文件和读取文件时都起作用;关闭checksum在写入文件和启动文件时大约能带来10%的性能提升,但是数据损坏时无法发现
    • dbfilename dump.rdb:RDB文件名
    • dir ./:RDB文件和AOF文件所在目录

4.2 AOF

4.2.1 概述

aof相当于把redis的写命令追加到一个文件中,类似mysql的binlog,redis服务器默认开启rdb关闭aof,如果要开启aof,要在配置文件中配置 appendonly yes

4.2.2 执行流程

由于需要记录Redis的每条写命令,因此AOF不需要触发,下面介绍AOF的执行流程。

AOF的执行流程包括:

  • 命令追加(append):将Redis的写命令追加到缓冲区aof_buf;
  • 文件写入(write)和文件同步(sync):根据不同的同步策略将aof_buf中的内容同步到硬盘;
  • 文件重写(rewrite):定期重写AOF文件,达到压缩的目的。

1)命令追加 append

Redis先将写命令追加到缓冲区,而不是直接写入文件,主要是为了避免每次有写命令都直接写入硬盘,导致硬盘IO成为Redis负载的瓶颈。

命令追加的格式是Redis命令请求的协议格式,它是一种纯文本格式,具有兼容性好、可读性强、容易处理、操作简单避免二次开销等优点;具体格式略。在AOF文件中,除了用于指定数据库的select命令(如select 0 为选中0号数据库)是由Redis添加的,其他都是客户端发送来的写命令。

2)文件写入(write)和文件同步(sync)

Redis提供了多种AOF缓存区的同步文件策略,策略涉及到操作系统的write函数和fsync函数,说明如下:

为了提高文件写入效率,在现代操作系统中,当用户调用write函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失;因此系统同时提供了fsync、fdatasync等同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。

AOF缓存区的同步文件策略由参数appendfsync控制,各个值的含义如下:

  • always:命令写入aof_buf后立即调用系统fsync操作同步到AOF文件,fsync完成后线程返回。这种情况下,每次有写命令都要同步到AOF文件,硬盘IO成为性能瓶颈,Redis只能支持大约几百TPS写入,严重降低了Redis的性能;即便是使用固态硬盘(SSD),每秒大约也只能处理几万个命令,而且会大大降低SSD的寿命。

  • no:命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步;同步由操作系统负责,通常同步周期为30秒。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证。

  • everysec:命令写入aof_buf后调用系统write操作,write完成后线程返回;fsync同步文件操作由专门的线程每秒调用一次。everysec是前述两种策略的折中,是性能和数据安全性的平衡,因此是Redis的默认配置,也是我们推荐的配置。

3) 文件重写(rewrite)

随着时间流逝,Redis服务器执行的写命令越来越多,AOF文件也会越来越大;过大的AOF文件不仅会影响服务器的正常运行,也会导致数据恢复需要的时间过长。

文件重写是指定期重写AOF文件,减小AOF文件的体积。需要注意的是,AOF重写是把Redis进程内的数据转化为写命令,同步到新的AOF文件;不会对旧的AOF文件进行任何读取、写入操作!

关于文件重写需要注意的另一点是:对于AOF持久化来说,文件重写虽然是强烈推荐的,但并不是必须的;即使没有文件重写,数据也可以被持久化并在Redis启动的时候导入;因此在一些实现中,会关闭自动的文件重写,然后通过定时任务在每天的某一时刻定时执行。

文件重写之所以能够压缩AOF文件,原因在于:

  • 过期的数据不再写入文件
  • 无效的命令不再写入文件:如有些数据被重复设值(set mykey v1, set mykey v2)、有些数据被删除了(sadd myset v1, del myset)等等
  • 多条命令可以合并为一个:如sadd myset v1, sadd myset v2, sadd myset v3可以合并为sadd myset v1 v2 v3。不过为了防止单条命令过大造成客户端缓冲区溢出,对于list、set、hash、zset类型的key,并不一定只使用一条命令;而是以某个常量为界将命令拆分为多条。这个常量在redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD中定义,不可更改,3.0版本中值是64。

重写的触发
1)手动:直接调用bgrewriteaof命令,该命令的执行与bgsave有些类似:都是fork子进程进行具体的工作,且都只有在fork时阻塞。

2)自动:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数,以及aof_current_size和aof_base_size状态确定触发时机。

  • auto-aof-rewrite-min-size:执行AOF重写时,文件的最小体积,默认值为64MB。
  • auto-aof-rewrite-percentage:执行AOF重写时,当前AOF大小(即aof_current_size)和上一次重写时AOF大小(aof_base_size)的比值。

重写的流程
在这里插入图片描述

  1. Redis父进程首先判断当前是否存在正在执行 bgsave/bgrewriteaof的子进程,如果存在则bgrewriteaof命令直接返回,如果存在bgsave命令则等bgsave执行完成后再执行。前面曾介绍过,这个主要是基于性能方面的考虑。

  2. 父进程执行fork操作创建子进程,这个过程中父进程是阻塞的。

3.1) 父进程fork后,bgrewriteaof命令返回”Background append only file rewrite started”信息并不再阻塞父进程,并可以响应其他命令。Redis的所有写命令依然写入AOF缓冲区,并根据appendfsync策略同步到硬盘,保证原有AOF机制的正确。

3.2) 由于fork操作使用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然在响应命令,因此Redis使用AOF重写缓冲区(图中的aof_rewrite_buf)保存这部分数据,防止新AOF文件生成期间丢失这部分数据。也就是说,bgrewriteaof执行期间,Redis的写命令同时追加到aof_buf和aof_rewirte_buf两个缓冲区。

  1. 子进程根据内存快照,按照命令合并规则写入到新的AOF文件。

5.1) 子进程写完新的AOF文件后,向父进程发信号,父进程更新统计信息,具体可以通过info persistence查看。

5.2) 父进程把AOF重写缓冲区的数据写入到新的AOF文件,这样就保证了新AOF文件所保存的数据库状态和服务器当前状态一致。

5.3) 使用新的AOF文件替换老文件,完成AOF重写。

4.2.3 其它

前面提到过,当AOF开启时并且aof文件存在时,Redis启动时会优先载入AOF文件来恢复数据;只有当AOF关闭时,才会载入RDB文件恢复数据。

常用配置:

  • appendonly no:是否开启AOF
  • appendfilename “appendonly.aof”:AOF文件名
  • dir ./:RDB文件和AOF文件所在目录
  • appendfsync everysec:fsync持久化策略
  • no-appendfsync-on-rewrite no:AOF重写期间是否禁止fsync;如果开启该选项,可以减轻文件重写时CPU和硬盘的负载(尤其是硬盘),但是可能会丢失AOF重写期间的数据;需要在负载和安全性之间进行平衡
  • auto-aof-rewrite-percentage 100:文件重写触发条件之一
  • auto-aof-rewrite-min-size 64mb:文件重写触发提交之一
  • aof-load-truncated yes:如果AOF文件结尾损坏,Redis启动时是否仍载入AOF文件

4.3 对比

4.3.1 优缺点

1)RDB持久化:

优点:RDB文件紧凑,体积小,网络传输快,适合全量复制;恢复速度比AOF快很多。当然,与AOF相比,RDB最重要的优点之一是对性能的影响相对较小。

缺点:RDB文件的致命缺点在于其数据快照的持久化方式决定了必然做不到实时持久化,而在数据越来越重要的今天,数据的大量丢失很多时候是无法接受的,因此AOF持久化成为主流。此外,RDB文件需要满足特定格式,兼容性差(如老版本的Redis不兼容新版本的RDB文件)。

2)AOF持久化:

与RDB持久化相对应,AOF的优点在于支持秒级持久化、兼容性好,缺点是文件大、恢复速度慢、对性能影响大。

4.3.2 策略选择

(1)如果Redis中的数据完全丢弃也没有关系(如Redis完全用作DB层数据的cache),那么无论是单机,还是主从架构,都可以不进行任何持久化。

(2)在单机环境下(对于个人开发者,这种情况可能比较常见),如果可以接受十几分钟或更多的数据丢失,选择RDB对Redis的性能更加有利;如果只能接受秒级别的数据丢失,应该选择AOF。

(3)但在多数情况下,我们都会配置主从环境,slave的存在既可以实现数据的热备,也可以进行读写分离分担Redis读请求,以及在master宕掉后继续提供服务。

在这种情况下,一种可行的做法是:

master:完全关闭持久化(包括RDB和AOF),这样可以让master的性能达到最好

slave:关闭RDB,开启AOF(如果对数据安全要求不高,开启RDB关闭AOF也可以),并定时对持久化文件进行备份(如备份到其他文件夹,并标记好备份的时间);然后关闭AOF的自动重写,然后添加定时任务,在每天Redis闲时(如凌晨12点)调用bgrewriteaof。

这里需要解释一下,为什么开启了主从复制,可以实现数据的热备份,还需要设置持久化呢?因为在一些特殊情况下,主从复制仍然不足以保证数据的安全,例如:

master和slave进程同时停止:考虑这样一种场景,如果master和slave在同一栋大楼或同一个机房,则一次停电事故就可能导致master和slave机器同时关机,Redis进程停止;如果没有持久化,则面临的是数据的完全丢失。

master误重启:考虑这样一种场景,master服务因为故障宕掉了,如果系统中有自动拉起机制(即检测到服务停止后重启该服务)将master自动重启,由于没有持久化文件,那么master重启后数据是空的,slave同步数据也变成了空的;如果master和slave都没有持久化,同样会面临数据的完全丢失。需要注意的是,即便是使用了哨兵(关于哨兵后面会有文章介绍)进行自动的主从切换,也有可能在哨兵轮询到master之前,便被自动拉起机制重启了。因此,应尽量避免“自动拉起机制”和“不做持久化”同时出现。

(4)异地灾备:上述讨论的几种持久化策略,针对的都是一般的系统故障,如进程异常退出、宕机、断电等,这些故障不会损坏硬盘。但是对于一些可能导致硬盘损坏的灾难情况,如火灾地震,就需要进行异地灾备。例如对于单机的情形,可以定时将RDB文件或重写后的AOF文件,通过scp拷贝到远程机器,如阿里云、AWS等;对于主从的情形,可以定时在master上执行bgsave,然后将RDB文件拷贝到远程机器,或者在slave上执行bgrewriteaof重写AOF文件后,将AOF文件拷贝到远程机器上。一般来说,由于RDB文件文件小、恢复快,因此灾难恢复常用RDB文件;异地备份的频率根据数据安全性的需要及其他条件来确定,但最好不要低于一天一次。

3.Redis数据结构及时间复杂度

3.1 时间复杂度

参考文章:https://www.cnblogs.com/liuqingsha3/p/10972106.html

只说特殊的

1)string:大部分都是O(1) MSET和MGET这种时间复杂度是O(N) N是操作的数量

2)list:大部分是O(1) 其中LINDEX(返回某个index上的元素)、LRANGE(返回某个返回的元素)、LINSERT(指定位置插入)是O(N),以上三个方法要慎用。

3)Hash:大部分是O(1),其中HGETALL(返回所有的k-v)、HKEYS(返回所有的k)、HVALS(返回所有的v)时间复杂度是O(N)

4)Set:大部分是O(1),如果是批量处理就是O(N),其中N是操作的元素数量,另外SMEMBERS(返回hash中所有成员)、SUNION(并集)、SUNIONSTORE(并集并存储在另一个set中)、SINTER(交集)、SINTERSTORE(交集并存储在另一个set中)、SDIFF(差集)、SDIFFSTORE(差集并存储在另一个SET中)的时间复杂度是O(N),以上操作要慎用

5)Sort Set:大部分是O(logN),批量操作是O(MlogN),其中M是操作的数量,N是已有数据的规模,另外ZRANGE\ZREVRANGE(升序、降序返回排名范围内所有的member)、ZRANGEBYSCORE/ZREVRANGEBYSCORE(返回指定score返回呃逆所有的member)、ZREMRANGEBYRANK/ZREMRANGEBYSCORE(移除Sorted Set中指定排名范围/指定score范围内的所有member)的时间复杂度O(log(N)+M)

4.面试相关

4.1 跳表

参考资料:https://www.cnblogs.com/Elliott-Su-Faith-change-our-life/p/7545940.html

1)跳表和平衡二叉树的区别:跳表是一种可以替代平衡树的数据结构。跳表追求的是概率性平衡,而不是严格平衡。因此,跟平衡二叉树相比,跳表的插入和删除操作要简单得多,执行也更快。

2)Redis里的跳表:p为层数概率取的0.25,每次根据此概率随机计算节点落在哪一层,经过计算平均每个节点只用维护1.33个指针,比二叉树2个指针要少,另外最坏情况下跳表的时间复杂度O(N),平均时间复杂度是O(logN)

3)一致性哈希算法和虚拟节点
参考资料:https://www.cnblogs.com/lpfuture/p/5796398.html
https://blog.csdn.net/z15732621582/article/details/79121213

4)缓存穿透:一般的缓存系统,都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统查找(比如DB)。一些恶意的请求会故意查询不存在的key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。

1:对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。

2:对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。

5)缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,会给后端系统带来很大压力。导致系统崩溃。

1:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

2:做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期

3:不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值