Redis


Redis知识点

Redis知识点2

第一部分 Redis存在的问题

一、缓存穿透

  1. 描述:访问一个缓存和数据库都不存在的 key,此时会直接打到数据库上,并且查不到数据,没法写缓存,所以下一次同样会打到数据库上。此时,缓存起不到作用,请求每次都会走到数据库,流量大时数据库可能会被打挂。此时缓存就好像被“穿透”了一样,起不到任何作用。
  2. 解决方案:
    • 接口校验。在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查询中,商品的ID是正整数,则可以直接对非正整数直接过滤等等。
    • 缓存空值。当访问缓存和DB都没有查询到值时,可以将空值写进缓存,但是设置较短的过期时间,该时间需要根据产品业务特性来设置。
    • 布隆过滤器。使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库。
      1. 布隆过滤器的特点是判断不存在的,则一定不存在;判断存在的,大概率存在,但也有小概率不存在。并且这个概率是可控的,我们可以让这个概率变小或者变高,取决于用户本身的需求。
      2. 实现原理:数据结构是一个默认都是0的bit数组,用多个不同的哈希函数生成多个哈希值,再把哈希值指向的bit位置1
      3. 源码解析:google的guava包中提供了布隆过滤器的Java实现,对应的类为BloomFilter(未完成)
      4. 参考链接:https://blog.csdn.net/lotus35/article/details/114005198

二、缓存击穿

  1. 描述:某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。

  2. 解决方案:

    1、加互斥锁。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。

    2、热点数据不过期。直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。

三、缓存雪崩

  1. 描述:大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力骤增,引起雪崩,甚至导致数据库被打挂。

  2. 解决方案:

    1、过期时间打散。既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。

    2、热点数据不过期。该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。

    3、加互斥锁。该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。

四、缓存与数据库双写一致性问题

  1. 由于操作数据库和操作redis缓存不是一个原子操作,且还会存在多个CPU之间并行执行的情况,所以就会有一个线程在操作数据库和缓存的时间节点之间,另外一个线程也在执行操作数据库和缓存,这样就会导致数据库与缓存之间会存在数据不一致的情况。

  2. 三种缓存策略:

    • 1、先更新数据库再更新缓存;2、先删除缓存再更新数据库;3、先更新数据库再删除缓存(抛弃第一种)
    • 第二、三种问题:更新数据库和删除缓存不是一个原子性操作,导致脏数据
    • 第二种方案解决办法:延时双删策略
    • 第三种方案解决办法:引入消息队列或者其他的binlog同步
    • 对于一致性要求不高的情况下,可以使用设置过期时间的方案。
  3. 更新缓存相对于删除缓存的缺点

    • 容易造成脏数据
    • 如果写入的缓存值需要复杂计算。更新缓存频率高会浪费性能。
    • 在写数据库场景多,读数据场景少的情况下,数据很多时候还没被读取到,又被更新了,这也浪费了性能呢
  4. 解决方案:(未理解)

    1、缓存延时双删:(还是可能存在数据不一致的问题)

    • 先删除缓存、再更新数据库、休眠一会(比如1秒)、再次删除缓存。
    • 可以将这一秒内所造成的的缓存脏数据删除

2、删除缓存失败重试解决方案:

  • 方案一:解决第二次删除缓存失败的问题,但是业务线代码造成大量的侵入

    (1)更新数据库数据;(2)缓存因为种种问题删除失败(3)将需要删除的key发送至消息队列(4)自己消费消息,获得需要删除的key(5)继续重试删除操作,直到成功

    缓存删除失败解决方案一

  • 方案二:读取biglog异步删除缓存

    (1)更新数据库数据(2)数据库会将操作信息写入binlog日志当中(3)订阅程序提取出所需要的数据以及key(4)另起一段非业务代码,获得该信息(5)尝试删除缓存操作,发现删除失败(6)将这些信息发送至消息队列(7)重新从消息队列中获得该数据,重试操作。

    缓存删除失败解决方案二

3、消息队列:先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果

4、设置过期时间:每次放入缓存的时候设置一个过期时间,接下来的操作只修改数据库,不操作缓存,等到缓存过期了再去数据库中读取

  1. 一致性

    • 强一致性:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
    • 弱一致性:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
    • 最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型
  2. 三个经典的缓存模式

    • Cache-Aside Pattern:旁路缓存模式,它的提出是为了尽可能地解决缓存与数据库的数据不一致问题。

      1、读流程:读的时候,先读缓存,缓存命中的话,直接返回数据;缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。

      2、写流程:更新数据库,然后再删除缓存

    • Read-Through/Write through(读写穿透):服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过抽象缓存层完成的。

      1、读流程:从缓存读取数据,读到直接返回;如果读取不到的话,从数据库加载,写入缓存后,再返回响应。

      2、写流程:由缓存抽象层完成数据源和缓存数据的更新

    • Write behind(异步缓存写入):由Cache Provider来负责缓存和数据库的读写;只更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库。

  3. 参考链接:

五、Redis并发竞争key(未理解)

  1. 描述:指的是多个redis的client同时set key引起的并发问题。

  2. 解决方案:

    1、分布式锁

    2、消息队列

第二部分 数据类型

键值对数据库结构

Redis中数据类型和数据结构的关系

一、基本数据类型

  1. String字符串:

    • 可以是字符串、整数或者浮点数或者序列化的对象(二进制安全);一个key对应一个value
    • 底层数据结构实现为简单动态字符串(SDS)和直接存储,但其编码方式可以是int、raw或者embstr,区别在于内存结构的不同
    • int编码:字符串保存的是整数值,并且这个正式可以用long类型来表示
    • raw编码:字符串保存的大于32字节的字符串值,则使用简单动态字符串(SDS)结构
    • embstr编码:字符串保存的小于等于32字节的字符串值,使用的也是简单的动态字符串(SDS结构)
  2. List列表:

    • 链表,每个节点包含一个字符串;双端链表实现
    • 编码可以是ziplist和linkedlist之一。
    • ziplist编码:ziplist编码的哈希随想底层实现是压缩列表,每个压缩里列表节点保存了一个列表元素。
    • linkedlist编码:linkedlist编码底层采用双端链表实现,每个双端链表节点都保存了一个字符串对象,在每个字符串对象内保存了一个列表元素。
  3. Set集合:

    • String 类型的无序集合。集合成员是唯一的;哈希表实现

    • 编码可以是intset和hashtable之一。

    • intset编码:intset编码的集合对象底层实现是整数集合,所有元素都保存在整数集合中。

    • hashtable编码:hashtable编码的集合对象底层实现是字典,字典的每个键都是一个字符串对象,保存一个集合元素,不同的是字典的值都是NULL

  4. Hash散列:

    • string 类型的 field(字段) 和 value(值) 的映射表
    • 编码可以是ziplist和hashtable之一。
    • ziplist编码:ziplist编码的哈希对象底层实现是压缩列表,在ziplist编码的哈希对象中,key-value键值对是以紧密相连的方式放入压缩链表的,先把key放入表尾,再放入value;键值对总是向表尾添加。
    • hashtable编码:hashtable编码的哈希对象底层实现是字典,哈希对象中的每个key-value对都使用一个字典键值对来保存。
  5. Zset有序集合:

    • string 类型元素的集合,且不允许重复的成员;每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序;有序集合的成员是唯一的,但分数(score)却可以重复;哈希表实现
    • 编码可以是ziplist和skiplist之一。
    • ziplist编码 :ziplist编码的有序集合对象底层实现是压缩列表,其结构与哈希对象类似,不同的是两个紧密相连的压缩列表节点,第一个保存元素的成员,第二个保存元素的分值,而且分值小的靠近表头,大的靠近表尾。
    • skiplist编码:skiplist编码的有序集合对象底层实现是跳跃表和字典两种;

二、高级数据类型

  1. HyperLogLogs(基数统计)
    • 基数(不重复的元素)——A、B集合中
    • 这个结构可以非常省内存的去统计各种计数,比如注册 IP 数、每日访问 IP 数的页面实时UV、在线用户数,共同好友数等。
  2. Bitmaps (位图)
    • 位图数据结构,都是操作二进制位来进行记录,只有0 和 1 两个状态
    • 统计用户信息,活跃,不活跃! 登录,未登录! 打卡,不打卡! 两个状态的,都可以使用 Bitmaps
  3. geospatial (地理位置)
    • 推算地理位置的信息: 两地之间的距离, 方圆几里的人

三、底层数据结构

  1. SDS:简单动态字符串(simple dynamic string SDS)

    SDS结构
    • 提出原因:C 语言的 char* 字符数组存在缺陷。
      1. 获取字符串长度的时间复杂度为 O(N);
      2. 字符串的结尾是以 “\0” 字符标识,字符串里面不能包含有 “\0” 字符,因此不能保存二进制数据;
      3. 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止
    • 结构:
      1. len,记录了字符串长度。这样获取字符串长度的时候,只需要返回这个成员变量值就行,时间复杂度只需要 O(1)。
      2. alloc,分配给字符数组的空间长度。这样在修改字符串的时候,可以通过 alloc - len 计算出剩余的空间大小,可以用来判断空间是否满足修改需求,如果不满足的话,就会自动将 SDS 的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改 SDS 的空间大小,也不会出现前面所说的缓冲区溢出的问题。
      3. flags,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。
      4. buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。
    • 优点:
      1. O(1)复杂度获取字符串长度:len字段
      2. 二进制安全:不需要用 “\0” 字符来标识字符串结尾了,而是有个专门的 len 成员变量来记录长度,所以可存储包含 “\0” 的数据。
      3. 不会发生缓冲区溢出:当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小
      4. 节省内存空间: 5 种类型—— sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。能灵活保存不同大小的字符串,从而有效节省内存空间
    /*
      * 保存字符串对象的结构
      */
    struct sdshdr {
        // buf 中已占用空间的长度
        int len;
        // buf 中剩余可用空间的长度
        int free;
        // 数据空间
        char buf[]
    };
    
  2. 链表:

    Redis链表结构

    • 概念:当list包含了数量较多的元素,或者列表中包含的元素都是比较长的字符串时,Redis会使用链表作为实现List的底层实现。
    • 优点:
      1. listNode 链表节点的结构里带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表
      2. list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1)
      3. list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1)
      4. listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值
    • 缺点:
      1. 链表每个节点之间的内存都是不连续的,意味着无法很好利用 CPU 缓存
      2. 保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大
    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;
    
  3. 哈希表:

    哈希表

    • 存储key-value键值对,并且key不重复;Hash 对象的一个底层实现就是哈希表,一个哈希表包含多个哈希节点,每个哈希节点保存一个键值对。

    • 哈希冲突:采用了「链式哈希」来解决哈希冲突

    • rehash:随着链表长度的增加,在查询这一位置上的数据的耗时就会增加,链表的查询的时间复杂度是 O(n)。

      哈希表rehash过程

      哈希表rehash过程2

      1. dict 结构体,这个结构体里定义了两个哈希表(ht[2]),进行 rehash 的时候,需要用上 2 个哈希表了。
      2. 过程:
        • 给「哈希表 2」 分配空间,一般会比「哈希表 1」 大 2 倍;
        • 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
        • 迁移完成后,「哈希表 1 」的空间会被释放,并把「哈希表 2」 设置为「哈希表 1」,然后在「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。
      3. 问题:如果「哈希表 1 」的数据量非常大,那么在迁移至「哈希表 2 」的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求。
      4. 解决:渐进式 rehash——将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。
        • 给「哈希表 2」 分配空间;
        • 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上;
        • 在进行渐进式 rehash 的过程中,会有两个哈希表,所以在渐进式 rehash 进行期间,哈希表元素的删除、查找、更新等操作都会在这两个哈希表进行。
        • 在渐进式 rehash 进行期间,新增一个 key-value 时,会被保存到「哈希表 2 」里面,而「哈希表 1」 则不再进行任何添加操作
      5. rehash 触发条件:
        • 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
        • 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。
    1.哈希表结构
    typedef struct dictht {    
        //哈希表数组    
        dictEntry **table;    
        //哈希表大小    
        unsigned long size;      
        //哈希表大小掩码,用于计算索引值    
        unsigned long sizemask;    
        //该哈希表已有的节点数量    
        unsigned long used;
    } dictht;
    
    2.哈希表节点
    typedef struct dictEntry {    
        //键值对中的键    
        void *key;    
        //键值对中的值    
        union {        
            void *val;        
            uint64_t u64;        
            int64_t s64;        
            double d;    
        } v;    
        //指向下一个哈希表节点,形成链表    
        struct dictEntry *next;
    } dictEntry;
    
    typedef struct dict {
        // 类型特定函数
        dictType *type;
        // 私有数据
        void *privdata;
        // 哈希表
        dictht ht[2];
        // rehash索引
        //当rehash不在进行时,值为-1
        in trehashidx; /* rehashing not in progress if rehashidx == -1 */
    } dict;
    
    typedef struct dictType {
        // 计算哈希值的函数
        unsigned int (*hashFunction)(const void *key);
        // 复制键的函数
        void *(*keyDup)(void *privdata, const void *key);
        // 复制值的函数
        void *(*valDup)(void *privdata, const void *obj);
        // 对比键的函数
        int (*keyCompare)(void *privdata, const void *key1, const void *key2);
        // 销毁键的函数
        void (*keyDestructor)(void *privdata, void *key);
        // 销毁值的函数
        void (*valDestructor)(void *privdata, void *obj);
    } dictType;
    
  4. 跳跃表:zskiplist(链表)和zskiplistNode (节点)

    跳表结构
    • 在 Zset 对象的底层实现用到了跳表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。
  • 参考链接:https://blog.csdn.net/lzhcoder/article/details/122539972

    • 结构:
      1. 跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快读定位数据。
      2. 每个节点中有多个指向其他节点的指针,从而快速访问节点。
      3. 跨度:用来记录两个节点之间的距离;为了计算这个节点在跳表中的排位(跳表中的节点都是按序排列的,那么计算某个节点排位的时候,从头节点点到该结点的查询路径上,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的排位。)
    • 查询过程:
      1. 查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。
      2. 在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件:
        • 如果当前节点的权重「小于」要查找的权重时,跳表就会访问该层上的下一个节点。
        • 如果当前节点的权重「等于」要查找的权重时,并且当前节点的 SDS 类型数据「小于」要查找的数据时,跳表就会访问该层上的下一个节点。
      3. 如果上面两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针,然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。
    • 跳表节点层数设置:
      1. 跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)
      2. 跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。
    typedef struct zskiplist {
        // 表头节点和表尾节点
        structz skiplistNode *header, *tail;
        // 表中节点的数量
        unsigned long length;
        // 表中层数最大的节点的层数
        int level;
    } zskiplist;
    
    //跳表的头尾节点,便于在O(1)时间复杂度内访问跳表的头节点和尾节点;
    //跳表的长度,便于在O(1)时间复杂度获取跳表节点的数量;
    //跳表的最大层数,便于在O(1)时间复杂度获取跳表中层高最大的那个节点的层数量;
    
    typedef struct zskiplistNode {    
        //Zset 对象的元素值    
        sds ele;    
        //元素权重值    
        double score;    
        //后向指针:指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便。
        struct zskiplistNode *backward;    
        //节点的level数组,保存每层上的前向指针和跨度    
        struct zskiplistLevel {        
            struct zskiplistNode *forward;        
            unsigned long span;    
        } level[];
    } zskiplistNode;
    
  1. 整数集合(Intset):

    • 一个特殊的集合,里面存储的数据只能够是整数,并且数据量不能过大。底层实现为数组

    • Set集合键的底层实现之一,当一个集合只包含整数元素,并且元素的个数不多时,Redis就会使用整数集合作为集合键的底层实现。

    • 整数集合的升级操作:节省内存资源

      整数集合的升级操作

      1. 概念:当我们将一个新元素加入到整数集合里面,如果新元素的类型(int32_t)比整数集合现有所有元素的类型(int16_t)都要长时,整数集合需要先进行升级,也就是按新元素的类型(int32_t)扩展 contents 数组的空间大小,然后才能将新元素加入到整数集合里,升级的过程中,也要维持整数集合的有序性。
    typedef struct intset {
        // 编码方式
        uint32_t encoding;
        // 集合包含的元素数量
        uint32_t length;
        // 保存元素的数组
        int8_t contents[];
    } intset;
    //保存元素的容器是一个 contents 数组,虽然 contents 被声明为 int8_t 类型的数组,但是实际上 contents 数组并不保存任何 int8_t 类型的元素,contents 数组的真正类型取决于 intset 结构体里的 encoding 属性的值
    
  2. 压缩列表:

    压缩列表

    • 概念:压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。List 对象在数据量比较少的情况下,会采用「压缩列表」作为底层数据结构的实现

    • 结构:

      1. zlbytes,记录整个压缩列表占用对内存字节数;
      2. zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
      3. zllen,记录压缩列表包含的节点数量;
      4. zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。
      5. prevlen,记录了「前一个节点」的长度;
      6. encoding,记录了当前节点实际数据的类型以及长度;
      7. data,记录了当前节点的实际数据;
    • 缺点:

      1. 查找效率:查找非头尾元素时,就没有这么高效了,只能逐个查找,复杂度是 O(N) 了,因此压缩列表不适合保存过多的元素。
      2. 连锁更新问题:插入新节点时,prevlen字段的变化会引起后续节点一起变化
  3. quicklist「双向链表 + 压缩列表」

    quicklist

    • 在 Redis 3.0 之前,List 对象的底层数据结构是双向链表或者压缩列表。然后在 Redis 3.2 的时候,List 对象的底层改由 quicklist 数据结构实现。
    • 解决压缩列表的不足:通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。(并没有完全解决连锁更新的问题)
    • 添加元素:在向 quicklist 添加一个元素的时候,不会像普通的链表那样,直接新建一个链表节点。而是会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构里的压缩列表,如果不能容纳,才会新建一个新的 quicklistNode 结构
    • 结构设计:
    typedef struct quicklist {    
        //quicklist的链表头    
        quicklistNode *head;      
        //quicklist的链表头      
        quicklistNode *tail;     
        //所有压缩列表中的总元素个数    
        unsigned long count;    
        //quicklistNodes的个数    
        unsigned long len;           
        ...
    } quicklist;
    
    typedef struct quicklistNode {    
        //前一个quicklistNode    
        struct quicklistNode *prev;        
        //下一个quicklistNode    
        struct quicklistNode *next;     
        //quicklistNode指向的压缩列表    
        unsigned char *zl;                  
        //压缩列表的的字节大小    
        unsigned int sz;                    
        //压缩列表的元素个数    
        unsigned int count : 16;           
        ....
    } quicklistNode;
    
  4. listpack

    listpack

    • 提出原因:解决连锁更新的问题;最大特点是 listpack 中每个节点不再包含前一个节点的长度。
    • 结构:
      1. encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
      2. data,实际存放的数据;
      3. len,encoding+data的总长度;

参考链接:

第三部分 内存淘汰机制

  1. 描述:一般来说,缓存的容量是小于数据总量的,所以,当缓存数据越来越多,Redis 不可避免的会被写满,这时候就涉及到 Redis 的内存淘汰机制了。我们需要选定某种策略将“不重要”的数据从 Redis 中清除,为新的数据腾出空间。
  2. 分类:
    • noeviction:此策略不会对缓存的数据进行淘汰,当内存不够了就会报错
    • 会进行淘汰的策略:以 volatile 开头的策略只针对设置了过期时间的数据,即使缓存没有被写满,如果数据过期也会被删除。以 allkeys 开头的策略是针对所有数据的,如果数据被选中了,即使过期时间没到,也会被删除。当然,如果它的过期时间到了但未被策略选中,同样会被删除。
      • allkeys-random:随机删除
      • allkeys-lru:使用 LRU 算法进行筛选删除
      • allkeys-lfu:使用 LFU 算法进行筛选删除
      • volatile-random:随机删除
      • volatile-ttl:根据过期时间先后进行删除,越早过期的越先被删除
      • volatile-lru:使用 LRU 算法进行筛选删除
      • volatile-lfu:使用 LFU 算法进行筛选删除
  3. LRU :全称是 Least Recently Used,即最近最少使用,会将最不常用的数据筛选出来,保留最近频繁使用的数据。
  4. LFU: 全称 Least Frequently Used,即最不经常使用策略,基于数据访问次数来淘汰数据的。为每个数据增加了一个计数器,来统计这个数据的访问次数。

第四部分 模式(分布式)

一、主从复制

  1. 概念:主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
  2. 作用
    • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
    • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
    • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
    • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。
  3. 实现原理
    • 连接建立阶段:在主从节点之间建立连接,为数据同步做好准备
    • 数据同步阶段;主从节点之间的连接建立以后,便可以开始进行数据同步,该阶段可以理解为从节点数据的初始化。具体执行的方式是:从节点向主节点发送psync命令,开始同步。
    • 命令传播阶段:主节点将自己执行的写命令发送给从节点,从节点接收命令并执行,从而保证主从节点数据的一致性。
  4. 【数据同步阶段】全量复制和部分复制:
    • 全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点。通过psync命令进行全量复制
      1. 从节点判断无法进行部分复制,向主节点发送全量复制的请求;或从节点发送部分复制的请求,但主节点判断无法进行部分复制;
      2. 主节点收到全量复制的命令后,执行bgsave,在后台生成RDB文件,并使用一个缓冲区(称为复制缓冲区)记录从现在开始执行的所有写命令
      3. 主节点的bgsave执行完成后,将RDB文件发送给从节点;从节点首先清除自己的旧数据,然后载入接收的RDB文件,将数据库状态更新至主节点执行bgsave时的数据库状态
      4. 主节点将前述复制缓冲区中的所有写命令发送给从节点,从节点执行这些写命令,将数据库状态更新至主节点的最新状态
      5. 如果从节点开启了AOF,则会触发bgrewriteaof的执行,从而保证AOF文件更新至主节点的最新状态
    • 部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。
      1. 复制偏移量:主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数,用于判断主从节点的数据库状态是否一致
      2. 复制积压缓冲区:由主节点维护的、固定长度的、先进先出(FIFO)队列;其作用是备份主节点最近发送给从节点的数据
      3. 服务器运行ID(runid):每个Redis节点(无论主从),在启动时都会自动生成一个随机ID;runid用来唯一识别一个Redis节点。
    • psync命令的执行
      1. 首先,从节点根据当前状态,决定如何调用psync命令
      2. 主节点根据收到的psync命令,及当前服务器状态,决定执行全量复制还是部分复制
  5. 【命令传播阶段】心跳机制:心跳机制对于主从复制的超时判断、数据安全等有作用。
    • 主->从(PING):每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。
    • 从->主(REPLCONF ACK):在命令传播阶段,从节点会向主节点发送REPLCONF ACK命令;作用为:实时监测主从节点网络状态、检测命令丢失、辅助保证从节点的数量和延迟
  6. 存在的问题
    • 读写分离及其中的问题
      1. 延迟与不一致问题:由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。
      2. 数据过期问题:在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。
      3. 故障切换问题:节点出故障时需要修改连接,连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。
    • 复制超时问题:
    • 复制中断问题:主从节点超时和复制缓冲区溢出问题导致复制中断
  7. 参考链接:

二、哨兵模式

哨兵模式

  1. 概念:实现哨兵模式就是为了监控主从的运行状况,对主从的健壮进行监控,就好像哨兵一样,只要有异常就发出警告,对异常状况进行处理。

  2. 优点:

    • 「监控」:监控master和slave是否正常运行,以及哨兵之间也会相互监控
    • 「自动故障恢复」:当master出现故障的时候,会自动选举一个slave作为master顶上去。
    • 提高了系统的可用性和性能、稳定性;能够及时发现系统的问题
  3. 缺点:增加了系统的复杂度;故障恢复的时间比较长

  4. 节点通信:

    1. INFO:该命令可以获取主从数据库的最新信息,可以实现新结点的发现
    2. PING:该命令被使用最频繁,该命令封装了自身节点和其它节点的状态数据。
    3. PONG:当节点收到MEET和PING,会回复PONG命令,也把自己的状态发送给对方。
    4. MEET:该命令在新结点加入集群的时候,会向老节点发送该命令,表示自己是个新人
    5. FAIL:当节点下线,会向集群中广播该消息。
    • 「当哨兵与master建立连接后,定期会向(10秒一次)master和slave发送INFO命令,若是master被标记为主观下线,频率就会变为1秒一次。」
    • 定期向_sentinel_:hello频道发送自己的信息,以便其它的哨兵能够订阅获取自己的信息,发送的内容包含「哨兵的ip和端口、运行id、配置版本、master名字、master的ip端口还有master的配置版本」
    • 定期的向master、slave和其它哨兵发送PING命令(每秒一次),以便检测对象是否存活」
  5. 上线和下线:(master状态)

    • 「主观下线」:一时刻哨兵发送的PING在指定时间内没有收到回复
    • 「客观下线」:认为该节点下线的哨兵达到一定的数量
    • 若是没有足够数量的sentinel同意该master下线,则该master客观下线的标识会被移除;若是master重新向哨兵的PING命令回复了客观下线的标识也会被移除。
  6. 选举算法:Raft(基本思路是先到先得)

    • 选举哨兵大佬

      1. 发现master下线的哨兵(sentinelA)会向其它的哨兵发送命令进行拉票,要求选择自己为哨兵大佬。
      2. 若是目标哨兵没有选择其它的哨兵,就会选择该哨兵(sentinelA)为大佬。
      3. 若是选择sentinelA的哨兵超过半数(半数原则),该大佬非sentinelA莫属。
      4. 如果有多个哨兵同时竞选,并且可能存在票数一致的情况,就会等待下次的一个随机时间再次发起竞选请求,进行新的一轮投票,直到大佬被选出来。
    • 选举slave作为master:

      1. 所有的slave中slave-priority优先级最高的会被选中。
      2. 若是优先级相同,会选择偏移量最大的,因为偏移量记录着数据的复制的增量,越大表示数据越完整。
      3. 若是以上两者都相同,选择ID最小的。

三、Cluster模式(集群模式)

  1. 集群模式实现了Redis数据的分布式存储,实现数据的分片,每个redis节点存储不同的内容,并且解决了在线的节点收缩(下线)和扩容(上线)问题。
  2. 数据分区原理:虚拟槽分区算法
    • 哈希分区的基本思路是:对数据的特征值(如key)进行哈希,然后根据哈希值决定数据落在哪个节点。常见的哈希分区包括:哈希取余分区、一致性哈希分区、带虚拟节点的一致性哈希分区等。
    • 虚拟槽分区算法:
      1. 槽是介于数据和实际节点之间的虚拟概念;每个实际节点包含一定数量的槽,每个槽包含哈希值在一定范围内的数据;数据的映射关系:数据hash->槽->实际节点
      2. 步骤:(1)Redis对数据的特征值(一般是key)计算哈希值,使用的算法是CRC16。(2)根据哈希值,计算数据属于哪个槽。(3)根据槽与节点的映射关系,计算数据属于哪个节点。
      3. 扩容和收缩:会重新计算每一个节点负责的槽范围,并发根据虚拟槽算法,将对应的数据更新到对应的节点。

虚拟槽分配

  1. 节点通信:和前面哨兵模式讲的命令基本一样。

  2. 数据请求:在Redis的底层维护了unsigned char myslots[CLUSTER_SLOTS/8] 一个数组存放每个节点的槽信息;每个redis底层还维护了一个clusterNode数组,大小也是16384,用于储存负责对应槽的节点的ip、端口等信息

  3. 优缺点:(1)高效可拓展(2)数据一致性问题;架构复杂性

  4. 集群脑裂:

    • 概念:因为网络问题,导致 redis master 节点跟 redis slave 节点和 sentinel集群处于不同的网络分区,此时因为 sentinel 集群无法感知到 master 的存在,所以将 slave 节点提升为master 节点。此时存在两个不同的 master 节点,就像一个大脑分裂成了两个。

    • 解决方案:配置文件中参数

      min-replicas-to-write 3
      min-replicas-max-lag 10
      //第一个参数表示连接到master的最少slave数量
      //第二个参数表示slave连接到master的最大延迟时间
      //按照上面的配置,要求至少3个slave节点,且数据复制和同步的延迟不能超过10秒,否则的话master就会拒绝写请求,配置了这两个参数之后,如果发生集群脑裂,原先的master节点接收到客户端的写入请求会拒绝,就可以减少数据同步之后的数据丢失。
      
    • https://blog.csdn.net/jack1liu/article/details/124710462

    集群脑裂

参考链接

  1. https://maimai.cn/article/detail?fid=1685414704&efid=G6S_BakYoEiDocGiH5fXFw

第五部分 持久化

一、RDB和AOF

  1. 持久化原因:Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘中,那么一旦服务器进程退出,服务器的数据库状态就会消失(即断电即失),所以需要Redis提供持久化功能。

  2. 持久化方式:

    • RDB持久化:快照方式备份:保存某一时间内的完整数据(Redis默认)
    • AOF持久化:日志方式备份:一开始时刻追加保存数据。
  3. 对比(数据完整性):

    • AOF使用日志方式:每秒或每次修改时,追加保存数据,在系统崩溃时,必定损失一小部分数据,但相对数据完整性最高
    • RDB使用快照的方式:保存某特定时间的完整数据,在保存过程系统不崩溃时,数据保存100%, 在保存过程系统崩溃,由于快照保存比较慢,崩溃时间数据相对大一部分会损失。
  4. RDB:Redis按照一定的时间周期将目前服务中的所有数据全部写入到磁盘中,原理则是将内存中的数据以快照的方式写入二进制文件中,默认的文件名是dump.rdb

    • 手动触发自动触发

      1)执行 save 或者 bgsave 命令;执行 flushall 命令

      2)Redis默认触发机制,当单位时间修改数量达到触发机制时,执行自动触发

    • 快照备份原理:触发RDB持久化机制,创建子进程,保存父进程快照,实现持久化

    • 快照的备份与恢复:只需要将RDB快照备份文件,放在redis启动目录即可,redis启动的时候会自动检查dump.rdb 恢复其中的备份数据!!

    • 优点:

      • 适合大规模的数据恢复:创建子线程进行备份,不影响主线线程工作
      • 对数据的完整性要求不高,RDB是很好的选择,RDB备份的是系统文件快照。
      • 只要在持久化备份时,redis不崩溃,RDB持久化备份数据的完整性是最高的。
    • 缺点:

      • 数据的完整性和一致性不高:周期性的同步,需要一定时间间隔进程操作!因为RDB可能在最后一次备份时宕机了(如果redis意外宕机了,这个最后一次修改时间数据就没有了)
      • 备份创建子线程消耗内存:备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。所以要考虑到大概两倍的数据膨胀性。
  5. AOF:以日志的形式来记录每个写的操作,将redis执行过的所有指令记录下来(读操作不记录),只许追加文件,但不可以改写文件,redis启动之初会读取该文件重新构建数据,换而言之,redis重启的话就根据日志文件的内容将写的指令从前到后执行一次,已完成数据的恢复工作

    • 备份与恢复:备份:以日志的方式记录所有指令记录;恢复:根据日志内容从头执行一遍,完成工作恢复。

    • 失败与恢复:

    • 持久化策略:

      1. appendfsync always #always表示每次写入都执行fsync,以保证数据同步到磁盘
      2. appendfsync everysec #everysec表示每秒执行一次fsync,可能会导致丢失这1s数据
      3. appendfsync no #no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快
    • 优点:三种持久化模式可以选择

    • 缺点:

      • 对于相同数量的数据集而言,AOF文件通常要大于RDB文件;RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
      • AOF运行效率也要比rdb慢:rdb是快照,快照直接拷贝,aof是指令,aof日志是把所有操作过的指令执行一遍,所以RDB运行效率比AOF要快,我们redis默认配置就是rdb持久化。

二、读时共享写时复制copy-on-write(COW)

  1. 概念:写入时复制是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。
  2. 优点:如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。
  3. 实现原理:

第六部分 事务

  1. 概念:Redis事务是一组命令的集合,将多个命令进行打包,然后这些命令会被顺序的添加到队列中,并且按顺序的执行这些命令。(保证「快速、高效」

  2. 步骤:开始事务(MULTI);命令入队;执行事务(EXEC)、撤销事务(DISCARD )

Redis执行命令流程

  1. WATCH 命令:在MULTI命令之前执行的,表示监视任意数量的key;若是被监视的任意一个key被更改,则队列中的命令不会被执行,直接向客户端返回(nil)表示事务执行失败

    redis watch命令数据结构

    • 底层实现中保存了watched_keys 字典,「字典的键保存的是监视的key,值是一个链表,链表中的每个节点值保存的是监视该key的客户端」
    • WATCH 命令的作用, 就是将当前客户端和要监视的键在 watched_keys 中进行关联。
    • watch的触发:在任何对数据库键空间进行修改的命令成功执行之后 =multi.c/touchWatchedKey函数都会被调用 —— 它检查数据库的watched_keys字典, 看是否有客户端在监视已经被命令修改的键, 如果有的话, 程序将所有监视这个/这些被修改键的客户端的REDIS_DIRTY_CAS选项打开。
    • 当客户端发送 EXEC 命令、触发事务执行时, 服务器会对客户端的状态进行检查:
      1. 如果客户端的 REDIS_DIRTY_CAS 选项已经被打开,那么说明被客户端监视的键至少有一个已经被修改了,事务的安全性已经被破坏。服务器会放弃执行这个事务,直接向客户端返回空回复,表示事务执行失败。
      2. 如果 REDIS_DIRTY_CAS 选项没有被打开,那么说明所有监视键都安全,服务器正式执行事务。
  2. UNWATCH命令:取消监视的key。

  3. 错误处理:

    • 错误类别:「语法错误」「运行错误」

      1)即使命令进入队列,只要存在语法错误,该队列中的命令都不会被执行,会直接向客户端返回事务执行失败的提示。

      2)运行错误中,正确的命令被执行,而错误的命令不会不执行;Redis在不执行命令的情况下,是无法发现的。(执行时使用不同类型的操作命令操作不同数据类型就会出现运行时错误)

  4. Redis事务与Mysql事务:

    • 原子性、一致性:Redis的事务为了保证Redis除了客户端的请求高效,去除了传统关系型数据库的「事务回滚、加锁、解锁」这些消耗性能的操作,不具备ACID特性,Redis的事务只能保证单个命令的原子性,数据的一致性也就无法保证
    • 持久化数据:Redis事务的耐久性由服务器所使用持久化模式决定的:
      1. 当服务器在无持久化的内存模式下运作时,事务不具有耐久性。因为一旦服务器停机,服务器所有的数据都将丢失。
      2. 当服务器在ROB持久化模式下运作时,事务同样不具有耐久性。因为服务器只会在特定的保存条件下才会执行BGSAVE命令,并且异步执行的BGSAVE命令不能保证事务的数据第一时间被保存到硬盘上。
      3. 当服务器运行在AOF持久化模式下,并且appendfsync选项的值为always时,程序总会在执行命令之后调用同步(sync)函数,将命令数据真正地保存到硬盘里。
    • 隔离性:因为Redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证, 在执行事务期间不会对事务进行中断,因此,Redis的事务总是以串行的方式运行的,并且 事务也总是具有隔离性的。

参考链接

第七部分 分布式锁

一、分布式锁的三种实现方式

  1. 基于数据库的实现方式

    • 核心思想是:在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引,想要执行某个方法,就使用这个方法名向表中插入数据,成功插入则获取锁,执行完成后删除对应的行数据释放锁。
  2. 基于Redis的实现方式

    • 实现思想:

      1. 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

      2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

      3. 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

  3. 基于ZooKeeper的实现方式

    • 基于ZooKeeper实现分布式锁的步骤如下:
      1. 创建一个目录mylock;
      2. 线程A想获取锁就在mylock目录下创建临时顺序节点;
      3. 获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
      4. 线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
      5. 线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
  4. 参考链接

第八部分 选择Redis的原因

一、Redis优点

  1. Redis优点:响应速度快,支持六种数据类型,操作是原子的,支持主从备份,性能极高
  2. 速度快的原因
    • 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
    • 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
    • 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
    • 使用多路I/O复用模型,非阻塞IO;多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。
    • 使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

二、适用场景

1、 会话缓存(Session Cache)

  • 最常用的一种使用 Redis 的情景是会话缓存(session cache)。用 Redis 缓存会话比其他存储(如 Memcached)的优势在于:Redis 提供持久化。
  • 当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗? 幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台 Magento 也提供 Redis 的插件。

2、 全页缓存(FPC)

  • 除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了 Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。 再次以 Magento 为例,Magento 提供一个插件来使用 Redis 作为全页缓存后端。
  • 此外,对WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。

3、 队列

  • Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis 能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop 操作。
  • 如果你快速的在 Google 中搜索“Redis queues”,你马上就能找到大量的开源项目, 这些项目的目的就是利用 Redis 创建非常好的后端工具,以满足各种队列需求。
  • 例如,Celery 有一个后台就是使用 Redis 作为 broker,你可以从这里去查看。

4,排行榜/计数器

  • Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的 10 个用户–我们称之为“user_scores”,我们只需要像下面一样执行即可: 当然,这是假定你是根据你用户的分数做递增的排序。
  • 如果你想返回用户及用户的分数,你需要这样执行: ZRANGE user_scores 0 10 WITHSCORES Agora Games 就是一个很好的例子,用 Ruby 实现的,它的排行榜就是使用
  • Redis 来存储数据的,你可以在这里看到。

5、发布/订阅

  • 最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统!

第九部分 面试题

  1. Redis有哪些应用场景?你用Redis有哪些场景?使用的是哪些数据结构?
  2. 如何保证缓存与数据库双写的一致性?这种方法有没有存在什么问题?
    • 缓存延时双删:先删除缓存、再更新数据库、休眠一会(比如1秒)再次删除缓存。(删除缓存重试机制)
    • 读取biglog异步删除缓存
  3. Redis怎么实现分布式锁?写代码
  4. 缓存穿透、缓存击穿、缓存雪崩是什么?解决方案?
  5. redis 一主一从,三个哨兵,还是对外提供一个单点服务,这个时候引入3个master节点,每个master都有一个从节点,3个哨兵,这样组成一个 redis 集群?整体对外提供一个缓存服务,怎么设计缓存的key应该命中到哪个主节点上面?
  6. 解释一个IO多路复用机制? redis 采用哪种方式实现的?
  7. Redis持久化方式RDB和AOF各有哪些优缺点?
  8. Redis一致性hash 算法 ?
  9. 跳表怎么做的,如果插入一个值,跳表怎么做,如何裂变,如果让你自己实现这个,你会怎么做,什么情况下会发生裂变等等。
  10. RDB中fork子进程的实现方式
    • fork是linux系统的调用:在当前进程中,fork一个子进程,子进程最初与主进程是共享一份内存区域的。由于主进程不断进行数据的写操作,与子进程存在并发冲突问题。
    • 此时,redis采用写时复制技术(cow):当主进程写操作时,首先会复制一份将要涉及写操作的内存页。然后主进程在新复制的内存页上进行写操作,原有内存页继续供子进程持久化。
  11. redis的原子命令有哪些?
  12. 缓存一致性方案中先删缓存的作用是什么?
posted @ 2021-12-20 18:10  汤十五  阅读(114)  评论(0)    收藏  举报