美文网首页技术分享
Redis(五):常见面试题目详解

Redis(五):常见面试题目详解

作者: 雪飘千里 | 来源:发表于2019-08-15 17:13 被阅读0次

1、什么是Redis

Redis 是一个基于内存的高性能 key-value数据库。支持多种数据类型

2、简单描述Redis的特点

Redis本质上是一个key-value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据(内存中)flush到硬盘上进行保存。

纯内存操作,Redis的性能非常出色,每秒可以处理超过10万次读写操作,是已知性能最快的key-value DB

Redis的出色之处,不仅仅是性能,Redis最大的魅力是支持保存多种数据结构;

此外,当个value的最大限制是1GB,不像memcached只能保存1MB的数据,因此Redis可以用来实现很多有用的功能;

Redis的主要缺点,就是数据库容量受到物理内容的限制,不能用做海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上

3、Redis支持的数据类型

String 、List 、 Set、 Sorted Set、 Hash

4、为什么Redis需要把所有数据放到内存中

  • 追求最快的数据读取速度,如果直接磁盘读取会非常慢
  • 为了保证数据安全,也会异步方式将数据写入磁盘
  • 可以设置Redis最大使用的内存,若达到内存限制后将不能继续存取数据

5、Redis是单线程的么?Redis为什么这么快,尤其是其采用单线程??

单线程

Redis是单线程处理网络指令请求,所以不需要考虑并发安全问题。
所有的网络请求都是一个线程处理,但不代表所有模块都是单线程。

高性能

因为它的所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换性能损耗问题;
而且正因为Redis是单线程,所以要小心使用Redis指令,对于那些耗时的指令(比如keys),一定要谨慎使用,一步小心就可能会导致Redis卡顿;

Redis单线程处理多个并发客户端连接:IO多路复用
Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。
Nginx也是采用IO多路复用原理解决C10k问题

6、Redis的持久化机制有哪些?区别是什么?优缺点是什么?

  • 1.RDB持久化:原理是将Redis在内存中的数据库记录定时dump到磁盘上的RDB持久化

  • 2.AOF(append only file)持久化:原理是将Redis的操作日志以追加的方式写入文件

区别:
RDB持久化的指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储

image.png

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录

image.png

RDB 优点:

  • RDB是紧凑的二进制文件,比较合适备份,全量复制等场景
  • RDB恢复数据远快于AOF

RDB缺点:

  • RDB无法实现实时或者秒级持久化
  • 新老版本无法兼容RDB格式

AOF优点:

  • 可以更好地保护数据不丢失
  • appen-only模式读取性能比较高
  • 适合做灾难性的误删除紧急恢复

AOF缺点:

  • 对于同一份文件,AOF文件要比RDB快照大
  • AOF开启后,写的QPS会有所影响,相对于RDB来说,写QPS要下降
  • 数据库恢复比较慢,不适合做冷备

7、Redis的缓存失效策略有哪几种

  • 定时删除策略
    在设置key的过期时间的同时,为该key创建一个定时器,让定时器在可以的过期时间来临时,对可以进行删除
    优点:保证内存尽快释放
    缺点:如果key过多,删除这些key会占用很多CPU时间,而且每个key创建一个定时器,验证影响性能
  • 惰性删除策略
    key过期的时候不删除,每次从数据库获取key的时候去检查是否过期,若过期,则删除,返回null

    优点:CPU占用时间比较少
    缺点:如果key很长时间没有被获取,将不会被删除,可能造成内存泄漏

  • 定期删除策略
    每隔一段时间执行一次删除(在redis.conf配置文件设置hz,1s刷新的频率)过期key的操作

    优点:可以控制删除操作的时长和频率,来减少CPU时间占用,可以避免惰性删除时候内存泄漏的问题
    缺点:对内存友好方面,不如定时策略;对cpu友好方面,不如惰性策略

Redis一般采用惰性策略+定期策略两个相结合

8、什么是缓存命中率?提高缓存命中率的方法有哪些??

  • 命中:可以直接通过缓存获取到需要的数据
  • 不命中:无法直接通过缓存获取到想要的数据,需要再次查询数据库或者执行其他的操作,原因可能是由于缓存中根本不存在,或者缓存已经过期

命中率越高表示使用缓存作用越好,性能越高(响应时间越短,吞吐量越高),并发能力也越好。

重点关注访问评率高且时效性相对低一些的业务数据上,利用预加载(预热)、扩容、优化缓存丽都。更新缓存等手段来提高命中率

9、redis分布式锁原理

9.1 单机模式

https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng==&mid=2247483893&idx=1&sn=32e7051116ab60e41f72e6c6e29876d9&chksm=fba6e9f6ccd160e0c9fa2ce4ea1051891482a95b1483a63d89d71b15b33afcdc1f2bec17c03c&mpshare=1&scene=1&srcid=0416Kx8ryElbpy4xfrPkSSdB&key=1eff032c36dd9b3716bab5844171cca99a4ea696da85eed0e4b2b7ea5c39a665110b82b4c975d2fd65c396e91f4c7b3e8590c2573c6b8925de0df7daa886be53d793e7f06b2c146270f7c0a5963dd26a&ascene=1&uin=MTg2ODMyMTYxNQ%3D%3D&devicetype=Windows+10&version=62060739&lang=zh_CN&pass_ticket=y1D2AijXbuJ8HCPhyIi0qPdkT0TXqKFYo%2FmW07fgvW%2FXxWFJiJjhjTsnInShv0ap

Redisson底层原理简单描述:
先判断一个key存在不存在,如果不存在,则set key,同时设置过期时间和value(1),
这个过程使用lua脚本来实现,可以保证多个命令的原子性,当业务完成以后,删除key;
如果存在说明已经有别的线程获取锁了,那么就循环等待一段时间后再去获取锁

如果是可重入锁呢:
先判断一个key存在不存在,如果不存在,则set key,同时设置过期时间和value(线程id:1),
如果存在,则判断value中的线程id是否是当前线程的id,如果是,说明是可重入锁,则value+1,变成(线程id:2),如果不是,说明是别的线程来获取锁,则获取失败;这个过程同样使用lua脚本一次性提交,保证原子性。

如何防止业务还没执行完,但是锁key过期呢,可以在线程加锁成功后,启动一个后台进程看门狗,去定时检查,如果线程还持有锁,就延长key的生存时间——Redisson就是这样实现的。

其实Jedis也有现成的实现方式,单机、集群、分片都有实现,底层原理是利用连用setnx、setex指令
(Redis从2.6之后支持setnx、setex连用)

jedis.set(key, value, "NX", "PX", expire)
image.png

注:setnx和setex都是原子性的
SETNX key value
将 key 的值设为 value ,当且仅当 key 不存在;若给定的 key 已经存在,则 SETNX 不做任何动作。
相当于是 EXISTS 、SET 两个命令连用
SETEX key seconds value
将value关联到key, 并将key的生存时间设为seconds(以秒为单位);如果key 已经存在,SETEX将重写旧值;
相当于是SET、EXPIRE两个命令连用

9.2 Cluster集群模式

很明显,上面介绍的分布式锁的实现只支持单机redis,工作中我们最常用的还是Cluster集群模式,上面的实现方式在集群模式下,是存在问题的,Cluster集群模式介绍见Redis(四):集群模式

整个过程如下:

  1. 客户端1在Redis的节点A上拿到了锁;
  2. 节点A宕机后,客户端2发起获取锁key的请求,这时请求就会落在节点B上;
  3. 节点B由于之前并没有存储锁key,所以客户端2也可以成功获取锁,即客户端1和客户端2同时持有了同一个资源的锁。

针对这个问题。Redis作者antirez提出了RedLock算法来解决这个问题

9.2.1 RedLock算法

RedLock算法思路如下:

  1. 获取当前时间的毫秒数startTime;

  2. 按顺序依次向N个Redis节点执行获取锁的操作,这个获取锁的操作和前面单Redis节点获取锁的过程相同,同时锁超时时间应该远小于锁的过期时间

  3. 如果客户端向某个Redis节点获取锁失败/超时后,应立即尝试下一个Redis节点;
    失败包括Redis节点不可用或者该Redis节点上的锁已经被其他客户端持有

  4. 如果客户端成功获取到超过半数的锁时,记录当前时间endTime,同时计算整个获取锁过程的总耗时costTime = endTime - startTime,如果获取锁总共消耗的时间远小于锁的过期时间(即costTime < expireTime),则认为客户端获取锁成功,否则,认为获取锁失败

  5. 如果获取锁成功,需要重新计算锁的过期时间。它等于最初锁的有效时间减去第三步计算出来获取锁消耗的时间,即expireTime - costTime

  6. 如果最终获取锁失败,那么客户端立即向所有Redis发起释放锁的操作。(和单机释放锁的逻辑一样)

9.2.2 缺陷

RedLock算法虽然可以解决单点Redis分布式锁的安全性问题,但如果集群中有节点发生崩溃重启,还是会对锁的安全性有影响的。

假设一共有5个Redis节点:A, B, C, D, E。设想发生了如下的事件序列:

  1. 客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住);
  2. 节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了;
  3. 节点C重启后,客户端2锁住了C, D, E,获取锁成功;

这样,客户端1和客户端2同时获得了锁(针对同一资源)。针对这样场景,解决方式也很简单,也就是让Redis崩溃后延迟重启,并且这个延迟时间大于锁的过期时间就好。这样等节点重启后,所有节点上的锁都已经失效了。也不存在以上出现2个客户端获取同一个资源的情况了

还有一种情况,如果客户端1获取锁后,访问共享资源操作执行任务时间过长(要么逻辑问题,要么发生了GC),导致锁过期了,而后续客户端2获取锁成功了,这样就会导致客户端1和客户端2同时操作共享资源,相当于同一个时刻出现了2个客户端获得了锁的情况。这也就是上面锁过期时间要远远大于加锁消耗的时间的原因。
服务器台数越多,出现不可预期的情况也越多,所以针对分布式锁的应用的时候需要多测试。
如果系统对共享资源有非常严格要求得情况下,还是建议需要做数据库锁的方案来补充,如飞机票或火车票座位得情况。
对于一些抢购获取,针对偶尔出现超卖,后续可以通过人工介入来处理,毕竟redis节点不是天天奔溃,同时数据库锁的方案
性能又低。

9.2.2 实现

redisson包已经有对redlock算法封装

public interface DistributedLock {
    /**
     * 获取锁
     * @author zhi.li
     * @return 锁标识
     */
    String acquire();

    /**
     * 释放锁
     * @author zhi.li
     * @param indentifier
     * @return
     */
    boolean release(String indentifier);
}

public class RedisDistributedRedLock implements DistributedLock {

    /**
     * redis 客户端
     */
    private RedissonClient redissonClient;

    /**
     * 分布式锁的键值
     */
    private String lockKey;

    private RLock redLock;

    /**
     * 锁的有效时间 10s
     */
    int expireTime = 10 * 1000;

    /**
     * 获取锁的超时时间
     */
    int acquireTimeout  = 500;

    public RedisDistributedRedLock(RedissonClient redissonClient, String lockKey) {
        this.redissonClient = redissonClient;
        this.lockKey = lockKey;
    }

    @Override
    public String acquire() {
        redLock = redissonClient.getLock(lockKey);
        boolean isLock;
        try{
            isLock = redLock.tryLock(acquireTimeout, expireTime, TimeUnit.MILLISECONDS);
            if(isLock){
                System.out.println(Thread.currentThread().getName() + " " + lockKey + "获得了锁");
                return null;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public boolean release(String indentifier) {
        if(null != redLock){
            redLock.unlock();
            return true;
        }

        return false;
    }
}

相关文章

网友评论

    本文标题:Redis(五):常见面试题目详解

    本文链接:https://www.haomeiwen.com/subject/xqbzjctx.html