Redis一点深入,一篇文章足够你踏入深入的大门

Redis

必看:本文大部分转载于[♥Redis教程 - Redis知识体系详解♥ | Java 全栈知识体系 (pdai.tech)](https://pdai.tech/md/db/nosql-redis/db-redis-overview.html)

如有侵权立马删除,并且发布道歉声明,本文仅为学习使用,并且强烈建议去查看原文,原文比我优秀很多

在这个时代,用户量访问量越来越多,为了提升吞吐量,所以有了缓存,根据所需直接获得目标减少返回,减少无意义的数据量,直接提升响应速度,减少IO次数。Redis在下图为第四分层的缓存。

img

缓存介质

虽然从硬件介质上来看,无非就是内存和硬盘两种,但从技术上,可以分成内存、硬盘文件、数据库。

  • 内存:将缓存存储于内存中是最快的选择,无需额外的I/O开销,但是内存的缺点是没有持久化落地物理磁盘,一旦应用异常break down而重新启动,数据很难或者无法复原。

  • 硬盘:一般来说,很多缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的情况下,可以被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的。

  • 数据库:前面有提到,增加缓存的策略的目的之一就是为了减少数据库的I/O压力。现在使用数据库做缓存介质是不是又回到了老问题上了? 其实,数据库也有很多种类型,像那些不支持SQL,只是简单的key-value存储结构的特殊数据库(如BerkeleyDB和Redis),响应速度和吞吐量都远远高于我们常用的关系型数据库等。

数据类型

Redis一共有九种数据类型,五种基本类型,三种特殊类型,Stream类型

五种基本类型:

String字符串 List列表 Set集合 Hash散列 Zset有序集合

  • String类型是二进制安全的,简单来说所有的数据都可以作为字符串进行保存。

  • List类型在Redis是使用双端链表进行实现的,根据双端链表我们可以根据特性进行更多的设计,如栈等。

  • Set类型是无序的唯一性集合,是使用哈希表来进行实现的,所以复杂度皆为O(1)。

  • Hash散列是一个String类型的Key Value映射表,比较适合储存对象。

  • Zset有序集合根据分数来进行排序,并且是有序但成员唯一的集合。

三种特殊类型

HyperLogLogs(基数统计) Bitmap (位存储) geospatial (地理位置)

  • HyperLogLogs可以非常省内存的去统计各种计数,比如注册 IP 数、每日访问 IP 数、页面实时UV、在线用户数,共同好友数等

  • Bitmap用来解决比如:统计用户信息,活跃,不活跃! 登录,未登录! 打卡,不打卡! 两个状态的,都可以使用 Bitmaps

  • geospatial 用来解决地理位置的保存,可以推算地理位置的信息: 两地之间的距离, 方圆几里的人

Stream类型

Redis5.0 中还增加了一个数据结构Stream,从字面上看是流类型,但其实从功能上看,应该是Redis对消息队列(MQ,Message Queue)的完善实现。

消息队列基本需要的:消息的生产者,消息怎样消费(单播多播,阻塞和非阻塞读取),消息消费有序,消息进行持久化。

数据结构

Redis的每一个数据类型都有一个底层的数据结构,在底层会有一个redisObject,这是一个对象,将所有的不同底层数据封装为这样一个对象,这样直接操作的就是redisObject,与Java中运用对象进行操作的目的基本一致,在里面封装了Type类型,encoding编码,ptr指向实例的指针,Lru记录访问的时间,refcount引用计数等。

以LPOP举一个例子:

img

/*
 * Redis 对象
 */
typedef struct redisObject {
​
    // 类型
    unsigned type:4;
​
    // 编码方式
    unsigned encoding:4;
​
    // LRU - 24位, 记录最末一次访问时间(相对于lru_clock); 或者 LFU(最少使用的数据:8位频率,16位访问时间)
    unsigned lru:LRU_BITS; // LRU_BITS: 24
​
    // 引用计数
    int refcount;
​
    // 指向底层数据结构实例
    void *ptr;
​
} robj;
​

img

type记录了所保存值的类型:

/*
* 对象类型
*/
#define OBJ_STRING 0 // 字符串
#define OBJ_LIST 1 // 列表
#define OBJ_SET 2 // 集合
#define OBJ_ZSET 3 // 有序集
#define OBJ_HASH 4 // 哈希表

encoding记录了对象所保存的值的编码

/*
* 对象编码
*/
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* 注意:版本2.6后不再使用. */
#define OBJ_ENCODING_LINKEDLIST 4 /* 注意:不再使用了,旧版本2.x中String的底层之一. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

下图展示了redisObject 、Redis 所有数据类型、Redis 所有编码方式以及底层数据结构之间的关系

img

底层数据结构的介绍

SDS
SDS对象底层结构

SDS对象,SDS是字符串对象,可以说为简单字符串对象,是Redis为了方便使用而自创的一个字符串数据结构类型。

img

其中sdshdr是头部, buf是真实存储用户数据的地方. 另外注意, 从命名上能看出来, 这个数据结构除了能存储二进制数据, 显然是用于设计作为字符串使用的, 所以在buf中, 用户数据后总跟着一个\0. 即图中 "数据" + "\0" 是为所谓的buf。

img

其中:

len保存了SDS保存字符串的长度

buf[] 数组用来保存字符串的每个元素

alloc分别以uint8, uint16, uint32, uint64表示整个SDS, 除过头部与末尾的\0, 剩余的字节数.

flags始终为一字节, 以低三位标示着头部的类型, 高5位未使用

SDS的好处
  • 常数复杂度获取字符串长度:里面直接记载着字符串的长度len

  • 杜绝缓冲区溢出:为了修改字符串时防止缓冲池超出,会检查len是否超过了长度,如果超过了,先进行空间的拓展,然后再进行修改,防止缓冲池超出。

  • 减少修改字符串的内存重新分配次数:当修改字符串的时候,C语言需要先释放在重新申请数据,而Redis定义的SDS有这额外分配的空间,如果增长的时候没有超过那部分空间,就直接修改,如果超过了,那么空间拓展,在空间拓展的时候,多扩展一部分,也就是所说的空间预分配,如果字符串是减小,那么也保留减少的那一部分,等待后续的使用,这叫惰性空间释放

  • 二进制安全:因为C字符串以空字符作为字符串结束的标识,而对于一些二进制文件(如图片等),内容可能包括空字符串,因此C字符串无法正确存取;而所有 SDS 的API 都是以处理二进制的方式来处理 buf 里面的元素,并且 SDS 不是以空字符串来判断是否结束,而是以 len 属性表示的长度来判断字符串是否结束。

  • 兼容部分 C 字符串函数:虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h>中的一部分函数。

压缩列表 - ZipList
ZipList储存结构

img

  • zlbytes字段的类型是uint32_t, 这个字段中存储的是整个ziplist所占用的内存的字节数

  • zltail字段的类型是uint32_t, 它指的是ziplist中最后一个entry的偏移量. 用于快速定位最后一个entry, 以快速完成pop等操作

  • zllen字段的类型是uint16_t, 它指的是整个ziplist中entry的数量. 这个值只占2bytes(16位): 如果ziplist中entry的数目小于65535(2的16次方), 那么该字段中存储的就是实际entry的值. 若等于或超过65535, 那么该字段的值固定为65535, 但实际数量需要一个个entry的去遍历所有entry才能得到.

  • zlend是一个终止字节, 其值为全F, 即0xff. ziplist保证任何情况下, 一个entry的首字节都不会是255

Entry结构

第一种情况:一般结构 <prevlen> <encoding> <entry-data>

prevlen:前一个entry的大小,编码方式见下文;

encoding:不同的情况下值不同,用于表示当前entry的类型和长度;

entry-data:真是用于存储entry表示的数据;

第二种情况:在entry中存储的是int类型时,encoding和entry-data会合并在encoding中表示,此时没有entry-data字段;

Redis中,在存储数据时,会先尝试将string转换成int存储,节省空间;

此时entry结构:<prevlen> <encoding>

  • prevlen编码

当前一个元素长度小于254(255用于zlend)的时候,prevlen长度为1个字节,值即为前一个entry的长度,如果长度大于等于254的时候,prevlen用5个字节表示,第一字节设置为254,后面4个字节存储一个小端的无符号整型,表示前一个entry的长度;

<prevlen from 0 to 253> <encoding> <entry>      //长度小于254结构
0xFE <4 bytes unsigned little endian prevlen> <encoding> <entry>   //长度大于等于254

  • encoding编码

encoding的长度和值根据保存的是int还是string,还有数据的长度而定;

前两位用来表示类型,当为“11”时,表示entry存储的是int类型,其它表示存储的是string;

简单来说:Entry的结构为<长度><类型><数据>当类型为int的时候,类型与数据结合起来。

ZipList节省内存的原因

ZipList他保存了每个Entry的长度,当遍历的时候就会使用长度来进行得到下一个Entry的起始,并且每一个Entry都按照着自己的长度的内存进行分配,多出的可能就几个字节,其他全是所需要的内存空间,简单来说:在保持必须有的内存空间,尽量的减少了其他所需的内存空间。

缺点:

在Entry数据需要修改的时候,都需要使用内存的分配,时间不占优。并且可能会有连锁的操作,如果prevlen的长度都为1个字节的时候,修改了一个Entry的数据,导置之前的prevlen变为了5个字节,然后又致使前一个长度变化,可能会导置连锁变化。

QuickList

它是一种以ziplist为结点的双端链表结构. 宏观上, quicklist是一个链表, 微观上, 链表中的每个结点都是一个ziplist。

简单来说:就是一个以ZipList为节点的双端链表


字典/哈希表 - Dict

本质上就是Hash表,采用链式地址法来解决Hash冲突的Hash表。


整数集 - IntSet

整数集合(intset)是集合类型的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。


跳表 - ZSkipLis

跳跃表结构在 Redis 中的运用场景只有一个,那就是作为有序列表 (Zset) 的使用。跳跃表的性能可以保证在查找,删除,添加等操作的时候在对数期望时间内完成,这个性能是可以和平衡树来相比较的,而且在实现方面比平衡树要优雅,这就是跳跃表的长处。跳跃表的缺点就是需要的存储空间比较大,属于利用空间来换取时间的数据结构。


img

img

简单来说:类似于B+树索引,不过是简化的,更容易想到,更容易进行实现。

编码

img

字符串对象编码

字符串对象的编码可以是int,raw或者embstr。

  • int 编码:保存的是可以用 long 类型表示的整数值。

  • embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

  • raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

int 编码是用来保存整数值,而embstr是用来保存短字符串,raw编码是用来保存长字符串。

embstr与raw的区别

embstr只需要分配一次空间(redisObject与SDS连续),而raw需要两次(redisObject与SDS),释放空间也是一样的,所以这也算是embstr的优点,但是因为字符串修改的时候,就需要都重新调配空间,所以redis不允许对embstr进行修改,仅为只读。

批注:Redis中对于浮点数类型也是作为字符串保存的,在需要的时候再将其转换成浮点数类型。

编码的转换

当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw。

对于 embstr 编码,由于 Redis 没有对其编写任何的修改程序(embstr 是只读的),在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。

List列表对象

list 列表,它是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边),它的底层实际上是个链表结构。


编码

列表对象的编码是quicklist。 (之前版本中有linked和ziplist这两种编码。进一步的, 目前Redis定义的10个对象编码方式宏名中, 有两个被完全闲置了, 分别是: OBJ_ENCODING_ZIPMAPOBJ_ENCODING_LINKEDLIST。 从Redis的演进历史上来看, 前者是后续可能会得到支持的编码值(代码还在), 后者则应该是被彻底淘汰了)

Hash哈希对象

哈希对象的键是一个字符串类型,值是一个键值对集合。


编码

哈希对象的编码可以是 ziplist 或者 hashtable;对应的底层实现有两种, 一种是ziplist, 一种是dict。

Set集合对象

集合对象 set 是 string 类型(整数也会转换成string类型进行存储)的无序集合。注意集合和列表的区别:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。


编码

集合对象的编码可以是 intset 或者 hashtable; 底层实现有两种, 分别是intset和dict。 显然当使用intset作为底层实现的数据结构时, 集合中存储的只能是数值数据, 且必须是整数; 而当使用dict作为集合对象的底层实现时, 是将数据全部存储于dict的键中, 值字段闲置不用.

集合对象的内存布局如下图所示:

img

有序集合对象

和上面的集合对象相比,有序集合对象是有序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。


编码

有序集合的底层实现依然有两种, 一种是使用ziplist作为底层实现, 另外一种比较特殊, 底层使用了两种数据结构: dict与skiplist. 前者对应的编码值宏为ZIPLIST, 后者对应的编码值宏为SKIPLIST

使用ziplist来实现在序集合很容易理解, 只需要在ziplist这个数据结构的基础上做好排序与去重就可以了. 使用zskiplist来实现有序集合也很容易理解, Redis中实现的这个跳跃表似乎天然就是为了实现有序集合对象而实现的, 那么为什么还要辅助一个dict实例呢? 我们先看来有序集合对象在这两种编码方式下的内存布局, 然后再做解释:

首先是编码为ZIPLIST时, 有序集合的内存布局如下:

img

后是编码为SKIPLIST时, 有序集合的内存布局如下:

img

Redis持久化

redis持久化是为了防止NoSQL数据库突然崩溃,导致数据丢失,需要从数据库中进行重新获取数据,导致数据库压力增加,而做的持久化,崩溃恢复。现在主流的两种持久化方式,也就是RDBAOF


RDB持久化

将数据库的数据进行存储,也就是当前数据库的快照版本,要早于或等于内存中的数据,有两种触发方式:手动触发以及自动触发。

手动触发:Save命令:同步的进行保存 bgSave命令:异步的进行保存(主流)

img

自动触发

  • redis.conf中配置save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件;

  • 主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点;

  • 执行debug reload命令重新加载redis时也会触发bgsave操作;

  • 默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作;

RDB深入理解

RDB在保存数据到硬盘当中的时候,如果在这个时刻进行了修改,Redis是使用Copy-on-Write来进行解决的,所以当在保存的时候,要修改一个数据的时候,就会生成这个数据的副本,RDB文件保存这个数据的副本,而依然对原文件进行修改。

img

在执行快照的时候,当崩溃恢复的时候会使用上一次的RDB文件进行恢复,但会丢弃一些数据量,也就是上一次快照与这一次快照中间所做的修改的数据会丢失。也不能连续进行保存,因为fork子线程很消耗资源,而且会阻塞主线程的存储,所以不建议频繁进行RDB存储。

RDB优缺点

  • 优点

    • RDB文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;

    • Redis加载RDB文件恢复数据要远远快于AOF方式;

  • 缺点

    • RDB方式实时性不够,无法做到秒级的持久化;

    • 每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高;

    • RDB文件是二进制的,没有可读性,AOF文件在了解其结构的情况下可以手动修改或者补全;

    • 版本兼容RDB文件问题;

针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决

AOF持久化

Redis是“写后”日志,Redis先执行命令,把数据写入内存,然后才记录日志。日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。


而AOF日志采用写后日志,即先写内存,后写日志

img

为什么采用写后日志

Redis要求高性能,采用写日志有两方面好处:

  • 避免额外的检查开销:Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。

  • 不会阻塞当前的写操作

但这种方式存在潜在风险:

  • 如果命令执行完成,写日志之前宕机了,会丢失数据。

  • 主线程写磁盘压力大,导致写盘慢,阻塞后续操作。

如何实现AOF

AOF日志记录Redis的每个写命令,步骤分为:命令追加(append)、文件写入(write)和文件同步(sync)。

  • 命令追加 当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。

  • 文件写入和同步 关于何时将 aof_buf 缓冲区的内容写入AOF文件中,Redis提供了三种写回策略:

img

Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;

Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;

No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。

深入理解AOF重写

AOF会记录每个写命令到AOF文件,随着时间越来越长,AOF文件会变得越来越大。如果不加以控制,会对Redis服务器,甚至对操作系统造成影响,而且AOF文件越大,数据恢复也越慢。为了解决AOF文件体积膨胀的问题,Redis提供AOF文件重写机制来对AOF文件进行“瘦身”


  • 图例解释AOF重写

Redis通过创建一个新的AOF文件来替换现有的AOF,新旧两个AOF文件保存的数据相同,但新AOF文件没有了冗余命令。

img

  • AOF重写会阻塞吗

AOF重写过程是由后台进程bgrewriteaof来完成的。主线程fork出后台的bgrewriteaof子进程,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。

所以aof在重写时,在fork进程时是会阻塞住主线程的。

  • AOF日志何时会重写

有两个配置项控制AOF重写的触发:

auto-aof-rewrite-min-size:表示运行AOF重写时文件的最小大小,默认为64MB。

auto-aof-rewrite-percentage:这个值的计算方式是,当前aof文件大小和上一次重写后aof文件大小的差值,再除以上一次重写后aof文件大小。也就是当前aof文件比上一次重写后aof文件的增量大小,和上一次重写后aof文件大小的比值。

  • 重写日志时,有新数据写入咋整

重写过程总结为:“一个拷贝,两处日志”。在fork出子进程时的拷贝,以及在重写时,如果有新数据写入,主线程就会将命令记录到两个aof日志内存缓冲区中。如果AOF写回策略配置的是always,则直接将命令写回旧的日志文件,并且保存一份命令至AOF重写缓冲区,这些操作对新的日志文件是不存在影响的。(旧的日志文件:主线程使用的日志文件,新的日志文件:bgrewriteaof进程使用的日志文件)

而在bgrewriteaof子进程完成会日志文件的重写操作后,会提示主线程已经完成重写操作,主线程会将AOF重写缓冲中的命令追加到新的日志文件后面。这时候在高并发的情况下,AOF重写缓冲区积累可能会很大,这样就会造成阻塞,Redis后来通过Linux管道技术让aof重写期间就能同时进行回放,这样aof重写结束后只需回放少量剩余的数据即可。

最后通过修改文件名的方式,保证文件切换的原子性。

在AOF重写日志期间发生宕机的话,因为日志文件还没切换,所以恢复数据时,用的还是旧的日志文件。

总结操作

  • 主线程fork出子进程重写aof日志

  • 子进程重写日志完成后,主线程追加aof日志缓冲

  • 替换日志文件

温馨提示

这里的进程和线程的概念有点混乱。因为后台的bgreweiteaof进程就只有一个线程在操作,而主线程是Redis的操作进程,也是单独一个线程。这里想表达的是Redis主进程在fork出一个后台进程之后,后台进程的操作和主进程是没有任何关联的,也不会阻塞主线程。

img

  • 主线程fork出子进程的是如何复制内存数据的

fork采用操作系统提供的写时复制(copy on write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成阻塞。fork子进程时,子进程时会拷贝父进程的页表,即虚实映射关系(虚拟内存和物理内存的映射索引表),而不会拷贝物理内存。这个拷贝会消耗大量cpu资源,并且拷贝完成前会阻塞主线程,阻塞时间取决于内存中的数据量,数据量越大,则内存页表越大。拷贝完成后,父子进程使用相同的内存地址空间。

但主进程是可以有数据写入的,这时候就会拷贝物理内存中的数据。如下图(进程1看做是主进程,进程2看做是子进程):

img

在主进程有数据写入时,而这个数据刚好在页c中,操作系统会创建这个页面的副本(页c的副本),即拷贝当前页的物理数据,将其映射到主进程中,而子进程还是使用原来的的页c。

  • 在重写日志整个过程时,主线程有哪些地方会被阻塞

  1. fork子进程时,需要拷贝虚拟页表,会对主线程阻塞。

  2. 主进程有bigkey写入时,操作系统会创建页面的副本,并拷贝原有的数据,会对主线程阻塞。

  3. 子进程重写日志完成后,主进程追加aof重写缓冲区时可能会对主线程阻塞。

  • 为什么AOF重写不复用原AOF日志

两方面原因:

  1. 父子进程写同一个文件会产生竞争问题,影响父进程的性能。

  2. 如果AOF重写过程中失败了,相当于污染了原本的AOF文件,无法做恢复数据使用。

RDB和AOF混合方式(4.0版本)

Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。

这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。

如下图所示,T1 和 T2 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。

img

这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势, 实际环境中用的很多。

从持久化中恢复数据

数据的备份、持久化做完了,我们如何从这些持久化文件中恢复数据呢?如果一台服务器上有既有RDB文件,又有AOF文件,该加载谁呢?


其实想要从这些文件中恢复数据,只需要重新启动Redis即可。我们还是通过图来了解这个流程:

img

  • redis重启时判断是否开启aof,如果开启了aof,那么就优先加载aof文件;

  • 如果aof存在,那么就去加载aof文件,加载成功的话redis重启成功,如果aof文件加载失败,那么会打印日志表示启动失败,此时可以去修复aof文件后重新启动;

  • 若aof文件不存在,那么redis就会转而去加载rdb文件,如果rdb文件不存在,redis直接启动成功;

  • 如果rdb文件存在就会去加载rdb文件恢复数据,如加载失败则打印日志提示启动失败,如加载成功,那么redis重启成功,且使用rdb文件恢复数据;

那么为什么会优先加载AOF呢?因为AOF保存的数据更完整,通过上面的分析我们知道AOF基本上最多损失1s的数据。

哨兵机制

Redis Sentinel,即Redis哨兵,在Redis 2.8版本开始引入。哨兵的核心功能是主节点的自动故障转移。


哨兵实现的功能
  • 监控:监控主节点,从节点是否挂掉

  • 故障转移:转移主从节点,当主节点挂掉之后,将从节点上位

  • 通知:当发生故障进行主从转移时,会对客户端进行通知

  • 配置提供者:当客户端进行哨兵的链接,会提供redis主节点的地址

哨兵相互发现

哨兵相互发现是通过Redis发布订阅的方式来相互注册的,哨兵将自己的地址和端口发到一个频道,其他哨兵通过这个频道得到IP地址与端口,便可以相互连接,哨兵通过这种方式进行互联形成哨兵集群,并且通过网络链接进行通信。

img

哨兵与redis库相连

哨兵向主库发送INFO消息,主库收到了,就像自己从库的地址发送给哨兵,其他哨兵做法也是类似的。

img

哨兵检查下线

哨兵检查redis库是否下线,有两种下线概念:主观下线,客观下线

主观下线:哨兵判断当前的主库是否下线

客观下线:大于等于配置项中的quorum的数目哨兵认为下线了,进行故障转移

流程:当哨兵主观判断主库下线了,这叫主观下线,然后哨兵会发送一个命令,让各个哨兵判断是否下线,如果投票数大于等于配置文件中quorum的数目,则是客观下线,需要故障转移。

哨兵故障转移

哨兵的故障转移很简单,首先在哨兵集群中选出一个领导者,当一位哨兵的投票数超过了当前哨兵数量的二分之一,并且超过了配置文件中quorum的数量,则被选择为故障转移的领导者。然后就是选择新的主库,新的主库判断条件:首先网络状态要良好,哨兵能够进行连接的,然后需要复制的偏移量最大,保存的数据最完整,最后根据文件中的优先度联合起来进行打分,然从中选择出新的主库,选出了新的主库,便进行从库转主库,原主库的从库变为新主库的从库,原主库变为新主库的从库。

img

缓存问题

缓存击穿:

大批量查询查询一个不存在的东西,而查询不到,就会查到数据库层面,缓存的意义就失效了,压力也给到了数据库那一边。

解决办法:

  • 接口层面对ID进行一个基本的判断

  • 没有查到,就在缓存层面添加一个key null的键值对,在查询的话,直接就返回了null值

  • 使用布隆过滤器,布隆过滤器是一个Hash的过滤器,快速判断是否此元素是否存在,如果没有那么就不会被允许过去

缓存击穿

当一个热点key过期了,一下子就有很多数量的查询达到了数据库这一边,数据库承受了很大的压力。

解决办法:

  • 热点key永远不过期

  • 重要的接口,做好限流,注意降级和熔断

  • 只允许一个查询进入数据库,查询完之后,立马加入缓存,然后其他查询进入缓存

缓存雪崩

大量的key同时过期,导致大量的访问到了数据库这一边

解决办法:

  • 设置key的过期时间为随机数字

  • 设置热点key永不过期

  • 分布式将key散在不同的服务器当中

缓存污染

不需要再用到的key,消耗内存空间。

缓存淘汰策略

Redis共支持八种淘汰策略,分别是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。

分三类看:

  • 不淘汰

    • noeviction (v4.0后默认的)

  • 对设置了过期时间的数据中进行淘汰

    • 随机:volatile-random

    • ttl:volatile-ttl

    • lru:volatile-lru

    • lfu:volatile-lfu

  • 全部数据进行淘汰

    • 随机:allkeys-random

    • lru:allkeys-lru

    • lfu:allkeys-lfu

  1. noeviction

该策略是Redis的默认策略。在这种策略下,一旦缓存被写满了,再有写请求来时,Redis 不再提供服务,而是直接返回错误。这种策略不会淘汰数据,所以无法解决缓存污染问题。一般生产环境不建议使用。

其他七种规则都会根据自己相应的规则来选择数据进行删除操作。

2.volatile-random

这个算法比较简单,在设置了过期时间的键值对中,进行随机删除。因为是随机删除,无法把不再访问的数据筛选出来,所以可能依然会存在缓存污染现象,无法解决缓存污染问题。

  1. volatile-ttl

这种算法判断淘汰数据时参考的指标比随机删除时多进行一步过期时间的排序。Redis在筛选需删除的数据时,越早过期的数据越优先被选择。

  1. volatile-lru

LRU算法:LRU 算法的全称是 Least Recently Used,按照最近最少使用的原则来筛选数据。这种模式下会使用 LRU 算法筛选设置了过期时间的键值对。

Redis优化的LRU算法实现

Redis会记录每个数据的最近一次被访问的时间戳。在Redis在决定淘汰的数据时,第一次会随机选出 N 个数据,把它们作为一个候选集合。接下来,Redis 会比较这 N 个数据的 lru 字段,把 lru 字段值最小的数据从缓存中淘汰出去。通过随机读取待删除集合,可以让Redis不用维护一个巨大的链表,也不用操作链表,进而提升性能。

Redis 选出的数据个数 N,通过 配置参数 maxmemory-samples 进行配置。个数N越大,则候选集合越大,选择到的最久未被使用的就更准确,N越小,选择到最久未被使用的数据的概率也会随之减小。

  1. volatile-lfu

会使用 LFU 算法选择设置了过期时间的键值对。

LFU 算法:LFU 缓存策略是在 LRU 策略基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。 Redis的LFU算法实现:

当 LFU 策略筛选数据时,Redis 会在候选集合中,根据数据 lru 字段的后 8bit 选择访问次数最少的数据进行淘汰。当访问次数相同时,再根据 lru 字段的前 16bit 值大小,选择访问时间最久远的数据进行淘汰。

Redis 只使用了 8bit 记录数据的访问次数,而 8bit 记录的最大值是 255,这样在访问快速的情况下,如果每次被访问就将访问次数加一,很快某条数据就达到最大值255,可能很多数据都是255,那么退化成LRU算法了。所以Redis为了解决这个问题,实现了一个更优的计数规则,并可以通过配置项,来控制计数器增加的速度。

参数

lfu-log-factor ,用计数器当前的值乘以配置项 lfu_log_factor 再加 1,再取其倒数,得到一个 p 值;然后,把这个 p 值和一个取值范围在(0,1)间的随机数 r 值比大小,只有 p 值大于 r 值时,计数器才加 1。

lfu-decay-time, 控制访问次数衰减。LFU 策略会计算当前时间和数据最近一次访问时间的差值,并把这个差值换算成以分钟为单位。然后,LFU 策略再把这个差值除以 lfu_decay_time 值,所得的结果就是数据 counter 要衰减的值。

lfu-log-factor设置越大,递增概率越低,lfu-decay-time设置越大,衰减速度会越慢。

我们在应用 LFU 策略时,一般可以将 lfu_log_factor 取值为 10。 如果业务应用中有短时高频访问的数据的话,建议把 lfu_decay_time 值设置为 1。可以快速衰减访问次数。

volatile-lfu 策略是 Redis 4.0 后新增。

  1. allkeys-lru

使用 LRU 算法在所有数据中进行筛选。具体LFU算法跟上述 volatile-lru 中介绍的一致,只是筛选的数据范围是全部缓存,这里就不在重复。

  1. allkeys-random

从所有键值对中随机选择并删除数据。volatile-random 跟 allkeys-random算法一样,随机删除就无法解决缓存污染问题。

  1. allkeys-lfu 使用 LFU 算法在所有数据中进行筛选。具体LFU算法跟上述 volatile-lfu 中介绍的一致,只是筛选的数据范围是全部缓存,这里就不在重复。

allkeys-lfu 策略是 Redis 4.0 后新增。

缓存双写一致性

方案:队列 + 重试机制

img

流程如下所示

  • 更新数据库数据;

  • 缓存因为种种问题删除失败

  • 将需要删除的key发送至消息队列

  • 自己消费消息,获得需要删除的key

  • 继续重试删除操作,直到成功

然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。

方案:异步更新缓存(基于订阅binlog的同步机制)

img

  1. 技术整体思路

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

1)读Redis:热数据基本都在Redis

2)写MySQL: 增删改都是操作MySQL

3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis

  1. Redis更新

1)数据操作主要分为两大块:

  • 一个是全量(将全部数据一次写入到redis)

  • 一个是增量(实时更新)

这里说的是增量,指的是mysql的update、insert、delate变更数据。

2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据

这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值