redis常见面试题

## **<center>redis</center>>**

### 数据类型

####     string
        Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,内部为当前字 符串实际分配的空间 capacity 一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,为了避免加倍后的冗余空间过大而导致浪费,所以扩容时一次只会多扩1M的空间。需要注意的是 字符串最大长度为 512M。

####     list
        Redis的列表相当于Java语言里面的LinkedList,注意它是链表而不是数组。这意味着list的插入和删除操作非常快,时间复杂度为O(1),但是索引定位很慢,时间复杂度为 O(n)。当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。
    
        Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进Redis 的列表,另一个线程从这个列表中轮询数据进行处理。
    
        Redis底层存储的还不是一个简单的linkedlist,而是称之为快速链表quicklist的一个结构。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如当列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。所以Redis将双端链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

####     hash
        Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞 时,就会将碰撞的元素使用链表串接起来。不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为 Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。
        渐进式 rehash ,会在rehash的同时,创建一个索引计数器并把它设置为0,保留新旧两个hash结构,查、删、改的时候会同时查询两个hash结构,增加的话只会增加到新的hash结构中,这样也是为了去保证旧的hash结构只减不增。然后在后续的定时任务中以及hash的子指令中,循序渐进地将旧 hash 的内容 一点点迁移到新的 hash 结构中,每次rehash完成后都会将计数器+1。当 hash 移除了最后一个元素之后,计数器设置为-1,该数据结构自动被删除,内存被回收。

####     set
        Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的 内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。
        当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。 set结构可以用来存储活动中奖的用户ID,因为有去重功能,可以保证同一个用户不会中奖两次。

####     zset
        Redis的有序集合它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个score,代表这个value的排序权重。它的内部实现用的是一种叫着「跳跃列表」的数据结构。zset中最后一个value被移除后,数据结构自动删除,内存被回收。

####     通用规则
        list/set/hash/zset 这四种数据结构是容器型数据结构,它们共享下面两条通用规则:
            1、create if not exists
            2、drop if no elements

### 数据结构
####     sds
        Redis 的字符串叫 sds,也就是 Simple Dynamic String。它的结构是一个带长度信息的字节数组。
            struct SDS<T> {
                T capacity; // 数组容量
                T len; // 数组长度
                byte flags; // 特殊标识位,不理睬它
                byte[] content; // 数组内容
            }
        content里面存储了真正的字符串的内容,capacity是分配的数组长度,len是字符串实际的长度
        Redis 的字符串有两种存储方式,在长度特别短时,使用 emb 形式存储 (embeded),当 长度超过 44 时,使用 raw 形式存储。embstr 存储形式是这样一种存储形式,它将 RedisObject对象头和SDS对象连续存在一起,使用malloc方法一次分配。而raw存储形式不一样,它需要两次malloc,两个对象头在内存地址上一般是不连续的。而内存分配器 jemalloc/tcmalloc 等分配内存大小的单位都是 2、4、8、16、32、64 等 等,为了能容纳一个完整的 embstr 对象,jemalloc 最少会分配 32 字节的空间,如果字符 串再稍微长一点,那就是 64 字节的空间。如果总体超出了 64 字节,Redis 认为它是一个 大字符串,不再使用 emdstr 形式存储,而该用 raw 形式。
        44=64-16-3-1   
            64字节是规定embeded最大的占用内存,16是redis对象头的长度,3是sds头的长度,1是最后的'\0'。

####     dict
        dict 是 Redis 服务器中出现最为频繁的复合型数据结构,除了 hash 结构的数据会用到字典外,整个 Redis 数据库的所有 key 和 value 也组成了一个全局字典,还有带过期时间的 key 集合也是一个字典。zset 集合中存储 value 和 score 值的映射关系也是通过 dict 结构实现的。
        dict 结构内部包含两个 hashtable,通常情况下只有一个 hashtable 是有值的。但是在dict 扩容缩容时,需要分配新的 hashtable,然后进行渐进式搬迁,这时候两个 hashtable 存储的分别是旧的 hashtable 和新的 hashtable。待搬迁结束后,旧的 hashtable 被删除,新的hashtable 取而代之。
        hashtable 的结构和 Java 的HashMap 几乎是一样的,都是通过分桶的方式解决 hash 冲突。第一维是数组,第二维是链表。数组中存储的是第二维链表的第一个元素的指针。
    
        扩容
            正常情况下,当 hash 表中元素的个数等于第一维数组的长度时,就会开始扩容,扩容 的新数组是原数组大小的 2 倍。但是当 Redis 正在做 bgsave,为了减少内存页的过多分离 (Copy On Write),Redis 尽量不去扩容 (dict_can_resize),但是如果 hash 表已经非常满 了,元素的个数已经达到了一维数组长度的 5 倍 (dict_force_resize_ratio),说明 hash 表 已经过于拥挤了,这个时候就会强制扩容。
        缩容
            当 hash 表因为元素的逐渐删除变得越来越稀疏时,Redis 会对 hash 表进行缩容来减少。hash 表的第一维数组空间占用。缩容的条件是元素个数低于数组长度的 10%。缩容不会考 虑 Redis 是否正在做 bgsave。

####     ziplist
        Redis 为了节约内存空间使用,zset和hash容器对象在元素个数较少的时候,采用压缩列表进行存储。压缩列表是一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。
        因为 ziplist 都是紧凑存储,没有冗余空间 。意味着插入一个新的元素就需要调用 realloc 扩展内存。取决于内存分配器算法和当前的 ziplist 内存 大小,realloc 可能会重新分配新的内存空间,并将之前的内容一次性拷贝到新的地址,也可能在原有的地址上进行扩展,这时就不需要进行旧内容的内存拷贝。
        如果 ziplist 占据内存太大,重新分配内存和拷贝内存就会有很大的消耗。所以 ziplist 不适合存储大型字符串,存储的元素也不宜过多。

####     quicklist
        在Redis早期的时候存储 list 列表数据结构使用的是压缩列表 ziplist 和普通的双向链表linkedlist,也就是元素少时用 ziplist,元素多时用 linkedlist。但是链表的附加成本相对比较高,prev与next指针就需要16字节,而且每个节点的内存都是单独分配,会让内存产生碎片化,从而影响内存管理的效率,在后面就加入了quicklist。
        它相当于ziplist与linkedlist的结合体,将linkedlist按段划分,然后每一段用ziplist紧凑存储,多个ziplist采用双向指针连接而且ziplist是可以深度压缩
        quicklist 默认的压缩深度是 0,也就是不压缩。压缩的实际深度由配置参数 list- compress-depth决定。为了支持快速的 push/pop 操作,quicklist 的首尾两个 ziplist 不压 缩,此时深度就是 1。如果深度为 2,就表示 quicklist 的首尾第一个 ziplist 以及首尾第二 个 ziplist 都不压缩。

####     skiplist
        Redis 的 zset 是一个复合结构,一方面它需要一个 hash 结构来存储 value 和 score 的 对应关系,另一方面需要提供按照 score 来排序的功能,还需要能够指定 score 的范围来获 取 value 列表的功能,这就需要另外一个结构跳跃列表。
    
        Redis 的跳跃表共有 64 层,意味着最多可以容纳 2^64 次方个元素。每一个 kv 块对应的结构如下面的代码中的 zslnode 结构,kvheader 也是这个结构,只不过 value 字段是 null 值——无效的,score 是Double.MIN_VALUE,用来垫底的。kv 之间使用指针串起来形成了双向链表结构,它们是有序 排列的,从小到大。不同的 kv 层高可能不一样,层数越高的 kv 越少。同一层的 kv会使用指针串起来。每一个层元素的遍历都是从 kv header 出发。
    
        struct zslnode {
            string value;
            double score;
            zslnode*[] forwards; //多层连接指针
            zslnode* backward; //回溯指针
        }
            struct zsl {
            zslnode* header; //跳跃列表头指针
            int maxLevel; //跳跃列表当前的最高层
            map<string, zslnode*> ht; // hash 结构的所有键值对
        }
    
        查找:我们要定位到那个某个kv,需要从header的最高层开始遍历找到第一个节点(最后一个比「我」小的元素),然后从这个节点开始降一层再遍历找到第二个节点 (最 后一个比「我」小的元素),然后一直降到最底层进行遍历就找到了期望的节点 (最底层的最 后一个比我「小」的元素)。
        我们将中间经过的一系列节点称之为「搜索路径」,它是从最高层一直到最底层的每一层最后一个比「我」小的元素节点列表。有了这个搜索路径,我们就可以插入这个新节点了。不过这个插入过程也不是特别简单。因为新插入的节点到底有多少层,得有个算法来分配一下,跳跃列表使用的是随机算法。
    
        插入:对于每一个新插入的节点,都需要调用一个随机算法给它分配一个合理的层数。直观上期望的目标是 50% 的 Level1,25% 的 Level2,12.5% 的 Level3,一直到最顶层 2^-63,因为这里每一层的晋升概率是 50%。
        首先我们在搜索合适插入点的过程中将「搜索路径」摸出来了,然后就可以开始创建新节点了,创建的时候需要给这个节点随机分配一个层数,再将搜索路径上的节点和这个新节点通过前向后向指针串起来。如果分配的新节点的高度高于当前跳跃列表的最大高度,就需要更新一下跳跃列表的最大高度。
    
        删除:删除过程和插入过程类似,都需先把这个「搜索路径」找出来。然后对于每个层的相关 节点都重排一下前向后向指针就可以了。同时还要注意更新一下最高层数 maxLevel。
    
        更新:当我们调用 zadd 方法时,如果对应的 value 不存在,那就是插入过程。如果这个value 已经存在了,只是调整一下 score的值,那就需要走一个更新的流程。假设这个新的score值不会带来排序位置上的改变,那么就不需要调整位置,直接修改元素的score值就可以了。但是如果排序位置改变了,那就要调整位置。一个简单的策略就是先删除这个元素,再插入这个元素,需要经过两次路径搜索。Redis就是这么干的。 不过 Redis 遇到 score 值改变了就直接删除再插入,不会去判断位置是否需要调整
            如果 score 值都一样呢?
                在一个极端的情况下,zset 中所有的 score 值都是一样的,zset 的查找性能会退化为 O(n) 么?Redis 作者自然考虑到了这一点,所以 zset 的排序元素不只看 score 值,如果 score 值相同还需要再比较 value 值 (字符串比较)。

####     Rax
        Rax 是 Redis 内部比较特殊的一个数据结构,它是一个有序字典树 (基数树 Radix Tree),按照 key 的字典序排列,支持快速地定位、插入和删除操作。Redis 五大基础数据结 构里面,能作为字典使用的有 hash 和 zset。hash 不具备排序功能,zset 则是按照 score 进 行排序的。rax 跟 zset 的不同在于它是按照 key 进行排序的。
        你也可以将公安局的人员档案信息看成一棵radixtree,它的key是每个人的身份证号,value是这个人的履历。因为身份证号的编码的前缀是按照地区进行一级一级划分的, 这点和单词非常类似。有了这棵树,你就可以快速地定位出人员档案,还可以快速查询出某个小片区都有哪些人。
    
        Rax被用在RedisStream结构里面用于存储消息队列,在Stream里面消息ID的前缀是时间戳+序号,这样的消息可以理解为时间序列消息。使用Rax结构进行存储就可以快 速地根据消息 ID 定位到具体的消息,然后继续遍历指定消息之后的所有消息。
        Rax 是一棵比较特殊的radixtree,它在结构上不是标准的radixtree。如果一个中间节点有多个子节点,那么路由键就只是一个字符。如果只有一个子节点,那么路由键就是一个字 符串。后者就是所谓的「压缩」形式,多个字符压在一起的字符串。

####    位图
            概念
                redis的数据结构之一,是一个byte数组,会自动扩容,当偏移量超过现有的内容范围,就会自动将数组进行零扩充()。如果要存hello的话,就需要将这个字符串的ASCII码找到,然后再不停地去存。位数组的存储字符的的位顺序是相反的。setbit s 1 1  setbit s 2 1  setbit s 5 1 后面的1就是为1,前面的数字是第几位,如果需要获取的话就只需要 get s 就可以了
            特性
                零存整取、零存零取、整存零取。零存:就是用setbit挨个设置,整存就是使用字符串去填充所有的位数组,零取就是获取这个对象的第n位是0/1,整取就是获取这个对象的整体。
                节省空间:因为是位图,每次存储0或者1,在一些特定情况下可以减少很多存储空间
                为布隆过滤器打基础。

####    HyperLogLog
            HyperLogLog 提供不精确的去重计数方案,虽然不精确但是也不是非常不精确,标准误差是 0.81%,这样就适用于一些知道大概的形式,而不需要具体的数据的场景。提供了两个指令 pfadd 和 pfcount,根据字面意义很好理解,一个是增加 计数,一个是获取计数。pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用 户 ID 塞进去就是。pfcount 和 scard 用法是一样的,直接获取计数值。HyperLogLog 除了前面说的 pfadd 和 pfcount 之外,还提供了第三个指令 pfmerge,用于 将多个 pf 计数值累加在一起形成一个新的 pf 值。HyperLogLog 这个数据结构不是免费的,不是说使用这个数据结构要花钱,它需要占据一定12k 的存储空间,所以它不适合存储单个用户的信息,适合存储上千万上亿的用户信息。
            它的存储空间采用稀疏矩阵存储,空间占用很小,仅仅在计数慢慢变大,稀疏矩阵占 用空间渐渐超过了阈值时才会一次性转变成稠密矩阵,才会占用 12k 的空间。

###    布隆过滤器:
        在Redis4之后出现的插件,为RedisServer服务。
    
        如果需要自定义参数的布隆过滤器,就得要在bf.add之前显示调用bf.reserve,它有三个参数key, error_rate 和 initial_size。错误率越低,需要的空间越大。initial_size 参数表示预计放 入的元素数量,当实际数量超出这个数值时,误判率会上升。
    
        所以需要提前设置一个较大的数值避免超出导致误判率升高。如果不使用 bf.reserve,默 认的 error_rate 是 0.01,默认的 initial_size 是 100。
    
        布隆过滤器的 initial_size 估计的过大,会浪费存储空间,估计的过小,就会影响准确 率,用户在使用之前一定要尽可能地精确估计好元素数量,还需要加上一定的冗余空间以避 免实际元素可能会意外高出估计值很多。
    
        布隆过滤器的 error_rate 越小,需要的存储空间就越大,对于不需要过于精确的场合, error_rate 设置稍大一点也无伤大雅。比如在新闻去重上而言,误判率高一点只会让小部分文 章不能让合适的人看到,文章的整体阅读量不会因为这点误判率就带来巨大的改变。
    
        当我们在往里面插入key的时候,尽量不要让实际元素数>初始化的数,如果超过了初始化的数,就得要对这个布隆过滤器进行重载,然后分配一个更大的内存。

####    原理:
            其实就是多个hash函数,当我们向布隆过滤器添加一个元素的时候,会用多个hash函数对key算出一个整数的索引,然后再将相应的位置变为1,每一个hash函数会算出不同的位置。所以当我们在判断一个key在不在的时候,就会通过对这个key多个hash时的索引,看这几个索引在的位置是不是都为1,如果都为1就说明存在,如果没有1说明不存在。所以对布隆过滤器而言是有一定的误差的,但是当我们在判断某个元素不存在的时候,这个key肯定不存在,但是在判断存在的时候,就有可能出现问题,因为可能会有其他key的hash函数将这个位置变为1,虽然概率比较小,但是还是存在这种情况下的。所以在判断存在的时候就只是极可能存在而不是一定存在
    
            错误率计算公式:
                k=0.7*(l/n) # 约等于
                f=0.6185^(l/n)

####    特点:
            1、位数组相对越长 (l/n),错误率 f 越低,这个和直观上理解是一致的
            2、位数组相对越长 (l/n),hash 函数需要的最佳数量也越多,影响计算效率
            3、当一个元素平均需要 1 个字节 (8bit) 的指纹空间时 (l/n=8),错误率大约为 2%
            4、错误率为 10%,一个元素需要的平均指纹空间为 4.792 个 bit,大约为 5bit
            5、错误率为 1%,一个元素需要的平均指纹空间为 9.585 个 bit,大约为 10bit
            6、错误率为 0.1%,一个元素需要的平均指纹空间为 14.377 个 bit,大约为 15bit

####    用处:
            布隆过滤器可以显著降低数据库的 IO 请求数量。当用户来查询某个 row 时,可以先通过内存中的布隆过滤器过滤掉大量不存在的 row 请求,然后再去磁盘进行查询。
            在爬虫的时候,我们就得需要对URL进行去重,已经爬过的页面那就不需要再爬了,如果用set去装的话就会非常浪费空间,如果使用布隆过滤器的话可以大幅降低去重存储消耗,只不过也会使得爬虫系统 错过少量的页面。
            邮箱系统的垃圾邮件过滤功能也普遍用到了布隆过滤器,因为用了这个过滤器,所以平时也会遇到某些正常的邮件被放进了垃圾邮件目录中,这个就是误判所致,概率很低。


### 持久化
#### rdb
        RDB其实也就是我们常说的快照,备份的是全量数据,以二进制文件的形式存放在内存中,存储的非常紧凑。因为redis是一个单线程的程序,这个线程同时负责多个客户端的并发读写操作和内存结构的逻辑读写,在快照过程中,Redis必须进行文件IO操作,但是文件IO操作不能用多路复用,所以就会导致文件IO操作会大幅度的拖累服务器的性能。所以,redis采用的是COW机制来作为快照持久化。在持久化的过程中,并不是这个主线程去进行持久化操作而是先fork出一个子进程,这个子进程才是处理快照操作的进程,主进程还是继续接收客户端的请求。这个时候子进程在做持久化操作,所以不会修改内存结构,但是主进程还在接收客户端的请求,这时候肯定会去修改内存结构。所以就有了COW的用武之处,COW会将某一个瞬间的数据快照,生成某个时间点的数据快照,然后去操作这个快照。
        阻塞的时机,RDB有两种命令,一个是save一个是bgsave。前者是父进程直接去进行备份,在此期间做的任何操作都会被阻塞,后者是fork出一个子进程,由子进程去做备份,但是它并不是不会阻塞的。在fork的时候,它会阻塞一会,等到fork成功之后,父进程就可以继续的接收到外界的命令了。
#### aof    

        开启后,所有的写命令会全部加载到一个buffer中,然后这个buffer根据配置的同步策略来向硬盘同步。有三种同步策略,分别是每写入一次就同步一次,每一秒同步一次还有就是不同步等待操作系统去进行同步。
        
        连续的增量备份,与MySQL的binlog比较类似。AOF记录的是数据修改的指令记录文本,随着时间的增长,这个文本会变得越来越大。为此,redis有了AOF重写的功能,将一些没有用的命令给删掉,只保留最后一次有效修改的命令,bgrewriteaof命令。
    
        重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常 使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实 例很大的情况下,启动需要花费很长的时间。Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文 件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自 持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可 以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。
        
        当父进程接收到重写的命令时,它首先会去fork出一个子进程,在fork过程中依然会有阻塞,还会带来另外一个问题就是AOF重写过程中,如果有新的命令对数据进行修改的话就会导致重写后的AOF和现有的数据库数据不一致。
        
        针对这个问题,AOF增加了一个缓冲区叫做AOF重写缓冲区,重写过程中,如果有新的修改数据的命令出现时,它首先会将命令存入到AOF文件,然后再把命令写入到AOF缓冲区中,等到重写结束后,就会把缓冲区的内容再重新写入到AOF文件中,这样就是为了保证从fork子线程之后,执行的所有写操作都会被加入到重写缓冲区中,而且也对现有的AOF文件处理不会有太大的影响,最后的话就会将AOF改名,把旧的AOF覆盖。这时也会进行阻塞
#### 优缺点
​    RDB存在哪些优势呢?
​        

        1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
        
        2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
        
        3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
        
        4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
        
    RDB又存在哪些劣势呢?
        
        1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
        
        2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
    
    AOF的优势有哪些呢?
        
        1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。
    
        2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题
    
        3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
        
        4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
    
    AOF的劣势有哪些呢?
        1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
        
        2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
    
    二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。    

 

### fork子线程时可能会出现的问题

在fork子线程时,它会将父进程的空间内存页表同时给复制过去。在复制空间内存页表的时候,对应的内存空间时不能进行修改的,也就是在这个时候会出现阻塞问题。

### setnx

    我们可以用redis的setnx去实现一个分布式锁,如果仅仅用setnx的话是有很大的问题的。如果一个线程获取了这个锁之后,在执行过程中还没来得及del的时候,程序出现了异常,就会陷入到一种死锁状态。为了防止这种情况的出现,可以加一个expire,这样有了过期时间后,就会减少一部分问题。但是还会有一些问题,就比如说setnx之后expir之前的这个时间段出现异常也会导致死锁状态。所以为了防止这种情况出现,可以把nx与ex同时作为set的参数,这样就可以达到一种原子性的状态。
    
    不支持可重入锁,所以如果分布式要使用可重入的话就得在逻辑层上修改,redis并不支持。所以,我们应该避免用redis的setnx做可重入锁的逻辑。

### scan

        当我们需要从海量的key中获取其中具有某些特性的key时,很多人第一个想法就是keys,举个栗子,我想要获取前缀是common的key时,很多人就会选择用这条命令去寻找符合自己预期的key。 ‘keys common*’ 但是,在我们的key非常多的时候,或者符合条件的key太多的时候,你就知道这个命令会造成多大的破坏。因为keys命令是遍历所有的key,所以说它的时间复杂度是O(n),如果我们的数据集非常大,就会导致redis服务卡顿,所有redis的读写命令都会被延后甚至超时,因为redis是单线程的程序,它需要顺序的去执行所有指令,当keys命令这里不停地运行,就会导致其他命令发生阻塞,必须等待keys命令执行完才可以执行。
    
        为了解决这种问题,redis引入了scan命令。
    
        scan命令拥有很多优点:
    
            1、通过游标分步进行,不会阻塞线程
    
            2、提供limit参数,可以控制返回条数,不像keys会返回所有的条数
    
        当然也会有比较多的缺点:
    
            1、返回的结果可能有重复,需要客户端去重
    
            2、遍历的过程中如果修改了数据,不能保证这个数据会被便利到
    
            3、单次返回的结果是null,并不意味着遍历结束了,应该看游标的返回值。因为limit并不是说返回的结果条数,而是一次遍历的槽值,

### 过期策略
        redis中所有的数据结构都可以设置过期时间,时间一到就会自动删除。但是呢,redis是一种单线程的NOSQL型数据库,当我们在删除过期key的时候也会占用进程处理的时间,如果删除的过于繁忙可能会导致线上的指令出现卡顿,为了解决这个问题,redis将所有的设置了过期时间的key存放在一个字典中,然后通过两种方式来删除过期的key。
    
        定时集中删除:定时遍历这个字典,然后删除到期的key。默认10次/s,但是不会扫描所有的过期字典中的key,而是先在所有的key中随机挑选出20个key,删除这20个key中过期的key。如果删除的过期key比率超过1/4就继续开始的步骤,然后多次循环之后,当过期key变得比较稀散了才会停止。
        
        零散惰性删除:在client访问这个key的时候先看这个key有没有过期,如果过期了就删除。


​        


### 淘汰策略

        redis是一款非常强大的基于内存的NOSQL型数据库,但是呢,当redis的内存超过了物理机的内存之后,内存的数据就会有磁盘产生频繁的交换,交换会让redis的性能急剧下降。所以在生产环境这我们是不会让redis出现这种情况的。所以就得要限制最大使用内存,redis里面有一个参数是maxmemory,来限制最大内存量。如果超过这个参数的数值的话就会出发淘汰策略。
    
        淘汰策略种类:
            noeviction:不会继续服务write服务,但是delete、read服务还是正常的,这样可以保证不会丢失数据,但是会让线上的服务无法持续进行。这是默认的淘汰策略
    
            volatile-lru:尝试着去淘汰设置了超时时间的键,优先淘汰使用最少的key,如果没有设置过期时间的key就不会被淘汰,可以保证需要序列化的数据不会丢失。因为淘汰的只是设置了过期时间并且使用最少的key
    
            volatile-ttl:和上面一样,但是优先淘汰的不是使用最少的key,而是淘汰最少ttl(寿命)的key。ttl越小越先被淘汰。
    
            volatile-random:和上面一样,不过是在过期key的集合中随机淘汰。
    
            allkeys-lru:相比前面的volatile淘汰的时设置有过期时间的key,这个的范围是所有的key。它淘汰的是在所有key中,使用最少的key。
    
            allkeys-random:在所有key中随机淘汰,风险性较高。


### 缓存穿透

        概念:
            用户的请求后,在数据库里没有,那么自然redis里也没有。这样就会直接穿过redis,到达DB,这就是缓存穿透。
    
        解决方案:
            缓存空值
                如果一个数据查询时空的时候,就先将这个空值去缓存到redis中,然后第二次的时候就会有值了。而不会继续访问数据库
            布隆过滤器
                和缓存空值比较类似,不过它是将空值放到布隆过滤器中,第二次查询的时候,先去布隆过滤器查,如果在布隆过滤器里面有的话就会直接返回空,如果没有再去查。
                优点:底层是bitmap,占据的空间比较少 。
                     性能比较高


### 缓存雪崩

        概念
            如果缓存在一段时间内大范围的过期,而新的缓存还没有更新出来,就会导致发生大范围的缓存穿透。所有的查询压力全部放在了数据库上,对数据库的CPU和内存造成了很大压力,甚至宕机这就是缓存雪崩
    
        解决方案
            1)加锁排队
                用setnx去加锁,如果成功,则把这条数据放在数据中,再load进缓存,如果失败则进行一次get操作
            2)双重缓存
                设置两个缓存,C1与C2,C1的过期时间可以设置的相对比较短,C2的过期时间可以设置的相对比较长。我们遇到请求可以先去查询C1,如果C1查询不到可以去查询C2。如果C2也查询不到再去数据库查询,这样减少了DB的压力,也可以预防缓存雪崩
            3)数据预热
                系统上线之前,先将相关的数据加载到缓存系统中。通过缓存reload机制,预先去更新缓存,在发生大的并发访问之前手动出发加载不同的key。
            4)定时更新缓存
                时效性要求不高的缓存,可以在容器启动时进行初始化加载,采用定时任务来更新或者删除缓存。
            5)随机时间
                给缓存的失效时间加一个随机值,尽量让缓存的失效时间均匀一些。

### 主从同步
        概念
            有主节点和从节点,主节点负责接收数据然后更新自身数据,再把数据传输到从节点上,而从节点就只是对外提供查询服务,而不会去主动地写服务,只有当主节点将数据同步给从节点的时候,这时候才会去修改数据。当我们在使用redis的时候,不可能只使用单机部署,肯定是通过多台机器部署然后设置主从模式。当主节点挂掉了,我们的OP同学还可以让从节点顶替主节点,以保证服务的可用。否则只等待主节点去恢复,会导致线上服务不可用,造成很大的损失。
    
            redis的主从同步是异步的,所以redis不满足CAP理论中的C(强一致性),满足的是A(可用性).redis保证的一致性是最终一致性,所以为了主从节点的数据一致,从节点会有各种方法去追上主节点的数据。
        方式
            增量同步
                主节点会将那些对自己有影响的指令记在小本本(本地的内存buffer)上。然后用异步的方式将这小本本上的数据发给从节点,从节点接收到数据后,开始执行小本本上的操作。在执行的过程中还会给主节点返回一个offset,表示自己执行到了哪里了。我们都知道,内存是很宝贵的。这里的内存buffer也是宝贵的,所以我们要去循环利用这里的内存buffer。这里的内存buffer就相当于一个环链表。如果内容满了,就会把之前的数据覆盖然后重新写,所以这里的指令并不是永远存在的。这就导致了一个问题,就是当出现网络问题之后,主从无法通信,当主接收到了很多修改的命令的时候可能会把这个环写满,然后继续写,就会导致主从通信恢复后,从节点和主节点的数据永远不一致的问题。为了解决这个尴尬的问题,就有了全量同步
    
            全量同步
                这里同步的是所有的数据,先在主节点上执行一次bgsave,将所有的数据写到磁盘文件中,然后再把这个文件丢给从节点。从节点接收后,先将自己的数据清空,然后把这份数据copy一份到自己的内存里。这个同步过程并不是阻塞的,这时候主节点还在对外提供服务,所以这时候可能会有修改指令进来,buffer也得继续写。
    
    线程模型
        概念
            Redis有了自己实现的网络事件处理器,它是基于Reactor模式开发的,叫做文件事件处理器。它总共分为四个部分:IO多路复用、多个套接字、文件事件分派器、事件处理器。
    
            如果这时有一个Redis客户端向Redis服务器发起连接,那么监听套接字将产生AE_READABLE事件, 触发连接应答处理器执行:处理器会对客户端的连接请求进行应答, 然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器进行关联,使得客户端可以向主服务器发送命令请求。之后,客户端向Redis服务器发送一个命令请求,那么客户端套接字将产生 AE_READABLE事件,引发命令请求处理器执行,处理器读取客户端的命令内容, 然后传给相关程序去执行。执行命令将产生相应的命令回复,为了将这些命令回复传送回客户端,服务器会将客户端套接字的AE_WRITABLE事件与命令回复处理器进行关联:当客户端尝试读取命令回复的时候,客户端套接字将产生AE_WRITABLE事件, 触发命令回复处理器执行, 当命令回复处理器将命令回复全部写入到套接字之后, 服务器就会解除客户端套接字的AE_WRITABLE事件与命令回复处理器之间的关联。
    
            https://juejin.im/post/5dabdb1ee51d45216d7b166a
    
    哨兵模式:
        概念:它是Redis的一种高可用架构,为了改进主从模式下主节点故障必须得要人工的去将从节点改为主节点,并且还要通知改变了主节点地址.里面包含了Sentinel节点和数据节点。Sentinel节点不会去存储数据,它只会监控数据节点和除了他之外的其他Sentinel节点。如果发现节点没有消息传过来之后就会下线这个节点,如果这个节点是主节点就会与其他的Sentinel节点进行协商,如果大部分的Sentinel节点觉得这个主节点不可达之后就会下线这个节点,然后推选出一个Sentinel节点作为大哨兵去完成故障自动迁移的操作,然后将这个变化通知给应用放。Sentinel节点必须是奇数,而且数量>2.
    
        哨兵是通过什么来获得各个节点的信息的呢?它这里有三种定时任务:
            1. 每隔10s,哨兵会向数据节点发送info指令,从而获取拓扑结构。
            2. 每隔2s,哨兵会向_sentinel_的hello频道发送 当前哨兵节点的信息还有对主节点的判断。这样是为了可以让新的哨兵节点加入进来。
            3. 每隔1s,当前哨兵节点会向其他的哨兵节点和数据节点发送ping命令,看是否得到回复.这样也是为了监控每个节点.
    
        主观下线:再哨兵向其他的节点发送ping命令之后,如果有节点没在规定的时间内获取到回复的话,哨兵节点就会认为这些节点已经下线.默认时间为30s
    
        客观下线:如果哨兵下线的是主节点的话,就会和其他的哨兵去发送对主节点的判断,如果有一半以上的哨兵觉得这个主节点是有问题的话,就会认为这个主节点是客观下线。所以说,客观下线是针对于主节点的。
    
        哨兵选举:如果哨兵对这个主节点认为客观下线的话,就会进行哨兵选举,最后选举出一个哨兵队长去进行一个故障转移的操作。Redis的选举算法是根据Paxos算法改进的一个名叫Raft的选举算法,如果想要了解这个算法的内容的话就去下面的这个网址里看看,这里不做详细介绍。
        https://www.infoq.cn/article/raft-paper
    
            流程:
            1.会向其他的哨兵节点发送sentinel is-master-down-by-addr命令,要将自己设置为队长。
            2.收到命令的哨兵节点如果没有同意其他的哨兵节点的请求的话就会同意这个,简单地说就是谁先来我就先同意谁,其他的统统拒绝。
            3.如果有哨兵的票数>=1/2哨兵数+1的话,就会成为队长
            4.如果这次没选出来就重新选举,回到1
    
        故障迁移:先过滤掉被主观下线的从节点,如果有从节点优先级的话就选最先的,如果没有的话就选偏移量最大的(偏移量越大,复制的约完整)如果还没选举出就选择runid(每个redis启动都会获得一个随机的id)最小的。
    
    集群:
        Redis的集群没有采用节点取余分区、也没有采用常用的一致性Hash分区,它使用的是Hash槽分区。槽是数据管理和迁移的基本单位,为了去方便数据拆分和集群扩展。它的Hash函数是CRC16(key)%16383。
        TODO 部分写在了博客,抽空完善一下,挪出来

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值