数据库学习之路二:Redis


    Redis是一个由C语言编写的,基于内存,单线程且支持持久化的key-value的NoSQL数据库,其中每个key和value都是使用对象表示的。Redis数据库具有以下特性:
    ● 基于内存运行,性能高;
    ● 支持分布式,理论上可以无限扩展
    ● 采用Key-Value存储方式,且可持久化。

  Q1.MySQL数据路与Redis数据库的区别 ?
    ① Redis是NoSQL,数据暂存在内存中,保存的时间有限;
    ② MySQL是关系型数据库,数据保存在硬盘中,存持久化数据。
  Q2. Redis为什么比MySQL快 ?
    ① Redis完全基于内存,数据存储在内存中。
    ② Redis的数据结构进行了专门的设计,对数据的操作简单。
    ③ Redis采用单线程,避免了上下文切换和竞争条件,不需要设置锁,也不存在多线程之间的切换而消耗CPU。
    ④ Redis 采用多路I/O复用,非阻塞IO


一. Redis 应用场景

   由于Redis是基于内存的,性能高,因此Redis的应用场景如下:
   数据的高并发读写(数据访问频率高),配合MySQL等关系型数据库做高速缓存,缓存高频率访问的数据,从而降低数据库的IO。
在这里插入图片描述


二. Redis 数据结构与底层原理

    在Redis中包含五大数据结构:String(字符串),Hash(哈希),List(列表),Set(集合),Sorted Set(有序集合)。不同的数据结构使用的命令不同,但所有的Redis命令都是原子操作(注意:对命令是原子的,而不是对字段是原子的),避免了多个客户端的操作命令在执行过程中的竞争,但不能避免多个客户端对字段执行操作时的竞争。

2.1 String

2.1.1 String类型
  String(字符串)类型是一个二进制安全的字符串,一个Key对应一个字符串。所谓二进制安全是指字符串不是根据某种特殊的标志来解析的(如C语言中的"\0"),因此无论输入是什么,总能保证输出是处理的原始输入而不是根据某种特殊格式来处理。一个字符串类型的键允许存储数据的最大容量是512MB。
在这里插入图片描述
2.1.2 String类型的应用场景
在这里插入图片描述
   ① 计数器:利用Redis中原子性的自增操作(incr,decr),可以实现「限制一个手机号发多少次短信」,「用户点赞数」,「用户访问数」。
   ② 限速器:限制某个用户访问某个应用的频率,如「抢购时防止用户多次点击带来的访问压力」,「用户下单时多次操作」。
   ③ 分布式锁: 通过set和del操作,在Redis中,加锁就是给Key键设置一个值,解锁就是将该Key键删除。

2.1.3 String类型底层原理
  为了实现扩展、安全和性能,String底层采用的是简单动态字符串 SDS。其数据结构如图所示:
在这里插入图片描述
   通过SDS,Redis的String有以下几个优点:
    ① 获取字符串长度的复杂度为O(1),不需要遍历所有元素。
    ② 不会造成缓冲区的溢出,在执行字符串拼接时,SDS会判断剩余的free空间是否能存下需要拼接的字符串。防止出现C语言中strcat()造成的字符串溢出问题。
    ③ 减少修改字符串带来的内存重分配次数。
    ● 在对SDS字符串进行修改后,若SDS的长度小于1MB时,则SDS会分配一个和len属性相同大小的未使用空间。即free=len
    ● 在对SDS字符串进行修改后,若SDS的长度大于1MB时,则SDS会分配1MB的未使用空间。即free=1MB
    ● 当SDS字符串进行删除时,不会进行内存的重新分配,多余的空间也不会被回收,会留着为字符串再次添加做准备,减少了内存重新分配的次数。
    ④ 字符串的二进制安全。String是根据len属性来判断当前字符串是否结束,而不是以‘\0’作为判断字符串结束的标准。因此字符串输出=字符串输入

2.2 List

2.2.1 List类型
    List类型:该类型是一个简单的字符串列表,一个Key对应一个List,按照插入顺序排序。List基于双链表实现,既可以从头部插入数据,也可以从尾部插入数据。一个列表最多可以包含2的23次方-1 个元素。
在这里插入图片描述
2.2.2 List应用场景
在这里插入图片描述
   ① 消息队列:通过lpop和rpush,实现简单的点对点的消息队列。
   ② 排行榜(非实时计算):将每隔一段时间计算一次的排行榜存储在list中,通过lrange命令可以分页查看队列中的数据。
   ③ 最新列表(非排序):每次通过lpush命令往列表中插入新的元素,然后通过lrange读取最新的元素列表,如朋友圈点赞列表,评论列表。

2.2.3 List类型底层原理
    在Redis 3.2版本之前,List使用两种数据结构实现:压缩列表ziplist和双向链表linkedlist
    1. 压缩列表 ziplist:
    ziplist是为了Redis节约内存而开发的。当元素个数较少时,Redis用ziplist来存储数据,当元素个数超过某个值时,链表键中会把 ziplist 转化为 linkedlist,字典键中会把 ziplist 转化为 hashtable
    ziplist 是由一系列特殊编码的内存块构成的列表(内存连续,但每个元素长度不同,采用变长编码), 一个ziplist 包含多个节点entry。由于ziplist的内存是连续分配的,所以遍历速度很快。
在这里插入图片描述
    2. 双向链表 linkedlist:
    在创建新列表时 redis 默认使用 redis_encoding_ziplist 编码, 当以下任意一个条件被满足时, 列表会被转换成 redis_encoding_linkedlist 编码:
    ● 试图往列表新添加一个字符串值,且这个字符串的长度超过 server.list_max_ziplist_value (默认值为64)。
    ● ziplist 包含的节点超过 server.list_max_ziplist_entries (默认值为512)。
    linkedlist是标准的双向链表,Node节点包含prevnext指针,可以进行双向遍历;同时还保存了 headtail 两个指针,因此,对链表的表头和表尾进行插入的复杂度都为O(1)—— 这是高效实现 LPUSHRPOPRPOPLPUSH 等命令的关键。

    在Redis 3.2版本之后,List的底层通过QuickList来实现。
    QuickList是一个ziplist组成的双向链表。每个节点使用ziplist来保存数据。
在这里插入图片描述

2.3 Hash

2.3.1 Hash类型
    Hash(哈希)类型:该类型是由字段field和关联的value组成的map。一个Key可以对应多个map。其中,fieldvalue都是字符串类型的。Hash类型常用来缓存对象信息,如用户信息,配置信息等。在这里插入图片描述
2.3.2 Hash应用场景
在这里插入图片描述
2.3.3 Hash类型底层原理
    对于Hash类型,其底层数据结构分为两种:ziplisthashtable。当满足以下两个条件时,采用ziplist数据结构,其余都采用hashtable数据结构:
    ● 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节。
    ● 哈希对象保存的键值对数量小于512个。

    1. ziplist数据结构在List中已经介绍过了,这里只介绍hashtable(字典),其数据结构如下图所示:
在这里插入图片描述
  Redis对哈希表的Rehash操作步骤如下:
    Step1. 为ht[1]分配空间,ht[1]哈希表的空间大小取决于操作,以及ht[0]当前包含的键值对的数量,让HashTable同时持有ht[0]和ht[1]两个哈希表。
   ● 扩展:ht[1]的大小为第一个大于等于ht[0].used*2。
   ● 收缩:ht[1]的大小为第一个大于等于ht[0].used/2 。
    Step2. 在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示Rehash工作正式开始。
    Step3. 在Rehash进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除
了执行指定的操作以外,还会将ht[0]哈希表在rehashidx索引上的所有键值对Rehash到ht[1],当Rehash工作完成之后,程序将rehashidx+1(表示下次将Rehash下一个桶)。
    Step4. 随着HashTable操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被Rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示Rehash操作已完成。

2.4 Set

2.4.1 Set类型
    Set类型是String类型的无序集合。集合成员是唯一的,不能出现重复的数据。集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

   Q1. Set 和 List 区别 ?
   ① Set类型元素的是唯一的,而List类型的元素是可以重复。
   ② List适合经常追加数据,插入,删除数据。但随机取数效率比较低;Set适合经常地随机储存,插入,删除。但是在遍历时效率比较低。

在这里插入图片描述
2.4.2 Set类型的应用场景
在这里插入图片描述
2.4.3 Set类型底层原理
   对于Set类型,其底层数据结构分为两种:intsethashtable。当满足下面两个条件时使用intset存储,否则使用hashtable
    ● 集合对象保存的所有元素都是整数值
    ● 集合对象保存的元素数量不超过512个。

   1. intset数据结构如下图所示:
在这里插入图片描述
   为了尽可能地节省内存,会根据插入数据的大小选择不一样的类型(编码方式)来进行存储。当插入的元素超过当前的编码方式时,需要对元素类型(编码方式)从后到前进行升级(2字节->4字节,4字节->8字节),升级后就无法降级。

2.5 Sorted Set

2.5.1 Sorted Set类型
   Sorted Set类型是String类型的有序集合,每个元素都会关联一个double类型的”分数“,Redis根据分数的大小对集合的成员进行排序。Sorted Set的成员是唯一的,但分数可以重复。Sorted Set是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
在这里插入图片描述

   Q1. Sorted Set 和 List 区别 ?
   ① List类型通过双向链表实现,获取靠近两端的数据速度快,而当元素增多后,访问中间数据的速度会较慢,所以List更适合"新鲜事“,”日志“,这类很少访问中间元素的应用。
   ② Sorted Set通过散列表和跳跃表实现,读取中间元素的速度也很快O(logN)。
   ③ List不能调整元素的位置,而Sorted Set可以通过更改元素分数来调整元素的位置。
   ④ Sorted Set 比 List 更耗费内存。

2.5.2 Sorted Set应用场景
    ① 排行榜(实时计算):在有序集合中对指定成员的分数加上增量,增量就是排行榜的热度,如「微博的热搜排行榜」。

2.5.3 Sorted Set类型
   对于Sorted Set类型其底层数据结构是通过跳跃表实现的。跳跃表主要是针对有序链表而提出的快速查找的数据结构。由于有序链表并不能像有序数组那样通过二分查找法来快速查找,因此通过建立索引的方式来提高查询效率。其数据结构如图所示:
在这里插入图片描述
    跳跃表的性质如下:
    ① 每一层都是一个有序的链表,最底层(Level 1)的链表包含所有元素。
    ② 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
    ③ 在Level i (i>1)层的节点,每个节点包含两个指针,一个指向同一链表中的下一个元素,另外一个指向下面一层的元素。
    ④ 每一个节点的层数都是随机算法得出的,插入一个新的节点不会影响其他节点的层数,因此插入操作只需要修改插入节点前后的指针即可,避免了对后续节点的重新调整。
    ⑤ 跳表搜索的时间复杂度平均 O(logN),最坏O(N)。


三. Redis 事务

3.1 事务简介

3.1.1 Redis事务基本概念
   Redis中的事务是一组命令的集合,一个事务中的命令要么都执行,要么都不执行,在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。同时事务中每个命令的执行结果都是最后一起返回的,在执行过程中无法获得某个命令的结果值。即一次性,顺序性,排他性。
   Redis的单条命令是原子性的,但Redis的事务不保证原子性且没有回滚,事务中任意命令执行失败(运行错误,而不是语法错误),其余的命令仍会被执行。如:
在这里插入图片描述
3.1.2 Redis事务的常用命令
   Redis事务通过MULTI命令开始,然后将命令入队,最后只有EXEC执行后,所有入队的命令才会被执行。
在这里插入图片描述
在这里插入图片描述

   Q1. Redis事务命令与普通命令执行上的区别 ?
   ① 非事务状态下的命令以单个命令为单位执行,前一个命令和后一个命令的客户端不一定是同一个;而事务状态则是以一个事务为单位,执行事务队列中的所有命令:除非当前事务执行完毕,否则服务器不会中断事务,也不会执行其他客户端的其他命令。
   ② 在非事务状态下,执行命令所得的结果会立即被返回给客户端;而事务则是将所有命令的结果集合到回复队列,再作为 EXEC 命令的结果返回给客户端。

3.2 Redis事务的应用

3.2.1 Redis事务+Watch 实现CAS(乐观锁)
   在锁机制的划分中,分为乐观锁和悲观锁。
   ● 悲观锁有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。传统的关系型数据库使用这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
在这里插入图片描述
   ● 乐观锁默认假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。
   根据Watch的机制:在事务 EXEC 命令执行时,Redis 会检查被 Watch 的 Key,只有被 Watch 的 Key 从 Watch 起始时至今没有发生过变更,EXEC 才会被执行。如果 Watch 的 Key 在 Watch 命令到 EXEC 命令之间发生过变化,则 EXEC 命令会返回失败。这个过程与CAS的实现效果相同。因此Redis事务+Watch 实现CAS(乐观锁)。

四. Redis 脚本

   对于Redis,可以通过Lua语言编写脚本,并传到Redis中执行。使用脚本的好处是:
     ① 减少网络开销,将原先的多次请求变为只需要一次请求,减少了命令的发送次数。
     ② 原子操作:Redis会将脚本作为一个整体执行,中间不会被其他命令插入,从而避免了竞态竞争。
     ③ 复用:当脚本写好后,后续遇到同样的需求直接调用即可。 在这里插入图片描述


五. Redis 持久化

   由于Redis运行在内存当中,当服务器关闭或Redis重启后,其存储的数据就会丢失,为了是Redis重启后能够恢复数据,需要将Redis持久化。Redis有两种方式来实现持久化:
   ● RDB方式:根据指定的规则,定时将内存中的数据存储到磁盘中。
   ● AOF方式:每次执行命令后将命令本身记录下来。

5.1 RDB 持久化

   Redis启动后会读取RDB快照文件,将数据从磁盘载入到内存。注意:通过RDB快照实现持久化,一旦Redis异常退出,就会丢失最后一次快照以后更改的所有数据。因此需要根据应用场合,设置RDB快照方式,将数据损失控制在能够接受的范围内。

5.1.1 RDB 持久化条件触发方式
   RDB持久化通过快照实现,Redis会在以下几种情况下对数据进行快照:
    ① 根据配置规则进行快照;
    ② 用户执行SAVEBGSAVE命令;
    ● 当执行SAVE命令时,Redis同步进行快照,在快照执行时会阻塞会阻塞所有来自客户端的请求,会导致Redis较长时间未响应。
    ● BGSAVE可以在后台异步进行快照操作,快照执行同时可以继续响应客户端请求。
    ③ 执行FLUSHALL命令:执行FLUSHALL后,Redis会清除所有数据。当自动快照条件不为空时,Redis会执行一次快照操作。
    ④ 执行复制(replication)时:当设置主从模式时。Redis会在复制初始化时进行自动快照。

5.1.2 RDB 快照原理
   通过RDB快照实现持久化时,Redis默认会将快照文件存储在Redis当前进程的工作目录中的dump.rbp文件中,通过配置文件的dirfilename两个参数分别指定快照文件的存储路径和文件名。
  RDB 快照过程如下:
    ① Redis 通过fork()函数复制一份当前进程(父进程)的副本(子进程)。fork()函数使用写时复制(Copy-on-write)策略,因此当父线程改变数据时会发生复制,因此新的RDB文件存储的是执行fork()时那一刻的内存数据。
    ② 父进程负责继续接收并处理客户端的命令请求,而子进程开始将内存中的数据写入磁盘的临时文件。
    ③ 当子进程写完所有数据后,会用该临时文件替换旧的RDB文件,此时一次快照操作完成。
在这里插入图片描述

5.2 AOF 持久化

   通过RDB快照方式持久化时,当异常退出时(进程意外终止时)会导致数据丢失。此时通过AOF持久化来避免这个问题。AOF持久化并不是通过快照内存的数据,而是以协议文本的方式,保存Redis所执行的写命令来记录数据库状态。

5.2.1 AOF 命令同步过程
   AOF 将写入命令记录到AOF文件的过程称为命令同步。同步命令到 AOF 文件的整个过程分为三个阶段:
   ① 命令传播: Redis 将执行完的命令、命令的参数、命令的参数个数等信息发送到 AOF 程序中。
   ② 缓存追加:AOF 程序根据接收到的命令数据,将命令转换为网络通讯协议的格式,然后将协议内容追加到服务器的 AOF 缓存中。
   ③ 文件写入和保存:AOF 缓存中的内容被写入(WRITE)到 AOF 文件末尾,当满足AOF保存条件时, fsync 函数或者 fdatasync 函数会被调用,将写入的内容保存(SAVE)到磁盘中。
在这里插入图片描述
5.2.2 AOF 的保存模式及其性能
   1. AOF三种保存模式:
   ① 不保存(AOF_FSYNC_NO): WRITESAVE都由主进程执行,两个操作都会阻塞主进程。
    ”不保存“模式在调用flushAppendOnlyFile函数后,WRITE都会被执行,但SAVE操作只会在以下一种情况下会被执行:
      ● Redis 被关闭;
      ● AOF功能被关闭;
      ● 系统的写缓存被刷新
   ② 每秒保存一次(AOF_FSYNC_EVERYSEC): WRITE操作由主进程执行,阻塞主进程。SAVE操作由子线程执行,不直接阻塞主进程,但SAVE操作完成的快慢会影响WRITE操作的阻塞时长。
在这里插入图片描述
   ③ 每执行一个命令保存一次(AOF_FSYNC_ALWAYS)
    每执行完一个命令,WRITESAVE都会被执行,但SAVE执行期间,主线程会被阻塞,不能接受命令请求。

   2. AOF保存模式的性能:
在这里插入图片描述
5.2.3 AOF 文件的读取和数据还原
    Redis 读取 AOF 文件并还原数据库的详细步骤如下:
    ① 创建一个不带网络连接的伪客户端。
    ② 读取 AOF 所保存的文本,并根据内容还原出命令、命令的参数以及命令的个数。
    ③ 根据命令、命令的参数和命令的个数,使用伪客户端执行该命令。
    ④ 执行 2 和 3 ,直到 AOF 文件中的所有命令执行完毕。

5.3 RDB 与AOF 持久化区别

    ① Redis 默认开启RDB持久化,RDB持久化适合大规模的数据恢复但它的数据一致性和完整性较差
    ② Redis 需要手动开启AOF持久化方式,默认是每秒将写操作日志追加到AOF文件中。
    ③ AOF 的数据完整性比RDB高,但记录内容多了,会影响数据恢复的效率。
    ④ 若只用Redis 做缓存,可以关闭持久化。若使用Redis的持久化。则可以将RDB和AOF都开启。


六. Redis 集群(Redis Cluster)

    作为小型项目,单个Redis服务器已经足够。但是面对大型的项目与服务:
      ●从结构上,单个Redis服务器容易出现单点故障使服务中断。
      ●从容量上,单个Redis服务器的内存容易出现存储瓶颈。
    因此,通过多台Redis服务器来组成Redis集群来提高性能,这就需要面对如何管理集群的问题。
在这里插入图片描述

6.1 Redis 主从复制

   Q1. 什么是主从复制 ?
    主从复制指的是发生在不同Redis数据库实例之间,单向的信息传播的行为,通常由被复制方和复制方组成,被复制方和复制方之间建立网络连接,复制方式通常为被复制方主动将数据发送到复制方,复制方接收到数据存储在当前实例,最终目的是为了保证双方的数据一致、同步主从复制会贯穿整个主从同步的过程,直至主从关系的终止。
   Q2. Redis主从复制的作用 ?
    主从复制技术使得数据库的读写操作可以分散在运行于不同CPU之上的独立服务器上。其主要作用为:
    ① 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
    ② 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
    ③ 负载均衡在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
    ④ 高可用基石:主从复制还是哨兵和集群能够实施的基础,因此主从复制是Redis高可用的基础。
   Q3. Redis主从复制存在的问题?
    ① Redis主从复制最主要的问题是主从一致性问题,假如主数据库写操作完成,但还未来及到从数据库,读请求又来了,此时读取的数据就不是最新的数据。
    ② Redis 主从复制不具备自动容错和恢复的功能,若是从主同步的过程网络出故障了,导致主从同步失败,也会出现问题数据一致性的问题。

    在主从复制中,数据库分为两类:主库从库主库可以进行读写操作(R/W),当写操作导致数据变化时会自动同步到从库。从库一般为只读操作,并接收来自主库的数据。主从复制存在两种架构:① 一主多从② 链式主从复制
在这里插入图片描述
6.1.1 Redis 主从复制的过程
    ① 当从服务器(slave)启动后会向主服务器(master)发送SYNC命令,master收到命令后通过RDB持久化的BGSAVE命令保存主服务器(master)快照,并且将快照期间主服务器接收到客户端的写命令保存起来(因为快照执行时会阻塞会阻塞所有来自客户端的请求)。
    ② master将保存的快照发送给slave,slave收到主数据库发送过来的快照就会加载到自己的数据库中。在此期间,master会一直接受客户端的命令,立即执行后将结果返回到客户端(即主从复制过程 与 master和客户端交互是异步的)。
    ③ 最后master将缓存的命令同步给slaveslave收到命令后执行一遍,这样masterslave数据就保持一致,即完成了主从复制的初始化。
    ④ 后续master收到写命令后,将命令发送到从slave,保持主从数据库的一致性。
在这里插入图片描述

6.1.2 Redis 主从增量复制
    当主服务器与从服务器之间发生重新连接时,如果进行一次完整的复制过程是没有必要的,因此Redis提出了增量复制,其实现原理如下:
    ① 从数据库会存储主数据库的运行ID(run id,每启动一次Redis实例会产生一个唯一的ID),在复制阶段,主服务器每将一个写命令发送到从服务器时,同时会存放到积压队列中,并记录积压队列中命令的偏移范围。同时从服务器也会记录该命令的偏移范围。
    ② 当主从服务器重新连接后,主服务器首先会判断从服务器发送过来的run id是否与自己的相同,然后判断从服务器中的最后同步的偏移量是否在积压队列中,如果存在则将积压队列中的命令发送给从服务器即可。
在这里插入图片描述

6.1.3 Redis 主从复制的持久化
    当采用主从复制方式时,为了提高性能,在从服务器中开启持久化,而在主服务器关闭持久化。这样会出现两种情况:
    ● 若从服务器崩溃,重启后会从主服务器中自动同步过来。
    ● 若主服务器崩溃,需要手动通过从服务器恢复数据到主服务器:
   ① 在从服务器中通过命令SLAVEOF NO ONE将从服务器提升为主服务器。
   ② 重启之前崩溃的主服务器,并通过命令SLAVEOF将其设置为新的主服务器的从服务器。

6.2 Redis 哨兵(Sentinel)

    由于Redis主从复制不具备自动容错和恢复的功能,因此在主从的基础上,通过哨兵模式(一个独立进程) 对主从服务器的运行状况进行监控,当主服务器出现问题时,自动将slave server转换为master server。
    通常情况下,哨兵机制会建立多个哨兵节点(进程),共同监控数据节点的运行状况,同时各个哨兵之间相互通信,交互主从节点的监控数据,每隔1秒每个哨兵会向整个集群 “master主服务器+slave从服务器+其他sentinel(哨兵)进程”,发送一次ping命令做一次心跳检测。Redis设置哨兵的命令如下: sentinel monitor <master-name主服务器名> <主服务器ip> <主服务器redis-port> <quorum,至少需要quorum个Sentinel进程同意才能将master判断为失效>。在设置哨兵时,只需要对master Server设置哨兵即可,哨兵进程会自动搜寻master server的所有slave server。

在这里插入图片描述
6.2.1 Redis 哨兵的功能
    ● 集群监控:负责监控Server和Slave进程是否正常工作;
    ● 消息通知:当某个实例故障时,发送消息给管理员;
    ● 故障转移:当master server挂掉后,哨兵会自动转移到slave server上,并通知管理员新的master server的地址。同时slave server会与新的master server之间进行全量复制(首次复制);

6.2.2 Redis Master server下线
    当一个master server出现故障时,此时需要所有的哨兵进行“选举投票”,从而Redis哨兵才能知道master server是否出现故障,才能去下线现有master server,将slave server选举为新的master server。这时“投票”会有两个过程:
    ① 主观下线(Subjectively Down,SDOWN)
在这里插入图片描述
    ② 客观下线(Objectively Down,ODOWN):当半数哨兵节点都主观判定主节点出现故障(主观下线),此时多个哨兵节点交换主观判定结果,会判定主节点客观下线。

6.2.3 Redis 选举新的Master server
    当哨兵判定master节点客观下线后,各个哨兵节点中发起投票机制Raft算法(选举算法,最终成为Leader的哨兵节点完成主从自动化切换的过程。
  在Raft算法中,任何时候一个服务器(节点)可以扮演下面角色之一:
    ① Leader: 处理所有客户端交互,日志复制等,在一个系统中只有一个Leader;
    ② Follower:选民,负责提供选票用于选举Leader;
    ③ Candidate:候选人,可以被选举为Leader;
在这里插入图片描述
在这里插入图片描述

6.3 Redis 集群数据分片(区)

    在集群模式实现了Redis数据的分布式存储,通过数据的分片,每个redis节点存储不同的内容,并且解决了在线的节点收缩(下线)和扩容(上线)问题,提高了系统的扩展性,实现了系统的高可用和高性能。
在这里插入图片描述

6.3.1 Redis 数据节点分区原理
    Redis集群对于数据分区采用虚拟哈希槽算法,会把Redis集群分为16384(0~16383)个Hash槽。Hash槽是 Redis 集群管理数据的基本单位,其特点如下:
    ① 由若干个槽组成一个节点,每一个节点负责维护一部分槽以及槽所映射的键值数据。
    ② Hash槽只会分配给每个分区的master主节点上,slave节点不会分配Hash槽,slave节点会同步master上的Hash槽。
    ③ 每个Hash槽可以存放多个Key,每一个数据key对应一个Hash槽。
    ④ Hash槽的目的是确认数据存放到哪个片区的Redis主节点上,实现Redis集群分摊Key。
在这里插入图片描述

   Q1. Redis虚拟Hash槽分区的特点 ?
    ① 解耦数据和节点之间的关系,简化了节点扩容和收缩难度;
    ② 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据;
    ③ 支持节点、槽和键之间的映射查询,用于数据路由,在线集群伸缩等场景。
   Q2. Redis如何判断key对应的槽道号是否由本节点管理? ?
    在Redis的底层维护了unsigned char myslots[CLUSTER_SLOTS/8] 数组存放每个节点的槽信息,该数组是一个16384位的二进制位序列,每一位对应一个槽道号,当该二进制序列的某一位为1时,代表当前结点对该槽道号具有管理权。
    ● 当客户端通过某一节点向集群发送set key value时,该节点会通过CRC16计算Key的Hash值(槽道号),当前结点根据槽道号去自己的二进制序列中去比对,如果该槽道号对应的二进制序列中的那一位为1,则直接将该键值对存储到当前节点中。如:节点1负责存储0~5000的槽数据,但此时只有0-4存储有数据,其他槽还没有数据,则只有0-4对应的值为1.
在这里插入图片描述
    ● 如果该Key不归当前节点存储,则根据clusterNode索引数组,通过槽道号找到该槽道号真正的管理节点,并将信息转交给其真正的管理节点进行保存。
在这里插入图片描述

6.3.2 Redis 数据节点之间的通信
    节点存储数据的同时还需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。Redis集群采用P2P的Gossip(流言)协议,Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息。
    集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,每个节点在固定周期内通过特定规则选择几个节点发送ping消息,接收到ping消息的节点用pong消息作为响应。(节点选择规则为:每秒会随机选取5个节点找出最久没有通信的节点发送ping消息,保证Gossip信息交换的随机性。每100毫秒都会扫描本地节点列表,如果发现节点最近一次接受pong消息的时间大于cluster_node_timeout/2,则立刻发送ping消息,防止该节点信息太长时间未更新)在这里插入图片描述


七. Redis 缓存问题

    Redis缓存在使用过程中,如果设计不当则会造成缓存出现问题,常见的缓存问题主要有:缓存穿透,缓存击穿,缓存雪崩。

7.1 缓存穿透

    缓存穿透是指查询一个不存在的数据,由于缓存是不命中时,出于容错考虑,会导致这个不存在的数据每次请求都要到数据库中去查询,失去了缓存的意义。当查询数量过大时会造成数据库宕机。

   Q1. 如何避免缓存穿透 ?
  避免缓存穿透通常有两种方法:缓存空数据布隆过滤器
   ① 缓存空数据
    发生缓存穿透,是因为缓存中没有存储这些空数据的key,导致这些请求全都打到数据库上,然而数据库中也没有该数据,使得下次请求会继续请求数据库,高并发时会导致数据库宕机。如果将数据库返回的空的查询结果也缓存在Redis中,则下次访问时,Redis会直接返回null,从而避免了查询数据库。
在这里插入图片描述
    采用缓存空数据的方式有以下问题:
    ● 采用空值做为缓存,意味着缓存层中存储了更多的键,需要更多的内存空间 ( 如果是攻击则会导致内存问题 ),因此需要这类数据设置一个较短的过期时间,让其自动剔除。
    ● 缓存层和存储层的数据会有一段时间窗口的不一致,会对业务有一定影响。例如空值的缓存过期时间设置为3分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,因此当存储层数据修改时,需要利用消息系统或者其他方式清除掉缓存层中的空对象。

   ② 布隆过滤器
    布隆过滤器是指在缓存之前再加一道屏障,里面存储目前数据库中存在的所有key的Hash值,当有查询请求的时候,首先去布隆过滤器中查询该key是否存在。若不存在,则说明数据库中也不存在该数据,因此缓存都不要查了,直接返回null。若存在,则继续执行后续的流程,先前往缓存中查询,缓存中没有的话再前往数据库中的查询。
在这里插入图片描述

   Q2. 两种方法有什么区别 ?
   对于恶意攻击,其Key往往不会相同,且数据量很大,对于同一个Key只会被请求一次,采用缓存空值的方式并不会起到保护数据库的作用。因此,对于空数据的key各不相同、key重复请求概率低应当采用布隆过滤器的方式,对于空数据的key数量有限、key重复请求概率较高,可以采用缓存空数据的方式

7.2 缓存击穿 (热点数据缓存过期)

    缓存击穿是指对于某些热点数据缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期会从后端数据库加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端数据库压垮。

  Q1. 如何避免缓存击穿 ?
  避免缓存击穿通常有两种方法:设置互斥锁设置缓存永不失效
    ① 设置互斥锁
    当出现缓存击穿时,缓存数据失效,大量热点数据会直接访问数据库,且在访问数据库后,其结果会重新缓存到Redis中。因此,可以在查询数据库的时候设置互斥锁,只允许一个线程来查询数据库,同时重新建立缓存,从而避免大量数据同时访问数据库。
在这里插入图片描述
    ② 设置缓存永不失效(过期)
    Redis缓存不失效是指从缓存层面来看,对热点Key不设置过期时间,所以不会出现热点key过期缓存击穿的问题。与此同时,为了保证Key数据的缓存更新,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
在这里插入图片描述

7.3 缓存雪崩 (大量缓存同时过期)

    缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致很多缓存在某一时刻同时失效,请求全部转发到数据库,数据库瞬时压力过重雪崩。

   Q1. 如何避免缓存雪崩 ?
   通过将缓存时间分散开,如设置为随机时间等,可以减少或避免缓存雪崩的出现。


八. Redis 内存管理

8.1 内存消耗划分

    Redis将数据保存在内存中,读写效率要比传统的将数据保存在磁盘上的数据库要快很多。所以,监控 Redis 的内存消耗并了解 Redis 内存模型对高效并长期稳定使用 Redis 至关重要。

8.1.1 Redis内存信息
    在Redis-cli客户端中,可以通过info memtory来查看Redis的内存信息。
在这里插入图片描述

   used_memory : 为Redis实际存储数据的内存总量,包括自身内存,对象内存,缓存内存(输入输出缓冲区,复制缓冲区,复制积压缓冲区)和Lua内存。
在这里插入图片描述
   user_memory_rss: 指从操作系统的角度来看,Redis占用的物理内存的总量。user_memory_rss = used_memory + 内存碎片

   mem_fragmentation_ratio: Redis内存碎片率。
    ● mem_fragmentation_ratio < 1: 表示Redis内存分配超出了物理内存,OS正在进行内存交换,内存交换会引起非常明显的响应延迟;
    ● mem_fragmentation_ratio ≈ 1.03 : Redis内存分配在合理区间。
    ● mem_fragmentation_ratio > 1.5 : Redis实际消耗的物理内存远大于存储数据所占用的内存,说明Redis内存碎片增加,内存管理出现问题。

    Q1. 哪些场景会造成大量内存碎片 ?
    Ⅰ. Redis的Key-Value数据变长,且频繁进行数据更新。Redis的每个K-V对初始化的内存大小是最适合的,当修改的Value与原来内存大小不适用的时候,就需要重新分配内存。重新分配之后,就会有一部分内存Redis无法正常回收,一直占用着,造成内存碎片。
    Ⅱ. maxmemory限制导致key被回收删除。Redis写入大量数据,这些数据的Key和存储的不一致,数据超过maxmemory限制后redis会通过Key的内存淘汰机制将部分旧数据淘汰,而被淘汰的数据本身占用的内存却没有被redis进程释放,导致redis内存的有效数据虽然没有超过最大内存,但是整个进程的内存在一直增长。

    maxmemory: Redis可用的最大物理内存,为0时表示没有内存限制。当user_memory_rss > maxmemory时会触发内存淘汰机制。如果占用内存超过服务器内存,会造成内存不足,导致Redis被杀死。


8.2 Redis 过期键删除策略

    在Redis中,可以对Key设置时间,当时间过期后删除Key,如通过EXPIRE/PEXPIRE <key> <time>可以对Key设置过期时间。当Key设置的时间过期后,Redis有三种策略对Redis的过期键进行处理:定时过期惰性过期定期过期

8.2.1 定时删除 (内存友好,CPU不友好)
    定时过期是指在设置键的过期时间的同时,创建一个定时器(timer),让定时器在Key到达过期时间时,立即执行对键的删除操作。但定时过期策略的缺点是:在过期键比较多的情况下,删除过期键可能会占用相当一部分CPU时间,在CPU时间非常紧张的情况下,将CPU时间用在删除和当前任务无关的过期键上,会对服务器的响应时间和吞吐量造成影响。

8.2.2 惰性删除 (内存不友好,CPU友好)
    惰性删除是指当客户端访问这个key的时候,Redis对key的过期时间进行检查,如果过期了就立即删除,不返回任何东西。在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,服务端不会自己去释放它们,那么这些过期键永远也不会被删除(除非手动执行FLUSHDB),从而导致内存泄漏。

8.2.3 定期删除 (设置合适的时长和频率)
    定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。Redis会将每个设置了过期时间的Key放入到一个独立的字典中,并定期扫描这个字典,扫描不会遍历过期字典中所有的key,而是采用贪心策略
    ① 从过期时间字典中随机选取若干个Key;
    ② 删除选取的Key中的过期Key;
    ③ 若过期的Key的比例超过1/4,则重复步骤①。
  因此,定期删除策略的难点是确定删除操作执行的时长和频率,如果删除操作执行得太频繁,就会退化成定时删除策略。如果删除操作执行得太少,会退化成惰性删除。


8.3 内存淘汰策略与算法

8.3.1 内存淘汰策略
    在Redis的内存使用过程中,当user_memory_rss > maxmemory时会触发Redis内存淘汰机制。Redis有6种淘汰策略,默认采用no-enviction策略:
    ① volatile-lru: 使用LRU算法进行数据淘汰(淘汰上次使用时间最早的,且使用次数最少的key),只淘汰设定了过期时间的key 。
    ② allkeys-lru: 使用LRU算法进行数据淘汰,所有的Key都可以被淘汰。
    ③ volatile-random: 随机淘汰数据,只淘汰设定了过期时间的Key。
    ④ allkeys-random: 随机淘汰数据,所有的Key都可以被淘汰。
    ⑤ volatile-ttl: 淘汰剩余过期时间最短的Key。
    ⑥ no-enviction: 不淘汰任何数据,当内存不足时,会返回错误。
    这里需要注意:Redis的内存淘汰机制是通过对Key进行淘汰的,如果被淘汰的数据(Value)本身没有被Redis进程释放,则会造成内存的泄露。

8.3.2 内存淘汰算法 (Redis LRU算法)
    Redis中的LRU与常规的LRU实现并不相同,常规LRU会准确的淘汰掉队头的元素,但是Redis的LRU并不维护队列,Redis LRU算法整体上是一个大的字典dict,key是一个string,而value都会保存为一个robj在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值