必看说明

  1. 这篇文章是笔者校招时整理的一部分,有的是直接给出了url链接,也有很多是自己看书的总结(所以直接看答案可能会看不懂),都是相当简洁的版本(反正就是很水很乱,哈哈哈)。这篇文章所包含的知识点,我后面大部分都会重新写详细版,目前放出这篇文章的意义只是给需要看的人一个参考。
  2. 比如说
    • Java集合的源码,我会写详细版【TODO,这些东西大部分已经看过了,在计划中】
    • Java并发我已经写了详细版。不过还有一些的东西没写比如:阻塞队列,ConcurrentHashMap,CopyOnWriteArrayList等这些并发容器,也是常考点,还有一些常非面试的内容比如手写线程池等等,手写阻塞队列等等我觉得很有意思的点。【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

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

从高精度转到低精度有可能损失精度,所以不会自动强转。比如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) {//线程1,2到达这里
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 查出来一个 value,2ms 搞定。性能提升 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 和 rollback,begin 指示事务的开始,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个指针,比平衡树更有优势。
  • 查找单个key,skiplist和平衡树的时间复杂度都为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. 是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
  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 log,undo 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只缓存索引块, 这中间还有换进换出的减少;

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

3)INNODB还需要维护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

这里我们只关注三种,分别是type,key,rows

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 最大等待时间(毫秒),如果在没有连接可用的情况下等待超过此时间,则抛出异常 30000(30秒)
minEvictableIdleTimeMillis 连接在池中保持空闲而不被回收的最小时间(毫秒) 60000(60秒)

数据库范式

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 型的 id,1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 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 代表机房 id,5 个 bit 代表机器 id。意思就是最多代表 2^5个机房(32个机房),每个机房里可以代表 2^5 个机器(32台机器)。
  • 12 bit:这个是用来记录同一个毫秒内产生的不同 id,12 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 Injection),Setter方法注入(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内部依赖于TransactionManager,TransactionManager是实际的事务管理对象

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为代表。

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

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

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

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

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

  • 如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。(CGLIB(Code 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%的成功率了。

5)F12链接提前暴露

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

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

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

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

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

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

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

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

8)MQ削峰填谷:你买东西少了你直接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