redis 以及 分布式锁 面试总结

1、什么是缓存雪崩,怎么解决
通畅,我们会使用缓存用于缓冲对DB的冲击,如果缓存宕机,所有请求将直接打在DB,造成DB宕机,从而导致整个系统宕机。
解决方法:
1、对缓存做高可用,防止缓存宕机
2、使用断路器,如果缓存宕机,为防止系统全部宕机,限制部分流量进入DB,保证部分可用,其余的请求返回断路器的默认值。

2、什么是缓存穿透,怎么解决
解释1:缓存查询一个没有的key,同时数据库也没有,如果黑客大量使用这种方式,那么就会导致DB宕机。

解决方案:我们可以使用一个默认值来防止,例如,当访问一个不存在的key,然后再去访问数据库还是没有,那么就在缓存中放一个占位符,下次来的时候检查这个占位符,如果发现有占位符就不去数据库查询,防止DB宕机了

解释2:大量请求查询一个刚刚失效的key,导致DB压力倍增,可能导致宕机,但实际上查询的相同的数据。

解决方法:可以在这些请求代码加上双重检查锁。但是那个阶段的请求i会变慢,不过总比DB宕机好。

3、什么是缓存并发竞争?怎么解决?
解释:多个客户端写一个key,如果顺序错了,数据就不对了。但是顺序我们无法控制。
解决方法:使用分布式锁,例如zk,同时加入数据的时间戳。同一时刻,只有抢到锁的客户端写入,同时写入时,比较当前数据时间戳和缓存中数据的时间戳。

4、什么是缓存和数据库双写不一致?怎么解决?
解释:连续写数据库和缓存,但是操作期间,出现并发了,数据不一致了。

1、先更新数据库,在更新缓存
这么做的问题是当有2个请求同时更新数据,那么如果不使用分布式锁,将无法控制最后缓存的值到底是多少。也就是并发写有问题。

在多线程并发情况下,假设有两个数据库修改请求,为保证数据库与redis的数据一致性,
修改请求的实现中需要修改数据库后,级联修改redis中的数据。
请求一:1.1修改数据库数据 1.2 修改redis数据
请求二:2.1修改数据库数据 2.2 修改redis数据
并发情况下就会存在1.1 —> 2.1 —> 2.2 —> 1.2的情况
(一定要理解线程并发执行多组原子操作执行顺序是可能存在交叉现象的)

此时存在的问题就是:
1.1修改数据库的数据最终保存到了redis中,2.1在1.1之后也修改了数据库数据。
此时出现了redis中数据和数据库数据不一致的情况,在后面的查询过程中就会长时间去先查redis,
从而出现查询到的数据并不是数据库中的真实数据的严重问题。
问题解决:
修改数据库级联修改redis数据改为 修改数据库数据后级联删除redis数据
至于是先执行1.2的redis删除,还是限制性2.2的redis删除,无关紧要。
结果都是redis中数据已被删除。之后的查询就会由于redis中没有数据而去查数据库,
此时即不会存在查询到的数据和数据库的数据不一致的情况。

2、先删除缓存,在更新数据库
这么做的问题:如果在删除缓存后,有客户端读数据,有可能读到旧数据,并有可能设置到缓存中,导致缓存中的数据一直是老数据。

有两种解决方式:
1、使用"双删",即删更删,最后一步的删除作为异步操作,就是防止有客户端读取的时候设置了旧值。
2、使用队列,当这个key不存在时,将其放入队列,串行执行,必须等到更新数据库完毕后才能读取数据。

3、先更新数据库再删除缓存

这个实际是常用的方案,但是有很多人不知道,这里介绍一下,这个叫 Cache Aside Pattern,老外发明的。如果先更新数据库,再删除缓存,那么就会出现更新数据库之前有瞬间数据不是很及时。
同时,如果在更新之前,缓存刚好失效了,读客户端有可能读到旧值,然后在写客户端删除结束后再次设置了旧值,非常巧合的情况。
有 2 个前提条件:缓存在写之前的时候失效,同时,在写客户度删除操作结束后,放置旧数据 —— 也就是读比写慢。设置有的写操作还会锁表。
所以,这个很难出现,但是如果出现了怎么办?使用双删!!!记录更新期间有没有客户端读数据库,如果有,在更新完数据库之后,执行延迟删除。
还有一种可能,如果执行更新数据库,准备执行删除缓存时,服务挂了,执行删除失败怎么办???
这就坑了!!!不过可以通过订阅数据库的 binlog 来删除。

上面的单删策略情况如下:
修改请求的实现中需要修改数据库后,级联删除redis中的数据。
请求一:1.1修改数据库数据 1.2 删除redis数据
请求二:2.1修改数据库数据 2.2 删除redis数据

假设现在并发存在一个查询请求
请求三:3.1查询redis中数据 3.2查询数据库数据 3.3 新查到的数据写入redis
(一定要理解带redis的查询请求实现逻辑,先查redis,数据不存在查数据库,
查到的数据写入redis以便以后的查询不去直接查数据库)

此时并发情况下就会存在1.1 —> 1.2 —> 3.1 —> 3.2 —> 2.1 —> 2.2 —> 3.3的情况

此时存在的问题就是:
此时数据库中的数据保存的是2.1修改后的数据,而redis中保存的数据是3.2中在1.1修改数据后的结果,
此时出现了redis中数据和数据库数据不一致的情况,在后面的查询过程中就会长时间去先查redis,
从而出现查询到的数据并不是数据库中的真实数据的严重问题。

上面的单删策略存在问题的情况如下:
请求一:1.1修改数据库数据 1.2 删除redis数据
请求二:2.1修改数据库数据 2.2 删除redis数据
请求三:3.1查询redis中数据 3.2查询数据库数据 3.3 新查到的数据写入redis

添加延时双删策略后的情况
请求一:1.1修改数据库数据 1.2 删除redis数据 1.3 延时3–5s再去删除redis中数据
请求二:2.1修改数据库数据 2.2 删除redis数据 2.3 延时3–5s再去删除redis中数据
请求三:3.1查询redis中数据 3.2 查询数据库数据 3.3 新查到的数据写入redis

双删策略为什么能解决问题:
因为存在了延时时间,故1.3或2.3 一定是最后执行的一步操作(并发中的延时一定要理解)
延时的根本目的就是为了让程序先把3.3执行完,再去删除redis

比较好的: 项目整合quartz等定时任务框架,去实现延时3–5s再去执行最后一步任务
比较一般的: 创建线程池,线程池中拿一个线程,线程体中延时3-5s再去执行最后一步任务(不能忘了启动线程)
比较差的: 单独创建一个线程去实现延时执行

5、什么是bloom filter 布隆过滤器
布隆过滤器实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。
布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
见的几个应用场景:

  • cerberus在收集监控数据的时候, 有的系统的监控项量会很大, 需要检查一个监控项的名字是否已经被记录到db过了, 如果没有的话就需要写入db.

  • 爬虫过滤已抓到的url就不再抓,可用bloom filter过滤

  • 垃圾邮件过滤。如果用哈希表,每存储一亿个 email地址,就需要 1.6GB的内存(用哈希表实现的具体办法是将每一个 email地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有 50%,因此一个 email地址需要占用十六个字节。一亿个地址大约要 1.6GB,即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百 GB的内存。而Bloom Filter只需要哈希表 1/8到 1/4 的大小就能解决同样的问题。

  • 优点:由于存放的不是完整的数据,所以占用的内存很少,而且新增,查询速度够快;

  • 缺点:随着数据的增加,误判率随之增加;无法做到删除数据;只能判断数据是否一定不存在,而无法判断数据是否一定存在。

6、为啥Redis那么快?

Redis采用的是基于内存的采用的是单进程单线程模型的 KV 数据库,由C语言编写,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。
完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。它的,数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
使用多路I/O复用模型,非阻塞IO;
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

7、单机会有瓶颈,那你们是怎么解决这个瓶颈的?
我们用到了集群的部署方式也就是Redis cluster,并且是主从同步读写分离,类似Mysql的主从同步,Redis cluster 支撑 N 个 Redis master node,每个master node都可以挂载多个 slave node。
这样整个 Redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。

8、单机redis分布式锁
set orderId:lock 随机值 nx px 30000
如果此时redis没有orderId:lock这个key就设置成功返回ok,反之返回nil
过30秒key会自动删除
finally手动删除key必须用lua脚本,lua脚本会将出入的随机值和redis的随机值作比较,如果相等就删除,防止误删其他请求的锁。

缺点:单机redis如果故障就会单点故障,普通主从的话,如果主节点挂了,key还没同步从节点,此时从节点切换为主节点,别人就会拿到锁。

9、redis官方推荐redlock 算法 适用于redis集群
这个场景假设有个redis cluster 5 个reids master实例,然后执行如下步骤获取一把锁:
1、获取当前时间戳,单位是毫秒
2、跟上面类似,轮流尝试在每个master节点上创建锁,过期时间较短,一般就即使毫秒
3、舱室在大多数节点上建立一个锁,比如5个节点就要求3个节点(n/2 +1)
4、客户端计算建立锁好的时间,如果建立的时间小于超时,就算是建立成功了。
5、要是别人建立了一把分布式锁,你就得不停轮询尝试获取这把锁。

10 zk分布式锁 (推荐作为分布式锁)
A系统获取锁得时候就是尝试去创建一个临时节点。如果这个临时节点不存在,你就创建成功。
如果b系统也来加锁,尝试去创建相同名称得临时节点,如果已经存在,说明别人这把锁说明已经是失败,对那个临时节点注册监听器就ok。
一旦临时节点删除,zk会通知b系统这个锁释放掉

临时顺序节点思路
//如果有一把锁,被多个人竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁,后面的每个人都会去监听排在自己前面的那个人创建的node上,一旦某个人释放了锁,排在自己后面的人就会被zookeeper给通知,一旦被通知了之后就是ok,自己就获取了锁,就可以执行代码了

临时节点是防止zk服务器挂了之后造成死锁

引入包

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>ZookeeperDistrributionLock</artifactId>
    <version>1.0-SNAPSHOT</version>
<dependencies>
    <!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.4.8</version>
    </dependency>
    <!--Curator是Netflix公司开源的一套zookeeper客户端框架,解决了很多Zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等等。Patrixck Hunt(Zookeeper)以一句“Guava is to Java that Curator to Zookeeper”给Curator予高度评价。
    <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework -->
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-framework</artifactId>
        <version>3.3.0</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>3.3.0</version>
    </dependency>

</dependencies>

</project>
package com.apache.app;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.io.IOException;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
* 自定义类实现锁接口
*
* 个人总结:分布式锁执行原理:
* 多个进程请求进入zookeeper,zookeeper是一个树状结构且同级节点不能重复且有序
* 第一步:多个进程在根节点下面先创建子节点
* 第二步: 只有顺序最小的节点才可以获得锁成功,其他节点都在监听上一个节点的状态,
* 如果上个节点状态发生变化,也就是获得锁成功后释放锁,那么这个节点才可以获得锁成功
* 这样保证了始终只是最小的节点获得锁。(获得锁也就是当前节点对应得进程才能进行某些操作)
* 现实场景模拟原理:好像是排队买食物的时候,每次只能有一个人消费,排队的人看到他前面
* 一个人买好东西离开之后,他才可以进行买东西,然后离去让他后面的人买东西。
*
* 应用场景: 解决商品超卖现象
*
*
*
*
*
*
*/
public class DistributeLock  implements Lock, Watcher {
    private ZooKeeper zk=null;
    private  String ROOT_LOCK="/locks";  //定义根节点
    private  String WAIT_LOCK;//等待前一个锁,相对于CURRENT_LOCK,那么前一个节点就是WAIT_LOCK
    //创建临时有序节点,每个进程进来都会创建成功 ,这个CURRENT_LOCK指的就是对应进程创建的临时节点
    private  String CURRENT_LOCK;//表示当前的锁
    /**
     * CountDownLatch:
     * 一个可以用来协调多个线程之间的同步,
     * 或者说起到线程之间的通信作用的工具类。
     * 它能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。
     * 计数器初始值为线程的数量。
     * 当每一个线程完成自己任务后,计数器的值就会减一。
     * 当计数器的值为0时,表示所有的线程都已经完成了任务,
     * 然后在CountDownLatch上等待的线程就可以恢复执行任务。
     */
    private CountDownLatch countDownLatch;//做一个控制的

    public DistributeLock() {
        try {
            //创建连接
            //this表示当前对象,因为当前对象实现了Watcher接口
            zk = new ZooKeeper("localhost:2181",4000,this);
          //exists判断根节点是否存在,含有注册事件 。watch: false .因为不需要再注册对这个节点的事件。
            Stat stat = zk.exists(ROOT_LOCK, false);
            if (stat == null) {
                //创建根节点
                zk.create(ROOT_LOCK,"0".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
                        CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }



    public void lock() {
        if(this.tryLock()){
            //如果获得锁成功,那么就恭喜获得锁了
            System.out.println(Thread.currentThread().getName()+"----->"+CURRENT_LOCK+"-->获得锁成功");
            return;//终止程序, 否则的话一直阻塞,一直等到锁释放为止。
        }
        waitForLock(WAIT_LOCK);//没有获得锁,继续等待锁。阻塞

    }
    //等待锁
    private   boolean  waitForLock(String prev){
        try {
            //监听当前节点的上一个节点
            Stat stat = zk.exists(prev, true);//因为它没有获得锁,所以就需要监听上一个节点的状态
            if (stat != null) {
                System.out.println(Thread.currentThread().getName()+"--->等待锁"+ROOT_LOCK+"/"+prev+"释放");
               //每次减少1
                countDownLatch=new CountDownLatch(1);
                //它能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。
                //直到prv节点不存在的时候,countDownLatch才释放。
                //个人理解:   countDownLatch发令枪在锁释放的时候,执行down()方法,
                // 其他进程节点开始抢锁。否则一直在 await()等待阻塞阶段。
                countDownLatch.await();
                System.out.println(Thread.currentThread().getName()+"---->"+"获得锁成功");
                //下面跳到  处理监听事件 process()的监听方法

            }
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return  false;
    };

    public void lockInterruptibly() throws InterruptedException {

    }
     //首先创建根下面的子节点,如果有三个客户端调用这个方法,那么会创建三个有序临时节点。
    public boolean tryLock() {
        try {
            //创建临时有序节点,每个进程进来都会创建成功 ,这个CURRENT_LOCK指的就是对应进程创建的临时节点
          CURRENT_LOCK = zk.create(ROOT_LOCK + "/", "0".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
          //1.针对每个客户端,他所拿到的是每个客户端所创建的临时节点
            //Thread.currentThread().getName()表示当前线程名。
            System.out.println(Thread.currentThread().getName()+"---->"+CURRENT_LOCK+"尝试竞争锁");
            //2.获取根节点下面的子节点 ;watch:false;解释: getChildren也是一个注册监听的方法,再监听就矛盾了
            List<String> childrens = zk.getChildren(ROOT_LOCK, false);
            //3.拿到之后进行排序,找最小节点
            SortedSet<String> sortedSet=new TreeSet<String>();//定义一个
            for (String children:  childrens) {
                sortedSet.add(ROOT_LOCK+"/"+children);
            }
            //4.sortedSet是树状结构,获得当前子节点中最小的“节点”。
            String firstNode = (String) sortedSet.first();
            /**
             *  如果前面有节点,那么就判断前面是最小的;当前没有节点了,那么当前就是最小的
             *  headSet可操纵回去的方法。相当于后退。
             *  假如 子节点CURRENT_LOCK 是sequence_2 , 结果会返回sequence_1;如果是CURRENCE_LOCK是sequence_1,
             *  那么结果就返回sequence_1。
             */
            SortedSet<String> lessThenMe = sortedSet.headSet(CURRENT_LOCK);
            if (CURRENT_LOCK.equals(firstNode)){
                //通过当前的节点(CURRENT_LOCK是指对应进程创建的节点)和子节点中最小的节点进行比较
                //如果相等,表示获得锁成功。
                return  true;
            }
            if (!lessThenMe.isEmpty()){
                WAIT_LOCK = lessThenMe.last();//获得比当前节点更小的最后一个节点,设置给WAIT_LOCK
            }



        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    public void unlock() {
        System.out.println(Thread.currentThread().getName()+"---->释放锁"+CURRENT_LOCK);
        try {
            //把临时有序节点都删掉。
            zk.delete(CURRENT_LOCK,-1);
            CURRENT_LOCK=null;
            zk.close();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

    public Condition newCondition() {
        return null;
    }
    //apache中核心jar提供的唯一接口方法。其他方法均是 java.util.concurrent,JDK自带的。
    //处理监听事件
    public void process(WatchedEvent watchedEvent) {

        //存在这样一个监听
        if (countDownLatch != null) {
            //直到prv节点不存在的时候(比如删除)
            this.countDownLatch.countDown();
        }
    }
}
package com.apache.app;

import java.io.IOException;
import java.util.concurrent.CountDownLatch;

public class TestDistributedLock {
    public static void main(String[] args) throws IOException {
        //模拟10个线程去访问锁,只有一个线程获得锁。
        CountDownLatch countDownLatch=new CountDownLatch(10);
        for (int i = 0; i <10; i++) {
            new Thread(()->{
                try {
                //先阻塞
                    countDownLatch.await();
                    DistributeLock distributeLock=new DistributeLock();
                    distributeLock.lock();//获得锁

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            ,"Thread-"+i).start();
        countDownLatch.countDown();
        }
        //键盘输入。
        System.in.read();
    }
}

11、基于redis的session共享
官网上有,它有一个过滤器SessionRepositryFilter,SessionRepositryRequestWrapper会将httpsession包装用它自己的springsession来代替httpsession,如果有就创建放入redis中,没有就新建一个放入redis中

最后附上敖丙大佬的图,比我详细多了
在这里插入图片描述

参考
https://mp.weixin.qq.com/s/UzYQRhwA4ubDry_Ve59Rpg
https://mp.weixin.qq.com/s/aOiadiWG2nNaZowmoDQPMQ
https://blog.csdn.net/qq_30347133/article/details/109471121

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值