Redis总结

Redis总结

什么是Redis

Redis是一个开源的,内存中的数据结构存储系统,可以用作数据库、缓存和消息中间件。它是key,value结构的存储系统,它支持多种数据类型的数据结构,如字符串(strings),散列(hashes),列表(lists),集合(sets),有序集合(sorted sets)等。它可以通过Redis哨兵和自动分区提供高可用性。引入Redis缓存机制可以有效的降低用户访问物理设备的频次,提高响应速度。

为什么要用Redis?

Q: redis中存储数据是以key-value形式, 那么为什么不用java的map 容器代替呢?

答:

  • java仅实现的是本地缓存, 如果有多台机器的话, 都需要各自保存一份数据, 不具有一致性
  • Redis是分布式缓存, 每个机器都共享一份数据, 具有一致性
  • java的map不是专业做缓存的, JVM的内存太大的时候容易挂掉, 且随着JVM结束而销毁, 一般用于临时存储数据, 且如果需要过期策略等机制都需要额外手写!
  • redis是专业缓存的, 可以有几十个G做缓存, 且能存入硬盘, 一旦重启也可将数据恢复, 自带丰富的数据结构, 和过期策略等机制

为什么要用redis而不用map做缓存?

  • Redis 可以用几十 G 内存来做缓存,Map 不行,一般 JVM 也就分几个 G 数据就够大了
  • Redis 的缓存可以持久化,Map 是内存对象,程序一重启数据就没了
  • Redis 可以实现分布式的缓存,Map 只能存在创建它的程序里
  • Redis 可以处理每秒百万级的并发,是专业的缓存服务,Map 只是一个普通的对象
  • Redis 缓存有过期机制,Map 本身无此功能
  • Redis 有丰富的 API,Map 就简单太多了

为什么要用缓存?

缓 解 数 据 库 压 力 !

Redis总结_第1张图片

Redis 为什么这么快?

1s读 10000次 写5000次

1.操作内存 :直接操作内存速度会相对较快。

2.5种数据结构的存在

  • SDS : 它的定义是不单单是一个char 数组构成,每个sds都会比它真实占用的字符长度都长,通过一个空闲标识符表示sds当前空闲字符有多少,如此设计,在一定长度范围的内的字符串都可以使用此sds,而且不会频繁的进行内存分配,直到此sds不能容纳分配的字符串,如果遇到这种情况情况,才需要进行扩扩容;这是redis的最基础的,所有的redis k-v 中的字符串都是依托于sds;

  • dict : 类似java中的hashmap( 数组, 负载因子, hash算法 )

    ​ 不过dict 有两个数组 其中一个用作扩容 ( dict的扩容是渐进式的,不会影响当前使用, 一点点迁移, 直到迁移完成 不像hashmap是new 一个更大的然后全部移走 )

3.跳跃表 : 跳跃表使用了历史上最屌的算法:抛硬币 在跳跃表是由N层链表组成,最底层是最完整的的数据,每次数据插入,率先进入到这个链表(有序的),插入完成后,通过抛硬币的算法,判断是否将数据向上层跑,如果是1的话,就抛到上层,然后继续抛硬盘,判断是否继续向上层抛,直到抛出了0结束整个操作,每抛到一层的时候,如果当前层没有数据,就构造一个链表,将数据放进去,然后使用指针指向来源地址,就这样依次类推,形成了跳跃表,每次查询,从最上层遍历查询,如果找到就返回结果,否则就在此层找到最接近查询的值,将查询操作移到另外一层,就是刚才说到来源地址,所在层,重复查询

4.使用单线程+多路 I/O 复用模型

  • 单线程足够简单,无论在redis的实现还是作为调用方,都不需要为数据并发提心吊胆,不需要加锁。
  • 不会出现不必要的线程调度,多线程频繁切换上下文,也会带来很多性能消耗
  • 多路 I/O 复用模型,说白了就是当一个请求来访问redis后,redis去组织数据要返回给请求,这个时间段,redis的请求入口不是阻塞的,其他请求可以继续向redis发送请求,等到redis io流完成后,再向调用者返回数据,这样一来,单线程也不怕会影响速度了

Redis总结_第2张图片

Redis数据结构

Redis总结_第3张图片

Redis Object

但要值得注意的是:Redis并没有直接使用上图的数据结构来实现key-value数据库,而是基于这些数据结构创建了一个对象系统。

简单来说:Redis使用对象来表示数据库中的键和值。每次我们在Redis数据库中新创建一个键值对时,至少会创建出两个对象。一个是键对象,一个是值对象。

Redis中的每个对象都由一个RedisObject结构来表示:

typedef struct redisObject{
    // 对象的类型
    unsigned type 4:;
    
    // 对象的编码格式
    unsigned encoding:4;
    
    // 指向底层实现数据结构的指针
    void * ptr;
    
    //.....

}robj;

简单来说就是Redis对key-value封装成对象,key是一个对象,value也是一个对象。每个对象都有type(类型)、encoding(编码)、ptr(指向底层数据结构的指针)等 来表示。

Redis总结_第4张图片

Redis数据结构

SDS

简单动态字符串(Simple dynamic string,SDS)

Redis中的字符串跟C语言中的字符串,是有点差距的

Redis使用sdshdr结构来表示一个SDS值:

struct sdshdr{

    // 字节数组,用于保存字符串
    char buf[];

    // 记录buf数组中已使用的字节数量,也是字符串的长度
    int len;

    // 记录buf数组未使用的字节数量
    int free;
}

Redis总结_第5张图片

使用SDS的好处

SDS与C的字符串表示比较

  • sdshdr数据结构中用len属性记录了字符串的长度。那么获取字符串的长度时,时间复杂度只需要O(1)。
  • SDS不会发生溢出的问题,如果修改SDS时,空间不足。先会扩展空间,再进行修改!(内部实现了动态扩展机制)。
  • SDS可以减少内存分配的次数(空间预分配机制)。在扩展空间时,除了分配修改时所必要的空间,还会分配额外的空闲空间(free 属性)。
  • SDS是二进制安全的,所有SDS API都会以处理二进制的方式来处理SDS存放在buf数组里的数据。

链表

Redis中的链表是怎么实现的:

使用listNode结构来表示每个节点:

typedef strcut listNode{

    //前置节点
    strcut listNode  *pre;

    //后置节点
    strcut listNode  *pre;

    //节点的值
    void  *value;

}listNode

使用listNode是可以组成链表了,Redis中使用list结构来持有链表

typedef struct list{

    //表头结点
    listNode  *head;

    //表尾节点
    listNode  *tail;

    //链表长度
    unsigned long len;

    //节点值复制函数
    void *(*dup) (void *ptr);

    //节点值释放函数
    void  (*free) (void *ptr);

    //节点值对比函数
    int (*match) (void *ptr,void *key);

}list

Redis总结_第6张图片

Redis链表的特性

Redis的链表有以下特性:

  • 无环双向链表
  • 获取表头指针,表尾指针,链表节点长度的时间复杂度均为O(1)
  • 链表使用void *指针来保存节点值,可以保存各种不同类型的值

哈希表

在Redis中,key-value的数据结构底层就是哈希表来实现的。对于哈希表来说,我们也并不陌生。在Java中,哈希表实际上就是数组+链表的形式来构建的。下面我们来看看Redis的哈希表是怎么构建的吧。

在Redis里边,哈希表使用dictht结构来定义:

  typedef struct dictht{

        //哈希表数组
        dictEntry **table;  

        //哈希表大小
        unsigned long size;    

        //哈希表大小掩码,用于计算索引值
        //总是等于size-1
        unsigned long sizemark;     

        //哈希表已有节点数量
        unsigned long used;

    }dictht

从结构上看,我们可以发现:Redis实现的哈希表和Java中实现的是类似的。只不过Redis多了几个属性来记录常用的值:sizemark(掩码)、used(已有的节点数量)、size(大小)。

同样地,Redis为了更好的操作,对哈希表往上再封装了一层(参考上面的Redis实现链表),使用dict结构来表示:

typedef struct dict {

    //类型特定函数
    dictType *type;

    //私有数据
    void *privdata;

    //哈希表
    dictht ht[2];

    //rehash索引
    //当rehash不进行时,值为-1
    int rehashidx;  

}dict;


//-----------------------------------

typedef struct dictType{

    //计算哈希值的函数
    unsigned int (*hashFunction)(const void * key);

    //复制键的函数
    void *(*keyDup)(void *private, const void *key);

    //复制值得函数
    void *(*valDup)(void *private, const void *obj);  

    //对比键的函数
    int (*keyCompare)(void *privdata , const void *key1, const void *key2)

    //销毁键的函数
    void (*keyDestructor)(void *private, void *key);

    //销毁值的函数
    void (*valDestructor)(void *private, void *obj);  

}dictType

所以,最后我们可以发现,Redis所实现的哈希表最后的数据结构是这样子的:

Redis总结_第7张图片

从代码实现和示例图上我们可以发现,Redis中有两个哈希表:

  • ht[0]:用于存放真实的key-vlaue数据
  • ht[1]:用于扩容(rehash)

Redis中哈希算法和哈希冲突跟Java实现的差不多,它俩差异就是:

  • Redis哈希冲突时:是将新节点添加在链表的表头
  • JDK1.8后,Java在哈希冲突时:是将新的节点添加到链表的表尾

rehash 就是 重新的分配元素并加入新的桶内,这称为rehash 就是扩容

rehash的过程

下面来具体讲讲Redis是怎么rehash的,因为我们从上面可以明显地看到,Redis是专门使用一个哈希表来做rehash的。这跟Java一次性直接rehash是有区别的。

在对哈希表进行扩展或者收缩操作时,reash过程并不是一次性地完成的,而是渐进式地完成的。

Redis在rehash时采取渐进式的原因:数据量如果过大的话,一次性rehash会有庞大的计算量,这很可能导致服务器一段时间内停止服务。

Redis具体是rehash时这么干的:

  • 在字典中维持一个索引计数器变量rehashidx,并将设置为0,表示rehash开始。
  • 在rehash期间每次对字典进行增加、查询、删除和更新操作时,除了执行指定命令外;还会将ht[0]中rehashidx索引上的值rehash到ht[1],操作完成后rehashidx+1。
  • 字典操作不断执行,最终在某个时间点,所有的键值对完成rehash,这时将rehashidx设置为-1,表示rehash完成
  • 在渐进式rehash过程中,字典会同时使用两个哈希表ht[0]和ht[1],所有的更新、删除、查找操作也会在两个哈希表进行。例如要查找一个键的话,服务器会优先查找ht[0],如果不存在,再查找ht[1],诸如此类。此外当执行新增操作时,新的键值对一律保存到ht[1],不再对ht[0]进行任何操作,以保证ht[0]的键值对数量只减不增,直至变为空表。

跳跃表

跳跃表(shiplist)是实现sortset(有序集合)的底层数据结构之一!

Redis总结_第8张图片

Redis的跳跃表实现由zskiplistzskiplistNode 两个结构组成。其中zskiplist保存跳跃表的信息(表头,表尾节点,长度),zskiplistNode则表示跳跃表的节点

按照惯例,我们来看看zskiplistNode跳跃表节点的结构是怎么样的:

typeof struct zskiplistNode {
        // 后退指针
        struct zskiplistNode *backward;
        // 分值
        double score;
        // 成员对象
        robj *obj;
        // 层
        struct zskiplistLevel {
                // 前进指针
                struct zskiplistNode *forward;
                // 跨度
                unsigned int span;
        } level[];
} zskiplistNode;

Redis总结_第9张图片

Redis总结_第10张图片

typeof struct zskiplist {
        // 表头节点,表尾节点
        struct skiplistNode *header,*tail;
        // 表中节点数量
        unsigned long length;
        // 表中最大层数
        int level;
} zskiplist;

最后我们整个跳跃表的示例图如下:

Redis总结_第11张图片

整数集合

整数集合(intset)是set(集合)的底层数据结构之一。当一个set(集合)只包含整数值元素,并且元素的数量不多时,Redis就会采用整数集合(intset)作为set(集合)的底层实现。

整数集合(intset)保证了元素是不会出现重复的,并且是有序的(从小到大排序),intset的结构是这样子的:

typeof struct intset {
        // 编码方式
        unit32_t encoding;
        // 集合包含的元素数量
        unit32_t lenght;
        // 保存元素的数组
        int8_t contents[];
} intset;

Redis总结_第12张图片

压缩列表

压缩列表(ziplist)是 List 和 Hash 的底层实现之一。

如果list的每个都是小整数值,或者是比较短的字符串,压缩列表(ziplist)作为list的底层实现。

压缩列表(ziplist)是Redis为了节约内存而开发的,是由一系列的特殊编码的连续内存块组成的顺序性数据结构

压缩列表结构图例如下:

Redis总结_第13张图片

下面我们看看节点的结构图:

Redis总结_第14张图片

压缩列表从表尾节点倒序遍历,首先指针通过 zltail偏移量指向表尾节点,然后通过指向节点记录的前一个节点的长度依次向前遍历访问整个压缩列表

Redis中数据类型与对应的数据结构

Redis总结_第15张图片

Redis总结_第16张图片

字符串(stirng)对象 3个

在上面的图我们知道string类型有三种编码格式

  • int:整数值,这个整数值可以使用long类型来表示

    如果是浮点数,那就用embstr或者raw编码。具体用哪个就看这个数的长度了

  • embstr:字符串值,这个字符串值的长度小于32字节

  • raw:字符串值,这个字符串值的长度大于32字节

embstr和raw的区别:

  • raw分配内存和释放内存的次数是两次,embstr是一次

  • embstr编码的数据保存在一块连续的内存里面

编码之间的转换:

  • int类型如果存的不再是一个整数值,则会从int转成raw
  • embstr是只读的,在修改的时候回从embstr转成raw

列表(list)对象 2个

在上面的图我们知道list类型有两种编码格式

  • ziplist:字符串元素的长度都小于64个字节&&总数量少于512个

  • linkedlist:字符串元素的长度大于64个字节||总数量大于512个

ZipList 编码的列表结构:

redis > RPUSH numbers 1 "three" 5
(integer) 3 

Redis总结_第17张图片

LinkedList编码的列表结构:

Redis总结_第18张图片

LinkedList编码的列表结构

编码之间的转换:

  • 原本是 ziplist 编码的,如果保存的数据长度太大或者元素数量过多,会转换成 linkedlist 编码的。

哈希(Hash)对象 2个

在上面的图我们知道Hash类型有两种编码格式:

  • ZipList:key和value的字符串长度都小于64字节&&键值对总数量小于512
  • HashTable:key和value的字符串长度大于64字节||键值对总数量大于512

ziplist编码的哈希结构:

Redis总结_第19张图片

压缩列表:

ziplist编码的哈希结构2

HashTable编码的哈希结构:

Redis总结_第20张图片

Redis总结_第21张图片

编码之间的转换:

  • 原本是ziplist编码的,如果保存的数据长度太大或者元素数量过多,会转换成 hashtable 编码的。

集合(set)对象 2个

在上面的图我们知道set类型有两种编码格式

  • IntSet:保存的元素全都是整数&&总数量小于512
  • HashTable:保存的元素不是整数||总数量大于512

intset编码的集合结构:

Redis总结_第22张图片

hashtable编码的集合结构:

Redis总结_第23张图片

Redis总结_第24张图片

编码之间的转换:

  • 原本是intset编码的,如果保存的数据不是整数值或者元素数量大于512,会转换成hashtable编码的。

有序集合(zset)对象 2个

在上面的图我们知道set类型有两种编码格式

  • ZipList:元素长度小于64&&总数量小于128
  • SkipList:元素长度大于64||总数量大于128

ziplist编码的有序集合结构:

Redis总结_第25张图片

压缩列表:

ziplist编码的有序集合结构2

skiplist编码的有序集合结构:

Redis总结_第26张图片

有序集合(sortset)对象同时采用SkipList和哈希表来实现:

  • skiplist能够达到插入的时间复杂度为O(logn),hashmap根据成员查分值的时间复杂度为O(1)

编码之间的转换:

  • 原本是ziplist编码的,如果保存的数据长度大于64或者元素数量大于128,会转换成skiplist编码的。

可以使用debug object key_name来查看数据类型内部结构

Redis对象一些细节

  • 服务器在执行某些命令的时候,会先检查给定的键的类型能否执行指定的命令。

    比如我们的数据结构是sortset,但你使用了list的命令。这是不对的,服务器会检查一下我们的数据结构是什么才会进一步执行命令

  • Redis的对象系统带有引用计数实现的内存回收机制

    对象不再被使用的时候,对象所占用的内存会释放掉

  • Redis会共享值为0到9999的字符串对象

  • 对象会记录自己的最后一次被访问时间,这个时间可以用于计算对象的空转时间。

Redis五种数据类型及应用场景

  • String: 一般做一些复杂的计数功能的缓存
  • List: 做简单的消息队列的功能
  • Hash: 单点登录
  • Set: 做全局去重的功能
  • SortedSet: 做排行榜应用,取TopN操作;延时任务;做范围查找

Redis总结_第27张图片

为什么 redis 实现有序集合不用红黑树或者平衡二叉树呢?

Redis之所以用跳表来实现有序集合

  1. 插入、删除、查找以及迭代输出有序序列这几个操作,红黑树都能完成,时间复杂度跟跳表是一样的。但是按照区间来查找数据,红黑树的效率就没有跳表高

  2. 跳表更加灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗

  3. 在实现方面,红黑树实现更加复杂,跳跃表实现比较简单,也更加直观,更加灵活。

  4. 红黑树/平衡二叉树这种树形结构,每次每隔两个节点建一个索引,而跳跃表可以多个节点,不限于两个节点。

  5. 跳跃表插入或删除操作只需要修改节点前后的指针,而不需要对多个节点都进行调整,而平衡二叉树则需要左旋或者右旋实现平衡。

  6. 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。

缓存穿透、击穿、雪崩

缓存穿透:

用户高并发环境下访问数据库和缓存中都不在的数据称为穿透现象。

Redis总结_第28张图片

解决方法:

  • 禁用ip, 限制ip访问
  • 限制每秒访问次数
  • 使用布隆过滤器的方法解决

布隆过滤器: **是一个很长的二进制向量和一系列随机映射函数。**可以用于检索一个元素是否在一个集合中,优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

用法: 由二进制向量,hash函数组合.
作用: 判断一个元素是否存在于集合中.
优点: 占用空间更小/效率更高
缺点: 有一定的误判率(hash碰撞), 删除困难.

由于hash碰撞问题,可能有多个key有相同的位置,可以得出结论 :
布隆过滤器认为数据存在,那么数据可能存在,如果认为数据不存在,那么一定不存在。

优化方法 : 可以用扩容二进制向量位数增加hash函数的个数来降低hash碰撞的几率。

布隆过滤器应用场景

说明:当用户查询服务器时,首先查询布隆过滤器,如果查询存在该数据,则执行后续的流程,
如果查询没有该数据,则直接返回.无需执行后续流程.

Redis总结_第29张图片

布隆过滤器算法介绍

Redis总结_第30张图片

关于布隆过滤器优化说明

根据hash原则 数据存在hash碰撞的概率. 则使用布隆过滤器容器造成误判. 如何解决?

Redis总结_第31张图片

**优化hash碰撞概率-增加hash函数个数 ** 优化hash碰撞概率-增加二进制向量

Redis总结_第32张图片

缓存击穿:

当某一个热点数据在缓存中突然失效,导致大量用户直接访问数据库,导致并发压力过高造成异常,这种情况称为击穿.

Redis总结_第33张图片

解决方法:

  • 尽可能将热点数据的超时时间设置长一点,且设定多级缓存定期更新热点数据的超时时间.

缓存雪崩:

在缓存服务器中,由于大量缓存数据失效导致用户访问的命中率过低,导致直接访问数据库。

Redis总结_第34张图片

解决方法:

设定多级缓存设定超时时间使用随机算法

持久化策略

Redis中将数据都保存到了内存中,但是内存的特点断电及擦除. 为了保证redis中的缓存数据不丢失,则需要将内存数据定期进行持久化操作.

持久化: 将内存数据,写到磁盘中.

RDB

特点:

  • RDB模式是Redis默认的持久化规则.
  • RDB模式记录的是Redis内存数据快照(只保留最新数据)
  • RDB模式定期持久化(时间可调) 可能会导致数据丢失.
  • RDB模式备份效率是最高的.
  • RDB模式备份是阻塞式的 在备份时不允许其他用户操作. 保证数据安全性

备份命令:

  • 主动备份 :save 会阻塞用户操作
  • 后台备份 :bgsave 异步的方式进行持久化操作 不会阻塞.

关于持久化配置

  • save 900 1 : 900秒内,用户执行了一次更新操作时,那么就持久化一次
  • save 300 10 :300秒内,用户执行了10次更新操作. 那么就持久化一次
  • save 60 10000 :60秒内,用户执行了10000次的更新操作,则持久化一次.
  • save 1 1: 1秒内 1次更新 持久化一次!! 性能特别低.

Redis总结_第35张图片

关于持久化文件名称设定

默认的条件下,持久化文件名称 dump.rdb

Redis总结_第36张图片

文件存储目录

./ 代表当前文件目录. 意义使用绝对路径的写法.

Redis总结_第37张图片

AOF

特点 :

  • AOF模式默认的条件下是关闭状态.需要手动开启.
  • AOF模式记录的是用户的操作过程. 可以实现实时持久化.保证数据不丢失.
  • AOF模式维护的持久化文件占用的空间较大.所以持久化效率不高. 并且需要定期的维护持久化文件.
  • AOF模式一旦开启,则redis以AOF模式为主 读取的是AOF文件.

AOF配置

开启AOF模式

Redis总结_第38张图片

持久化策略
always : 用户更新一次,则持久化一次.
everysec : 每秒持久化一次 效率更高
no : 不主动持久化. 操作系统有关. 几乎不用.

Redis总结_第39张图片

企业策略:既要满足效率,又不能丢失数据.

主从结构:主机RDB,从机AOF.

FlushAll的补救方式

场景1: redis中的服务只开启了默认的持久策略 RDB模式.

解决方案:

  • 关闭现有的redis服务器.

  • 检查RDB文件是否被覆盖. 如果文件没有覆盖.则重启redis即可.(希望渺茫)

  • 如果flushAll命令,同时执行了save操作,则RDB模式无效.

场景2: redis中的服务开启了AOF模式.

解决方案:

  • 关闭redis服务器.
  • 编辑redis 持久化文件 将flushAll命令删除.
  • 重启redis服务器

Redis数据淘汰算法

众所周知,Redis的所有数据都存储在内存中,但是内存是一种有限的资源,所以为了防止Redis无限制的使用内存,在启动Redis时可以通过配置项maxmemory来指定其最大能使用的内存容量。例如可以通过以下配置来设置Redis最大能使用 1G 内存:maxmemory 1G

**当Redis使用的内存超过配置的 maxmemory 时,便会触发数据淘汰策略。**Redis提供了多种数据淘汰的策略,如下:

  • volatile-lru: 最近最少使用算法,从设置了过期时间的键中选择空转时间最长的键值对清除掉
  • volatile-lfu: 最近最不经常使用算法,从设置了过期时间的键中选择某段时间之内使用频次最小的键值对清除掉
  • volatile-ttl: 从设置了过期时间的键中选择过期时间最早的键值对清除
  • volatile-random: 从设置了过期时间的键中,随机选择键进行清除
  • allkeys-lru: 最近最少使用算法,从所有的键中选择空转时间最长的键值对清除
  • allkeys-lfu: 最近最不经常使用算法,从所有的键中选择某段时间之内使用频次最少的键值对清除
  • allkeys-random: 所有的键中,随机选择键进行删除
  • noeviction: 不做任何的清理工作,在redis的内存超过限制之后,所有的写入操作都会返回错误;但是读操作都能正常的进行

可以在启动Redis时,通过配置项maxmemory_policy来指定要使用的数据淘汰策略。例如要使用volatile-lru策略可以通过以下配置来指定:maxmemory_policy volatile-lru

LRU算法

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面(数据)予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
计算维度: 自上一次以来所经历的时间T.

Redis总结_第40张图片

说明:LRU算法是内存优化中最好用的算法.

LFU算法

LFU(least frequently used (LFU) page-replacement algorithm)。即最不经常使用页置换算法,要求在页置换时置换引用计数最小的页,因为经常使用的页应该有一个较大的引用次数。但是有些页在开始时使用次数很多,但以后就不再使用,这类页将会长时间留在内存中,因此可以将引用计数寄存器定时右移一位,形成指数衰减的平均使用次数。
维度: 引用次数
常识: 计算机左移 扩大倍数
计算机右移 缩小倍数

随机算法

随机删除数据.

TTL算法

说明:将剩余存活时间排序,将马上要被删除的数据,提前删除.

Redis总结_第41张图片

Redis默认的内存优化策略

说明1: Redis中采用的策略定期删除+惰性删除策略

说明2:

  • 定期删除: redis默认每隔100ms 检查是否有过期的key, 检查时随机的方式进行检查.(不是检查所有的数据,因为效率太低.)

    问题: 由于数据众多,可能抽取时没有被选中.可能出现 该数据已经到了超时时间,但是redis并没有马上删除数据.

  • 惰性策略: 当用户获取key的时候,首先检查数据是否已经过了超时时间. 如果已经超时,则删除数据.

    问题: 由于数据众多, 用户不可能将所有的内存数据都get一遍.必然会出现 需要删除的数据一直保留在内存中的现象.占用内存资源.

  • 可以采用上述的内存优化手段,主动的删除.

Redis LRU的具体实现

一致性hash算法

算法介绍

一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,目的是解决分布式缓存的问题。 [1] 在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系。一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题 [2] 。
作用: 解决缓存数据,在哪存储的问题…

算法说明

常识:

  1. 如果数据相同,则hash结果必然相同.
  2. 常见hash值 由8位16进制数组成. 共用多少种可能性? 2^32

Redis总结_第42张图片

平衡性

①平衡性是指hash的结果应该平均分配到各个节点,这样从算法上解决了负载均衡问题 [4] 。
说明:通过虚拟节点实现数据的平衡

Redis总结_第43张图片

单调性

②单调性是指在新增或者删减节点时,不影响系统正常运行 [4] 。
原则: 如果节点新增/减少 应该尽可能保证原始数据尽可能不变.

分散性

③分散性是指数据应该分散地存放在分布式集群中的各个节点(节点自己可以有备份),不必每个节点都存储所有的数据 [4]
将数据分散存储,即使将来服务器宕机,则影响只是一部分,.而不是全部.
谚语: 鸡蛋不要放到一个篮子里.

Redis总结_第44张图片

Redis哨兵工作原理

如果redis分片中有一个节点宕机了,则可能会影响整个服务的运行,redis分片没有实现高可用,所以就需要使用哨兵来监控主机.

Redis总结_第45张图片

原理说明:
1.哨兵监控主机的运行的状态. 通过心跳检测机制(PING-PONG)如果连续3次节点没有响应,则断定主机宕机,哨兵开始进行选举.
2.哨兵通过链接主机,获取主机的相关配置信息(包含主从结构),挑选链接当前主机的从机.根据随机算法挑选出新的主机. 并且将其他的节点设置为新主机的从.

关于分片/哨兵总结

1.分片机制: 可以实现内存数据的扩容. 但是本身没有实现高可用的效果.
2.哨兵机制: 哨兵可以实现redis节点的高可用.但是哨兵本身没有实现高可用的效果.

需求:

  • 不依赖第三方实现高可用
  • 实现内存数据的扩容
  • 各个节点可以高可用.
  • Redis集群搭建 功能包含上述的3种机制. 一般公司中都会采用集群的方式部署redis.

Redis集群宕机条件

宕机条件: Redis中的主机缺失时,并且没有从机替补,Redis内存数据丢失.这时Redis集群崩溃了.

问题1: 6台redis 3主3从(1主1从分为3组). 至少Redis宕机几台集群崩溃. 至少2台 集群崩溃.
问题2: 9台redis 3主6从(1主2从分为3组). 至少宕机几台Redis集群崩溃. 至少5台 集群崩溃.

集群宕机的条件: 当主机的数量不能保证时集群崩溃.
特点:集群中如果主机宕机,那么从机可以继续提供服务,
当主机中没有从机时,则向其它主机借用多余的从机.继续提供服务.如果主机宕机时没有从机可用,则集群崩溃.
答案:9个redis节点,节点宕机5-7次时集群才崩溃.

Redis总结_第46张图片

hash槽算法

Hash槽算法 分区算法.
说明: RedisCluster采用此分区,所有的键根据哈希函数(CRC16[key]%16384)映射到0-16383槽内,共16384个槽位,每个节点维护部分槽及槽所映射的键值数据.根据主节点的个数,均衡划分区间.
算法:哈希函数: Hash()=CRC16[key]%16384

Redis总结_第47张图片

当向redis集群中插入数据时,首先将key进行计算.之后将计算结果匹配到具体的某一个槽的区间内,之后再将数据set到管理该槽的节点中.

Redis总结_第48张图片

关于集群/分片算法说明

问题:一个数据很大.一个槽位不够存怎么办??? 错误?? A 逻辑错误 B. 有道理
解答:
1.一致性hash算法 hash(key) 43亿 按照顺时针方向找到最近的节点 进行set操作.
2.Hash槽算法 crc16(key)%16384 (0-16383) 计算的结果归哪个节点管理,则将数据保存到节点中.
核心知识: 一致性hash算法/hash槽算法 都是用来确定 数据归谁管理的问题. 最终的数据都会存储到node节点中.

问: Redis集群中一共可以存储16384个数据? A 对 B 错 为什么???
小明猜想: 由于redis中共有16384个槽位,所以每个槽位存储一个key.那么不就是16384个key吗??
答案: 错误
原因: Redis集群中确实有16384个槽位.但是这些槽位是用来划分数据归谁管理的.不是用来存储数据的. 并且根据hash计算的规则肯能出现碰撞的问题.比如
hash(key1)%16384=3000
hash(key2)%16384=3000

						说明key1和key2归同一个node管理.
						node.set(key1,value1);
						node.set(key2,value2);
		由于槽位只是用来区分数据,数据到底能存储多少个完成由redis内存决定.

问题: 为Redis集群中最多有多少台主机?? 16384台主机

几台服务器可以搭建集群

公式:存活节点>N/2

算数计算:

1个节点 1-1>1/2 假的
2个节点 2-1>2/2 假的
3个节点 3-1>3/2 真的
结论:集群搭建最小服务器数:3台

集群一般都是奇数台

原因:

3个节点和4个节点都是允许宕机1台,他们的容灾效果相同,所以一般是奇数个

集群的脑裂现象

说明:

由于集群工作过程中主机意外宕机,之后集群开始进行选举,如果出现多次连续平票状态时,则可能出现脑裂现象.

脑裂发生的概率是:1/8=12.5%

Redis总结_第49张图片

你可能感兴趣的:(redis,缓存,java)