Redis面试题

Redis的数据结构及使用场景

Redis的数据结构有:

  1. 字符串:可以用来做最简单的数据缓存,可以缓存某个简单的字符串,也可以缓存某个json格式的字符串,Redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器,Session共享,分布式ID
  2. 哈希表:可以用来存储一些key-value对,更适合用来存储对象
  3. 列表:Redis的列表通过命令的组合,既可以当作栈,也可以当作队列来使用,可以用来缓存类似微信公众号,微博等消息流数据
  4. 集合:和列表类似,也可以存储多个元素,但是不能重复,集合可以进行交集,并集,差集操作,从而可以实现类似,我和某人共同关注的人,朋友圈点赞等功能
  5. 有序集合:集合是无序的,有序集合可以设置顺序,可以用来实现排行榜功能

RDB和AOF机制

RDB:Redis DataBase

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

优点

  1. 整个Redis数据库将只包含一个文件dump.rdb,方便持久化
  2. 容灾行好,方便备份
  3. 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化,使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能

缺点

  1. 数据安全性低,RDB是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失,所以这种方式更适合数据要求不严谨的时候
  2. 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是一秒钟

AOP:Append Only File

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

优点

  1. 数据安全,Redis中提供了三种同步策略,即每秒同步,每修改同步和不同步,事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么一秒钟之内修改的数据将会丢失,而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中
  2. 通过append模式写文件,即使中途服务器宕机也不会破坏已经存在的内容,可以通过redis-check-aof工具解决数据一致性问题
  3. AOF机制的rewrite模式,定期对AOF文件进行重写,以达到压缩的目的

缺点

  1. AOF文件比RDB文件大,且恢复速度慢
  2. 数据集大的时候,比rdb启动效率低
  3. 运行效率没有RDB高

AOF文件比RDB更新频率高,优先使用AOF还原数据
AOF比RDB更安全
RDB性能比AOF好
如果两个都配了优先加载AOF

Redis的过期键的删除策略

Redis是key-value数据库,我们可以设置Redis中缓存的key的过期时间.Redis的过期策略就是指当Redis中缓存的key过期了,Redis如何处理

  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除.该策略可以最大化地节省CPU资源,却对内存非常不友好,极端情况下可能出现大量地过期key没有再次被访问,从而不会被清除,占用大量内存
  • 定时过期,定时器–>时间到了就删除
  • 定期过期:每隔一定地时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期地key,该策略是前两者的一个折中方案.通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果

(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度UNIX时间戳表示的过期时间,键空间是指该Redis集群中保存的所有键).

Redis中同时使用了惰性过期和定期过期两种过期策略

Redis线程模型,单线程为什么快

Redis基于Reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器file event handler.这个文件事件处理器,它是单线程的,所以Redis才叫做单线程的模型,它采用IO多路复用机制来同时监听多个Socket,根据Socket上的事件类型来选择对应的事件处理器来处理这个事件.可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis内部的线程模型的简单性

文件事件处理器的结构包含4个部分:多个Socket,IO多路复用程序,文件事件分派器以及事件处理器(命令请求处理器,命令回复处理器,连接应答处理器).

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

然后一个Socket的事件处理完之后,IO多路复用程序才将队列中的下一个Socket给事件分派器,文件事件分派器会根据每个Socket当前产生的事件,来选择对应的事件处理器来处理

单线程快的原因
纯内存操作
核心是基于非阻塞的IO多路复用机制
单线程反而避免了多线程的频繁上下文切换带来的性能问题

缓存雪崩,缓存穿透,缓存击穿

缓存雪崩

缓存雪崩是指缓存同一时间大面积的失效,所以,所有的请求都会落到数据库上,造成数据库短时间承受大量请求而崩掉(缓存重启)

解决方案

  • 缓存数据过期时间设置随机,防止同一时间大量数据过期现象发生
  • 给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失败,则更新数据缓存
  • 缓存预热(重启)
  • 互斥锁(查数据库对redis加锁)

缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉

解决方案

  • 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用).这样可以防止攻击用户用同一个id暴力攻击
  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力

缓存击穿

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存事件到期),这时由于并发用户特别多,同时读缓存没有读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力.和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库

解决方案

  • 设置热点数据永远不过期
  • 加互斥锁

简述redis事务实现

深入理解Redis事务

事务开始前可以先watch key

  1. 事务开始
    MULTI命令的执行,标识着一个事务的开始.MULTI命令会将客户端状态的flags属性中打开REDIS_MULTI标识来完成的
  2. 命令入队
    当一个客户端切换到事务状态之后,服务器会根据这个客户端发送来的命令来执行不同的操作.如果客户端发送的命令为MULTI,EXEC,DISCARD,WATCH,MULTI四个命令的其中一个,那么服务器立即执行这个命令,否则将命令放入一个事务队列里面,然后向客户端返回QUEUED恢复
  • 如果客户端发送的命令为EXEC,DISCARD,WATCH,MULTI四个命令的其中一个,那么服务器立即执行这个命令
  • 如果客户端发送的是四个命令以外的其他命令,那么服务器并不立即执行这个命令
    首先检查此命令的格式是否正确,如果不正确,服务器会在客户端状态(redisClient)的flags属性关闭REDIS_MULTI标识,并且返回错误信息给客户端
    如果正确,将这个命令放入一个事务队列里面,然后向客户端返回QUEUED回复

事务队列是按照FIFO的方式保存入队的命令

  1. 事务执行
    客户端发送EXEC命令,服务器执行EXEC命令逻辑.
  • 如果客户端状态的flags属性不包括REDIS_MULTI标识,或者包含REDIS_DIRTY_CAS或者REDIS_DIRTY_EXEC标识,那么就直接取消事务的执行
  • 否则客户端处于事务状态(flags有REDIS_MULTI标识),服务器会遍历客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果全部返回给客户端

Redis不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误
Redis事务不支持检查那些程序员自己的逻辑错误,例如对String类型的数据库键执行对HashMap类型的操作

  • WATCH命令是一个乐观锁,可以为Redis事务提供check-and-set(CAS)行为,可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令
  • MULTI命令用于开启一个事务,它总是返回OK.MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行
  • EXEC:执行所有事务块内的命令,返回事务块内所有命令的返回值,按命令执行的先后顺序排列,当操作被打断时,返回空值null
  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务,并且客户端会从事务状态中退出
  • UNWATCH命令可以取消watch对所有key的监控

redis集群方案

主从

这种模式比较简单,主库可以读写,并且会和从库进行数据同步,这种模式下,客户端直接连主库或者某个从库,但是一旦主库或从库宕机后,客户端需要手动修改IP,另外,这种模式也比较难进行扩容,整个集群所能存储的数据收到某台及其的内存容量,所以不可能支持特大数据量

哨兵模式

sentinel,哨兵是redis集群中非常重要的一个组件,主要有以下功能:

  • 集群监控:负责监控 redis master 和 slave 进程是否正常工作
  • 消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员.
  • 故障转移:如果master node 挂掉了,会自动转移到slave node上
  • 配置中心:如果故障转移发生了,通知client客户端新的master地址

哨兵用于实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作

  • 故障转移时,判断一个master node是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举
  • 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的
  • 哨兵通常需要三个实例,来保证自己的健壮性
  • 哨兵+redis主从的部署架构,是不保证数据零丢失的,只能保证redis集群的高可用性
  • 对于哨兵+redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练.

Redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供,采用Slot(槽)的概念,一共分成16384个槽,将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行
方案说明

  • 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384个槽位
  • 每份数据分片会存储在多个互为主从的多节点上
  • 数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
  • 同一分片多个节点间的数据不保持强一致性
  • 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
  • 扩容时需要把旧节点的数据迁移一部分到新节点

在redis cluster架构下,每个redis要放开两个端口号,比如一个是6379,另外一个就是加10000的端口号,比如16379

16379端口号是用来节点间通信的,也就是cluster bus的通信,用来进行故障检测,配置更新,故障转移授权.cluster bus用了另外一种二进制协议,gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间

优点

  • 无中心架构,支持动态扩容,对业务透明
  • 具备Sentinel监控和自动Failover(故障转移)能力
  • 客户端不需要连接集群所有节点,连接集群中一个可用节点即可
  • 高性能,客户端直连redis服务,面取了proxy代理的损耗

缺点

  • 运维和复杂,数据库迁移需要人工干预
  • 只能使用0号数据库
  • 不支持批量操作
  • 分布式逻辑和存储模块耦合等

Redis Sharding是Redis Cluster出来之前,业界晋遍使用的多Redis实例集群方法。其主要思想是采用哈希算法将Redis数据B9key进行散列,通过数,特定的key会映射到特定的Redis节点上。Javaredis客户端驱动jedis,支ßRedisSharding功能,即ShardedJedis以及结合缓存池的ShardedJedisPooI

优点

优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易

线性扩展,系统的灵活性很强

缺点

由于sharding处理放到客户端,规模进一步扩大时给运维带来挑战。

客户端sharding不支持动态增删节点。服务Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。思接不能共享,当应用规模增大时,资源浪费制约优化

redis主从复制的核心原理

在这里插入图片描述
Redis的面试连环炮

Redis分布式锁

Redis分布式锁 (图解-秒懂-史上最全) - 疯狂创客圈 - 博客园
Redis/Redis实现分布式锁 · 陌溪/LearningNotes - 码云 - 开源中国

Redis内存满了怎么办

redis默认内存多少?怎么查看?如何修改?

默认在64位操作系统下不限制内存大小,32位最大3G

一般推荐Redis设置内存位最大物理内存的四分之三

可以通过配置文件修改和查看(redis.conf)
在这里插入图片描述
可以通过命令修改
在这里插入图片描述
在这里插入图片描述

内存满了会怎么样?

在这里插入图片描述

Redis过期淘汰策略

配置文件中的八种缓存淘汰策略
在这里插入图片描述

redis过期键的删除策略

如果一个键是过期的,那它到了过期时间之后是不是马上就从内存中被删除呢?

  • 定时删除
    Redis不可能时时刻刻遍历所有被设置了生存时间的key,来检测数据是否以及到达过期时间,然后对它进行删除
    立即删除能保证内存中的数据的最大新鲜度,因为它保证过期键值会在过期后马上被删除,其所占用的内存也会随之释放.但是立即删除对cpu是最不友好的,因为删除操作会占用cpu的时间
    这会产生大量的性能消耗,同时也会影响数据的读取操作

总结:对cpu不友好,用处理器性能换取存储空间

  • 惰性删除
    数据到达过期时间,不做处理,等下次访问该数据时
    如果未过期,返回数据
    发现已过期,删除,返回不存在
    惰性删除策略的缺点是,它对内存是最不友好的
    如果一个键已经过期,而这个键又仍然保留在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放
    在使用惰性删除策略时,如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么它们也许永远也不会被删除,我们甚至可以将这种情况视作内存泄漏-无用的垃圾数据占用了大量的内存,而服务器却不会自己去释放它们

总结:对内存不友好,用存储空间换了处理器性能

  • 定期删除
    定期删除策略每隔一段时间执行一次删除过期操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响

周期性轮询redis库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度
特点1:CPU性能占用设置有峰值,检测频度可自定义设置
特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
总结:周期性抽查存储空间(随机抽查,重点抽查)
定期删除的难点是确定删除操作执行的时长和频率:如果删除操作执行的太频繁,或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将CPU时间过多地消耗在删除过期键上面.如果删除操作执行地太少,或者执行地时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况.因此,如果要采用定期删除策略的话,服务器必须根据情况,合理地设置删除操作地执行时长和执行频率

由于定期删除和惰性删除具有随机性,所以必须要有一个更好地兜底方案

八种缓存过期淘汰策略

  • noeviction:不会驱逐任何key
  • allkeys-lru:对所有key使用LRU算法进行删除 ✔
  • volatile-lru:对所有设置了过期时间地key使用LRU算法进行删除
  • allkeys-random:对所有key随机删除
  • volatile-random:对所有设置了过期时间地key随机删除
  • volatile-ttl:删除马上要过期地key
  • allkeys-lfu:对所有key使用LFU算法进行删除
  • volatile-lfu:对所有设置了过期时间的key使用LFU算法进行删除

LRU Least Recently Used
LFU Least Frequently Used

如何配置

在这里插入图片描述
配置文件也可以

LRU算法

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法

选择最近最久未使用的数据予以淘汰

public class LRU {
   
    //map负责查找,构建一个虚拟的双向链表,它里面安装的就是一个个Node节点,作为数据载体

    //构造一个Node节点,作为数据载体
    class Node<K, V> {
        K key;
        V value;
        Node<K, V> prev;
        Node<K, V> next;

        public Node() {
            this.prev = this.next = null;

        }

       

        public Node(K key, V value) {
            this.key = key;
            this.value = value;
            this.prev = this.next = null;
        }
    }

    //构造虚拟一个双向队列,里面安放的就是我们的Node
    class DoubleLinkedList<K, V> {
        Node<K, V> head;
        Node<K, V> tail;

       

        //构造方法
        public DoubleLinkedList() {
            head = new Node<>();
            tail = new Node<>();

            head.next = tail;
            tail.prev = head;
        }

        //添加到头
        public void addHead(Node<K, V> node) {
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;

        }

        //删除节点
        public void removeNode(Node<K, V> node) {
            node.next.prev = node.prev;
            node.prev.next = node.next;
            node.next = null;
            node.prev = null;
        }

        //获得最后一个节点
        public Node getLast(){
            return tail.prev;
        }
    }
    private int cacheSize;

    Map<Integer, Node<Integer,Integer>> map;
    DoubleLinkedList<Integer,Integer> doubleLinkedList;

    public LRU(int cacheSize){
        this.cacheSize = cacheSize;//缓存大小
        map = new HashMap<>();
        doubleLinkedList = new DoubleLinkedList<>();


    }
    public int get(int key){
        if (!map.containsKey(key)){
            return -1;
        }
        Node<Integer, Integer> node = map.get(key);
        doubleLinkedList.removeNode(node);
        doubleLinkedList.addHead(node);
        return node.value;
    }

    public void put(int key,int value){
        if(map.containsKey(key)){
            Node<Integer, Integer> node = map.get(key);
            node.value = value;
            map.put(key, node);
            doubleLinkedList.removeNode(node);
            doubleLinkedList.addHead(node);
        }else{
            if(map.size() == cacheSize){
                Node<Integer,Integer> last = doubleLinkedList.getLast();
                map.remove(last.key);
                doubleLinkedList.removeNode(last);
            }
            //新增一个value
            Node<Integer, Integer> newNode = new Node<>(key, value);
            map.put(key, newNode);
            doubleLinkedList.addHead(newNode);
        }
    }

    
}

Redis补充知识

Redis的面试连环炮

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值