【2024】Java知识点汇总----Redis篇

前言

java知识点汇总🍅
在这里插入图片描述

redis篇

Redis是一个开源的的内存数据结构存储系统,可用做数据库、缓存和消息消息中间件,支持多种数据类型。适用于各种应用场景,尤其适用于需要高性能读写操作的场景、例如缓存、会话管理、排行榜等等。

而Redis之所以这么快主要是基于一下特点:

  • 基于内存实现(相对于磁盘来说,读写速度不是一个量级)
  • 高效的数据结构(hash结构)
  • 合理的数据编码(redis有多种数据类型,并且每个数据类型针对不同场景也有不同的底层数据结构编码)
  • 合适的线程模型(多路复用模型)

1、数据类型

1.1、String:字符串

String是Redis中最基础的数据类型,也是在实际应用中使用最多的数据类型。底层的数据结构实现主要有INT和SDS(简单动态字符串)两种数据结构实现。

  • INT:以long类型存储,用来存储一些可以直接转换成整数型的数据,并且可以直接进行加减操作运算

  • SDS:SDS的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的ArrayList,采用预分配冗余空间的方式来减少内存空间的的频繁分配,

    1. **动态调整大小:SDS可以根据字符串的长度动态调整内存大小,**当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
    2. **SDS 获取字符串长度的时间复杂度是 O(1):**因为内部有len字段维护了字符串长度信息,因此不管长度时多少都可以直接获取长度。并且可以有效的防止缓冲区溢出。
    3. **SDS不仅可以保存文本数据,还可以保存二进制数据:**SDS的所有API都会以处理二进制的方式处理SDS存储在buf[]中的数据。所以SDS不仅可以保存文本数据,还可以保存图片、视频等二进制数据。

    SDN结构如下:

    struct sdshdr {
        int len;         // 已使用的长度
        int free;        // 剩余的可用空间
        char buf[];      // 实际保存字符串的地方
    };
    

字符串对象内部编码有3种:intrawembstr

字符串对象和编码以及底层数据结构关系图如下:

在这里插入图片描述

如果字符串对象保存的是一个整数值,并且这个整数值可以用long类型来表示,则编码类型采用int类型;如果字符串对象保存的字符串则会使用SDS来保存这个字符串,

如果这个字符串的长度小于等于32字节则会采用embstr 编码(embstr编码是专门用于保存短字符串的一种优化方式);如果字符串长度大于32字节那么编码方式会变为raw

应用场景:

  • 缓存:直接缓存值
  • 计数器:因为Redis是单线程,所以执行的过程是原子的,因此可以用作计数器来记录点击量、点赞数等等。
  • 分布式锁:SET有一个NX参数,可以实现只有在[key不存在时才可以插入]
  • 共享session信息:在处理会话时,一般需要通过session保存用户登陆信息。单系统的话可以直接保存在服务器。但分布式系统则不行,会出现重复登陆,则需要借助Redis进行统一管理和存储

1.2、List:数组

主要用于存储有序的字符串列表,可以从列表的两端添加或删除元素

底层数据结构在3.2版本以前则采用的是LinkedListZipList,而在3.2版本以后则使用quicklist代替了双端链表和压缩链表**。**

  • LinkedList(双端列表):在数据较多时采用双端链表,双端表允许我们在O(1) 时间内从头部和尾部进行插入和删除操作

  • **ZipList(压缩链列表):**是一段连续的内存空间,将多个元素顺序存储在一个紧凑的结构中。它类似于一个数组,但元素之间没有明确的分隔符,而是通过特殊的编码方式来区分。

    在这里插入图片描述

  • **quicklist(快速列表):**底层是由zipList和LinkedList组合的混合体,外部是一个LinkedList内部是存储的是一个zipList;相当于是由多个zip使用双向指针串接起来

    在这里插入图片描述

应用场景:

  • 消息队列:利用list都PUSH 操作,将任务存储在list中,然后工作线程在通过POP操作取出。要注意的一点,list的队列并不会主动的去通知消费者有新消息写入,如果消费者想要及时处理消息,则需要不停的去调用POP命令,有新消息就读取到,没有就读取到空值。

    在这里插入图片描述

  • 所以Redis提供了一个BRPOP命令,可以实现阻塞式读取。

    在这里插入图片描述

  • 也可以用作消息排队、栈等先进先出的需求等

1.3、Set:集合

Set类型是一种无序、不重复的集合,集合中的元素没有先后顺序但都是唯一,有点类似于java的HashSet。主要可用作存储一个列表数据,又不希望出现重复数据时,Set是一个很好的选择。并且Set还可以判断某个元素是否在一个Set集合内。Set还可以支持多个集合之间的并集、差集、交集操作

底层数据结构主要分两种,如果元素都是整数并且小于一定范围则使用IntSet来存储;如果不满足前面的条件则会用HashTable来进行存储。

  • IntSet(整数集合):使用整数集合来存储成员,适用于所有数据都是整数的小集合

    在这里插入图片描述

  • dict(字典表)字典表主要是通过hash函数将元素映射到桶中的数据结构。当不满足使用IntSet的时候就会采用dict来存储,会把value设置为nul。

    在这里插入图片描述

应用场景:

  • 点赞:比如key是文章id,value是用户id,这样就可以有效的避免出现用户重复点赞的情况
  • 获取共同好友:如key是用户id,value是好友id,通过SINTER key1 key2判断两个集合的交集,则可以获取到共同好友数
  • **抽奖活动:**每次从集合中抽出指定的中奖名额,如果允许重复中奖则可以使用SRANDMEMBER lucky 中奖数 ,如果不允许重复中奖则可以用SPOP lucky 中奖数

1.4、ZSet:有序集合

redis的有序集合,每个元素相比于set都多了一个排序属性score,都关联一个分数,这些元素会按照分数进行排序。其他的和set差不多都是不能有重复的元素。

Zset底层主要使用的ZipList和SkipList + dict实现,如果元素个数小于128个,并且每个元素的值小于64字节,则使用ziplist作为底层数据结构;否则则采用SkipList + dict作为底层数据结构**。在Redis7.0之后ZSet的底层的ziplist也逐渐被quicklist**代替

  • skiplist + dict 的组合实现

    • **skiplist:**用来存储ZSet的成员和分数,并维护成员按照分数升序排序。跳跃表的多层结构保证了它能在 O(log n) 的平均时间复杂度内高效的查找、插入和删除操作。并且SkipList是一个概率数据结构,它是根据概率来计算后向指针列表层数,这从而也会影响到具体的一个查找性能,
      skiplist的数据结构定义:

      //用来计算指针列表层数的
      #define ZSKIPLIST_MAXLEVEL 32  
      #define ZSKIPLIST_P 0.25
      
      // zskiplistNode定义了skiplist中的每一个节点结构
      typedef struct zskiplistNode {
          robj *obj;  //节点数据
          double score;  //分数
          struct zskiplistNode *backward;  //指向链表前一个节点的指针(前向指针),节点只有1个前向指针
          struct zskiplistLevel { 
              struct zskiplistNode *forward; //后向指针指向
              unsigned int span; //表示跨越了多少个节点,这个计数不包括指针的起点节点,但包括指针的终点节点。
          } level[]; //存放指向各层链表后一个节点的指针(后向指针)的数组对象
      } zskiplistNode;
      
      // zskiplist定义了真正的skiplist结构
      typedef struct zskiplist {
          struct zskiplistNode *header, *tail; //头指针和尾指针
          unsigned long length; //链表长度
          int level;节点层数的最大值
      } zskiplist;
      
  • dict (字典):用于快速查找成员,和set类型类似。字典的键是成员,值是 skiplist 中对应节点的指针。查找某个成员时,先通过字典找到对应 skiplist 节点,然后可以直接访问成员和分数。

应用场景:

  • 排行榜:如成绩排名等需要根据分数值排序的场景

skiplist具体案例实现介绍

把下面分数下输入到zset中

  • Alice 87.5
  • Bob 89.0
  • Charles 65.5
  • David 78.0
  • Emily 93.5
  • Fred 87.5

在这里插入图片描述

注意:图中后向指针指向上面括号中的数字,表示对呀的span的值。表示当前指针跨越了多少个节点,不包括指针的开始节点,但包括结束节点。

假设我们要查找score=89.0的元素(即Bob的成绩)的排名,红色的就是指向的最终方向。

首先使用89.0跟78.0,然后在和87.5比,比他们大,则继续向后比,如果比他们小则在当前节点的level[]取下层的元素;然后再通过87.5节点上指向的就是89.0分的元素,则代表找到这个元素。通过把这些指针上面的指针累加起来,就得到Bob的排名(2+2+1)-1=4(减1是因为rank值以0起始)。

需要注意这里算的是从小到大的排名,而如果要算从大到小的排名,只需要用skiplist长度减去查找路径上的span累加值,即6-(2+2+1)=1。

1.5、Hash:哈希

hash类似于java的hashMap,用于存储键值对的集合,特别适用于存储对象信息,可以用来存储对象的多个字段信息。Hash 中的每个键都是唯一的字符串,对应的值可以是字符串、数字或其他数据类型。

底层实现主要由ziplistdict构成**,如果元素个数少于512个,并且每个字段多键和值都比较短时,就会采用zipList,否则则采用dict进行存储。在Redis7.0之后Hash的底层的ziplist也逐渐被quicklist**代替

应用场景:

  • **缓存对象信息:**Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
  • **购物车:**用户id,商品id,商品数量

1.6、Bitmap:位图

Bitmap是一种高效的存储大量布尔值(0或1)的数据结构。它可以被理解为一个位为单位的数组,每个位为单位的数组,每个位可以表示一个布尔值,0代表false,1代表true

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。

应用场景:

  • 用户签到:可以使用BitMap记录每个用户每日签到情况。
  • 统计活跃用户:统计用户在特定时间内的活跃情况
  • 记录用户行为:可以使用 BitMap 记录用户是否执行了某些操作

1.7、Geospatial:地理位置

顾名思义,Geospatial主要是用来存储地理信息(经纬度)的,它主要用来构建地理位置信息的应用。

使用 Geohash 算法将经纬度编码成字符串,并存储在键中,Geohash是一种将二维空间映射到一维字符串的算法,它可以将地理位置,编码成一个字符串,并且字符串的前缀可以表示一个区域。

例如:37.785389, -122.405691 (旧金山) ====⇒ 9q97q5

1.8、Stream:流

Stream类型是一种新的数据结构,主要是用于构建消息队列和流式数据处理应用的。它可以被理解为一个有序的日志,Stream 类型提供了对消息的持久化、有序存储和消费者组管理等功能,适用于实时数据处理和消息队列场景。并且Stream中的消息会被持久化到磁盘中,而且Stream还支持消息确认机制,相当于一个轻量级别的MQ。

Stream有效的解决了redis消息队列的一些缺陷,在Stream没出现之前,Redis实现消息队列的两种方式都有一定的缺陷,如:

  1. 发布订阅模式:不能持久化也就无法保证可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷
  2. List实现的消息队列不能进行重复消费,一个消息被消费完就会被删除,而且生产者需要自行实现全局唯一ID

在这里插入图片描述

应用场景:

  • 消息队列:可以用作构建一个消息队列,提供给不同服务传递数据
  • 实时数据处理:可以使用Stream接收实时数据流,并进行实时处理和分析。
  • 日志收集:可以使用Stream收集应用程序的日志,并且进行存储

2、持久化方案

2.1、RDB快照

RDB持久化会将Redis的数据以快照的方式保存到一个二进制文件中(默认文件为dump.rdb)。Redis定期将内存中的数据写入磁盘,形成一个快照文件。

触发条件主要分为手动触发自动触发两种。

  • 手动触发:使用**savebgsave**命令

    • save:save命令会阻塞redis服务器进程,直到RDB文件创建完毕为止,在服务器进行阻塞期间,服务器不能处理任何命令请求。
    • bgsave:bgsave命令会通过fork函数创建一个子进程在后台生成快照文件,不会阻塞redis服务,服务进程可以继续存储命令请求,真正阻塞的只有fork那很短暂的时刻。
      • fork函数主要是利用linux操作系统的写时复制(Copy On Write,即 COW)机制,让父子线程共享内存,从而减少内存占用,并且避免了没有必要的数据复制

    bgsave命令期间,客户端发送的save和bgsave命令会被拒绝,这样主要是为了防止父子进程之间产生竞争

  • 自动触发:

    • 通过配置redis.conf文件触发规则,自行执行

      # 当用户设置了多个save的选项配置,只要其中任一条满足,Redis都会触发一次BGSAVE操作
      save 900 1 
      save 300 10 
      save 60 10000
      # 以上配置的含义:900秒之内至少一次写操作、300秒之内至少发生10次写操作、
      # 60秒之内发生至少10000次写操作,只要满足任一条件,均会触发bgsave
      
    • 执行shutdown命令关闭服务器时,如果没有开启AOF持久化功能,那么就会自动执行一次bgsave

    • 主从同步:

优缺点:

  • 优点:容易简单,恢复速度快,适合大规模的数据恢复场景,如备份全量恢复
  • 缺点:没办法做到实时持久化,可能会在间隔期间宕机会出现数据丢失的情况,占用磁盘空间可能会比较大

2.2、AOF日志

AOF持久化是将Redis执行的所有写操作都记录到一个日志(appendonly.aof)文件中,redis启动时会读取AOF文件,将记录的操作重复一遍,从而恢复数据。

AOF默认是关闭的,通过redis.conf配置文件进行开启

## 此选项为aof功能的开关,默认为“no”,可以通过“yes”来开启aof功能  
## 只有在“yes”下,aof重写/文件同步等特性才会生效  
appendonly yes  

## 指定aof文件名称  
appendfilename appendonly.aof  

## 指定aof缓冲区同步策略,有三个合法值:always everysec no,默认为everysec  
appendfsync everysec  

AOF在写入时,并不是每次都直接往磁盘写,而是会将写命令追加到AOF缓冲区的末位,之后AOF缓冲区再同步到磁盘。这样主要是为了提高效率,毕竟如果每次写到话性能太低了。

AOF缓冲区同步到磁盘的过程由flushAppendonlyFile 函数完成,该参数有以下三个选项:

  • **always:**每次发生写时都同步到AOF文件,是最安全的的选项,效率最低
  • **everysec:**每秒同步一次到AOF文件,在性能和安全之间做到一个平衡
  • **no:**不主动写入AOF文件,何时同步由操作系统决定

AOF重写

AOF重写主要是对AOF文件的一个优化,虽然叫重写,但并不是去对向有的数据进行重写,AOF重写主要是通过去redis服务器中读取当前的数据来进行写的。

如:假设我给分别执行了下面几条命令

rpush list "A"
rpush list "B"
rpush list "C"

如果redis为了保存当前list的数据,就需要AOF文件保存三条命令,而我现在要对AOF进行重写,不是去重新写入这三条命令,而是会去直接读取redis服务器中list的数据,然后用一条命令:rpush list "A" "B" "C" "D" "E" "F"进行存储,这样就会由六条命令变为一条数据

优缺点:

优点:数据的一致性和完整性更高, 并且可以配置不同的同步策略:例如 alwayseverysecno,控制 AOF 文件的同步频率,平衡性能和数据安全。

缺点:AOF记录的内容越多,文件越大,数据恢复越慢

2.3、混合持久化

Redis4.0之后新推出的一个持久化方案,RDB-AOF 混合持久化。

开启混合持久化后,会先以RDB格式把当前Redis服务器的数据写入到(appendonly.aof)文件中,然后在把后续的增量数据追加到(appendonly.aof)文件文件后面。因为RDB是二进制写入到,这样文件的体积会变的更小,并且后续的增量数据是以AOF的格式写入的。而且恢复数据的时候也是先恢复前面的RDB的在去恢复后面追加的AOF数据。

这样既避免了存粹的AOF模式写入文件过大数据恢复慢的缺点也避免了RDB模式在写入时会有暂停的缺点。

文件格式:

在这里插入图片描述

3、缓存问题

3.1、缓存击穿

产生原因:

由于大量请求数据库的数据在redis缓存中都没查询到数据,则都会去服务器查询,一般造成这个的原因主要可能是业务/开发/运维设计有问题造成大量请求无法在redis中命中,还可能是黑客dos攻击等原因造成。

解决办法:

  1. 通过网关过滤一次ip请求,以及请求过多等异常非法请求,并且对参数进行校准排除非法参数
  2. 如果这个请求redis查询不到,在数据库查询还是查询为nul,则把nul也存储到redis
  3. 使用布隆过滤器,快速判断请求的值是否存储在redis中,如果有则请求通过放行到redis中去查询,如果没有,则直接返回结果
    1. 具体实现:它由初始值为0的位图数组和N个哈希函数组成,一个对一个key进行N个hash算法获取N个值,在比特数组中将这N个值散列后设定为1,然后查的时候如果特定的这几个位置都为1,那么布隆过滤器判断该值存在(会出现误判)

3.2、缓存穿透

产生原因:

当redis存储的热点数据过期时,大量的请求到来,从而造成大批量的请求打到数据库。

解决办法:

  1. 对热点数据不设置过期时间,或者通过异步线程定时更新那些快要过期的热点数据(类似redisson的”看门狗机制”)的过期时间
  2. 使用互斥锁方案,缓存失效时,不是立即去数据库查询,而是通过原子命令让只有一个成功的请求可以去查询数据库,其他的失败请求重新去查询缓存,这样就只会有一个请求到达数据库

3.3、缓存雪崩

产生原因:

redis存储的key大量过期,请求的数据都访问到数据库,引起数据库宕机

解决办法:

  1. 设置过期时间时,在后面加上随机数,避免统一设置的key统一过期,
  2. 搭建高可用的redis集群

4、常用场景

4.1、分布式锁

产生原因:在开发过程中,我们为例提高效率,我们往往会引用多线程并行的方式来提高访问修改并发,我们在对面并发问题时,多个线程一起修改时,如果是单应用部署,直接通过synchronized关键字、 ReetrantLock 类等进程锁来控制一个JVM进程那多个线程对本地共享资源对访问。

但如果是在分布式、微服务等架构下,不同的服务器/客户端通常运行在独立的JVM进程上,使用本地锁的方式就不能有效的解决这个问题,因此就引出了一个分布式锁的概念,具体架构如下。

在这里插入图片描述

通过redis实现分布式锁主要可分为三种方式:

  • 直接通过SETNX命令实现:

    SETNX:SETNX是redis的一个原子操作命令,仅当该键不存在时才能设置成功,如果成功则会返回1,不成功则会返回0

    • **实现流程:**模拟一个扣减库存逻辑
      1. A线程和B线程同时去获取锁扣减库存,假如A线程获取到锁,B线程在去获取则会获取失败。而在设置的时候为了避免出现A线程因为某些问题一直不释放锁的情况,我们会在设置一个过期时间,使得可以A线程在达到特定的时间后自动释放锁。
      2. 在这之中B线程会一直循环去获取锁,直到A线程执行完业务代码,通过delete命令释放锁,才会成功获取到锁。
      3. 删除锁的时候也出现一种情况,如果A线程在执行完业务的时候锁已经到期自动释放锁了,这时候再去删除的话,可能就会误删B线程的锁。所以在删除锁的时候可以做一个优化,设置给每一个请求设置一个唯一ID,在删除之前去验证该锁是不是自己的锁,这样就可以避免出现误删的情况。
    • **可能会出现的问题:**在释放锁的时候可能会出现,验证完id到删除锁不是原子操作,就可能会验证完后,A线程获取到的锁自动过期,B线程去获取到了锁,此时A线程在去删除则会把B线程的锁删除。
  • 通过SETNX命令和编写Lua脚本实现

    • 实现流程:

      1. 前面的获取锁是一样的通过SETNX命令获取,

      2. 释放锁针对释放锁时不能原子操作,可以优化为使用Lua脚本编写一个验证锁是否是自己的,在去删除的脚本实现原子操作。

        private static final String UNLOCK_SCRIPT =
                "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                        "redis.call('del', KEYS[1]); " +
                        "return true; " +
                        "else return false; " +
                        "end";
        
        
    • **可能会出现的问题:**该方式虽然没啥大的问题,但该方式因为设置锁时,由于时间只能设置固定的,因此在一些特定的业务场景可能还是会出现一定的问题。需要很好的去把握设置锁的过期时间,过长可能会造成失效不及时,过短可能会导致频繁地去获取锁。

  • 通过Redisson框架实现

    Redisson是一个基于Redis的分布式java对象和服务框架,它提供了一系列的分布式对象和服务,使得在java应用程序中使用Redis变得更加方便和高效。并且内部提供了很多锁供我们选择

    Redisson常用分布式锁对象

    • redissonClient.getLock(key) : 可重入锁(最常用的)
    • redissonClient.getFairLock(key):公平锁
    • redissonClient.getSpinLock(key):自旋锁
    • redissonClient.getReadWriteLock(key):读写锁
    • redissonClient.getMultiLock(lock):多重锁

    获取锁的方式:

    • lock.lock():阻塞的方式获取,该方式去获取锁,会一直阻塞请求,直到获取到锁。该方式获取到锁如果。默认30秒为最大时间10秒续期一次,
    • lock.tryLock(expireTime,TimeUnit.SECONDS):非阻塞的方式获取锁,最多可以设置三个参数,分别指定重试时间,锁过期时间和时间单位;一般建议设置一个重试时间,如果指定时间没法则返回false,因为毕竟是非阻塞执行,如果不设置则会一直去请求获取锁。

    **释放锁:**则直接通过lock.unlock()进行释放即可

    具体执行逻辑则类似

4.2、接口幂等性

在编程过程中,接口接口幂等指的是多次调用一个接口,对系统的产生与影响只有一次,相同参数多次重复调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。

出现的原因:

  • 前端重复提交:用户注册,用户创建商品等操作,前端都会提交一些数据给后台服务,后台需要根据用户提交的数据在数据库中创建记录,如果用户不小心多点了几次,后端收到好几次提交,这时数据库就会出现多条记录
  • 接口超时重试:对于给第三方调用的接口,有可能会因为网络原因而调用失败,这时,一般在设计的时候会对接口调用加上失败重试的机制。如果第一次调用已经执行了一半时,发生网络异常,这时再次调用时就会出现脏数据的存在
  • 消息重复消费:在使用消息中间件来处理消息队列,且手动ack确认消息被正常消费时。如果消费者突然断开连接。那么已经执行了一半的消息会重新放回队列,则会被其他消费者重新消费

解决方案:

  • 通过token机制实现:

    1. 客户端先发送一次请求去获取token,服务端会生成一个全局唯一的ID作为token保存在redis中,同时把这个ID返回给客户端
    2. 客户端在确认提交时,则携带这个token请求
    3. 服务端会校验这个token,如果校验成功,则执行业务,并删除redis中的token
    4. 如果校验失败,说明已经有请求成功执行了。表示重复消费

    在这里插入图片描述

  • 基于redis的原子命令SETNX实现:

    1. 前端提交后端端时候携带一个唯一字段(可以是订单id或者客户请求客户端IP加路径等)
    2. 将该字段以SETNX的方式存如redis中,并根据业务设置相应的超时时间(可以通过lua脚本实现,因为需要设置时一起设置超时时间)
    3. 如果设置成功,证明这是第一次请求,则执行后续业务逻辑
    4. 如果设置失败,则代表前面已经有请求执行成功了,表示重复消费

    在这里插入图片描述

  • 或者通过mysql的唯一主键实现。

5、高可用

Redis单机版可能在生产环境中常常无法保证高可用性需求,一旦单点故障,整个服务都会瘫痪。所以就需要使用一些高可用的方案解决这个问题。

具体方案:

  • Redis主从模式:redis提供的主从模式,是通过复制的方式,将主节点上的数据同步一份到从节点上。主节点只会有一个,从节点可以有多个,并且在主从复制的基础上实现了读写分离;主节点负责写,从节点负责读,在写多读上的场景上非常适用。

    在这里插入图片描述

    优缺点:

    • 优点:相对简单,两台机子就行。
    • 缺点,如果主节点挂了,需要手动设置一个从节点转为主节点,同时需要修改应用方的 主节点地址,整个过程需要人工,相对比较麻烦
  • Redis Sentinel( 哨兵)模式:Sentinel 是一个独立进程,用于管理多个 Redis 实例。它监控主服务器的状态,如果主服务器出现故障,Sentinel 会自动将其中一个从服务器提升为主服务器,提供高可用性。部署至少三台节点实现

    在监控的过程中如果主节点挂了,则sentinel会通过投票选举出一个从节点作为主节点

    在这里插入图片描述

    优缺点:

    • 优点:可以通过哨兵监听节点状态,在自动选举出主节点,提高可用性
    • 缺点: 单点故障问题依然存在(Sentinel 集群本身需要高可用性保障), 脑裂问题(brain split,多个 Sentinel 同时认为主服务器挂了,各自选出一个新的主服务器),数据丢失风险(主从复制延迟)
  • Redis Cluster集群模式:Redis Cluster是一种分布式数据库方案,集群通过数据分片进行数据管理,每个节点负责存储一部分数据,客户端通过客户端路由算法连接到正确的节点。 每个节点都是主从结构。

    Redis Cluster采用的是无中心结构,每个节点都可以保存数据和整个集群状态,每个节点都和其他所有节点连接。要让集群正常运作至少需要三个主节点,即 Redis Cluster 至少为 6 个实例才能保证组成完整高可用的集群。

    在这里插入图片描述

    优缺点:

    • 优点:数据分区,突破了 Redis 单机内存大小的限制,存储容量大大增加,另一方面 每个主节点都可以对外提供读服务和写服务,大大提高了集群的响应能力。并且集群支持主从复制和主节点的故障转移(与哨兵类似)。
    • 缺点:耗费资源相对较多,配置和运维成本较高;数据迁移可能导致短暂服务中断。

6、过期策略和淘汰机制

6.1、过期策略

redis在存储数据时,如果设置了过期时间,缓存数据到了过期时间就会失效,那么redis是如何进行数据清除掉掉这些失效的数据的呢,这就要用到redis的过期策略了。

过期策略分类:

  1. 定时删除策略:在设置键的过期的同时,同时设置一个定时器,如果键过期则同时删除这个数据
    1. 优点:该策略可以立即删除掉这些过期的数据,对内存比较友好,可以有效的减少内存的占有
    2. 缺点:该策略因为每一个键都需要设置定时器去删除数据,因此对CPU的占用也会非常大,从而也会影响到缓存的响应时间和吞吐量
  2. 惰性删除策略:键过期时,暂时不删除,需要等到下一次有请求对该键进行访问的时候,查看该键是否过期了,如果过期了则会把该数据进行清除,否则则继续保留
    1. 优点:该策略可以最大的节省CPU资源,因为减少了定时器线程的建立,
    2. 缺点:对内存非常不友好,因为数据不是及时删除,需要等待到下一次访问,在一些极端情况下,可能会缓存大量的为删除的过期数据,占有大量内存
  3. 定期删除策略:每隔一段时间都会扫描一定数量的key,看这部分数据哪些对key的时间过期,过期则把这部分数据进行清除
    1. 优点:可以定期对数据进行清除释放内存,对内存相对比较友好,并且不用每个每个键设置定时器耗费过大的cup性能
    2. 缺点:删除的时候会耗费性能,但相对前两个中和很多

7、网络模型

使用过redis的都知道redis处理数据非常的快,官方做过测试,在一台普通的linux上跑单个redis实例,处理简单的命令,QPS都可以达到8w+,至于为何可以做到这么快有以下几种原因:

  • Redis是基于纯内存的IO操作,相对于其他的磁盘IO来说,速度性能上基本可以快上多个量级
  • 还有就是因为Redis采用的是单线程模型,因为redis的操作相对比较简单,而且基于内存,每条命令的处理速度会非常快,而使用单线程可以有效的减少了上下文切换,以及同步机制下锁带来的开销。
  • 再有就是redis使用的网络模型进行了深度优化,采用的是基于Reactor开发了自己的IO多路复用的事件驱动模型

Reactor(反应堆模式):redis是基于该设计模式开发了自己的网络模型,形成了一个完备的基于IO多路复用的事件驱动服务器

在这里插入图片描述

redis在6.0版本之前使用的是单线程机制实现的,在6.0之后引入了单线程机制,但不是完全抛弃单线程,**只是在解析客户端命令和回写响应数据时采用了多线程,**核心的命令执行,IO多路复用模块还是主线程执行

  • 单线程事件循环

    1. redis服务器启动,开启事件循环,注册acceptTcpHandler连接应答处理器到用户配置的监听端口对应的文件描述符,等待新连接的到来

    2. 客户端和服务器建立网络连接,

    3. acceptTcpHandler 被调用,读取处理器绑定到新连接,并初始化一个 client 绑定这个客户端连接

    4. 客户端发送redis命令,redis主线程通过readQueryFromClient读取客户端数据并解析成命令。

    5. redis主线程执行命令,访问数据结构,并生成结果

    6. redis主线程通过sendReplyToClient将执行结果返回给客户端

      在这里插入图片描述

  • 多线程异步复用

    1. redis服务器启动,开启事件循环,注册acceptTcpHandler连接应答处理器到用户配置的监听端口对应的文件描述符,等待新连接的到来
    2. 客户端和服务器建立网络连接,
    3. acceptTcpHandler 被调用,读取处理器绑定到新连接,并初始化一个 client 绑定这个客户端连接
    4. 客户端发送redis命令,redis主线程不会自己去处理解析这个数据会把这个数据client放入一个 LIFO 队列 clients_pending_read。
    5. 主线程利用 Round-Robin 轮询负载均衡策略,均匀的把LIFO分配给各个IO线程进行处理解析数据。
    6. 主线程轮休完毕后,会去读取IO线程解析的命令执行,访问数据结构,并将执行的结果交给IO线程,IO线程把处理结果返回给客户端

    在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值