面试典问—redis

你简历中项目经验是不是有用到redis,但是面试官随便问你几个问题,你都是打不出来,或者答非所问,本篇文章针对redis面试中经常问到的问题出发,根据自身的理解,参考相关的文章,get到下面几点。

1.什么是redis?

  1. redis就是一个数据库,不过与传统数据库不同的是,redis的数据存在于内存中,所以读写速度非常快,所以经常被应用到缓存方向,
  2. 另外redis可以作为分布式锁。
  3. 同时redis提供多种数据类型支持不同的业务场景。
  4. 除此之外,redis支持事务,持久化,LUA脚本、LRU驱动事件、多种集群方案。

2.为什么要使用redis/为什么要使用缓存?

两个角度回答:高性能和高并发。

  • 高性能:假如用户第一次访问数据库中的数据,访问会比较慢,因为是从硬盘上读取的,如果此时将用户访问的数据存到缓存中,这样下一次访问的时候可以直接从缓存中获取,操作缓存就是直接操作内存,所以速度非常快。如果数据库中的数据发生变化,清除相应缓存或者同步改变数据即可。
  • 高并发直接操作缓存能够承受的请求数远远大于直接访问数据库,所以当我们把数据库中的部分数据放到缓存中,这样用户的一部分请求直接到缓存就不用在访问数据库了。

举一个项目的例子:一个http请求获取天气系统的天气数据,然后该天气系统会访问其他http服务获得数据。
不添加缓存:每次请求都要再进行一次http请求,耗费时间,性能差,同时,天气系统访问那个http服务的次数多了,可能会被人家限制不让访问。
添加缓存:第一次请求获取数据可能较慢,但是获取天气数据添加到缓存中,第二次获取数据,直接从天气系统的内存中获取,非常快,同时不再访问那个http服务,这样之后每次请求到缓存获取数据,允许的请求数量自然大了。

3.使用redis或者map作为缓存的优缺点?

缓存分为本地缓存和分布式缓存:

以java为例,使用map实现的是本地缓存,好处是轻量,快速,生命周期随着jvm的销毁而结束。并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用redis或者memcached之类的是分布式缓存,多实例的情况下,共用一份缓存数据,缓存具有一致性,缺点就是要实现redis的高可用,整个程序搭建比较复杂。

应用场景:如果只有少量数据作为缓存,并且没有持久化的需求,就可以直接使用map作为缓存。

详细区别:

  1. redis可以使用几十G的内存作为缓存,JVM分配几个G就够大了。
  2. redis的数据可以持久化,map是内存对象,系统重启就没了。
  3. redis可以用作分布式的缓存,而map只存在创建它的系统中。
  4. redis有丰富的API,map简单太多了。
  5. redis有过期机制。

4.redis 的线程模型

redis 内部使用文件事件处理器 file event handler,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

文件事件处理器的结构包含 4 个部分:

  • 多个 socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。

5.为啥 redis 单线程模型也能效率这么高?

  1. 纯内存操作
  2. 核心是基于非阻塞的 IO 多路复用机制(不太懂原理的东西)
  3. 单线程反而避免了多线程的频繁上下文切换问题

6.客户端与 redis 的一次通信过程:

在这里插入图片描述
客户端 socket01 向 redis 的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。

假设此时客户端发送了一个 set key value 请求,此时 redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。

如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。

这样便完成了一次通信。

7.redis 和 memcached 的区别

对于 redis 和 memcached 我总结了下面四点。现在公司一般都是用 redis 来实现缓存,而且 redis 自身也越来越强大了!

  • redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
  • 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
  • Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
    在这里插入图片描述

7.redis 常见数据结构以及使用场景分析

  1. String

常用命令:set,get,decr,incr,mget等
结构:String数据结构是简单的key-value结构,value可以是字符串,也可以是数字
常用key-value缓存应用:常规计数,微博数,粉丝数。

  1. Hash

常用命令:hget,hset,hgetall
hash是一个String类型的field和value的映射表,hash特别适用于存储对象,

应用场景:比如存储用户的信息,商品的信息。比如下面存放了我本人的信息

key=JavaUser599
value={
  “id”: 1,
  “name”: “xiaodeng”,
  “age”: 22,
  “location”: “Wuan, HeBei”
}
  1. List

常用命令:lpush,rpush,lpop,rpop,lrange等

结构:List就是链表,redis的实现是一个双向链表,即可以支持反向查找和遍历,不过带来额外的内存开销。
应用场景:比如微博的关注列表,粉丝列表,消息列表等。

另外,可以通过lrange命令,就是从某个元素开始查找多少个元素,可以基于List实现分页操作,这是一个很棒的功能,基于redis实现简单的高性能分页,可以做微博那种不断下拉分页的的东西(一页一页走),性能高。

  1. Set

常用命令:sadd,spop,smembers,sunion等

结构,set对外提供的功能与List类似,都是列表的功能,特殊在于,Set可以自动排重。

适用场景:当你需要存储一个列表数据,同时不希望出现重复的数据,set是一个好选择,并且set提供了判断某个成员是否在set集合中的接口,list没有,可以基于set实现交集,并集,差集的操作。

比如在微博应用中,把一个用户所有关注的人存在一个集合中,把它的粉丝都存在一个集合中,redis可以很方便地实现共同关注,共同粉丝,共同喜欢等功能,这个过程就是求交集的过程。

SINTERSTORE myset myset1 myset2   //将myset1和myset2集合的交集数据放到myset集合中,返回集合中的元素个数。
  1. Sorted Set

常用命令:zadd,zrange,zrem,zcard等

和set相比,Sorted Set增加了一个权重参数score,使得集合中的参数可以根据score排序。
举例,在直播系统中,根据直播间用户刷礼物作为权重参数,来排序用户。

8.redis 设置过期时间

一般项目中的token或者一些登录信息,尤其是短信验证码都是有时间限制的。如果自己项目中判断是否过期,影响性能。刚好redis中有个设置时间过期的功能,即对存储在redis数据库中的数据设置一个过期时间。作为一个缓存数据库,这是非常实用的。

我们set key的时候,都可以设置一个expire time,就是过期时间。通过过期时间可以指定这个数据的存活时间。

如果我们对一批数据设置存活时间为一个小时,那么接下来的一个小时,redis是如何删除的呢?
定期删除+惰性删除

  • 定期删除:redis默认每隔100ms就随机抽取一些设置了过期时间的key,检查是否过期,过期就直接删除。注意是随机抽取 数据,为什么要随机呢?
    如果redis中存了几十万key,每隔100ms遍历所有的key,会严重消耗cpu资源。
  • 惰性删除:定期删除导致很多过期的key没有被删除,所以有了惰性删除,就是当过期的key没有被删除,还存在内存中,只有再次查询时,判断它过期了,才删除,就是惰性删除。

9.redis 内存淘汰机制

在第8个问题中,redis删除过期数据是通过定期删除+惰性删除,如果定期删除存在大量过期时间没有删除,同时也没有再次访问,那么这些过期数据大量堆积在内存中,导致redis内存耗尽怎么办?

同样的还有一个问题:mysql中2000w条数据,redis中存在20w条数据,如果保证redis中都是热点数据?

以上redis提供了6种数据淘汰策略:

  1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
  7. volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  8. allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key

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

redis支持两种不同的持久化操作,一种是快照,另外一种是只追加文件

  • 快照(snapshotting)持久化(RDB)redis默认采取的持久化方式
    Redis可以通过快照的方式来获得存储在内存中某一时间点上的数据副本。redis创建快照后,
  1. 可以对快照进行备份。
  2. 可以将快照复制到其他服务器上,从而创建具有相同数据的服务器副本(redis主从结构,主要提高Redis的性能)
  3. 还可以将快照留在原地,作为服务器重启的时候使用

快照持久化是Redis默认采用的持久化方式,在redis.conf配置文件中默认有此下配置:

save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 300 10          #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
  • AOF(append-only file)持久化(redis默认没有开启)
    修改redis.conf参数开启AOF持久化。
appendonly yes

开启AOF持久化后每执行一条会更改redis中数据的命令,redis就会将该命令写入硬盘中的AOF文件中,AOP文件的保存位置和RDB保存的位置相同,都是通过dir参数来设置的,默认的文件名称是appendonly.aof。

在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no        #让操作系统决定何时进行同步

为了兼顾数据和性能,用户最好选择everysec,每秒钟同步一次AOF文件,Redis性能几乎没有影响,而且即使出现系统崩溃,最多损失一秒的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

  • Redis 4.0 对于持久化机制的优化

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

11. redis事务

  • redis事务命令:
  1. multi:标记一个事务块的开始
  2. exec:执行所有事务块内的命令
  3. discard:取消事务,放弃执行事务块内的所有命令
  4. unwatch:取消watch命令对所有事务块的监视
  5. watch:监视一个或多个key,如果事务执行之前,这个key被其他命令改动,那么事务执行失败

事务提供了一种将多个命令请求打包,然后一次性按顺序执行多个命令的机制,并且在执行期间,服务器不会中断该事务而去执行其他的客户端命令。它会将事务中的所有命令都执行完毕,才会处理其他客户端的请求。

注意:redis同一个事务中,如果有一条命令执行失败,其后的命令仍会执行,没有回滚。

12.缓存雪崩和缓存穿透问题解决方案

1. 缓存雪崩

  • 什么是缓存雪崩?
    缓存雪崩:缓存同一时间大面积失效,导致大量请求落到数据库上,然后数据库承受不了那么多请求崩掉。
  • 有哪些解决办法:
  1. 事前:尽量保证redis集群的高可用(主从模式,副本机制),发现宕机尽快补上,选择合适的淘汰策略,
  2. 事中:本地缓存(map就是本地缓存,支持少量的缓存数据,redis是分布式缓存)+hystrix限流和降级,避免mysql崩掉。比如mysql每秒最多能处理2000请求,假如缓存崩掉,大量请求要访问数据库,可以使用本地缓存处理一些请求,但本地缓存只能存放少量数据,更多的要使用hystrix限流,如果每秒5000,限流只允许通过2000请求,其他3000请求走降级,叫服务降级,响应一些默认值或者友好提示给客户。
  3. 事后:利用redis持久化机制,尽快恢复redis集群,一旦重启,自动加载磁盘上的数据,恢复内存中的数据。只要缓存一旦恢复 ,请求就可以走缓存。
    在这里插入图片描述

2. 缓存穿透

1. 什么是缓存穿透?
就是大量请求的key根本不存在缓存中,然后导致直接请求在数据库中,根本就么有经过缓存这一层。

举个例子:某个黑客故意制造缓存中不存在的key发起大量请求,导致大量请求直接作用在数据库上。一般mysql默认的最大连接数是150左右,其次服务器的cpu,内存,网络等都会限制并发能力,一般3000并发请求就能大死大部分数据库了。
2. 有哪些解决办法?

  • 最基本的方法是首先做好校验,一些不合法的参数请求直接抛出异常信息返回给客户端,比如查询的数据库id不能小于0,传入的邮箱格式不对等。
  • 如果缓存和数据库中都查不到某个key的数据就直接写一个key值到缓存上并设置过期时间和相应的淘汰机制。

SET key value EX 100 //时间单位为:秒

这种方案可以解决key变化不频繁的情况,如果黑客恶意攻击每次构建不一样的key,会导致redis中存在大量无效的key。
这种方案不能从根本上解决问题,如果非要使用这种方案,尽量将无效key的过期时间设置短一些比如1分钟。
下面用java代码展示这种方案:

public Object getObjectInclNullById(Integer id) {
    // 从缓存中获取数据
    Object cacheValue = cache.get(id);
    // 缓存为空
    if (cacheValue == null) {
        // 从数据库中获取
        Object storageValue = storage.get(key);
        // 缓存空对象
        cache.set(key, storageValue);
        // 如果存储数据为空,需要设置一个过期时间(300秒)
        if (storageValue == null) {  
            cache.expire(key, 60 * 5);// 必须设置过期时间,否则有被攻击的风险
        }
        return storageValue;
    }
    return cacheValue;
}
  • 使用布隆过滤器来解决数据穿透问题。
    布隆过滤器是一个非常生气的数据结构,通过它我们可以方便地判断一个 给定数据是否存在海量数据中。
    下面是使用布隆过滤器方案解决缓存穿透问题:
    在这里插入图片描述
    首先请求会经过布隆过滤器,判断是否key是否存在海量数据中,过滤非法key直接返回客户端,然后再访问redis缓存,不存在,最后访问mysql数据库。

如果想了解布隆过滤器可以查看这篇文章《不了解布隆过滤器?一文给你整的明明白白!》 ,强烈推荐!

13.如何解决redis并发竞争key问题?

所谓的redis并发竞争key问题也就是多个系统同时对key进行操作,但是最后执行的顺序和我们期望的执行顺序不同,这样就导致了结果的不一样!

解决方案:分布式锁(zookeeper和redis都可以实现分布式锁),如果不存在Redis并发竞争key问题,就不要使用分布式锁,影响性能。

基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。

在实践中,当然是从以可靠性为主。所以首推Zookeeper。

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

一般来说分两种情况:

  • 允许缓存可以稍微和数据库偶尔有不一致的情况
  1. 读的时候,先读缓存,缓存没有,在去读数据库,然后取出数据后放入缓存,然后返回。
  2. 写的时候,先更新数据库,然后删除缓存

此时会衍生出三个问题?

1.为什么是删除缓存,而不是更新缓存?
原因很简单,很多时候缓存中的数据并不是单纯从数据库中拿出来的,可能需要通过计算,才能放入缓存。
另外更新缓存的代价很高,同时如果数据库频繁地修改数据,同时消耗很大的代价更新完缓存,但是,这个缓存到底会不会频繁地访问到?
所以直接删除缓存,等读的时候在从数据库中获取,添加到缓存中,利用懒加载的思想,不去做浪费资源的事情。

2.先更新数据库,再删除缓存,有什么问题?怎么解决?
会导致数据不一致的情况:比如先更新数据库,再删除缓存,如果缓存删除失败,就会导致数据不一致。

解决思路:先删除缓存,然后再更新数据库,如果数据库更新失败,那么数据库也是旧的数据,缓存也是空的,再次查询也是查询数据库数据到缓存中,不会出现不一致的情况。

3.在大量并发读的情况,同时同时有更新数据的情况,先删除缓存,再更新数据库有可能也会出现数据不一致的情况!
即,数据发生变更,先删除缓存,此时还没来得及更新数据库的数据,此时读请求进来,看到缓存中没有,访问数据库,查到了数据库旧的数据,然后添加到缓存中,随后数据变更完成对数据库数据的修改,此时就会出现缓存和数据库不一致。

解决方案:自己想吧
参考:https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md

  • 缓存+数据库必须保持一致性,不允许出现缓存和数据库出现不一致的情况
    解决方案:读请求和写请求串行化
    就是在写的时候不允许读,在读的时候不允许写。写操作,要写就写缓存+数据库,不能中途,去读。如果在写数据库时,还没写缓存,这时读操作,读到未修改的缓存,就会出现缓存和数据库不一致。

本文参考:JavaGuide
redis线程模型参考:https://www.javazhiyin.com/22943.html
redis.conf注释参考:http://download.redis.io/redis-stable/redis.conf
更多:邓新

发布了34 篇原创文章 · 获赞 0 · 访问量 1089

猜你喜欢

转载自blog.csdn.net/qq_42634696/article/details/105207524
今日推荐