一 Java相关

1. Java基础

1.1. String、StringBuffer、StringBuider区别?

可变性

  • String类使用final关键字修饰,底层采用byte数组实现所以是不可变得。
  • StringBuilder 与 StringBuffer 都继承⾃ AbstractStringBuilder 类,也是使用char[]数组实现的,所以这两个都是可变的。

线程安全性

  • String 中的对象是不可变的,也就可以理解为常量,线程安全。
  • StringBuffer对方法加了同步锁,所以是线程安全的。StringBuilder没有加同步锁所以是线程不安全的。

1.2 接⼝和抽象类的区别是什么?

  1. 子类限制: 一个类可以实现多个接口,但只能实现一个抽象类
  2. 应用: 从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,⽽接⼝是对⾏为的抽象,是⼀种⾏为的规范。
  3. 权限: 抽象类可以使用任何权限, 而接口只能使用public访问权限。

1.3 构造器 Constructor 是否可被 override?

构造器不能被重写,但是可以重载

1.4 重载和重写的区别

重载就是同样的一个方法能够根据输入参数数据的不同,做出不同的处理 重写就是当子类继承父类的方法,输⼊数据⼀样,但要做出有别于⽗类的响应时,你就要覆盖⽗类⽅法

1.5 == 与 equals区别

==: 它的作用就是判断两个对象的地址是否相等。 (基本数据类型比较的是值,引用数据类型比较的是地址值)

equest:它的作用也是判断两个对象是否相等。但它一般有两种使用情况:

  1. 类没有重写equest()方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
  2. 类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。

1.6 hashCode与equest

⾯试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写 equals 时必须重写hashCode ⽅法?”

1.6.1 hashcode介绍:

hashcode()的作用就是获取哈希码,也叫散列码。实际上是一个int整数。这个哈希码的作用就是确定该对象在哈希表中的索引位置。hashcode定义在JDK中的Object类中,也就意味着java中任何类都有hashcode这个方法。

  1. public native int hashCode(); //本地方法

1.6.2 为什么要有hashcode?

我们以HashSet如何检查重复”为例⼦来说明为什么要有 hashCode?

当你把对象加入HashSet时,HashSet会先计算对象的HashCode值来判断对象加入的位置,同时也会与其他已经加入对象的hashcode值进行比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode 值的对象,这时会调⽤ equals() ⽅法来检查 hashcode 相等的对象是否真的相同。如果两者相同, HashSet 就不会让其加⼊操作成功。如果不同的话,就会重新散列到其他位置。这样我们就⼤⼤减少了 equals 的次数,相应就⼤⼤提⾼了执⾏速度。

1.6.3 为什么重写 equals 时必须重写 hashCode ⽅法?

如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals⽅法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。

1.7 java中有某个字段不想被序列化怎么办?

对于不想被序列化的字段,可以使用transient关键字修饰。transient 只能修饰变量,不能修饰类和⽅法。

1.8 BIO,NIO,AIO 有什么区别

BIO (Blocking I/O):同步阻塞 I/O 模式,数据的读取写⼊必须阻塞在⼀个线程内等待其完成。

NIO (Non-blocking/New I/O): NIO 是⼀种同步⾮阻塞的 I/O 模型,在 Java 1.4 中引⼊了NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。

AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引⼊了 NIO 的改进版 NIO 2,它是异步⾮阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应⽤操作之后会直接返回,不会堵塞在那⾥,当后台处理完成,操作系统会通知相应的线程进⾏后续的操作。

1.9 深拷⻉ vs 浅拷⻉

  1. 浅拷⻉:对基本数据类型进⾏值传递,对引⽤数据类型进⾏引⽤传递般的拷⻉,此为浅拷⻉。
  2. 深拷⻉:对基本数据类型进⾏值传递,对引⽤数据类型,创建⼀个新的对象,并复制其内容,此为深拷⻉。

2. Java集合

2.1 说说List,Set,Map三者的区别?

  • List:存储的数据是有序的,可重复的
  • Set:存储的数据是无序的,不可重复的
  • Map:使用键值对存储数据,Key 是⽆序的、不可重复的,value 是⽆序的、可重复的,每个键最多映射到⼀个值。

2.2 ArrayList与LinkedList区别

  1. 线程是否安全: 两者都是线程不安全的
  2. 底层数据结构: ArrayList底层数据结构使用Object数组; LinkedList底层使用的是双向链表
  3. 插⼊和删除是否受元素位置的影响:ArrayList 采⽤数组存储,所以插⼊和删除元素的时间复杂度受元素位置的影响。。 ②LinkedList 采⽤链表存储,所以对于 add(E e) ⽅法的插⼊,删除元素时间复杂度不受元素位置的影响,

2.3 ArrayList 扩容机制

(JDK8) ArrayList中有三种方式来初始化

汇总 - 图1

  1. /**
  2. * 默认初始容量大小
  3. */
  4. private static final int DEFAULT_CAPACITY = 10;
  5. private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  6. /**
  7. *默认构造函数,使用初始容量10构造一个空列表(无参数构造)
  8. */
  9. public ArrayList() {
  10. this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
  11. }
  12. /**
  13. * 带初始容量参数的构造函数。(用户自己指定容量)
  14. */
  15. public ArrayList(int initialCapacity) {
  16. if (initialCapacity > 0) {//初始容量大于0
  17. //创建initialCapacity大小的数组
  18. this.elementData = new Object[initialCapacity];
  19. } else if (initialCapacity == 0) {//初始容量等于0
  20. //创建空数组
  21. this.elementData = EMPTY_ELEMENTDATA;
  22. } else {//初始容量小于0,抛出异常
  23. throw new IllegalArgumentException("Illegal Capacity: "+
  24. initialCapacity);
  25. }
  26. }
  27. /**
  28. *构造包含指定collection元素的列表,这些元素利用该集合的迭代器按顺序返回
  29. *如果指定的集合为null,throws NullPointerException。
  30. */
  31. public ArrayList(Collection<? extends E> c) {
  32. elementData = c.toArray();
  33. if ((size = elementData.length) != 0) {
  34. // c.toArray might (incorrectly) not return Object[] (see 6260652)
  35. if (elementData.getClass() != Object[].class)
  36. elementData = Arrays.copyOf(elementData, size, Object[].class);
  37. } else {
  38. // replace with empty array.
  39. this.elementData = EMPTY_ELEMENTDATA;
  40. }
  41. }

补充:JDK7 new无参构造的ArrayList对象时,直接创建了长度是10的Object[]数组elementData 。jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式。JDK8的内存优化也值得我们在平时开发中学习。

以无参构造方法创建ArrayList时,实际上初始化赋值的是一个空值。当真正对数据进行添加元素时,才真正分配容量。即向数组中添加第一个元素时,数组容量扩容至10。

扩容的公式为: 新容量 = 旧容量/2 + 旧容量 也就是1.5倍

2.4 HashMap与HashTable的区别

  1. 线程是否安全: HashMap是非线程安全的,HashTable是线程安全的。HashTable内部方法基本都是通过Synchornized关键字修饰效率较低。 所以想要线程安全还是使用 ConCurrentHashMap
  2. 效率: 因为线程是否安全的问题,HashMap要比HashTbale效率要高一点。
  3. 对Null key 和Null value的支持: HashMap可以存储Null值的key和value,但Null作为key时只能有一个,Null作为value时可以有多个; HashTable不允许有Null的键值,否为会报空指针异常。
  4. 初始容量⼤⼩和每次扩充容量⼤⼩的不同 :
    ① 创建时如果不指定容量初始值, Hashtable默认的初始⼤⼩为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化⼤⼩为 16。之后每次扩充,容量变为原来的 2 倍。
    ② 创建时如果给定了容量初始值,那么
    Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为 2 的幂次⽅⼤⼩( HashMap 中的 tableSizeFor() ⽅法保证,下⾯给出了源代码)。也就是说 HashMap 总是使⽤ 2 的幂作为哈希表的⼤⼩,后⾯会介绍到为什么是 2 的幂次⽅。
  5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了᫾⼤的变化,当链表⻓度⼤于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这样的机制。

2.5 HashMap的底层实现

JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间

2.6 HashMap、Hashtable、ConcurrentHashMap的原理与区别

HashTable

  • 底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
  • 初始size为11,扩容:newsize = olesize*2+1

HashMap

  • 底层数组+链表实现,可以存储null键和null值,线程不安全
  • 初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
  • 扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入

ConcurrentHashMap

  • 底层采用分段的数组+链表实现,线程安全
  • 通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
  • Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
  • 有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
  • 扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

汇总 - 图2

汇总 - 图3

3. 多线程

3.1 进程和线程的区别

进程是线程的容器

比如说qq、微信对应的就是一个进程;而线程就好比一个聊天窗口。一个进程可以包含多个线程。而一个线程只能属于一个进程。

3.2 创建线程有哪些方式?

  • 继承Thread
  • 实现Runable接口
  • 实现Callable接口
    实现Runable和Callable接口的类只能被当成一个 在线程中运行的任务,不是真正意思上的线程,因此最终还是要通过Thread来调用。

3.3 并发和并行

Erlang 之父 Joe Armstrong 用一张5岁小孩都能看懂的图解释了并发与并行的区别

汇总 - 图4

并发是两个队列交替使用一台咖啡机

并行是两个队列同时使用两台咖啡机

串行是一个队列只能使用一台咖啡机顺序执行

并发是不是一个线程,并行是一个线程?

答:并行和并发都可以是多个线程,就看这些线程能否同时被多个cpu执行,如果可以就是并行,而并发是多个线程被一个cpu轮流切换执行

3.4 线程的几个状态

  1. // Thread.state 枚举类
  2. public enum State {
  3. // 新生
  4. NEW,
  5. //运行
  6. RUNNABLE,
  7. //阻塞
  8. BLOCKED,
  9. //等待
  10. WAITING,
  11. //超时等待
  12. TIMED_WAITING,
  13. //终止、停止
  14. TERMINATED;
  15. }

3.5 什么是上下⽂切换?

概括来说就是:当前任务在执⾏完 CPU 时间⽚切换到另⼀个任务之前会先保存⾃⼰的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是⼀次上下⽂切换。

3.6 什么是线程死锁?

线程死锁描述的是这样⼀种情况:多个线程同时被阻塞,它们中的⼀个或者全部都在等待某个资源被释放。由于线程被⽆限期地阻塞,因此程序不可能正常终⽌

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对⽅的资源,所以这两个线程就会互相等待⽽进⼊死锁状态

汇总 - 图5

3.6.1 ⽣死锁必须具备以下四个条件:
  1. 互斥条件:该资源任意⼀个时刻只由⼀个线程占⽤。
  2. 请求与保持条件:⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件: 线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。
  4. 循环等待条件: 若⼲进程之间形成⼀种头尾相接的循环等待资源关系。

3.7 sleep() ⽅法和 wait() ⽅法区别和共同点?

  • 两者最大的区别是: Sleep()方法没有释放锁,而wait()方法释放了锁
  • 来自不同的类, Sleep方法来自Therad类,而wait()方法来自Object类
  • wait方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法唤醒。 Sleep()方法指令完成后会自动苏醒。

3.8 为什么我们调⽤ start() ⽅法时会执⾏ run() ⽅法,为什么我们不能直接调⽤ run() ⽅法?

new一个Thread,线程进入了新建状态。调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。start()会执行线程的相应准备工作,然后自动执行run()方法。

总结: 调⽤ start() ⽅法⽅可启动线程并使线程进⼊就绪状态,直接执⾏ run() ⽅法的话不会以多线程的⽅式执⾏。

3.9 讲⼀下 JMM(Java 内存模型)

当前的 Java 内存模型下,线程可以把变量保存本地内存(⽐如机器的寄存器)中,⽽不是直接在主存中进⾏读写。这就可能造成⼀个线程在主存中修改了⼀个变量的值,⽽另外⼀个线程还继续使⽤它在寄存器中的变量值的拷⻉,造成数据的不⼀致。

汇总 - 图6

要解决这个问题,就需要把变量声明为 **volatile** ,这就指示 JVM,这个变量是共享且不稳定的,每次使⽤它都到主存中进⾏读取。所以, **volatile** 关键字 除了防⽌ **JVM** 的指令重排 ,还有⼀个重要的作⽤就是保证变量的可⻅性。

3.10 线程池

线程池: 3大方法、7大参数、4种拒绝策略

3.10.1 Executors 工具类

汇总 - 图7

  1. ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 单个线 程// ExecutorService ExecutorService threadPool = Executors.newFixedThreadPool(5); // 创建一 个固定的线程池的大小 // ExecutorService threadPool = Executors.newCachedThreadPool(); // 可伸缩 的,遇强则强,遇弱则弱

3.10.2 ThreadPoolExecutor 类

4种拒绝策略

  1. // ThreadPoolExecutor 构造器
  2. // new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异常
  3. // new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
  4. // new ThreadPoolExecutor.DiscardPolicy() // 队列满了,丢掉任务,不会抛出异常!
  5. // new ThreadPoolExecutor.DiscardOldestPolicy() // 队列满了,尝试去和最早的竞争,也不会抛出异常!
  6. public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
  7. int maximumPoolSize, //最大线程池大小
  8. long keepAliveTime, //存活时间超时了没有人调用就会释放
  9. TimeUnit unit, //时间单位
  10. BlockingQueue<Runnable> workQueue, //阻塞队列
  11. ThreadFactory threadFactory, //线程工厂
  12. RejectedExecutionHandler handler) { //拒绝策略
  13. // ....
  14. }

3.11 CAS

CAS的全称是Compare-And-Swap,它是一条CPU并发原语。 正如它的名字一样,比较并交换,它是一种很重要的同步思想。如果主内存的值跟期望值一样,那么就进行修改,否则一直重试,直到一致为止。

CAS的方式为乐观锁,Synchronized为悲观锁。因此使用CAS解决并发问题通常情况下更优

汇总 - 图8

3.11.1 CAS的ABA问题

所谓ABA问题,其实用最通俗易懂的话语来总结就是狸猫换太子 就是比较并交换的循环,存在一个时间差,而这个时间差可能带来意想不到的问题。

  1. 例:
  2. 比如有两个线程:
  3. 一开始都从主内存中拷贝了原值3;
  4. A线程执行var5=this.getIntVolatile,即var5=3。此时A线程挂起;
  5. B修改原为4,B线程执行完毕;
  6. 然后B线程觉得修改错了,然后又重新把值改为3;
  7. A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存的值等于快照值3,(**但是却不知道B曾经修改过**),修改成功。

如何解决?

  1. `通过AtomicReference原子引用,就是加版本号。 本质上就是一个乐观锁的实现。`

4. JVM

4.1 Java内存区域(运行时数据区)

JDK1.8之前:

汇总 - 图9

JDK1.8:

汇总 - 图10

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地⽅法栈

线程共享的:

  • ⽅法区
  • 直接内存 (⾮运⾏时数据区的⼀部分)

4.1.1 程序计数器

作用:

程序计数器用来存储下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令

注意:程序计数器是唯一一个不会出现OOM的内存区域,它的生命周期与线程的生命周期一致。

4.1.2 Java 虚拟机栈

汇总 - 图11

java虚拟机栈也是线程私有的,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次次方法的调用。

作用

主管java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。 只有出栈和出栈

Java 虚拟机栈会出现两种错误: StackOverFlowError 和 OutOfMemoryError 。

StackOverFlowError : 若 Java 虚拟机栈的内存⼤⼩不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最⼤深度的时候,就抛出 StackOverFlowError 错误。

OutOfMemoryError : 若 Java 虚拟机堆中没有空闲内存,并且垃圾回收器也⽆法提供更多内存的话。就会抛出 OutOfMemoryError 错误。

4.1.3 本地方法栈

和虚拟机栈作用相似,区别是: 虚拟机栈是虚拟机执行Java方法服务,而本地方法栈则为虚拟机使⽤到的 Native ⽅法服务

本地⽅法被执⾏的时候,在本地⽅法栈也会创建⼀个栈帧,⽤于存放该本地⽅法的局部变量表、操作数栈、动态链接、出⼝信息。

⽅法执⾏完毕后相应的栈帧也会出栈并释放内存空间,也会出现 StackOverFlowError 和OutOfMemoryError 两种错误。

4.1.4 堆

汇总 - 图12

堆针对一个jvm进程来说是唯一的,也就是说一个进程只有一个jvm,但是进程包含多个线程,他们是共享同一个对空间的堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。

  1. -Xms"用于表示堆区的起始内存,等价于`-XX:InitialHeapSize`
  2. -Xmx"则用于表示堆区的最大内存,等价于`-XX:MaxHeapSize`

堆内存细分

jdk7:

jdk7及以前:新生代+老年代+永久代

  • Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
  • Tenure generation space 养老区 Old/Tenure
  • Permanent Space 永久区 Perm

jdk8:

Java 8及之后:新生区+养老区+元空间

  • Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
  • Tenure generation space 养老区 Old/Tenure
  • Meta Space 元空间 Meta

4.1.5 ⽅法区

⽅法区与 Java 堆⼀样,是各个线程共享的内存区域,它⽤于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把⽅法区描述为堆的⼀个逻辑部分,但是它却有⼀个别名叫做 Non-Heap(⾮堆),⽬的应该是与 Java 堆区分开来。

  1. -XX:PermSize=N //⽅法区 (永久代) 初始⼤⼩
  2. -XX:MaxPermSize=N //⽅法区 (永久代) 最⼤⼤⼩,超过这个值将会抛出 OutOfMemoryError
  3. -XX:MetaspaceSize=N //设置 Metaspace 的初始(和最⼩⼤⼩)
  4. -XX:MaxMetaspaceSize=N //设置 Metaspace 的最⼤⼤⼩

4.2 Java对象的创建过程

  1. 类加载检查: 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且插件这个符号引用代表的类是否已经被加载、解析和初始化过。 如果没有那必须先执行相应的类加载过程
  2. 为对象分配内存:首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。如果内存规整:虚拟机将采用的是指针碰撞法(Bump The Point)来为对象分配内存。如果内存不规整:虚拟机需要维护一个空闲列表(Free List)来为对象分配内存。
    在创建对象的时候有⼀个很重要的问题,就是线程安全,因为在实际开发过程中,创建对象是很频繁的事情,作为虚拟机来说,必须要保证线程是安全的,通常来讲,虚拟机采⽤两种⽅式来保证线程安全:
    • CAS+失败重试: CAS 是乐观锁的⼀种实现⽅式。所谓乐观锁就是,每次不加锁⽽是假设没有冲突⽽去完成某项操作,如果因为冲突失败就重试,直到成功为⽌。虚拟机采⽤ CAS配上失败重试的⽅式保证更新操作的原⼦性。
    • TLAB: 为每⼀个线程预先在 Eden 区分配⼀块⼉内存,JVM 在给线程中的对象分配内存
      时,⾸先在 TLAB 分配,当对象⼤于 TLAB 中的剩余内存或 TLAB 的内存已⽤尽时,再采⽤
      上述的 CAS 进⾏内存分配
  3. 初始化零值:
    所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用
  4. 设置对象头:
    初始化零值完成之后,虚拟机要对对象进⾏必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运⾏状态的不同如是否启⽤偏向锁等,对象头会有不同的设置⽅式。
  5. 执行init方法进行初始化:
    在上⾯⼯作都完成之后,从虚拟机的视⻆来看,⼀个新的对象已经产⽣了,但从 Java 程序的视⻆来看,对象创建才刚开始, ⽅法还没有执⾏,所有的字段都还为零。所以⼀般来说,执⾏ new 指令之后会接着执⾏ ⽅法,把对象按照程序员的意愿进⾏初始化,这样⼀个真正可⽤的对象才算完全产⽣出来。

4.3 对象的访问定位有哪两种⽅式?

建立对象目的就是为了使用对象,我们的Java程序通过栈上的Reference数据来操作堆上的具体对象。对象的访问⽅式有虚拟机实现⽽定,⽬前主流的访问⽅式有①使⽤句柄和②直接指针两种:

  1. 句柄:如果使用句柄的话,那么Java堆中会划分出一块内存来作为句柄池,reference中存储的就是句柄的地址,而句柄中包含了对象实例数据与类型各自的具体地址信息;
    汇总 - 图13
  2. 直接指针:如果使⽤直接指针访问,那么 Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,⽽reference 中存储的直接就是对象的地址
    汇总 - 图14

两者区别:这两种方式各有优势。使⽤句柄来访问的最⼤好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,⽽ reference 本身不需要修改。使⽤直接指针访问⽅式最⼤的好处就是速度快,它节省了⼀次指针定位的时间开销。

4.4 如何判断对象是否死亡?(两种⽅法)

  • 引用计数器
    给对象中添加⼀个引⽤计数器,每当有⼀个地⽅引⽤它,计数器就加1;当引⽤失效,计数器就减1;任何时候计数器为0的对象就是不可能再被使⽤的。
  • 可达性分析算法
    这个算法的基本思想就是通过⼀系列的称为 “GC Roots” 的对象作为起点,从这些节点开始下搜索,节点所⾛过的路径称为引⽤链,当⼀个对象到 GC Roots 没有任何引⽤链相连的话,则证明此对象是不可⽤的。

4.5 四种引用

  1. 强引用--不回收:
  2. java中最常见的就是强引用,普通对象默认就是强引用。不会被回收
  3. 软引用--内存不足回收:
  4. 软引用是描述一些还有用,但非必须的对象。只被软引用关联的对象,在系统发生内存溢出之前会把这些对象进行二次回收,如果这次回收空 间还是不足才会OOM
  5. 弱引用--发现即回收:
  6. 被弱引用关联的对象只能生存到下一次垃圾收集为止。 在系统GC时,只要发现软引用的对象就会回收。
  7. 虚引用--对象回收跟组:
  8. 如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时可能被回收。

4.6 垃圾收集算法

4.6.1 标记-清除算法

标记清除算法是最常见的垃圾收集算法。当堆空间被耗尽的时候,就会停止整个程序(STW),进行两项操作。第一是标记,第二是清除

标记: 从根节点开始遍历,标记所有被引用的对象

清除: 从堆内存中从头到尾进行遍历,如果发现没有被标记的对象就进行回收。

缺点: 1.效率不高 2. 这种方式清理出来的空间不连续,会产生内存碎片

4.6.2 复制算法

复制算法的高效性,是建立在存货对象少,垃圾对象多的的前提下。显然比较适合新生代,不适合老年代。

过程: 将存活的内存空间分为两块,每次只使用一块,在垃圾回收的时候将正在使用的内存块中存活的对象复制到另一块中,之后清理正在使用的内存块。

缺点: 此算法缺点也比较明显,就是需要两倍的内存空间

4.6.3 标记-压缩(整理)算法

标记整理算法等同于 标记清除算法后,但后续步骤不是直接对可回收对象回收,⽽是让所有存活的对象向⼀端移动,然后直接清理掉端边界以外的内存。

过程:

  1. 1. 第一阶段和标记清除算法一样,从根节点开始标记处所有的存活对象
  2. 2. 第二阶段将所有的存储对象压缩到内存的一端,按顺序排放
  3. 3. 之后清理边界外的内存空间

4.6.4 分代收集算法

当前虚拟机的垃圾收集都采⽤分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为⼏块。⼀般将 java 堆分为新⽣代和⽼年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。

⽐如在新⽣代中,每次收集都会有⼤量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。⽽⽼年代的对象存活⼏率是⽐较⾼的,⽽且没有额外的空间对它进⾏分配担保,所以我们必须选择标记-清除标记-整理算法进⾏垃圾收集。

4.7 常⻅的垃圾回收器

  1. 新生代: SerialParNewParallel Scavenge
  2. 老年代: seiral oldParallel OldCMS
  3. 整堆收集器: G1

4.7.1 Serial、SerialOld—串行回收

serial是最基本也是历史最悠久的算法,在jdk1.3之前的唯一选择,串行回收,Stw机制的方式进行回收

新⽣代采⽤复制算法,⽼年代采⽤标记--整理算法。

优势: 简单高效,与其它收集器相比,对于单个cpu来说,serial收集器由于没有线程交互的开销,使用此收集器是个不错的选择。

  1. -XX:UseSerialGC 指定串行回收器

4.7.2 ParNew收集器—并行回收

如果说Serial是单线程的收集器,那么ParNew则是多线程版本的垃圾收集器。new只作用于新生代。

ParNew收集器除了采用并行回收方式执行内存回收外,两款垃圾收集器没有任何区别, 同样采用的是复制算法

  1. -XX:UseParNewGc 指定并行垃圾回收器ParNew

4.7.3 Parallel回收器—吞吐量优先

年轻代中除了ParNew收集器是基于并行回收之外,Parallel Scavenge收集器同样也是采用了复制算法,STW机制的方式进行回收。

Parallel old采用的标记压缩算法,回收老年代。同样是基于并发回收和STW机制。

新生代采用复制算法,老年代采用标记--整理算法。

  1. -XX:UseParallelGc 指令年轻代使用Parallel收集器
  2. -XX:UseParallelOldGc 指令老年代使用ParallelOld 收集器

4.7.4 CMS收集器—低延迟

JDK在1.5推出了CMS垃圾收集器,这款是真正意义上的并发收集器。第一次实现了让垃圾收集与用户线程同时工作。

CMS收集器是一款以获取最短回收停顿时间为⽬标的收集器

基于并发"标记清除"实现,仅作用与老年代

四个步骤:

  • 初始标记:暂停所有的其它线程,并记录下直接与GcRoot相连的对象,速度很快;一旦标记完成后就会恢复之前被暂停的线程
  • 并发标记:从GcRoots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
  • 重新标记:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因为用户线程继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
  • 并发清楚:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

优点: 并发收集、 低停顿

缺点:

  • 对cpu资源敏感
  • 无法处理浮动垃圾
  • 它使用 标记-清除算法会导致产生内存碎片

二、计算机基础

1. 计算机网络

1.1 TCP 三次握⼿和四次挥⼿(⾯试常客)

为了准确无误的把数据送达目标处,TCP采用了三次握手策略

1.1.1 TCP三次握手

汇总 - 图15

汇总 - 图16

客户端–发送带有 SYN 标志的数据包–⼀次握⼿–服务端 服务端–发送带有 SYN/ACK 标志的数据包–⼆次握⼿–客户端 客户端–发送带有带有 ACK 标志的数据包–三次握⼿–服务端

1.1.2 TCP四次挥手

汇总 - 图17

客户端-发送⼀个 FIN,⽤来关闭客户端到服务器的数据传送 服务器-收到这个 FIN,它发回⼀ 个 ACK,确认序号为收到的序号加1 。和 SYN ⼀样,⼀个FIN 将占⽤⼀个序号 服务器-关闭与客户端的连接,发送⼀个FIN给客户端 客户端-发回 ACK 报⽂确认,并将确认序号设置为收到序号加1

1.2 在浏览器中输⼊url地址 ->> 显示主⻚的过程

汇总 - 图18

  1. 浏览器查找域名的ip地址 DNS
  2. 浏览器向web服务器发送一个HTTP请求
  3. web服务器收到请求后处理请求
  4. 服务器返回一个HTML相应
  5. 浏览器渲染HTML

1.3 HTTP 1.0HTTP 1.1的主要区别是什么?

HTTP1.0最早在⽹⻚中使⽤是在1996年,那个时候只是使⽤⼀些᫾为简单的⽹⻚上和⽹络请求上,⽽HTTP1.1则在1999年才开始⼴泛应⽤于现在的各⼤浏览器⽹络请求中,同时HTTP1.1也是当前使⽤最为⼴泛的HTTP协议。

  1. 长连接: HTTP1.0中,默认使用的是短连接,也就是说每次请求都要建立一次连接(如js、css文件等)。
    短连接: HTTP1.1起,默认使用长连接,默认开启Connection: keep-alive。

1.4 HTTPHTTPS 的区别?

  1. 端口: HTTP的URL是由http://起始且默认端口为80。 而HTTPS的URL是由https://默认端口为443
  2. 安全性和资源消耗: HTTP协议运行在TCP之上,所有的传输都是明文,客户端和服务端都无法校验对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议,SSL/TSL运行在TCP之上。所有的传输内容都经过加密,加密采用非对称加密。

三、 数据库

1. MySQL

1.1 MyISAM和InnoDB区别

MyISAM是Mysql默认的数据库引擎(5.5版本之前)。虽然性能极佳,而且提供了大量的特性,包括全文检索、压缩、空间函数等,但是MyISAM不支持事务和行级锁, 而且最大的缺陷就是崩溃后无法安全恢复。 5.5版本之后,Mysql引入了InnoDB存储引擎。

  1. 是否支持事务: InooDB支持事务,而MyISAM不支持事务
  2. 是否支持行级锁: InnoDB支持行级锁,而MyISAM支持表级锁
  3. 是否支持崩溃后的安全修复: InnoDb支持,而MyISAM不支持
  4. 是否⽀持外键: MyISAM不⽀持,⽽InnoDB⽀持。
  5. 是否⽀持MVCC: 仅 InnoDB ⽀持。应对⾼并发事务, MVCC⽐单纯的加锁更⾼效;MVCC只 在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下⼯作;MVCC可以使⽤ 乐 观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统⼀。推

1.2 什么是事务?

事务就是一组操作,要么全部成功要么全部失败

事务最经典也经常被拿出来说例⼦就是转账了。假如⼩明要给⼩红转账1000元,这个转账会涉及到两个关键操作就是:将⼩明的余额减少1000元,将⼩红的余额增加1000元。万⼀在这两个操作之间突然出现错误⽐如银⾏系统崩溃,导致⼩明余额减少⽽⼩红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。

1.3 事务4大特性

  1. 原子性(atomicity): 一组操作要么全部成功要么全部失败
  2. 一致性(consistency): 执行事务前后,数据保持一致|事务前后,数据总额保持一致
  3. 隔离性(Isolation): 并发访问数据库时,各个事务是相互隔离的
  4. 持久性(Durability): 旦事务提交,对数据的改变就是永久的,即使数据组发生故障

1.4 并发事务带来的问题

在典型的应⽤程序中,多个事务并发运⾏,经常会操作相同的数据来完成各⾃的任务(多个⽤户对同⼀数据进⾏操作)。并发虽然是必须的,但可能会导致以下的问题。

  • 脏读:当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另一个事务也方法了这条数据,然后使用了这个数据。因为这个数据还是没有提交的数据,那么另一个事务读到的这个数据是”脏”数据。
  • 丢失修改: 指在一个事务读取一个数据时,另一个事务也访问了改数据,那么第一个事务修改了这个数据后,第二个事务也修改了这条数据。这样第一个事务内修改结果就会丢失,因此成为丢失修改。例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失
  • 不可重复读: 指在一个事务内多次读同一个数据。在这个事务还没有结束时,另一个事务也访问了改数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据不一致。这样就发生了一个事务内两次读到的数据不一样的情况。 因此可以成为不可重复读
  • 幻读: 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以被称为幻读。

不可重复读与幻读的区别:

  1. 不可重复读的重点是修改比如多次读取一条数据时发现其中某些值被修改。 幻读的重点在于新增或者删除比如多次读取一条数据发现记录多了或少了。

1.5 事务的隔离级别

  • READ-UNCOMMITTED(读取未提交):最低的隔离级别,允许读取尚未提交的数据。可能会发生脏读、不可重复读、幻读
  • READ-COMMITTED(读取已提交):允许读取并发事务已经提交的诗句。 可以阻止脏读,但是幻读或不可重复读还是有可能发生
  • REPEATABLE-READ(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改的。 可以阻止脏读和不可重复读,但幻读扔有可能发生。
  • SERIALIZABLE(可串⾏化):最高的隔离级别,完全服从ACID的隔离级别。 所有的事务依次执行。 可以阻止脏读、不可重复读以及幻读。

MySQL InnoDB 存储引擎的默认⽀持的隔离级别是 REPEATABLE-READ(可重读)。我们可以通过 SELECT @@tx_isolation; 命令来查看

1.6 大表优化

当数据库中一个表里的数据过大时,数据库的CRUD的性能就会明显下降,一些常见的解决措施如下:

1.6.1 限定查询范围

禁止任何不带任何条件范围的查询语句。 如: 我们在查历史订单的时候,可以控制在一个月以内

1.6.2 读/写分离

经典的数据库拆分方案,主库负责写,从库负责读。

1.6.3 垂直分区/垂直分表

汇总 - 图19

简单来说就是把一张列比较多表,拆分为多张表。如:商品表,可以拆分为商品表、商品详情表等

优点: 垂直分区可以简化表的结构,易于维护

缺点: 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应⽤层进⾏Join来解决。此外,垂直分区会让事务变得更加复杂;

1.6.4 水分分区/水平分表

保持数据表结构不变,通过某种策略存储数据分⽚。这样每⼀⽚数据分散到不同的表或者库中,达到了分布式的⽬的。 ⽔平拆分可以⽀撑⾮常⼤的数据量。

比如, 主键为偶数放到表A,为奇数放到表B

实施方案:

  • 客户端代理: 分片逻辑在应用层,封装在Jar包中,通过修改或者封装JDBC来实现。比如 Sharding-jdbc。
  • 中间件代理: 在应⽤和数据中间加了⼀个代理层。分⽚逻辑统⼀维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、⽹易的DDB等等都是这种架构的实现。

1.7 分布式ID解决方案

水平分表会引发一个问题,就是表中的组件怎么设置。 有以下几种方案:

  • UUID: 不适合做组件,因为太长了,并且无序不可读,查询效率低。
  • 数据库自增ID:两台服务器服务器分别设置不同的步长。这种方式生成的id会不好拓展。
  • 利用redis生成ID: 性能⽐᫾好,灵活⽅便,不依赖于数据库。但是引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂。
  • Twitter的snowflake算法: 算法通过时间来生成唯一的id,通常不会重复;

2. Redis缓存

2.1 Redis 常⻅数据结构以及使⽤场景

2.1.1 string
  1. 介绍:最简单的类型,就是普通的set和get,做简单的kv缓存
  2. 常⽤命令:set,get,strlen,exists,dect,incr,setex 等等。
  3. 应⽤场景:⼀般常⽤在需要计数的场景,⽐如⽤户的访问次数、热点⽂章的点赞转发数量等。
  1. set key1 haha #设置key
  2. get key1 #获取key
  3. exists key1 #是否存在
  4. strlen key1 #获取长度
  5. del key1 #删除key

2.1.2 list

汇总 - 图20

  1. 介绍: list 即是 链表。链表是⼀种⾮常常⻅的数据结构,特点是易于数据元素的插⼊和删除并且且可以灵活调整链表⻓度,但是链表的随机访问困难。Redis 的 list 的实现为⼀个 双向链表,即可以⽀持反向查找和遍历,更⽅便操作,不过带来了部分额外的内存开销。
  2. 常用命令: rpush,lpop,lpush,rpop,lrangellen 等。
  3. 使用场景: 可以通过 rpush/lpop 实现队列等
  1. lrange mylist 0 -1 # 0开始位置,-1结束 -1则表示查看所有
  2. len mylist #查询list的元素个数
  3. lindex mylist 1 #获取list中指定位置的元素
  4. lpush mylist 1 #添加一个或多个元素值list的头部
  5. rpush mylist 1 #添加一个或多个元素值list的尾部
  6. lpop mylist #从list中删除并返回第一个元素
  7. rpop mylist #从list中删除并返回最后一个元素

2.1.3 hash
  1. 介绍: hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 hash 做了更多优化。另外,hash 是⼀个 string 类型的 field 和 value 的映射表,特别适合⽤于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 ⽐如我们可以 hash 数据结构来存储⽤户信息,商品信息等等。
  2. 常⽤命令:hset,hmset,hexists,hget,hgetall,hkeys,hvals 等。
  3. 应⽤场景: 系统中对象数据的存储。
  1. hset sutdent name zhangsan
  2. hset student age 20
  3. hset student id 1
  4. hget student name

2.1.4 Set
  1. 介绍: set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是⼀种⽆序集合,集合中的元素没有先后顺序。当你需要存储⼀个列表数据,⼜不希望出现重复数据时,set 是⼀个很好的选择,并且 set 提供了判断某个成员是否在⼀个 set 集合内的重要接⼝,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
  2. 常用命令: sadd,spop,smembers,sismember,scard,sinterstore,sunion
  3. 应⽤场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
  1. sadd mySet 1 # 添加元素
  2. smembers mySet # 查看全部元素
  3. sismember mySet 3 # 判断是否包含某
  4. srem mySet 1 # 删除某个/些元素
  5. scard mySet # 查看元素个数
  6. spop mySet # 随机删除一个元素
  7. #-------操作多个set-------
  8. # 将一个set的元素移动到另外一个set
  9. smove yourSet mySet 2
  10. # 求两set的交集
  11. sinter yourSet mySet
  12. # 求两set的并集
  13. sunion yourSet mySet
  14. # 求在yourSet中而不在mySet中的元素
  15. sdiff yourSet mySet

2.1.5 Sorted Sets
  1. 介绍: 和 set 相⽐,sorted set 增加了⼀个权重参数 score,使得集合中的元素能够按 score进⾏有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap和 TreeSet 的结合体。
  2. 常⽤命令: zadd,zcard,zscore,zrange,zrevrange,zrem 等。
  3. 应⽤场景: 需要对数据根据某个权重进⾏排序的场景。⽐如在直播系统中,实时排⾏信息包含直播间在线⽤户列表,各种礼物排⾏榜,弹幕消息(可以理解为按消息维度的消息排⾏榜)等信息。
  1. zadd board 85 zhangsan
  2. zadd board 72 lisi
  3. zadd board 96 wangwu
  4. zadd board 63 zhaoliu
  5. # 获取排名前三的用户(默认是升序,所以需要 rev 改为降序)
  6. zrevrange board 0 3
  7. # 获取某用户的排名
  8. zrank board zhaoliu

2.2 Redis为什么不使用多线程?

虽然说Redis使用的是单线程,但是实际上,Redis在4.0版本后就已经加入了多线程的支持。不过,redis4.0增加的多线程主要是针对一些大键值对的删除操作命令,使用这些命令就会使用主处理之外的其它线程来操作。

大体上来说redis6.0之前还是单线程处理

为什么redis6.0要使用单线程?

  1. 1. 单线程编程容易并且更容易维护
  2. 2. redis的瓶颈不再CPU,主要在内存
  3. 3. 多线程就会存在死锁、线程上下⽂切换等问题,甚⾄会影响性能。

2.3 Redis过期策略

Redis 过期策略是:定期删除+惰性删除

定期删除:

所谓定期删除,指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查是否过期如果过期就删除。 这个每隔100ms不是遍历所有设置过期时间的key,实际上市随机抽取一些key来检查和删除的

惰性删除

  1. **就是在你获取某个key的时候,redis`检查一下这个key是否设置了过期时间以及是否过期了`,如果过期了就会删除,不会返回任何东西**

实际上这种还是有问题的,如果定期删除掉了很多过期的key,但是你又没有及时去查讯也就没走惰性删除。如果大量的key堆积在内存中,导致redis内存快耗尽了怎么办? 内存淘汰机制

2.3.1 内存淘汰机制
  • noeviction:当内存不足以容纳新数据时,新写入的操作会报错。 这个基本没人用
  • allkeys-lru:当内存不足以容纳新数据时,在键空间中,移除最新最少使用的key。 这个是常用的
  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key。一般没人用吧
  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key。 这个一般不太合适
  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除

2.4 Redis持久化

很多时候我们需要把内存中的数据持久化到磁盘中,大部分原因在于重启机器或机器故障数据恢复 。 reids支持两种不同的序列化操作。Redis 的⼀种持久化⽅式叫快照(snapshottingRDB),另⼀种⽅式是只追加文件(append-only file, AOF

RDB(snapshotting)持久化

RDB就是在指定时间间隔内,将内存中的数据集快照写入磁盘。redis重启时是将快照文件直接读取到内存中,来恢复数据。

汇总 - 图21

优点:

  1. RDB文件紧凑,全量备份,非常适合于备份和容灾恢复
  2. 生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任务io操作
  3. RDB在恢复大数据集时的速度比AOF的恢复速度要快

缺点:

  1. RDB这种方式不适合对数据完整性要求严格的情况,因为是每隔一段时间备份一次数据的。 可能会丢失数据

AOF(append-only file)持久化

AOF就是以日志记录的形式记录redis的每个写操作,将redis执行的所有写指令记录下来(读操作不记录),只许追加文件不可更改文件。redis启动后会读取appendonly.aof文件来实现重新恢复数据。

汇总 - 图22

  1. ## 开启AOF
  2. appendonly yes
  3. ## AOF三种持久化方式:
  4. appendfsync always:每修改同步,每一次发生数据变更都会持久化到磁盘上,性能较差,但数据完整性较好。
  5. appendfsync everysec: 每秒同步,每秒内记录操作,异步操作,如果一秒内宕机,有数据丢失。
  6. appendfsync no:不同步。

2.5 缓存穿透

缓存穿透简单来说就是大量请求的key根本不在缓存中,导致请求直接到了数据库上,根本没有经过缓存。容易将数据库打死。

汇总 - 图23

解决方式:

  1. 最简单的方式就是做好参数校验,一些不合法的信息直接抛出异常给客户端。比如查询的数据库id不能小于0、参数是否合法等等
  1. 缓存无效的key:每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN 。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
  2. 布隆过滤器

2.6 缓存雪崩

缓存雪崩简单来说就是 缓存在同一时间失效或者缓存意外宕机导致所有的请求直接打到数据库,导致数据库崩溃。

汇总 - 图24

处理方式

缓存雪崩的事前事中事后的解决方案如下:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
  • 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。