Redis之问

1、redis有哪些数据结构?
list,set,hash,zset,string,Hyperloglog,bitmap,Bloom Filter,GEO。
(是intset)在这里插入图片描述

2、list内部编码?
list内部可以有三种编码,分别是:ziplist,quicklist,linkedlist。其中,前两种在redis3.2以后完全被quicklist代替。
3、什么是ziplist?
ziplist,压缩列表,在元素较少时,list采用他来进行存储。压缩列表一块连续的内存空间,元素之间紧挨着存储,没有任何冗余空隙。
ziplist的结构
entry组成
3、ziplist如何根据某个节点计算他的前一个节点?
用指向当前节点的指针 e,减去前一个entry的长度,得出的结果就是指向前一个节点的地址 p。
4、为什么要使用ziplist?
因为双向链表占用的内存比压缩列表要多(比如prv和next指针), 所以当创建新的列表键时, 列表会优先考虑使用压缩列表, 并且在有需要的时候, 才从压缩列表实现转换到双向链表实现。
5、ziplist转化为linkedlist的条件?
· 试图往列表新添加一个字符串值,且这个字符串的长度超过server.list_max_ziplist_value (默认值为 64 )。
· ziplist 包含的节点超过 server.list_max_ziplist_entries (默认值为 512 )。
以上两个条件在redis.conf文件中可以配置
list-max-ziplist-value 64
list-max-ziplist-entries 512
6、ziplist的缺点?
字段、值比较小,才会用ziplist,只能顺序查找,插入的时候需要扩展内存。存在连锁更新问题。
7、什么是连锁更新问题?
因为在ziplist中,每个zlentry都存储着前一个节点所占的字节数,而这个数值又是变长编码的。假设存在一个压缩列表,其包含e1、e2、e3、e4……,e1节点的大小为253字节,那么e2.prevrawlen的大小为1字节,如果此时在e2与e1之间插入了一个新节点e_new,e_new编码后的整体长度(包含e1的长度)为254字节,此时e2.prevrawlen就需要扩充为5字节;如果e2的整体长度变化又引起了e3.prevrawlen的存储长度变化,那么e3也需要扩…….如此递归直到表尾节点或者某一个节点的prevrawlen本身长度可以容纳前一个节点的变化。其中每一次扩充都需要进行空间再分配操作。删除节点亦是如此,只要引起了操作节点之后的节点的prevrawlen的变化,都可能引起连锁更新。
连锁更新在最坏情况下需要进行N次空间再分配,而每次空间再分配的最坏时间复杂度为O(N),因此连锁更新的总体时间复杂度是O(N^2)。
即使涉及连锁更新的时间复杂度这么高,但它能引起的性能问题的概率是极低的:需要列表中存在大量的节点长度接近254的zlentry。
8、redis是如何知道entry存储的哪种类型的数据?
根据entry的encoding字段。
9、为什么ziplist不适合存储大型字符串?
因为如果ziplist占据内存太大,重新分配内存和拷贝内存就会有很大的消耗。
10、ziplist的查找时间复杂度?
如果是收尾的话,可以根据表头的三个字段查找,所以是O(1),如果是中间结点的话是O(n)。
11、1、整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢?
1、内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少。Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。
2、数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阈值后,避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率。
12、2、什么是quicklist?
它是一个双向链表,而且是一个ziplist的双向链表。
13、为什么要使用quicklist?
双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。
因此引入了quicklist。
14、quicklist查找的时间复杂度?
如果是在收尾的话就是O(1),如果是在中间的话是O(n)。
15、一个quicklist节点包含多长的ziplist合适?
每个quicklist节点上的ziplist越短,则内存碎片越多。内存碎片多了,有可能在内存中产生很多无法被利用的小碎片,从而降低存储效率。这种情况的极端是每个quicklist节点上的ziplist只包含一个数据项,这就蜕化成一个普通的双向链表了。
每个quicklist节点上的ziplist越长,则为ziplist分配大块连续内存空间的难度就越大。有可能出现内存里有很多小块的空闲空间(它们加起来很多),但却找不到一块足够大的空闲空间分配给ziplist的情况。这同样会降低存储效率。这种情况的极端是整个quicklist只有一个节点,所有的数据项都分配在这仅有的一个节点的ziplist里面。这其实蜕化成一个ziplist了。
所以必须要根据场景来,redis提供了list-max-ziplist-size -2这个参数,我们可以合理的设置这个参数的值。
quicklist
16、什么是压缩深度?
quicklist会对中间节点的ziplist进行压缩。压缩中间节点也是为了节约空间,不压缩首尾节点,是因为收尾节点容易被使用。压缩深度指的就是首位节点有几个不压缩。
17、set内部编码?
hashtable和ziplist。
18、什么是dict?
dict
dict结构通常包含两个hashtable,通常情况下只有一个是有值的。但是在dict扩容缩容时,需要分配新的hashtable,然后渐进式搬迁,这时候两个hashtable存储的分别是旧的hashtable和新的hashtable。dict使用的算法是哈希算法,解决哈希冲突的手段是链地址法。
19、直接rehash有什么问题
给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
释放哈希表 1 的空间。
这个过程看似简单,但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。
20、什么是渐进式rehash?
简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。如下图所示:
渐进式rehash
此外,redis还在定时任务中对他进行rehash。
21、rehash中查找和插入是如何进行的?
查找
如果当前正在进行重哈希,那么将重哈希过程向前推进一步(即调用_dictRehashStep)。实际上,除了查找,插入和删除也都会触发这一动作。这就将重哈希过程分散到各个查找、插入和删除操作中去了,而不是集中在某一个操作中一次性做完。
计算key的哈希值。先在第一个哈希表ht[0]上进行查找。在table数组上定位到哈希值对应的位置(通过哈希值与sizemask进行按位与),然后在对应的dictEntry链表上进行查找。查找的时候需要对key进行比较,这时候调用dictCompareKeys,它里面的实现会调用到keyCompare。如果找到就返回该项。否则,进行下一步。
判断当前是否在重哈希,如果没有,那么在ht[0]上的查找结果就是最终结果(没找到,返回NULL)。否则,在ht[1]上进行查找(过程与上一步相同)。
增加
如果正在重哈希中,它会把数据插入到ht[1];否则插入到ht[0]。
在对应的bucket中插入数据的时候,总是插入到dictEntry的头部。因为新数据接下来被访问的概率可能比较高,这样再次查找它时就比较次数较少。
_dictKeyIndex在dict中寻找插入位置。如果不在重哈希过程中,它只查找ht[0];否则查找ht[0]和ht[1]。
_dictKeyIndex可能触发dict内存扩展(_dictExpandIfNeeded,它将哈希表长度扩展为原来两倍,具体请参考dict.c中源码)。
23、hash内部编码?
ziplist和hashtable。
24、zset内部编码?
ziplist和skiplist。
25、什么是skiplist?
跳表是有顺序的链表,跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,如下图所示:
skiplist
26、跳表的查找和增加?
查找find
比如我们要查找key为19的结点,那么我们不需要逐个遍历,而是按照如下步骤:从header出发,从高到低的level进行查找,先索引到9这个结点,发现9 < 19,继续查找(然后在level=2这层),查找到21这个节点,由于21 > 19, 所以结点不往前走,而是level由2降低到1
然后索引到17这个节点,由于17 < 19, 所以继续往后,索引到21这个结点,发现21>19, 所以level由1降低到0
在结点17上,level=0索引到19,查找完毕。
如果在level=0这层没有查找到,那么说明不存在key为19的节点,查找失败
插入
insert
查找合适的插入位置,比如上图中要插入key为17的结点,就需要一路查找到12,由于12 < 17,而12的下一个结点19 > 17,因而满足条件
创建新结点,并且产生一个在1~MAX_LEVEL之间的随机level值作为该结点的level
调整指针指向
27、为什么redis要用跳表,而MySQL用B+树?
因为对于MySQL来说,会涉及到大量的磁盘IO,所以引入了B+树这个数据结构来减少IO操作,而Redis是 内存中读取数据,不涉及IO,因此使用了跳表,而且相对于B+树,跳表实现更为简单,性能也不差。
28、为什么redis用跳表,不用红黑树或者AVL?
红黑树,AVL与跳表时间复杂度差不多,但是对于代码的编写和可读性来说,跳表要易读且容易编写。当然跳表占用内存会比红黑树大。
29、String的内部编码?
intset,embstr,row
30、什么是SDS?
String的底层实现。

struct sdshdr {
    int len;
    int free;
    char buf[];
};

31、SDS有什么好处?
二进制安全:
在C语言中,读取字符串遵循的是“遇零则止”,即,读取字符串,当读取到“\0”,就认为已经读到了结尾,哪怕后面还有字符串也不会读取了,像图片、音频等二进制数据,经常会穿插“\0”在其中,好端端的图片、音频就毁了…但是现在有了一个字段来存储字符串的实际长度,读取字符串的时候,先看下这个字符串的长度是多少,然后往后读多少位就可以了。
获得字符串长度操作O(1):
在C语言中,求字符串的长度只能遍历,时间复杂度是o(n),单线程的Redis表示鸭梨山大,但是现在引入了一个字段来存储字符串的实际长度,时间复杂度瞬间降低成了o(1)。
相对来说,缓冲出更不容易溢出:
字符串拼接是开发中常见的操作,C语言的字符串是不记录字符串长度的,一旦我们调用了拼接函数,而没有提前计算好内存,就会产生缓冲区溢出的情况,但是现在引入了free字段,来记录剩余的空间,做拼接操作之前,先去看下还有多少剩余空间,如果够,那就放心的做拼接操作,不够,就进行扩容。
可以减少内存分配的次数
32、SDS内存分配策略?
空间预分配:当对字符串进行拼接操作的时候,Redis会很贴心的分配一定的剩余空间,这块剩余空间现在看起来是有点浪费,但是我们如果继续拼接,这块剩余空间的作用就出来了。
惰性空间释放:当我们做了字符串缩减的操作,Redis并不会马上回收空间,因为你可能即将又要做字符串的拼接操作,如果你再次操作,还是没有用到这部分空间,Redis也会去回收这部分空间。
33、SDS扩容策略?
字符串长度在小于1M之前,扩容空间采用加倍策略,也就是保留100%的冗余空间。当长度大于1M后,每次最多分配1M大小的冗余空间(为了避免加背后的冗余空间过大而导致浪费)。
34、为什么要使用SDS?
见问题31.
35、什么是inteset?
是一个由整数组成的有序集合,从而便于在上面进行二分查找,用于快速地判断一个元素是否属于这个集合。它在内存分配上与ziplist有些类似,是连续的一整块内存空间,而且对于大整数和小整数(按绝对值)采取了不同的编码,尽量对内存的使用进行了优化。
36、intset与ziplist的对比
ziplist可以存储任意二进制串,而intset只能存储整数。
ziplist是无序的,而intset是从小到大有序的。因此,在ziplist上查找只能遍 历,而在intset上可以进行二分查找,性能更高。
ziplist可以对每个数据项进行不同的变长编码(每个数据项前面都有数据长度字段len),而intset只能整体使用一个统一的编码(encoding)。
37、什么时候set会使用intset?
Set集合中必须是64位有符号的十进制整型;
元素个数不能超过set-max-intset-entries配置,默认512;
38、什么是embstr?raw呢?
分别是redis的字符串的两种存储方式。
39、什么时候会使用embstr?raw呢?为什么?有什么区别?
当字符串长度大于44的时候会使用raw,小于44的时候会使用embstr。
对于redis来说,它认为64字节以上的字符串为大字符串,由于redisObject占据16个字节,free占据3个字节,\0有一个字节,所以是64-16-3-1个字节。
对于embstr来说,只会分配一次内存,redisObject对象头和SDS对象是连续的。对于raw来说,会分配两次内存,redisObject对象头和SDS对象一般不连续。
40、SDS扩容策略?
字符串长度在小于1M前,扩容采取加倍策略,也就是保留100%的冗余空间。当长度超过1M后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配1M大小的冗余空间。

应用篇

41、redis可以有哪些应用场景?
分布式锁,消息队列,限流,保存会话,排行榜……
42、分布式锁的要点
互斥性。在任意时刻,只有一个客户端能持有锁。
不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
具有容错性。只要大部分的节点正常运行,客户端就可以加锁和解锁。
解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
超时判定。

内存篇

43、请实现一个LRU算法。

public class LRU {
    class DLinkedNode {
        int key;
        int value;
        DLinkedNode prev;
        DLinkedNode next;

        public DLinkedNode() {
        }

        public DLinkedNode(int _key, int _value) {
            key = _key;
            value = _value;
        }
    }

    private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
    private int size;
    private int capacity;
    private DLinkedNode head, tail;

    public LRU(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        // 使用伪头部和伪尾部节点
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            return -1;
        }
        // 如果 key 存在,先通过哈希表定位,再移到头部
        moveToHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        DLinkedNode node = cache.get(key);
        if (node == null) {
            // 如果 key 不存在,创建一个新的节点
            DLinkedNode newNode = new DLinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            ++size;
            if (size > capacity) {
                // 如果超出容量,删除双向链表的尾部节点
                DLinkedNode tail = removeTail();
                // 删除哈希表中对应的项
                cache.remove(tail.key);
                --size;
            }
        } else {
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node.value = value;
            moveToHead(node);
        }
    }

    private void addToHead(DLinkedNode node) {
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }

    private void removeNode(DLinkedNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void moveToHead(DLinkedNode node) {
        removeNode(node);
        addToHead(node);
    }

    private DLinkedNode removeTail() {
        DLinkedNode res = tail.prev;
        removeNode(res);
        return res;
    }
}

44、** redis的过期策略是什么,请简述**
定期删除
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的 key。
Redis 默认会每秒进行十次过期扫描(100ms一次),过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。
1.从过期字典中随机 20 个 key;
2.删除这 20 个 key 中已经过期的 key;
3.如果过期的 key 比率超过 1/4,那就重复步骤 1;
redis默认是每隔 100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载。
惰性删除
所谓惰性策略就是在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除,不会给你返回任何东西。
定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,即当你主动去查过期的key时,如果发现key过期了,就立即进行删除,不返回任何东西.
总结:定期删除是集中处理,惰性删除是零散处理。
45、为什么需要内存淘汰策略?
无论是定期删除还是懒惰删除,都可能存在key没有被删掉的场景,所以需要内存淘汰策略。
46、redis内存淘汰策略?
noeviction:当内存超过maxmemory的值时,只允许读取和删除,不允许新增。
allkeys-lru:从所有的key中,淘汰最近最久未使用的键。
allkeys-random:从所有key中,随机淘汰。
volatile-lru:从设置了过期时间的key中,淘汰最久未使用的键。
volatile-ttl:从设置了过期时间的key中,淘汰ttl值最小的。
volatile-random:从设置了过期时间,随机淘汰。
allkeys-lfu,淘汰整个键值中最少使用的键值,这也就是我们常说的LRU算法。
volatile-lfu,淘汰所有设置了过期时间的键值中最少使用的键值。
47、redis的近似LRU算法?
redis为实现近似LRU算法,它为每个key增加了一个额外的小字段,这个字段的长度是24bit,也就是最后一次被访问的时间。然后随机采样出5个key(通过maxmemory_samples来调整,采样数量越大越接近于正经的LRU算法,但是也带来了淘汰速率的问题)淘汰掉最旧的key,直到Redis占用内存小于maxmemory为止。在3.0以后增加了LRU淘汰池,进一步提高了与LRU算法的近似效果。
48、如何查看redis内存使用?
使用info指令,定位到memory
memory

线程模型篇

49、redis线程模型是什么?
redis线程模型

1)文件事件处理器
Redis基于Reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器 file event handler。这个文件事件处理器,它是单线程的,所以 Redis 才叫做单线程的模型,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件。
如果被监听的 Socket 准备好执行accept、read、write、close等操作的时候,跟操作对应的文件事件就会产生,这个时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。
文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个Socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了 Redis 内部的线程模型的简单性。
文件事件处理器的结构包含4个部分:多个Socket、IO多路复用程序、文件事件分派器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)。
多个 Socket 可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个 Socket,会将 Socket 放入一个队列中排队,每次从队列中取出一个 Socket 给事件分派器,事件分派器把 Socket 给对应的事件处理器。
然后一个 Socket 的事件处理完之后,IO多路复用程序才会将队列中的下一个 Socket 给事件分派器。文件事件分派器会根据每个 Socket 当前产生的事件,来选择对应的事件处理器来处理。
2)文件事件
当 Socket 变得可读时(比如客户端对redis执行write操作,或者close操作),或者有新的可以应答的 Sccket 出现时(客户端对redis执行connect操作),Socket就会产生一个AE_READABLE事件。
当 Socket 变得可写的时候(客户端对redis执行read操作),Socket 会产生一个AE_WRITABLE事件。
IO 多路复用程序可以同时监听 AE_REABLE 和 AE_WRITABLE 两种事件,如果一个Socket同时产生了这两种事件,那么文件事件分派器优先处理 AE_READABLE 事件,然后才是 AE_WRITABLE 事件。
3)文件事件处理器
如果是客户端要连接redis,那么会为 Socket 关联连接应答处理器。
如果是客户端要写数据到redis,那么会为 Socket 关联命令请求处理器。
如果是客户端要从redis读数据,那么会为 Socket 关联命令回复处理器。
4)客户端与redis通信的一次流程
在 Redis 启动初始化的时候,Redis 会将连接应答处理器跟 AE_READABLE 事件关联起来,接着如果一个客户端跟Redis发起连接,此时会产生一个 AE_READABLE 事件,然后由连接应答处理器来处理跟客户端建立连接,创建客户端对应的 Socket,同时将这个 Socket 的 AE_READABLE 事件跟命令请求处理器关联起来。
当客户端向Redis发起请求的时候(不管是读请求还是写请求,都一样),首先就会在 Socket 产生一个 AE_READABLE 事件,然后由对应的命令请求处理器来处理。这个命令请求处理器就会从Socket中读取请求相关数据,然后进行执行和处理。
接着Redis这边准备好了给客户端的响应数据之后,就会将Socket的AE_WRITABLE事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在 Socket 上产生一个 AE_WRITABLE 事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入 Socket,供客户端来读取。
命令回复处理器写完之后,就会删除这个 Socket 的 AE_WRITABLE 事件和命令回复处理器的关联关系。
50、为什么redis选择单线程?
使用单线程模型能带来更好的可维护性,方便开发和调试;
使用单线程模型也能并发的处理客户端的请求;
Redis 服务中运行的绝大多数操作的性能瓶颈都不是 CPU;
简单总结一下,Redis 并不是 CPU 密集型的服务,如果不开启 AOF 备份,所有 Redis 的操作都会在内存中完成不会涉及任何的 I/O 操作,这些数据的读写由于只发生在内存中,所以处理速度是非常快的;整个服务的瓶颈在于网络传输带来的延迟和等待客户端的数据传输,也就是网络 I/O,所以使用多线程模型处理全部的外部请求可能不是一个好的方案。
多线程虽然会帮助我们更充分地利用 CPU 资源,但是操作系统上线程的切换也不是免费的,线程切换其实会带来额外的开销,其中包括:
保存线程 1 的执行上下文;
加载线程 2 的执行上下文;
频繁的对线程的上下文进行切换可能还会导致性能地急剧下降,这可能会导致我们不仅没有提升请求处理的平均速度,反而进行了负优化,所以这也是为什么 Redis 对于使用多线程技术非常谨慎。

备份篇

51、redis有哪几种备份方式?
RDB和AOF。
52、RDB和AOF有什么区别?
RDB备份,是快照备份,有主动触发和被动触发两种方式触发,他会备份在某一时刻的redis所有数据,所以可能存在数据丢失的问题。
AOF备份,是追加写备份,以追加写的方式对指令进行备份,因此可能造成备份文件特别大,恢复起来非常慢的的问题。
53、RDB备份如何主动触发?被动呢?
RDB主动触发分为使用save指令和bgsave指令。save的问题的在于它会阻塞整个redis进程,使得redis完全不可用。bgsave指令并不会阻塞进程,它会新建一个子进程,使用cow技术手段对数据进行备份,bgsave的问题在于当数据量特别大时,新建子进程也会耗费大量时间,所以save指令相对于bgsave指令会快一些。
被动触发的方式是满足配置文件的save选项,如果用户设置了多个配置save配置选项那么任何一个条件满足就会触发BGSAVE。例如:
sava选项
除此之外如果使用flushall指令和退出shutdown指令也会触发。
只需要把rdb文件放到启动目录下就会自动恢复数据
54、AOF刷新磁盘策略?
always:把每一个指令都刷新的磁盘。
everysec:每秒执行一次同步。
no:只把文件刷新到缓冲区,由操作系统决定何时写入磁盘。
55、AOF重写何时会触发?为什么AOF要先执行指令后执行写日志
主动触发:使用bgrewriteaof指令。
被动触发:同时满足auto-aof-rewrite-percentage, auto-aof-rewrite-min-sieze。
对于Redis来说,为了避免检测指令正确的额外开销,以及不阻塞当前写线程,所以采用了先写日志,再执行指令。
56、什么是混合持久化?
将rdb文件和增量的AOF日志放在一起,这里的AOF日志并不是所有的AOF日志,而是自持久化结束的这段时间发生的增量日志,这部分日志通常很小。
57、RDB和AOF的优缺点?
RDB优点:压缩二进制适合备份,加载恢复数据比AOF快。
RDB缺点:没法做到实时持久化,频繁操作消耗资源。
AOF优点:通过追加命令可以实时持久化。
AOF缺点:加载恢复数据慢。
58、如果AOF文件损坏了怎么办?
如果AOF文件损坏了,可以使用redis-check-aof来修复AOF文件,当然,毕竟是损坏,redis也不知道是怎么样损坏的,因此会对一些损坏数据进行删除。
59、如果AOF文件太大了怎么办?
fork一个新的进程,来将我们的文件进行重写,默认64MB

集群篇

58、redis有哪几种集群方案?
主从复制模式
Sentinel(哨兵)模式
Cluster模式
59、主从复制模式的工作机制?
slave启动后,向master发送SYNC命令,master接收到SYNC命令后通过bgsave保存快照(即上文所介绍的RDB持久化),并使用缓冲区记录保存快照这段时间内执行的写命令
master将保存的快照文件发送给slave,并继续记录执行的写命令
slave接收到快照文件后,加载快照文件,载入数据
master快照发送完后开始向slave发送缓冲区的写命令,slave接收命令并执行,完成复制初始化
此后master每次执行一个写命令都会同步发送给slave,保持master与slave之间数据的一致性
60、主从复制的优缺点?
优点:
master能自动将数据同步到slave,可以进行读写分离,分担master的读压力
master、slave之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求
缺点:
不具备自动容错与恢复功能,master或slave的宕机都可能导致客户端请求失败,需要等待机器重启或手动切换客户端IP才能恢复
master宕机,如果宕机前数据没有同步完,则切换IP后会存在数据不一致的问题
难以支持在线扩容,Redis的容量受限于单机配置
60.1、主从复制的作用?

  1. 数据冗余:当从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  2. 故障恢复:当从节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余
  3. 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载,尤其是写少读多的场景下,通过从多个节点分担读负载,可以大大提高Redis服务器的并发量。
  4. 高可用基石:除上述作用外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制Redis高可用的基础。

60.2、Tips:主从复制需要改哪些选项?
端口,pid,logfile,dump名称

61、什么是哨兵模式?
在主从复制的基础上,引入了哨兵来自动处理故障
62、哨兵有哪些功能?
监控master、slave是否正常运行
当master出现故障时,能自动将一个slave转换为master
多个哨兵可以监控同一个redis,哨兵之间也会自动监控
63、哨兵模式的工作机制?

在配置文件中通过sentinel monitor <master-name> <ip> <redis-port> <quorum>来定位master的IP、端口,一个哨兵可以监控多个master数据库,只需要提供多个该配置项即可。哨兵启动后,会与要监控的master建立两条连接:

一条连接用来订阅master的sentinel:hello频道与获取其他监控该master的哨兵节点信息
另一条连接定期向master发送INFO等命令获取master本身的信息
与master建立连接后,哨兵会执行三个操作:

定期(一般10s一次,当master被标记为主观下线时,改为1s一次)向master和slave发送INFO命令
定期向master和slave的sentinel:hello频道发送自己的信息
定期(1s一次)向master、slave和其他哨兵发送PING命令
发送INFO命令可以获取当前数据库的相关信息从而实现新节点的自动发现。所以说哨兵只需要配置master数据库信息就可以自动发现其slave信息。获取到slave信息后,哨兵也会与slave建立两条连接执行监控。通过INFO命令,哨兵可以获取主从数据库的最新信息,并进行相应的操作,比如角色变更等。

接下来哨兵向主从数据库的sentinel:hello频道发送信息与同样监控这些数据库的哨兵共享自己的信息,发送内容为哨兵的ip端口、运行id、配置版本、master名字、master的ip端口还有master的配置版本。这些信息有以下用处:

其他哨兵可以通过该信息判断发送者是否是新发现的哨兵,如果是的话会创建一个到该哨兵的连接用于发送PING命令。
其他哨兵通过该信息可以判断master的版本,如果该版本高于直接记录的版本,将会更新
当实现了自动发现slave和其他哨兵节点后,哨兵就可以通过定期发送PING命令定时监控这些数据库和节点有没有停止服务。
如果被PING的数据库或者节点超时(通过 sentinel down-after-milliseconds master-name milliseconds 配置)未回复,哨兵认为其主观下线(sdown,s就是Subjectively —— 主观地)。

如果下线的是master,哨兵会向其它哨兵发送命令询问它们是否也认为该master主观下线,如果达到一定数目(即配置文件中的quorum)投票,哨兵会认为该master已经客观下线(odown,o就是Objectively —— 客观地),并选举领头的哨兵节点对主从系统发起故障恢复。

若没有足够的sentinel进程同意master下线,master的客观下线状态会被移除,若master重新向sentinel进程发送的PING命令返回有效回复,master的主观下线状态就会被移除。

哨兵认为master客观下线后,故障恢复的操作需要由选举的领头哨兵来执行,选举采用Raft算法:

发现master下线的哨兵节点(我们称他为A)向每个哨兵发送命令,要求对方选自己为领头哨兵
如果目标哨兵节点没有选过其他人,则会同意选举A为领头哨兵
如果有超过一半的哨兵同意选举A为领头,则A当选
如果有多个哨兵节点同时参选领头,此时有可能存在一轮投票无竞选者胜出,此时每个参选的节点等待一个随机时间后再次发起参选请求,进行下一轮投票竞选,直至选举出领头哨兵
选出领头哨兵后,领头者开始对系统进行故障恢复,从出现故障的master的从数据库中挑选一个来当选新的master,选择规则如下:

所有在线的slave中选择优先级最高的,优先级可以通过slave-priority配置
如果有多个最高优先级的slave,则选取复制偏移量最大(即复制越完整)的当选
如果以上条件都一样,选取id最小的slave
挑选出需要继任的slave后,领头哨兵向该数据库发送命令使其升格为master,然后再向其他slave发送命令接受新的master,最后更新数据。将已经停止的旧的master更新为新的master的从数据库,使其恢复服务后以slave的身份继续运行。
64、哨兵模式的优缺点?
优点:
哨兵模式基于主从复制模式,所以主从复制模式有的优点,哨兵模式也有
哨兵模式下,master挂掉可以自动进行切换,系统可用性更高
缺点:
同样也继承了主从模式难以在线扩容的缺点,Redis的容量受限于单机配置
需要额外的资源来启动sentinel进程,实现相对复杂一点,同时slave节点作为备份节点不提供服务
65、Cluster模式的特点?
所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
节点的fail是通过集群中超过半数的节点检测失效时才生效。
客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
66、Cluster模式的工作机制?
在Redis的每个节点上,都有一个插槽(slot),取值范围为0-16383
当我们存取key的时候,Redis会根据CRC16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作
为了保证高可用,Cluster模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点
当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点都宕机了,那么该集群就无法再提供服务了
Cluster模式集群节点最小配置6个节点(3主3从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。
67、Cluster模式的优缺点
优点:
无中心架构,数据按照slot分布在多个节点。
集群中的每个节点都是平等的关系,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。
可线性扩展到1000多个节点,节点可动态添加或删除
能够实现自动故障转移,节点之间通过gossip协议交换状态信息,用投票机制完成slave到master的角色转换
缺点:
客户端实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度。目前仅JedisCluster相对成熟,异常处理还不完善,比如常见的“max redirect exception”
节点会因为某些原因发生阻塞(阻塞时间大于 cluster-node-timeout)被判断下线,这种failover是没有必要的
数据通过异步复制,不保证数据的强一致性
slave充当“冷备”,不能缓解读压力
批量操作限制,目前只支持具有相同slot值的key执行批量操作,对mset、mget、sunion等操作支持不友好
key事务操作支持有线,只支持多key在同一节点的事务操作,多key分布不同节点时无法使用事务功能
不支持多数据库空间,单机redis可以支持16个db,集群模式下只能使用一个,即db 0 Redis Cluster模式不建议使用pipeline和multi-keys操作,减少max redirect产生的场景。

缓存篇

68、什么是缓存穿透?如何解决?
缓存穿透指的是:同一时刻,大量的并发请求数据库中不存在的信息,他既不会命中缓存,也不会命中数据库,但是他会查找数据库。
解决方案:
(1)将空数据存入缓存:管数据库中有没有查询到数据,都往缓存中添加一条数据,同时设置一个简短的过期时间,这样下次请求的时候就会直接在缓存中返回
(2)布隆过滤器:布隆过滤对所有的可能查询的参数以hash形式存储,在控制器层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。举例:将真实正确Id在添加完成之后便加入到过滤器当中,每次再进行查询时,先确认要查询的Id是否在过滤器中,如果不在,则说明Id为非法Id。
69、缓存击穿如何解决?
缓存击穿是指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db(数据库)。
解决方案:
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
70、缓存雪崩怎么办?
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。
解决方案:为每一个key设置一个随机过期时间。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值