Files
JavaYouth/docs/suibi/我的校招-不完全知识点整理.md
2021-04-28 18:38:41 +08:00

129 KiB
Raw Blame History

title, tags, categories, keywords, description, cover, abbrlink, date
title tags categories keywords description cover abbrlink date
我的校招-不完全知识点整理
校招
面试
随笔
校招,面试 如题,一部分的整理【三万字】,具体的看文章 https://gitee.com/youthlql/randombg/raw/master/bg/00178.webp 5df2d017 2021-04-22 14:21:58

必看说明

  1. 这篇文章是笔者校招时整理的一部分有的是直接给出了url链接也有很多是自己看书的总结(所以直接看答案可能会看不懂),都是相当简洁的版本(反正就是很水很乱,哈哈哈)。这篇文章所包含的知识点,我后面大部分都会重新写详细版,目前放出这篇文章的意义只是给需要看的人一个参考。
  2. 比如说
    • Java集合的源码我会写详细版【TODO这些东西大部分已经看过了在计划中】
    • Java并发我已经写了详细版。不过还有一些的东西没写比如阻塞队列ConcurrentHashMapCopyOnWriteArrayList等这些并发容器也是常考点还有一些常非面试的内容比如手写线程池等等手写阻塞队列等等我觉得很有意思的点。【TODO看了一部分内容在计划中】
    • jvm也已经写了详细版。还有jvm实战还没写【暂时没计划因为我还不会嘿嘿】
    • Mysql部分很重要下面给出的是极简版【TODO看了一部分内容在计划中】
    • Redis【暂时没计划】
    • Dubbo源码【TODO正在学习在计划中】
    • 等等
  3. 希望大家多看些书和视频,背答案只是为了应试(无奈),多看书和视频以及一些讲的比较好的博客,才能理解的更深刻。
  4. 此篇文章发表于2021年4月不再更新后续可能删除

Java基础

为什么重写 equals 方法就必须重写 hashcode 方法?

hashCode介绍

  • hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在 JDK 的 Object.java 中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。

  • hashCode() 在散列表中才有用,在其它情况下没用。散列表存储的是键值对(key-value)在散列表中hashCode() 的作用是获取对象的散列码,进而快速确定该对象在散列表中的位置。(可以快速找到所需要的对象)

为什么要有 hashCode

**问题:**假设HashSet中已经有1000个元素。当插入第1001个元素时需要怎么处理

  • 因为HashSet是Set集合它不允许有重复元素。“将第1001个元素逐个的和前面1000个元素进行比较”显然这个效率是相等低下的。

  • 散列表很好的解决了这个问题,它根据元素的散列码计算出元素在散列表中的位置,然后将元素插入该位置即可。对于相同的元素,自然是只保存了一个。

为什么重写equals方法就必须重写hashcode方法

  • 在散列表中, 1、如果两个对象相等那么它们的hashCode()值一定要相同; 这里的相等是指通过equals()比较两个对象 时返回true

    2、如果两个对象hashCode()相等,它们并不一定相等。(不相等时就是哈希冲突)

    注意:这是在散列表中的情况。在非散列表中一定如此!

  • 考虑只重写equals而不重写 hashcode 时虽然两个属性值完全相同的对象通过equals方法判断为true但是当把这两个对象加入到 HashSet 时。会发现HashSet中有重复元素这就是因为HashSet 使用 hashcode 判断对象是否已存在时造成了歧义,结果会导 致HashSet 的不正常运行。所以重写 equals 方法必须重写 hashcode 方法。

深拷贝和浅拷贝

https://blog.csdn.net/baiye_xing/article/details/71788741

  1. 浅拷贝:对一个对象进行拷贝时,这个对象对应的类里的成员变量。
    • 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值拷贝,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据
    • 对于数据类型是引用数据类型的成员变量(也就是子对象,或者数组啥的)也就是只是将该成员变量的引用值引用拷贝【并发引用传递Java本质还是值传递】复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
  2. 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
  3. 也就是说浅拷贝对于子对象只是拷贝了引用值,并没有真正的拷贝整个对象。

深拷贝实现思路:

1、对于每个子对象都实现Cloneable 接口并重写clone方法。最后在最顶层的类的重写的 clone 方法中调用所有子对象的 clone 方法即可实现深拷贝。【简单的说就是:每一层的每个子对象都进行浅拷贝=深拷贝】

2、利用序列化。【先对对象进行序列化紧接着马上反序列化出 】

八中数据类型及其范围

为什么是-128-127 https://zhidao.baidu.com/question/588564780479617005.html

  • byte1字节范围为-128-127 -2^7——2^7-1
  • short2字节范围为-32768-32767 -2^15——2^15-1
  • int4字节 -2^31——2^31-1
  • long8字节
  • float4字节
  • double8字节
  • booolean比较特殊
    • 官方文档boolean 值只有 true 和 false 两种,这个数据类型只代表 1 bit 的信息但是它的“大小”没有严格的定义。也就是说不管它占多大的空间只有一个bit的信息是有意义的。
    • 单个boolean 类型变量被编译成 int 类型来使用,占 4 个 byte 。
    • boolean 数组被编译成 byte 数组类型,每个 boolean 数组成员占 1 个 byte。
    • 在 Java 虚拟机里1 表示 true 0 表示 false 。
    • 这只是 Java 虚拟机的建议。
    • 可以肯定的是,不会是 1 个 bit 。
  • char2字节

从高精度转到低精度有可能损失精度所以不会自动强转。比如int高精度就不会被自动强转为short如果需要强转就只能强制转换。

Java中int类型的最小值是怎么表示的

首先计算机中保存的都是补码,都是二进制的补码

int型能表示的最大正数

int型的32bit位中第一位是符号为正数位0。因此int型能表示的最大的正数的二进制码是0111 1111 1111 1111 1111 1111 1111 1111也就是2^31-1。

int型能表示的最小负数

最小的负数的二进制码是1000 0000 0000 0000 0000 0000 0000 0000其补码还是1000 0000 0000 0000 0000 0000 0000 0000值是2^31。用2^31来代替-0

反射的作用及机制

名词解释

Java的反射reflection机制是指在程序的运行状态中可以构造任意一个类的对象可以了解任意一个对象所属的类可以了解任意一个类的成员变量和方法可以调用任意一个对象的属性和方法。这种动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键

用途

  • 常用在通用框架里比如Spring。为了保证框架的通用性它们可能需要根据配置文件加载不同的对象或类调用不同的方法这个时候就必须用到反射运行时动态加载需要加载的对象

原理

public class  NewInstanceTest {

    @Test
    public void test1() throws IllegalAccessException, InstantiationException {

        Class<Person> clazz = Person.class;
        /*
        newInstance():调用此方法,创建对应的运行时类的对象。内部调用了运行时类的空参的构造器。

        要想此方法正常的创建运行时类的对象,要求:
        1.运行时类必须提供空参的构造器
        2.空参的构造器的访问权限得够。通常设置为public。


        在javabean中要求提供一个public的空参构造器。原因
        1.便于通过反射,创建运行时类的对象
        2.便于子类继承此运行时类时默认调用super()时,保证父类有此构造器

         */
        Person obj = clazz.newInstance();
        System.out.println(obj);

    }

    //体会反射的动态性
    @Test
    public void test2(){

        for(int i = 0;i < 100;i++){
            int num = new Random().nextInt(3);//0,1,2
            String classPath = "";
            switch(num){
                case 0:
                    classPath = "java.util.Date";
                    break;
                case 1:
                    classPath = "java.lang.Object";
                    break;
                case 2:
                    classPath = "com.atguigu.java.Person";
                    break;
            }

            try {
                Object obj = getInstance(classPath);
                System.out.println(obj);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }



    }

    /*
    创建一个指定类的对象。
    classPath:指定类的全类名
     */
    public Object getInstance(String classPath) throws Exception {
       Class clazz =  Class.forName(classPath);
       return clazz.newInstance();
    }

说一下序列化,网络传输使用什么序列化?序列化有哪些方式

https://www.jianshu.com/p/7298f0c559dc

代理

https://www.cnblogs.com/cC-Zhou/p/9525638.html

静态代理

代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。上面介绍的是静态代理的内容,为什么叫做静态呢?因为它的代理类是事先写好的。而动态代理是动态生成的代理类

动态代理和静态代理区别

  1. 静态代理,代理类需要自己编写代码写成。
  2. 动态代理,代理类通过 newProxyInstance方法生成。
  3. 不管是静态代理还是动态代理,代理与被代理者都要实现两样接口,它们的实质是面向接口编程。
  4. 静态代理和动态代理的区别是在于要不要开发者自己定义 代理 类。
  5. 动态代理通过 Proxy 动态生成 proxy class但是它也指定了一个 InvocationHandler 的实现类。
  6. 代理模式本质上的目的是为了增强现有代码的功能。
/**
 * 
 *  动态代理:
 *  特点:字节码随用随创建,随用随加载
 *  作用:不修改源码的基础上对方法增强
 *  分类:
 *      基于接口的动态代理
 *      基于子类的动态代理
 *  基于接口的动态代理:
 *      涉及的类Proxy
 *      提供者JDK官方
 *  如何创建代理对象:
 *      使用Proxy类中的newProxyInstance方法
 *  创建代理对象的要求:
 *      被代理类最少实现一个接口,如果没有则不能使用
 *  newProxyInstance方法的参数
 *      ClassLoader类加载器
 *          它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
 *      Class[]:字节码数组
 *          它是用于让代理对象和被代理对象有相同方法。固定写法。固定写接口
 *      InvocationHandler用于提供增强的代码
 *          它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
 *          此接口的实现类都是谁用谁写。
 */
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
                producer.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 作用:执行被代理对象的任何接口方法都会经过该方法
                     * 方法参数的含义
                     * @param proxy   代理对象的引用
                     * @param method  当前执行的方法
                     * @param args    当前执行方法所需的参数
                     * @return        和被代理对象方法有相同的返回值
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        //提供增强的代码
                        Object returnValue = null;

                        //1.获取方法执行的参数
                        Float money = (Float)args[0];
                        //2.判断当前方法是不是销售
                        if("saleProduct".equals(method.getName())) {
                            returnValue = method.invoke(producer, money*0.8f);
                        }
                        return returnValue;
                    }
                });
        proxyProducer.saleProduct(10000f);
    }

来源黑马Spring讲的动态代理部分

Cglib动态代理

public static void main(String[] args) {
    final Producer producer = new Producer();

    /**
     * 动态代理:
     *  特点:字节码随用随创建,随用随加载
     *  作用:不修改源码的基础上对方法增强
     *  分类:
     *      基于接口的动态代理
     *      基于子类的动态代理
     *  基于子类的动态代理:
     *      涉及的类Enhancer
     *      提供者第三方cglib库
     *  如何创建代理对象:
     *      使用Enhancer类中的create方法
     *  创建代理对象的要求:
     *      被代理类不能是最终类
     *  create方法的参数
     *      Class字节码
     *          它是用于指定被代理对象的字节码。
     *
     *      Callback用于提供增强的代码
     *          它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
     *          此接口的实现类都是谁用谁写。
     *          我们一般写的都是该接口的子接口实现类MethodInterceptor
     */
    Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
        /**
         * 执行被代理对象的任何方法都会经过该方法
         * @param proxy
         * @param method
         * @param args
         *    以上三个参数和基于接口的动态代理中invoke方法的参数是一样的
         * @param methodProxy :当前执行方法的代理对象
         * @return
         * @throws Throwable
         */
        @Override
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            //提供增强的代码
            Object returnValue = null;

            //1.获取方法执行的参数
            Float money = (Float)args[0];
            //2.判断当前方法是不是销售
            if("saleProduct".equals(method.getName())) {
                returnValue = method.invoke(producer, money*0.8f);
            }
            return returnValue;
        }
    });
    cglibProducer.saleProduct(12000f);
}

Comparable和Comparator有什么区别

https://www.cnblogs.com/starry-skys/p/12157141.html

  1. 它们出自不同的包Comparator在 java.util 包下Comparable在 java.lang 包下。
  2. Comparator 使用比较灵活,不需要修改实体类源码,但是需要实现一个比较器。
  3. Comparable 使用简单,但是对代码有侵入性,需要修改实体类源码。
  4. 都是接口

引用传递和值传递

1、JavaGuide-Java基础知识https://snailclimb.gitee.io/javaguide/#/docs/java/Java基础知识1.4.2

2、https://www.zhihu.com/question/31203609

  • 值传递( pass by value )是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

  • 引用传递( pass by reference )是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

JavaGuide里的第三个例子。如果是引用传递的话交换数据时(不是第二个例子里的赋值语句),应该是真正的交换内存地址,而不是交换引用。

装箱和拆箱

JavaGuide-Java基础知识https://snailclimb.gitee.io/javaguide/#/docs/java/Java基础知识 1.3.2

在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法。

static变量存储位置

笔者的JVM篇说的很详细

JDK7以上静态变量存储在其对应的Class对象中。而Class对象作为对象和其他普通对象一样都是存在java堆中的。

super()和this()不能同时在一个构造函数中出现

this()和this. 不一样

public class JZ_056 {
    private int num;

    private String str;

    public JZ_056() {
        System.out.println("调用无参构造");
    }

    public JZ_056(int num) {
        this();  //调用无参构造
        System.out.println("调用有参构造");
        this.num = num;
    }

    public JZ_056(int num, String str) {
        this(1);  //这个会调用只有一个参数的有参构造
        this.num = num;
        this.str = str;
    }

    public static void main(String[] args) {
        JZ_056 jz_056 = new JZ_056(2, "哈哈");
    }

}

由上面代码可以看出来this()方法可以调用本类的无参构造函数。如果本类继承的有父类那么无参构造函数会有一个隐式的super()的存在。所以在同一个构造函数里面如果this()之后再调用super()那么就相当于调用了两次super(),也就是调用了两次父类的无参构造,就失去了语句的意义,编译器也不会通过。

面向对象三大特征

https://www.cnblogs.com/wujing-hubei/p/6012105.html

1、多态是建立在继承的基础上的,是指子类类型的对象可以赋值给父类类型的引用变量,但运行时仍表现子类的行为特征。也就是说,同一种类型的对象执行同一个方法时可以表现出不同的行为特征。

泛型

笔者有篇泛型文章讲的很详细,包括泛型擦除这些都讲了

父子类相关的问题

https://www.nowcoder.com/questionTerminal/d31ea6176417421b9152d19e8bd1b689

https://www.nowcoder.com/profile/2164604/test/35226827/365890

http://www.mamicode.com/info-detail-2252140.html

https://developer.aliyun.com/article/653204

new出来的对象都是在堆上分配的吗

https://imlql.cn/post/50ac3a1c.html我在此篇文章的 堆是分配对象的唯一选择么?有讲解。

Java集合

这部分我会重新写新文章

ArrayList和LinkedList时间复杂度

头部插入由于ArrayList头部插入需要移动后面所有元素所以必然导致效率低。LinkedList不用移动后面元素自然会快一些。 中间插入查看源码会注意到LinkedList的中间插入其实是先判断插入位置距离头尾哪边更接近然后从近的一端遍历找到对应位置而ArrayList是需要将后半部分的数据复制重排所以两种方式其实都逃不过遍历的操作相对效率都很低但是从实验结果还是ArrayList更胜一筹我猜测这与数组在内存中是连续存储有关。 尾部插入ArrayList并不需要复制重排数据所以效率很高这也应该是我们日常写代码时的首选操作而LinkedList由于还需要new对象和变换指针所以效率反而低于ArrayList。

删除操作和添加操作没有什么区别。所以笼统的说LinkedList插入删除效率比ArrayList高是不对的有时候反而还低。之所以笼统的说LinkedList插入删除效率比ArrayList高我猜测是ArrayList复制数组需要时间也占一定的时间复杂度。而因为数据量太少这种效果就体现不出来。

https://blog.csdn.net/hollis_chuang/article/details/102480657

fail-fast和fail-safe

https://blog.csdn.net/zwwhnly/article/details/104987143

https://blog.csdn.net/Kato_op/article/details/80356618

https://blog.csdn.net/striner/article/details/86375684

LinkedHashMap

https://www.cnblogs.com/xiaowangbangzhu/p/10445574.html

https://blog.csdn.net/qq_28051453/article/details/71169801

如何保证插入数据的有序性?

1、在实现上LinkedHashMap 很多方法直接继承自 HashMap比如put remove方法就是直接用的父类的仅为维护双向链表覆写了部分方法get方法是重写的

2、重新定义了数组中保存的元素Entry继承于HashMap.node)该Entry除了保存当前对象的引用外还保存了其上一个元素before和下一个元素after的引用从而在哈希表的基础上又构成了双向链接列表。仍然保留hash拉链法的next属性所以既可像HashMap一样快速查找用next获取该链表下一个Entry。也可以通过双向链接通过after完成所有数据的有序迭代.

3、在这个构造方法中,有个accessOrder,它不同的值有不同的意义: 默认为false,即按插入时候的顺序进行迭代。设置为true后按访问时候的顺序进行迭代输出即链表的最后一个元素总是最近才访问的。

**访问的顺序:**如果有1 2 3这3个Entry那么访问了1就把1移到尾部去即2 3 1。每次访问都把访问的那个数据移到双向队列的尾部去那么每次要淘汰数据的时候双向队列最头的那个数据不就是最不常访问的那个数据了吗换句话说双向链表最头的那个数据就是要淘汰的数据。

4、链表节点的删除过程

与插入操作一样LinkedHashMap 删除操作相关的代码也是直接用父类的实现但是LinkHashMap 重写了removeNode()方法 afterNodeRemoval方法该removeNode方法在hashMap 删除的基础上有调用了afterNodeRemoval 回调方法。完成删除。

删除的过程并不复杂,上面这么多代码其实就做了三件事:

  1. 根据 hash 定位到桶位置
  2. 遍历链表或调用红黑树相关的删除方法
  3. 从 LinkedHashMap 维护的双链表中移除要删除的节点

红黑树

https://blog.csdn.net/qq_36610462/article/details/83277524

集合继承结构

Java并发

下面只补充遗漏的剩余的所有在我的Java并发系列文章都有

手写DCL并解释为什么是这样写

外层的if是为了让线程尽量少的进入synchronized块

内层的if则看下面的解释

public static Object getInstance() {
    if(instance == null) {//线程12到达这里
        synchronized(b) {//线程1到这里开始继续往下执行线程2等待
            if(instance == null) {//线程1到这里发现instance为空继续执行if代码块
             //执行完成后退出同步区域然后线程2进入同步代码块如果在这里不再加一次判断
             //就会造成instance再次实例化由于增加了判断
             //线程2到这里发现instance已被实例化于是就跳过了if代码块
               instance = new Object();
            }
        }
    }
 }

我们知道ArrayList是线程不安全,请编写一个不安全的案例并给出解决方案

/* 笔记
 * 1.只有一边写一边读的时候才会报java.util.ConcurrentModificationException
 *   单独测写的时候都没有报这个异常
 * */
public class Video20_01 {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        for (int i = 0; i < 20; i++) {
             new Thread(() ->{
                list.add(UUID.randomUUID().toString().substring(0,8)); //这个是写
                System.out.println(list);//这个是读
             },"线程" + String.valueOf(i)).start();
        }
    }
}
/**
 * @Author: 
 * @Date: 2019/9/25 8:45
 * <p>
 * 功能描述: 集合不安全问题的解决(写时复制)
 */

public class Video20_02 {

    public static void main(String[] args) {
//        List<String> list = new Vector<>();//不推荐
//        List<String> list = Collections.synchronizedList(new ArrayList<>());//不推荐
        List<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 20; i++) {
            new Thread(() ->{
                list.add(UUID.randomUUID().toString().substring(0,8)); //这个是写
                System.out.println(list);//这个是读
            },"线程" + String.valueOf(i)).start();
        }

        ConcurrentHashMap<Object, Object> test = new ConcurrentHashMap<>();
        test.put(1,1);
    }
}

ThreadLocal

简单原理

 /*	public class ThreadLocal<T> {}源码   */
	public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);  //①
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;  //②
    }
/*	public class ThreadLocal<T> {}源码   */



/*	public class Thread implements Runnable {}*/
	ThreadLocal.ThreadLocalMap threadLocals = null; 

1、由源码可以看到ThreadLocal是在set的时候才和线程建立关系。并且在get的时候通过获取线程来判断是哪一个线程。

2、同时每个Thread类都有一个ThreadLocal.ThreadLocalMap类的变量set方法里通过getMap获取这个变量然后在这个map里设置值所以才有了每一个线程都可以通过ThreadLocal设置专属变量的说法。

为什么Key要使用弱引用及内存泄漏原因

https://blog.csdn.net/puppylpg/article/details/80433271

JVM

这里我只截图下我准备的面试题目录因为所有答案你都能在笔者的JVM文章里找到答案

Redis

为什么使用Redis

用缓存,主要有两个用途:高性能高并发

高性能

假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作 mysql半天查出来一个结果耗时 600ms。但是这个结果可能接下来几个小时都不会变了或者变了也可以不用立即反馈给用户。那么此时咋办

缓存啊,折腾 600ms 查出来的结果,扔缓存里,一个 key 对应一个 value下次再有人查别走 mysql 折腾 600ms 了,直接从缓存里,通过一个 key 查出来一个 value2ms 搞定。性能提升 300 倍。

就是说对于一些需要复杂操作耗时查出来的结果,且确定后面不怎么变化,但是有很多读请求,那么直接将查询出来的结果放在缓存中,后面直接读缓存就好。

高并发

mysql 这么重的数据库压根儿设计不是让你玩儿高并发的虽然也可以玩儿但是天然支持不好。mysql 单机支撑到 2000QPS 也开始容易报警了。

所以要是你有个系统,高峰期一秒钟过来的请求有 1 万,那一个 mysql 单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放 mysql。缓存功能简单说白了就是 key-value 式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发 so easy。单机承载并发量是 mysql 单机的几十倍。

缓存是走内存的,内存天然就支撑高并发。

分布式缓存和本地缓存有啥区别

  1. 缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
  2. 使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或 memcached服务的高可用整个程序架构上较为复杂。

redis和Memecache的区别

  1. redis 支持更丰富的数据类型支持更复杂的应用场景Memcached 只支持最简单的 k/v 数据类型
  2. Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memecache 把数据全部存在内存之中。
  3. redis 目前是原生支持 cluster 模式的而Memecache原生不支持集群。
  4. Memcached 是多线程,非阻塞 IO 复用的网络模型Redis 使用单线程的多路 IO 复用模型。
  5. Redis 支持发布订阅模型、Lua脚本、事务等功能而Memcached不支持。并且Redis支持更多的编程语言。

redis常用数据结构和使用场景

字符串String

使用场景:

  • 缓存,用于支持高并发
  • 计数器,视频播放数
  • 限速,处于安全考虑,每次进行登录时让用户输入手机验证码,为了短信接口不被频繁访问,会限制用户每分钟获取验证码的频率。就是设置过期时间

列表list

使用场景:

  • 文章列表,每个用户都有属于自己的文章列表,现在需要分页展示文章列表,此时可以考虑使用列表,列表不但有序,同时支持按照索引范围获取元素。(lrange命令)
  • 消息队列,使用列表技巧:
    • lpush+lpop=Stack(栈)
    • lpush+rpop=Queue队列
    • lpush+ltrim=Capped Collection有限集合
    • lpush+brpop=Message Queue消息队列
  • 时间轴

字典hash

使用场景:

  • 记录帖子的点赞数、评论数和点击数。帖子的标题、摘要、作者和封面信息,用于列表页展示
  • 用户信息管理key是用户标识value是用户信息。hash 特别适合用于存储对象

集合set

使用场景:

  • 标签tag集合类型比较典型的使用场景如一个用户对娱乐、体育比较感兴趣另一个可能对新闻感兴 趣,这些兴趣就是标签,有了这些数据就可以得到同一标签的人,以及用户的共同爱好的标签。

有序集合zset

  • 排行榜,记录热榜帖子 ID 列表,总热榜和分类热榜

HyperLogLog

统计网页的uv(独立访客,每个用户每天只记录一次)如果用SET的话空间耗费会很大

pv(浏览量,用户每点一次记录一次)可以用String来统计

Zset底层实现跳表搜索插入删除过程

Zset底层实现

跳跃表

跳表搜索插入删除过程?

搜索过程:

假设我们需要查找的值为k(score)

  1. 需要从 header 的当前最高层maxLevel开始遍历直到找到 最后一个比k小的节点
  2. 然后从这个节点开始降一层再遍历找到第二个节点 (最后一个比k小的节点)
  3. 然后以此类推一直降到最底层进行遍历就找到了期望的节点 。

注意:

  1. 不是和当前元素比较,是和当前元素的下一元素比较,所以才能知道(最后一个比k小的元素)
  2. redis跳跃表的排序是首先比较score,如果score相等还需要比较value

插入过程:

  1. 首先是有一个搜索确定位置的过程,逐步降级寻找目标节点,得到「搜索路径」

  2. 然后才开始插入

    2-1. 为每个节点随机出一个层数(level)

    2-2. 创建新节点,再将搜索路径上的节点和这个新节点通过前向后向指针串起来

    2-3. 如果分配的新节点的高度高于当前跳跃列表的最大高度,更新一下跳跃列表的最大高度,并且填充跨度

删除过程:

删除过程和插入过程类似,都需先把这个「搜索路径」找出来。然后对于每个层的相关节点都重排一下前向后向指针就可以了。同时还要注意更新一下最高层数maxLevel

更新过程

  1. 当我们调用 ZADD 方法时,如果对应的 value 不存在,那就是插入过程,如果这个 value 已经存在,只是调整一下 score 的值,那就需要走一个更新流程。
  2. 假设这个新的 score 值并不会带来排序上的变化,那么就不需要调整位置,直接修改元素的 score 值就可以了,
  3. 但是如果排序位置改变了那就需要调整位置。Redis 采用了一个非常简单的策略,把这个元素删除再插入这个需要经过两次路径搜索从这一点上来看Redis 的 ZADD 代码似乎还有进一步优化的空间。

元素排名的实现

  1. 跳跃表本身是有序的Redis 在 skiplist 的 forward 指针上进行了优化,给每一个 forward 指针都增加了 跨度span 属性,用来 记录了前进指针所指向节点和当前节点的距离(也就是跨过了几个节点)。在源码中我们也可以看到 Redis 在插入、删除操作时都会小心翼翼地更新 span 值的大小。
  2. 所以,沿着 "搜索路径",把所有经过节点的跨度 span 值进行累加就可以算出当前元素的最终 rank 值了。

redis过期淘汰策略

这个讲得好:https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-expiration-policies-and-lru

补充:

  1. volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  2. allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

lru和lfu区别

  • LRU最近最少使用淘汰算法Least Recently Used。LRU是淘汰一段时间内没有被使用的页面。

  • LFU最不经常使用淘汰算法Least Frequently Used。LFU是淘汰一段时间内使用次数最少的页面。

redis持久化机制都有什么优缺点持久化的时候还能接受请求吗

参考:

Redis 的数据 全部存储内存 中,如果 突然宕机,数据就会全部丢失,因此必须有一套机制来保证 Redis 的数据不会因为故障而丢失,这种机制就是 Redis 的 持久化机制,它会将内存中的数据库状态 保存到磁盘 中。

下面是总结

RDB快照

  1. Redis 快照 是最简单的 Redis 持久性模式。当满足特定条件时它将生成数据集的时间点快照例如如果先前的快照是在2分钟前创建的并且现在已经至少有 100 次新写入,则将创建一个新的快照。此条件可以由用户配置 Redis 实例来控制【JavaGuide里有如何配置】。快照是一次全量备份

  2. 但我们知道Redis 是一个 单线程 的程序,这意味着,我们不仅仅要响应用户的请求,还需要进行内存快照。而后者要求 Redis 必须进行 IO 操作,这会严重拖累服务器的性能。

  3. 还有一个重要的问题是,我们在 持久化的同时内存数据结构 还可能在 变化,比如一个大型的 hash 字典正在持久化,结果一个请求过来把它删除了,还没持久化完呢。怎么办呢

  4. 操作系统多进程 COW(Copy On Write) 机制可以解决上述问题

    4-1.Redis 在持久化时会调用 glibc 的函数 fork 产生一个子进程,简单理解也就是基于当前进程 复制 了一个进程,主进程和子进程会共享内存里面的代码块和数据段

    4-2.所以 快照持久化 可以完全交给 子进程 来处理,父进程 则继续 处理客户端请求子进程 做数据持久化,它 不会修改现有的内存数据结构,它只是对数据结构进行遍历读取,然后序列化写到磁盘中。但是 父进程 不一样,它必须持续服务客户端请求,然后对 内存数据结构进行不间断的修改

    4-3.这个时候就会使用操作系统的 COW 机制来进行 数据段页面 的分离。数据段是由很多操作系统的页面组合而成,当父进程对其中一个页面的数据进行修改时,会将被共享的页面复 制一份分离出来,然后 对这个复制的页面进行修改。这时 子进程 相应的页面是 没有变化的,还是进程产生时那一瞬间的数据。

    4-4.子进程因为数据没有变化,它能看到的内存里的数据在进程产生的一瞬间就凝固了,再也不会改变,这也是为什么 Redis 的持久化 叫「快照」的原因。接下来子进程就可以非常安心的遍历数据了进行序列化写磁盘了。

AOF

AOF原理

  1. AOF(Append Only File - 仅追加文件) 它的工作方式非常简单:每次执行 修改内存 中数据集的写操作时,都会 记录 该操作。假设 AOF 日志记录了自 Redis 实例创建以来 所有的修改性指令序列,那么就可以通过对一个空的 Redis 实例 顺序执行所有的指令,也就是 「重放」,来恢复 Redis 当前实例的内存数据结构的状态。
  2. 当 Redis 收到客户端修改指令后,会先进行参数校验、然后直接执行指令,如果没问题,就 立即 将该指令文本 存储 到 AOF 日志中,也就是说,先执行指令再将日志存盘。这一点不同于 MySQLLevelDBHBase 等存储引擎,如果我们先存储日志再做逻辑处理,这样就可以保证即使宕机了,我们仍然可以通过之前保存的日志恢复到之前的数据状态,但是 Redis 为什么没有这么做呢?
  3. 引用一条来自知乎上的回答我甚至觉得没有什么特别的原因。仅仅是因为由于AOF文件会比较大为了避免写入无效指令错误指令必须先做指令检查如何检查只能先执行了。因为语法级别检查并不能保证指令的有效性比如删除一个不存在的key。而MySQL这种是因为它本身就维护了所有的表的信息所以可以语法检查后过滤掉大部分无效指令直接记录日志然后再执行。

AOF重写

  1. Redis 在长期运行的过程中AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦身。
  2. AOF重写可以产生一个新的AOF文件这个新的AOF文件和原有的AOF文件所保存的数据库状态一样但体积更小。
  3. Redis 提供了 bgrewriteaof 指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了,瘦身工作就完成了。

fsync

  1. AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时实际上是将内容写到了内核为文件描述符分配的一个内存缓存中然后内核会异步将这些数据刷回到磁盘的。这就意味着如果机器突然宕机AOF 日志内容可能还没有来得及完全刷到磁盘中,这个时候就会出现日志丢失。那该怎么办?
  2. Linux 的glibc提供了fsync(int fd)函数可以将指定文件的内容强制从内核缓存刷到磁盘。只要 Redis 进程实时调用 fsync 函数就可以保证 aof 日志不丢失。但是 fsync 是一个磁盘 IO 操作,它很慢!
  3. 所以在生产环境的服务器中Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能使得数据少丢失。

Redis4.0混合持久化

  1. 重启 Redis 时,我们很少使用 rdb 来恢复内存状态,因为会丢失大量数据。我们通常使用 AOF 日志重放,但是重放 AOF 日志性能相对 rdb 来说要慢很多,这样在 Redis 实例很大的情况下,启动需要花费很长的时间。
  2. Redis 4.0 为了解决这个问题,带来了一个新的持久化选项——混合持久化。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是自持久化开始到持久化结束的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
  3. 于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重启效率因此大幅得到提升。

redis事务

https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5afc3747f265da0b71567686

事务简介

  1. 每个事务的操作都有 begin、commit 和 rollbackbegin 指示事务的开始commit 指示事务的提交rollback 指示事务的回滚。Redis 在形式上看起来也差不多,分别是 multi/exec/discard。multi 指示事务的开始exec 指示事务的执行discard 指示事务的丢弃。
  2. 所有的指令在 exec 之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到 exec 指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。因为 Redis 的单线程特性,它不用担心自己在执行队列的时候被其它指令打搅,可以保证他们能得到的「原子性」执行。但是 Redis 的事务根本不能算「原子性」,而仅仅是满足了事务的「隔离性」,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利

为什么 Redis 的事务不能支持回滚?

  1. redis是先执行指令然后记录日志如果执行失败日志也不会记录也就不能回滚了

Redis 是单线程命令是按顺序执行无并发已经有Multi 和 Exec了为什么还需要Watch?

  1. redis事务在执行时是单线程运行的。但是在执行前有可能别的客户端已经修改了事务里执行的key。所以在multi事务开始之前用watch检测这个key避免被其他客户端改变的。如果这个key被改变 了 exec的时候就会报错 不执行这个事务。

  2. 这里的“other client” 并不一定是另外一个客户端watch操作执行之后multi之外任何操作都可以认为是other clinet在操作即使仍然是在同一个客户端上操作exec该事务也仍旧会失败。

    redis使用watch实现cas具体示例https://www.jianshu.com/p/0244a875aa26

  3. 分布式锁是悲观锁redis的watch机制是乐观锁。悲观锁的意思就是我不允许你修改。乐观锁的意思就是你修改了之后要告诉我我让我的操作失败。

redis是单线程还是多线程为什么那么快

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

  • 纯内存操作。
  • 核心是基于非阻塞的 IO 多路复用机制。
  • C 语言实现一般来说C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。
  • 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。

redis的线程模型简介及一次通信过程图解(问的概率小)

https://doocs.github.io/advanced-java/#/./docs/high-concurrency/redis-single-thread-model

几种IO模型

我写过这篇文章,可以翻一下

select、poll、epoll的区别

select, poll, epoll 都是I/O多路复用的具体的实现之所以有这三个存在其实是他们出现是有先后顺序的。

1.select

  1. 它仅仅知道了有I/O事件发生了却并不知道是哪那几个流可能有一个多个甚至全部只能无差别轮询所有流找出能读出数据或者写入数据的流对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
  2. 单个进程可监视的fd_set(监听的端口个数)数量被限制32位机默认是1024个64位机默认是2048。但可通过修改宏定义或编译内核修改句柄数量

2.poll

poll本质上和select没有区别采用链表的方式替换原有fd_set数据结构,而使其没有连接数的限制

3.epoll

  1. epoll可以理解为event poll不同于忙轮询和无差别轮询epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动每个事件关联上fd此时我们对这些流的操作都是有意义的。复杂度降低到了O(1)
  2. 效率提升不是轮询的方式不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数。即Epoll最大的优点就在于它只管你“活跃”的连接而跟连接总数无关因此在实际的网络环境中Epoll的效率就会远远高于select和poll。
  3. epoll通过内核和用户空间共享一块内存来实现的。select和poll都是内核需要将消息传递到用户空间都需要内核拷贝动作
  4. epoll有EPOLLLT和EPOLLET两种触发模式也就是水平触发和边沿触发两种模式。(暂时不去记,有个印象,大致是什么样就可以)

redis集群数据分布方式有什么优点一致性hash呢

详细看这里:https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-cluster

分布式寻址有哪几种?

  • hash 算法(大量缓存重建)
  • 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
  • redis cluster 的 hash slot 算法

redis集群数据分布方式

redis cluster采用hash slot。(中文就是hash槽)

优点:

  1. 任何一台机器宕机,其它节点,不影响的。因为 key 找的是 hash slot不是机器。

一致性hash

思想上就是一个环key取hash然后顺时针找第一个节点

优点:

  1. 如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点之间的数据,其它不受影响。增加一个节点也同理。

缺点:

  1. 一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布负载均衡。

为什么使用跳跃表,不用平衡树,hash表

  • skiplist和各种平衡树如AVL、红黑树等的元素是有序排列的而哈希表不是有序的。因此在哈希表上只能做单个key的查找不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
  • 在做范围查找的时候平衡树比skiplist操作要复杂。在平衡树上我们找到指定范围的小值之后还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单只需要在找到小值之后对第1层链表进行若干步的遍历就可以实现。
  • 平衡树的插入和删除操作可能引发子树的调整逻辑复杂而skiplist的插入和删除只需要修改相邻节点的指针操作简单又快速。
  • 从内存占用上来说skiplist比平衡树更灵活一些。一般来说平衡树每个节点包含2个指针分别指向左右子树而skiplist每个节点包含的指针数目平均为1/(1-p)具体取决于参数p的大小。如果像Redis里的实现一样取p=1/4那么平均每个节点包含1.33个指针,比平衡树更有优势。
  • 查找单个keyskiplist和平衡树的时间复杂度都为O(log n)大体相当而哈希表在保持较低的哈希值冲突概率的前提下查找时间复杂度接近O(1)性能更高一些。所以我们平常使用的各种Map或dictionary结构大都是基于哈希表实现的。
  • 从算法实现难度上来比较skiplist比平衡树要简单得多

HyperLogLog(问的概率小)

参考:

布隆过滤器

整体参考:

原理

原理:https://juejin.im/book/5afc2e5f6fb9a07a9b362527/section/5b33657cf265da597b0f99ab

看上面那篇文章的-布隆过滤器的原理-这部分

使用场景

  • 大数据判断是否存在:这就可以实现出上述的去重功能,如果你的服务器内存足够大的话,那么使用 HashMap 可能是一个不错的解决方案,理论上时间复杂度可以达到 O(1 的级别,但是当数据量起来之后,还是只能考虑布隆过滤器。
  • 解决缓存穿透:我们经常会把一些热点数据放在 Redis 中当作缓存,例如产品详情。 通常一个请求过来之后我们会先查询缓存,而不用直接读取数据库,这是提升性能最简单也是最普遍的做法,但是 如果一直请求一个不存在的缓存,那么此时一定不存在缓存,那就会有大量请求直接打到数据库 上,造成 缓存穿透,布隆过滤器也可以用来解决此类问题。
  • 爬虫/ 邮箱等系统的过滤:平时不知道你有没有注意到有一些正常的邮件也会被放进垃圾邮件目录中,这就是使用布隆过滤器误判 导致的。
  • 推荐系统去重:比如抖音的推荐系统去重,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉那些已经看过的内容。通过布隆过滤器判断是否已经看过的重复内容

注意事项

  1. 当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。
  2. 使用时不要让实际元素远大于初始化大小,当实际元素开始超出初始化大小时,应该对布隆过滤器进行重建,重新分配一个 size 更大的过滤器,再将所有的历史元素批量 add 进去 (这就要求我们在其它的存储器中记录所有的历史元素)。因为 error_rate 不会因为数量超出就急剧增加,这就给我们重建过滤器提供了较为宽松的时间。

大致空间占用

  1. 当一个元素平均需要 1 个字节 (8bit) 的指纹空间时 (l/n=8),错误率大约为 2%
  2. 错误率为 10%,一个元素需要的平均指纹空间为 4.792 个 bit大约为 5bit
  3. 错误率为 1%,一个元素需要的平均指纹空间为 9.585 个 bit大约为 10bit
  4. 错误率为 0.1%,一个元素需要的平均指纹空间为 14.377 个 bit大约为 15bi

如何保证 redis 的高并发和高可用?(问的概率小)

  1. redis 实现高并发主要依靠主从架构,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万 QPS多从用来查询数据多个从实例可以提供每秒 10w 的 QPS。如果想要在实现高并发的同时容纳大量的数据那么就需要 redis 集群,使用 redis 集群之后,可以提供每秒几十万的读写并发。
  2. redis 高可用,如果是做主从架构部署,那么加上哨兵就可以了,就可以实现,任何一个实例宕机,可以进行主备切换。

redis主从架构(问的概率小)

https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-master-slave

如果问到的话,答个大概其实就可以了

redis哨兵机制(问的概率小)

https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-sentinel

如果问到的话,答个大概其实就可以了

redis集群也就是redis cluster问的概率小

https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-cluster

注意redis cluster和redis replication redis哨兵的关系

redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。

高并发环境使用缓冲会出现什么问题?

缓存穿透

是什么:

缓存穿透

  1. 是指查询一个一定不存在的数据由于缓存是不命中将去查询数据库但是数据库也无此记录这将导致这个不存在的数据每次请求都要到存储层去查询失去了缓存的意义。在流量大时可能DB就挂掉了要是有人利用不存在的key频繁攻击我们的应用这就是漏洞。
  2. 举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。

解决:

  1. 会在接口层增加校验比如用户鉴权校验参数做校验不合法的参数直接代码Return比如id 做基础校验id <=0的直接拦截等。

  2. 缓存空但它的过期时间会很短最长不超过五分钟。为了防止缓存穿透将null或者空字符串值设置给redis。比如

    set -999 UNKNOWN ,set -1 null

  3. 布隆过滤器

比较新的解决办法:布隆过滤器

1、Redis还有一个高级用法**布隆过滤器Bloom Filter**这个也能很好的防止缓存穿透的发生他的原理也很简单就是利用高效的数据结构和算法快速判断出你这个Key是否在数据库中存在不存在你return就好了存在你就去查了DB刷新KV再return。

2、请注意用 redis 也可以做到判断 key 对应的value 在数据库中存不在那就是把数据库里的所有value对应的key都储存在redis 中,而value可以为空然后判断下key.IsExists()就可以了但是这无疑会浪费大量空间因为存储了数据库中所有的key。而且这也不符合缓存的初衷咱不能暴力的把所有key都存下来而是查询了啥key我们缓存啥key。而布隆过滤器是一种非常高效的数据结构把所有数据库的value对应的key 存储到布隆过滤器里,几乎不消耗什么空间,而且查询也是相当的快!但是请注意,它只能判断 key 是否存在而且会有一定的误差。所以一个查询先通过布隆顾虑器判断key是否存在(key 对应的value是否存在数据库中),如果不存在直接返回空就好了。

缓存雪崩

是什么:

缓存雪崩

  1. 是指在我们设置缓存时采用了相同的过期时间导致缓存在某一时刻同时失效请求全部转发到DBDB瞬时压力过重雪崩。
  2. 所有缓存机器意外发生了全盘宕机。缓存挂了,此时 请求全部落数据库,数据库必然扛不住。

解决:

  1. 原有的失效时间基础上增加一个随机值比如1-5分钟随机这样每一个缓存的过期时间的重复率就会降低就很难引发集体失效的事件。
  2. 事前redis 高可用,主从+哨兵redis cluster避免全盘崩溃。
  3. 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
  4. 事后redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

缓存击穿

是什么:

对于一些设置了过期时间的key如果这些key可能会在某些时间点被超高并发地访问是一种非常“热点”的数据。这个时候需要考虑一个问题如果这个key在大量请求同时进来前正好失效那么所有对这个key的数据查询都落到db我们称为缓存击穿。

解决:

不同场景下的解决方式可如下:

  • 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。
  • 若缓存的数据更新不频繁,且缓存更新的整个流程耗时较少的情况下,则可以采用基于 redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。
  • 若缓存的数据更新频繁或者缓存更新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动的重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。

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

https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/redis-consistence

讲的蛮好

LRU和LFU

https://www.cnblogs.com/sddai/p/9739900.html

1、LRU和LFU都是内存管理的页面置换算法。

2、LRU最近最少使用淘汰算法Least Recently Used。LRU是淘汰最长时间没有被使用的页面。

3、LFU最不经常使用淘汰算法Least Frequently Used。LFU是淘汰一段时间内使用次数最少的页面。

4、LRU的意思是只要在最近用了一次这个页面这个页面就可以不用被淘汰。LFU的意思是即使我最近用了某个页面但是这个页面在一段时间内使用次数还是最少的话我还是要淘汰它相当于LFU说的是使用频率。

参考

下面是参考的比较多的,参考的比较少的直接在文中写明了

Mysql

Mysql这里特别强调一下下面的内容其实我看了一些书所以下面的参考链接是不完全的只是答案版本。Mysql的部分详细文章也在我的计划中

事务4大特性一致性具体指什么这4个特性mysql如何保证实现的

事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。

https://www.zhihu.com/question/31346392

ACID(四大特性)

  1. 原子性Atomicity 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性Consistency 是指事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。保证数据库一致性是指当事务完成时必须使所有数据都具有一致的状态【Mysql是怎样运行的如果数据库中的数据全部符合现实世界中的约束all defined rules我们说这些数据就是一致的或者说符合一致性的。】
  3. 隔离性Isolation 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间是独立的;
  4. 持久性Durability 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失

这4个特性mysql如何保证实现的

总结一下:

  • 保证一致性:

    • 从数据库层面数据库通过原子性、隔离性、持久性来保证一致性。也就是说ACID四大特性之中C(一致性)是目的A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段
    • 从应用层面,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据!
  • 保证原子性是利用Innodb的undo logundo log名为回滚日志是实现原子性的关键。undo log记录了这些回滚需要的信息当事务执行失败或调用了rollback导致事务需要回滚便可以利用undo log中的信息将数据回滚到修改之前的样子。

  • 保证持久性是利用Innodb的redo log。当做数据修改的时候不仅在内存中操作还会在redo log中记录这次操作。当事务提交的时候会将redo log日志进行刷盘。当数据库宕机重启的时候会将redo log中的内容恢复到数据库中再根据undo log和binlog内容决定回滚数据还是提交数据

    • 采用redo log的好处其实好处就是将redo log进行刷盘比对数据页刷盘效率高具体表现如下
      • redo log体积小毕竟只记录了哪一页修改了啥因此体积小刷盘快。
      • redo log是一直往末尾进行追加属于顺序IO。效率显然比随机IO来的快。
  • 保证隔离性利用的是锁和MVCC机制

事务隔离级别4个隔离级别分别有什么并发问题

  • READ-UNCOMMITTED(读取未提交) 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
  • READ-COMMITTED(读取已提交) 只允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
  • REPEATABLE-READ(可重复读) 在同一个事务内对同一数据的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • SERIALIZABLE(可串行化) 最高的隔离级别完全服从ACID的隔离级别。所有的事务依次逐个执行这样事务之间就完全不可能产生干扰也就是说该级别可以防止脏读、不可重复读以及幻读。

脏写(Dirty Write:如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了脏写

脏读(Drity Read):如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读【某个事务已更新一份数据另一个事务在此时读取了同一份数据由于某些原因前一个RollBack了操作则后一个事务所读取的数据就会是不正确的。】 不可重复读(Non-repeatable read):如果一个事务能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值。【强调的是每次都能读到最新数据】 幻读(Phantom Read):如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读【那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢?其实这相当于对每一条记录都发生了不可重复读的现象。幻读只是重点强调了读取到了之前读取没有获取到的记录。】

具体讲解:https://blog.csdn.net/qq_35433593/article/details/86094028

Mysql默认隔离级别如何保证并发安全

  • Mysql 默认采用的 REPEATABLE_READ隔离级别
  • 并发安全的实现基于锁机制和并发调度。其中并发调度使用的是MVVC多版本并发控制通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。

隔离级别的单位是数据表还是数据行?如串行化级别,两个事务访问不同的数据行,能并发?

  • READ-UNCOMMITTED(读取未提交)不加锁
  • READ-COMMITTED(读取已提交) 行锁
  • REPEATABLE-READ(可重复读) 行锁
  • SERIALIZABLE(可串行化) 表锁

不能,串行化就直接把表锁住了,无法并发。

存储引擎Innodb和Myisam的区别以及使用场景

介绍Inodb锁机制行锁表锁意向锁

https://blog.csdn.net/Waves___/article/details/105295060

介绍MVCC.

  1. 多版本控制(MVCC): 指的是一种提高并发的技术。最早的数据库系统只有读读之间可以并发读写写读写写都要阻塞。引入多版本之后只有写写之间相互阻塞其他三种操作都可以并行这样大幅度提高了InnoDB的并发度。在内部实现中InnoDB通过undo log保存每条数据的多个版本并且能够找回数据历史版本提供给用户读每个事务读到的数据版本可能是不一样的。在同一个事务中用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。

  2. MVCC只在 Read Committed 和 Repeatable Read两个隔离级别下工作。其他两个隔离级别和MVCC不兼容Read Uncommitted总是读取最新的记录行而不是符合当前事务版本的记录行Serializable 则会对所有读取的记录行都加锁。

  3. MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 "行级锁+MVCC"一起实现的,正常读的时候不加锁,写的时候加锁。而 MCVV 的实现依赖隐藏字段、Read View、Undo log

具体看这里:https://blog.csdn.net/Waves___/article/details/105295060

小总结一下

①每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表。所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id

②ReadView需要判断一下版本链中的哪个版本是当前事务可见的。

  • m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
  • min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
  • max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
  • creator_trx_id:表示生成该ReadView的事务的事务id

③有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

  • 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的trx_id属性值在ReadViewmin_trx_idmax_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

哈希索引是如何实现的?

对于哈希索引来说,底层的数据结构就是哈希表。对于哈希索引,InnoDB是自适应哈希索引hash索引的创建由InnoDB存储引擎引擎自动优化创建我们干预不了

  • 哈希索引也没办法利用索引完成排序
  • 不支持最左匹配原则
  • 在有大量重复键值情况下,哈希索引的效率也是极低的---->哈希碰撞问题。
  • 不支持范围查询

B树索引为什么使用B+树相对于B树有什么优点为什么不能红黑树要提到磁盘预读

B+树比B树有什么优点

1、B+树只有叶节点存放数据其余节点用来存索引而B-树是每个索引节点都会有Data域。B+树单一节点存储更多的数据使得查询的IO次数更少。

2、所有查询都要查找到叶子节点查询性能稳定。

3、B+树所有数据都在叶子节点,所有叶子节点形成有序链表,便于范围查询

为什么不能用红黑树

1.在大规模数据存储的时候平衡二叉树往往出现由于树的深度过大而造成磁盘IO读写过于频繁进而导致效率低下的情况

2.数据库系统的设计者巧妙利用了磁盘预读原理将一个节点的大小设为等于一个页这样每个节点只需要一次I/O就可以完全载入。

这个网址有答案:http://www.coder55.com/question/139

什么是磁盘预读

1、由于存储介质的特性磁盘本身存取就比主存慢很多再加上机械运动耗费磁盘的存取速度往往是主存的几百分分之一因此为了提高效率要尽量减少磁 盘I/O。为了达到这个目的磁盘往往不是严格按需读取而是每次都会预读即使只需要一个字节磁盘也会从这个位置开始顺序向后读取一定长度的数据放 入内存。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。

https://www.cnblogs.com/leezhxing/p/4420988.html

聚簇索引和非聚簇索引区别

1.聚簇索引

  1. 使用记录主键值的大小进行记录和页的排序,这包括三个方面的含义:

    • 页内的记录是按照主键的大小顺序排成一个单向链表。
    • 各个存放用户记录的页也是根据页中用户记录的主键大小顺序排成一个双向链表。
    • 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的主键大小顺序排成一个双向链表。
  2. B+树的叶子节点存储的是完整的用户记录。

    所谓完整的用户记录,就是指这个记录中存储了所有列的值(包括隐藏列)。

2.非聚簇索引

也叫二级索引(使用唯一索引或普通索引)

  • 使用记录c2列的大小进行记录和页的排序,这包括三个方面的含义:
    • 页内的记录是按照c2列的大小顺序排成一个单向链表。
    • 各个存放用户记录的页也是根据页中记录的c2列大小顺序排成一个双向链表。
    • 存放目录项记录的页分为不同的层次,在同一层次中的页也是根据页中目录项记录的c2列大小顺序排成一个双向链表。
  • B+树的叶子节点存储的并不是完整的用户记录,而只是c2列+主键这两个列的值。
  • 目录项记录中不再是主键+页号的搭配,而变成了c2列+页号的搭配。

如果用C2和C3列共同建二级索引这个二级索引也叫做联合索引。

3.区别

  • 聚集索引在叶子节点存储的是表中的数据
  • 非聚集索引在叶子节点存储的是主键和索引列
  • 使用非聚集索引查询出数据时,拿到叶子上的主键再去查到想要查找的数据。(拿到主键再查找这个过程叫做回表)

回表查询和覆盖索引

1.回表查询

使用非聚集索引查询出数据时,拿到叶子上的主键再去查到想要查找的数据。(拿到主键再查找这个过程叫做回表)

2.覆盖索引

  • 我们前面知道了,如果不是聚集索引,叶子节点存储的是主键+索引列值
  • 最终还是要“回表”,也就是要通过主键查找一次。这样就会比较慢
  • 覆盖索引就是把要查询出的列和索引是对应的,不做回表操作!

13、如何创建索引

14、如何使用索引避免全表扫描

15、Explain语句各字段的意义

最左前缀!!

1.最左前缀

联合索引B+树是如何建立的是如何查询的当where子句中出现>时,联合索引命中是如何的? 如 where a > 10 and b = “111”时联合索引如何创建mysql优化器会针对得做出优化吗

如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列。遇到范围查询(>、<、between、like左匹配)等就不能进一步匹配了,后续退化为线性查找。

MySQL中一条SQL语句的执行过程(未总结)

剩余相关知识看JavaGuide

https://snailclimb.gitee.io/javaguide/#/docs/database/一条sql语句在mysql中如何执行的?id=%e4%b8%89-%e6%80%bb%e7%bb%93

查询语句

select * from tb_student  A where A.age='18' and A.name=' 张三 ';
  • 先用连接器检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 sql 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。

  • 通过分析器进行词法分析,提取 sql 语句的关键元素,比如提取上面这个语句是查询 select提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id='1'。然后判断这个 sql 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。

  • 接下来就是优化器进行确定执行方案,上面的 sql 语句,可以有两种执行方案:

      a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。
      b.先找出学生中年龄 18 岁的学生然后再查询姓名为“张三”的学生。Copy to clipboardErrorCopied
    

    那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。

  • 执行器执行并返回引擎的执行结果【进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。】

SQL Select语句完整的执行顺序[从DBMS使用者角度] :

  1. from子句组装来自不同数据源的数据;

  2. where子句基于指定的条件对记录行进行筛选;

  3. group by子句将数据划分为多个分组;

  4. 使用聚集函数进行计算;

  5. 使用having子句筛选分组;

  6. 计算所有的表达式;

  7. 使用order by对结果集进行排序。

SQL Select语句的执行步骤[从DBMS实现者角度,这个对我们用户意义不大] :

1)语法分析,分析语句的语法是否符合规范,衡量语句中各表达式的意义。

2)语义分析, 检查语句中涉及的所有数据库对象是否存在,且用户有相应的权限。

3)视图转换,将涉及视图的查询语句转换为相应的对基表查询语句。

4)表达式转换将复杂的SQL表达式转换为较简单的等效连接表达式。

5)选择优化器,不同的优化器一般产生不同的“ 执行计划"

6)选择连接方式ORACLE 有三种连接方式对多表连接ORACLE可选择适当的连接方式。

7)选择连接顺序对多表连接ORACLE选择哪-对表先连接,选择这两表中哪个表做为源数据表。

8)选择数据的搜索路径,根据以上条件选择合适的数据搜索路径,如是选用全表搜索还是利用索引或是其他的方式。9)运行"执行计划”。

更新语句

update tb_student A set A.age='19' where A.name=' 张三 ';

其实更新语句也基本上会沿着上一个查询的流程走只不过执行更新的时候肯定要记录日志啦这就会引入日志模块了MySQL 自带的日志模块式 binlog归档日志 ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 redo log重做日志,我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下:

  • 先查询到张三这一条数据,如果有缓存,也是会用到缓存。
  • 然后拿到查询的语句,把 age 改为 19然后调用引擎 API 接口写入这一行数据InnoDB 引擎把数据保存在内存中,同时记录 redo log此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。
  • 执行器收到通知后记录 binlog然后调用引擎接口提交 redo log 为提交状态。
  • 更新完成。

既然增加树的路数可以降低树的高度,那么无限增加树的路数是不是可以有最优的查找效率?

这样会形成一个有序数组文件系统和数据库的索引都是存在硬盘上的并且如果数据量大的话不一定能一次性加载到内存中。有序数组没法一次性加载进内存会进行很多次IO效率又会降下来。这时候B+树的多路存储威力就出来了可以每次加载B+树的一个结点,然后一步步往下找

当前读和快照读

https://www.jianshu.com/p/785cef383ed6

MySQL中IS NULL、IS NOT NULL、!=可以用索引吗?

我们分别分析了拥有IS NULLIS NOT NULL!=这三个条件的查询是在什么情况下使用二级索引来执行的,核心结论就是:成本决定执行计划,跟使用什么查询条件并没有什么关系。优化器会首先针对可能使用到的二级索引划分几个范围区间,然后分别调查这些区间内有多少条记录,在这些范围区间内的二级索引记录的总和占总共的记录数量的比例达到某个值时,优化器将放弃使用二级索引执行查询,转而采用全表扫描。

为什么MyISAM会比Innodb的查询速度快?

INNODB在做SELECT的时候要维护的东西比MYISAM引擎多很多: 1数据块INNODB要缓存MYISAM只缓存索引块 这中间还有换进换出的减少;

2innodb寻址要映射到块再到行MYISAM记录的直接是文件的OFFSET定位比INNODB要快

3INNODB还需要维护MVCC一致虽然你的场景没有但他还是需要去检查和维护 MVCC (Multi-Version Concurrency Control)多版本并发控制

limit分页查询相关问题

limit的用法

https://www.cnblogs.com/cai170221/p/7122289.html

limit会对前面的数据进行IO吗

https://blog.csdn.net/qq_34208844/article/details/104043486

分页查找如果要查找很靠后的页面如何比如100万之后查10条怎么优化

https://blog.csdn.net/weixin_43066287/article/details/90024600

EXPLAIN

这里我们只关注三种分别是typekeyrows

type

代表着MySQL对某个表的执行查询时的访问类型

常见的几种:

all<index<range<ref<eq_ref<const。

const根据主键或者唯一二级索引列与常数进行等值匹配时

ref当通过普通的二级索引列与常量进行等值匹配时来查询某个表

possible_keys和key

EXPLAIN语句输出的执行计划中,possible_keys列表示在某个查询语句中,对某个表执行单表查询时可能用到的索引有哪些,key列表示实际用到的索引有哪些,比方说下边这个查询:

mysql> EXPLAIN SELECT * FROM s1 WHERE key1 > 'z' AND key3 = 'a';
+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys     | key      | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | s1    | NULL       | ref  | idx_key1,idx_key3 | idx_key3 | 303     | const |    6 |     2.75 | Using where |
+----+-------------+-------+------------+------+-------------------+----------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

上述执行计划的possible_keys列的值是idx_key1,idx_key3,表示该查询可能使用到idx_key1,idx_key3两个索引,然后key列的值是idx_key3,表示经过查询优化器计算使用不同索引的成本后,最后决定使用idx_key3来执行查询比较划算。

rows

如果查询优化器决定使用全表扫描的方式对某个表执行查询时,执行计划的rows列就代表预计需要扫描的行数,如果使用索引来执行查询时,执行计划的rows列就代表预计扫描的索引记录行数。比如下边这个查询:

怎么抓取慢sql

正确方式:

说明开启慢查询日志可以让MySQL记录下查询超过指定时间的语句通过定位分析性能的瓶颈才能更好的优化数据库系统的性能。

首先打开慢查询 show variables like 'slow_query%';

show variables like 'long_query_time';

方法一:全局变量设置 将 slow_query_log 全局变量设置为“ON”状态

mysql> set global slow_query_log='ON'; 设置慢查询日志存放的位置

mysql> set global slow_query_log_file='/usr/local/mysql/data/slow.log'; 查询超过1秒就记录

mysql> set global long_query_time=1;

方法二:配置文件设置 修改配置文件my.cnf在[mysqld]下的下方加入

[mysqld]
slow_query_log = ON
slow_query_log_file = /usr/local/mysql/data/slow.log
long_query_time = 1

3.重启MySQL服务

service mysqld restart

1.执行一条慢查询SQL语句

mysql> select sleep(2);

2.查看是否生成慢查询日志

ls /usr/local/mysql/data/slow.log

如果日志存在MySQL开启慢查询设置成功

explain分析sql

show profile分析sql

SQL执行慢的原因

偶尔比较慢

1.数据库刷新脏页

  • redolog写满更新数据或者插入数据时会先在内存中将相应的数据更新并不会立刻持久化到磁盘中去而是把更新记录存到redolog日志中去待到空闲时再通过redolog把最新数据同步到磁盘中去。所以当redolog写满的时候就不会等到空闲时而是暂停手中的活去把数据同步到磁盘中所以这个时候SQL就会执行的比较慢
  • 内存写满如果一次查询的数据过多查询的数据页并不在内存中这时候就需要申请新的内存空间而如果此时内存已满就需要淘汰一部分内存数据页如果是干净页就直接释放如果是脏页就需要flush
  • 数据库认为空闲的时候:这时候系统不忙
  • 数据库正常关闭内存脏页flush到磁盘上

2.无法获取锁

一直很慢

  1. 字段没有索引
  2. 有索引没用
  3. 索引没用上
  4. 数据库选错索引:通过区分度判断走索引的话反而扫描的行数很大而且索引要走两边,选择全表扫描

redo日志undo日志binlog日志

1、设计数据库的大叔把这些为了回滚而记录的这些东东称之为撤销日志英文名为undo log,我们也可以土洋结合,称之为undo日志

2、我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效即使后来系统崩溃在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘只需要把修改了哪些东西记录一下就好所以上述内容也被称之为重做日志,英文名为redo log

3、binlog其实就是记录了数据库执行更改的所有操作因此很显然它可以用来做数据归档和数据恢复

4、binlog主要用来做数据归档但是它并不具备崩溃恢复的能力也就是说如果你的系统突然崩溃重启后可能会有部分数据丢失而redo log的存在则可以完美解决这个问题。

5、redo log是InnoDB引擎特有的binlog是MySQL的Server层实现的所有引擎都可以使用。

6、redo log是物理日志记录的是“在某个数据页上做了什么修改”binlog是逻辑日志记录的是这个语句的原始逻辑比如“给ID=2这一行的c字段加1 ”。

7、redo log是循环写的空间固定会用完binlog是可以追加写入的。“追加写”是指binlog文件写到一定大小后会切换到下一个并不会覆盖以前的日志。

count(1)count(*)count(filed)

  • count(*)包括了所有的列相当于行数在统计结果的时候不会忽略列值为NULL

  • count(列名)在统计结果的时候会忽略列值为NULL

  • count(1)包括了所有列用1代表代码行在统计结果的时候不会忽略列值为NULL

对于COUNT(1)和COUNT(*)执行优化器的优化是完全一样的

数据库连接池

https://www.cnblogs.com/whb11/p/11315463.html

池化技术

1、池是一种广义上的池比如数据库连接池、线程池、内存池、对象池等。以数据库连接池为例数据库连接是一种关键的有限的昂贵的资源这一点在多用户的网页应用程序中体现得尤为突出。 一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完都关闭连接,这样会造成系统的性能低下。数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并讲这些连接组成一个连接池(简单说:在一个“池”里放了好多半成品的数据库联接对象),由应用程序动态地对池中的连接进行申请、使用和释放,但并不销毁。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。

2、池技术的优势是

  • 可以消除对象创建所带来的延迟,从而提高系统的性能。
  • 也可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。
  • 连接复用,降低资源消耗。避免因创建过多连接而导致内存不够用了。

常见的连接池参数

maxActive 连接池同一时间可分配的最大活跃连接数 100
maxIdle 始终保留在池中的最大连接数如果启用将定期检查限制连接超出此属性设定的值且空闲时间超过minEvictableIdleTimeMillis的连接则释放 与maxActive设定的值相同
minIdle 始终保留在池中的最小连接数,池中的连接数量若低于此值则创建新的连接,如果连接验证失败将缩小至此值 与initialSize设定的值相同
initialSize 连接池启动时创建的初始连接数量 10
maxWait 最大等待时间(毫秒),如果在没有连接可用的情况下等待超过此时间,则抛出异常 3000030秒
minEvictableIdleTimeMillis 连接在池中保持空闲而不被回收的最小时间(毫秒) 6000060秒

数据库范式

https://blog.csdn.net/weixin_43433032/article/details/89293663

  • 属于第一范式关系的所有属性都不可再分,即数据项不可分
  • 第二范式是指每个表必须有一个有且仅有一个数据项作为关键字或主键primary key其他数据项与关键字或者主键一一对应即其他数据项完全依赖于关键字或主键。由此可知单主属性的关系均属于第二范式
    • 候选码: 若关系中的某一属性组的值能唯一地标识一个元组,而其子集不能,则称该属性组为候选码。若一个关系中有多个候选码,则选定其中一个为主码。
    • 以上面的学生表为例,表中的码为学号(码可以为学号或者姓名,此处假定码为学号),非主属性为性别、年龄(其余都为主属性),当学号确定时,性别、年龄也都惟一的被确定为,故学生表的设计满足第二范式(学生表为单主属性的关系)。
  • 第三范式:非主属性既不传递依赖于码,也不部分依赖于码
    • 人话:非主码字段既不传递依赖于主码,也不部分依赖于主码。这里的主码常见的就是主键

索引失效的场景

1、索引列类型是字符串查询条件未加引号。

2、使用like时通配符在前

3、在查询条件中使用OR

4、对索引列进行函数运算

5、虽然使用了索引列但是对索引进行了诸如加减乘除的运算

6、联合索引不符合最左前缀法则

7、使用索引需要扫描的行数过多这时mysql优化器会直接使用全表扫描

三大引擎

MyISAM

特性:

  ①不支持事务。

  ②表级锁定,并发性能大大降低。

  ③读写互相阻塞。

适用场景:

  ①不支持事务。

  ②并发相对较低,表锁定。

  ③执行大量select语句操作的表。

  ④count(*)操作较快。

  ⑤不支持外键。

查询速度快的原因a.MyISAM存储的直接是文件的offset。b.不用维护mvcc。

InnoDB

特征:

  ①良好的事务支持:支持事务隔离的四个级别。

  ②行级锁定:使用间隙锁??????

  ③外键约束。

  ④支持丢失数据的自动恢复。

Memory

  在内存中默认使用hash索引等值条件查找快速快范围查找慢断电后数据丢失但表结构存在

Mysql主从复制

https://www.jianshu.com/p/faf0127f1cb2

https://doocs.gitee.io/advanced-java/#/./docs/high-concurrency/mysql-read-write-separation.md

前提是作为主服务器角色的数据库服务器必须开启二进制日志,且复制是异步的

1、主服务器上面的任何修改都会通过自己的 I/O tread(I/O 线程)保存在二进制日志 Binary log 里面。

2、从服务器上面也启动一个 I/O thread通过配置好的用户名和密码, 连接到主服务器上面请求读取二进制日志,然后把读取到的二进制日志写到本地的一个Realy log(中继日志)里面。

3、从服务器上面同时开启一个 SQL thread 定时检查 Realy log(这个文件也是二进制的),如果发现有更新立即把更新的内容在本机的数据库上面执行一遍。

  • 这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行 SQL 的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。

  • 而且这里还有另外一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。

    所以 MySQL 实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。

    这个所谓半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。

    所谓并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

MySQL 主从同步延时问题(精华)

一般来说,如果主从延迟较为严重,有以下解决方案:

  • 分库,将一个主库拆分为多个主库,每个主库的写并发就减少了几倍,此时主从延迟可以忽略不计。
  • 打开 MySQL 支持的并行复制,多个库并行复制。如果说某个库的写入并发就是特别高,单库写并发达到了 2000/s并行复制还是没意义。
  • 重写代码,写代码的同学,要慎重,插入数据时立马查询可能查不到。
  • 如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询设置直连主库不推荐这种方法,你要是这么搞,读写分离的意义就丧失了。

Mysql分库分表

https://gitee.com/youthlql/advanced-java/blob/master/docs/high-concurrency/database-shard.md

分表

比如你单表都几千万数据了,,单表数据量太大,会极大影响你的 sql 执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,单表到几百万的时候,性能就会相对差一些了,就得分表了。

分库

分库就是你一个库一般而言,最多支撑到并发 2000一定要扩容了而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

分库分表中间件

Sharding-jdbc

当当开源的,属于 client 层方案,目前已经更名为 ShardingSphere(后文所提到的 Sharding-jdbc,等同于 ShardingSphere)。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且截至 2019.4,已经推出到了 4.0.0-RC1 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务最大努力送达型事务、TCC 事务)。

Sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合 Sharding-jdbc 的依赖;

Mycat

基于 Cobar 改造的,属于 proxy 层方案,支持的功能非常完善。

Mycat 这种 proxy 层方案的缺点在于需要部署,自己运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。

拆分维度

  • 水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。

  • 垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表**或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

如何让系统从未分库分表动态切换到分库分表上?

停机迁移方案

双写迁移方案

1、简单来说就是在线上系统里面之前所有写库的地方增删改操作除了对老库增删改,都加上对新库的增删改,这就是所谓的双写,同时写俩库,老库和新库。

2、然后系统部署之后,新库数据差太远,用导数据工具,跑起来读老库数据写新库,写的时候要根据 modified 这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。简单来说,就是不允许用老数据覆盖新数据。

3、导完一轮之后有可能数据还是存在不一致那么就程序自动做一轮校验比对新老库每个表的每条数据接着如果有不一样的就针对那些不一样的从老库读数据再次写。反复循环直到两个库每个表的数据都完全一致为止。

4、接着当数据完全一致了就 ok 了,基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。所以现在基本玩儿数据迁移之类的,都是这么干的。

如何设计可以动态扩容缩容的分库分表方案?

1、第一次分库分表就一次性给他分个够,一开始上来就是 32 个库,每个库 32 个表,那么总共是 1024 张表。这个分法,第一,基本上国内的互联网肯定都是够用了,第二,无论是并发支撑还是数据量支撑都没问题。

2、每个库正常承载的写入并发量是 1000那么 32 个库就可以承载 32 * 1000 = 32000 的写并发,如果每个库承载 1500 的写并发32 * 1500 = 48000 的写并发,接近 5 万每秒的写入并发前面再加一个MQ削峰每秒写入 MQ 8 万条数据,每秒消费 5 万条数据。

3、一个实践是利用 32 * 32 来分库分表,即分为 32 个库,每个库里一个表分为 32 张表。一共就是 1024 张表。根据某个 id 先根据 32 取模路由到库,再根据 32 取模路由到库里的表。

分库分表之后id 主键如何处理?

snowflake 算法

snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala 语言实现,是把一个 64 位的 long 型的 id1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id12 bit 作为序列号。

  • 1 bit不用为啥呢因为二进制里第一个 bit 为如果是 1那么都是负数但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
  • 41 bit表示的是时间戳单位是毫秒。41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2^41 - 1 个毫秒值换算成年就是表示69年的时间。
  • 10 bit记录工作机器 id代表的是这个服务最多可以部署在 2^10台机器上哪也就是1024台机器。但是 10 bit 里 5 个 bit 代表机房 id5 个 bit 代表机器 id。意思就是最多代表 2^5个机房32个机房每个机房里可以代表 2^5 个机器32台机器
  • 12 bit这个是用来记录同一个毫秒内产生的不同 id12 bit 可以代表的最大正整数是 2^12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。
0 | 0001100 10100010 10111110 10001001 01011100 00 | 10001 | 1 1001 | 0000 00000000

Spring

ThinkWon博客https://blog.csdn.net/ThinkWon/article/details/104397516

Spring IOC

什么是依赖注入

控制反转IoC是一个很大的概念可以用不同的方式来实现。其主要实现方式有两种依赖注入和依赖查找

依赖注入相对于IoC而言依赖注入(DI)更加准确地描述了IoC的设计理念。所谓依赖注入Dependency Injection即组件之间的依赖关系由容器在应用系统运行期来决定也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之中。组件不做定位查询只提供普通的Java方法让容器去决定依赖关系。

依赖注入的基本原则

依赖注入的基本原则是应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由IoC容器负责“查找资源”的逻辑应该从应用组件的代码中抽取出来交给IoC容器负责。容器全权负责组件的装配它会把符合依赖关系的对象通过属性JavaBean中的setter或者是构造器传递给需要的对象。

依赖注入有什么优势

依赖注入之所以更流行是因为它是一种更可取的方式让容器全权负责依赖查询受管组件只需要暴露JavaBean的setter方法或者带参数的构造器或者接口使容器可以在初始化时组装对象的依赖关系。其与依赖查找方式相比主要优势为

  • 查找定位操作与应用代码完全无关。
  • 不依赖于容器的API可以很容易地在任何容器以外使用应用对象。
  • 不需要特殊的接口,绝大多数对象可以做到完全不必依赖容器。

有哪些不同类型的依赖注入实现方式?

依赖注入是时下最流行的IoC实现方式依赖注入分为接口注入Interface InjectionSetter方法注入Setter Injection和构造器注入Constructor Injection三种方式。其中接口注入由于在灵活性和易用性比较差现在从Spring4开始已被废弃。

构造器依赖注入:构造器依赖注入通过容器触发一个类的构造器来实现的,该类有一系列参数,每个参数代表一个对其他类的依赖。

Setter方法注入Setter方法注入是容器通过调用无参构造器或无参static工厂 方法实例化bean之后调用该bean的setter方法即实现了基于setter的依赖注入。

Spring AOP动态

看ThinkWon博客

Bean生命周期

  • Bean 容器找到配置文件中 Spring Bean 的定义Bean 容器利用 Java Reflection API 创建一个Bean的实例。

  • 如果涉及到一些属性值 利用set方法设置一些属性值。

  • 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法传入Bean的名字。

  • (如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,传入 ClassLoader对象的实例。)

  • 与上面的类似,如果实现了其他 *.Aware接口,就调用相应的方法。比如:

    1. 如果bean实现了BeanFactoryAware接口Spring将调用setBeanFactory()方法将BeanFactory容器实例传入

    2. 如果bean实现了ApplicationContextAware接口Spring将调用setApplicationContext()方法将bean所在的应用上下文的引用传入进来

  • 如果bean实现了BeanPostProcessor接口(如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象),执行postProcessBeforeInitialization() 方法

  • 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。

  • 如果bean使用initmethod声明了初始化方法该方法也会被调用

  • 如果bean实现了BeanPostProcessor接口(如果有和加载这个 Bean的 Spring 容器相关的 BeanPostProcessor 对象),执行postProcessAfterInitialization() 方法

  • 此时bean已经准备就绪可以被应用程序使用了它们将一直驻留在应用上下文中直到该应用上下文被销毁

  • 当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。

  • 当要销毁 Bean 的时候同样如果bean使用destroy-method声明了销毁方法该方法也会被调用。

BeanPostProcessor作用https://www.jianshu.com/p/369a54201943

Bean作用域默认什么级别是否线程安全Spring如何保障线程安全的?

Bean作用域

  • singleton : 唯一 bean 实例Spring 中的 bean 默认都是单例的。
  • prototype : 每次请求(将其注入到另一个bean中或者以程序的方式调用容器的 getBean()方法)都会创建一个新的 bean 实例。
  • request : 每一次HTTP请求都会产生一个新的bean该bean仅在当前HTTP request内有效。
  • session : 在一个HTTP Session中一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。。
  • global-session 在一个全局的HTTP Session中一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。

默认级别

singleton(单例)

是否线程安全

不是Spring框架中的单例bean不是线程安全的

Spring如何解决线程安全

在一般情况下只有无状态的Bean才可以在多线程环境下共享在Spring中绝大部分Bean都可以声明为singleton作用域因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理解决线程安全问题。

  • 有状态就是有数据存储功能。
  • 无状态就是不会保存数据。

(实际上大部分时候 spring bean 无状态的(比如 dao 类),所有某种程度上来说 bean 也是安全的,但如果 bean 有状态的话(比如 view model 对象),那就要开发者自己去保证线程安全了,最简单的就是改变 bean 的作用域把“singleton”变更为“prototype”这样请求 bean 相当于 new Bean()了,所以就可以保证线程安全了。)

Spring事务隔离级别和事务传播属性

支持当前事务的情况:

  • TransactionDefinition.PROPAGATION_REQUIRED 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  • TransactionDefinition.PROPAGATION_SUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
  • TransactionDefinition.PROPAGATION_MANDATORY 如果当前存在事务则加入该事务如果当前没有事务则抛出异常。mandatory强制性

不支持当前事务的情况:

  • TransactionDefinition.PROPAGATION_REQUIRES_NEW 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER 以非事务方式运行,如果当前存在事务,则抛出异常。

其他情况:

  • TransactionDefinition.PROPAGATION_NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则创建一个新的事务。

补充

脏读(Drity Read)某个事务已更新一份数据另一个事务在此时读取了同一份数据由于某些原因前一个RollBack了操作则后一个事务所读取的数据就会是不正确的。 不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。 幻读(Phantom Read):同一个事务内多次查询返回的结果集不一样。比如同一个事务 A 第一次查询时候有 n 条记录,但是第二次同等条件下查询却有 n+1 条记录,这就好像产生了幻觉。发生幻读的原因也是另外一个事务新增或者删除或者修改了第一个事务结果集里面的数据,同一个记录的数据内容被修改了,所有数据行的记录就变多或者变少了

具体讲解:https://blog.csdn.net/qq_35433593/article/details/86094028

Spring以及Spring MVC常见注解

https://blog.csdn.net/hsf15768615284/article/details/81623881

@autowired和@resource的区别当UserDao存在不止一个bean或没有存在时会怎样?怎么解决?

区别

@Autowired可用于构造函数、成员变量、Setter方法 @Autowired和@Resource之间的区别

  • @Autowired默认是按照类型装配注入的默认情况下它要求依赖对象必须存在可以设置它required属性为false

  • @Resource默认是按照名称来装配注入的只有当找不到与名称匹配的bean才会按照类型来装配注入。

会怎么样

@Autowired是根据类型进行自动装配的。如果当Spring上下文中存在不止一个UserDao类型的bean时就会抛出BeanCreationException异常;如果Spring上下文中不存在UserDao类型的bean也会抛出BeanCreationException异常。我们可以使用@Qualifier配合@Autowired来解决这些问题。如果我们想使用名称装配可以结合@Qualifier注解进行使用如下

再不懂的话看这里:https://www.cnblogs.com/aspirant/p/10431029.html

SpringBoot自动配置的原理是什么介绍SpringBootApplication注解.

原理是什么

1.Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到所有jar包中META-INF/spring.factories配置文件中的所有自动配置类并对其进行加载。

2.而这些自动配置类都是以AutoConfiguration结尾来命名的xxxAutoConfiguration配置类中通过@EnableConfigurationProperties注解取得xxxProperties类在全局配置文件中配置的属性(如server.port)

3.而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。

具体讲解:https://blog.csdn.net/u014745069/article/details/83820511

https://github.com/Snailclimb/springboot-guide/blob/master/docs/interview/springboot-questions.md

SpringBootApplication注解

  1. @SpringBootApplication是一个复合注解或派生注解在@SpringBootApplication中有一个注解@EnableAutoConfiguration字面意思就是开启自动配置
  2. @EnableAutoConfiguration 注解通过Spring 提供的 @Import 注解导入了AutoConfigurationImportSelector
  3. AutoConfigurationImportSelector的selectImports()方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories的jar包。spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件。
  4. 这个spring.factories文件也是一组一组的key=value的形式其中一个key是EnableAutoConfiguration类的全类名而它的value是一个xxxxAutoConfiguration的类名的列表这些类名以逗号分隔。将所有自动配置类加载到Spring容器中用他们来做自动配置。

@Transactional注解加载和运行机制?

事务的原理

1、Spring事务 的本质其实就是数据库对事务的支持没有数据库的事务支持spring是无法提供事务功能的。

2、@Transactional注解可以帮助我们把事务开启、提交或者回滚的操作免去了重复的事务管理逻辑。是通过aop的方式进行管理.

3、Spring AOP中对一个方法进行代理的话肯定需要定义切点。在@Transactional的实现中同样如此spring为我们定义了以 @Transactional 注解为植入点的切点,这样才能知道@Transactional注解标注的方法需要被代理。@Transactional的作用一个就是标识方法需要被代理一个就是携带事务管理需要的一些属性信息。

4、bean在进行实例化的时候会判断适配了BeanFactoryTransactionAttributeSourceAdvisor也就是调用的方法是否适配了切面

5、如果没有适配的话就返回原对象给IOC容器

6、如果适配了的话就会创建代理对象返回给IOC容器。AOP动态代理时开始执行的方法是DynamicAdvisedInterceptor#intercept【这个方法只要是aop都会执行下面的TransactionInterceptor相当于其具体的实现】。最终执行的方法是TransactionInterceptor#invoke方法。并且把CglibMethodInvocation注入到invoke方法中CglibMethodInvocation就是包装了目标对象的方法调用的所有必须信息因此在TransactionInterceptor#invoke里面也是可以调用目标方法的并且还可以实现类似@Around的逻辑在目标方法调用前后继续注入一些其他逻辑比如事务管理逻辑。

7、TransactionInterceptor内部依赖于TransactionManagerTransactionManager是实际的事务管理对象

Spring中用到了哪些设计模式单例、***、工厂、适配、观察者之类的说一说就行

  • 工厂设计模式Spring使用工厂模式可以通过 BeanFactoryApplicationContext 创建 bean 对象。

  • 单例设计模式Spring 中 bean 的默认作用域就是 singleton(单例)的。

  • 代理设计模式Spring AOP 就是基于代理的AOP能够将那些与业务无关却为业务模块所共同调用的逻辑或责任例如事务处理、日志管理、权限控制等封装起来便于减少系统的重复代码降低模块间的耦合度。

  • 观察者模式定义对象间的一种一对多的依赖关系当一个对象的状态发生改变时所有依赖于它的对象都得到通知并被自动更新。常用的地方是listener的实现如ApplicationListener。

  • 适配器模式:适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。

    • Spring AOP 的增强或通知(Advice)使用到了适配器模式Springmvc也用到了
  • 模板方法模式JdbcTemplate中的execute方法

  • 包装器模式:转换数据源

讲解的地方:https://blog.csdn.net/qq_34337272/article/details/90487768

https://www.jianshu.com/p/ace7de971a57

Spring由哪些模块组成

看ThinkWon博客

Spring中的代理

Spring AOP and AspectJ AOP 有什么区别AOP 有哪些实现方式?

AOP实现的关键在于 代理模式AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ动态代理则以Spring AOP为代表。

1AspectJ是静态代理的增强所谓静态代理就是AOP框架会在编译阶段生成AOP代理类因此也称为编译时增强他会在编译阶段将AspectJ(切面)织入到Java字节码中运行的时候就是增强之后的AOP对象。

2Spring AOP使用的动态代理所谓的动态代理就是说AOP框架不会去修改字节码而是每次运行时在内存中临时为方法生成一个AOP对象这个AOP对象包含了目标对象的全部方法并且在特定的切点做了增强处理并回调原对象的方法。

JDK动态代理和CGLIB动态代理的区别

Spring AOP中的动态代理主要有两种方式JDK动态代理和CGLIB动态代理

  • JDK动态代理只提供接口的代理不支持类的代理。核心InvocationHandler接口和Proxy类InvocationHandler 通过invoke()方法反射来调用目标类中的代码动态地将横切逻辑和业务编织在一起接着Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。

  • 如果代理类没有实现 InvocationHandler 接口那么Spring AOP会选择使用CGLIB来动态代理目标类。(CGLIBCode Generation Library是一个代码生成的类库可以在运行时动态的生成指定类的一个子类对象并覆盖其中特定方法并添加增强代码从而实现AOP。)CGLIB是通过继承的方式做的动态代理因此如果某个类被标记为final那么它是无法使用CGLIB做动态代理的。

静态代理与动态代理区别在于生成AOP代理对象的时机不同相对来说AspectJ的静态代理方式具有更好的性能但是AspectJ需要特定的编译器进行处理而Spring AOP则无需特定的编译器处理。

IOC循环依赖及其解决方案

1.什么是循环依赖

https://www.cnblogs.com/java-chen-hao/p/11139887.html

https://blog.csdn.net/qq_36381855/article/details/79752689

2.如何检测循环依赖

可以 Bean在创建的时候给其打个标记如果递归调用回来发现正在创建中的话--->即可说明循环依赖。

3.Spring如何解决循环依赖

https://blog.csdn.net/f641385712/article/details/92801300

总结

/** Cache of singleton objects: bean name --> bean instance */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);

/** Cache of singleton factories: bean name --> ObjectFactory */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);

/** Cache of early singleton objects: bean name --> bean instance */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
缓存 用途
singletonObjects 用于存放完全初始化好的 bean从该缓存中取出的 bean 可以直接使用
earlySingletonObjects 存放原始的 bean 对象(尚未填充属性),用于解决循环依赖
singletonFactories 存放 bean 工厂对象,用于解决循环依赖

1.他们就是 Spring 解决 singleton bean 的关键因素所在,我称他们为三级缓存,第一级为 singletonObjects第二级为 earlySingletonObjects第三级为 singletonFactories。

2.Spring 在创建 bean 的时候并不是等它完全完成,而是在创建过程中将创建中的 bean 的 ObjectFactory 提前曝光(即加入到 singletonFactories 缓存中),这样一旦下一个 bean 创建的时候需要依赖 bean 从三级缓存中拿到ObjectFactory然后直接使用 ObjectFactory 的 getObject() 获取bean也就是 getSingleton()中的代码片段了。

Springmvc原理流程

参考:https://blog.nowcoder.net/n/31217f1bdf644371842a1bc85f1f4987

流程说明(重要):

1、用户请求发送至 DispatcherServlet 类进行处理

2、DispatcherServlet 类调用HandlerMapping 类请求查找 Handler

3、HandlerMapping 类根据 request 请求的 URL 等信息,以及相关拦截器 interceptor ,查找能够进行处理的 Handler最后给DispatcherServlet(前端控制器) 返回一个执行链也就是一个HandlerExecutionChain

4、前端控制器请求(适配器)HandlerAdapter 执行 Handler(也就是常说的controller控制器)

5-7、HandlerAdapter 执行相关 Handler ,并获取 ModelAndView 类的对象。最后将ModelAndView 类的对象返回给前端控制器

8、DispatcherServlet 请求 ViewResolver 类进行视图解析。

9、ViewResolver 类进行视图解析获取 View 对象,最后向前端控制器返回 View 对象

10、DispatcherServlet 类进行视图 View 渲染填充Model

11、DispatcherServlet 类向用户返回响应。

关于interceptor与Filter区别

属性 拦截器Interceptor 过滤器Filter
原理 基于java的反射机制 基于函数回调
创建 (在context.xml中配置)由Spring容器初始化。 (在web.xml中配置filter基本属性)由web容器创建
servlet 容器 拦截器不直接依赖于servlet容器 过滤器依赖于servlet容器
作用对象 拦截器只能对action请求起作用 过滤器则可以对几乎所有的请求起作用
访问范围 拦截器可以访问action上下文、值栈里的对象可以获取IOC容器中的各个bean。 不能
使用场景 即可用于Web也可以用于其他Application 基于Servlet规范只能用于Web
使用选择 可以深入到方法执行前后,使用场景更广 只能在Servlet前后起作用
在Action的生命周期中拦截器可以多次调用 而过滤器只能在容器初始化时被调用一次。

什么是action请求

就是经过controller的请求直接输入页面路径不拦截 http://localhost:8080/admin/index/index.html这样的话就不走拦截器直接跳转到页面上了

IOC容器初始化流程

简单的总结:https://cxis.me/2020/03/22/Spring%E4%B8%ADIOC%E5%AE%B9%E5%99%A8%E7%9A%84%E5%88%9D%E5%A7%8B%E5%8C%96%E8%BF%87%E7%A8%8B%E6%80%BB%E7%BB%93/

  1. Spring启动。
  2. 加载配置文件xml、JavaConfig、注解、其他形式等等将描述我们自己定义的和Spring内置的定义的Bean加载进来。
  3. 加载完配置文件后将配置文件转化成统一的Resource来处理。
  4. 使用Resource解析将我们定义的一些配置都转化成Spring内部的标识形式BeanDefinition。
  5. 在低级的容器BeanFactory中到这里就可以宣告Spring容器初始化完成了Bean的初始化是在我们使用Bean的时候触发的在高级的容器ApplicationContext中会自动触发那些lazy-init=false的单例Bean让Bean以及依赖的Bean进行初始化的流程初始化完成Bean之后高级容器也初始化完成了。
  6. 在我们的应用中使用Bean。
  7. Spring容器关闭销毁各个Bean。

基本没碰到过问的

Mybatis

SQL注入

https://blog.csdn.net/qq_41246635/article/details/81392818

Mybatis中#与$的区别

https://www.cnblogs.com/PoetryAndYou/p/11622334.html

https://blog.csdn.net/j04110414/article/details/78914787

场景题

如何设计一个秒杀系统

秒杀系统的难点

首先我们先看下秒杀场景的难点到底在哪?在秒杀场景中最大的问题在于容易产生大并发请求、产生超卖现象和性能问题,下面我们分别分析下下面这三个问题:

1瞬时大并发一提到秒杀系统给人最深刻的印象是超大的瞬时并发这时你可以联想到小米手机的抢购场景在小米手机抢购的场景一般都会有10w的用户同时访问一个商品页面去抢购手机这就是一个典型的瞬时大并发如果系统没有经过限流或者熔断处理那么系统瞬间就会崩掉就好像被DDos攻击一样

2超卖秒杀除了大并发这样的难点还有一个所有电商都会遇到的痛那就是超卖电商搞大促最怕什么最怕的就是超卖产生超卖了以后会影响到用户体验会导致订单系统、库存系统、供应链等等产生的问题是一系列的连锁反应所以电商都不希望超卖发生但是在大并发的场景最容易发生的就是超卖不同线程读取到的当前库存数据可能下个毫秒就被其他线程修改了如果没有一定的锁库存机制那么库存数据必然出错都不用上万并发几十并发就可以导致商品超卖

3性能当遇到大并发和超卖问题后必然会引出另一个问题那就是性能问题如何保证在大并发请求下系统能够有好的性能让用户能够有更好的体验不然每个用户都等几十秒才能知道结果那体验必然是很糟糕的

4黄牛你这么低的价格假如我抢到了我转手卖掉我不是血赚?就算我不卖我也不亏啊,那用户知道,你知道,别的别有用心的人(黑客、黄牛…)肯定也知道的。

那简单啊我知道你什么时候抢我搞个几十台机器搞点脚本我也模拟出来十几万个人左右的请求那我是不是意味着我基本上有80%的成功率了。

5F12链接提前暴露

从整个秒杀系统的架构其实和一般的互联网系统架构本身没有太多的不同,核心理念还是通过缓存、异步、限流来保证系统的高并发和高可用。下面从一笔秒杀交易的流程来描述下秒杀系统架构设计的要点:

1对于大秒杀活动一般运营会配置静态的活动页面配置静态活动页面主要有两个目的一方面是为了便于在各种社交媒体分发另一方面是因为秒杀活动页的流量是大促期间最大的通过配置成静态页面可以将页面发布在公有云上动态的横向扩展

2将秒杀活动的静态页面提前刷新到CDN节点通过CDN节点的页面缓存来缓解访问压力和公司网络带宽CDN上缓存js、css和图片或者Nginx服务器提供的静态资源功能。

3将秒杀服务部署在公有云的web server上使用公有云最大的好处就是能够根据活动的火爆程度动态扩容而且成本较低同时将访问压力隔离在公司系统外部

4在提供真正商品秒杀业务功能的app server上需要进行交易限流、熔断控制防止因为秒杀交易影响到其他正常服务的提供。

5服务降级处理除了上面讲到的限流和熔断控制我们还设定了降级开关对于首页、购物车、订单查询、大数据等功能都会进行一定程度的服务降级。比如进行秒杀的时候将订单查询系统进行降级减少秒杀压力

6如何防止超卖现象的发生我们日常的下单过程中防止超卖一般是通过在数据库上加锁来实现。但是还是无法满足秒杀的上万并发需求我们的方案其实也很简单实时库存的扣减在缓存中进行异步扣减数据库中的库存保证缓存中和数据库中库存的最终一致性。

7库存预热那不简单了我们要开始秒杀前你通过定时任务或者运维同学提前把商品的库存加载到Redis中让整个流程都在Redis里面去做然后等秒杀介绍了再异步的去修改库存就好了。

8MQ削峰填谷你买东西少了你直接100个请求改库我觉得没问题但是万一秒杀一万个10万个呢

你可以把它放消息队列,然后一点点消费去改库存就好了嘛,不过单个商品其实一次修改就够了,我这里说的是某个点多个商品一起秒杀的场景,像极了双十一零点。

  1. 使用缓存。
  2. 页面静态化技术。
  3. 数据库优化。
  4. 分类数据库中活跃的数据。
  5. 批量读取和延迟修改。
  6. 读写分离。

如何解决高并发秒杀的超卖问题

由秒杀引发的一个问题

  • 秒杀最大的一个问题就是解决超卖的问题。其中一种解决超卖如下方式:
1 update goods set num = num - 1 WHERE id = 1001 and num > 0

我们假设现在商品只剩下一件了,此时数据库中 num = 1

但有100个线程同时读取到了这个 num = 1所以100个线程都开始减库存了。

但你会最终会发觉,其实只有一个线程减库存成功其他99个线程全部失败。

为何?

这就是MySQL中的排他锁起了作用。

排他锁又称为写锁简称X锁顾名思义排他锁就是不能与其他所并存如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。

就是类似于我在执行update操作的时候这一行是一个事务**(默认加了排他锁**)。这一行不能被任何其他线程修改和读写

  • 第二种解决超卖的方式如下
1 select version from goods WHERE id= 1001
2 update goods set num = num - 1, version = version + 1 WHERE id= 1001 AND num > 0 AND version = @version(上面查到的version);

这种方式采用了版本号的方式,其实也就是CAS的原理。

假设此时version = 100 num = 1; 100个线程进入到了这里同时他们select出来版本号都是version = 100。

然后直接update的时候只有其中一个先update了同时更新了版本号。

那么其他99个在更新的时候会发觉version并不等于上次select的version就说明version被其他线程修改过了。那么我就放弃这次update

  • 第三种解决超卖的方式如下

利用redis的单线程预减库存。比如商品有100件。那么我在redis存储一个k,v。例如 <gs1001, 100>

每一个用户线程进来key值就减1等减到0的时候全部拒绝剩下的请求。

那么也就是只有100个线程会进入到后续操作。所以一定不会出现超卖的现象

  • 总结

可见第二种CAS是失败重试并无加锁。应该比第一种加锁效率要高很多。类似于Java中的Synchronize和CAS