redis面试总结

132 阅读17分钟

关系型数据库和非关系型数据库

所谓非关系型数据库,其实是相对于关系型数据库而言的,我们都知道关系型数据库通常都是处理一些结构化的数据,这些数据通常都是有某些对应关系; 而非关系型数据库(NoSQL)通常用于存储那些类型不固定的,也没有什么规律的数据。

目前比较常用的非关系型数据库有:

Redis

• Memcache

• MongoDb

• HBase

redis 简介

Redis是一个 Key-Value 存储结构,它支持存储的 value 类型非常丰富,包括:

• string(字符串)

• hash(哈希)

• list(列表)

• set(无序集合)

zset(sorted set:有序集合)

Redis为什么这么快

1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4、使用多路I/O复用模型,非阻塞IO;

5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

I/O多路复用

多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。

**这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。**采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量

警告1:这里我们一直在强调的单线程,只是在处理我们的网络请求的时候只有一个线程来处理,一个正式的Redis Server运行的时候肯定是不止一个线程的,这里需要大家明确的注意一下!例如Redis进行持久化的时候会以子进程或者子线程的方式执行

为什么要用 redis /为什么要用缓存(高性能、高并发)

在用说为什么要用什么技术的时候总是要结合使用场景,redis有以下使用场景:

**1.排行榜,**如果使用传统的关系型数据库来做,非常麻烦,而利用Redis的SortSet数据结构能够非常方便搞定;

2.计算器/限速器,利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;

3.好友关系,利用集合的一些命令,比如求交集、并集、差集等,可以方便搞定一些共同好友、共同爱好之类的功能;

4.简单消息队列,除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;

5.Session共享,以PHP为例,默认Session是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。

在以下场景,redis不适用:

数据量太大、数据访问频率非常低的业务都不适合使用Redis,数据太大会增加成本,访问频率太低,保存在内存中纯属浪费资源。

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

redis 和 memcached 的区别

  • redis 相比 memcached 来说,拥有更多的数据结构,能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, redis 会是不错的选择。
  • 在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。
  • 由于 redis 只使用单核,而 memcached 可以使用多核,所以平均每一个核上 redis 在存储小数据时比 memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis,虽然 redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色。

redis 常见数据结构以及使用场景分析(String、Hash、List、Set、Sorted Set

  • string: 在RedisString是可以修改的,称为动态字符串
  • listRedis中的listJava中的LinkedList很像,底层都是一种链表结构, list的插入和删除操作非常快,时间复杂度为 0(1),不像数组结构插入、删除操作需要移动数据。 应用场景:(1)消息队列:lpoprpush(或者反过来,lpushrpop)能实现队列的功能。(2)朋友圈的点赞列表、评论列表、排行榜 。
  • hashRedis 中的 Hash和 Java的 HashMap 更加相似,都是数组+链表的结构,当发生 hash 碰撞时将会把元素追加到链表上,值得注意的是在 RedisHashvalue 只能是字符串。应用场景: (1)购物车:hset [key] [field] [value] 命令, 可以实现以用户Id商品Idfield,商品数量为value,恰好构成了购物车的3个要素。(2) 存储对象:hash类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
  • setRedis 中的 setJava中的HashSet 有些类似,它内部的键值对是无序的、唯一 的。它的内部实现相当于一个特殊的字典,字典中所有的value都是一个值 NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。 应用场景:sinter 命令可以获得A和B两个用户的共同好友; sismember命令可以判断A是否是B的好友; scard命令可以获取好友数量;
  • zsetzset也叫SortedSet一方面它是个 set ,保证了内部 value 的唯一性,另方面它可以给每个 value 赋予一个score,代表这个value的排序权重。它的内部实现用的是一种叫作“跳跃列表”的数据结构。 应用场景:(1)zset 可以用做排行榜,但是和list不同的是zset它能够实现动态的排序,例如: 可以用来存储粉丝列表,value 值是粉丝的用户 ID,score 是关注时间,我们可以对粉丝列表按关注时间进行排序。(2)zset还可以用来存储学生的成绩,value值是学生的 ID,score` 是他的考试成绩。 我们对成绩按分数进行排序就可以得到他的名次。

redis 过期策略

redis 过期策略是:定期删除+惰性删除

定期删除: 指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。

惰性删除: 获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。

redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?

redis 内存淘汰机制有以下几个:

  • noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错。
  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key。
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。

手写LRU

public class LRUCache {
    ListNode firstListNode = new ListNode(null, null, -1, -1);
    ListNode lastListNode = new ListNode(null, null, -1, -1);
    HashMap<Integer, ListNode> hashMap = null;
    int capacity;

    public LRUCache(int capacity) {
        this.capacity = capacity;
        hashMap = new HashMap<>(capacity);
        firstListNode.setNextListNode(lastListNode);
        lastListNode.setPreListNode(firstListNode);
    }

    public int get(int key) {
        ListNode curListNode = hashMap.get(key);
        if (curListNode == null){
            return -1;
        }else{
            if (curListNode.getPreListNode()!=firstListNode){
                moveToFirst(curListNode);
            }
            return curListNode.getValue();
        }
    }

    public void put(int key, int value) {
        ListNode curListNode = hashMap.get(key);
        if (curListNode == null){
            curListNode = new ListNode(firstListNode, firstListNode.getNextListNode(), key, value);
            firstListNode.getNextListNode().setPreListNode(curListNode);
            firstListNode.setNextListNode(curListNode);
            hashMap.put(key, curListNode);
            if (hashMap.size() > capacity){
                deleteLast();
            }
        }else{
            curListNode.setValue(value);
            if (curListNode.getPreListNode() != firstListNode){
                moveToFirst(curListNode);
            }
        }
    }

    public void moveToFirst(ListNode curListNode){
        curListNode.getPreListNode().setNextListNode(curListNode.getNextListNode());
        curListNode.getNextListNode().setPreListNode(curListNode.getPreListNode());

        curListNode.setPreListNode(firstListNode);
        curListNode.setNextListNode(firstListNode.getNextListNode());
        firstListNode.getNextListNode().setPreListNode(curListNode);
        firstListNode.setNextListNode(curListNode);
    }

    public void deleteLast(){
        ListNode dele = lastListNode.getPreListNode();
        dele.getPreListNode().setNextListNode(lastListNode);
        lastListNode.setPreListNode(dele.getPreListNode());
        hashMap.remove(dele.getKey());
    }

    class ListNode{
        public int key;
        public int value;
        ListNode preListNode = null;
        ListNode nextListNode = null;

        public ListNode(ListNode pre, ListNode next, int key, int value){
            this.preListNode = pre;
            this.nextListNode = next;
            this.key = key;
            this.value = value;
        }

        public int getKey() {
            return key;
        }

        public void setKey(int key) {
            this.key = key;
        }

        public int getValue() {
            return value;
        }

        public void setValue(int value) {
            this.value = value;
        }

        public ListNode getPreListNode() {
            return preListNode;
        }

        public void setPreListNode(ListNode preListNode) {
            this.preListNode = preListNode;
        }

        public ListNode getNextListNode() {
            return nextListNode;
        }

        public void setNextListNode(ListNode nextListNode) {
            this.nextListNode = nextListNode;
        }
    }
}

redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复

redis持久化机制包括:RDB机制和AOF机制。

  • RDB: Redis提供了2个命令来创建RDB文件,SAVEBGSAVESAVE命令会阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求。 BGSAVE命令会派生出一个子进程,然后由子进程负责创建RDB文件,服务器进程(父进程)继续处理命令请求。BGSAVE具体流程: (1)执行bgsave命令的时候,Redis主进程会检查是否有子进程在执行RDB/AOF持久化任务,如果有的话,直接返回。(2) Redis主进程会fork一个子进程来执行执行RDB操作,fork操作会对主进程造成阻塞(影响Redis的读写),fork操作完成后会发消息给主进程,从而不再阻塞主进程。(阻塞仅指主进程fork子进程的过程,后续子进程执行操作时不会阻塞)。(3) RDB子进程会根据Redis主进程的内存生成临时的快照文件,持久化完成后会使用临时快照文件替换掉原来的RDB文件。(该过程中主进程的读写不受影响,但Redis的写操作不会同步到主进程的主内存中,而是会写到一个临时的内存区域作为一个副本) (4) 子进程完成RDB持久化后会发消息给主进程,通知RDB持久化完成(将上阶段内存副本中的增量写数据同步到主内存)
  • AOF(默认关闭): AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库数据的。持久化流程:当AOF持久化功能处于打开状态时,Redis服务器在执行完一个写命令之后,将被执行的写命令追加到服务器状态的AOF缓冲区的末尾,然后Redis服务器会根据配置文件中appendfsync选项的值来决定何时将AOF缓冲区中的内容写入和同步到AOF文件里面。apendfsync选项:(1) always :每一条AOF记录都立即同步到文件,性能很低,但较为安全。 (2)everysec : 每秒同步一次,性能和安全都比较中庸的方式,也是redis推荐的方式。如果遇到物理服务器故障,可能导致最多1秒的AOF记录丢失。 (3)no: Redis永不直接调用文件同步,而是让操作系统来决定何时同步磁盘。性能较好,但很不安全。

重写(rewrite)机制

AOF日志会在持续运行中持续增大, 文件的体积会越来越大,如果不做控制,会造成以下2点问题:

  • 过多的占用服务器磁盘空间,可能会对Redis服务器甚至整个宿主计算机造成影响 。
  • AOF文件的体积越大,使用AOF文件来进行数据库还原所需的时间就越多。

解决方案:定期进行AOF重写,对AOF日志进行瘦身。

RPUSH list "A“ "B"
RPUSH list "C“
RPUSH list "D“ "E"
LPOP list
LPOP list
RPUSH list "F" "G"

AOF重写: AOF文件重写并不需要对现有的AOF文件进行任何读取、分析或者写入操作,而是通过读取服务器当前的数据库数据来实现的。 以上面的list键为例,旧的AOF文件保存了6条命令来记录list键的状态,但list键的结果是“C” "D" "E" "F" "G"这样的数据,所以AOF文件重写时,可以用一条RPUSH list “C” "D" "E" "F" "G"命令来代替之前的六条命令,这样就可以将保存list键所需的命令从六条减少为一条了。

redis 事务

事务提供了一种"将多个命令打包,一次性提交并按顺序执行"的机制,提交后在事务执行中不会中断。只有在执行完所有命令后才会继续执行来自其他客户的消息。

Redis通过multi,exec,discard,watch实现事务功能。 1. multi:开始事务 2. exec:提交事务并执行 3. discard:取消事务 4. watch:事务开始之前监视任意数量的键。

事务过程:执行multi命令后会将Redis_multi选项打开,让客户端从非事务状态进入到事务状态。在这个状态中,Redis命令不会立即执行,而是进入一个先进先出的事务队列中。直到执行exec命令时, Redis根据客户端所保存的事务队列, 以先进先出的方式执行事务队列中的命令 。当exec命令执行完毕时,Redis会将结果保存到一个回复队列,并将回复队列返回给客户端。客户端从事务状态退出,一个事务执行完毕。

discard取消一个事务的命令,表示这个事务被取消。客户端从事务状态退出,回到非事务状态,Redis_multi选项关闭。

watch命令在事务开始之前监视任意数量的键:当调用exce命令执行事务时,如果任意一个被监视的键已经被其他客户端修改了,那么整个事务不再执行,直接返回失败。

事务异常: (1)事务中命令如果出现错误语法,将导致事务无法执行。 (2) 当事务执行到中间遇到失败了,如对一个字符串进行incr命令,事务在遇到命令执行失败后,后续的命令还继续执行,所以books的值能继续得到设置。这种异常只有程序员在代码中避免。

Redis 常见异常及解决方案(缓存穿透、缓存击穿、缓存雪崩、缓存预热、缓存降级

  • 缓存雪崩: 同一时间大面积失效,那一瞬间Redis跟没有一样 ,如果这时大量的请求同时到达,并直接访问数据库。数据库会由于扛不住压力而崩溃。解决方案: (1)在批量往Redis存数据的时候,把每个Key的失效时间都加个随机值就好了,这样可以保证数据不会在同一时间大面积失效。(2)如果Redis是集群部署,将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题。
  • 缓存击穿: 指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库。解决方案:设置热点数据永远不过期或加上互斥锁。
  • 缓存穿透:缓存穿透是指缓存和数据库中都没有的数据,而用户不断恶意发起对不存在数据的请求。过多的请求会导致数据库压力过大,严重会击垮数据库。解决方案: (1)接口层增加校验,比如用户鉴权校验,参数做校验,不合法的参数直接代码Return,比如:id 做基础校验,id <=0的直接拦截等。 (2)将无效Key的value设置为null、稍后重试。并设置一个较短的过期时间,防止恶意用户在但秒内发起多次请求。(3)利用布隆过滤器判断key值是否存在,不存在的话直接return。
  • 缓存预热: 缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据解决方案: (1)数据量不大的时候,工程启动的时候进行加载缓存动作。(2)数据量大的时候,设置一个定时任务脚本,进行缓存的刷新。(3)数据量太大的时候,优先保证热点数据进行提前加载到缓存。
  • 缓存降级: 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

redis分布式环境下常见的应用场景(分布式锁、分布式自增 ID

Redis 集群模式(主从模式、哨兵模式、Cluster 集群模式

如何解决 Redis 的并发竞争 Key 问题

这里的并发指的是多个redis的client同时set key引起的并发问题。其实redis自身就是单线程操作,多个client并发操作,按照先到先执行的原则,先到的先执行,其余的阻塞。当然,另外的解决方案是把redis.set操作放在队列中使其串行化,必须的一个一个执行。

如何保证缓存与数据库双写时的数据一致性?

不要求强一致性的情况下:更新操作先删除缓再更新数据库(还是有可能出现数据不一致)

要求强一致性的情况下:只能牺牲性能,将对于数据的更新操作放到队列中,对于对应数据的读取操作只能等更新操作完成后进行。