Java面试题库整理
2024-01-20 16:05:23 2 举报
AI智能生成
这是一份详尽的Java面试题库,涵盖了从基础知识到高级编程技巧的所有内容。这份题库旨在帮助Java开发者和求职者更好地准备面试,提升他们的专业技能。题库中的问题包括Java的基本语法、面向对象编程、异常处理、多线程编程、集合框架、JVM性能调优等主题。此外,还包含了一些关于Java 8的新特性和Spring框架的问题。这些问题都是经过精心挑选和设计的,既有理论知识的考察,也有实际编程能力的测试。通过回答这些问题,求职者可以全面地了解和掌握Java的知识体系,提高他们的面试成功率。
作者其他创作
大纲/内容
Java集合
1. 常见的集合有哪些?
Collection接口的字接口
Set:不能包含重复的元素
HashSet、ThreeSet、LinkedHashSet等
底层用map实现的,value是object
List:有序集合,可以包含重复的元素,提供索引访问方式
ArrayList、LinkedList、Stack、Vector
List 常用的有哪些类型?
ArrayList
基于数组实现的动态数组,支持快速随机访问和插入/删除元素。适用于大多数情况下的通用列表需求。
最小扩容+1,期望扩容到旧空间的2倍,最大容量Integer最大值-8,add判断容量不足的时候进行扩容
LinkedList
双向链表的实现,适用于频繁的插入和删除操作,但不适合随机访问。通常用于实现队列和栈。
Node结点,双向列表
Vector
与 ArrayList 类似,但是是线程安全的。然而,由于同步开销,在多线程情况下性能较低,通常不推荐使用。
Stack
继承自 Vector,用于实现栈数据结构。
CopyOnWriteArrayList
一个线程安全的 ArrayList 变体,适用于读多写少的场景。每次写操作都会创建一个新的副本,因此写操作的性能较低。
用关键字synchronized将代码块包起来
Immutable List
不可变的列表,例如 Collections.unmodifiableList 创建的列表,一旦创建后就不能再添加、删除或修改元素。通常用于确保数据不可变性。
Arrays.asList()/List.of(). 返回的是不可变列表
实现自定义 List:
可以根据自己的需求实现自定义的 List 类型,这可以通过实现 List 接口或继承现有的 List 实现类来完成。
queue:按照先进先出的规则来维护对象产生的顺序
Map接口
HashMap、Hashtable、LinkedHashMap、ConcurrentHashMap、ThreeMap等
不能包含重复的key,可以有重复的value
Iterator
所有集合类都继承了该接口,用于遍历并选择集合中元素的接口
迭代器:也是一种设计模式的概念。是一个对象,它的工作是遍历并选择序列中的对象,而不必关心序列底层结构。
迭代器:也是一种设计模式的概念。是一个对象,它的工作是遍历并选择序列中的对象,而不必关心序列底层结构。
主要方法
hasNext:是否有下一个元素
next:返回下一个元素
remove:删除当前元素
2. 哪些集合对元素可以随机访问?
实现了RandomAccess接口的集合都可以做到随机访问,该接口是一个标记接口,未定义任何方法
以下这些集合都实现了List接口,而List接口提供了对元素的随机访问。
以下这些集合都实现了List接口,而List接口提供了对元素的随机访问。
ArrayList:ArrayList是基于数组实现的动态数组。由于其内部数据结构是数组,因此可以通过索引快速访问元素。
Vector:Vector与ArrayList类似,也是基于数组的动态数组。不过Vector是线程安全的,支持同步,但因此可能性能稍差。 所有的public 方法加了synchronized 关键字
LinkedList:虽然LinkedList也可以通过索引访问元素,但它的访问效率较低,因为它是基于链表实现的,需要遍历链表来找到指定索引的元素。
3. Comparable 和 Comparator 接口的区别?
Comparable是一个接口,位于 java.lang 包中。
内部只有一个方法compareTo,该方法返回一个整数值。可以实现对类对象的比较,升序。
自然排序是对象本身所具有的默认排序规则。
当使用 Arrays.sort() 或 Collections.sort() 这样的排序方法时,它们会使用对象的 compareTo 方法进行排序。
内部只有一个方法compareTo,该方法返回一个整数值。可以实现对类对象的比较,升序。
自然排序是对象本身所具有的默认排序规则。
当使用 Arrays.sort() 或 Collections.sort() 这样的排序方法时,它们会使用对象的 compareTo 方法进行排序。
Comparator是一个接口,java.util包下的,在类外部定义排序规则。
传入各种自定义排序规则的 Comparator 实现类,对同样的类制定不同的排序策略。
Comparator 接口有一个 compare 方法,用于比较两个对象。
使用 Comparator 时,你需要在排序时显式指定要使用的比较器。
传入各种自定义排序规则的 Comparator 实现类,对同样的类制定不同的排序策略。
Comparator 接口有一个 compare 方法,用于比较两个对象。
使用 Comparator 时,你需要在排序时显式指定要使用的比较器。
总的来说,Comparable 用于为对象定义默认的自然排序,而 Comparator 用于在不修改类定义的情况下进行自定义排序。你可以根据需要选择使用哪种接口来实现对象的比较。
4. Collection 和 Collections 的区别?
Collection是一个接口,是各种集合类的父接口
Collections是一个工具类,包含有各种有关集合操作的静态方法,像synchronized方法提供了线程安全的集合,sort对集合进行排序
5. Enumeration 和 Iterator 接口的区别?
Enumeration速度是Iterator的2倍,同时占用更少的内存。
Iterator远远比Enumeration安全,因为其他线程不能够修改正在被iterator遍历的集合里面的对象。
Enumeration 位于 java.util 包中,定义在 java.util.Enumeration 接口中。
它只包含两个方法:hasMoreElements() 和 nextElement()。
Enumeration 是只读的,不支持对集合进行修改操作,只能用于遍历元素。
它只包含两个方法:hasMoreElements() 和 nextElement()。
Enumeration 是只读的,不支持对集合进行修改操作,只能用于遍历元素。
Iterator 位于 java.util 包中,定义在 java.util.Iterator 接口中。
它包含三个方法:hasNext()、next() 和 remove()(remove 方法用于从集合中删除当前元素,是可选操作)。
Iterator 是支持对集合进行修改操作的,可以通过 remove 方法删除元素。
它包含三个方法:hasNext()、next() 和 remove()(remove 方法用于从集合中删除当前元素,是可选操作)。
Iterator 是支持对集合进行修改操作的,可以通过 remove 方法删除元素。
6. 集合使用泛型有什么优点?
所有集合接口和实现都大量使用它,泛型允许我们为集合提供一个可以容纳对象类型。泛型可以使得代码更加简洁,不需要显示转化和instanceOf操作,也给运行时带来好处,不会产生类型检查的指令
7. Map为什么不继承Collection?
首先Map提供的是键值对映射(即Key和value的映射),而collection提供的是一组数据(并不是键值对映射)
map如果继承了collection接口的话还违反了面向对象的接口分离原则。
Map和List、set不同,Map放的是键值对,list、set放的是一个个的对象。说到底是因为数据结构不同,数据结构不同,操作就不一样,所以接口是分开的
接口分离原则:客户端不应该依赖它不需要的接口。
8. 线程安全的Map有哪些?
Collections里面提供了synchronizedMap
通过Collections工具类的static方法可以创建一个线程安全的Map,例如Collections.synchronizedMap(new HashMap<>())。
这种方式会对Map中的所有操作都加上同步锁,因此在多线程环境下会比较安全。
但是需要注意,这种方式可能会导致性能瓶颈,因为锁的粒度较大。
这种方式会对Map中的所有操作都加上同步锁,因此在多线程环境下会比较安全。
但是需要注意,这种方式可能会导致性能瓶颈,因为锁的粒度较大。
HashTable
虽然Hashtable在较新的Java版本中已经不太推荐使用,因为它的性能相对较低,但它是一个线程安全的Map实现。
在一些早期的Java应用中可能会看到Hashtable的使用。
在一些早期的Java应用中可能会看到Hashtable的使用。
ConcurrentHashMap
ConcurrentHashMap 是 Java 提供的线程安全的 Map 实现。它使用分段锁的机制,允许多个线程同时读取,而写入操作会锁住相关的段。
这提供了良好的并发性能,特别适用于高并发环境。
这提供了良好的并发性能,特别适用于高并发环境。
ConcurrentHashMap的子类
除了ConcurrentHashMap本身,Java还提供了一些其他线程安全的Map实现,如ConcurrentLinkedHashMap和ConcurrentSkipListMap。
这些实现在特定的使用场景下可能更有优势。
这些实现在特定的使用场景下可能更有优势。
10. 系统讲一下HashMap
简介
主要用来存放键值对,基于Map接口进行实现,非线程安全;可以存储null值的key,但有且只有一个
底层数据结构
1.8之前采用数组和链表的形式,链表主要是为了解决哈希冲突存在(拉链式解决冲突)
1.8之后,当链表长度大于阈值(并且桶数组长度需要大于64),将链表转成红黑树,减少搜索时间
关键参数
链表转红黑树
链表长度阈值:8
桶数组长度:64
转链表长度阈值:6
默认初始化大小:16
扩容倍数:2
加载因子:0.75
太大会导致查找元素效率低
太小会导致存放数据比较分散
modCount : 每次扩容或者更改map结构都会进行计数
最大容量: 1 << 30
构造方法
默认构造方法
包含另一个Map的构造函数
指定容量大小的构造方法
指定 容量大小 和 加载因子 的构造函数
在实际开发过程中是要指定大小的, 避免一致调用resize 方法
原理
hash原理
通过key的hashCode,经过扰动函数计算hash值,然后(n - 1) & hash判断元素存放位置
扰动函数(原理类似)
1.7 hash方法,扰动4次
h ^= (h >>> 20) ^ (h >>> 12);
h ^ (h >>> 7) ^ (h >>> 4);
h ^ (h >>> 7) ^ (h >>> 4);
1.8 hash方法,扰动一次
h ^ (h >>> 16)
put方法
1.8
对外提供put方法,对内调用的是putval方法
1. 如果定位到数组位置没有元素,直接插入
2. 如果定位到数组位置有元素
2.1 比较,如果key相同,直接覆盖
2.2 如果key不同,判断p是否树节点,是的话调用 e = ((TreeNode<K, v=V>)p).putTreeVal(this, hash, key, value)将元素添加进去
2.3 key不同,并且不是树结点,遍历链表进行尾部插入
注:插入后如果链表长度超过8会进行判断,首先判断同数组大小是否大于等于64,如果大于进行链表树化,否则同数组扩容
1.7
1. 定位到数组位没有元素,直接插入
2. 有元素,遍历以这个结点元素的头节点的链表,依次比较,如果Key相同直接覆盖。否则采用头插法插入元素
resize方法
扩容会伴随着重新hash分配,并且会遍历hash表内所有元素,非常耗时
1. 判断是否最大容量,最大容量就不再扩充
2. 没超过就扩充到原来的两倍
3. 将链表的结点进行rehash重新分配桶数组
为什么扩容要按照2倍进行扩容?
通过hash值定位桶的位置时,是通过 & 操作进行的,性能上会比 %、/ 快很多(网上说是快10倍左右),这样可以快速定位到具体的桶下标
要么在k要么在k+n,这样可以链长变短,如果其他倍数1.5,那这样的话可能会生成更长的链
HashSet
底层基于HashMap进行数据存储,Map的key就是HashSet存放的数据
有一个默认统一的Object对象,存放value,所以HashSet中其实就是HashMap的key,value统一为一个Object对象
11. HashMap和Hashtable的区别?
线程是否安全:HashMap 是非线程安全的,HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过synchronized 修饰。
因为线程安全的问题,HashMap 要比 HashTable 效率高一点。
HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException。
HashTable对null key 和 null value 都不支持/HashMap是支持的。
HashTable在put 的时候如果发现是null值,会直接抛出空指针异常。(value判断,key调用hashCode的时候会报错)
HashMap对null key 和 null value 做了特殊的处理。判断key为null的时候放到table[0] 里。
创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。
创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小
JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
12. 为什么HashMap线程不安全?
1.7 : 对线程对Map进行扩容。某个线程执行过程中,被挂起,其他线程已经完成数据迁移,等CPU资源释放后被挂起的线程重新执行之前的逻辑,数据已经被改变,造成死循环、数据丢失。头插法会导致死循环
1.8 : 对线程put操作。假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。
1.7 到 1.8 的改善:数据丢失、死循环已经在在JDK1.8中已经得到了很好的解决,如果去阅读1.8的源码会发现找不到HashMap#transfer(),因为JDK1.8直接在HashMap#resize()中完成了数据迁移。
体现
1.7 : 主要发生在扩容阶段: 死循环、数据丢失
1.8 : 主要发生在put阶段:数据覆盖
13. 讲一下ConcurrentHashMap为什么是线程安全的/底层实现?
1.7
介绍
分成多个Segment组合,每一个Segment代表相当于一个HashMap,有多少个Segment,就可以支持多少并发,默认16
初始化逻辑
1. 必要参数校验。
2. 校验并发级别 concurrencyLevel 大小,如果大于最大值,重置为最大值。无惨构造默认值是 16.
3. 寻找并发级别 concurrencyLevel 之上最近的 2 的幂次方值,作为初始化容量大小,默认是 16。
4. 记录 segmentShift 偏移量,这个值为【容量 = 2 的 N 次方】中的 N,在后面 Put 时计算位置时会用到。默认是 32 - sshift = 28.
5. 记录 segmentMask,默认是 ssize - 1 = 16 -1 = 15.
6. 初始化 segments [0],默认大小为 2,负载因子 0.75,扩容阀值是 2*0.75=1.5,插入第二个值时才会进行扩容。
put逻辑流程
1. 计算要 put 的 key 的位置,获取指定位置的 Segment。
2. 如果指定位置的 Segment 为空,则初始化这个 Segment.
2.1 检查计算得到的位置的 Segment 是否为 null.
2.2 为 null 继续初始化,使用 Segment [0] 的容量和负载因子创建一个 HashEntry 数组。
2.3 再次检查计算得到的指定位置的 Segment 是否为 null.
2.4 使用创建的 HashEntry 数组初始化这个 Segment.
2.5 自旋判断计算得到的指定位置的 Segment 是否为 null,使用 CAS 在这个位置赋值为 Segment.
3. Segment.put 插入 key,value 值。
1. tryLock () 获取锁,获取不到使用 scanAndLockForPut 方法继续获取。
scanAndLockForPut方法:不断的自旋 tryLock() 获取锁。当自旋次数大于指定次数时,使用 lock() 阻塞获取锁。
2. 计算 put 的数据要放入的 index 位置,然后获取这个位置上的 HashEntry 。
3. 遍历 put 新元素,为什么要遍历?因为这里获取的 HashEntry 可能是一个空元素,也可能是链表已存在,所以要区别对待。
3.1 如果这个位置上的 HashEntry 不存在:
3.1.1 如果当前容量大于扩容阀值,小于最大容量,进行扩容。
3.1.2 直接头插法插入。
3.2 如果这个位置上的 HashEntry 存在:
3.2.1 判断链表当前元素 Key 和 hash 值是否和要 put 的 key 和 hash 值一致。一致则替换值
3.2.2 不一致,获取链表下一个节点,直到发现相同进行值替换,或者链表表里完毕没有相同的。
如果当前容量大于扩容阀值,小于最大容量,进行扩容。
直接链表头插法插入。
4. 如果要插入的位置之前已经存在,替换后返回旧值,否则返回 null.
rehash
ConcurrentHashMap扩容会扩容到原来的两倍,老数组的数据移动到新数组时要么位置不变,要么index+oldSize,扩容之后使用链表头插法插入指定位置
get
计算得到key防止的位置
遍历指定位置查找相同key的value值
1.8
介绍
采用Node数组 + 链表 / 红黑树,当链表达到一定长度时,链表会转成红黑树
初始化流程
通过自旋 和 CAS 操作完成
sizeCtl代表当前初始化状态
-1 : 正在初始化中
-N : N-1个线程正在扩容
表示table初始化大小,如果table没有初始化
表示table容量,如果 table 已经初始化
put流程
1. 根据key计算hashcode
2. 判断是否需要进行初始化
3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
4. 如果当前位置的 hashcode == MOVED == -1, 则需要进行扩容。
5. 如果都不满足,则利用 synchronized 锁写入数据。
6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。
get流程
1. 根据 hash 值计算位置。
2. 查找到指定位置,如果头节点就是要找的,直接返回它的 value.
3. 如果头节点 hash 值小于 0 ,说明正在扩容或者是红黑树,查找之。
4. 如果是链表,遍历查找之。
使用final和volatile修饰有什么作用?
final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变形对象不需要同步就能自由地被访问和共享。
使用volatile来保证某个变量内存的改变对其他线程即时可见,在配合CAS可以实现不加锁对并发操作的支持。get操作可以无锁是由于Node的元素val和指针next是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
hashMap hashTable concurrentMap 之间的区别
线程安全
HashMap 不是线程安全的。在多线程环境中,对 HashMap 进行并发的读写操作可能导致不一致的结果。因此,HashMap 不适合在多线程环境下使用,除非采取额外的同步措施。
HashTable 是线程安全的。它的所有公共方法都被同步,因此可以安全地在多线程环境中使用。然而,由于同步的开销,HashTable 的性能在高度并发的情况下可能不如其他更高级的并发集合。
ConcurrentMap 是专门为多线程环境设计的集合,它提供了更高的并发性能。ConcurrentMap 的常见实现类包括 ConcurrentHashMap。它们使用了更精细的锁策略,以允许更多的并发操作,同时仍然保持线程安全性。
HashTable 是线程安全的。它的所有公共方法都被同步,因此可以安全地在多线程环境中使用。然而,由于同步的开销,HashTable 的性能在高度并发的情况下可能不如其他更高级的并发集合。
ConcurrentMap 是专门为多线程环境设计的集合,它提供了更高的并发性能。ConcurrentMap 的常见实现类包括 ConcurrentHashMap。它们使用了更精细的锁策略,以允许更多的并发操作,同时仍然保持线程安全性。
空键或空值
HashMap 允许空键(key)和空值(value)。
HashTable 不允许空键或空值。如果尝试在 HashTable 中放入空键或空值,将抛出 NullPointerException。
ConcurrentMap 的行为取决于具体的实现。通常,它们允许空键或空值,但也可以通过实现特定的 ConcurrentMap 接口来限制这一行为。
HashTable 不允许空键或空值。如果尝试在 HashTable 中放入空键或空值,将抛出 NullPointerException。
ConcurrentMap 的行为取决于具体的实现。通常,它们允许空键或空值,但也可以通过实现特定的 ConcurrentMap 接口来限制这一行为。
性能和并发性
HashMap 在非多线程环境中通常具有较好的性能,但不适合多线程环境。
HashTable 在多线程环境中也可用,但性能相对较低,因为它需要同步。
ConcurrentMap 是为高并发环境而设计的,提供了更好的性能。ConcurrentHashMap 具有较好的并发性能,可用于高度并发的应用。
HashTable 在多线程环境中也可用,但性能相对较低,因为它需要同步。
ConcurrentMap 是为高并发环境而设计的,提供了更好的性能。ConcurrentHashMap 具有较好的并发性能,可用于高度并发的应用。
迭代器
HashMap 的迭代器是快速失败的。这意味着如果在迭代过程中对 HashMap 进行结构性修改(如添加或删除元素),将抛出 ConcurrentModificationException。
HashTable 的迭代器不是快速失败的,允许在迭代期间对其进行修改。
ConcurrentMap 的迭代器通常是弱一致的,它们可能会看到更新之前或之后的数据,但不会抛出 ConcurrentModificationException。
HashTable 的迭代器不是快速失败的,允许在迭代期间对其进行修改。
ConcurrentMap 的迭代器通常是弱一致的,它们可能会看到更新之前或之后的数据,但不会抛出 ConcurrentModificationException。
14. 讲一下LinkedList?
LinkedList是实现了List接口和Deque接口的双端列表。其底层的链表结构使得它支持高效的插入和删除操作。同时实现了Deque接口,使得LinkedList具有队列的特性。队列不是线程安全的,如果需要变成线程安全的,可以调用静态类Collections中的synchronizedList方法
Collections中的synchronizedList方法 返回的是Collections中的子类,其实就是提供的方法都加上synchronized关键字
private static class Node<E> {
E item;//节点值
Node<E> next;//后继节点
Node<E> prev;//前驱节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
E item;//节点值
Node<E> next;//后继节点
Node<E> prev;//前驱节点
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
注:虽然说删除、插入操作高效,只是删除这个动作比较高效,前置查找定位也需要O(n)的复杂度,只不过LinkedList不需要检验扩容操作
15. 讲一下ArrayList?
扩容机制
构造函数
默认构造函数
1.6之后:赋值一个空数组,在对数据进行添加元素时,才真正的分配容量
1.6之前:默认直接创建一个容量为10的数组
带容量参数的构造函数:按照容量进行分配
带collection元素集合的构造函数,利用元素集合的迭代器按顺序返回
add方法
11之前
先调用 ensureCapacityInternal方法
再调用ensureExplicitCapacity方法
再调用grow方法
11之后
去除了ensureCapacityInternal和ensureExplicitCapacity 方法
判断长度是否等于数组长度,如果超过了就进行扩容grow方法,否则就直接赋值
hugeCapacity方法
传入参数和最大容量比较,大于最大容量,新容量为Integer.MAX_VALUE,否则新容量为Integer.MAX_VALUE - 8
System.arraycopy 方法
native方法
浅拷贝,基本数据类型是值拷贝,对象是引用拷贝
Arrays.copyOf方法
新建一个数组,然后层也是调用System.arraycopy 方法进行拷贝
外部调用方法 ensureCapacity
在大量add元素之前使用该方法,以减少扩容次数
关键参数
默认长度
10
扩容倍数
1.5
思考
ArrayList在扩容的时候会创建一个新的数组,会消耗一定的性能,所以如果可以确认大致大小,在使用时进行初始化大小。或者调用ensureCapacity方法进行设置
16. ArrayList和LinkedList的比较
ArrayList
底层使用Object[]存储,线程不安全
插入和删除的平均复杂度为O(n)
支持快速随机访问
其空间浪费主要在list列表的结尾会预留一定的容量空间
LinkedList
线程不安全
底层采用双向链表数据结构,jdk1.6之前是循环列表,jdk1.7取消了循环列表
不支持随机访问,头插(删)/尾插(删) 复杂度是O(1),但是指定位置操作是 O(n) ,因为需要遍历到目标位置
空间花费在每个元素都需要消耗比ArrayLisst更多的空间。(存储前驱和后继节点)
17. ArrayList和Vector的比较
Vector : List古老的实现类,底层使用Object[]存储,线程安全
实现线程安全是将一些操作方法都加上synchronized
ArrayList:底层使用Object[]存储,线程不安全
18. 线程安全的List有哪些?
Collections.SynchronizedList
提供了一些方法,采用synchronized同步代码块的方式进行线程安全
CopyOnWriteArrayList
写时复制:再添加元素的时候,先把原List列表复制一份,再添加新的元素
使用了ReentrantLock
引伸到和synchronized 的区别
新版本,使用的是synchronized关键字
在高并发读操作的时候,不用加锁,提升了性能,但是会读到旧数据
CopyOnWriteArraySet
就是使用 CopyOnWriteArrayList 的 addIfAbsent 方法来去重的,添加元素的时候判断对象是否已经存在,不存在才添加进集合。
19. 什么是 fail-safe/fail-fast?
fail-fast
当我们在遍历集合元素的时候,经常会使用迭代器,但在迭代器遍历元素的过程中,如果集合的结构被改变的话,就会抛出异常,防止继续遍历。这就是所谓的快速失败机制。
HashMap:当Iterator这个迭代器被创建后,除了迭代器本身的方法(remove)可以改变集合的结构外,其他的因素如若改变了集合的结构,都被抛出ConcurrentModificationException异常。
原理
从源码我们可以发现,迭代器在执行next()等方法的时候,都会调用checkForComodification()这个方法,查看modCount==expectedModCount?如果相等则抛出异常。
expectedModcount:这个值在对象被创建的时候就被赋予了一个固定的值modCount。也就是说这个值是不变的。也就是说,如果在迭代器遍历元素的时候,如果modCount这个值发生了改变,那么再次遍历时就会抛出异常。
modCount 在我们对集合元素个数做出改变的时候,modCount就会被修改
fail-safe
当集合的结构被改变的时候,fail-safe机制会在复制原集合的一份数据出来,然后在复制的那份数据遍历。(CopyOnWrite采用了该思想)
缺点
复制时需要额外的空间和时间上的开销。
不能保证遍历的是最新内容。
20. 怎么确保一个集合不能被修改?
采用Collections包下的unmodifiable集合
Arrays.asList()方法返回的也是一个不能修改的集合
Collections.unmodifiableList(List)
Collections.unmodifiableSet(Set)
Collections.unmodifiableMap(Map)
扩展:缓存算法
FIFO算法
FIFO 算法是一种比较容易实现的算法。它的思想是先进先出(FIFO,队列),这是最简单、最公平的一种思想,即如果一个数据是最先进入的,那么可以认为在将来它被访问的可能性很小。空间满的时候,最先进入的数据会被最早置换(淘汰)掉。
LRU算法
linkedhashmap
LFU算法
哈希列表+最小堆
21.LinkedHashMap(LRU实现)
重写removeEldestEntry方法,当长度大于容量时,将最少访问的结点删除掉
accessOrder 是否按照访问顺序排序,默认是false,需要显示打开
afterNodeAccess
在get和put(存在)之后将结点放到最后面
afterNodeInsertion
如果removeEldestEntry返回true,删除头节点
afterNodeRemoval
删除节点后维护双向链表的前后顺序
底层实现详解
没有具体要求,也可以是String
JVM
1. Java 为什么能一次编写,处处运行?
Java虚拟机屏蔽了不同操作系统的差异,面向开发者提供了一致语言语法层次
不同的操作系统,Java虚拟机实现的方式可能不同
2. 讲一下JVM内存区域
线程私有
虚拟机栈
线程私有的,生命周期和线程相同
每个方法被执行的时候,Java虚拟机会同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、方法出口等信息。
每个方法被调用到执行完毕的过程对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
运行时栈帧(结构)
概述
栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息,每个方法的调用从开始到执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程
在编译Java程序源码的时候,栈帧中需要多大的局部变量表,需要多深的操作数栈已经被分析计算出来,并且写入到方法表的Code属性中。一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式。
一个线程中的方法调用链可能会很长,以Java程序角度看,同一时刻、同一条线程里面,调用堆栈的所有方法都同时处于执行状态,对于执行引擎来说,活动线程中,只有位于栈顶的栈帧才是生效的,被称为“当前栈帧”,与栈帧关联的方法被称为“当前方法”
局部变量表
一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量
以变量槽为最小单位,每个变量槽都应该能存放一个 boolean、byte、char、short、int、float、reference或returnAddress。这八种数据类型都可以使用32位或更小的物理空间来存储,允许变量槽长度随着处理器、操作系统或虚拟机实现的不同发生变化。
reference类型 表示对一个对象实例的引用。可以通过这个引用做到两件事
根据引用直接或者间接地查找到对象在Java堆中地数据存放地起始地址或者索引
根据引用直接或者间接查找到对象所属数据类型在方法区中存储的类型信息。
returnAddress目前很少见了,曾经为字节码指令 jsr、jsr_w和ret服务,一些古老的虚拟机曾经使用这些指令实现异常处理跳转
对于64为数据类型 long、double 会以高对齐地方式分配两个连续的变量槽空间
操作栈
后入先出(LIFO)的栈。和局部变量表一样,操作数栈最大深度在编译堵塞时候被写入到Code属性的max_stacks。操作数栈的每个元素都是可以包括long和double在内的任意Java数据类型。32位占容量1,64位占容量2
动态链接
每个栈帧都包含一个指向运行时常量池中栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
常量池中存在有大量的符号引用,字节码中的方法调用指令以常量池里指向方法的符号引用作为参数。
这些符号引用一部分会在类加载阶段或者第一次使用的时候呗转化位直接引用,这种转化被称为静态解析。
另外一部分将在每次运行期间都转化位直接引用,这部分称为动态连接
返回地址
当一个方法开始执行后,只有两种方式推出方法。第一种是 执行引擎遇到任意一个方法返回的字节码指令,这个时候可能会有返回值传递给上层的方法调用者。方法是否有返回值以及返回值的类型将根据遇到何种方法指令来决定,退出的方式称为“正常调用完成”
如果遇到异常,并且该异常没有在任何方法体内得到处理,会导致方法退出,这种方式退出称为“异常调用完成”。一场完成出口的方式退出不会给它的上层调用者提供任何返回值
附加信息
本地方法栈
和虚拟机栈类似,不过都是本地方法
程序计数器
记录当前线程所执行的字节码行号指示器。字节码解释器工作时是通过改变这个计数器的值来选取下一条需要执行的字节码指令,是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成
同一确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能够恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响独立存储
此区域是唯一一个在《Java虚拟机规范》中,没有规定任何OOME的区域
线程共享
方法区
各个线程共享地内存区域,用于存储已被虚拟机加载地类型信息,常量、静态变量、即时编译器编译后地代码缓存等数据
1.7 以前可以称之为 永久代,1.8之后 可以称为元空间,方法区的概念属于逻辑上的概念
运行时常量池
是方法区地一部分,用于存放编译器生成地各种字面常量与符号引用,将在类加载后存放到方法区的运行时常量池中
具备动态性,除了Class文件的常量表以外
逻辑上是方法区的一部分,物理上还是存放在Java堆里面
1.8 的实现是:静态变量、常量池 都放在了堆里面。
堆
是虚拟机管理内存最大的一块,被所有线程共享的一块内存区域,在虚拟机创建的时候。唯一目的是存放对象实例,几乎所有的对象实例都在这里分配
从回收角度看,现代垃圾回收器基于分代理论设计的。将Java堆细分的目的是为了更好地回收内存,更快地分配内存
所以分为新生代 和 老年代。 再细致一点:eden空间 from survivor 空间等
所以分为新生代 和 老年代。 再细致一点:eden空间 from survivor 空间等
从内存分配的角度来看,线程共享的java堆中可能分出多个线程私有的分配缓冲区
可以是固定大小的也可以是可扩展的,如果没有内存完成实例地分配,并且堆也无法扩展时,Java虚拟机将会抛出OOME异常
jvm内存为什么要分成新生代,老年代,持久代。新生代中为什么要分eden和survivor
1) 共享内存区划分
共享内存区 = 持久带 + 堆
持久带 = 方法区 + 其他
Java 堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
共享内存区 = 持久带 + 堆
持久带 = 方法区 + 其他
Java 堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
2) 一些参数的配置
默认,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 – XX:NewRatio 配置。
默认,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定)
Survivor 区中✁对象被复制次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold)
默认,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ,可以通过参数 – XX:NewRatio 配置。
默认,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定)
Survivor 区中✁对象被复制次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold)
3)为什么要分为 Eden 和 Survivor?为什么要设置两个 Survivor
区?
如果没有 Survivor,Eden 区每进行一次 Minor GC,存活的对象就会被送到老
年代。老年代很快被填满,触发 Major GC.老年代的内存空间远大于新生代,进
行一次 Full GC 的耗时间比 Minor GC 长得多,所以需要分为 Eden 和 Survivor。
Survivor 的存在意义,就减少被送到老年代对象,进而减少 Full GC发生,
Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活对象,
才会被送到老年代。
设置两个 Survivor 区最大的好处就解决了碎片化,刚刚新对象在 Eden 中,
经历一次 Minor GC,Eden 中✁存活对象就会被移动到第一块 survivor space
S0,Eden 被清空;等 Eden 区再满了,就再触发一次 Minor GC,Eden 和 S0
中
存活对象又会被复制送入第二块 survivor space S1(这个过程非常重要,因为
这种复制算法保证了 S1 中来自S0 和 Eden 两部分存活对象占用连续✁内存空
间,避免了碎片化的发生)
区?
如果没有 Survivor,Eden 区每进行一次 Minor GC,存活的对象就会被送到老
年代。老年代很快被填满,触发 Major GC.老年代的内存空间远大于新生代,进
行一次 Full GC 的耗时间比 Minor GC 长得多,所以需要分为 Eden 和 Survivor。
Survivor 的存在意义,就减少被送到老年代对象,进而减少 Full GC发生,
Survivor 的预筛选保证,只有经历 16 次 Minor GC 还能在新生代中存活对象,
才会被送到老年代。
设置两个 Survivor 区最大的好处就解决了碎片化,刚刚新对象在 Eden 中,
经历一次 Minor GC,Eden 中✁存活对象就会被移动到第一块 survivor space
S0,Eden 被清空;等 Eden 区再满了,就再触发一次 Minor GC,Eden 和 S0
中
存活对象又会被复制送入第二块 survivor space S1(这个过程非常重要,因为
这种复制算法保证了 S1 中来自S0 和 Eden 两部分存活对象占用连续✁内存空
间,避免了碎片化的发生)
3. 对象都是在堆中分配的吗?
不一定,在满足特定条件下,可以在栈上分配内存。
虚拟机栈一般是用来存储基本类数据类型、引用和返回地址的。
Java JIT编译器有两项优化,分别是 逃逸分析 和 标量替换。这样可以使得Java在栈上去分配对象
逃逸分析
Java Hotspot 虚拟机可以分析新创对象的使用范围,并且决定是否在Java堆上进行分配的一项技术。该技术在Java SE6u23+之后默认设置为启用,不需要额外加这个参数
JVM参数
开启逃逸分析:-XX:+DoEscapeAnalysis
关闭逃逸分析:-XX:-DoEscapeAnalysis
显示分析结果:-XX:+PrintEscapeAnalysis
对象逃逸状态
全局逃逸
一个对象作用范围逃出了当前方法或者线程。
对象是一个静态变量
对象是一个已经发生逃逸的对象
对象作为当前方法的返回值
参数逃逸
一个对象被作为方法参数传递或者被参数引用,但是在调用过程中不会发生全局逃逸,这个状态是通过被调用方法的字节码确定的
没有逃逸
方法中的对象没有发生逃逸
当一个对象没有逃逸时,可以得到虚拟机的优化
锁消除
当编译器确定当前对象只有当前线程在使用,那么会移除对象的同步锁
锁消除的JVM参数
开启锁消除:-XX:+EliminateLocks
关闭锁消除:-XX:-EliminateLocks
锁消除在 JDK8 中都是默认开启的,并且锁消除都要建立在逃逸分析的基础上。
标量替换
标量:基础类型和对象的引用可以理解为标量,不能被进一步分解;聚合量:能被进一步分解的量(对象)
标量替换:对象是聚合量,它又可以被进一步分解成标量,将其成员变量分解为分散的变量;
如果一个对象没有发生逃逸,就不需要创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,同时也提升了应用程序性能。
JVM参数
开启标量替换:-XX:+EliminateAllocations
关闭标量替换:-XX:-EliminateAllocations
显示标量替换详情:-XX:+PrintEliminateAllocations
标量替换同样在 JDK8 中都是默认开启的,并且都要建立在逃逸分析的基础上。
栈上分配
当对象没有发生逃逸时,该对象就可以通过标量替换分解成成员标量分配在栈内存中,和方法的生命周期一致,随着栈帧出栈时销毁,减少了 GC 压力,提高了应用程序性能。
在平时开发过程中就要可尽可能的控制变量的作用范围了,变量范围越小越好,让虚拟机尽可能有优化的空间。
4. 你怎么理解 强、软、弱、虚引用?
引用回收时间与用途
强引用
程序代码中普遍存在的引用赋值,只要强引用关系在,垃圾回收器永远不会回收掉被引用的对象
软引用
描述一些还有用,但是非必须的对象,只是被软引用关联着的对象,系统将要发生内存溢出异常前会进行回收
弱引用
描述一些非必须的对象,比软引用还要弱一些,只能保存到下一次垃圾回收。
ThreadLocal 的实现
虚引用
最弱的一种引用关系,设置虚引用的唯一目的是为了能在这个对象被收集器回收时,收到一个系统通知
5. 常用的JVM参数有哪些?
堆设置
-Xms 初始堆大小,ms是memory start的简称 ,等价于-XX:InitialHeapSize
-Xmx 最大堆大小,mx是memory max的简称 ,等价于参数-XX:MaxHeapSize
在通常情况下,服务器项目在运行过程中,堆空间会不断的收缩与扩张,势必会造成不必要的系统压力。所以在生产环境中,JVM的Xms和Xmx要设置成一样的,能够避免GC在调整堆大小带来的不必要的压力。
-XX:NewSize=n 设置年轻代大小
-XX:NewRatio=n 设置年轻代和年老代的比值。如:-XX:NewRatio=3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4,默认新生代和老年代的比例=1:2。
-XX:SurvivorRatio=n 年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个,默认是8,表示Eden:S0:S1=8:1:1 ( -XX:SurvivorRatio=3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5)
-XX:MaxPermSize=n 设置持久代大小
-XX:MetaspaceSize 设置元空间大小
收集器设置
-XX:+UseSerialGC 设置串行收集器
-XX:+UseParallelGC 设置并行收集器
-XX:+UseParalledlOldGC 设置并行年老代收集器
-XX:+UseConcMarkSweepGC 设置并发收集器
-XX:+UseG1GC 设置G1收集器
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filenameGC日志输出到文件里filename,比如:-Xloggc:/gc.log
并行收集器设置
-XX:ParallelGCThreads=n 设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n 设置并行收集最大暂停时间
-XX:GCTimeRatio=n 设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
-XX:MaxGCPauseMillis=n设置并行收集最大暂停时间
并发收集器设置
-XX:+CMSIncrementalMode 设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n 设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
6. Java 8 中内存结构有什么变化?
HotSpot上面 1.7和其 以前 方法区和堆在物理内存上是连续的,方法区也被称之为永久代。
在1.7 的时候,方法区中的部分数据已经开始转移到堆/本地内存里面了,比如符号引用转移到本地内存;类的静态变量和字符串常量池转移到堆里;
1.8 之后取消了永久代的概念 转成元数据区,元数据区和本地内存一样大,但是其实同样可以去配置JVM这个参数
7. 了解类加载的阶段吗?
加载
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口
加载阶段结束后,Java虚拟机外部的二进制字节流按照虚拟机所规定的格式存储在方法区中了
链接
验证
验证是连接阶段的第一步,目的是保证Class文件的字节流中包含的信息符合全部约束要求。
文件格式验证
检查是否符合Class文件格式的规范
元数据校验
对字节码描述的信息进行语义分析,保证描述的信息符合《Java语言规范》的要求
字节码验证
目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
符号引用验证
对类自身外的各类信息进行匹配性校验
准备
正式为类中定义的变量来分配内存并设置类变量初始值的阶段
解析
Java虚拟机将常量池内的符号引用替换为直接引用的过程
类和接口的解析
字段解析
方法解析
接口方法解析
初始化
初始化阶段是类加载过程的最后一步,在类加载过程中,除了加载阶段用户应用程序可以通过自定义类加载器的方式局部参与之外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类种编写的Java程序代码,将主导权交给应用程序
准备阶段时,变量已经赋值过一次系统要求的初始零值,在初始化阶段,会根据程序编码制定的主观计划去初始化变量和其他资源
使用
使用
卸载
卸载
8. 类加载器是什么?
类加载阶段中“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码称为“类加载器”(Class Loader)
虽然只用于实现类加载的动作,但是Java中需中起到作用远超过类加载阶段。
对于任意一个类,必须由加载它的类加载器和它本身一起共同确立其在Java虚拟机中的唯一性。对于每个类加载器,都有独立的类名称空间。比较两个类是否相等,只有在这两个类是同一个类加载器的前提下才有意义
9. 什么是双亲委派?
分类
启动类加载器
使用C++实现,是虚拟自身的一部分
其他所有的类加载器
全部继承自抽象类java.lang.ClassLoader
启动类加载器
负责加载存放在<JAVA_HOME>\lib目录,或者被 -Xbootclasspath参数所指定的路径中存放的,是Java虚拟机能够识别的(rt.jar,tools.jar)
扩展类加载器
这个类是在类sun.misc.Launcher$ExtClassLoader中以Java代码的形式实现,负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库
Java9之后这种扩展机制,被模块化带来的天然扩展能力所取代
应用程序类加载器
由sun.misc.Launcher$AppClassLoader实现。这个类加载器是ClassLoader类中的getSystemClassLoader方法的返回值,所以有些场合称为“系统类加载器”,负责加载用户类路径上所有的类库。如果应用程序中没有自定义过自己的类加载器,一般情况下使用该默认类加载器
工作过程
如果一个类加载器收到了类加载的请求,不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个类加载请求时,子加载器才会尝试自己去完成加载
这样可以保证类的唯一性
比如Object类,放在rt中的,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器,这样可以保证Object类在程序的各种类加载器环境中保持时同一个类的
双亲委派实现
10. 为什么要打破双亲委派?
双亲委派机制很好地解决了各个类加载器协作时基础类型的一致性问题,但是如果有基础类型又要调用回用户的代码,这个时候就会破坏双亲委派机制
线程上下文类加载器,通过Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,会从父线程中继承一个。如果应用程序的全局范围内没有设置过的话,默认的就是应用程序类加载器。
有了线程上下文类加载器,程序可以做一些“舞弊”的事请,比如JNDI可以使用线程上下文类加载器去加载SPI服务代码。这是一种父类加载器去请求字类加载器完成类加载的行为。
涉及SPI的加载基本上都采用这种模式完成,JNDI、JDBC等。当SPI服务多于一个的时候,JDK6提供了ServiceLoader类,以MERA-INF/services中的配置信息,辅以责任链模式,才算是给SPI加载提供了相对合理的方案
模块化系统(JDK9后的类加载器委派关系)
Java9以后,引入了Java模块化系统是对Java技术的一次重要升级,Java虚拟机对类加载的架构也做了相应的调整
扩展类加载器被平台类加载器替代。随着整个JDK基于模块化进行构建(原来的rt.jar和tools.jar被拆分成数十个JMOD文件)
Java类库已经天然满足可扩展的需求,无需再保留<JAVA_HOME>\lin\ext 目录,所以这部分的扩展类加载器完成了它的使命。
当平台及应用程序类加载器请求,再委派给父加载器前,要先判断该类是否能够归属到某个系统模块中,如果可以找到这样的归属,就要优先委派给负责那个模块的加载器完成加载(第四次破坏双亲委派)
11. 什么是JVM 内存模型?
介绍
JMM:Java虚拟机规范定义一种模型来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各个平台下都可以达到一致的内存访问结果。
为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序
java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。
Java语言对三个特性如何提供保证
原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。
synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性
Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。
有序性
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
可以通过volatile关键字来保证一定的“有序性”
通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码
Java内存模型具备一些先天的“有序性”(happens-before原则)
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
一段程序代码的执行在单个线程中看起来是有序的。只能保证程序在单线程上执行起来是有序的。无法保证多线程的执行争取性
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
并发产生读写时,写优先于读
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
深入理解JVM虚拟机描述
每个Java线程都有自己的工作内存,工作内存中保存了被该线程使用的变量的主内存副本。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。不同线程之间也不能直接访问对方工作内存中的变量
12. 什么是指令重排?
JVM可能会对我们写的语句进行指令重排,处理器为了提高程序运行的效率,对代码进行优化,不保证各个语句的执行顺序一致,但是可以在单线程中保证最终的执行结果一致
volatile等 可以做到禁止指令重排(具体的原理是内存屏障)
13. 什么是内存屏障?
每个CPU都有自己的缓存,缓存的目的是为了提高性能,避免每次都要向内存存取,存在一定的弊端:不能实时和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同
volatile 关键字修饰的变量可以解决上述问题,通过内存屏障的方式,不同的硬件平台实现内存屏障的手段是不一致的,但是Java通过屏蔽这些差异,统一由JVM来生成内存屏障指令。
硬件层面的内存屏障
Load Barrier 读屏障
在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制重新从主内存加载数据;
Store Barrier 写屏障
在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
Java层面的内存屏障
LoadLoad
对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore
对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore
对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad
对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
volatile语义中的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态
在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。所以可以有可见性和有序性,但是不能保证原子性
final语义中的内存屏障
新建对象过程中,构造体中对final域的初始化写入和这个对象赋值给其他引用变量,这两个操作不能重排序;
初次读包含final域的对象引用和读取这个final域,这两个操作不能重排序;(晦涩,意思就是先赋值引用,再调用final值)
写final域:在编译器写final域完毕,构造体结束之前,会插入一个StoreStore屏障,保证前面的对final写入对其他线程/CPU可见,并阻止重排序。
读final域:在上述规则2中,两步操作不能重排序的机理就是在读final域前插入了LoadLoad屏障。
14. Happens-Before原则?
Java内存模型具备一些先天的“有序性”(happens-before原则)
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
一段程序代码的执行在单个线程中看起来是有序的。只能保证程序在单线程上执行起来是有序的。无法保证多线程的执行争取性
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
并发产生读写时,写优先于读
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
15. Minor GC 和 Full GC 是什么?
minor GC
新生代(新生代分为一个 Eden区和两个Survivor区)的垃圾收集叫做 Minor GC
新生代共有 两个 Survivor区,我们分别用 from 和 to来指代,当发生 Minor GC时,Eden 区和 from 指向的 Survivor 区中的存活对象会被复制(此处采用标记 - 复制算法)到 to 指向的 Survivor区中,然后交换 from 和 to指针,以保证下一次 Minor GC时,to 指向的 Survivor区还是空的。
Survivor区对象晋升位老年代对象的条件
Java虚拟机会记录 Survivor区中的对象一共被来回复制了几次。如果一个对象被复制的次数为 15 (对应虚拟机参数 -XX:+MaxTenuringThreshold),那么该对象将被晋升为至老年代,(至于为什么是 15次,原因是 HotSpot会在对象头的中的标记字段里记录年龄,分配到的空间只有4位,所以最多只能记录到15)。另外,如果单个 Survivor 区已经被占用了 50% (对应虚拟机参数: -XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。
在Minor GC过程中,Survivor 可能不足以容纳Eden和另一个Survivor中的存活对象。如果Survivor中的存活对象溢出,多余的对象将被移到老年代,这称为过早提升(Premature Promotion),这会导致老年代中短期存活对象的增长,可能会引发严重的性能问题。再进一步说,在Minor GC过程中,如果老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行Full GC,这将导致遍历整个Java堆,这称为提升失败(Promotion Failure)。
卡表
Minor GC存在一个问题就是,老年代的对象可能引用新生代的对象,在标记存活对象的时候,就需要扫描老年代的对象,如果该对象拥有对新生代对象的引用,那么这个引用也会被作为 GC Roots。这相当于就做了全堆扫描。
卡表的具体策略是将老年代的空间分成大小为 512B的若干张卡,并且维护一个卡表,卡表本省是字节数组,数组中的每个元素对应着一张卡,其实就是一个标识位,这个标识位代表对应的卡是否可能存有指向新生代对象的引用,如果可能存在,那么我们认为这张卡是脏的,即脏卡。
在进行Minor GC的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的老年代指向新生代的引用加入到 Minor GC的GC Roots里,当完成所有脏卡的扫描之后,Java 虚拟机便会将所有脏卡的标识位清零。这样虚拟机以空间换时间,避免了全表扫描
Full GC
收集整个堆,包括 新生代,老年代,永久代(在 JDK 1.8及以后,永久代被移除,换为metaspace 元空间)等所有部分的模式
当准备要触发一次 young GC时,如果发现统计数据说之前 young GC的平均晋升大小比目前的 old gen剩余的空间大,则不会触发young GC而是转为触发 full GC
如果有永久代(perm gen),要在永久代分配空间但已经没有足够空间时,也要触发一次 full GC
System.gc(),heap dump带GC,其默认都是触发 full GC.
jvm中一次完整的GC流程是怎么样的,对象如何到老年代
Java 堆 = 老年代 + 新生代
新生代 = Eden + S0 + S1
当 Eden 区的空间满了, Java 虚拟机会触发一次 Minor GC,以收集新生代的
垃圾,存活下来的对象,则会转移到 Survivor 区。
大对象(需要大量连续内存空间的 Java 对象,如那种很长的字符串)直接进入老
年态;
如果对象在 Eden 出生,并经过第一次 Minor GC 后仍然存活,并且被 Survivor
容纳的话,年龄设为 1,每熬过一次 Minor GC,年龄+1,若年龄超过一定限制
(15),则被晋升到老年态。即长期存活的对象进入老年态。
老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行 Full GC,
FullGC 清理整个内存堆——包括年轻代和年老代。
Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次 Minor GC,
比 Minor GC 慢 10 倍以上。
新生代 = Eden + S0 + S1
当 Eden 区的空间满了, Java 虚拟机会触发一次 Minor GC,以收集新生代的
垃圾,存活下来的对象,则会转移到 Survivor 区。
大对象(需要大量连续内存空间的 Java 对象,如那种很长的字符串)直接进入老
年态;
如果对象在 Eden 出生,并经过第一次 Minor GC 后仍然存活,并且被 Survivor
容纳的话,年龄设为 1,每熬过一次 Minor GC,年龄+1,若年龄超过一定限制
(15),则被晋升到老年态。即长期存活的对象进入老年态。
老年代满了而无法容纳更多的对象,Minor GC 之后通常就会进行 Full GC,
FullGC 清理整个内存堆——包括年轻代和年老代。
Major GC 发生在老年代的GC,清理老年区,经常会伴随至少一次 Minor GC,
比 Minor GC 慢 10 倍以上。
16. JVM如何判断一个对象可以被回收?
引用计数算法
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加一,当引用失效时,计数值减一。计数为0的对象是不可再被使用的。
循环引用可能导致算法无法判断对象是否存活
可达性分析
通过一系列“GC Roots”的根对象作为起始节点集,从这些结点开始,根据引用关系向下搜索,搜索过程走过的路径称为“引用链”。如果某个对象到GC Roots之间没有任何引用链,标识对象不可达
GC Roots对象
虚拟机栈中引用的对象
参数
局部变量
临时变量
方法区中静态属性引用的对象
Java类中引用类型静态变量
方法区中常量引用对象
字符串常量池里的引用
本地方法栈中native方法引用的对象
Java虚拟机内部引用
基本数据类型对应的Class对象
常驻异常对象(NullPointException、OutOfMemoryError),系统类加载器
所有被同步锁持有的对象
17. 常见的垃圾回收器有哪些?
Serial收集器
历史最悠久的收集器,HotSpot虚拟机新生带收集器唯一选择,单线程工作的收集器
在线程工作时,必须暂停其他所有工作线程,直到收集结束
新生代采用标记复制算法,老年代采用标记整理算法
Serial Old 收集器
Serial收集器 的老年代版本
ParNew收集器
实际上时Serial收集器的多线程并行版本,除了同时适用多条线程进行垃圾收集之外,其余行为都和Serial收集器完全一致
支持并行收集,可以和CMS收集器配合工作
新生代采用标记复制算法,老年代采用标记整理算法
Parallel Scavenge 收集器
新生带收集器,标记-复制 算法实现,可以并行收集的多线程收集器
目的时达到可以控制的吞吐量
两个重要参数
吞吐量配置(0-100)整数
最大收集时间,导致吞吐量变小
Parallel Old 收集器
Parallel 收集器的老年代版本
CMS收集器 🌟
一种以获取最短回收的短时间为目标的收集器,基于标记清除算法实现
步骤
初始标记
需要"STOP THE WORLD",标记GC ROOTS 可以直接关联的对象,速度很快
并发标记
从GC ROOTS 直接关联对象开始遍历整个对象图的过程,不需要暂停用户线程,可以与垃圾收集线程一起并发运行
重新标记
修正并发标记期间,用户线程继续运作而导致标记产生变动的一部分对象标记记录
并发清除
清理删除标记阶段判断的已死亡对象,不许要移动存活对象,可以与用户线程一起工作
缺点
会占用一部分处理器资源。默认启动的线程数(处理器数量+3)/4,所以只有大于4核以上时,占用资源不到25%,如果小于4个处理器,对用户线程影响比较大
JDK5以下,启动的阈值时68%,JDK6开始,启动阈值92%,但是可能会有无法满足分配新对象的需求
会产生大量的内存碎片,当无法找到足够大的供键来分配时,会触发一次FullGC
有两个参数可以调整
+UseCMS-CompactAtFullCollection
进行FullGC时开启内存碎片合并整理过程
+UseCMS-CompactAtFullCollection
要求CMS在执行若干次不整理空间的Full GC之后进行碎片整理
G1收集器 🌟
开创了收集器面向局部收集的涉及思路和基于Region的内存布局形式。一款主要面向服务端应用的垃圾收集器
面向堆内存任何部分来组成回收器,衡量标准不是属于哪个分代,而是哪块内存中存放垃圾数量多,回收收益最大,Mixed GC模式
把Java堆划分成多个大小相等的独立区间(Region),每个区间都可以扮演Eden、Survivor空间或者老年代空间
超过一个区间大小的对象会存放在N个连续的Region中,大多数行为把Humongous Region 作为老年代的一部分
维护一个优先级列表,根据垃圾堆的价值大小来确认。
每个Region维护自己的记忆集,会记录下别的Region指向自己的指针
步骤
初始标记
标记GC Roots
并发标记
标记GC Roots可以到达的所有对象
最终标记
并发标记中,可能存在的新垃圾
筛选回收
更新Region统计数据,对各个Region回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,自由选择多个Region构成回收集,把决定回收的部分Region存活对象复制到空的Region中,再清除旧的全部空间。需要暂停用户线程
参数
-XX:G1HeapRegionSize
1MB~32MB,2的N次幂
18.常见的垃圾回收算法有哪些?
标记清除
首先标记除所有需要回收的对象,标记完之后,统一回收所有未被标记的对象
缺陷
执行效率不稳定,如果Java堆中包含大量对象,大部分是需要被回收的,这时必须进行大量标记和清除动作。执行效率随着对象的增加而降低
内存空间碎片化问题,标记、清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致后续程序运行中需要分配较大对象时无法找到足够连续内存不得不提前触发一次垃圾收集动作
标记复制
为了解决面对大量可回收对象执行效率低的问题,提出半区复制的垃圾收集算法。将可用内存按照容量分成大小相等的两块,每次用完,将存活的对象复制到另外一块上面,然后将使用过的清除掉
如果内存中大量都下个是存活着,会产生大量的内存复制的开销,如果大多对象是可回收的,那么复制的就占小部分
适用于新生代的垃圾收集
Appel式回收
将新生代分成一块较大的Eden空间和两个较小的Survivor空间,每次分配只使用Eden和其中一块Survivor
默认比例是 8:1:1 ,如果存活对象多于10%时,需要依赖其他内存区域(老年代)进行分配担保
标记整理
标记复制算法在对象存活率较高的时候,需要进行较多的复制操作,效率会降低,一般老年代不能直接选用该算法。
针对老年带对象存亡特征,提出 标记-整理 算法。标记过程一致,但是后续步骤不是直接对可回收对象进行清理,让所有存活对象想内存空间一端移动,直接清理掉边界以外的内存
移动对象会使回收变得复杂,不移动会使内存空间会有大量内存碎片
19. 常用的主流JVM虚拟机有哪些?
Hotspot VM
J9 VM
Zing VM
JRockit(Oracle)
20. 什么是内存泄露?为什么会发生内存泄露?
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间;内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
当集合里面的对象属性被修改后,再调用remove()方法时不起作用
public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孙悟空","pwd2",26);
Person p3 = new Person("猪八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3); //此时remove不掉,造成内存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!
for (Person person : set)
{
System.out.println(person);
}
}
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孙悟空","pwd2",26);
Person p3 = new Person("猪八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:3 个元素!
p3.setAge(2); //修改p3的年龄,此时p3元素对应的hashcode值发生改变
set.remove(p3); //此时remove不掉,造成内存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println("总共有:"+set.size()+" 个元素!"); //结果:总共有:4 个元素!
for (Person person : set)
{
System.out.println(person);
}
}
如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露
数据库连接,网络连接(socket)和io连接,除非其显式的调用了其close方法()将其连接关闭,否则是不会自动被GC回收的,可能会产生内存泄露。
什么情况下会发生栈内存溢出
递归调用深度过深: 当一个函数递归调用自身或其他函数太深,导致调用栈中的栈帧过多,栈内存会耗尽。这通常是因为没有递归终止条件,或者终止条件设置不当,导致递归无法结束。
局部变量占用过多内存: 如果在函数中定义了大量的局部变量,而这些变量在函数调用过程中没有被释放,会导致栈内存的快速耗尽。这通常发生在某个函数中创建了大型数组或数据结构。
大规模的方法调用链: 如果在程序中有大规模的方法调用链,每次方法调用都会创建一个新的栈帧,栈内存会不断增长。如果方法调用链太长,栈内存可能会耗尽。
递归死循环: 如果递归函数中存在死循环,栈内存会持续分配新的栈帧,直到栈内存用尽。这通常是由于递归终止条件设置不当或逻辑错误引起的。
线程过多: 对于多线程应用程序,每个线程都有自己的调用栈。如果创建了大量线程,每个线程都占用一部分栈内存,总体可能导致栈内存不足。
栈帧过大: 如果某个函数的栈帧过大,比如包含大型数据结构,也可能导致栈内存溢出。
栈内存溢出通常会导致程序崩溃,且错误信息通常包括 "java.lang.StackOverflowError"(对于Java)或类似的错误信息。为了避免栈内存溢出,需要谨慎设计递归和方法调用链,合理控制局部变量的大小,以及监控线程的数量,特别是在多线程应用中。
局部变量占用过多内存: 如果在函数中定义了大量的局部变量,而这些变量在函数调用过程中没有被释放,会导致栈内存的快速耗尽。这通常发生在某个函数中创建了大型数组或数据结构。
大规模的方法调用链: 如果在程序中有大规模的方法调用链,每次方法调用都会创建一个新的栈帧,栈内存会不断增长。如果方法调用链太长,栈内存可能会耗尽。
递归死循环: 如果递归函数中存在死循环,栈内存会持续分配新的栈帧,直到栈内存用尽。这通常是由于递归终止条件设置不当或逻辑错误引起的。
线程过多: 对于多线程应用程序,每个线程都有自己的调用栈。如果创建了大量线程,每个线程都占用一部分栈内存,总体可能导致栈内存不足。
栈帧过大: 如果某个函数的栈帧过大,比如包含大型数据结构,也可能导致栈内存溢出。
栈内存溢出通常会导致程序崩溃,且错误信息通常包括 "java.lang.StackOverflowError"(对于Java)或类似的错误信息。为了避免栈内存溢出,需要谨慎设计递归和方法调用链,合理控制局部变量的大小,以及监控线程的数量,特别是在多线程应用中。
21. 直接内存是什么/有什么用/如何访问?
设计逻辑
Direct Memory 并不是虚拟机运行时数据区的一部分;
由于在 JDK 1.4 中引入了 NIO 机制,为此实现了一种通过 native 函数直接分配对外内存的,而这一切是通过以下两个概念实现的:
通道(Channel)
缓冲区(Buffer)
通过存储在 Java 堆里面的 DirectByteBuffer 对象对这块内存的引用进行操作
因避免了 Java 堆和 Native 堆(native heap)中来回复制数据,所以在一些场景中显著提高了性能
直接内存出现 OutOfMemoryError 异常的原因是物理机器的内存是受限的,但是我们通常会忘记需要为直接内存在物理机中预留相关内存空间;
垃圾回收
直接内存也是会被 JVM 虚拟机管理进行完全不被引用的对象回收处理。
直接内存中的对象并不是如普通对象样被 GC 管理
为什么需要DirectByteBuffer
NIO 操作并不适合直接在堆上操作。由于 heap 受到 GC 的直接管理,在 IO 写入的过程中 GC 可能会进行内存空间整理,这导致了一次 IO 写入的内存地址不完整。如果 IO 时间过长,那么则可能会引起堆空间溢出。
HeapByteBuffer 和 DirectByteBuffer 之间的区别
HeapByteBuffer 实现方式是拷贝,也就是栈访问 Buffer 内的数据还是需要从堆外内存拷贝到 heap 内
DirectByteBuffer 直接在堆内储存有对其的引用,不需要复制就能访问
内存分配问题
直接内存的最大大小可以通过 -XX:MaxDirectMemorySize 来设置,默认是 64M。
Unsafe
主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。
ByteBuffer 提供的静态方法:java.nio.ByteBuffer#allocateDirect 将 Unsafe 类分配内存的相关操作封装好提供给开发者
22. JVM调优命令有哪些?
jps
JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程。
jstat
jstat(JVM statistics Monitoring)是用于监视虚拟机运行时状态信息的命令,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。
jmap
jmap(JVM Memory Map)命令用于生成heap dump文件
还可以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。
jhat
jhat(JVM Heap Analysis Tool)命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。
jstack
jstack用于生成java虚拟机当前时刻的线程快照。
线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
如果java程序崩溃生成core文件,jstack工具可以用来获得core文件的java stack和native stack的信息,从而可以轻松地知道java程序是如何崩溃和在程序何处发生问题。
jstack工具还可以附属到正在运行的java程序中,看到当时运行的java程序的java stack和native stack的信息, 如果现在运行的java程序呈现hung的状态,jstack是非常有用的。
jinfo
jinfo(JVM Configuration info)这个命令作用是实时查看和调整虚拟机运行参数。
23. 常用的JVM 问题定位工具有哪些?
top命令查看 CPU的负载情况
或者使用JVM调优命令查看信息
怎么打出线程栈信息
强引用,软引用,弱引用,虚引用的区别
多线程
1. 进程和线程的区别?
基本概念
进程是对运行程序的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发
线程是进程的子任务,是CPU调度和分派的基本单位,用于保证程序的实时性,实现进程内部的并发;每个线程独自占用一个虚拟处理器,独自的指令计数器和处理器状态。但是共享同一地址空间。
区别
一个线程只能属于一个进程,一个进程可以有多个线程,但至少有一个线程。
进程是资源分配的最小单位,线程是CPU调度的最小单位
在系统开销方面,切换进程的开销远大于切换线程的开销
进程间通信IPC,线程可以直接读写进程数据段(全局变量)来进行通信,但是要考虑到数据的一致性,需要进行同步互斥手段进行辅助
进程间不会相互影响,线程挂掉可能导致整个进程挂掉
进程适用于多核多机分布系统,线程适合多核系统
2. 讲一下你了解的哪些进程之间的通信/线程之间的通信?
进程之间的通信
消息队列
对于具有写操作的权限进程可以向消息队列里面去添加新的信息(生产者);对于具有读操作的进程可以从消息队列中读取信息(消费者);
特点
面向记录的,消息具有特定的格式以及特定的优先级
独立于发送与接收进程
可以实现消息的随机查询,消息不一定是先进先出的读取,也可以根据消息的类型读取
信号量
是一个计数器,可以用来控制多个进程对共享资源的访问
特点
用于进程间同步,若在进程间传输数据需要结合共享内存
用户系统的PV操作,程序对信号量的操作都是原子操作
每次对信号量的PV操作不仅局限于对信号量加1或者减1操作,可以加减任意正整数
支持信号量组
共享内存 redis
多个进程可以访问同一块内存空间,可以及时看到对方进程中对共享内存中数据的更新,需要依靠同步操作,互斥锁,信号量等
特点
共享内存是最快的一种IPC,因为进程是直接对内存进行存取的
因为多进程可以同时操作,所以需要同步
信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
套接字Socket
socket也是一种进程间通信机制,和其他通信机制不同的是,可以用于不同主机之间的进程通信
线程之间的通信
临界区
通过多线程的串行来访问公共资源或者一段代码,速度快,适合控制数据访问
互斥量
使用锁机制 Synchronized/Lock ,可以保证一个公共资源不会被多个线程同时访问
信号量
为控制有限数量的用户资源而设计,允许多个线程在同一时刻访问同一个资源,一般需要限制同一时刻访问此资源的最大线程数目
事件
wait/notify:通过通知操作来保持多线程同步,可以方便实现多线程优先级比较操作
3. 什么是原子性、可见性、有序性?
原子性、可见性、有序性 是 并发的三大性质
原子性
一个操作不可中断,要么全部执行成功,要么全部执行失败
Java内存模型中定义了8种不可再分的操作
作用于主内存变量
lock(锁定):将一个变量标识为线程独占的状态
unlock(解锁):将一个变量从锁定状态释放出来,释放后可以被其他线程锁定
read(读取):把一个变量的值从主内存传输到线程的工作内存,以便后续load操作
wirte(写):将store操作从工作内存中得到的变量的值存放入主内存变量中
作用于工作内存变量
load(载入):把内存中一个变量的值传递给执行引擎,当虚拟机遇到一个需要使用变量的值的字节码指令时,执行这个操作
use(使用):把工作内存中一个变量的值传递给执行引擎,当虚拟机遇到一个需要使用到变量的值的字节码指令时,执行这个操作
assign(赋值):从一个执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时,执行这个操作
store(存储):把工作内存中一个变量的值传给主内存中以便后续write操作使用
可以使用synchronized或者锁来实现,一段代码的原子操作
volatile可以提供有序性和可见性,但是不能提供原子性
有序性
程序运行时的操作是有序的
在Java内存模型中,为了性能优化,编译器和处理器会进行指令重排,在单线程的程序中操作不会变化,但是在多线程情况下,可能就会出现问题
使用volatile 关键字来保证有序性
同步机制 - 使用synchronized关键保证有序性
同步机制 - 使用Lock保证有序性
public class Singleton {
private Singleton() { }
private volatile static Singleton instance;
public Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
private Singleton() { }
private volatile static Singleton instance;
public Singleton getInstance(){
if(instance==null){
synchronized (Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
如果没有volatile可能发生的情况
synchronized关键字表示同一时刻只能由一个线程进行读取,当线程被占用后,其他线程只能等待,因此是串行执行的,所以可以保证有序性,Lock同理
可见性
可见性是指当一个线程修改了共享变量后,其他线程能够立刻得到值这个修改
synchronized,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁时会将共享变量同步到主内中。synchronized具有可见性
volatile中会通过指令添加lock指令,以实现内存可见性,volatile具有可见性
内存屏障、volatile的内存屏障、final中内存屏障
4. 为什么要使用多线程?
使用多线程可以把占据时间长的程序中的任务放到后台处理
在多核机器上可以提升机器的利用率,使得程序的运行效率提高
多线程可以讲复杂任务分解成更小的,更想对独立的线程,简化编程任务以及易于维护和测试
编写多线程程序时,需要注意共享资源的线程安全
大量的线程也可能会影响性能,操作系统进行切换时会消耗一定的时间
5. 创建线程有哪几种方式?
1. 继承Thread类,并重写run方法,然后使用该类的实例进行start方法
2. 实现Runnable接口,并实现run方法;将其传入Thread构造器中,调用thread的start方法;(注:不会抛出异常)
3. 实现Callable接口,实现call方法,call方法是带返回值的,通过FutureTask构造方法,将callable实现类传进去,FeatureTask作为Thread的target创建Thread线程对象,通过get方法获取执行结果。(在get的时候,如果执行有问题会抛出异常)
4. 通过实现线程池,然后使用线程池可以提交任务,创建线程
6. 什么是守护线程?
Java中线程分成两类:守护线程、用户线程。
当JVM中不存在任何一个正在运行的非守护线程是,则JVM进程会退出
守护线程拥有自动结束自己生命周期的特性,非守护线程不具备这个特点
读写操作或者计算操作不应该放在守护线程中,可能用户线程已执行完成,守护线程退出,会导致守护线程中数据丢失
守护线程经常作用于执行一些后台任务,比如JDK的垃圾回收任务,因此有时候也被称为是后台线程
7. 线程的状态有哪几种?怎么流转的?
线程状态
新建(New):创建后未启动的线程状态;因为在没有start之前,该线程不存在和创建一个普通java对象没什么区别
运行(Runnable):包含Running和Ready的状态
无限期等待(Waiting):不会被分配CPU执行时间,需要显示唤醒
期限等待(Timed Waiting):在一定时间后系统自动唤醒
阻塞(Blocked):等待获取排他锁
结束(Terminated):已终止线程状态,线程已经结束执行
线程状态流转
8. 线程的优先级有什么用?
Java中的线程可以设置优先级,范围是1~10,默认为5; 你可以使用常量,如MIN_PRIORITY = 1 ,MAX_PRIORITY = 10 ,NORM_PRIORITY = 5 来设定优先级。
该值是给操作系统作为调度的参考,但线程最终的调用顺序是由操作系统的调度算法来决定的
t.setPriority()用来设定线程的优先级。
9. JUC是指什么?谈谈你对JUC的了解?
Java5.0 提供了java.util.concurrent包(简称JUC),包含了并发编程中常用的实用工具类
知识体系
tools(工具类)
又称信号量三组工具类
countDownLatch:是一个同步辅助类,在完成一组正在其他线程中执行的操作之前,允许一个或多个线程一直等待
CyclicBarrier:是一个同步辅助器,允许一组线程互相等待,知道某个公共屏障点,并且在释放等待线程后可以继续重用
Semaphore:是一个计数信号量,本质是一个“共享锁”。维护了一个信号量许可集,可以通过acquire()方法来获取信号量的许可;当信号量中有可用的许可时可以获取许可,否则线程必须等待,知道有可用许可。线程可以通过调用release()来释放它锁持有的信号量许可。
Phaser:Phaser是JDK 7新增的一个同步辅助类,它可以实现CyclicBarrier和CountDownLatch类似的功能,而且它支持对任务的动态调整,并支持分层结构来达到更高的吞吐量。
Exchanger:Exchanger是用于线程协作的工具类, 主要用于两个线程之间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。这两个线程通过exchange()方法交换数据,当一个线程先执行exchange()方法后,它会一直等待第二个线程也执行exchange()方法,当这两个线程到达同步点时,这两个线程就可以交换数据了。
Executor(执行者)
Java里面线程池的顶级接口,只是一个执行线程的工具,真正的线程池接口是ExecutorService
Atomic(原子性包)
JDK提供的一组原子操作类,采用CAS原理进行操作
Lock(锁)
JDK提供的锁机制,相比synchronized关键字来进行同步锁,功能更加强大,底层实现基本基于AQS
Collections(集合)
主要提供线程安全的容器
基于 AQS 实现的各种工具类
10. i++ 是线程安全的吗?
不是线程安全的。i++不是原子操作,而是由三条指令合成
1. 从内存中把i的值取出来放到CPU寄存器中(CPU cache)
2. CPU寄存器的值 +1
3. 把CPU寄存器的值写回内存
volatile不能解决线程安全问题,只能保证可见性,不能保证原子性
解决方法:
1、对 i++ 操作的方法加同步锁,同时只能有一个线程执行 i++ 操作
2、使用支持原子性操作的类,如 java.util.concurrent.atomic.AtomicInteger,它使用的是 CAS 算法,效率优于第 1 种;
1、对 i++ 操作的方法加同步锁,同时只能有一个线程执行 i++ 操作
2、使用支持原子性操作的类,如 java.util.concurrent.atomic.AtomicInteger,它使用的是 CAS 算法,效率优于第 1 种;
11. join 方法有什么用?什么原理?
t.join方法用来阻塞调用此方法的线程,直到线程t完成执行,再继续
原理:通过源码可以了解到,在join方法中调用了wait方法,表示主线程拥有自线程对象的锁,并且进入等待状态;当自线程执行结束时,JVM会自动调用lock.notify_all()方法,此时主线程被唤醒了,可以继续跑下去了
12. 如何让一个线程休眠?sleep
使用sleep方法,可以使得当前线程处于阻塞状态(休眠);当线程重新被唤醒时,会从阻塞状态变成就绪状态
sleep和wait方法的区别
sleep方法调用时不会释放锁
wait方法需要在synchronized关键字块/方法里调用,会释放掉锁,同时进入等待池中,等待其他线程调用notify才可继续被唤醒执行
sleep是线程的方法,wait/ntify/notifyAll 是Object类的方法
jdk 1.5 之后引入了 TimeUnit ,对sleep方法做了很好的封装,省去时间换算的步骤且更优雅
13. Thread.yield 方法有什么用?
yield方法表示谦让的意思,它让掉当前CPU的时间片,使得正在运行中的线程重新变成就绪状态,并且重新竞争CPU的调度权,
也就是说如果cpu资源不紧张的话会忽略这种提示。
也就是说如果cpu资源不紧张的话会忽略这种提示。
sleep和yield方法的区别
都可以暂停当前线程,sleep可以指定休眠时间。yield不可指定,也不能一定担保
sleep 会导致当前线程暂停指定的时间,没有CPU时间片的消耗。yield 只是对CPU调度器的一个提示,如果CPU没有忽略这个提示的话,会导致线程上下问切换。
sleep会使线程短暂block,会在给定的时间内释放CPU资源
yield,sleep在暂停过程中,如果持有锁,不会释放锁资源
yield不能被中断,sleep可以接受中断
yield 在JDK1.5以前版本中实际上是调用了 sleep(0)
14. 怎么理解 Java 中的线程中断?
线程中断的3个方法
java.lang.Thread#interrupt()
中断目标线程,给目标线程发一个中断信号,线程被打上中断标记
java.lang.Thread#isInterrupted()
判断目标线程是否被中断,不会清除中断标记
java.lang.Thread#interrupted()
判断目标线程是否被中断,会清除中断标记
原理
若另一个线程调用被阻塞线程的interrupt方法,则会打断这种阻塞。打断一个线程并不等于该线程的生命周期结束,仅仅是打断了当前线程的阻塞状态。
调用Thread对象的interrupt函数不是立即中断线程,只是将线程中断状态标志设置为true
当线程中有调用阻塞函数sleep、wait、join时,阻塞函数调用后,会不断轮训中断标志状态是否为true
如果为true则停止阻塞并且抛出InterruptedException,同时重置中断标志;如果为false,继续阻塞直到阻塞正常结束
注意:如果一个线程已经是死亡状态,那么尝试对其的interrupt会直接被忽略
15. 你怎么理解 wait、notify、notifyAll?
两个池
锁池。 Entry set
等待池 Wait set
wait
该方法用来当线程进入休眠状态,直到被通知notify 或者中断interrupt 。
在调用wait方法前,线程必须要获得该对象的对象级别锁,只能在同步方法/同步代码块中调用wait方法。
进入wait方法后,当前线程释放锁。从wait返回之后,线程需要和其他线程重新竞争获得锁。
如果调用wait时,没有持有锁,抛出IllegalMonitorStateException,是一个Runtime异常
在调用wait方法前,线程必须要获得该对象的对象级别锁,只能在同步方法/同步代码块中调用wait方法。
进入wait方法后,当前线程释放锁。从wait返回之后,线程需要和其他线程重新竞争获得锁。
如果调用wait时,没有持有锁,抛出IllegalMonitorStateException,是一个Runtime异常
wait 和 sleep 的区别
都可以是线程进入阻塞状态
都是可中断的方法,被中断后会收到中断异常
wait 是object 方法; sleep 是thread 特有的方法
wait 方法的执行必须是在同步方法中进行,而sleep 不需要
线程在同步方法中执行sleep时不会释放monitor锁,而wait方法则会释放monitor锁
sleep方法短暂休眠之后会主动退出阻塞,而wait方法则需要被其他线程中断之后才能退出
notify
一样需要在同步方法/块中调用,调用前也必须要获得该对象的对象级别锁。
该方法用来通知那些可能处于等待该对象的对象锁的其他线程。
如果有多个线程等待,则线程规划器会任意挑出一个wait状态的线程发出通知,并使得它等待获取该对象的对象锁
(notify 后,当前线程不会马上释放该对象锁,wait 所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁)
如果有多个线程等待,则线程规划器会任意挑出一个wait状态的线程发出通知,并使得它等待获取该对象的对象锁
(notify 后,当前线程不会马上释放该对象锁,wait 所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁)
notifyAll
一样需要在同步方法/块中调用,调用前也必须要获得该对象的对象级别锁。
notifyAll 使所有原来在该对象上 wait 的线程统统退出 wait 的状态(即全部被唤醒,不再等待 notify 或 notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll 线程退出调用了 notifyAll 的 synchronized 代码块的时候),他们就会去竞争。
如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。
16. 同步和异步的区别?
同步是阻塞模式,异步是非阻塞模式。(讲一下两者的处理模式)
同步的缺点:
1. 如果第一次提交等待的时间过长的话会导致第二次提交耗时过长
2. 系统同时受理业务数量有限,系统整体吞吐量不高
3.频繁创建开启和销毁线程,增加系统额外开销
4.业务达到峰值的时候,大量的业务处理线程会阻塞会导致频繁的cpu上下文切换,从而降低系统性能
1. 如果第一次提交等待的时间过长的话会导致第二次提交耗时过长
2. 系统同时受理业务数量有限,系统整体吞吐量不高
3.频繁创建开启和销毁线程,增加系统额外开销
4.业务达到峰值的时候,大量的业务处理线程会阻塞会导致频繁的cpu上下文切换,从而降低系统性能
异步非阻塞:
1.不用等结果处理结束之后才能返回,从而提高了系统的吞吐量和并发量
2.若服务端的线程数量在一个可控范围之内的话是不会导致太多的CPU上下文切换带来的额外开销的
3. 线程可以重复利用,减少了不断创建线程带来的资源浪费
4.缺点:想要得到结果需要再次调用接口方法进行查询
1.不用等结果处理结束之后才能返回,从而提高了系统的吞吐量和并发量
2.若服务端的线程数量在一个可控范围之内的话是不会导致太多的CPU上下文切换带来的额外开销的
3. 线程可以重复利用,减少了不断创建线程带来的资源浪费
4.缺点:想要得到结果需要再次调用接口方法进行查询
一般会采用异步去执行比较耗时,或者对最终结果没有影响的操作,从而提高效率
如何关闭一个线程?
正常关闭
1.线程结束生命周期正常结束
2.捕获中断信号关闭线程
3.使用volatile开关控制
异常退出
进程假死
进程虽然存在,但是没有日志输出,程序不进行任何作业,看起来就像是死了一样,但实际上它没有死。
某个线程阻塞了
线程出现死锁
排除进程假死的工具+命令
17. 什么是死锁?
多线程编程中,为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之上加上互斥锁,只有获得锁的线程,才能操作共享资源,获取不到锁的线程只能等待,直到锁被释放
两个线程为了保护两个不同的共享资源使用了两个互斥锁,这两个互斥锁应用不当的时候,可能会导致两个线程都在等待对方释放锁,在没有外力的作用下,线程会相互等待,这个时候就会发生死锁
死锁四条件
互斥条件
多个线程不能同时使用同一个资源
持有并等待条件
持有并等待条件
不可剥夺条件
线程已经持有了资源,在自己使用完之前不能,被其他线程获取,线程B也想使用此资源,只能在线程A使用完并释放后获取
环路等待条件
死锁发生的时候,两个线程获取资源的顺序形成了环
程序死锁
(1)交叉锁可导致程序出现死锁
相互占有,相互等待
打开jstack工具 或者 jconsole工具 可以看到报错信息会打印出来
(2) 内存不足
两个线程有可能都在等待彼此能够释放内存资源
(3)一问一答式的数据交换
客户端发起某个请求立即等待接收;服务端错过请求,仍在等待一问一答式的数据交换,双方相互等待
(4)数据库锁
某个线程执行for update 语句退出了事务,其他线程访问该数据库的时候都将陷入死锁
(5)文件锁
某线程获得文件锁意外退出,其他读取该文件的线程也将会进入死锁知道系统释放文件句柄资源
(6)死循环引起的死锁
由于程序处理不当,进入了死循环,虽然查看线程堆栈信息不会发现任何死锁的迹象,但是程序不工作,CPU占有率高居不下,这种死锁称为系统假死,想要做出dump会非常困难的。
可以使用jstack jconsole jvisualvm 工具进行诊断,但是不会给出明显的提示。
因为工作的线程并未blocked 而是始终处于runnable状态,cpu高居不下,甚至不能够正常运行命令
这个主要看的就是哪个方法耗时很长,很长的话应该就是不正常。检查是否有死循环
因为工作的线程并未blocked 而是始终处于runnable状态,cpu高居不下,甚至不能够正常运行命令
这个主要看的就是哪个方法耗时很长,很长的话应该就是不正常。检查是否有死循环
Java排查死锁可以采用 jstack 工具,它是jdk自带的线程堆栈分析工具
18. 如何解决死锁?
最常见的方式是使用资源有序分配法
线程A和线程B获取资源的顺序要一致,当线程A先尝试获取A资源,然后尝试获取B资源的时候;线程B同样也是先尝试获取A资源,然后尝试获取B资源,AB线程总是以同样的顺序申请自己想要的资源即可
防止
在程序运行之前防止发生死锁,也就是破坏四个条件中的一项
使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。 —— 互斥条件
采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行。 —— 破坏持有并等待
剥夺调度能够防止死锁,但是只适用于内存和处理器资源。
方法一:占有资源的进程若要申请新资源,必须主动释放已占有资源,若需要此资源,应该向系统重新申请。
方法二:资源分配管理程序为进程分配新资源时,若有则分配;否则将剥夺此进程已占有的全部资源,并让进程进入等待资源状态,资源充足后再唤醒它重新申请所有所需资源。
方法一:占有资源的进程若要申请新资源,必须主动释放已占有资源,若需要此资源,应该向系统重新申请。
方法二:资源分配管理程序为进程分配新资源时,若有则分配;否则将剥夺此进程已占有的全部资源,并让进程进入等待资源状态,资源充足后再唤醒它重新申请所有所需资源。
给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
采用层次分配策略,将系统中所有的资源排列到不同层次中
一个进程得到某层的一个资源后,只能申请较高一层的资源
当进程释放某层的一个资源时,必须先释放所占有的较高层的资源
当进程获得某层的一个资源时,如果想申请同层的另一个资源,必须先释放此层中已占有的资源
采用层次分配策略,将系统中所有的资源排列到不同层次中
一个进程得到某层的一个资源后,只能申请较高一层的资源
当进程释放某层的一个资源时,必须先释放所占有的较高层的资源
当进程获得某层的一个资源时,如果想申请同层的另一个资源,必须先释放此层中已占有的资源
破坏循环等待条件
避免
各种死锁防止方法能够防止发生死锁,但必然会降低系统并发性,导致低效的资源利用率。
安全状态
单资源银行家算法
多资源银行家算法
检测和恢复
对资源的分配加以适当限制可防止或避免死锁发生,但不利于进程对系统资源的充分共享。
检查资源是否有环路
恢复
资源剥夺法
剥夺陷于死锁的进程所占用的资源,但并不撤销此进程,直至死锁解除。
剥夺陷于死锁的进程所占用的资源,但并不撤销此进程,直至死锁解除。
进程回退法
根据系统保存的检查点让所有的进程回退,直到足以解除死锁,这种措施要求系统建立保存检查点、回退及重启机制。
根据系统保存的检查点让所有的进程回退,直到足以解除死锁,这种措施要求系统建立保存检查点、回退及重启机制。
进程撤销法
撤销陷入死锁的所有进程,解除死锁,继续运行。
逐个撤销陷入死锁的进程,回收其资源并重新分配,直至死锁解除。
撤销陷入死锁的所有进程,解除死锁,继续运行。
逐个撤销陷入死锁的进程,回收其资源并重新分配,直至死锁解除。
1.CPU消耗时间最少者 2.产生的输出量最小者
3.预计剩余执行时间最长者 4.分得的资源数量最少者后优先级最低者
3.预计剩余执行时间最长者 4.分得的资源数量最少者后优先级最低者
系统重启法
结束所有进程的执行并重新启动操作系统。这种方法很简单,但先前的工作全部作废,损失很大。
结束所有进程的执行并重新启动操作系统。这种方法很简单,但先前的工作全部作废,损失很大。
19. 什么是活锁?
两个以上的线程在执行的时候,因为互相谦让资源,结果都拿不到资源没法运行程序
账户A转账户B,账户B转账户 A。那么,线程一会占有账户A,线程二则会占有账户B。线程一拿不到账号B,转账没法执行,线程一结束;线程二也拿不到账号A,转账也没法执行,线程二结束。之后开始第二轮循环,再循环过程中又是重复,这样的话就是活锁
如何避免活锁?
在解锁之前等待一段时间,由于每个线程解锁时间都不相同,那么重复的概率很小,两个线程的加解锁操作就错开了,转账也能很快完成
一般情况下,活锁出现会非常少见,即使出现了,程序也会自动解开
20. 什么是无锁?
无锁编程一般就是不使用锁而达到线程安全的目的,一般CAS算法就是常用的无锁算法
好处:提高性能,提高响应性,避免死锁
坏处:需要复杂协调和状态管理的情况下不是很实用
21. 什么是线程饥饿?
多线程场景下,某个线程一直阻塞在同步代码块外无法进入临界区消费,这种情况就是线程饥饿
导致原因
高优先级线程吞噬所有低优先级线程的CPU
线程优先级设置不合理,一般情况下,大多数应用不需要改变优先级
线程被永远阻塞在等待进入同步块的状态
线程在等待一个本身也处于永久等待完成的对象(调用这个对象的wait方法)
多个线程处在wait方法执行,对其调用notify不会保证哪个线程会唤醒,存在一个线程重来都得不到唤醒
一般推荐使用notifyAll,这样不必一个一个notify,交给CPU具体执行某个线程即可,不需要自己过多考率导致饥饿
解决方式
使用公平锁。 但是公平锁会导致性能上有一定的损失,需要进行考虑
扩大线程池中的线程数,暂时解决饥饿问题;
多创建一个线程池解决问题,各个线程池各司其职
22. 什么是 CAS?
CAS是比较并替换,是一种实现并发算法时,常用到的技术,Java并发包中原子类就是基于这种思想
CAS 需要有三个操作数: 内存值V,旧的预期值A,需要更新的目标值B;CAS执行时,当且仅当V==A时,将V修改成B否则什么都不做,整个比较替换的操作时原子操作
CAS会有ABA的问题
CAS使用流程
从内存中获取V读取到值A
根据A计算目标值B
通过CAS以原子的方式将V从A修改到B
第一步到第三步的过程可能V的值从A变成B在变成A,这样其实就是有问题的了
解决方式
采用版本号的方式进行解决,Java中也有提供相应的原子类
23. 阻塞和非阻塞的区别?
阻塞和非阻塞关注的是 程序在等待调用结果时的状态
阻塞调用表示,在调用结果返回回来前,当前的线程会被挂起,调用线程只有在得到结果之后才会执行下步操作
非阻塞调用表示不能立即得到结果前也不会阻塞当前线程。
24. 并发和并行的区别?
并发是逻辑上的同时发生
一个处理器同时处理多个任务
并行是物理上的同时发生
多个处理器同时处理多个不同的任务
并发和并行的区别
25. 为什么不推荐使用stop停止线程?
不使用stop原因
stop方法本质上不安全
使用Thread.stop停止线程会导致解锁所有已锁定的监视器,直接释放当前已经获得的所有锁,使得当前线程直接进入阻塞状态。
使用stop,我们不清楚线程到底运行到什么地方,暴力中断了线程,线程后续是比较重要的业务逻辑/资源释放,那么中断后的结果会很严重。
不使用suspend的原因
suspend的作用是挂起某个线程知道调用resume方法来回复该线程
调用了suspend方法之后,不会去释放被挂起线程获取到的锁,其他线程不能访问这些资源
suspend某个线程之后,如果在resume的过程中,出现异常导致resume方法执行失败,lock无法释放,导致死锁
较高的Java版本已经将这两个方法标记上@Derecated标签了
26. 如何优雅地停止线程?
定义一个变量,由目标线程去不断的检查变量的状态,当变量达到某个状态的时候,停止线程;这个变量需要加上volatile,保证变量的可见性
使用interrupt方法中断线程,原理其实是一致的,只不过线程封装成方法实现了。我们可以看到interrupt方法其实就是将中断状态变量设置成true,在调用isInterrupted方法的时候,可以看到就是去判断这个状态的,然后进行优雅退出线程。包括sleep等方法内部有去轮询这个状态是否为true,然后会抛出中断异常
synchronized
什么是synchronized?
synchronized 关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象是多线程可见的,那么该对象的所有读写都将通过同步的方式来进行。
1. 提供一种锁机制,能够确保共享变量的互斥访问,从而防止数据不一致出现
2.包括了monitor enter 和 monitor exit 两个jvm指令,能够确保在任何时候任何线程执行到monitor enter 成功之前都必须从主内存中获取到数据而不是从缓存中,monitor exit 运行成功之后共享变量被更新后的只必须刷入到主内存
3.synchronized 的指令严格按照java happens- before 规则 一个monitor exit 指令之前必定要有一个monitor enter
synchronized 用于对对代码块或者方法进行修饰:同步代码块 / 同步方法
要注意的问题:
1. 与monitor 关联的对象不能为空
2. synchronized 作用域太大
代表其效率低,甚至还会丧失并发的优势
3. 不同的monitor 企图锁相同的方法
在run方法中锁起来,根本起不到互斥作用
4.多个锁的交叉导致死锁
sychronized 嵌套 synchronized
27. Synchronized 同步锁有哪几种用法?
面向对象的同步锁
同步普通方法
这种方式一个实例只有一个线程可以进入该方法,相当于锁对象
/**
* 用在普通方法
*/
private synchronized void synchronizedMethod() {
System.out.println("synchronizedMethod");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
* 用在普通方法
*/
private synchronized void synchronizedMethod() {
System.out.println("synchronizedMethod");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
同步this实例
表示锁住整个当前对象实例,只有获取到这个对象的锁才能进入代码块
/**
* 用在this
*/
private void synchronizedThis() {
synchronized (this) {
System.out.println("synchronizedThis");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
* 用在this
*/
private void synchronizedThis() {
synchronized (this) {
System.out.println("synchronizedThis");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
同步对象锁
表示锁住某个对象,只有获取到这个对象的锁才能进入代码块
/**
* 用在对象
*/
private void synchronizedInstance() {
synchronized (LOCK) {
System.out.println("synchronizedInstance");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
* 用在对象
*/
private void synchronizedInstance() {
synchronized (LOCK) {
System.out.println("synchronizedInstance");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
面向类的同步锁
同步静态方法
不管多少个类的实例,同时只有一个线程可以获取锁进入到方法中
/**
* 用在静态方法
*/
private synchronized static void synchronizedStaticMethod() {
System.out.println("synchronizedStaticMethod");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
* 用在静态方法
*/
private synchronized static void synchronizedStaticMethod() {
System.out.println("synchronizedStaticMethod");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
同步类
同时只有一个线程能访问带有同步类锁的方法
/**
* 用在类
*/
private void synchronizedClass() {
synchronized (TestSynchronized.class) {
System.out.println("synchronizedClass");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 用在类
*/
private void synchronizedGetClass() {
synchronized (this.getClass()) {
System.out.println("synchronizedGetClass");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
* 用在类
*/
private void synchronizedClass() {
synchronized (TestSynchronized.class) {
System.out.println("synchronizedClass");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 用在类
*/
private void synchronizedGetClass() {
synchronized (this.getClass()) {
System.out.println("synchronizedGetClass");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Java中除了基本类型,可以说万物皆对象,逻辑上锁住的是类,但是锁住的其实是class这个对象。所以还是锁住的是对象
如果获取了某个实例的锁,还能继续获取类锁吗?
是可以的,两个锁没有冲突的地方,一个锁住的是实例,一个锁住的是这个实例的Class对象
28. 讲一下锁升级吧
锁升级描述的是jdk6以后,Java对synchronized的优化,使得其在竞争并不是很大的场景下,有较高的性能;锁升级按照锁类型可以分成4个阶段
对象在JVM里的空间布局
对象在堆中的组成
对象头:包括关于堆对象的布局、类型、GC状态、同步状态和标识哈希码的基本信息。Java对象和JVM内部对象都有一个共同的对象头格式
实例数据:主要是存放类的数据信息,父类的信息,对象字段属性信息
对齐填充:为了字节对齐,填充的数据不是必须的
对象头
Mark Word
用于存储对象自身的运行时数据,哈希码,GC分代年龄、锁状态标识、线程持有的锁、偏向线程ID、偏向时间戳等
在32位JVM中是32bit
在64位JVM中长度是64bit
基本组成
锁标志位(lock):区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
分代年龄(age):表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
对象的hashcode(hash):运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。
偏向锁的线程ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
epoch:偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。
Klass Pointer
类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
实例数据
对象有属性字段,则这里会有数据信息。如果对象无属性字段,则这里就不会有数据。根据字段类型的不同占不同的字节,例如boolean类型占1个字节,int类型占4个字节等等;
对齐数据
对象可以有对齐数据也可以没有。默认情况下,Java虚拟机堆中对象的起始地址需要对齐至8的倍数。如果一个对象用不到8N个字节则需要对其填充,以此来补齐对象头和实例数据占用内存之后剩余的空间大小。如果对象头和实例数据已经占满了JVM所分配的内存空间,那么就不用再进行对齐填充了。
所有的对象分配的字节总SIZE需要是8的倍数,如果前面的对象头和实例数据占用的总SIZE不满足要求,则通过对齐数据来填满。
为什么要对齐数据?
字段内存对齐的其中一个原因,是让字段只出现在同一CPU的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。其实对其填充的最终目的是为了计算机高效寻址。
偏向锁
在无竞争的情况下,把整个同步都消除掉,CAS都不做
锁会偏向第一个获得它的线程,在接下来执行过程中,如果该锁没有被其他线程获取,持有锁的线程将永远不需要再同步
偏向锁一般只会出现在第一个获得该锁的线程,如果调用了Object的hashCode方法会变成未锁定的不可偏向状态,撤销偏向,或者之后其他线程获取会是轻量级锁状态。原因是threadID变成了hash code
锁状态流转
Object对象的hashCode事一致性哈希码,这个值是可以保持不变的,通过对象头中存储计算结果保证,在一次计算之后,再次调用该方法不会再发生改变。因此当一个对象已经计算过一致性哈希码之后,它就再也无法进入偏向锁状态了。不过重写的hashCode方法是不会有这样的问题的
轻量级锁
各个状态下对象头MarkWord的组成
在代码进入同步块时,如果同步对象没有被锁定,虚拟机首先姜在当前线程的栈帧中建立一个锁记录的空间,用于存储对象目前的MarkWord的拷贝。
虚拟机将使用CAS操作尝试把对象的MarkWord更新位指向Lock Record的指针,如果更新动作成功了,代表线程拥有对象的锁,并且将标志位变成'00'表示处于轻量级锁定状态
如果操作失败了,意味着至少存在一条线程与当前线程存在竞争关系,首先会判断markWord是否指向当前线程的栈帧,如果是的话,说明当前线程已经拥有整个对象的锁,直接进入同步块即可。
如果出现两条以上的线程竞争同一个锁的情况,轻量级锁不在有效,必须膨胀为重量级锁,锁标志变成'10'
解锁过程也是通过CAS操作及逆行替换的,如果替换失败,则说明有其他线程尝试获取过锁,在释放的同时,唤醒被挂起的线程
轻量级锁能够提升同步性能的依据是:绝大部分锁,在整个同步周期内都是不存在竞争的。不存在竞争可以避免使用互斥量的开销,如果存在锁竞争,除了互斥量本身的开销外,还额外发生了CAS操作的开销,在此情况下轻量级锁会比重量级锁更慢
自旋锁
在JDK1.4已经引入,不过默认关闭。
在互斥锁中,挂起线程和恢复线程的操作都需要转入内核态中完成,这样会带来比较大的开销。
在获取不到的时候会进行一个忙循环,默认是10次,自旋等待不能替代阻塞,但是可以避免线程切换转入到内核态
如果超过自旋次数,会进行锁升级。
自适应锁
如果在同个锁对象上获得过锁,会进而允许自选持续更多次。比如100次。
如果对于某个锁很少成功获得过锁,有可能会取消掉自旋的过程。
随着程序运行时间的增加,性能监控信息的不断完善,会对锁的状态预测越来越准
重量级锁
内置锁在Java中被抽象为监视器锁,Java1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。成本非常高,包括系统调用引起的内核态与用户态的切换、线程阻塞、线程切换等。也称之为“重量级锁
注
锁膨胀是不可逆的,一旦从偏向锁->轻量级锁->重量级锁 就不可逆了
偏向锁
适合场景并发场景很小,或者不产生并发的情况下,一个线程持有很少会有其他线程持有该锁
并发场景较多,很快就会膨胀为轻量级锁了
轻量级锁
并发场景少,或者并发等待时间较短(自旋获取锁)的场景
并发场景大的情况下,会很快膨胀为重量级锁
synchronized关键字 不适用于 阶段性并发很大的场景,适合无并发、并发量很少的情况去保证线程安全
29. 什么是锁消除/锁粗化?
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的请求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。
锁粗化:讲多次锁的请求合并到一个请求中,以降低段时间内大量锁请求、同步、释放带来的性能损耗
两个获取相同锁代码块之间有一定的代码,但是可以很快执行完成,系统会合并这两个代码块,放入到同一个锁代码块上
public void doSomethingMethod(){
synchronized(lock){
//do some thing
}
//这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
synchronized(lock){
//do other thing
}
}
synchronized(lock){
//do some thing
}
//这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
synchronized(lock){
//do other thing
}
}
public void doSomethingMethod(){
//进行锁粗化:整合成一次锁请求、同步、释放
synchronized(lock){
//do some thing
//做其它不需要同步但能很快执行完的工作
//do other thing
}
}
//进行锁粗化:整合成一次锁请求、同步、释放
synchronized(lock){
//do some thing
//做其它不需要同步但能很快执行完的工作
//do other thing
}
}
for(int i=0;i<size;i++){
synchronized(lock){
}
}
synchronized(lock){
}
}
synchronized(lock){
for(int i=0;i<size;i++){
}
}
for(int i=0;i<size;i++){
}
}
锁消除:虚拟机在即时编译的过程中,监测到不可能存在共享数据的竞争,此时就去掉锁;锁消除具体的判断依据来自逃逸技术:如果判断到一段代码中,在堆上的所有数据都不会逃逸出去,被其他线程访问到,那就可以把他们当作栈上的数据对待,认为他们是线程私有的,同步加锁就不用进行
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
StingBuffer是线程安全的,append方法加上了synchronized关键字,在这个场景下,每次调用concatString方法都会生成一个新的对象,那么这个对象进行append方法其实不需要有锁操作,会进行锁消除
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
StingBuffer是线程安全的,append方法加上了synchronized关键字,在这个场景下,每次调用concatString方法都会生成一个新的对象,那么这个对象进行append方法其实不需要有锁操作,会进行锁消除
30. 什么是重入锁?
重入锁实现了Lock接口,当前线程可以反复加锁,表示可重入。如果设计成不可重入的情况,那么一段逻辑中多次获取同一个锁会导致形成死锁
synchronized同样也是可重入的
ReentrantLock几个重要方法
lock()
锁空闲:直接获取锁,设置锁持有数量为1
当前线程持有锁:直接获取锁并返回,同时锁持有者数量递增1
其他线程持有锁:当前线程休眠等待,直到获取锁为止
lockInterruptibly()
获取锁,逻辑和lock一样,这个方法可以响应中断
tryLock()
尝试获取锁,成功返回true,失败返回false
锁空闲:直接获取锁:返回true,同时设置持有锁数量为1
当前线程持有锁:直接获取锁返回:true,同时锁持有者数量递增1
其他线程持有锁:获取锁失败,返回:false
tryLock(long timeout, TimeUnit unit)
逻辑和tryLock一致,带有一定的停留时间
unlock()
释放锁,每次锁持有者数量递减1,直到0为止
newCondition()
返回该锁的Condition实例,可以实现synchronized关键字类似 wait/notify 多线程通信的功能
ReentrantLock底层是通过 AQS 来实现的,重入次数统计是AQS中状态state,默认情况下锁的实现是非公平锁
31. Synchronized 与 ReentrantLock 的区别?
相似点
都是采用加锁式同步,都是阻塞式同步,并且都是可重入的。
不同点
synchronized是Java语言的关键字,是原声语法层面上的互斥,通过jvm底层实现,RenntrantLock是JDK1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally 语句块来完成
synchronized是Java提供的原子性内置锁机制,不可被中断,如果线程A不释放线程B一直等待;使用ReentrantLock,如果线程A不释放,线程B等待了很长时间后,可以中断去做其他事。
ReentrantLock 只能修饰代码块,synchronized 可以修饰代码块和方法
synchronized的锁是非公平锁,ReentrantLock默认情况下是非公平锁,可以在构造器中传入boolean值设置锁是否为公平
synchronized中所对象的wait、notify、notifyAll方法可以实现一个隐含的条件;ReentrantLock可以同时绑定多个Condition对象
synchronized是Java语言关键字,自动释放锁;ReentrantLock需要配合try/finally语句,需要手动释放锁,否则会造成资源被永久占用
ReentrantLock 可以知道是否成功获得了锁,synchronized 不行
一定要认真研读
32. 什么是读写锁/公平锁/非公平锁/乐观锁悲观锁?
笔记来自于
Java中的"锁"事
线程要不要锁住同步资源?
锁住
悲观锁
认为自己在使用数据的时候,一定会有别的线程来修改数据,因此获取数据的时候会先加锁,确保数据不会被别的线程修改。synchronized关键字和Lock的实现类都是悲观锁
不锁柱
乐观锁
认为自己再使用数据的时候,不会有别的而线程来修改数据,所以不会添加锁,只是在更新数据的时候判之前有没有别的线程更新了这个数据。CAS无锁算法
CAS
需要读写的内存的值V
需要比较的值A
需要写入的新值B
只有V等于A的时候,CAS通过原子的方式用新值B来更新V,否则不会执行任何操作
缺点
ABA问题
CAS需要在操作值的时候检查内存值是否发生变化,没有发生变化才会更新内存值。如果A变成了B然后变成了A,那么值没有发生变化,但实际上是有变化的。添加版本号即可解决
JDK从1.5开始提供了AtomicStampedReference类来解决ABA问题,具体操作封装在compareAndSet()中。compareAndSet()首先检查当前引用和当前标志与预期引用和预期标志是否相等,如果都相等,则以原子方式将引用值和标志的值设置为给定的更新值。
循环时间开销大
CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销。
只能保证一个共享变量的原子操作
对一个共享变量执行操作时,CAS能够保证原子操作,但是对多个共享变量操作时,CAS是无法保证操作的原子性的
Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。
锁住同步资源失败,线程要不要阻塞?
阻塞
不阻塞
自旋锁
在许多场景里面,同步资源的锁定时间很短,为了一小段时间去切换线程,线程挂起和恢复现场会让系统得不偿失。线程切换可能会导致系统用户态到内核态的切换
可以让后面请求锁的线程先不放弃CPU执行时间,看持有锁的线程是否会很快释放锁。
缺点:不能替代阻塞,虽然避免线程切换的开销,但是要占用处理器时间。如果锁被占用时间很短,效果会很好。如果被占用的时间很长,自旋超过了限定的次数(默认10次)没有获取到锁,就应当挂起线程
自适应自旋锁
自适应意味着自旋的时间不再固定,由前一次在同一个锁上的自旋时间及锁拥有者的状态来决定。如果同一个锁对象,自旋时间持续相对更长的时间,进而允许自旋等待它等待更长的时间;如果自旋很少成功获得过,以后尝试获取这个锁可能忽略掉自旋的过程,直接阻塞线程,避免浪费处理器资源
多个线程竞争同步资源的流程细节有没有区别?
以下四种状态是synchronized关键字的四个锁状态(详见:锁升级)
不锁住资源,多个线程只能有一个能修改资源成功,其他线程会重试
无锁
无锁没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功
CAS
同一个线程执行同步资源时自动获取资源
偏向锁
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
多个线程竞争同步资源时,没有获取资源的线程自旋等待锁释放
轻量级锁
锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。
多个线程竞争同步资源时,没有获取资源的线程阻塞等待唤醒
重量级锁
升级为重量级锁时,锁标志的状态值变为“10”,此时Mark Word中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。
多个线程竞争锁时要不要排队?
排队
公平锁
多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁.
优点
等待锁的线程不会饿死
缺点
整体的吞吐效率相对非公平锁要低,等待队列中,除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大
先尝试插队,插队失败再排队
非公平锁
多个线程加锁时,直接尝试获取锁,获取不到才会等待队列的队尾等待,锁刚好可用,那么线程可以无需等待阻塞
优点
可以减少唤醒线程的开销,整体吞吐率高,线程有几率不阻塞直接获得锁
缺点
处于等待队列中的线程可能会饿死,或者等很久才会获得锁
ReentrantLock公平锁、非公平锁实现
内部有个类Sync,Sync继承自AQS,添加锁和释放锁大部分操作实际上在Sync中实现。
公平锁 FairSync、非公平锁 NonfairSync 都继承自 Sync,ReentratLock默认使用非公平锁
通过源码可以了解,公平锁相比于非公平锁的加锁源码中,多了hasQueuedPredecessors方法,主要是判断当前线程是否处于同步队列中的第一个。
公平锁是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时,不考虑队列等待的问题,直接尝试获取锁。
一个线程中的多个流程能不能获取同一把锁?
能
可重入锁
递归锁,在同一线程外层方法获取锁的时候,在进入内层方法会自动获取锁,不会因为之前获取过还没有释放而阻塞。ReentrantLock和synchronized都是可重入锁,可以一定程度避免死锁
不能
不可重入锁
锁只能被获取一次
实现
ReentrantLock 和 NoReentrantLock 都继承自AQS,父类 AQS 维护了一个同步状态status来计数重入次数
当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0 标识没有其他线程正在执行同步代码,则把status赋值为1,然后线程开始执行。如果status != 0,判断当前线程是否是获取到这个锁的线程,如果是的话 status + 1,且当前线程可以再次获取锁.
非可重入锁
直接尝试去获取并且更新当前status的值,如果status != 0 会导致锁获取失败,当前线程阻塞
多个线程能不能共享一把锁?
能
共享锁
可以被多个线程所持有,其他线程只能对加锁对象再加共享锁,不能加排它锁,获得锁的线程只能读取数据不能修改数据
不能
排他锁(互斥锁)
该锁只能被一个线程持有,如果一个线程对A加上排它锁之后,其他线程不能再对A添加任何类型的锁
实现
ReentrantReadWriteLock有两把锁 ReadLock 和 WriteLock 。可以发现ReadLock 和 WriteLock 靠内部类Sync实现的锁,是AQS的子类。
读锁是共享锁,写锁是互斥锁,在并发读的情况下,可以非常高效,有了很大的提升。AQS中有个state用来描述锁的数量。读写锁有两个锁,因此按位切分成两个部分,高16位 表示读锁的个数,低16位标识写锁的个数
写锁,加锁源码
获取当前锁的状态c
获取当前写锁的个数w
获取当前写锁的个数w
如果c等于0,表示没有锁,CAS增加写锁,并且将当前线程设置为锁的拥有者
如果c不等于0,w等于0或者当前线程不是排他锁的持有者返回失败
如果写入锁的数量大于最大数量 抛出异常
CAS 尝试获得锁
读锁,加锁源码
获取当前锁状态state
获取当前写锁数量,如果当前有写锁并且锁持有者不是当前线程,返回获取锁失败
获取读锁数量
如果当前读锁数量为最大数量 2的16次方-1 抛出Error
CAS尝试加锁数量(state)
如果读锁为0,设置第一访问线程为当前线程,第一次访问线程持有锁数量为1
如果当前访问线程为第一次获取读锁线程,第一次获取读锁线程持有锁数量加1
HoldCounter用来记录当前线程id和锁的持有数量;ThreadLocalHoldCounter继承于ThreadLocal,用来存放每个线程的holdcounter;
如果counter为null或者counter的线程id不是当前线程,从当前线程holds获取当前线程的counter(重写了initialValue,使得获取的时候如果没有值,就获取新创建的counter对象)
使得当前counter加一
33. 什么是线程池?
线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待监督管理者分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。
ThreadPoolExecutor
构造方法
corePoolSize:核心线程数量,当有新任务在execute()方法提交时,会有以下判断
如果运行的线程少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
如果线程池中的线程数量大于等于 corePoolSize 且小于 maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务;
如果设置的corePoolSize 和 maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;
如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务;
任务提交时,判断的顺序为 corePoolSize –> workQueue –> maximumPoolSize
maximumPoolSize:最大线程数量;
workQueue:等待队列,当任务提交时,如果线程池中的线程数量大于等于corePoolSize的时候,把该任务封装成一个Worker对象放入等待队列;
workQueue:保存等待执行的任务的阻塞队列,当提交一个新的任务到线程池以后, 线程池会根据当前线程池中正在运行着的线程的数量来决定对该任务的处理方式
keepAliveTime:线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;
threadFactory:它是ThreadFactory类型的变量,用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。
handler:它是RejectedExecutionHandler类型的变量,表示线程池的饱和策略。如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种策略处理该任务。
线程池工作流程
运行状态(线程池生命周期)
RUNNING
能接受新提交的任务,并且也能处理阻塞队列中的任务
SHUTDOWN
关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务
STOP
不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程
TIDYING
所有任务都已终止了,workerCount(有效线程数)为0
TREMINATED
在terminated()方法执行完后进入该状态
线程池拒绝策略
AbortPolicy:直接抛出异常,这是默认策略;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并尝试执行当前任务;
DiscardPolicy:直接丢弃任务,不抛出异常;
实现 RejectedExecutionHandler 接口进行自定义拒绝策略
任务缓冲
任务缓冲模块是线程池能够管理任务的核心部分。线程池的本质是对任务和线程的管理,而做到这一点最关键的思想就是将任务和线程两者解耦,不让两者直接关联,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
阻塞队列一般设置的时候需要传入队列大小,防止无限增长,导致OOM
ArrayBlockingQueue
一个用数组实现的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。支持公平锁和非公平锁
LinkedBlockingQueue
一个由链表结构组成的有界队列,此队列按照先进先出(FIFO)的原则对元素进行排序。此队列默认长度为Integer.MAX_VALUE,所以默认创建的该队列有容量危险
PriorityBlockingQueue
一个支持线程优先级排序的无界队列,默认自然序进行排序,也可以自定义实现compareTo()方法来指定元素排序规则,不能保证优先级元素的排序
DelayQueue
一个实PriorityBlockingQueue实现延迟获取的无界队列,在创建元素时,可以指定多久才能从队列中获取当前元素。只有延迟期满后才能从队列中获取元素
SynchronousQueue
一个不存储元素的阻塞队列,每个put操作必须等待take操作,否则不能添加元素。支持公平锁和非公平锁。SynchronousQueue的一个使用场景时在线程池里。Exectors.newCachedThreadPool()就使用了SynchronousQueue,这个线程池根据需要(新任务到来时),创建新的线程,如果由空闲线程则会重复使用,线程空闲了60秒后会被回收
LinkedTransferQueue
一个由链表结构组成的无界阻塞队列,相当于其他队列,LinkedrTansferQueue队列多了transfer和tryTransfer方法
LinkedBlockingDeque
一个由链表结构组成的双向阻塞队列。队列头部和尾部都可以添加和移除元素,多线程并发时,可以将锁的竞争最多降到一半
使用线程池可以带来的好处
降低资源消耗:通过池话技术重复利用已创建的线程,降低线程创建和销毁造成的损耗
提高响应速度:任务到达时,无需等待线程创建即可立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。
34. Java 里面有哪些内置的线程池?
Executors中提供了多种线程池快速创建方法,可以大致了解一下,自己在实践过程中还是采用ThreadPoolExecutor来进行构建,以便于自定义,同时理解线程池执行工作流程
固定长度的线程池,使用时,阻塞队列没有设置大小,大量请求来时队列耗费大量内存导致OOM。(LinkedBlockingQueue默认长度Integer.MAX_VALUE)
单线程的线程池,缺陷同上
缓存线程池:核心线程为0,最大线程非常大,动态创建特点;大量请求过来时会导致创建大量线程,可能导致OOM
35. 线程池 submit 和 execute 有什么区别?
execute只能提交Runnable类型任务,submit既能提交Runnable类型任务,也能提交Callable类型任务
exectue会直接抛出执行时的异常,submit会吃掉异常,在Future的get方法中将执行过程中的异常重新抛出
exectue所属的顶层接口是Executor,submit所属的顶层接口是ExecutorService。实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。
36. 如何查看线程池的运行状态?
ThreadPoolExecutor中有几个方法可以时我们监控到线程池目前的运行情况
getQueue().size()
排队线程数
getActiveCount()
当前活动线程数
getCompletedTaskCount()
执行完成线程数
getTaskCount()
总线程数
37. 如何合理地设置线程池参数?
线程池参数的配置往往和具体的任务有关系,IO密集型和CPU密集型往往是不一致的参数
理论
Ncpu = CPU数量,Ucpu = 目标CPU的使用率 0 <= Ucpu <= 1,W/C = 等待时间与计算时间的比例
最优的池大小为 Nthreads = Ncpu * Ucpu * (1 + W / C)
经验值
IO密集型一般采用2N,CPU密集型一般采用N+1
CPU密集型,假设的是CPU利用率达到100%,那么线程数就是CPU的核数,+1是为了某个线程恰好发生一个错误的时候而暂停,那么可以确保这种情况下CPU还能继续执行
IO密集型,假设所有操作时间几乎都是IO操作耗时,W/C的值为1,对应线程数为2N
其他做法
美团采用了动态化线程池设计
38. 如何关闭线程?
线程池关闭相关的方法主要如下
void shutdown
线程可以正常执行结束后再关闭线程池
首先会见线程设置成SHUTDOWN状态,然后中断没有正在运行的线程
正在执行的线程和已经在队列中的线程并不会被中断,等所有任务全部执行后才会关闭线程池
调用该方法后,如果还有新任务被提交,线程池会根据拒绝策略直接拒绝
List<Runnable> shutDownNow
中断没有正在运行的线程,向正在运行的线程发出中断通知,正在运行的线程接收到中断信号后选择处理
在队列中的人任务全部取消执行,并且转移到新的list队列中并返回
boolean awaitTermination
boolean isShutDown
判断是否开始停止线程池,不代表线程池已经停止
boolean isTerminated
判断线程池是否已经完全停止
Reentrantlock 详细解读
AQS的介绍
ReentrantReadWriteLock 详细介绍
39. AQS 是什么?底层原理是什么?
从设计模式角度看AQS
模板方法模式
定义一个操作中算法框架,将一些步骤继承到子类中,使得子类可以不改变算法的结构即可重新定义该算法中的特定步骤
行为型设计模式,针对不同场景,把对象在逻辑流程中的流转抽象成各种行为,模板方法模式实现的思想是把一套反复使用的优秀经验抽离成通用逻辑,一些特定步骤交由字类实现,提高可扩展性,灵活性
实现关键:有一个上层抽象类,将部分通用逻辑以具体方法的形式实现,不可重写。在抽象类中需要声明抽象方法,并将方法提供给子类实现特定步骤,给予子类更大的灵活性
AQS
AQS全称AbstractQueuedSynchronizer,AQS并不实现任何同步接口,它提供了一些可以被具体实现类直接调用的一些原子操作方法来重写相应的同步逻辑,子类们只需要重写改变state变量的protected方法.
ReentedLock通过静态内部类实现AQS模板,重写获取释放资源的方式,借助AQS提供的各种同步方式,比如获取资源失败重试、队列排队等待资源、获取资源出队列,来实现ReentedLock的同步器功能。
AQS底层原理
AQS框架
从上而下分为五层,从AQS对外暴漏的API到底层基础数据
自定义同步器接入时,只需要重写第一层所需要的部分方法即可,不需要关注底层具体实现的流程。
当自定义同步器进行加锁或者解锁操作,先经过第一层API进入到AQS内部方法,然后经过第二层锁获取,接着对获取锁失败的流程,进入第三四层的等待队列处理,这些都依赖于第五层的基础数据提供层
原理概述
AQS是Java中锁的基础,主要由两个队列组成。一个队列是同步队列,另一个是条件队列。
同步队列
1. 同步队列的队列头部是head,队列尾部是tail节点,head节点是一个空节点,同步队列是一个双向链表,通过next和prev连接所有节点
2. 所有的线程在竞争锁的时候都会创建一个Node节点,线程与节点绑定在一起,(如果是同步锁和排他锁不同之处是通过nextWaiter来区分的)并且添加到同步队列的尾部
3. head的第一个节点获取锁,其余节点都需要等待被唤醒
4. 同步队列中的节点会存在取消和null的情况(如:线程超时中断、线程更新节点的中间态),被取消和null的节点不能被唤醒,将会被视为无效节点
5. 一个线程只能被有效的前驱节点(取消和null的节点除外)唤醒
6. 持有锁的线程只能是有一个,其他有效节点对应的线程都会被挂起
1. 同步队列的队列头部是head,队列尾部是tail节点,head节点是一个空节点,同步队列是一个双向链表,通过next和prev连接所有节点
2. 所有的线程在竞争锁的时候都会创建一个Node节点,线程与节点绑定在一起,(如果是同步锁和排他锁不同之处是通过nextWaiter来区分的)并且添加到同步队列的尾部
3. head的第一个节点获取锁,其余节点都需要等待被唤醒
4. 同步队列中的节点会存在取消和null的情况(如:线程超时中断、线程更新节点的中间态),被取消和null的节点不能被唤醒,将会被视为无效节点
5. 一个线程只能被有效的前驱节点(取消和null的节点除外)唤醒
6. 持有锁的线程只能是有一个,其他有效节点对应的线程都会被挂起
子主题
条件队列
1. 一个同步队列可以对应多个条件队列
2. 条件队列是一个单向链表,通过nextWaiter来连接起来,条件队列的头节点是firstWaiter,尾节点是lastWaiter
3. 某个条件队列中满足条件的节点(被signal或signalAll方法唤醒的节点)才会被转移到同步队列
4. 条件队列中的被转移到同步队列的节点是从头节点开始,条件队列中被阻塞的线程会添加到队列的尾部
1. 一个同步队列可以对应多个条件队列
2. 条件队列是一个单向链表,通过nextWaiter来连接起来,条件队列的头节点是firstWaiter,尾节点是lastWaiter
3. 某个条件队列中满足条件的节点(被signal或signalAll方法唤醒的节点)才会被转移到同步队列
4. 条件队列中的被转移到同步队列的节点是从头节点开始,条件队列中被阻塞的线程会添加到队列的尾部
子主题
AQS的思想是,如果被请求的共享资源空闲,那么将当前请求的资源线程设置为有效的工作线程,将共享资源设置为锁定状态;
如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁分配。这个机制的主要实现是CLH队列变体实现的,将暂时获取不到锁的线程加入到队列中
如果共享资源被占用,需要一定的阻塞等待唤醒机制来保证锁分配。这个机制的主要实现是CLH队列变体实现的,将暂时获取不到锁的线程加入到队列中
CLH队列变体:虚拟双向列表(FIFO),通过将每条请求共享资源的线程封装成一个节点来实现锁的分配
AQS使用一个volatile的int类型成员变量来表示同步状态,通过内置的FIFO队列完成资源获取的排队工作,通过CAS完成对State值的修改
AQS数据结构(Node)
waitStatus
当前节点在队列中的状态
0
当一个Node被初始化的时候的默认值
CANCELLED(1)
线程获取锁的请求已经取消了
CONDITION(-2)
节点在等待队列中,节点线程等待唤醒
PROPAGATE(-3)
当前线程处在SHARED的情况下,该字段才会使用
SIGNAL(-1)
线程已经准备好了,等待资源释放
thread
处于该节点的线程
prev
前驱节点
predecessor
返回前驱节点,没有的话抛出npe
nextWaiter
指向下一个CONDITION状态的节点
next
后继指针
acquire和 release 方法要做的事请
acquire
1. 创建一个Node节点node(该节点可能是排他锁,也可以能是共享锁)
2. 将node添加到同步队列尾部,如果同步队列为空(初始情况下),需要先创建一个空的头节点,然后再添加到队列的尾部
3. 如果node的前驱节点是head,说明node是第一个节点,能够获取锁,需要将head修改成node,释放前驱节点的资源
4. 如果node的前驱节点不是head,说明获取锁失败,需要检测是否需要将node绑定的线程挂起,分以下几种情况:
(1)如果node的waitStatus已经被设置为SIGNAL 表示需要被挂起
(2)如果node的waitStatus设置为CANCEL表示该节点已经被取消,需要被去掉,并修改 node的prev,直到链接上一个有效的节点为止
(3)否则将node的waitStatus设置为SIGNAL,表示即将要被挂起
5. 如果需要将node绑定的线程挂起,则让出CPU,直到当前驱节点来唤起node才会开始继续从步骤3开始执行
2. 将node添加到同步队列尾部,如果同步队列为空(初始情况下),需要先创建一个空的头节点,然后再添加到队列的尾部
3. 如果node的前驱节点是head,说明node是第一个节点,能够获取锁,需要将head修改成node,释放前驱节点的资源
4. 如果node的前驱节点不是head,说明获取锁失败,需要检测是否需要将node绑定的线程挂起,分以下几种情况:
(1)如果node的waitStatus已经被设置为SIGNAL 表示需要被挂起
(2)如果node的waitStatus设置为CANCEL表示该节点已经被取消,需要被去掉,并修改 node的prev,直到链接上一个有效的节点为止
(3)否则将node的waitStatus设置为SIGNAL,表示即将要被挂起
5. 如果需要将node绑定的线程挂起,则让出CPU,直到当前驱节点来唤起node才会开始继续从步骤3开始执行
acquire方法:获取排他锁
tryAcquire(arg):对外提供的一个扩展方法,常用的锁都要实现这个方法,具体实现与锁相关
addWaiter(Node.EXCLUSIVE): 创建一个排他锁节点,并将该节点添加到同步队列尾部
acquireQueued:同步队列中的节点获取排他锁
shouldParkAfterFailedAcquire:检测线程获取锁失败以后是否需要被挂起
parkAndCheckInterrupt:将当前线程挂起,并检测当前线程是否中断
cancelAcquire:取消节点
unparkSuccessor:唤醒后继节点
release
(1)释放当前获取锁的线程持有的资源
(2)唤醒有效的一个后继节点
(2)唤醒有效的一个后继节点
release方法:释放排他锁
releaseShared方法:释放共享锁
await
实现await功能需要做的事情:
1. 创建一个CONDITION类型的节点,将该节点添加到条件队列
2. 释放已经获取的锁(因为只有当前线程先获取了锁才可能再调用Condition.await()方法)
3. 如果无法获取锁,线程挂起
1. 创建一个CONDITION类型的节点,将该节点添加到条件队列
2. 释放已经获取的锁(因为只有当前线程先获取了锁才可能再调用Condition.await()方法)
3. 如果无法获取锁,线程挂起
signal
实现signal功能需要做的事情:
1. 将条件队列中的节点加入同步队列
2. 唤醒线程
1. 将条件队列中的节点加入同步队列
2. 唤醒线程
同步状态state
private volatile int state
getState()
获取state的值
setState()
设置state的值
compareAndSetState()
使用CAS方式更新state
AQS重要方法
AQS提供了大量用于自定义同步器实现的Protected方法。自定义同步实现的相关方法也只是通过修改state字段来实现多线程的独占模式或者共享模式,自定义同步器需要实现以下方法
protected boolean isHeldExclusively()
该线程是否正在独占资源。只有用到Condition才需要去实现它。
protected boolean tryAcquire(int arg)
独占方式。arg为获取锁的次数,尝试获取资源,成功则返回True,失败则返回False。
protected boolean tryRelease(int arg)
独占方式。arg为释放锁的次数,尝试释放资源,成功则返回True,失败则返回False。
protected int tryAcquireShared(int arg)
共享方式。arg为获取锁的次数,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int arg)
共享方式。arg为释放锁的次数,尝试释放资源,如果释放后允许唤醒后续等待结点返回True,否则返回False。
AQS应用
ReentrantLock的可重入应用
state初始化为0,表示没有任何线程持有锁
当有线程持有该锁时,值会在原来的基础上+1,同一个线程多次获得锁,就会多次+1,这就是可重入的概念
解锁也是对改字段-1,一直到0,线程对锁释放
Semaphore
使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数,acquireShared会减少计数。
CountDownLatch
使用AQS同步状态来表示计数。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过。
ReentrantReadWriteLock
使用AQS同步状态中的16位保存写锁持有的次数,剩下的16位用于保存读锁的持有次数。
ThreadPoolExecutor
Worker利用AQS同步状态实现对独占线程变量的设置(tryAcquire和tryRelease)。
40. Java 中的 Fork Join 框架有什么用?
分支/合并框(Fork/Join)是Java7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。它使用工作窃取(work-stealing)算法,主要用于实现“分而治之”。
ForkJoinPool
充当fork/join框架里面的管理者,最原始的任务都要交给它才能处理。它负责控制整个fork/join有多少个workerThread,workerThread的创建,激活都是由它来掌控。它还负责workQueue队列的创建和分配,每当创建一个workerThread,它负责分配相应的workQueue。然后它把接到的活都交给workerThread去处理,它可以说是整个frok/join的容器。
ForkJoinTask
代表fork/join里面任务类型,一般用它的两个子类RecursiveTask、RecursiveAction。这两个区别在于RecursiveTask任务是有返回值,RecursiveAction没有返回值。任务的处理逻辑包括任务的切分都集中在compute()方法里面。
41. ThreadLocal 有什么用?
概念
ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。
数据结构
Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。
ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。
每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
它的key是ThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型。
GC之后Key是否为NULL?
Java引用类型
强引用:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
如果采用 new ThreadLocal<>().set(s); 这种方式,没有任何指向引用值,在GC之后key是会被回收的
如果先 ThreadLocal t = new ThreadLocal<>(); t.set(s); 这种方式,GC之后key不会被回收,t.get()是可以拿到的,还是有强引用存在
强引用存在
如果我们的强引用不存在的话,那么 key 就会被回收,也就是会出现我们 value 没被回收,key 被回收,导致 value 永远存在,出现内存泄漏。
set方法
主流程如图所示,具体细节在ThreadLocalMap中实现
ThreadLocalMap
Hash算法
int i = key.threadLocalHashCode & (len-1);
threadLocalHashCode值的计算
ThreadLocal中有一个属性为HASH_INCREMENT = 0x61c88647
每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c8864
这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。产生的哈希码分布很均匀
Hash冲突
采用黄金分隔数来作为hash计算因子,大大减少了Hash冲突的概率,但是还是会有冲突存在
采用线性探索的形式进行查找找到Entry的值为NULL的时候才放入槽中
如果有找到key值为NULL的槽,那么会向前查找entry不为NULL最前面的key为null 的下标,在后续会去清理过期的元素,然后会将key为NULL的替换掉,并向后搜索,如果有key一致的会删除掉。
清理过期Key
探测式清理
遍历散列数组,从开始位置向后探测清理过期数据,将过期数据的Entry设置为null,沿途中碰到未过期的数据则将此数据rehash后重新在table数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的Entry=null的桶中,使rehash后的Entry数据距离正确的桶的位置更近一些。
启发式清理
没有太多了解
扩容机制
执行启发式清理后没有清理任何数据,且当前散列数组中entry数量达到列表扩容的阈值 threshold (len*2/3)
再会执行一次探测式清理,看最终的大小是否大于等于 threshold 的 3 / 4来进行扩容
扩容是2倍扩容,然后会对所有元素进行rehash
get方法
通过key找出对应的桶下标,然后进行判断,如果是就直接返回
否则就继续向后进行搜索,如果遇到key为null的时候会进行探测式数据回收,然后数据迁移
接着继续向后搜索
使用场景
适用于变量在线程间隔离,在方法间共享的场景。
用户信息获取比较昂贵(调用服务查询),那么在ThreadLocal中缓存是比较合适的做法。后续可能会校验用户信息,或者用到用户信息等情况
缺点
线程池会重用固定几个线程,一旦线程重用有可能从ThreadLocal中获取到的是之前其他用户的请求
尽量在代理中使用try/finally块中进行回收清理
42. volatile 关键字有什么用?
概念:一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
禁止进行指令重排序。
原理和实现机制:观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令
确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;
即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5
volatile关键字能保证,执行到语句3时,语句1和语句2必定是执行完毕了的,且语句1和语句2的执行结果对语句3、语句4、语句5是可见的。
强制将对缓存的修改操作立即写入主存;
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
如果没有volatile关键字对变量进行修饰。有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。
如果是写操作,它会导致其他CPU中对应的缓存行无效。
eg1 : 单例模式 双重校验锁 - 顺序性特点
双重校验锁:为了在多线程情况下可以正常产生一个单例
volatile: 防止指令重排,导致对象未初始化,业务端拿到实例会有相应的风险
instance = new Singleton() 分成三个阶段,指令重排可能导致 2、3 阶段交换次序,并在未初始化时,返回给业务端的实例未进行初始化
1. 分配对象空间
2. 初始化对象
3. 设置instance指向分配的内存地址
在1.4版本之前可能会出现该情况,高版本的JDK不需要再关注该问题
public class Singleton {
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private static volatile Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
eg2: 开关控制利用可见性的特点
eg3: 状态标记利用顺序性特点
volatile 和 synchronized 的区别
使用上的区别
volatile 只能用于修饰实力变量或者类变量。 不可以修饰 方法,方法参数和局部变量,常量。
synchronized 关键字不能用于对变量的修饰,只能用于修饰方法或者语句块
volatile 修饰的变量可以为null ,synchronized 关键字同步语句块的monitor 对象不能为null
对原子性的保证
volatile 不能保证原子性
被 synchronized 关键字修饰的同步代码是无法被中途打断的, 保证代码的原子性
对可见性的保证
两者均可以保证共享资源在多线程间的可见性,但是实现机制完全不同。
synchronized 借助于JVM指令 monitor enter 和 monitor exit 对通过排他的方式使得同步代码串行化,在monitor exit 的时候所有的共享资源都会被刷新到主内存中
volatile 使用的是机器指令(偏硬件) lock 的方式迫使对其他线程工作内存中的数据失效,不得到驻内存中进行再次加载
对有序性的保证
volatile 关键字禁止JVM编译器以及处理器对其进行重排序,所以能够保证有序性
synchronized 关键字的顺序性是以程序的串行化执行换来的
其他
volatile 不会使线程陷入阻塞
synchronized 关键字会是线程进入阻塞状态
43. CyclicBarrier 有什么用?
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做 的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一 个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
默认的构造方法是 public CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 await 方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。
CyclicBarrier 还提供一个更高级的构造函数 public CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时,优先执行 barrierAction,方便处理更复杂的业务场景。
在线程池中使用 CyclicBarrier 时一定要注意线程的数量要多于 CyclicBarrier 实例中设置的阻塞线程的数量就会发生死锁。 调用 await() 方法的次数一定要等于屏障中设置的阻塞线程的数量,否则也会死锁。
44. CountDownLatch 有什么用?
使用AQS同步状态来表示计数。计数为0时,所有的Acquire操作(CountDownLatch的await方法)才可以通过。
45. CountDownLatch 与 CyclicBarrier 的区别?
二者都能让一个或多个线程阻塞等待,都可以用在多个线程间的协调,起到线程同步的作用。
CountDownLatch 的计数器只能使用一次,而 CyclicBarrier 的计数器可以反复 使用。
CountDownLatch.await 一般阻塞工作线程,所有的进行预备工作的线程执行 countDown,而 CyclicBarrier 通过工作线程调用 await 从而自行阻塞,直到所有工作线程达到指定屏障,所有的线程才会返回各自执行自己的工作。
在控制多个线程同时运行上,CountDownLatch 可以不限线程数量,而 CyclicBarrier 是固定线程数。
CyclicBarrier 还可以提供一个 barrierAction,合并多线程计算结果。
46. Semaphore 有什么用?
使用AQS同步状态来保存信号量的当前计数。tryRelease会增加计数,acquireShared会减少计数。
线程之间通信的一种方式,可以用来空值同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源
主要方法(令牌可以当作一个信号)
acquire()
获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。
acquire(int permits)
获取一个令牌,在获取到令牌、或者被其他线程调用中断、或超时之前线程一直处于阻塞状态。
acquireUninterruptibly()
获取一个令牌,在获取到令牌之前线程一直处于阻塞状态(忽略中断)。
tryAcquire()
尝试获得令牌,返回获取令牌成功或失败,不阻塞线程。
tryAcquire(long timeout, TimeUnit unit)
尝试获得令牌,在超时时间内循环尝试获取,直到尝试获取成功或超时返回,不阻塞线程。
release()
释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。
hasQueuedThreads()
等待队列里是否还存在等待线程。
getQueueLength()
获取等待队列里阻塞的线程数。
drainPermits()
清空令牌把可用令牌数置为0,返回清空令牌的数量。
availablePermits()
返回可用的令牌数量。
47. Exchanger 有什么用?
Exchanger是java 5引入的并发类,Exchanger顾名思义就是用来做交换的。这里主要是两个线程之间交换持有的对象。当Exchanger在一个线程中调用exchange方法之后,会等待另外的线程调用同样的exchange方法。两个线程都调用exchange方法之后,传入的参数就会交换。
两个主要方法
public V exchange(V x) throws InterruptedException
当这个方法被调用的时候,当前线程将会等待直到其他的线程调用同样的方法。当其他的线程调用exchange之后,当前线程将会继续执行。
public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException
第一个方法类似,区别是多了一个timeout时间。如果在timeout时间之内没有其他线程调用exchange方法,则会抛出TimeoutException。
48. LockSupport 有什么用?
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,或者唤醒。
concurrent包是基于AQS框架的,AQS借助于两个类:Unsafe(提供CAS操作)、LockSupport(提供park/unpark操作)
常用方法
public static void park(Object blocker);
暂停当前线程,不可重入
public static void parkNanos(Object blocker, long nanos);
暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline);
暂停当前线程,直到某个时间
public static void park();
无期限暂停当前线程
public static void parkNanos(long nanos);
暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline);
暂停当前线程,直到某个时间
public static void unpark(Thread thread);
恢复当前线程
public static Object getBlocker(Thread t);
49. Java 中原子操作的类有哪些?
引入Unsafe机制来实现非阻塞同步(乐观锁),基于CAS提供了一套原子工具类
分类
基本类型
AtomicBoolean
布尔类型原子类
AtomicInteger
整型原子类
AtomicLong
长整型原子类
引用类型
AtomicReference
引用类型原子类
AtomicarkableReference
带有标记位的引用类型原子类
AtomicStampedReference
带有版本号的引用类型原子类
数组类型
AtomicIntegerArray
整型数组原子类
AtomicLongArray
长整型数组原子类
AtomicReferenceArray
引用类型数组原子类
属性更新器类型
AtomicIntegerFieldUpdater
整型字段的原子更新器
AtomicLongFieldUpdater
长整型字段原子更新器
AtomicReferenceFieldUpdater
原子更新引用类型里的字段
源码分析(AtomicInteger)
public final int get()
获取当前值
public final int getAndSet(int newValue)
获取当前值,并设置新值
public final int getAndIncrement()
获取当前值,并自增
public final int getAndDecrement()
获取当前值,并自减
public final int getAndAdd(int delta)
获取当前值,并加上预期值
boolean compareAndSet(int expect, int update)
如果输入值(update)等于预期值,将该值设置为输入值
public final void lazySet(int newValue)
最终设置为 newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
value - value 属性使用 volatile 修饰,使得对 value 的修改在并发环境下对所有线程可见。
valueOffset - value 属性的偏移量,通过这个偏移量可以快速定位到 value 字段,这个是实现 AtomicInteger 的关键。
unsafe - Unsafe 类型的属性,它为 AtomicInteger 提供了 CAS 操作。
可能导致问题
ABA
解决方式
AtomicMarkableReference
使用一个布尔值变量作为标志,修改时在 true/false 切换
降低ABA发生的概率
AtomicStampedReference
使用一个整型值作为版本号。,每次更新前先比较版本号,一致才进行修改
从根本上解决ABA问题
50. Java并发容器有哪些?
ConcurrentHashMap:并发版HashMap 🌟
CopyOnWriteArrayList:并发版ArrayList 🌟
CopyOnWriteArraySet:并发Set
ConcurrentLinkedQueue:并发队列(基于链表)
ConcurrentLinkedDeque:并发队列(基于双向链表)
ConcurrentSkipListMap:基于跳表的并发Map
ConcurrentSkipListSet:基于跳表的并发Set
ArrayBlockingQueue:阻塞队列(基于数组) 🌟
LinkedBlockingQueue:阻塞队列(基于链表) 🌟
LinkedBlockingDeque:阻塞队列(基于双向链表)
PriorityBlockingQueue:线程安全的优先队列 🌟
SynchronousQueue:读写成对的队列
LinkedTransferQueue:基于链表的数据交换队列
DelayQueue:延时队列
52. 什么是幂等性?
可以理解为执行一次操作,和执行多次操作最终的效果是一致的
比如在和消息队列相关的业务中,我们最好将业务设计成幂等操作,这样如果发生了消息重复消费的场景,也可以保证不影响最终结果
Spring
AOP
作用
以动态和非侵入式的方式来增强服务功能。
实现原理
通过动态代理,如果在某个Bean配置了切面,会在创建这个Bean的时候,实际上是创建这个Bean的代理对象,后需对bean中方法的调用,实际上都是调用代理类重写的代理方法
两种动态代理实现
JDK动态代理
SpringAOP 默认使用JDK动态代理,如果类实现了接口,Spring就会使用这种方式实现动态代理
实现原理
通过反射,生成一个代理类,代理类实现了原来类的全部接口,并对接口中定义的所有方法进行了代理。当通过代理对象执行原来类方法时,代理类底层会通过繁盛机制,回调实现的InvocationHandler接口的invoker方法
优点
JDK动态代理时JDK原生的,不需要任何依赖即可使用
通过反射机制生成代理类的速度要比CGLIB操作字节码生成代理类的速度更快
缺点
使用JDK动态代理,被代理的类必须实现接口否则无法代理
JDK动态代理无法为没有在接口中定
CGLIB动态代理
JDK动态代理存在的限制是必须实现接口类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理类没有实现接口,那么Spring会使用CGLIB动态代理来生成代理对象,CGLIB直接操作字节码,生成类的字类,重写类的方法完成代理
实现原理
底层采用ASM字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写类所有可以重写的方法。在重写过程中,定义了额外的逻辑,织入到方法中进行增强。
优点
CGLIB代理的类不需要实现接口,CGLIB生成的代理类是直接继承自需要呗代理的类
生成的代理类是原来类的子类,意味着代理类可以为原来的那个类中,所有能够被子类重写的方法进行代理
生成的代理类,和自己编写编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以执行的效率高于JDK动态代理
缺点
CGLIB代理类使用的继承,意味着如果需要被代理的类是一个final类,无法使用CGLIB
CGLIB实现代理方法的方式是重写父类方法,无法对final方法或者private方法进行代理,子类无法重写这些方法
CGLIB生成代理类的方式通过字节码,这种方式生成的字节码要JDK反射生成的代理类速度慢
IOC
简介
控制反转,面向对象编程中的一种设计原则,用来解决计算机代码之间的耦合度
基本思想:借助“第三方”实现具有依赖关系的对象之间耦合
IOC容器会帮对象找相应的依赖对象并注入
DI
依赖注入,组件之间的依赖关系由容器在运行期决定。由容器动态将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多的功能,而是为了提升组件复用的频率,并且为系统搭建一个灵活、可扩展的平台
装配实践
@Component
最普通的组件,可以注入到Spring容器中进行管理
@Repository
作用于持久层
@Service
作用于业务逻辑层
@Controller
作用于表现层
注入实践
@Autowired
Spring提供的自动注入注解,按照byType注入
如果想要按照byName来装配,可以结合@Qualifier注解一起使用
@Resource
按照byName自动注入。@Resource有两个重要的属性 name和type
如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。
如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。
如果指定了type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。
如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。
Spring常见源码面试题
- Bean的生命周期
- Bean的生命周期
6. spring 中bean 的作用域有哪些
首先呢,Spring框架里面的IOC容器,可以非常方便的去帮助我们管理应用里面的Bean对象实例。
我们只需要按照Spring里面提供的xml或者注解等方式去告诉IOC容器,哪些Bean需要被IOC容器管理就行了。
其次呢,既然是Bean对象实例的管理,那意味着这些实例,是存在生命周期,也就是bean的作用域。
我们只需要按照Spring里面提供的xml或者注解等方式去告诉IOC容器,哪些Bean需要被IOC容器管理就行了。
其次呢,既然是Bean对象实例的管理,那意味着这些实例,是存在生命周期,也就是bean的作用域。
常规生命周期
singleton(单例)。 这是Spring默认的作用域。在整个应用程序上下文中,只存在一个bean实例。
这个bean将被缓存,每次请求时都返回相同的实例。适用于那些无状态的bean。
这个bean将被缓存,每次请求时都返回相同的实例。适用于那些无状态的bean。
prototype(原型)。 每次请求都会创建一个新的bean实例,不会被缓存。适用于那些有状态的bean。
在基于Spring框架下的Web应用里面,增加了一个会话纬度来控制Bean的生命周期,
主要有
主要有
Request(请求): 在一次HTTP请求(例如,一个HTTP请求到达Controller)中,会创建一个新的bean实例。适用于Web应用程序。
Session(会话): 在用户会话期间只有一个bean实例。适用于Web应用程序,同一个session共享同一个Bean实例,不同的session产生不同的Bean实例。
Global Session(全局会话): 这个作用域的bean只在portlet环境下有效,它代表整个portlet应用程序的会话。针对全局session纬度,共享同一个Bean实例
Custom Scope(自定义作用域): 您还可以定义自己的自定义作用域,以满足特定应用程序需求。这通常需要实现org.springframework.beans.factory.config.Scope接口。
设计模式
- Spring Cloud的实现原理/也可以用来回答微服务框架
-服务注册与发现
- Eureka
- Consul
- ZooKeeper
- Nacos
-服务调用与负载均衡
- Ribbon
负载均衡
- Feign
- LoadBalancer
负载均衡
- Zuul
API 网关
- Gateway
网关
-服务容错与熔断
- Hystrix
- Resilience4j
- Sentinel
- 配置中心
- Config
- Apollo
- Nacos
- 分布式事务
- Seata
- TCC
- XA
- SAGA
- AT
- BASE
- 分布式消息传递
RabbitMQ
RocketMQ
Kafka
- 分布式追踪
Spring Cloud Sleuth
Zipkin
意义:
1、在Spring Cloud出现之前,为了解决微服务架构里面的各种技术问题,需要去集成各种开源框架,因为标准和兼容性问题,所以在实践的时候很麻烦。还有很些企业基于阿里的Dubbo进行二次开发自研出Dubbo X,而Spring Cloud统一了这样一个标准。
2、使用Spring Cloud降低了微服务架构的开发难度,只需要在Spring Boot的项目基础上通过Starter启动依赖集成相关组件就能轻松解决各种问题。
1、在Spring Cloud出现之前,为了解决微服务架构里面的各种技术问题,需要去集成各种开源框架,因为标准和兼容性问题,所以在实践的时候很麻烦。还有很些企业基于阿里的Dubbo进行二次开发自研出Dubbo X,而Spring Cloud统一了这样一个标准。
2、使用Spring Cloud降低了微服务架构的开发难度,只需要在Spring Boot的项目基础上通过Starter启动依赖集成相关组件就能轻松解决各种问题。
- 其他相关问题
- Spring的优点和缺点
- Spring的核心模块
- Spring的设计原则
- Spring的线程安全性
- Spring的并发性能
- Spring的扩展点
- Spring的版本演进
- Spring的未来发展方向将以上思维导图保存为txt文件,输出如下:
1.spring 中有两个id相同的bean 会报错吗?如果会报错在哪个阶段报出来?
首先,在同一个xml配置文件中,不能存在id相同的两个bean, 否则spring容器启动的时候会报错
因为id这个属性表示一个Bean的唯一标志符号,所以Spring在启动的时候会去验证id的唯一性,一旦发现重复就会报错,这个错误发生Spring对XML文件进行解析转化为BeanDefinition的阶段。
但是在两个不同的Spring配置文件里面,可以存在id相同的两个bean。 IOC容器在加载Bean的时候,默认会多个相同id的bean进行覆盖。
在Spring3.x版本以后,这个问题发生了变化,Spring3.x里面提供@Configuration注解去声明一个配置类,然后使用@Bean注解实现Bean的声明,这种方式完全取代了XMl。
在这种情况下,如果我们在同一个配置类里面声明多个相同名字的bean,在Spring IOC容器中只会注册第一个声明的Bean的实例。后续重复名字的Bean就不会再注册了。
但是在两个不同的Spring配置文件里面,可以存在id相同的两个bean。 IOC容器在加载Bean的时候,默认会多个相同id的bean进行覆盖。
在Spring3.x版本以后,这个问题发生了变化,Spring3.x里面提供@Configuration注解去声明一个配置类,然后使用@Bean注解实现Bean的声明,这种方式完全取代了XMl。
在这种情况下,如果我们在同一个配置类里面声明多个相同名字的bean,在Spring IOC容器中只会注册第一个声明的Bean的实例。后续重复名字的Bean就不会再注册了。
如果使用@Autowired注解根据类型实现依赖注入,因为IOC容器只注册了第一个实例,所以启动的时候会提示找不到第二个实例。
如果使用@Resource注解根据名称实现依赖注入,那么在IOC容器得到的实例对象是第一个实例,那么第二个实例就会提示类型不匹配错误,这个错误是会在spring ioc 容器里面的bean初始化之后的依赖注入阶段发生的
2. 如何理解spring boot 中的starter
Starter是Spring Boot的四大核心功能特性之一,Spring Boot里面的这些特性,都是为了让开发者在开发基于Spring生态下的企业级应用
时,只需要关心业务逻辑,减少对配置和外部环境的依赖。
时,只需要关心业务逻辑,减少对配置和外部环境的依赖。
自动配置(Auto-Configuration):
Spring Boot采用了“约定优于配置”的原则,通过自动配置功能自动设置应用程序所需的配置。
Spring Boot采用了“约定优于配置”的原则,通过自动配置功能自动设置应用程序所需的配置。
起步依赖(Starter Dependencies):
Spring Boot提供了一组预定义的"起步依赖",这些依赖封装了常用的功能集合,如Web应用、数据访问、消息队列等。
Spring Boot提供了一组预定义的"起步依赖",这些依赖封装了常用的功能集合,如Web应用、数据访问、消息队列等。
嵌入式Web服务器(Embedded Web Server):
Spring Boot支持多种嵌入式Web服务器,如Tomcat、Jetty和Undertow。
Spring Boot支持多种嵌入式Web服务器,如Tomcat、Jetty和Undertow。
生产就绪功能(Production-Ready Features):
Spring Boot提供了一系列生产就绪功能,如健康检查、指标收集、应用程序信息展示和外部化配置。
这些功能有助于监控和管理应用程序,以确保其在生产环境中稳定运行。
Spring Boot提供了一系列生产就绪功能,如健康检查、指标收集、应用程序信息展示和外部化配置。
这些功能有助于监控和管理应用程序,以确保其在生产环境中稳定运行。
其中,Starter是启动依赖,它的主要作用有几个。Starter组件以功能为维度,来维护对应的jar包的版本依赖,使得开发者可以不需要去关心这些版本冲突这种容易出错的细节。
Starter组件会把对应功能的所有jar包依赖全部导入进来,避免了开发者自己去引入依赖带来的麻烦。
Starter内部集成了自动装配的机制,也就说在程序中依赖对应的starter组件以后,这个组件自动会集成到Spring生态下,并且对于相关Bean的管理,也是基于自动装配机制来完成。
依赖Starter组件后,这个组件对应的功能所需要维护的外部化配置,会自动集成到Spring Boot里面,我们只需要在application.properties文件里面进行维护就行了,比如Redis这个starter,只需要在application.properties文件里面添加redis的连接信息就可以直接使用了。
在我看来,Starter组件几乎完美的体现了Spring Boot里面约定优于配置的理念。
Starter组件会把对应功能的所有jar包依赖全部导入进来,避免了开发者自己去引入依赖带来的麻烦。
Starter内部集成了自动装配的机制,也就说在程序中依赖对应的starter组件以后,这个组件自动会集成到Spring生态下,并且对于相关Bean的管理,也是基于自动装配机制来完成。
依赖Starter组件后,这个组件对应的功能所需要维护的外部化配置,会自动集成到Spring Boot里面,我们只需要在application.properties文件里面进行维护就行了,比如Redis这个starter,只需要在application.properties文件里面添加redis的连接信息就可以直接使用了。
在我看来,Starter组件几乎完美的体现了Spring Boot里面约定优于配置的理念。
子主题
另外,Spring Boot官方提供了很多的Starter组件,比如Redis、JPA、MongoDB等等。但是官方并不一定维护了所有中间件的Starter,所以对于不存在的Starter,第三方组件一般会自己去维护一个。
官方的starter和第三方的starter组件,最大的区别在于命名上。
官方维护的starter的以spring-boot-starter开头的前缀。
第三方维护的starter是以spring-boot-starter结尾的后缀
这也是一种约定优于配置的体现。
官方的starter和第三方的starter组件,最大的区别在于命名上。
官方维护的starter的以spring-boot-starter开头的前缀。
第三方维护的starter是以spring-boot-starter结尾的后缀
这也是一种约定优于配置的体现。
3.为什么使用spring框架
Spring是一个轻量级应用框架,它提供了IoC和AOP这两个核心的功能。
它的核心目的是为了简化企业级应用程序的开发,使得开发者只需要关心业务需求,不需
要关心Bean的管理,以及通过切面增强功能减少代码的侵入性。
从Spring本身的特性来看,我认为有几个关键点是我们选择Spring框架的原因。
轻量:Spring是轻量的,基本的版本大约2MB。
IOC/DI:Spring通过IOC容器实现了Bean的生命周期的管理,以及通过DI实现依赖注入,从而实现了对象依赖的松耦合管理。
面向切面的编程(AOP):Spring支持面向切面的编程,从而把应用业务逻辑和系统服务分开。
MVC框架:Spring MVC提供了功能更加强大且更加灵活的Web框架支持
事务管理:Spring通过AOP实现了事务的统一管理,对应用开发中的事务处理提供了非常灵活的支持
最后,Spring从第一个版本发布到现在,它的生态已经非常庞大了。在业务开发领域,
Spring生态几乎提供了非常完善的支持,更重要的是社区的活跃度和技术的成熟度都非常高,以上就是我对这个
问题的理解。
它的核心目的是为了简化企业级应用程序的开发,使得开发者只需要关心业务需求,不需
要关心Bean的管理,以及通过切面增强功能减少代码的侵入性。
从Spring本身的特性来看,我认为有几个关键点是我们选择Spring框架的原因。
轻量:Spring是轻量的,基本的版本大约2MB。
IOC/DI:Spring通过IOC容器实现了Bean的生命周期的管理,以及通过DI实现依赖注入,从而实现了对象依赖的松耦合管理。
面向切面的编程(AOP):Spring支持面向切面的编程,从而把应用业务逻辑和系统服务分开。
MVC框架:Spring MVC提供了功能更加强大且更加灵活的Web框架支持
事务管理:Spring通过AOP实现了事务的统一管理,对应用开发中的事务处理提供了非常灵活的支持
最后,Spring从第一个版本发布到现在,它的生态已经非常庞大了。在业务开发领域,
Spring生态几乎提供了非常完善的支持,更重要的是社区的活跃度和技术的成熟度都非常高,以上就是我对这个
问题的理解。
4.为什么越来越多的人选择spring boot
1.java web开发史
在最初发布的Java版本中,包含Java SE、JavaEE、JavaME。
Java SE(Standard Edition)作为标准版本,提供最核心的基础功能,
Java EE(Enterprise Edition)作为企业版,主要用于企业级的Web开发,
JavaME(Micro Edition)作为微型版本主要应用与移动设备的开发。
随着Java的语言的广泛应用,Java也找到了自身的优势,Java EE版本被应用得最多。
从1996年开始,JavaEE开发是基于JSP + Java Bean来完成的。
后来慢慢地演变,1997年官方推出了 JSP + Servlet + Java Bean来进行开发,
Servlet起到了调度控制的作用,这是MVC设计的雏形。
然后,发展出现了 JSP + Sevlet + Java Bean + Dao的模式,将业务逻辑处理和数据库访问分离,出现了三层架构设计理念。
再后来,1998年前后,为了满足多服务器之间的通信,采用了JSP + Servlet + EJB的形式,出现了RPC设计的雏形。
之后,继续演变,在2006年首次出现 JSF + EJB + JPA,提出前后端完全隔离开发的思想。
当然,这些技术都是Java官方提供的。
Java SE(Standard Edition)作为标准版本,提供最核心的基础功能,
Java EE(Enterprise Edition)作为企业版,主要用于企业级的Web开发,
JavaME(Micro Edition)作为微型版本主要应用与移动设备的开发。
随着Java的语言的广泛应用,Java也找到了自身的优势,Java EE版本被应用得最多。
从1996年开始,JavaEE开发是基于JSP + Java Bean来完成的。
后来慢慢地演变,1997年官方推出了 JSP + Servlet + Java Bean来进行开发,
Servlet起到了调度控制的作用,这是MVC设计的雏形。
然后,发展出现了 JSP + Sevlet + Java Bean + Dao的模式,将业务逻辑处理和数据库访问分离,出现了三层架构设计理念。
再后来,1998年前后,为了满足多服务器之间的通信,采用了JSP + Servlet + EJB的形式,出现了RPC设计的雏形。
之后,继续演变,在2006年首次出现 JSF + EJB + JPA,提出前后端完全隔离开发的思想。
当然,这些技术都是Java官方提供的。
2. spring 演变
使用之前需要完成大量的个性化配置,xml maven 依赖
为了简化开发,14年发布了全新的开源轻量级开源框架,spring boot1.0 版本
spring boot 的核心功能
(1)可以独立运行的spring 项目
(2)内嵌的servlet容器
(3)提供starter简化maven 依赖
(4)自动配置spring
(5)无代码生成。 无xml配置
5.spring 如何解决循环依赖问题
什么是循环依赖
循环依赖就是指循环引用,是两个或多个Bean相互之间的持有对方的引用。在代码中,
如果将两个或多个Bean互相之间持有对方的引用,因为Spring中加入了依赖注入机制,也
就是自动给属性赋值。Spring给属性赋值时,将会导致死循环。
如果将两个或多个Bean互相之间持有对方的引用,因为Spring中加入了依赖注入机制,也
就是自动给属性赋值。Spring给属性赋值时,将会导致死循环。
哪些情况会出现循环依赖
1、相互依赖,也就是A 依赖 B,B 又依赖 A,它们之间形成了循环依赖
2、三者间依赖,也就是A 依赖 B,B 依赖 C,C 又依赖 A,形成了循环依赖。
3、自我依赖,也是A依赖A形成了循环依赖自己依赖自己。
如何解决循环依赖问题
Spring通过三级缓存解决了循环依赖,其中一级缓存为单例池(singletonObjects), -- 成熟的bean
二级缓存为早期曝光对象earlySingletonObjects, -- 二级缓存map 为了保证单例
三级缓存为早期曝光对象工厂(singletonFactories)。
Spring中设计了三级缓存来解决循环依赖问题,当我们去调用getBean()方法的时候,Spring会先从一级缓存中去找到目标Bean,如果发现一级缓存中没有便会去二级缓存中去找,而如果一、二级缓存中都没有找到,意味着该目标Bean还没有实例化。
于是,Spring容器会实例化目标Bean(PS:刚初始化的Bean称为早期Bean) 。然后,将目标Bean放入到二级缓存中,同时,加上标记是否存在循环依赖。如果不存在循环依赖便会将目标Bean存入到二级缓存,否则,便会标记该Bean存在循环依赖,然后将等待下一次轮询赋值,也就是解析@Autowired注解。等@Autowired注解赋值完成后,会将目标Bean存入到一级缓存。
Spring一级缓存中存放所有的成熟Bean,二级缓存中存放所有的早期Bean,先取一级缓存,再去二级缓存
二级缓存为早期曝光对象earlySingletonObjects, -- 二级缓存map 为了保证单例
三级缓存为早期曝光对象工厂(singletonFactories)。
Spring中设计了三级缓存来解决循环依赖问题,当我们去调用getBean()方法的时候,Spring会先从一级缓存中去找到目标Bean,如果发现一级缓存中没有便会去二级缓存中去找,而如果一、二级缓存中都没有找到,意味着该目标Bean还没有实例化。
于是,Spring容器会实例化目标Bean(PS:刚初始化的Bean称为早期Bean) 。然后,将目标Bean放入到二级缓存中,同时,加上标记是否存在循环依赖。如果不存在循环依赖便会将目标Bean存入到二级缓存,否则,便会标记该Bean存在循环依赖,然后将等待下一次轮询赋值,也就是解析@Autowired注解。等@Autowired注解赋值完成后,会将目标Bean存入到一级缓存。
Spring一级缓存中存放所有的成熟Bean,二级缓存中存放所有的早期Bean,先取一级缓存,再去二级缓存
respurce
为什么需要三级缓存而不是二级缓存
如果要使用二级缓存解决循环依赖,意味着所有Bean在实例化后就要完成AOP代理,这样违背了Spring设计的原则,
Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来
在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。
Spring在设计之初就是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来
在Bean生命周期的最后一步来完成AOP代理,而不是在实例化后就立马进行AOP代理。
spring 哪些情况下不能解决循环依赖的问题
1.多例Bean通过setter注入的情况,不能解决循环依赖问题
2.构造器注入的Bean的情况,不能解决循环依赖问题
3.单例的代理Bean通过Setter注入的情况,不能解决循环依赖问题
4.设置了@DependsOn的Bean的情况,不能解决循环依赖问题
2.构造器注入的Bean的情况,不能解决循环依赖问题
3.单例的代理Bean通过Setter注入的情况,不能解决循环依赖问题
4.设置了@DependsOn的Bean的情况,不能解决循环依赖问题
Beanfactory 和 FactoryBean 的区别
Spring里面的核心功能是IOC容器,所谓IOC容器呢,本质上就是一个Bean的容器
或者是一个Bean的工厂。
它能够根据xml里面声明的Bean配置进行bean的加载和初始化,然后BeanFactory来生产
我们需要的各种各样的Bean。
所以一方面,BeanFactory是所有Spring Bean容器的顶级接口,它为Spring的容器定义了一套规范,
并提供像getBean这样的方法从容器中获取指定的Bean实例。
另一方面,BeanFactory在产生Bean的同时,还提供了解决Bean之间的依赖注入的能力,也就是所
谓的DI。
FactoryBean是一个工厂Bean,它是一个接口,主要的功能是动态生成某一个类型的
Bean的实例,也就是说,我们可以自定义一个Bean并且加载到IOC容器里面。
它里面有一个重要的方法叫getObject(),这个方法里面就是用来实现动态构建Bean的过
程。
Spring Cloud里面的OpenFeign组件,客户端的代理类,就是使用了FactoryBean来实现
的
或者是一个Bean的工厂。
它能够根据xml里面声明的Bean配置进行bean的加载和初始化,然后BeanFactory来生产
我们需要的各种各样的Bean。
所以一方面,BeanFactory是所有Spring Bean容器的顶级接口,它为Spring的容器定义了一套规范,
并提供像getBean这样的方法从容器中获取指定的Bean实例。
另一方面,BeanFactory在产生Bean的同时,还提供了解决Bean之间的依赖注入的能力,也就是所
谓的DI。
FactoryBean是一个工厂Bean,它是一个接口,主要的功能是动态生成某一个类型的
Bean的实例,也就是说,我们可以自定义一个Bean并且加载到IOC容器里面。
它里面有一个重要的方法叫getObject(),这个方法里面就是用来实现动态构建Bean的过
程。
Spring Cloud里面的OpenFeign组件,客户端的代理类,就是使用了FactoryBean来实现
的
7. spring 中事务的传播行为有哪些
举个栗子,方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。
tips:
Spring中事务的默认实现使用的是AOP,也就是代理的方式,如果大家在使用代码测试时,同一个Service类中的方法相互调用需要使用注入的对象来调用,不要直接使用this.方法名来调用,this.方法名调用是对象内部方法调用,不会通过Spring代理,也就是事务不会起作用
tips:
Spring中事务的默认实现使用的是AOP,也就是代理的方式,如果大家在使用代码测试时,同一个Service类中的方法相互调用需要使用注入的对象来调用,不要直接使用this.方法名来调用,this.方法名调用是对象内部方法调用,不会通过Spring代理,也就是事务不会起作用
REQUIRED:默认的Spring事物传播级别,如果当前存在事务,则加入这个事务,如果不
存在事务,就新建一个事务。
存在事务,就新建一个事务。
REQUIRE_NEW:不管是否存在事务,都会新开一个事务,新老事务相互独立。外部事务
抛出异常回滚不会影响内部事务的正常提交
抛出异常回滚不会影响内部事务的正常提交
NESTED:如果当前存在事务,则嵌套在当前事务中执行。如果当前没有事务,则新建一
个事务,类似于REQUIRE_NEW。
个事务,类似于REQUIRE_NEW。
SUPPORTS:表示支持当前事务,如果当前不存在事务,以非事务的方式执行。
NOT_SUPPORTED:表示以非事务的方式来运行,如果当前存在事务,则把当前事务挂起。
MANDATORY:强制事务执行,若当前不存在事务,则抛出异常.
NEVER:以非事务的方式执行,如果当前存在事务,则抛出异常
8. spring 里面的事务和分布式事务的使用如何区分,以及这两个事务之间有什么关联
首先, 在Spring里面并没有提供事务,它只是提供了对数据库事务管理的封装。
通过声明式的事务配置,使得开发人员可以从一些复杂的事务处理中得到解脱,
我们不再需要关心连接的获取、连接的关闭、事务提交、事务回滚这些操作。
更加聚焦在业务开发层面。
所以,Spring里面的事务,本质上就是数据库层面的事务,这种事务的管理,主要是针对
单个数据库里面多个数据表操作的,去满足事务的ACID特性。
通过声明式的事务配置,使得开发人员可以从一些复杂的事务处理中得到解脱,
我们不再需要关心连接的获取、连接的关闭、事务提交、事务回滚这些操作。
更加聚焦在业务开发层面。
所以,Spring里面的事务,本质上就是数据库层面的事务,这种事务的管理,主要是针对
单个数据库里面多个数据表操作的,去满足事务的ACID特性。
分布式事务,是解决多个数据库的事务操作的数据一致性问题,传统的关系型数据库不支
持跨库事务的操作,所以需要引入分布式事务的解决方案。
而Spring并没有提供分布式事务场景的支持,所以Spring事务和分布式事务在使用上并没
有直接的关联性。
但是我们可以使用一些主流的事务解决框架,比如Seata,集成到Spring生态里面,去解
决分布式事务的问题。
持跨库事务的操作,所以需要引入分布式事务的解决方案。
而Spring并没有提供分布式事务场景的支持,所以Spring事务和分布式事务在使用上并没
有直接的关联性。
但是我们可以使用一些主流的事务解决框架,比如Seata,集成到Spring生态里面,去解
决分布式事务的问题。
- 分布式事务
- Seata
- TCC
- XA
- SAGA
- AT
- BASE
9.spring bean 生命周期的执行流程
Spring 中 Bean 的生命周期是指:Bean 在 Spring(IoC)中从创建到销毁的整个过程。
Spring 中 Bean 的生命周期主要包含以下 5 部分:
1. 实例化:为 Bean 分配内存空间; ——创建前的准备阶段
2. 设置属性:将当前类依赖的 Bean 属性,进行注入和装配; ——创建实例阶段
3. 初始化: ——依赖注入阶段
(1)执行各种通知;
(2)执行初始化的前置方法;
(3)执行初始化方法;
(4)执行初始化的后置方法。
4. 使用 Bean:在程序中使用 Bean 对象; ——容器缓存
5. 销毁 Bean:将 Bean 对象进行销毁操作。 ——销毁实例
以上生命周期中,需要注意的是:“实例化”和“初始化”是两个完全不同的过程,千万不要搞混,实例化只是给 Bean 分配了内存空间,而初始化则是将程序的执行权,从系统级别转换到用户级别,并开始执行用户添加的业务代码。
Spring 中 Bean 的生命周期主要包含以下 5 部分:
1. 实例化:为 Bean 分配内存空间; ——创建前的准备阶段
2. 设置属性:将当前类依赖的 Bean 属性,进行注入和装配; ——创建实例阶段
3. 初始化: ——依赖注入阶段
(1)执行各种通知;
(2)执行初始化的前置方法;
(3)执行初始化方法;
(4)执行初始化的后置方法。
4. 使用 Bean:在程序中使用 Bean 对象; ——容器缓存
5. 销毁 Bean:将 Bean 对象进行销毁操作。 ——销毁实例
以上生命周期中,需要注意的是:“实例化”和“初始化”是两个完全不同的过程,千万不要搞混,实例化只是给 Bean 分配了内存空间,而初始化则是将程序的执行权,从系统级别转换到用户级别,并开始执行用户添加的业务代码。
创建准备阶段:这个阶段主要是在开始Bean加载之前,从Spring上下文和相关配置中解析并查找Bean有关的配置内容,
比如`init-method`-容器在初始化bean时调用的方法、`destory-method`,容器在销毁Bean时调用的方法。以及,BeanFactoryPostProcessor这类的bean加载过程中的前置和后置处理。这些类或者配置其实是Spring提供给开发者,用来实现Bean加载过程中的扩展机制,在很多和Spring集成的中间件经常使用,比如Dubbo。
比如`init-method`-容器在初始化bean时调用的方法、`destory-method`,容器在销毁Bean时调用的方法。以及,BeanFactoryPostProcessor这类的bean加载过程中的前置和后置处理。这些类或者配置其实是Spring提供给开发者,用来实现Bean加载过程中的扩展机制,在很多和Spring集成的中间件经常使用,比如Dubbo。
创建实例阶段:这个阶段主要是通过反射来创建Bean的实例对象,并且扫描和解析Bean声明的一些属性。
依赖注入阶段:在这个阶段,会检测被实例化的Bean是否存在其他依赖,如果存在其他依赖,就需要对这些被依赖Bean进行注入。比如通过读取
`@Autowired`、@Setter等依赖注入的配置。在这个阶段还会触发一些扩展的调用,
比如常见的扩展类:BeanPostProcessors(用来实现Bean初始化前后的回调)、
InitializingBean类(这个类有一个afterPropertiesSet()方法,给属性赋值)、
还有BeanFactoryAware等等。
`@Autowired`、@Setter等依赖注入的配置。在这个阶段还会触发一些扩展的调用,
比如常见的扩展类:BeanPostProcessors(用来实现Bean初始化前后的回调)、
InitializingBean类(这个类有一个afterPropertiesSet()方法,给属性赋值)、
还有BeanFactoryAware等等。
容器缓存阶段:
容器缓存阶段主要是把Bean保存到IoC容器中缓存起来,到了这个阶段,Bean就可以被开发者使用了。
这个阶段涉及到的操作,常见的有,`init-method`这个属性配置的方法,会在这个阶段调用。
在比如BeanPostProcessors方法中的后置处理器方法如:postProcessAfterInitialization,也是在这个阶段触发的。
容器缓存阶段主要是把Bean保存到IoC容器中缓存起来,到了这个阶段,Bean就可以被开发者使用了。
这个阶段涉及到的操作,常见的有,`init-method`这个属性配置的方法,会在这个阶段调用。
在比如BeanPostProcessors方法中的后置处理器方法如:postProcessAfterInitialization,也是在这个阶段触发的。
销毁实例阶段:这个阶段,是完成Spring应用上下文关闭时,将销毁Spring上下文中所有的Bean。
如果Bean实现了DisposableBean接口,或者配置了`destory-method`属性,将会在这个阶段被调用。
如果Bean实现了DisposableBean接口,或者配置了`destory-method`属性,将会在这个阶段被调用。
10. spring 中有哪些方式可以把 bean注入到 ioc容器
使用xml的方式来声明Bean的定义,Spring容器在启动的时候会加载并解析这个xml,把bean装载到IOC容器中。
使用@CompontScan注解来扫描声明了@Controller、@Service、@Repository、 @Component注解的类。
使用@Configuration注解声明配置类,并使用@Bean注解实现Bean的定义,这种方式其实是xml配置方式的一种演变,是Spring迈入到无配置化时代的里程碑。
使用@Import注解,导入配置类或者普通的Bean。使用FactoryBean工厂bean,动态构建一个Bean实例,Spring Cloud OpenFeign里面的动态代理实例就是使用FactoryBean来实现的。
实现ImportBeanDefinitionRegistrar接口,可以动态注入Bean实例。这个在Spring Boot里面的启动注解有用到。
实现ImportSelector接口,动态批量注入配置类或者Bean对象,这个在Spring Boot里面的自动装配机制里面有用到
通过Java代码: 在Java代码中通过ApplicationContext接口手动创建Bean并将它们注册到容器。
11. spring boot 中自动装配机制的原理
在Spring Boot应用里面,只需要在启动类加上@SpringBootApplication注解就可以实
现自动装配。 @SpringBootApplication是一个复合注解,真正实现自动装配的注解是
@EnableAutoConfiguration。
在我看来,SpringBoot是约定优于配置这一理念下的产物,所以在很多的地方,都会看到
这类的思想。它的出现,让开发人员更加聚焦在了业务代码的编写上,而不需要去关心和
业务无关的配置。
其实,自动装配的思想,在SpringFramework3.x版本里面的@Enable注解,就有了实现
的雏形。@Enable注解是模块驱动的意思,我们只需要增加某个@Enable注解,就自动打
开某个功能,而不需要针对这个功能去做Bean的配置,@Enable底层也是帮我们去自动
完成这个模块相关Bean的注入。
在Spring Boot应用里面,只需要在启动类加上@SpringBootApplication注解就可以实
现自动装配。 @SpringBootApplication是一个复合注解,真正实现自动装配的注解是
@EnableAutoConfiguration。
在我看来,SpringBoot是约定优于配置这一理念下的产物,所以在很多的地方,都会看到
这类的思想。它的出现,让开发人员更加聚焦在了业务代码的编写上,而不需要去关心和
业务无关的配置。
其实,自动装配的思想,在SpringFramework3.x版本里面的@Enable注解,就有了实现
的雏形。@Enable注解是模块驱动的意思,我们只需要增加某个@Enable注解,就自动打
开某个功能,而不需要针对这个功能去做Bean的配置,@Enable底层也是帮我们去自动
完成这个模块相关Bean的注入。
什么是springboot 自动装配
自动装配,简单来说就是自动把第三方组件的Bean装载到Spring IOC器里面,不需要开发人员再去写Bean的装配配置。
SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot 在启动时会扫描外部引用 jar 包中的META-INF/spring.factories文件,将文件中配置的类型信息加载到 Spring 容器,并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装置进 SpringBoot。
SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot 在启动时会扫描外部引用 jar 包中的META-INF/spring.factories文件,将文件中配置的类型信息加载到 Spring 容器,并执行类中定义的各种操作。对于外部 jar 来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装置进 SpringBoot。
spring boot是如何实现自动装配的?如何按需加载
SpringBoot 的核心注解 SpringBootApplication
大概可以把 @SpringBootApplication看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:
@Configuration:允许在上下文中注册额外的 bean 或导入其他配置类
@ComponentScan: 扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。容器中将排除TypeExcludeFilter和AutoConfigurationExcludeFilter。
@EnableAutoConfiguration 启用 SpringBoot 的自动配置机制;也是实现自动装配的重要注解;
根据源码可以看出 EnableAutoConfiguration 只是一个简单地注解,自动装配核心功能的实现实际是通过 AutoConfigurationImportSelector类。
大概可以把 @SpringBootApplication看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan 注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:
@Configuration:允许在上下文中注册额外的 bean 或导入其他配置类
@ComponentScan: 扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。容器中将排除TypeExcludeFilter和AutoConfigurationExcludeFilter。
@EnableAutoConfiguration 启用 SpringBoot 的自动配置机制;也是实现自动装配的重要注解;
根据源码可以看出 EnableAutoConfiguration 只是一个简单地注解,自动装配核心功能的实现实际是通过 AutoConfigurationImportSelector类。
AutoConfigurationImportSelector:加载自动装配类
可以看出,AutoConfigurationImportSelector 类实现了 ImportSelector接口,也就实现了这个接口中的 selectImports方法,该方法主要用于获取所有符合条件的类的全限定类名,这些类需要被加载到 IoC 容器中。
// 这里我们需要重点关注一下getAutoConfigurationEntry()方法,这个方法主要负责加载自动配置类的。 -- 讲下以下步骤:
// 这里我们需要重点关注一下getAutoConfigurationEntry()方法,这个方法主要负责加载自动配置类的。 -- 讲下以下步骤:
第一步:判断自动装配开关是否打开。默认spring.boot.enableautoconfiguration=true,可在 application.properties 或 application.yml 中设置。
第二步:用于获取EnableAutoConfiguration注解中的 exclude 和 excludeName。
第三步:获取需要自动装配的所有配置类,读取META-INF/spring.factories
第四步:“spring.factories中这么多配置,每次启动都要全部加载么?”。
很明显,这是不现实的。这一步有经历了一遍筛选,@ConditionalOnXXX 中的所有条件都满足,该类才会生效。
很明显,这是不现实的。这一步有经历了一遍筛选,@ConditionalOnXXX 中的所有条件都满足,该类才会生效。
如何实现一个starter
第一步,创建threadpool-spring-boot-starter工程
第二步,引入 Spring Boot 相关依赖
第三步,创建ThreadPoolAutoConfiguration
第四步,在threadpool-spring-boot-starter工程的 resources 包下创建META-INF/spring.factories文件
最后新建工程引入threadpool-spring-boot-starter
总结
Spring Boot 通过@EnableAutoConfiguration开启自动装配,通过 SpringFactoriesLoader 最终加载META-INF/spring.factories中的自动配置类实现自动装配,自动配置类其实就是通过@Conditional按需加载的配置类,想要其生效必须引入spring-boot-starter-xxx包实现起步依赖
12. spring boot 的约定优于配置,你的理解是什么?
首先,约定优于配置是一种软件设计的范式,它的核心思想是减少软件开发人员对于配置
项的维护,从而让开发人员更加聚焦在业务逻辑上。
Spring Boot就是约定优于配置这一理念下的产物,它类似于Spring框架下的一个脚手架,
通过Spring Boot,我们可以快速开发基于Spring生态下的应用程序。
基于传统的Spring框架开发web应用,我们需要做很多和业务开发无关并且只需要做一次
的配置,比如
(1)管理jar包依赖
(2)web.xml维护
(3)Dispatch-Servlet.xml配置项维护
(4)应用部署到Web容器
(5)第三方组件集成到Spring IOC容器中的配置项维护
而在Spring Boot中,我们不需要再去做这些繁琐的配置,Spring Boot已经自动帮我们完成了,这就是约定由于配置思想的体现。
Spring Boot约定由于配置的体现有很多,比如
(1)Spring Boot Starter启动依赖,它能帮我们管理所有jar包版本
(2)如果当前应用依赖了spring mvc相关的jar,那么Spring Boot会自动内置Tomcat容器来运行web应用,我们不需要再去单独做应用部署。
Spring Boot的自动装配机制的实现中,通过扫描约定路径下的spring.factories文件来识别配置类,实现Bean的自动装配。
默认加载的配置文件application.properties等等。
总的来说,约定优于配置是一个比较常见的软件设计思想,它的核心本质都是为了更高效
以及更便捷的实现软件系统的开发和维护。
项的维护,从而让开发人员更加聚焦在业务逻辑上。
Spring Boot就是约定优于配置这一理念下的产物,它类似于Spring框架下的一个脚手架,
通过Spring Boot,我们可以快速开发基于Spring生态下的应用程序。
基于传统的Spring框架开发web应用,我们需要做很多和业务开发无关并且只需要做一次
的配置,比如
(1)管理jar包依赖
(2)web.xml维护
(3)Dispatch-Servlet.xml配置项维护
(4)应用部署到Web容器
(5)第三方组件集成到Spring IOC容器中的配置项维护
而在Spring Boot中,我们不需要再去做这些繁琐的配置,Spring Boot已经自动帮我们完成了,这就是约定由于配置思想的体现。
Spring Boot约定由于配置的体现有很多,比如
(1)Spring Boot Starter启动依赖,它能帮我们管理所有jar包版本
(2)如果当前应用依赖了spring mvc相关的jar,那么Spring Boot会自动内置Tomcat容器来运行web应用,我们不需要再去单独做应用部署。
Spring Boot的自动装配机制的实现中,通过扫描约定路径下的spring.factories文件来识别配置类,实现Bean的自动装配。
默认加载的配置文件application.properties等等。
总的来说,约定优于配置是一个比较常见的软件设计思想,它的核心本质都是为了更高效
以及更便捷的实现软件系统的开发和维护。
14.spring ioc的工作流程
ioc 是什么
IOC的全称是Inversion Of Control, 也就是控制反转,它的核心思想是把对象的管理权限交给容器。
应用程序如果需要使用到某个对象实例,直接从IOC容器中去获取就行,这样设计的好处是降低了程序里面对象与对象之间的耦合性。
使得程序的整个体系结构变得更加灵活。
应用程序如果需要使用到某个对象实例,直接从IOC容器中去获取就行,这样设计的好处是降低了程序里面对象与对象之间的耦合性。
使得程序的整个体系结构变得更加灵活。
bean的声明方式
Spring里面很多方式去定义Bean,比如XML里面的<bean>标签、@Service、@Component、@Repository、@Configuration配置类中的@Bean注解等等。
Spring在启动的时候,会去解析这些Bean然后保存到IOC容器里面。
Spring在启动的时候,会去解析这些Bean然后保存到IOC容器里面。
ioc的工作流程
第一个阶段,就是IOC容器的初始化
这个阶段主要是根据程序中定义的XML或者注解等Bean的声明方式
通过解析和加载后生成BeanDefinition,然后把BeanDefinition注册到IOC容器。
通过注解或者xml声明的bean都会解析得到一个BeanDefinition实体,实体中包含这个bean中定义的基本属性。
最后把这个BeanDefinition保存到一个Map集合里面,从而完成了IOC的初始化。
IoC容器的作用就是对这些注册的Bean的定义信息进行处理和维护,它IoC容器控制反转的核心。
这个阶段主要是根据程序中定义的XML或者注解等Bean的声明方式
通过解析和加载后生成BeanDefinition,然后把BeanDefinition注册到IOC容器。
通过注解或者xml声明的bean都会解析得到一个BeanDefinition实体,实体中包含这个bean中定义的基本属性。
最后把这个BeanDefinition保存到一个Map集合里面,从而完成了IOC的初始化。
IoC容器的作用就是对这些注册的Bean的定义信息进行处理和维护,它IoC容器控制反转的核心。
第二个阶段,完成Bean初始化及依赖注入
然后进入到第二个阶段,这个阶段会做两个事情
(1)通过反射针对没有设置lazy-init属性的单例bean进行初始化。
(2)完成Bean的依赖注入。
然后进入到第二个阶段,这个阶段会做两个事情
(1)通过反射针对没有设置lazy-init属性的单例bean进行初始化。
(2)完成Bean的依赖注入。
第三个阶段,Bean的使用
通常我们会通过@Autowired或者BeanFactory.getBean()从IOC容器中获取指定的bean实例。
另外,针对设置layy-init属性以及非单例bean的实例化,是在每次获取bean对象的时候,调用bean的初始化方法来完成实例化的,并且Spring IOC容器不会去管理这些Bean。
通常我们会通过@Autowired或者BeanFactory.getBean()从IOC容器中获取指定的bean实例。
另外,针对设置layy-init属性以及非单例bean的实例化,是在每次获取bean对象的时候,调用bean的初始化方法来完成实例化的,并且Spring IOC容器不会去管理这些Bean。
15.谈谈你对spring ioc和di 的理解
首先,Spring IOC,全称控制反转(Inversion of Control)。
在传统的Java程序开发中,我们只能通过new关键字来创建对象,这种导致程序中对象的依赖关系比较复杂,耦合度较高。而IOC的主要作用是实现了对象的管理,也就是我们把设计好的对象交给了IOC容器控制,然后在需要用到目标对象的时候,直接从容器中去获取。有了IOC容器来管理Bean以后,相当于把对象的创建和查找依赖对象的控制权交给了容器,这种设计理念使得对象与对象之间是一种松耦合状态,极大提升程序的灵活性以及功能的复用性。
然后,DI表示依赖注入,也就是对于IOC容器中管理的Bean,如果Bean之间存在依赖关系,那么IOC容器需要自动实现依赖对象的实例注入,通常有三种方法来描述Bean之间的依赖关系。
接口注入
setter注入
构造器注入
另外,为了更加灵活的实现Bean实例的依赖注入,Spring还提供了@Resource和@Autowired这两个注解。
分别是根据bean的id和bean的类型来实现依赖注入。
在传统的Java程序开发中,我们只能通过new关键字来创建对象,这种导致程序中对象的依赖关系比较复杂,耦合度较高。而IOC的主要作用是实现了对象的管理,也就是我们把设计好的对象交给了IOC容器控制,然后在需要用到目标对象的时候,直接从容器中去获取。有了IOC容器来管理Bean以后,相当于把对象的创建和查找依赖对象的控制权交给了容器,这种设计理念使得对象与对象之间是一种松耦合状态,极大提升程序的灵活性以及功能的复用性。
然后,DI表示依赖注入,也就是对于IOC容器中管理的Bean,如果Bean之间存在依赖关系,那么IOC容器需要自动实现依赖对象的实例注入,通常有三种方法来描述Bean之间的依赖关系。
接口注入
setter注入
构造器注入
另外,为了更加灵活的实现Bean实例的依赖注入,Spring还提供了@Resource和@Autowired这两个注解。
分别是根据bean的id和bean的类型来实现依赖注入。
16. 谈谈你对 spring MVC 的理解
执行流程
配置阶段
配置阶段,主要是完成对xml配置和注解配置。
具体步骤如下:
首先,从web.xml开始,配置DispatcherServlet的url匹配规则和Spring主配
置文件的加载路径
然后,就是配置注解,比如@Controller、@Service、@Autowrited以及
@RequestMapping等。
具体步骤如下:
首先,从web.xml开始,配置DispatcherServlet的url匹配规则和Spring主配
置文件的加载路径
然后,就是配置注解,比如@Controller、@Service、@Autowrited以及
@RequestMapping等。
初始化阶段
初始化阶段,主要是加载并解析配置信息以及IoC容器、DI操作和 HandlerMapping的初始化。
具体步骤如下:
(1)首先,Web容器启动以后,会由Web容器自动调用DispatcherServlet的init()方法。
(2)然后,在init()方法中,会初始化IoC容器,IoC容器其实就是个Map。
(3)紧接着,根据配置好的扫描包路径,扫描出相关的类,并利用反射进行实例化,存放到IoC容器中。
缓存之后,Spring将再次迭代扫描IoC容器中的实例,给需要自动赋值的属性自动赋值。
比如加了@Autowrited的属性。最后,读取@RequestMapping注解,获得请求url,将url和Method建议一对
一的映射关系并缓存起来。我们可以简单粗暴地理解为缓存在一个Map中,它的Key就是url,它的值是Method。
具体步骤如下:
(1)首先,Web容器启动以后,会由Web容器自动调用DispatcherServlet的init()方法。
(2)然后,在init()方法中,会初始化IoC容器,IoC容器其实就是个Map。
(3)紧接着,根据配置好的扫描包路径,扫描出相关的类,并利用反射进行实例化,存放到IoC容器中。
缓存之后,Spring将再次迭代扫描IoC容器中的实例,给需要自动赋值的属性自动赋值。
比如加了@Autowrited的属性。最后,读取@RequestMapping注解,获得请求url,将url和Method建议一对
一的映射关系并缓存起来。我们可以简单粗暴地理解为缓存在一个Map中,它的Key就是url,它的值是Method。
运行阶段
运行阶段,在Spring启动以后,等待用户请求,完成内部调度并返回响应结果。
用户在浏览器输入url之后,Web容器会接收到用户请求。Web容器会自动调用
doGet()或者doPost()方法。从doGet()或者doPost()方法中,我们可以获得两个
对象,分别是request和response。通过request可以获得用户请求带过来的信息,
通过response可以往浏览器端输出响应结果。
然后,根据request中获得的请求url,可以从HandlerMapping中找到对应
Method。
接着,还是利用反射调用方法,将获得方法调用的返回结果。
最后,将返回结果通过response输出到浏览器,用户就可以看到响应结果。
用户在浏览器输入url之后,Web容器会接收到用户请求。Web容器会自动调用
doGet()或者doPost()方法。从doGet()或者doPost()方法中,我们可以获得两个
对象,分别是request和response。通过request可以获得用户请求带过来的信息,
通过response可以往浏览器端输出响应结果。
然后,根据request中获得的请求url,可以从HandlerMapping中找到对应
Method。
接着,还是利用反射调用方法,将获得方法调用的返回结果。
最后,将返回结果通过response输出到浏览器,用户就可以看到响应结果。
首先,Spring MVC是是属于Spring Framework生态里面的一个模块,它是在Servlet基础上构建并且使用MVC模式设计的一个Web框架,主要的目的是简化传统Servlet+JSP模式下的Web开发方式。
其次,Spring MVC的整体架构设计对Java Web里面的MVC架构模式做了增强和扩展,主要有几个方面。把传统MVC框架里面的Controller控制器做了拆分,分成了前端控制器DispatcherServlet 和 后端控制器Controller。把Model模型拆分成业务层Service和数据访问层Repository。在视图层,可以支持不同的视图,比如Freemark、velocity、JSP等等。所以,Spring MVC天生就是为了MVC模式而设计的,因此在开发MVC应用的时候会更加方便和灵活。
其次,Spring MVC的整体架构设计对Java Web里面的MVC架构模式做了增强和扩展,主要有几个方面。把传统MVC框架里面的Controller控制器做了拆分,分成了前端控制器DispatcherServlet 和 后端控制器Controller。把Model模型拆分成业务层Service和数据访问层Repository。在视图层,可以支持不同的视图,比如Freemark、velocity、JSP等等。所以,Spring MVC天生就是为了MVC模式而设计的,因此在开发MVC应用的时候会更加方便和灵活。
九大组件
MultipartResolver多文件上传组件、
LocaleResolver多语言支持组件、
ThemeResolver主题模板处理组件、
HandlerMappings URL映射组件、
HandlerAdapters业务逻辑适配组件、
HandlerExceptionResolvers异常处理组件、
RequestToViewNameTranslator视图名称提取组件、
ViewResolvers视图渲染组件
FlashMapManager闪存管理组件。
LocaleResolver多语言支持组件、
ThemeResolver主题模板处理组件、
HandlerMappings URL映射组件、
HandlerAdapters业务逻辑适配组件、
HandlerExceptionResolvers异常处理组件、
RequestToViewNameTranslator视图名称提取组件、
ViewResolvers视图渲染组件
FlashMapManager闪存管理组件。
17.spring 中实现一步调用的方式有哪些
注解方式
可以在配置类和方法上加特定注解。首先,在配置类加上@EnableAsync来启用异步注解,
然后,使用@Async注解标记需要异步执行的方法,使用@Async标记的异步方法可以带参数,也可以带有返回值。返回值类型必须是
java.util.concurrent.Future或其子类,可以是以下3种类型:
1)由Java原生API提供的Future。
2)由Spring提供的ListenableFuture后者AsyncResult。
3)Java 8提供的CompletableFuture。
需要说明的是,@Async默认会使用SimpleAsyncTaskExecutor来执行,而这个线程池不会复用线程。所以,
通常要使用异步处理,我们都会自定义线程池。
然后,使用@Async注解标记需要异步执行的方法,使用@Async标记的异步方法可以带参数,也可以带有返回值。返回值类型必须是
java.util.concurrent.Future或其子类,可以是以下3种类型:
1)由Java原生API提供的Future。
2)由Spring提供的ListenableFuture后者AsyncResult。
3)Java 8提供的CompletableFuture。
需要说明的是,@Async默认会使用SimpleAsyncTaskExecutor来执行,而这个线程池不会复用线程。所以,
通常要使用异步处理,我们都会自定义线程池。
内置线程池
可以使用Spring内置的线程池来实现异步调用,比如ThreadPoolTaskExecutor 和SimpleAsyncTaskExecutor。
Spring提供了许多TaskExecutor的内置实现。下面简单介绍5种内置的线程池。
1)SimpleAsyncTaskExecutor:它不会复用线程,每次调用都是启动一个新线程。
2)ConcurrentTaskExecutor:它是Java API中Executor实例的适配器。
3)ThreadPoolTaskExecutor:这个线程池是最常用的。它公开了用于配置的bean属性,并将它包装在
TaskExecutor中。
4)WorkManagerTaskExecutor:它基于CommonJ WorkManager来实现的,并且是在Spring上下文中的WebLogic或WebSphere中设置CommonJ线程池的工具类。
5)DefaultManagedTaskExecutor:主要用于支持JSR-236兼容的运行时环境,它是使用JNDI获得ManagedExecutorService,作为CommonJ WorkManager的替代方案。
通常情况下,ThreadPoolTaskExecuto最为常用,只要当ThreadPoolTaskExecutor不能满足需求时,可以使用ConcurrentTaskExecutor。如果在代码中声明了多个线程池,Spring会默认按照以下搜索顺序来调用线
程池:
第一步,检查上下文中的唯一TaskExecutor Bean。
第二步,检查名为“ taskExecutor”的Executor Bean。
第三步,以上都无法无法处理,就会使用SimpleAsyncTaskExecutor来执行。
Spring提供了许多TaskExecutor的内置实现。下面简单介绍5种内置的线程池。
1)SimpleAsyncTaskExecutor:它不会复用线程,每次调用都是启动一个新线程。
2)ConcurrentTaskExecutor:它是Java API中Executor实例的适配器。
3)ThreadPoolTaskExecutor:这个线程池是最常用的。它公开了用于配置的bean属性,并将它包装在
TaskExecutor中。
4)WorkManagerTaskExecutor:它基于CommonJ WorkManager来实现的,并且是在Spring上下文中的WebLogic或WebSphere中设置CommonJ线程池的工具类。
5)DefaultManagedTaskExecutor:主要用于支持JSR-236兼容的运行时环境,它是使用JNDI获得ManagedExecutorService,作为CommonJ WorkManager的替代方案。
通常情况下,ThreadPoolTaskExecuto最为常用,只要当ThreadPoolTaskExecutor不能满足需求时,可以使用ConcurrentTaskExecutor。如果在代码中声明了多个线程池,Spring会默认按照以下搜索顺序来调用线
程池:
第一步,检查上下文中的唯一TaskExecutor Bean。
第二步,检查名为“ taskExecutor”的Executor Bean。
第三步,以上都无法无法处理,就会使用SimpleAsyncTaskExecutor来执行。
自定义线程池
可以通过实现AsyncConfigurer接口或者直接继承AsyncConfigurerSupport类来自定义线程池。但是非完全托管的Bean和完全托管的Bean实现方式有点小差异。只要在异步方法上添加@Bean注解,不需要手动调用线程池的initialize()方法,在Bean在初始化之后会自动调用。需要注意的是,在同级类中直接调用异步方法无法实现异步。
MyBatis
1. 什么是 mybatis
Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。
什么是ORM
全称为 Object Relational Mapping。对象-映射-关系型数据库。对象关系映射(简称 ORM,或 O/RM,或 O/R mapping),用于实现面向对象编程语言里不同类型系统的数据之间的转换。简单的说,ORM 是通过使用描述对象和数据库之间映射的元数据,将程序中的对象与关系数据库相互映射。
ORM 提供了实现持久化层的另一种模式,它采用映射元数据来描述对象关系的映射,使得 ORM 中间件能在任何一个应用的业务逻辑层和数据库层之间充当桥梁。
全称为 Object Relational Mapping。对象-映射-关系型数据库。对象关系映射(简称 ORM,或 O/RM,或 O/R mapping),用于实现面向对象编程语言里不同类型系统的数据之间的转换。简单的说,ORM 是通过使用描述对象和数据库之间映射的元数据,将程序中的对象与关系数据库相互映射。
ORM 提供了实现持久化层的另一种模式,它采用映射元数据来描述对象关系的映射,使得 ORM 中间件能在任何一个应用的业务逻辑层和数据库层之间充当桥梁。
MyBatis可以使用XML或注解来配置和映射原生信息,将POJO映射成数据库中的记录,避免了几乎所有的JDBC代码和手动设置参数以及获取结果集。
通过xml文件或注解的方式将要执行的各种statement配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。
mybatis 整体架构
基础支持层
【数据源模块】【事务管理模块】【缓存模块】【binding 模块】【反射模块】【类型转换】【日志模块】【资源加载】【解析器模块】
核心处理层
【配置解析】【参数映射】【sql解析】【sql执行】【结果集映射】【插件】
接口层
sqlsession
mybatis 缓存机制
2. mybatis的优点
1、基于SQL语句编程,比较灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
2、消除了JDBC大量冗余的代码,不需要手动开关连接;
3、与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
4、能够与Spring很好的集成;
5、提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
2、消除了JDBC大量冗余的代码,不需要手动开关连接;
3、与各种数据库兼容(因为 MyBatis 使用 JDBC 来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
4、能够与Spring很好的集成;
5、提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
3. mybatis 框架的缺点
1、SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
2、SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
2、SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。
4. mybatis 框架适用的场景
1、MyBatis专注于SQL本身,是一个足够灵活的DAO层解决方案。
2、对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis将是不错的选择。
2、对性能的要求很高,或者需求变化较多的项目,如互联网项目,MyBatis将是不错的选择。
5. mybatis 和 hibernate 有哪些不同
1、Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。
2、Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。
3、Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。
2、Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。
3、Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。
6、#{} 和${}的区别是什么?
#{}是预编译处理,${}是字符串替换。
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理{}替换成变量的值。
使用#{}可以有效的防止SQL注入,提高系统安全性
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
Mybatis在处理{}替换成变量的值。
使用#{}可以有效的防止SQL注入,提高系统安全性
7、jdbc有几个步骤
加载驱动程序
获得数据库连接
创建一个 Statement 对象
操作数据库,实现增删改查
获取结果集
关闭资源
获得数据库连接
创建一个 Statement 对象
操作数据库,实现增删改查
获取结果集
关闭资源
Mybatis 是如何进行分页的?
Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页,先把数据都查出来,然后再做分页。
可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是什么
分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql(SQL 拼接 limit),根据 dialect 方言,添加对应的物理分页语句和物理分页参数,用到了技术 JDK 动态代理,用到了责任链设计模式。
简述mybatis的插件运行原理
Mybatis 仅可以编写针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这 4 种接口的插件,Mybatis 使用 JDK 的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是 InvocationHandler 的 invoke()方法,当然,只会拦截那些你指定需要拦截的方法。
9.如何编写一个插件?
Mybatis 动态 sql 有什么用?
Mybatis 动态 sql 可以在 Xml 映射文件内,以标签的形式编写动态 sql,执行原理是根据表达式的值完成逻辑判断 并动态调整 sql 的功能。
Mybatis 提供了 9 种动态 sql 标签:trim | where | set | foreach | if | choose | when | otherwise | bind。
Mybatis 提供了 9 种动态 sql 标签:trim | where | set | foreach | if | choose | when | otherwise | bind。
<if>:条件判断标签,允许你根据条件来包裹SQL片段。如果条件为真,包裹的SQL片段将被包含在最终SQL中。
<choose>、<when>、<otherwise>:选择标签,用于根据多个条件中的一个选择执行的SQL片段。
<trim>:用于修剪SQL片段中的多余空白字符。
<where>:用于包裹SQL片段中的条件,自动去除多余的AND或OR。
<set>:用于包裹SQL片段中的更新语句,自动去除多余的逗号。
.Xml 映射文件中有哪些标签?
除了常见的 select|insert|updae|delete 标签之外,还有:
<resultMap>、<parameterMap>、<sql>、<include>、<selectKey>,
加上动态 sql 的 9 个标签,其中<sql>为 sql 片段标签,
通过<include>标签引入 sql 片段,<selectKey>为不支持自增的主键生成策略标签。
<resultMap>、<parameterMap>、<sql>、<include>、<selectKey>,
加上动态 sql 的 9 个标签,其中<sql>为 sql 片段标签,
通过<include>标签引入 sql 片段,<selectKey>为不支持自增的主键生成策略标签。
Mybatis 是否支持延迟加载?
Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,
association 指的就是一对一,collection 指的就是一对多查询。
在 Mybatis 配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
association 指的就是一对一,collection 指的就是一对多查询。
在 Mybatis 配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。
13.延迟加载的基本原理是什么?
延迟加载的基本原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法。
比如调用a.getB().getName(),拦截器 invoke()方法发现 a.getB()是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用a.setB(b),于是 a 的对象 b 属性就有值了,接着完成a.getB().getName()方法的调用。
当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。
比如调用a.getB().getName(),拦截器 invoke()方法发现 a.getB()是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用a.setB(b),于是 a 的对象 b 属性就有值了,接着完成a.getB().getName()方法的调用。
当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。
14.mapper.xml 文件对应的 Dao 接口原理是?
简单说:使用了 JDK 动态代理和反射,把接口和 xml 绑定在一起而搞定的。
15.Dao 接口里的方法,参数不同时能重载吗?
不能
17.Mybatis 执行批量插入,能返回数据库主键列表吗?
1、对于支持生成自增主键的数据库:增加 useGenerateKeys 和 keyProperty ,<insert>标签属性。
2、不支持生成自增主键的数据库:使用<selectKey>。
注意 Mybatis 的版本,官方在这个 3.3.1 版本中加入了批量新增返回主键 id 的功能 。
2、不支持生成自增主键的数据库:使用<selectKey>。
注意 Mybatis 的版本,官方在这个 3.3.1 版本中加入了批量新增返回主键 id 的功能 。
18.不同的 Xml 映射文件,id 是否可以重复?
不同的 Xml 映射文件,如果配置了 namespace,那么 id 可以重复;如果没有配置 namespace,那么 id 不能重复;毕竟 namespace 不是必须的,只是最佳实践而已。
原因就是 namespace+id 是作为 Map<String, MappedStatement>的 key 使用的,如果没有 namespace,就剩下 id,那么,id 重复会导致数据互相覆盖。有了 namespace,自然 id 就可以重复,namespace 不同,namespace+id 自然也就不同。
原因就是 namespace+id 是作为 Map<String, MappedStatement>的 key 使用的,如果没有 namespace,就剩下 id,那么,id 重复会导致数据互相覆盖。有了 namespace,自然 id 就可以重复,namespace 不同,namespace+id 自然也就不同。
19.Mybatis 中 Executor 执行器的区别是?
Mybatis 有三种基本的 Executor 执行器,「SimpleExecutor、ReuseExecutor、BatchExecutor。」
SimpleExecutor:每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。
ReuseExecutor:执行 update 或 select,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map<String, Statement>内,供下一次使用。简言之,就是重复使用 Statement 对象。
BatchExecutor:执行 update(没有 select,JDBC 批处理不支持 select),将所有 sql 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理。与 JDBC 批处理相同。
作用范围:Executor 的这些特点,都严格限制在 SqlSession 生命周期范围内。
SimpleExecutor:每执行一次 update 或 select,就开启一个 Statement 对象,用完立刻关闭 Statement 对象。
ReuseExecutor:执行 update 或 select,以 sql 作为 key 查找 Statement 对象,存在就使用,不存在就创建,用完后,不关闭 Statement 对象,而是放置于 Map<String, Statement>内,供下一次使用。简言之,就是重复使用 Statement 对象。
BatchExecutor:执行 update(没有 select,JDBC 批处理不支持 select),将所有 sql 都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个 Statement 对象,每个 Statement 对象都是 addBatch()完毕后,等待逐一执行 executeBatch()批处理。与 JDBC 批处理相同。
作用范围:Executor 的这些特点,都严格限制在 SqlSession 生命周期范围内。
20.为什么说 Mybatis 是半自动 ORM 映射工具?
(1)Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。
而 Mybatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。
(2)Mybatis 直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是 mybatis 无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套 sql 映射文件,工作量大。
(3)Hibernate 对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用 hibernate 开发可以节省很多代码,提高效率。
其实关于常见 ORM 框架还设有 Spring 的 JPA,后期的面试可能会更倾向于问 JPA 和 Mybatis 的区别了
而 Mybatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。
(2)Mybatis 直接编写原生态 sql,可以严格控制 sql 执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是 mybatis 无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套 sql 映射文件,工作量大。
(3)Hibernate 对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用 hibernate 开发可以节省很多代码,提高效率。
其实关于常见 ORM 框架还设有 Spring 的 JPA,后期的面试可能会更倾向于问 JPA 和 Mybatis 的区别了
21.Mybatis 全局配置文件中有哪些标签?
configuration 配置
properties 属性:可以加载
properties 配置文件的信息
settings 设置:可以设置 mybatis 的全局属性
typeAliases 类型命名
typeHandlers 类型处理器
objectFactory 对象工厂
plugins 插件
environments 环境
environment 环境变量
transactionManager 事务管理器
dataSource 数据源
mappers 映射器
properties 属性:可以加载
properties 配置文件的信息
settings 设置:可以设置 mybatis 的全局属性
typeAliases 类型命名
typeHandlers 类型处理器
objectFactory 对象工厂
plugins 插件
environments 环境
environment 环境变量
transactionManager 事务管理器
dataSource 数据源
mappers 映射器
22.当实体类中的属性名和表中的字段名不一样时怎么办 ?
第 1 种:通过在查询的 sql 语句中定义字段名的别名,让字段名的别名和实体类的属性名一致。
<select id="selectById" resultMap="User">
select id, name as userName, pwd as password from m_user
<where>
<if test="id != null">
id = #{id}
</if>
</where>
</select>
<select id="selectById" resultMap="User">
select id, name as userName, pwd as password from m_user
<where>
<if test="id != null">
id = #{id}
</if>
</where>
</select>
第 2 种:通过<resultMap>来映射字段名和实体类属性名的一一对应的关系。
第 3 种:使用注解时候,使用 Result,和第二种类似。
23.模糊查询 like 语句该怎么写?
第 1 种:在 Java 代码中添加 sql 通配符。
String wildcardname = “%smi%”;
list<name> names = mapper.selectlike(wildcardname);
<select id=”selectlike”>
select * from foo where bar like #{value}
</select>
String wildcardname = “%smi%”;
list<name> names = mapper.selectlike(wildcardname);
<select id=”selectlike”>
select * from foo where bar like #{value}
</select>
第 2 种:在 sql 语句中拼接通配符,会引起 sql 注入
String wildcardname = “smi”;
list<name> names = mapper.selectlike(wildcardname);
<select id=”selectlike”>
select * from foo where bar like "%"#{value}"%"
</select>
String wildcardname = “smi”;
list<name> names = mapper.selectlike(wildcardname);
<select id=”selectlike”>
select * from foo where bar like "%"#{value}"%"
</select>
24.mybatis 构建步骤
整体步骤:
创建数据库表-》创建数据库表对应的实体类-〉创建xxmapper接口类 -> 创建xxmapper.xml -》创建config.xml并配置-〉创建test类
创建数据库表-》创建数据库表对应的实体类-〉创建xxmapper接口类 -> 创建xxmapper.xml -》创建config.xml并配置-〉创建test类
25.简述一下mybatis 的手动编程步骤-生命周期
1、创建 SqlSessionFactory
2、通过 SqlSessionFactory 创建 SqlSession
3、通过 sqlsession 执行数据库操作
4、调用 session.commit()提交事务
5、调用 session.close()关闭会话
2、通过 SqlSessionFactory 创建 SqlSession
3、通过 sqlsession 执行数据库操作
4、调用 session.commit()提交事务
5、调用 session.close()关闭会话
26. Mybatis 工作的流程是?
总结来讲
加载配置并初始化
接收调用请求
处理操作请求触发条件
返回处理结果
详细讲
1.读取mybatis的核心配置文件。
mybatis-config.xml为MyBatis的全局配置文件,
用于配置数据库连接、属性、类型别名、类型处理器、插件、环境配置、映射器(mapper.xml)等信息,
这个过程中有一个比较重要的部分就是映射文件其实是配在这里的;
这个核心配置文件最终会被sqlsessionFactory解析为 一个Environment对象 和 Configuration对象
mybatis-config.xml为MyBatis的全局配置文件,
用于配置数据库连接、属性、类型别名、类型处理器、插件、环境配置、映射器(mapper.xml)等信息,
这个过程中有一个比较重要的部分就是映射文件其实是配在这里的;
这个核心配置文件最终会被sqlsessionFactory解析为 一个Environment对象 和 Configuration对象
2. 加载映射文件。
映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,映射文件是在mybatis-config.xml中加载;
可以加载多个映射文件。常见的配置的方式有两种,一种是package扫描包,一种是mapper找到配置文件的位置。
映射文件即SQL映射文件,该文件中配置了操作数据库的SQL语句,映射文件是在mybatis-config.xml中加载;
可以加载多个映射文件。常见的配置的方式有两种,一种是package扫描包,一种是mapper找到配置文件的位置。
3. 构造会话工厂获取SqlSessionFactory。
这个过程其实是用建造者设计模式使用SqlSessionFactoryBuilder对象构建的,SqlSessionFactory的最佳作用域是应用作用域。
这个过程其实是用建造者设计模式使用SqlSessionFactoryBuilder对象构建的,SqlSessionFactory的最佳作用域是应用作用域。
4、创建会话对象SqlSession。
由会话工厂创建SqlSession对象,对象中包含了执行SQL语句的所有方法,每个线程都应该有它自己的 SqlSession 实例。SqlSession的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。
由会话工厂创建SqlSession对象,对象中包含了执行SQL语句的所有方法,每个线程都应该有它自己的 SqlSession 实例。SqlSession的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。
5、Executor执行器。
是MyBatis的核心,负责SQL语句的生成和查询缓存的维护,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护
SimpleExecutor -- SIMPLE 就是普通的执行器。
ReuseExecutor-执行器会重用预处理语句(PreparedStatements)
BatchExecutor --它是批处理执行器
是MyBatis的核心,负责SQL语句的生成和查询缓存的维护,它将根据SqlSession传递的参数动态地生成需要执行的SQL语句,同时负责查询缓存的维护
SimpleExecutor -- SIMPLE 就是普通的执行器。
ReuseExecutor-执行器会重用预处理语句(PreparedStatements)
BatchExecutor --它是批处理执行器
mybatis 允许拦截的方法:
Executor 【update,query,commit,rollback】
StatementHandler 【prepare,parameterize,batch,update,query等】
ParameterHandler 【getParameterObject,setParameters等】
ResultSetHandler 【handleResultSets,handleOuputParameters等】
Executor 【update,query,commit,rollback】
StatementHandler 【prepare,parameterize,batch,update,query等】
ParameterHandler 【getParameterObject,setParameters等】
ResultSetHandler 【handleResultSets,handleOuputParameters等】
用户需要自定义拦截器出了继承interceptor 接口,还需要使用@intercepts 和
@signature 两个注解进行标识。
@Intercepts({@Signature(type = Executor.class,method = "query",
args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class})})
type 指定拦截的类型
method 指定拦截的方法
args 指定被拦截方法的参数列表
@signature 两个注解进行标识。
@Intercepts({@Signature(type = Executor.class,method = "query",
args = {MappedStatement.class,Object.class, RowBounds.class, ResultHandler.class})})
type 指定拦截的类型
method 指定拦截的方法
args 指定被拦截方法的参数列表
6、MappedStatement对象。MappedStatement是对解析的SQL的语句封装,一个MappedStatement代表了一个sql语句标签,如下:
<!--一个动态sql标签就是一个`MappedStatement`对象-->
<select id="selectUserList" resultType="com.mybatis.User">
select * from t_user
</select>
<!--一个动态sql标签就是一个`MappedStatement`对象-->
<select id="selectUserList" resultType="com.mybatis.User">
select * from t_user
</select>
7、输入参数映射。
输入参数类型可以是基本数据类型,也可以是Map、List、POJO类型复杂数据类型,这个过程类似于JDBC的预编译处理参数的过程,有两个属性 parameterType和parameterMap
输入参数类型可以是基本数据类型,也可以是Map、List、POJO类型复杂数据类型,这个过程类似于JDBC的预编译处理参数的过程,有两个属性 parameterType和parameterMap
8、封装结果集。
可以封装成多种类型可以是基本数据类型,也可以是Map、List、POJO类型复杂数据类型。封装结果集的过程就和JDBC封装结果集是一样的。也有两个常用的属性resultType和resultMap。
可以封装成多种类型可以是基本数据类型,也可以是Map、List、POJO类型复杂数据类型。封装结果集的过程就和JDBC封装结果集是一样的。也有两个常用的属性resultType和resultMap。
mapper如何传递多个参数
顺序传参法
@param注解传参法
map传参法
java bean 传参法
Mybatis是否可以映射Enum枚举类?
Mybatis当然可以映射枚举类,不单可以映射枚举类,Mybatis可以映射任何对象到表的一列上。映射方式为自定义一个TypeHandler,实现TypeHandler的setParameter()和getResult()接口方法。
TypeHandler有两个作用,一是完成从javaType至jdbcType的转换,二是完成jdbcType至javaType的转换,体现为setParameter()和getResult()两个方法,分别代表设置sql问号占位符参数和获取列查询结果。
TypeHandler有两个作用,一是完成从javaType至jdbcType的转换,二是完成jdbcType至javaType的转换,体现为setParameter()和getResult()两个方法,分别代表设置sql问号占位符参数和获取列查询结果。
Java基础
谈谈对Java平台的理解
Java和其他语言相比的差异性
面向对象:封装,继承,多态
语法差异不太大,不仅吸收了c++的各种优点也摒弃掉了c++中比较难理解的多继承,指针等概念
语法差异不太大,不仅吸收了c++的各种优点也摒弃掉了c++中比较难理解的多继承,指针等概念
封装:封装把⼀个对象的属性私有化,同时提供⼀些可以被外界访问的属性的⽅法。
访问修饰符 public、private、protected、以及不写(默认)时的区别?
Java 中,可以使用访问控制符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。
default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。
private : 在同一类内可见。可以修饰变量、方法。注意:不能修饰类(外部类)
public : 对所有类可见。可以修饰类、接口、变量、方法
protected : 对同一包内的类和所有子类可见。可以修饰变量、方法。注意:不能修饰类(外部类)。
default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。
private : 在同一类内可见。可以修饰变量、方法。注意:不能修饰类(外部类)
public : 对所有类可见。可以修饰类、接口、变量、方法
protected : 对同一包内的类和所有子类可见。可以修饰变量、方法。注意:不能修饰类(外部类)。
继承:继承是使⽤已存在的类的定义作为基础创建新的类,新类的定义可以增加新的属性或新的方法,也可以继承父类的属性和方法。通过继承可以很方便地进行代码复用。
关于继承有以下三个要点:
1. ⼦类拥有⽗类对象所有的属性和⽅法(包括私有属性和私有⽅法),但是⽗类中的私有属性和⽅法⼦类是⽆法访问,只是拥有。
2. ⼦类可以拥有⾃⼰属性和⽅法,即⼦类可以对⽗类进⾏扩展。
3. ⼦类可以⽤⾃⼰的⽅式实现⽗类的⽅法。
关于继承有以下三个要点:
1. ⼦类拥有⽗类对象所有的属性和⽅法(包括私有属性和私有⽅法),但是⽗类中的私有属性和⽅法⼦类是⽆法访问,只是拥有。
2. ⼦类可以拥有⾃⼰属性和⽅法,即⼦类可以对⽗类进⾏扩展。
3. ⼦类可以⽤⾃⼰的⽅式实现⽗类的⽅法。
多态:
所谓多态就是指程序中定义的引⽤变量所指向的具体类型和通过该引⽤变量发出的⽅法调⽤在编程时并不确定,⽽是在程序运⾏期间才确定,即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定。
在 Java 中有两种形式可以实现多态:继承(多个⼦类对同⼀⽅法的重写)和接⼝(实现接⼝并覆盖接⼝中同⼀⽅法)。
所谓多态就是指程序中定义的引⽤变量所指向的具体类型和通过该引⽤变量发出的⽅法调⽤在编程时并不确定,⽽是在程序运⾏期间才确定,即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定。
在 Java 中有两种形式可以实现多态:继承(多个⼦类对同⼀⽅法的重写)和接⼝(实现接⼝并覆盖接⼝中同⼀⽅法)。
重载(overload)和重写(override)的区别?
方法的重载和重写都是实现多态的方式,区别在于
重载是实现的是编译时的多态性,而重写实现的是运行时的多态性。
重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;
重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。
方法重载的规则:
方法名一致,参数列表中参数的顺序,类型,个数不同。
重载与方法的返回值无关,存在于父类和子类,同类中。
可以抛出不同的异常,可以有不同修饰符。
重载是实现的是编译时的多态性,而重写实现的是运行时的多态性。
重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;
重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。
方法重载的规则:
方法名一致,参数列表中参数的顺序,类型,个数不同。
重载与方法的返回值无关,存在于父类和子类,同类中。
可以抛出不同的异常,可以有不同修饰符。
需要学习的是,特有的特性,底层的一些实现
JVM的平台无关
一次编写,到处运行
一次编写,到处运行
垃圾回收机制
支持多线程;c++语言没有内置的多线程机制
编译和解释并存
6.为什么说 Java 语言“编译与解释并存”?
高级编程语言按照程序的执行方式分为编译型和解释型两种。
简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;
解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。
Java 语言既具有编译型语言的特征,也具有解释型语言的特征, Java 编写的程序需要先经过编译步骤,生成字节码(\*.class 文件),这种字节码必须再经过 JVM,解释成操作系统能识别的机器码,在由操作系统执行。因此,我们可以认为 Java 语言编译与解释并存。
高级编程语言按照程序的执行方式分为编译型和解释型两种。
简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;
解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。
Java 语言既具有编译型语言的特征,也具有解释型语言的特征, Java 编写的程序需要先经过编译步骤,生成字节码(\*.class 文件),这种字节码必须再经过 JVM,解释成操作系统能识别的机器码,在由操作系统执行。因此,我们可以认为 Java 语言编译与解释并存。
Java的语法特性
范型
Lambda表达式
集合类
IO/NIO
并发
JVM
Java类加载机制
内存布局
垃圾回收/常见的垃圾收集器
内存模型
JVM、JDK 和 JRE 有什么区别?
JVM:Java Virtual Machine,Java 虚拟机,Java 程序运行在 Java 虚拟机上。针对不同系统的实现(Windows,Linux,macOS)不同的 JVM,因此 Java 语言可以实现跨平台。
JRE: Java 运⾏时环境。它是运⾏已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,Java 命令和其他的⼀些基础构件。但是,它不能⽤于创建新程序。
JDK: Java Development Kit,它是功能⻬全的 Java SDK。它拥有 JRE 所拥有的⼀切,还有编译器(javac)和⼯具(如 javadoc 和 jdb)。它能够创建和编译程序。
java 有哪些数据类型
定义:Java 语言是强类型语言,对于每一种数据都定义了明确的具体的数据类型,在内存中分配了不同大小的内存空间。
基本数据类型:
数值型
· 整数类型(byte、short、int、long)
· 浮点类型(float、double)
字符型(char)
布尔型(boolean)
引用数据类型:
类(class)
接口(interface)
数组([])
数值型
· 整数类型(byte、short、int、long)
· 浮点类型(float、double)
字符型(char)
布尔型(boolean)
引用数据类型:
类(class)
接口(interface)
数组([])
不初始化,默认的值
-2^ 31 -2^31-1
基本类型和包装类型
基本类型对应的包装类型
boolean
Boolean
byte
8bits
Byte
short
16bits
Short
char
16bits
Character
int
32bits
Integer
float
32bits
Float
double
64bits
Double
long
64bits
Long
自动装箱、拆箱
装箱
Integer.valueOf()
拆箱
Integer.intValue()
缓存
Boolean
缓存了true/false对应的实例,Boolean.TRUE/FALSE
Short
缓存了-128/127之间的数值
Byte
全部都被缓存了
Character
缓存范围'\u0000'到'\u007F'
Integer
缓存了-128/127之间的数值
避免无意中的装箱、拆箱行为,在性能敏感的场合,创建10w个Java对象和10w个整型的开销不是一个数量级的
对象的组成
对象头
哈希码、锁状态标志、线程持有锁、偏向线程id、gc分代年龄等
类型指针,对象指向类型元数据的指针
对象指向其类
对象实例
对象存储的真正有效信息
对齐填充
占位符,主要是保证该对象是8字节的倍数
自动类型转换,强制类型转换?
Java 所有的数值型变量可以相互转换,当把一个表数范围小的数值或变量直接赋给另一个表数范围大的变量时,可以进行自动类型转换;反之,需要强制转换。
什么事自动拆箱/装箱?
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
拆箱:将包装类型转换为基本数据类型;
Java细节,自动封箱拆箱导致的NullPointerException问题
(一)这里拿到的值实际是Boolean类型的null值,作为boolean类型返回需要进行拆箱,而null值拆箱
因为Boolean类型是一个类,所以Boolean类型对象的默认值是null,我这直接当boolean使用明显不对,包装类型并不能完全代替基本类型。
因为Boolean类型是一个类,所以Boolean类型对象的默认值是null,我这直接当boolean使用明显不对,包装类型并不能完全代替基本类型。
(二)首先了解一下字符串拼接的原理。Java使用 “+” 拼接字符串看起来像操作符重载,实际上并不是,Java是不支持运算符重载的,这其实只是Java提供的一个语法糖。反编译代码后会发现,其实调用的是StringBuilder的append方法
装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时, 插人必要的方法调用。虚拟机只是执行这些字节码。
对于装箱和拆箱,实际上打开源码看一下就明白。以Boolean为例,装箱就是调用Boolean的valueOf方法选择预置的 TRUE or FALSE 对象(已经创建好的static对象),拆箱就是调用Boolean对象的booleanValue方法返回value。
对于装箱和拆箱,实际上打开源码看一下就明白。以Boolean为例,装箱就是调用Boolean的valueOf方法选择预置的 TRUE or FALSE 对象(已经创建好的static对象),拆箱就是调用Boolean对象的booleanValue方法返回value。
所以既然装箱和拆箱是编译器执行的,那毫无疑问,这里肯定是会执行appendboolean b)这个方法了。也就是说,Boolean类型的值在拼接时,首先要需要调用它的booleanValue()方法完成拆箱,然而和第一个问题一样,null对象怎么调用方法,自然抛出了空指针异常。
this 关键字有什么作用?
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
this 的用法在 Java 中大体可以分为 3 种:
1. 普通的直接引用,this 相当于是指向当前对象本身
2. 形参与成员变量名字重名,用 this 来区分:
public Person(String name,int age){
this.name=name;
this.age=age;
}
3. 引用本类的构造函数
this 的用法在 Java 中大体可以分为 3 种:
1. 普通的直接引用,this 相当于是指向当前对象本身
2. 形参与成员变量名字重名,用 this 来区分:
public Person(String name,int age){
this.name=name;
this.age=age;
}
3. 引用本类的构造函数
Java平台的理解
回答套路
Java和其他语言相比的差异性
Java的语法特性
范型
Lambda表达式
集合类
IO/NIO
并发
JVM
Java类加载机制
内存布局
垃圾回收/常见的垃圾收集器
内存模型
经典回答
一次书写,到处运行,优秀的跨平台能力
垃圾回收机制,Java通过垃圾回收机制回收分配内存,大部分情况下,程序员不需要操心内存的分配和回收
JRE是Java运行环境美包涵JVM和Java类库,以及一些模块。
JDK是JRE的超集,提供更多的工具,编译器、各种诊断工具
Java源代码,首先通过Javac编译成为字节码,在运行时,通过Java虚拟机内嵌解释器将字节码转成机器码。常见的JVM都提供了JIT编译器,动态编译器。JIT可以在运行时将热点代码编译成机器码,属于编译执行
考点分析
表现思维深入并系统化,Java知识理解全面,避免使面试官认为是一个“知其然不一致其所以然”的人。
知识扩展
Java语言特性,泛型、Lambda语言特性;基础类库,包含集合、IO/NIO、网络、并发、安全等基础类库。
JVM基础概念和机制,Java类加载机制,常见版本JDK内嵌Class-Loader,类加载的大致过程,自定义类加载;垃圾收集器等
运行时,JVM会通过类加载器加载字节码,解释或者编译执行。JDK8采用的是解释和编译混合的模式。通常运行在server模式的JVM会进行上万次调用收集足够的信息进行高效编译。client模式是1500次。JVM内置两种不同的JIT compiler,一种适用启动速度敏感的应用,一种是优化长时间运行的服务器端应用
还有一种AOT,直接将字节码编译成机器码,避免JIT预热等各方面开销
Exception和Error的区别
Java中异常处理体系
Error:是Throwable的子类,用于指示合理的应用程序不应该尝试捕获的严重问题,无需在throws子句中抛出。
virtualMachineError
stackOverflowError
OutOfMemaryError
AWTError
Exception
Checked Exception
受检异常
受检异常
IO Exception
FileNotFoundException
检查异常:必须在代码中显示进行捕获异常
在网络IO中超时重试,以防在过程中,溜掉一些业务相关的内容
RuntimeException
运行时异常
运行时异常
NullPointerException, ArrayIndexOutOfBoundSException, IllegalArgumentException, ClassCastException
不检查异常:不需要在代码中显示进行捕获的异常
一般业务也会使用运行时异常,这些异常继承于RuntimeException,这样就可以在切面进行统一处理和封装返回给其他业务端和前端
代码规范中如何设计Exception
在捕获的过程中不要捕获类似Exception这样的通用异常,应该捕获一些特定的异常
不要生吞异常,在捕获到异常之后,不要什么都不处理,这样很难在出现问题的时候进行诊断
异常输出打印需要使用产品日志,详细输出到日志系统中
遵守Throw early,catch late 原则
可以使用 try-whith-resources 和 multiple catch
Exception和Error区别
经典回答
Exception和Error都是继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出或者捕获,是一场处理机制的基本组成类型
Exception和Error体现了Java平台设计者对不同异常情况的分类。Exception是程序正常运行中,可以预料的意外情况,可能并且应该被捕获
Error指在正常情况下,不太可能出现的情况,绝大部分的Error都会导致程序处于非正常、不可恢复状态。比如OOM
Exception分为可检查异常和不检查异常,可检查异常在源代码中必须显式进行捕获处理,编译期检查的一部分
不检查异常是所谓的运行时异常类似NullPointerException、ArrayIndexOutOfBoundsException,通常是可以编码避免的逻辑错误
考点分析
Exception和Error区别,是从概念角度考察了Java处理机制,处于理解层面,只要阐述清楚即可
异常处理方式
抛出异常
throw
throws
捕获异常
try - catch
遇到异常不进行具体处理,而是继续抛给调用者 (throw,throws)
抛出异常有三种形式,一是 throw,一个 throws,还有一种系统自动抛异常。
throws 用在方法上,后面跟的是异常类,可以跟多个;而 throw 用在方法内,后面跟的是异常对象。
抛出异常有三种形式,一是 throw,一个 throws,还有一种系统自动抛异常。
throws 用在方法上,后面跟的是异常类,可以跟多个;而 throw 用在方法内,后面跟的是异常对象。
try catch 捕获异常
在 catch 语句块中补货发生的异常,并进行处理。
try {
//包含可能会出现异常的代码以及声明异常的方法
}catch(Exception e) {
//捕获异常并进行处理
}finally { }
//可选,必执行的代码
}
try-catch 捕获异常的时候还可以选择加上 finally 语句块,finally 语句块不管程序是否正常执行,最终它都会必然执行。
在 catch 语句块中补货发生的异常,并进行处理。
try {
//包含可能会出现异常的代码以及声明异常的方法
}catch(Exception e) {
//捕获异常并进行处理
}finally { }
//可选,必执行的代码
}
try-catch 捕获异常的时候还可以选择加上 finally 语句块,finally 语句块不管程序是否正常执行,最终它都会必然执行。
final、finally、finalize
只是看起来相似,但是作用完全不一样
final修饰 类、方法、变量,分别不同的意义
类
代表不可以继承扩展
变量
不可以被修改
方法
不可以重写
finally代码块
保障代码一定要被执行的一种机制,关闭JDBC等操作
更加推荐try- whit-resources
finally 作为异常处理的一部分,它只能在 try/catch 语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下,System.exit (0) 可以阻断 finally 执行。
finalize方法的作用
Object中了的一个方法,设计的目的是,保证对象在被垃圾回收之前完成特定资源的回收
目前不推荐使用,在JDK9开始被标记为deprecated了
finalize 是在 java.lang.Object 里定义的方法,也就是说每一个对象都有这么个方法,这个方法在 gc 启动,该对象被回收的时候被调用。
一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象,所以有可能调用 finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法。
一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象,所以有可能调用 finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法。
==和 equals 的区别?
== : 它的作⽤是判断两个对象的地址是不是相等。即,判断两个对象是不是同⼀个对象(基本数据类型 == 比较的是值,引⽤数据类型 == 比较的是内存地址)。
equals() : 它的作⽤也是判断两个对象是否相等。但是这个“相等”一般也分两种情况:
默认情况:类没有覆盖 equals() ⽅法。则通过 equals() 比较该类的两个对象时,等价于通过“ == ”比较这两个对象,还是相当于比较内存地址
自定义情况:类覆盖了 equals() ⽅法。我们平时覆盖的 equals()方法一般是比较两个对象的内容是否相同,自定义了一个相等的标准,也就是两个对象的值是否相等。
默认情况:类没有覆盖 equals() ⽅法。则通过 equals() 比较该类的两个对象时,等价于通过“ == ”比较这两个对象,还是相当于比较内存地址
自定义情况:类覆盖了 equals() ⽅法。我们平时覆盖的 equals()方法一般是比较两个对象的内容是否相同,自定义了一个相等的标准,也就是两个对象的值是否相等。
hashCode 与 equals?
什么是 HashCode?
hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数,定义在 Object 类中, 是一个本地⽅法,这个⽅法通常⽤来将对象的内存地址转换为整数之后返回。
public native int hashCode();
哈希码主要在哈希表这类集合映射的时候用到,哈希表存储的是键值对(key-value),它的特点是:能根据“键”快速的映射到对应的“值”。这其中就利⽤到哈希码!
hashCode() 的作⽤是获取哈希码,也称为散列码;它实际上是返回⼀个 int 整数,定义在 Object 类中, 是一个本地⽅法,这个⽅法通常⽤来将对象的内存地址转换为整数之后返回。
public native int hashCode();
哈希码主要在哈希表这类集合映射的时候用到,哈希表存储的是键值对(key-value),它的特点是:能根据“键”快速的映射到对应的“值”。这其中就利⽤到哈希码!
为什么重写 quals 时必须重写 hashCode ⽅法?
如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals ⽅法都返回 true。反之,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。
hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写 hashCode() ,则该 class 的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)
如果两个对象相等,则 hashcode ⼀定也是相同的。两个对象相等,对两个对象分别调⽤ equals ⽅法都返回 true。反之,两个对象有相同的 hashcode 值,它们也不⼀定是相等的 。因此,equals ⽅法被覆盖过,则 hashCode ⽅法也必须被覆盖。
hashCode() 的默认⾏为是对堆上的对象产⽣独特值。如果没有重写 hashCode() ,则该 class 的两个对象⽆论如何都不会相等(即使这两个对象指向相同的数据)
为什么两个对象有相同的 hashcode 值,它们也不⼀定是相等的?
因为可能会碰撞, hashCode() 所使⽤的散列算法也许刚好会让多个对象传回相同的散列值。越糟糕的散列算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode )。
因为可能会碰撞, hashCode() 所使⽤的散列算法也许刚好会让多个对象传回相同的散列值。越糟糕的散列算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是不同的对象得到相同的 hashCode )。
Java 是值传递,还是引用传递?
Java 语言是值传递。Java 语言的方法调用只支持参数的值传递。当一个对象实例作为一个参数被传递到方法中时,参数的值就是对该对象的引用。对象的属性可以在被调用过程中被改变,但对对象引用的改变是不会影响到调用者的。
JVM 的内存分为堆和栈,其中栈中存储了基本数据类型和引用数据类型实例的地址,也就是对象地址。
而对象所占的空间是在堆中开辟的,所以传递的时候可以理解为把变量存储的对象地址给传递过去,因此引用类型也是值传递。
JVM 的内存分为堆和栈,其中栈中存储了基本数据类型和引用数据类型实例的地址,也就是对象地址。
而对象所占的空间是在堆中开辟的,所以传递的时候可以理解为把变量存储的对象地址给传递过去,因此引用类型也是值传递。
深拷贝和浅拷贝?
浅拷贝:仅拷贝被拷贝对象的成员变量的值,也就是基本数据类型变量的值,和引用数据类型变量的地址值,而对于引用类型变量指向的堆中的对象不会拷贝。
深拷贝:完全拷贝一个对象,拷贝被拷贝对象的成员变量的值,堆中的对象也会拷贝一份。
深拷贝:完全拷贝一个对象,拷贝被拷贝对象的成员变量的值,堆中的对象也会拷贝一份。
因此深拷贝是安全的,浅拷贝的话如果有引用类型,那么拷贝后对象,引用类型变量修改,会影响原对象。
浅拷贝如何实现呢?
Object 类提供的 clone()方法可以非常简单地实现对象的浅拷贝。
Object 类提供的 clone()方法可以非常简单地实现对象的浅拷贝。
深拷贝如何实现呢?
重写克隆方法:重写克隆方法,引用类型变量单独克隆,这里可能会涉及多层递归。
序列化:可以先将原对象序列化,再反序列化成拷贝对象。
重写克隆方法:重写克隆方法,引用类型变量单独克隆,这里可能会涉及多层递归。
序列化:可以先将原对象序列化,再反序列化成拷贝对象。
强引用、软引用、弱引用、幻想引用区别
概念定位
强引用
最常见的一种普通对象的引用,只要有强引用只想一个对象,就表明对象还活着,垃圾收集器不会碰这些对象。显示地将引用赋值为null,表示可以被垃圾回收了,但是还是需要看具体的回收策略
软引用
相对强引用弱化的一些引用,可以让对象豁免一些垃圾回收,只有站在JVM认为内存不足时,才会试图祸首软引用只想的对象。会保证在抛出OOMError前,清理软引用指向的对象
弱引用
不能使对象豁免垃圾收集,仅仅提供一种访问在弱引用状态下对象的途径。多缓存实现的选择
幻想引用(虚引用)
仅仅是提供了一种确保对象被finalize后,做某些事情的机制
软引用和弱引用,垃圾收集器会存在二次确认的问题,保证处于弱引用状态的对象,没有改变为强引用状态
弱引用的用途,在Java中的实践ThreadLocal
ThreadLocal其实只是符号意义,本身不存储变量。仅仅是用来索引各个线程中的变量副本
Entry中的ThreadLocal对象是采用弱引用引入的
为什么ThreadLocalMap要使用弱引用存储ThreadLocal?
如果使用强引用,那么当ThreadLocal不再使用需要回收的时候,发现某个线程中ThreadLocalMap存在ThreadLocal的强引用,那么就无法回收,造成内存泄漏
弱引用可以防止长期存在的线程(通常使用线程池),导致ThreadLocal无法回收 造成的内存泄漏
那为什么会说ThreadLocal会造成内存泄漏?
虽然Key是通过 弱引用引入的,但是value本身就是通过强引用引入的,导致即使不做处理,ThreadLocalMap和线程的生命周期是一致的,线程长期不释放,即使ThreadLocal中本身由于弱引用机制已经回收了,但是value还是驻留在线程中ThreadLocalMap的entry里,可能会导致内存泄漏
其实有一定的防止内存泄漏的工作,在存放的时候探测到key为null的entry的时候,会进行探测式的清理,将null key的entry清理掉
所以在使用的前后需要调用remove清理,同时对异常情况也要在finally中清理
Java 创建对象有哪几种方式?
(1)new 创建新对象
(2)通过反射机制
(3)采用 clone 机制
(4)通过序列化机制
前两者都需要显式地调用构造方法。对于 clone 机制,需要注意浅拷贝和深拷贝的区别,对于序列化机制需要明确其实现原理,在 Java 中序列化可以通过实现 Externalizable 或者 Serializable 来实现。
(2)通过反射机制
(3)采用 clone 机制
(4)通过序列化机制
前两者都需要显式地调用构造方法。对于 clone 机制,需要注意浅拷贝和深拷贝的区别,对于序列化机制需要明确其实现原理,在 Java 中序列化可以通过实现 Externalizable 或者 Serializable 来实现。
String 是 Java 基本数据类型吗?可以被继承吗?
String 是 Java 基本数据类型吗?
不是。Java 中的基本数据类型只有 8 个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type)。
String 是一个比较特殊的引用数据类型。
不是。Java 中的基本数据类型只有 8 个:byte、short、int、long、float、double、char、boolean;除了基本类型(primitive type),剩下的都是引用类型(reference type)。
String 是一个比较特殊的引用数据类型。
String 类可以继承吗?
不行。String 类使用 final 修饰,是所谓的不可变类,无法被继承。
不行。String 类使用 final 修饰,是所谓的不可变类,无法被继承。
String、StringBuffer、StringBuilder区别
概念定位
String
典型的Immutable类,被声明为final class,所有的属性都是final的。由于不可变,拼接、剪裁字符串都会产生新的String对象
StringBuilder
为了解决拼接产生太多的中间对象的问题而提供的一个类,可以使用append方法,将自负串添加到已有的序列末尾处
StringBuffer
本质上和StringBuilder没有差别,在修改的方法级别上加上了synchronized关键字,使得线程安全
底层实现
StringBuilder
继承AbstractStringBuilder,包含了基本操作
底层采用char数组,JDK9以后采用byte数组
最终拼接的长度如果可预计,先预定一个长度,以防后续扩容产生多重开销(抛弃原有数组,创建新数组)arraycopy
关键参数
默认长度
16
扩容倍数
2
字符串缓存
Java6之后提供了intern()方法,通知JVM将字符串缓存起来,以备重复使用,如果已有缓存的字符串,就会返回缓存里的实例
但是在Java6中不推荐使用,缓存存在永久代中,基本不会被fullGC之外的垃圾收集照顾到,所以使用不当会导致OOM
后续将缓存放到了堆中、JDK8之后放到了MetaSpace区域中
需要显示调用,对代码的美观会产生不太好的效果
在性能上StringBuilder > StringBuffer > String
String的演进
历史版本中采用char数组进行数据存储,但是拉丁语系文字只需要一个bytes字节,造成了大部分的浪费
Java9中,引入了Compact Strings设计,对字符串进行大量的改进,从char数组改成 byte数组 ➕ 一个编码标识coder 并对相关方法进行重写,以保证没有任何性能所损失,在一些场景中,占据了更小的内存占用和更快的操作速度!
String str1 = new String("abc")和 String str2 = "abc" 和 区别?
两个语句都会去字符串常量池中检查是否已经存在 “abc”,如果有则直接使用,如果没有则会在常量池中创建 “abc” 对象。
但是不同的是,String str1 = new String("abc") 还会通过 new String() 在堆里创建一个 "abc" 字符串对象实例。所以后者可以理解为被前者包含。
String s = new String("abc")创建了几个对象?
很明显,一个或两个。如果字符串常量池已经有“abc”,则是一个;否则,两个。
当字符创常量池没有 “abc”,此时会创建如下两个对象:
一个是字符串字面量 "abc" 所对应的、字符串常量池中的实例
另一个是通过 new String() 创建并初始化的,内容与"abc"相同的实例,在堆中。
String s = new String("abc")创建了几个对象?
很明显,一个或两个。如果字符串常量池已经有“abc”,则是一个;否则,两个。
当字符创常量池没有 “abc”,此时会创建如下两个对象:
一个是字符串字面量 "abc" 所对应的、字符串常量池中的实例
另一个是通过 new String() 创建并初始化的,内容与"abc"相同的实例,在堆中。
String 不是不可变类吗?字符串拼接是如何实现的?
String 的确是不可变的,“+”的拼接操作,其实是会生成新的对象。
在jdk1.8 之前,a 和 b 初始化时位于字符串常量池,ab 拼接后的对象位于堆中。经过拼接新生成了 String 对象。如果拼接多次,那么会生成多个中间对象。
在Java8 时JDK 对“+”号拼接进行了优化,上面所写的拼接方式会被优化为基于 StringBuilder 的 append 方法进行处理。Java 会在编译期对“+”号进行处理。
下面是通过 javap -verbose 命令反编译字节码的结果,很显然可以看到 StringBuilder 的创建和 append 方法的调用。
下面是通过 javap -verbose 命令反编译字节码的结果,很显然可以看到 StringBuilder 的创建和 append 方法的调用。
也就是说其实上面的代码其实相当于:
String a = "hello ";
String b = "world!";
StringBuilder sb = new StringBuilder();
sb.append(a);
sb.append(b);
String ab = sb.toString();
String a = "hello ";
String b = "world!";
StringBuilder sb = new StringBuilder();
sb.append(a);
sb.append(b);
String ab = sb.toString();
通过加号拼接字符串会创建多个 String 对象,因此性能比 StringBuilder 差,就是错误的了。因为本质上加号拼接的效果最终经过编译器处理之后和 StringBuilder 是一致的。
intern 方法有什么作用?
如果当前字符串内容存在于字符串常量池(即 equals()方法为 true,也就是内容一样),直接返回字符串常量池中的字符串
否则,将此 String 对象添加到池中,并返回 String 对象的引用
否则,将此 String 对象添加到池中,并返回 String 对象的引用
Integer a= 127,Integer b = 127;Integer c= 128,Integer d = 128;,相等吗?
答案是 a 和 b 相等,c 和 d 不相等。
对于基本数据类型==比较的值
对于引用数据类型==比较的是地址
Integer a= 127 这种赋值,是用到了 Integer 自动装箱的机制。自动装箱的时候会去缓存池里取 Integer 对象,没有取到才会创建新的对象。
如果整型字面量的值在-128 到 127 之间,那么自动装箱时不会 new 新的 Integer 对象,而是直接引用缓存池中的 Integer 对象,超过范围 a1==b1 的结果是 false
对于基本数据类型==比较的值
对于引用数据类型==比较的是地址
Integer a= 127 这种赋值,是用到了 Integer 自动装箱的机制。自动装箱的时候会去缓存池里取 Integer 对象,没有取到才会创建新的对象。
如果整型字面量的值在-128 到 127 之间,那么自动装箱时不会 new 新的 Integer 对象,而是直接引用缓存池中的 Integer 对象,超过范围 a1==b1 的结果是 false
什么是 Integer 缓存?
因为根据实践发现大部分的数据操作都集中在值比较小的范围,因此 Integer 搞了个缓存池,默认范围是 -128 到 127,可以根据通过设置JVM-XX:AutoBoxCacheMax=来修改缓存的最大值,最小值改不了。
实现的原理是 int 在自动装箱的时候会调用 Integer.valueOf,进而用到了 IntegerCache。 【可以看下 Integer.valueOf 的源码 】
因为根据实践发现大部分的数据操作都集中在值比较小的范围,因此 Integer 搞了个缓存池,默认范围是 -128 到 127,可以根据通过设置JVM-XX:AutoBoxCacheMax=来修改缓存的最大值,最小值改不了。
实现的原理是 int 在自动装箱的时候会调用 Integer.valueOf,进而用到了 IntegerCache。 【可以看下 Integer.valueOf 的源码 】
在 Boxing Conversion 部分的Java语言规范(JLS)规定如下:
如果一个变量 p 的值属于:-128至127之间的整数(§3.10.1),true 和 false的布尔值 (§3.10.3),’u0000′ 至 ‘u007f’ 之间的字符(§3.10.4)中时,将 p 包装成 a 和 b 两个对象时,可以直接使用 a == b 判断 a 和 b 的值是否相等。
所有整数类型的类都有类似的缓存机制:
有 ByteCache 用于缓存 Byte 对象
有 ShortCache 用于缓存 Short 对象
有 LongCache 用于缓存 Long 对象
Byte,Short,Long 的缓存池范围默认都是: -128 到 127。可以看出,Byte的所有值都在缓存区中,用它生成的相同值对象都是相等的。
所有整型(Byte,Short,Long)的比较规律与Integer是一样的。
同时Character 对象也有CharacterCache 缓存 池,范围是 0 到 127。
除了 Integer 可以通过参数改变范围外,其它的都不行。
如果一个变量 p 的值属于:-128至127之间的整数(§3.10.1),true 和 false的布尔值 (§3.10.3),’u0000′ 至 ‘u007f’ 之间的字符(§3.10.4)中时,将 p 包装成 a 和 b 两个对象时,可以直接使用 a == b 判断 a 和 b 的值是否相等。
所有整数类型的类都有类似的缓存机制:
有 ByteCache 用于缓存 Byte 对象
有 ShortCache 用于缓存 Short 对象
有 LongCache 用于缓存 Long 对象
Byte,Short,Long 的缓存池范围默认都是: -128 到 127。可以看出,Byte的所有值都在缓存区中,用它生成的相同值对象都是相等的。
所有整型(Byte,Short,Long)的比较规律与Integer是一样的。
同时Character 对象也有CharacterCache 缓存 池,范围是 0 到 127。
除了 Integer 可以通过参数改变范围外,其它的都不行。
很简单,就是判断下值是否在缓存范围之内,如果是的话去 IntegerCache 中取,不是的话就创建一个新的 Integer 对象。
IntegerCache 是一个静态内部类, 在静态块中会初始化好缓存值。
private static class IntegerCache {
……
static {
//创建Integer对象存储
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
……
}
}
IntegerCache 是一个静态内部类, 在静态块中会初始化好缓存值。
private static class IntegerCache {
……
static {
//创建Integer对象存储
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
……
}
}
String 怎么转成 Integer 的?原理?
String 转成 Integer,主要有两个方法:
Integer.parseInt(String s)
Integer.valueOf(String s)
不管哪一种,最终还是会调用 Integer 类内中的parseInt(String s, int radix)方法。
其实里面主要做了一些负向累加
Integer.parseInt(String s)
Integer.valueOf(String s)
不管哪一种,最终还是会调用 Integer 类内中的parseInt(String s, int radix)方法。
其实里面主要做了一些负向累加
Object 类方法
对象比较:
public native int hashCode() :native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的 HashMap。
public boolean equals(Object obj):用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写用户比较字符串的值是否相等。
public native int hashCode() :native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的 HashMap。
public boolean equals(Object obj):用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写用户比较字符串的值是否相等。
对象拷贝:
protected native Object clone() throws CloneNotSupportedException:naitive 方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为 true,x.clone().getClass() == x.getClass() 为 true。Object 本身没有实现 Cloneable 接口,所以不重写 clone 方法并且进行调用的话会发生 CloneNotSupportedException 异常。
protected native Object clone() throws CloneNotSupportedException:naitive 方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于任何对象 x,表达式 x.clone() != x 为 true,x.clone().getClass() == x.getClass() 为 true。Object 本身没有实现 Cloneable 接口,所以不重写 clone 方法并且进行调用的话会发生 CloneNotSupportedException 异常。
对象转字符串:
public String toString():返回类的名字@实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
public String toString():返回类的名字@实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。
多线程调度:
public final native void notify():native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll():native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException:native 方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 。timeout 是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException:多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。
public final void wait() throws InterruptedException:跟之前的 2 个 wait 方法一样,只不过该方法一直等待,没有超时时间这个概念
public final native void notify():native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
public final native void notifyAll():native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。
public final native void wait(long timeout) throws InterruptedException:native 方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 。timeout 是等待时间。
public final void wait(long timeout, int nanos) throws InterruptedException:多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 毫秒。
public final void wait() throws InterruptedException:跟之前的 2 个 wait 方法一样,只不过该方法一直等待,没有超时时间这个概念
反射:
public final native Class<?> getClass():native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
public final native Class<?> getClass():native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。
垃圾回收:
protected void finalize() throws Throwable :通知垃圾收集器回收对象。
protected void finalize() throws Throwable :通知垃圾收集器回收对象。
动态代理
设计模式——代理模式
角色
公共接口
具体的类
代理类
代理类持有具体类的实力,代提代理类实例去执行操作
InvocationHandler
动态代理需要的类
动态代理
运行时动态构建代理,动态处理代理方法调用的机制,RPC调用、面向切面编程(AOP)
通过代理可以让调用者与实现者之间解耦。增强代理对象的功能,并且做到代码复用
JDK 代理
最小化依赖关系,减少依赖意味着简化开发和维护,JDK本身的支持,比cglib更加可靠
平滑进行JDK版本升级,自家门类库通常需要进行更新保证在新版Java上可以使用
代码实现简单
实现InvocationHandler 然后重写invoke方法
使用Proxy.newProxyInstance产生代理对象
被代理类必须要有接口
反射
Java语言提供的一种基础功能,赋予程序在运行时的自省功能,通过反射可以直接操作类或者对象,获取某个对象的类定义,获取类声明的属性和方法
一般框架可以利用反射做到 加载、持久化数据等逻辑,不需要开发者手动写类似的重复代码
cglib代理
针对类实现代理,通过ASM字节码生成框架对字节码修改生成子类
Spring AOP
默认使用JDK动态代理
在代理类不是接口实现的话,会采用cglib代理,执行方法不是接口实现的话也不会进行代理
final修饰的方法不能被代理
静态代理
和动态代理有区别的地方是 静态代理的代理类是提前写好的,但是动态代理不需要自己编写代理类
静态代理做扩展的时候需要对每个方法都进行扩展,不能做到统一扩展,但是动态代理最终调用的是handler的invoke方法,更加方便做同意的扩展,比如说说对某些方法进行耗时统计
Vector、ArrayList、LinkedList区别
Vector
早期提供的线程安全的动态数组。内部使用对象数组来保存数据,可以根据需要自动增加容量,数组已满时,会创建新的数组,并拷贝原油数组数据
ArrayList
并不是线程安全的,扩容时以1.5倍进行容量调整
LinkedList
Java提供的双向链表
List具体总结看
HashTable、HashMap、TreeMap区别
HashTable
早期Java中提供的一个哈希表实现,本身是同步的,不支持null键和值,比较少倍推荐使用
HashMap
和HashTable类似,主要区别在于HashMap不是同步的,支持null键和值,put和get可以达到常数时间的性能,绝大部分利用键值对存储场景的首选
TreeMap
基于红黑树提供的顺序访问的Map,各种操作都是O(log(n))复杂度,顺序可以由Comparator决定
ConcurrentHashMap
线程安全的哈希表
Map具体总结看
Java IO方式
传统的IO
概念
基于流模型实现,提供了我们常用的IO功能:File抽象、输入输出流等。
交互方式是同步、阻塞的方式,在读取流写入流时,完成该动作前,线程会一直阻塞在那里,可靠的线性顺序。
java.io包的好处是 代码比较简单、直观。IO效率和扩展性存在局限,容易成为应用性能瓶颈。
java.net下面提供部分网络API,(Socket、HttpURLConnection)也同样归类于同步阻塞IO类库,网络通信同属IO行为
理解
输入流、输出流(InputStream/OutputStream)是用于读取或写入字节的,例如操作图片文件。
Reader/Writer 则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息。本质上计算机操作的都是字节,不管是网络通信还是文件读取,Reader/Writer 相当于构建了应用逻辑和原始数据之间的桥梁。
BufferedOutputStream 等带缓冲区的实现,可以避免频繁的磁盘读写,进而提高 IO 处理效率。这种设计利用了缓冲区,将批量数据进行一次操作,但在使用中千万别忘了 flush。
很多 IO 工具类都实现了 Closeable 接口。需要利用 try-with-resources、 try-finally 等机制保证 FileInputStream 被明确关闭,进而相应文件描述符也会失效,否则将导致资源无法被释放。
IO相关类图
NIO
概念
在Java 1.4 中引入 java.nio,提供了Channel、Selector、Buffer等新抽象,可以构建多路复用、同步非阻塞IO城西,提供了更加接近操作系统底层高性能数据操作的方式
组成
Buffer:高效的数据容器,除了布尔类型,所有的原始数据类型都有相应的Buffer实现
Channel:类似Linux操作系统上看到的文件描述符,是NIO中,被用来支持批量式IO操作
File、Socket通常被认为是比较高层次的抽象,Channel是更加操作底层的一种抽象
Selector:NIO实现多路复用的基础,提供了一种高效的机制,可以检测注册在Selector上的多个Channel中,是否有Channel处于就绪状态,实现了单线程队多Channel的高效管理。其原理基于底层操作系统机制,不同版本有相应区别(Linux epoll、Windows iocp)
Charset:提供Unicode字符串定义,NIO也提供了相应的编码器
NIO2(AIO)
概念
Java 7 中,NIO有了进一步的改进,NIO 2,异步非阻塞IO方式。异步IO操作基于事件和回调机制。
应用操作直接返回,不会阻塞,后台处理完成后,操作系统会通知相应线程进行后续工作
利用事件回调机制,处理Accept、Read操作
概念区分
同步和异步
同步是一种可靠的有序运行机制,后续任务需要等待当前调用返回才会进行下一步
异步相反,其他任务不需要等待当前调用返回,依靠事件、回调等机制来实现任务次序关系
阻塞和非阻塞
阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当前条件就绪才能继续。
非阻塞不管IO操作是否结束,直接返回后续操作在后台继续
抽象类(abstract class)和接口(interface)有什么区别?
抽象类(Abstract Class):
抽象类是一个可以包含抽象方法的类,抽象方法是一种没有具体实现的方法,只包含方法的签名(名称、参数和返回类型)。
抽象类可以包含普通方法,这些方法有具体实现。
子类继承抽象类时,必须提供实现父类的抽象方法。否则,子类也必须声明为抽象类。
抽象类可以包含实例变量,构造函数,属性(属性也可以是抽象的)等。
一个类只能继承一个抽象类,因为Java等编程语言中不支持多继承。
抽象类是一个可以包含抽象方法的类,抽象方法是一种没有具体实现的方法,只包含方法的签名(名称、参数和返回类型)。
抽象类可以包含普通方法,这些方法有具体实现。
子类继承抽象类时,必须提供实现父类的抽象方法。否则,子类也必须声明为抽象类。
抽象类可以包含实例变量,构造函数,属性(属性也可以是抽象的)等。
一个类只能继承一个抽象类,因为Java等编程语言中不支持多继承。
接口(Interface):
接口是一种约定,定义了一组方法的声明,但没有具体实现。所有接口方法都是公开、抽象的。
类可以实现多个接口,这种多继承是通过实现不同接口来实现的。
接口中的方法没有方法体,只有方法签名,它们在实现类中被具体实现。
接口可以被看作是一种契约,规定了实现类必须提供的一组方法。
接口通常用于定义类之间的契约和通用行为,以实现多态性和代码的重用。
接口是一种约定,定义了一组方法的声明,但没有具体实现。所有接口方法都是公开、抽象的。
类可以实现多个接口,这种多继承是通过实现不同接口来实现的。
接口中的方法没有方法体,只有方法签名,它们在实现类中被具体实现。
接口可以被看作是一种契约,规定了实现类必须提供的一组方法。
接口通常用于定义类之间的契约和通用行为,以实现多态性和代码的重用。
成员变量与局部变量的区别有哪些?
从语法形式上看:成员变量是属于类的,⽽局部变量是在⽅法中定义的变量或是⽅法的参数;成员变量可以被 public , private , static 等修饰符所修饰,⽽局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
从变量在内存中的存储⽅式来看:如果成员变量是使⽤ static 修饰的,那么这个成员变量是属于类的,如果没有使⽤ static 修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型为基本数据类型,那么存储在栈内存,如果为引⽤数据类型,那存放的是指向堆内存对象的引⽤或者是指向常量池中的地址。
从变量在内存中的⽣存时间上看:成员变量是对象的⼀部分,它随着对象的创建⽽存在,⽽局部变量随着⽅法的调⽤⽽⾃动消失。
成员变量如果没有被赋初值:则会⾃动以类型的默认值⽽赋值(⼀种情况例外:被 final 修饰的成员变量也必须显式地赋值),⽽局部变量则不会⾃动赋值。
静态变量和实例变量的区别?静态方法、实例方法呢?
静态变量和实例变量的区别?
静态变量: 是被 static 修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个副本。
实例变量: 必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。
实例变量: 必须依存于某一实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。
静态⽅法和实例⽅法有何不同?
静态方法:static 修饰的方法,也被称为类方法。在外部调⽤静态⽅法时,可以使⽤"类名.⽅法名"的⽅式,也可以使⽤"对象名.⽅法名"的⽅式。静态方法里不能访问类的非静态成员变量和方法。
实例⽅法:依存于类的实例,需要使用"对象名.⽅法名"的⽅式调用;可以访问类的所有成员变量和方法。
实例⽅法:依存于类的实例,需要使用"对象名.⽅法名"的⽅式调用;可以访问类的所有成员变量和方法。
IO
1. 什么是IO?
IO在计算机中指Input/Output,也就是输入和输出。由于程序和运行时数据是在内存中驻留,由CPU这个超快的计算核心来执行,涉及到数据交换的地方,通常是磁盘、网络等,就需要IO接口。
IO编程中,Stream(流)是一个很重要的概念,可以把流想象成一个水管,数据就是水管里的水,但是只能单向流动。Input Stream就是数据从外面(磁盘、网络)流进内存,Output Stream就是数据从内存流到外面去。对于浏览网页来说,浏览器和新浪服务器之间至少需要建立两根水管,才可以既能发数据,又能收数据。
流按照不同的特点,有很多种划分方式。
按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流
按照流的流向分,可以分为输入流和输出流;
按照操作单元划分,可以划分为字节流和字符流;
按照流的角色划分为节点流和处理流
IO 流用到了什么设计模式?
其实,Java 的 IO 流体系还用到了一个设计模式——装饰器模式。
InputStream 相关的部分类图如下,篇幅有限,装饰器模式就不展开说了。
其实,Java 的 IO 流体系还用到了一个设计模式——装饰器模式。
InputStream 相关的部分类图如下,篇幅有限,装饰器模式就不展开说了。
2. 常用的IO类有哪些?
字节流
InputStream
ByteArrayInputStream
PipedInputStream
FilterInputStream
BufferInputStream
可以将多个字节的数据放入内存中,让CPU与内存互动提高效率
DataInputStream
FileInputStream
ObjectInputStream
OutputStream
ByteArrayOutputStream
PipedOutputStream
FileterOutputStream
BufferOutputStream
可以将多个字节的数据放入内存中,让CPU与内存互动提高效率
DataOutputStream
PrintStream
FileOutputStream
ObjectOutputStream
以字节的方式读取或者写入文件
File类
文件操作
字符流
Reader
CharArrayReader
PipedReader
FilterReader
BufferedReader
InputStreamReader
FileReader
Writer
CharArrayWriter
PipedWriter
FilterWriter
BufferedWriter
OutputStreamWriter
FileWriter
PrintWriter
字符流与字节流的区别在于,字符流每次读或写是以char为单位,而不是byte,说明速度更快。
3. 怎么理解 BIO、NIO、AIO?
BIO(同步阻塞)
客户端在请求数据的过程中,保持一个连接,不能做其他事情
连接是双向的,对于客户端和服务端都需要一个线程来维护这个连接,如果服务端没有数据给客户端,客户端需要一直等待,该连接也需要一直维持,阻塞会给服务端带来较大的性能负担
客户端不能做其他事情,只能等待请求完成,本身的性能没有得到充分的释放,等待是浪费时间的
NIO(同步非阻塞)
客户端在请求数据的过程中,不用保持一个连接,不能做其他事情
NIO使用轮询的方式代替始终保持一个连接,从而节约了内存消耗
NIO三个实体
Buffer(缓冲区)
客户端存放服务端信息的一个容器,把数据准备好后,会通过Channel往Buffer里面传
类型
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
Channel(通道)
双工连接通道,在请求过程中,客户端和服务器端中间的Channel在不停执行“连接、询问、断开”的过程。直到数据准备好,再通过Channel传回来
类型
FileChannel(文件读取数据)
DatagramChannel(读写UDP网络协议数据)
SocketChannel(读写TCP网络协议数据)
ServerSocketChannel(可以监听TCP连接)
Selector(多路复用器)
监控数据是否准备好,应答Channel。
多个Channel反复轮询时,Selector就看该Channel所需的数据是否准备好了;如果准备好了,则将数据通过Channel返回给该客户端的Buffer,该客户端再进行后续其他操作;如果没准备好,则告诉Channel还需要继续轮询;多个Channel反复询问Selector,Selector为这些Channel一一解答。
Netty主要就是封装了NIO的接口
AIO(异步非阻塞)
客户端在请求数据过程中不用保持一个连接,可以做其他事情;目前应用不广泛
AIO不用始终保持一个连接,方式和NIO不同,使得这个方式可以让客户端做其他事情
引入了通知机制,客户端请求服务器端是否有数据,如果没有告诉客户端没有数据,客户端可以做其他事情,等到服务端有数据的时候通知客户端,并把数据转过去。
引入了其他消耗,服务端需要主动通知客户端,“通知”的业务逻辑是要消耗一定资源的;客户端在做其他事情,突然有前面的事情过来,必然会引入多线程的协调工作
4. 什么是比特(Bit)、字节(Byte)、字符(Char)?
Bit(比特)
计算机中存储数据的最小单位,是二进制数中的一个位数,值为“0”或“1”
Byte(字节)
计算机存储数据的单元,是一个8位的二进制数,8bit
Char(字符)
人们使用的一个记号,一个字符2个字节
5. 字节流和字符流的区别?
字节流操作的基本单元为字节;字符流操作的基本单元为Unicode码元。
字节流默认不使用缓冲区;字符流使用缓冲区。
字节流在操作的时候本身是不会用到缓冲区的,是与文件本身直接操作的,所以字节流在操作文件时,即使不关闭资源,文件也能输出;
字符流在操作的时候是使用到缓冲区的。如果字符流不调用close或flush方法,则不会输出任何内容。
字节流通常用于处理二进制数据,实际上它可以处理任意类型的数据,但它不支持直接写入或读取Unicode码元;字符流通常处理文本数据,它支持写入及读取Unicode码元。
字节流可用于任何类型的对象,包括二进制对象,而字符流只能处理字符或者字符串; 字节流提供了处理任何类型的IO操作的功能,但它不能直接处理Unicode字符,而字符流就可以。
既然有了字节流,为什么还要有字符流?
其实字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还比较耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。
所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
6. 讲一下Java的序列化吧?
介绍
序列化
把对象转成有序字节流,以便在网络传输或者保存本地文件中
反序列化
根据字节流中保存的对象状态及描述信息,通过反序列化重建对象
序列化场景
暂存大对象
Java对象需要持久化的时候
需要在网路,例如socket中传输Java对象。
深度克隆
跨虚拟机通信
序列化一个对象
实现Serializabel接口
标识接口,标识该类可以被序列化
ObjectOutputStream 对象序列化字节流
ObjectInputStream 字节流反序列化对象
注意
静态变量和transient关键字修饰的变量不能被序列化
序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。
transient作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient变量的值设为初始值,如int型的是0。
Serializable 接口有什么用
这个接口只是一个标记,没有具体的作用,但是如果不实现这个接口,在有些序列化场景会报错,所以一般建议,创建的 JavaBean 类都实现 Serializable。
serialVersionUID
虚拟机是否允许对象反序列化,不是取决于该对象所属类路径和功能代码是否与虚拟机加载的类一致,而是主要取决于对象所属类与虚拟机加载的该类的序列化 ID 是否一致
默认情况下是 1L
常见的序列化方式
java 序列化
java语言本省提供,使用比较方面和简单
不支持跨语言处理、性能相对不是很好,序列化以后产生的数据相对较大
XML序列化
XML序列化的好处在于可读性好,方便阅读和调试。
序列化以后的 字节码文件比较大,而且效率不高,适应于对性能不高,而且QPS较低的企业级内部系统之间的数据交换的场景,同时XML又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟知的WebService,就是采用XML格式对数据进行序列化的
JSON序列化
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于XML来说,JON的字节流较小,而且可读性也非常好。现在JSON数据格式的其他运用最普遍的。序列化方式还衍生了阿里的fastjson,美团的MSON,谷歌的GSON等更加优秀的转码工具。
Hessian 序列化框架
Hessian是一个支持跨语言传输的二进制序列化协议,相对于Java默认的序列化机制来说,Hessian具有更好的性能和易用性,而且支持多重不同的语言,实际上Dubbo采用的就是Hessian序列化来实现,只不过Dubbo对Hessian进行重构,性能更高。
Protobuf 序列化框架
Protobuf是Google的一种数据交换格式,它独立于语言、独立于平台。
Google 提供了多种语言来实现,比如 Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件Protobuf 使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的 RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中但是但是要使用 Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器。
如果有些变量不想序列化,怎么办?
对于不想进行序列化的变量,使用transient关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;
当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
transient 只能修饰变量,不能修饰类和方法。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;
当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。
transient 只能修饰变量,不能修饰类和方法。
7. TCP/IP四层模型/OSI七层模型有哪些?
应用层
应用层
提供为应用软件而设的接口,以设置与另一应用软件之间的通信
DNS、HTTP、FTP、IMAP4、POP3、SSH、TELNET
表达层
把数据转换为能与接收者的系统格式兼容并适合传输的格式
会话层
负责在数据传输中设置和维护计算机网络中两台计算机之间的通信连接
传输层
传输层
把传输表头(TH)加至数据以形成数据包。传输表头包含了所使用的协议等发送信息
TCP、UDP、PPTP、TLS/SSL
网络层
网络层
决定数据的路径选择和转寄,将网络表头(NH)加至数据包,以形成分组。网络表头包含了网络数据
IP(v4·v6)、ICMP(v6)、IGMP、Ipsec
数据链路层
数据链路层
负责网络寻址、错误侦测和改错。当表头和表尾被加至数据包时,会形成帧。数据链表头(DLH)是包含了物理地址和错误侦测及改错的方法。数据链表尾(DLT)是一串指示数据包末端的字符串
Wi-Fi(IEEE 802.11)、ARP、WiMAX(IEEE 802.16)、PPP、PPPoE、L2TP
物理层
在局部局域网上传送数据帧(data frame),它负责管理计算机通信设备和网络媒体之间的互通。包括了针脚、电压、线缆规范、集线器、中继器、网卡、主机接口卡等
8. TCP 和 UDP 协议的区别?
TCP是面向连接的协议,在收发数据前必须和对方建立可靠的连接,建立连接的3次握手、断开连接的4次挥手,为数据传输打下可靠基础;UDP是一个面向无连接的协议,数据传输前,源端和终端不建立连接,发送端尽可能快的将数据扔到网络上,接收端从消息队列中读取消息段。
TCP提供可靠交付的服务,传输过程中采用许多方法保证在连接上提供可靠的传输服务,如编号与确认、流量控制、计时器等,确保数据无差错,不丢失,不重复且按序到达;UDP使用尽可能最大努力交付,但不保证可靠交付。
TCP报文首部有20个字节,额外开销大;UDP报文首部只有8个字节,标题短,开销小。
TCP协议面向字节流,将应用层报文看成一串无结构的字节流,分解为多个TCP报文段传输后,在目的站重新装配;UDP协议面向报文,不拆分应用层报文,只保留报文边界,一次发送一个报文,接收方去除报文首部后,原封不动将报文交给上层应用。
TCP拥塞控制、流量控制、重传机制、滑动窗口等机制保证传输质量;UDP没有。
TCP只能点对点全双工通信;UDP支持一对一、一对多、多对一和多对多的交互通信。
场景
为了实现TCP网络通信的可靠性,增加校验和、序号标识、滑动窗口、确认应答、拥塞控制等复杂的机制,建立了繁琐的握手过程,增加了TCP对系统资源的消耗;TCP的重传机制、顺序控制机制等对数据传输有一定延时影响,降低了传输效率。TCP适合对传输效率要求低,但准确率要求高的应用场景,比如万维网(HTTP)、文件传输(FTP)、电子邮件(SMTP)等。
UDP是无连接的,不可靠传输,尽最大努力交付数据,协议简单、资源要求少、传输速度快、实时性高的特点,适用于对传输效率要求高,但准确率要求低的应用场景,比如域名转换(DNS)、远程文件服务器(NFS)等。
9. TCP为什么要三次握手,两次不可以吗?
其实这是由TCP的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力,不然容易出现丢包的现象
第一次握手: 可以确认客服端的发送能力
第二次握手: 可以确认服务端的接收能力 和 发送能力
第三次握手: 可以确认客户端的接收能力。
java 范型了解吗?什么是类型擦除?介绍下通配符?
什么是范型
Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
泛型常用的通配符有哪些?
常用的通配符为: T,E,K,V,?
? 表示不确定的 java 类型
T (type) 表示具体的一个 java 类型
K V (key value) 分别代表 java 键值中的 Key Value
E (element) 代表 Element
? 表示不确定的 java 类型
T (type) 表示具体的一个 java 类型
K V (key value) 分别代表 java 键值中的 Key Value
E (element) 代表 Element
什么是泛型擦除?
所谓的泛型擦除,官方名叫“类型擦除”。
Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的类型信息都会被擦掉。
也就是说,在运行的时候是没有泛型的。
Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的类型信息都会被擦掉。
也就是说,在运行的时候是没有泛型的。
为什么要类型擦除呢?
主要是为了向下兼容,因为 JDK5 之前是没有泛型的,为了让 JVM 保持向下兼容,就出了类型擦除这个策略。
说一下你对注解的理解?
Java 注解本质上是一个标记,注解可以标记在类上、方法上、属性上等,标记自身也可以设置一些值
例如我们常见的 AOP,使用注解作为切点就是运行期注解的应用;比如 lombok,就是注解在编译期的运行。
注解生命周期有三大类,分别是:
RetentionPolicy.SOURCE:给编译器用的,不会写入 class 文件
RetentionPolicy.CLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了
RetentionPolicy.RUNTIME:会写入 class 文件,永久保存,可以通过反射获取注解信息
例如我们常见的 AOP,使用注解作为切点就是运行期注解的应用;比如 lombok,就是注解在编译期的运行。
注解生命周期有三大类,分别是:
RetentionPolicy.SOURCE:给编译器用的,不会写入 class 文件
RetentionPolicy.CLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了
RetentionPolicy.RUNTIME:会写入 class 文件,永久保存,可以通过反射获取注解信息
子主题
什么是反射?应用?原理?
什么是反射?
我们通常都是利用new方式来创建对象实例,这可以说就是一种“正射”,这种方式在编译时候就确定了类型信息。
而如果,我们想在时候动态地获取类信息、创建类实例、调用类方法这时候就要用到反射。
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
反射最核心的四个类
而如果,我们想在时候动态地获取类信息、创建类实例、调用类方法这时候就要用到反射。
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
反射最核心的四个类
反射的应用场景?
一般我们平时都是在在写业务代码,很少会接触到直接使用反射机制的场景。
但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
像 Spring 里的很多 注解 ,它真正的功能实现就是利用反射。
就像为什么我们使用 Spring 的时候 ,一个@Component注解就声明了一个类为 Spring Bean 呢?为什么通过一个 @Value注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
这些都是因为我们可以基于反射操作类,然后获取到类/属性/方法/方法的参数上的注解,注解这里就有两个作用,一是标记,我们对注解标记的类/属性/方法进行对应的处理;二是注解本身有一些信息,可以参与到处理的逻辑中。
但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。
像 Spring 里的很多 注解 ,它真正的功能实现就是利用反射。
就像为什么我们使用 Spring 的时候 ,一个@Component注解就声明了一个类为 Spring Bean 呢?为什么通过一个 @Value注解就读取到配置文件中的值呢?究竟是怎么起作用的呢?
这些都是因为我们可以基于反射操作类,然后获取到类/属性/方法/方法的参数上的注解,注解这里就有两个作用,一是标记,我们对注解标记的类/属性/方法进行对应的处理;二是注解本身有一些信息,可以参与到处理的逻辑中。
反射的原理?
我们都知道 Java 程序的执行分为编译和运行两步,编译之后会生成字节码(.class)文件,JVM 进行类加载的时候,会加载字节码文件,将类型相关的所有信息加载进方法区,反射就是去获取这些信息,然后进行各种操作。
jdk 1.8 都有哪些新特性
接口默认方法:Java 8 允许我们给接口添加一个非抽象的方法实现,只需要使用 default 关键字修饰即可
Lambda 表达式和函数式接口:Lambda 表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。Lambda 允许把函数作为一个方法的参数(函数作为参数传递到方法中),使用 Lambda 表达式使代码更加简洁,但是也不要滥用,否则会有可读性等问题,《Effective Java》作者 Josh Bloch 建议使用 Lambda 表达式最好不要超过 3 行。
Stream API:用函数式编程方式在集合类上进行复杂操作的工具,配合 Lambda 表达式可以方便的对集合进行处理。
Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。
简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。
简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
日期时间 API:Java 8 引入了新的日期时间 API 改进了日期时间的管理。
Optional 类:用来解决空指针异常的问题。很久以前 Google Guava 项目引入了 Optional 作为解决空指针异常的一种方式,不赞成代码被 null 检查的代码污染,期望程序员写整洁的代码。受 Google Guava 的鼓励,Optional 现在是 Java 8 库的一部分。
Lambda 表达式了解多少?
Lambda 表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。
比如我们以前使用 Runnable 创建并运行线程:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running before Java8!");
}
}).start();
这是通过内部类的方式来重写 run 方法,使用 Lambda 表达式,还可以更加简洁:
new Thread( () -> System.out.println("Thread is running since Java8!") ).start();
当然不是每个接口都可以缩写成 Lambda 表达式。只有那些函数式接口(Functional Interface)才能缩写成 Lambda 表示式。
所谓函数式接口(Functional Interface)就是只包含一个抽象方法的声明。针对该接口类型的所有 Lambda 表达式都会与这个抽象方法匹配。
比如我们以前使用 Runnable 创建并运行线程:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running before Java8!");
}
}).start();
这是通过内部类的方式来重写 run 方法,使用 Lambda 表达式,还可以更加简洁:
new Thread( () -> System.out.println("Thread is running since Java8!") ).start();
当然不是每个接口都可以缩写成 Lambda 表达式。只有那些函数式接口(Functional Interface)才能缩写成 Lambda 表示式。
所谓函数式接口(Functional Interface)就是只包含一个抽象方法的声明。针对该接口类型的所有 Lambda 表达式都会与这个抽象方法匹配。
JDK 1.8 API 包含了很多内置的函数式接口。其中就包括我们在老版本中经常见到的 Comparator 和 Runnable,Java 8 为他们都添加了 @FunctionalInterface 注解,以用来支持 Lambda 表达式。
除了这两个之外,还有 Callable、Predicate、Function、Supplier、Consumer 等等。
除了这两个之外,还有 Callable、Predicate、Function、Supplier、Consumer 等等。
Optional 了解吗?
Optional是用于防范NullPointerException。
可以将 Optional 看做是包装对象(可能是 null, 也有可能非 null)的容器。当我们定义了 一个方法,这个方法返回的对象可能是空,也有可能非空的时候,我们就可以考虑用 Optional 来包装它,这也是在 Java 8 被推荐使用的做法。
Optional<String> optional = Optional.of("bam");
optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
optional.isPresent(); // true
optional.get(); // "bam"
optional.orElse("fallback"); // "bam"
optional.ifPresent((s) -> System.out.println(s.charAt(0))); // "b"
Stream 流用过吗?
Stream 流,简单来说,使用 java.util.Stream 对一个包含一个或多个元素的集合做各种操作。这些操作可能是 中间操作 亦或是 终端操作。 终端操作会返回一个结果,而中间操作会返回一个 Stream 流。
Filter 过滤
Sorted 排序
Map 转换
Match 匹配
Count 计数
Reduce
数据结构
树
二叉树
每个节点最多含有两个子树的树
红黑树/自平衡二叉树
每个节点最多也是含有两个子树的树
从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。
因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,
而不同于普通的二叉查找树
因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,
而不同于普通的二叉查找树
B-树
M阶 B树
1. 树中每个结点至多有m个孩子;
2. 除根结点和叶子结点外,其它每个结点至少有m/2个孩子;
3. 若根结点不是叶子结点,则至少有2个孩子;
4. 所有叶子结点(失败节点)都出现在同一层,叶子结点不包含任何关键字信息;
5. 所有非终端结点中包含下列信息数据 ( n, A0 , K1 , A1 , K2 , A2 , … , Kn , An ),
其中: Ki (i=1,…,n)为关键字,且Ki < Ki+1 , Ai (i=0,…,n)为指向子树根结点的指针, n为关键字的个数
6. 非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,
其它P[i]指向关键字属于(K[i-1], K[i])的子树;
2. 除根结点和叶子结点外,其它每个结点至少有m/2个孩子;
3. 若根结点不是叶子结点,则至少有2个孩子;
4. 所有叶子结点(失败节点)都出现在同一层,叶子结点不包含任何关键字信息;
5. 所有非终端结点中包含下列信息数据 ( n, A0 , K1 , A1 , K2 , A2 , … , Kn , An ),
其中: Ki (i=1,…,n)为关键字,且Ki < Ki+1 , Ai (i=0,…,n)为指向子树根结点的指针, n为关键字的个数
6. 非叶子结点的指针:P[1], P[2], …, P[M];其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,
其它P[i]指向关键字属于(K[i-1], K[i])的子树;
搜索
从根结点开始,对结点内的关键字(有序)序列进行二分查找,
如果命中则结束,否则进入查询关键字所属范围的儿子结点;
重复,直到所对应的儿子指针为空,或已经是叶子结点;
因此,B-Tree的查找过程是一个顺指针查找结点和在结点的关键字中进行查找的交叉进行的过程。
如果命中则结束,否则进入查询关键字所属范围的儿子结点;
重复,直到所对应的儿子指针为空,或已经是叶子结点;
因此,B-Tree的查找过程是一个顺指针查找结点和在结点的关键字中进行查找的交叉进行的过程。
插入
B-树是从空树起,逐个插入关键码而生成的。
在B-树,每个非失败结点的关键码个数都在[ m/2 -1, m-1]之间。插入在某个叶结点开始。如果在关键码插入后结点中的关键码个数超出了上界 m-1,则结点需要“分裂”,否则可以直接插入。
实现结点“分裂”的原则是:
设结点 A 中已经有 m-1 个关键码,当再插入一个关键码后结点中的状态为( m, A0, K1, A1, K2, A2, ……, Km, Am)其中 Ki < Ki+1, 1 =< m
这时必须把结点 p 分裂成两个结点 p 和 q,它们包含的信息分别为:
结点 p:( m/2 -1, A0, K1, A1, ……, Km/2 -1, Am/2 -1)
结点 q:(m - m/2, Am/2, Km/2+1, Am/2+1, ……, Km, Am)
位于中间的关键码 Km/2 与指向新结点 q 的指针形成一个二元组 ( Km/2, q ),插入到这两个结点的双亲结点中去。
在B-树,每个非失败结点的关键码个数都在[ m/2 -1, m-1]之间。插入在某个叶结点开始。如果在关键码插入后结点中的关键码个数超出了上界 m-1,则结点需要“分裂”,否则可以直接插入。
实现结点“分裂”的原则是:
设结点 A 中已经有 m-1 个关键码,当再插入一个关键码后结点中的状态为( m, A0, K1, A1, K2, A2, ……, Km, Am)其中 Ki < Ki+1, 1 =< m
这时必须把结点 p 分裂成两个结点 p 和 q,它们包含的信息分别为:
结点 p:( m/2 -1, A0, K1, A1, ……, Km/2 -1, Am/2 -1)
结点 q:(m - m/2, Am/2, Km/2+1, Am/2+1, ……, Km, Am)
位于中间的关键码 Km/2 与指向新结点 q 的指针形成一个二元组 ( Km/2, q ),插入到这两个结点的双亲结点中去。
B+树
M阶 B+树
树中每个非叶结点最多有 m 棵子树;
1. 根结点 (非叶结点) 至少有 2 棵子树。除根结点外, 其它的非叶结点至少有 ém/2ù 棵子树;
2. 有 n 棵子树的非叶结点有 n-1 个关键码。
3. 所有叶结点都处于同一层次上,包含了全部关键码及指向相应数据对象存放地址的指针,且叶结点本身按关键码从小到大顺序链接;
4. 每个叶结点中的子树棵数 n 可以多于 m,可以少于 m,视关键码字节数及对象地址指针字节数而定。
5. 若设结点可容纳最大关键码数为 m1,则指向对象的地址指针也有 m1 个。
6. 结点中的子树棵数 n 应满足 n 属于[m1/2, m1]
7. 若根结点同时又是叶结点,则结点格式同叶结点。
8. 所有的非叶结点可以看成是索引部分,结点中关键码 Ki 与指向子树的指针 Pi 构成对子树 (即下一层索引块) 的索引项 ( Ki, Pi ),Ki 是子树中最小的关键码。
特别地,子树指针 P0 所指子树上所有关键码均小于 K1。结点格式同B树。
叶结点中存放的是对实际数据对象的索引。
在B+树中有两个头指针:一个指向B+树的根结点,一个指向关键码最小的叶结点。
1. 根结点 (非叶结点) 至少有 2 棵子树。除根结点外, 其它的非叶结点至少有 ém/2ù 棵子树;
2. 有 n 棵子树的非叶结点有 n-1 个关键码。
3. 所有叶结点都处于同一层次上,包含了全部关键码及指向相应数据对象存放地址的指针,且叶结点本身按关键码从小到大顺序链接;
4. 每个叶结点中的子树棵数 n 可以多于 m,可以少于 m,视关键码字节数及对象地址指针字节数而定。
5. 若设结点可容纳最大关键码数为 m1,则指向对象的地址指针也有 m1 个。
6. 结点中的子树棵数 n 应满足 n 属于[m1/2, m1]
7. 若根结点同时又是叶结点,则结点格式同叶结点。
8. 所有的非叶结点可以看成是索引部分,结点中关键码 Ki 与指向子树的指针 Pi 构成对子树 (即下一层索引块) 的索引项 ( Ki, Pi ),Ki 是子树中最小的关键码。
特别地,子树指针 P0 所指子树上所有关键码均小于 K1。结点格式同B树。
叶结点中存放的是对实际数据对象的索引。
在B+树中有两个头指针:一个指向B+树的根结点,一个指向关键码最小的叶结点。
特新
所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
不可能在非叶子结点命中;
非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
更适合文件索引系统
不可能在非叶子结点命中;
非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
更适合文件索引系统
作为InnoDB的索引
j技术团队
为什么mysql 要选择用B+树而不是用B树?
树的高度决定磁盘的io 次数;
B 树 的叶子结点也会存储 完整的数据信息/
和B树相比的B+ 树有以下几点的优化:
1. B+树的所有数据都存储在叶子结点,非叶子结点只存储索引 ——
每层能存储的索引数量更多 | 层高相同的情况下存的数据更多
io 次数也会更加稳定
减少分裂
2. 叶子结点数据使用双向链表的方式进行关联 ——
非常适合范围查询
全局扫描能力更强
1. B+树的所有数据都存储在叶子结点,非叶子结点只存储索引 ——
每层能存储的索引数量更多 | 层高相同的情况下存的数据更多
io 次数也会更加稳定
减少分裂
2. 叶子结点数据使用双向链表的方式进行关联 ——
非常适合范围查询
全局扫描能力更强
mongo DB - 非关系性数据库
innodb - 关系型数据库
排序算法
计算机原理
1. HTTP 和 HTTPS 的区别?
HTTP
超文本传输协议,是一个基于TCP/IP通信协议来传递数据的协议,传输的数据为HTML文件、图片文件、查询结果等
客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。
HTTP允许传输任意类型的数据对象。传输的类型由Content-Type加以标记。
限制每次连接只处理一个请求。服务器处理完请求,并收到客户的应答后,即断开连接,但是却不利于客户端与服务器保持会话连接,为了弥补这种不足,产生了两项记录http状态的技术,一个叫做Cookie,一个叫做Session。
HTTPS
HTTP+SSL/TLS,通过SSL证书来验证服务器的身份,并为浏览器和服务器之间的通信进行加密,目前使用广泛的是TLS1.1 和 TLS1.2
流程
首先客户端通过URL访问服务器建立SSL连接。
服务端收到客户端请求后,会将网站支持的证书信息(证书中包含公钥)传送一份给客户端。
客户端的服务器开始协商SSL连接的安全等级,也就是信息加密的等级。
客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。
服务器利用自己的私钥解密出会话密钥。
服务器利用会话密钥加密与客户端之间的通信。
缺点
需要多次握手,会导致页面加载时间耗时较长
HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗
SSL涉及到安全算法会消耗CPU资源
区别
HTTP是明文的HTTPS使用了SSL/TLS加密处理
HTTP使用80端口为默认端口,HTTPS使用443端口为默认端口
HTTP1.0 1.1 2.0 的区别
HTTP 1.0:
连接管理: HTTP 1.0使用短连接,即每个HTTP请求都需要一个新的TCP连接。这导致了较高的连接开销,因为每个连接的建立和断开都需要时间。
请求-响应模型: 在HTTP 1.0中,每个请求都对应一个响应,请求和响应之间是一对一的关系。
头部信息: HTTP 1.0的头部信息较少,只支持基本的头部字段。
连接管理: HTTP 1.0使用短连接,即每个HTTP请求都需要一个新的TCP连接。这导致了较高的连接开销,因为每个连接的建立和断开都需要时间。
请求-响应模型: 在HTTP 1.0中,每个请求都对应一个响应,请求和响应之间是一对一的关系。
头部信息: HTTP 1.0的头部信息较少,只支持基本的头部字段。
HTTP 1.1:
连接管理: HTTP 1.1引入了持久连接(Keep-Alive),允许多个HTTP请求和响应通过同一个TCP连接传输。这减少了连接建立和断开的开销,提高了性能。
请求-响应模型: HTTP 1.1支持流水线化(Pipelining),允许多个请求在一个连接上并行发送,而不需要等待前一个请求的响应。这提高了效率。
头部信息: HTTP 1.1引入了更多的头部字段,包括缓存控制、内容协商、范围请求等。
连接管理: HTTP 1.1引入了持久连接(Keep-Alive),允许多个HTTP请求和响应通过同一个TCP连接传输。这减少了连接建立和断开的开销,提高了性能。
请求-响应模型: HTTP 1.1支持流水线化(Pipelining),允许多个请求在一个连接上并行发送,而不需要等待前一个请求的响应。这提高了效率。
头部信息: HTTP 1.1引入了更多的头部字段,包括缓存控制、内容协商、范围请求等。
HTTP 2.0:
二进制协议: HTTP 2.0采用二进制协议而不是文本协议,这减少了数据传输的开销。
多路复用: HTTP 2.0支持多路复用,允许多个请求和响应共享一个连接,无需等待。这极大提高了性能。
头部压缩: HTTP 2.0使用头部压缩技术,减小了每个请求的头部大小,减少了带宽占用。
服务器推送: HTTP 2.0允许服务器在客户端请求之前主动推送资源,提高了页面加载速度。
二进制协议: HTTP 2.0采用二进制协议而不是文本协议,这减少了数据传输的开销。
多路复用: HTTP 2.0支持多路复用,允许多个请求和响应共享一个连接,无需等待。这极大提高了性能。
头部压缩: HTTP 2.0使用头部压缩技术,减小了每个请求的头部大小,减少了带宽占用。
服务器推送: HTTP 2.0允许服务器在客户端请求之前主动推送资源,提高了页面加载速度。
也就是说,HTTP 2.0在性能和效率方面有显著改进,主要通过多路复用、头部压缩和服务器推送来实现。HTTP 1.1在HTTP 1.0的基础上引入了持久连接和请求-响应的流水线化,但仍然使用文本协议。根据性能需求和服务器支持情况,可以选择不同的HTTP版本。
HTTP如何实现长连接,什么时候回超时
http 长连接 和 tcp 长连接 的区别
HTTP 的 Keep-Alive,是由应用层(用户态)实现的,称为 HTTP 长连接;
TCP 的 Keepalive,是由TCP 层(内核态)实现的,称为 TCP 保活机制;
HTTP 的 Keep-Alive 也叫 HTTP 长连接,该功能是由「应用程序」实现的,可以使得用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,减少了 HTTP 短连接带来的多次 TCP 连接建立和释放的开销。
TCP 的 Keepalive 也叫 TCP 保活机制,该功能是由「内核」实现的,当客户端和服务端长达一定时间没有进行数据交互时,内核为了确保该连接是否还有效,就会发送探测报文,来检测对方是否还在线,然后来决定是否要关闭该连接。
TCP 的 Keepalive,是由TCP 层(内核态)实现的,称为 TCP 保活机制;
HTTP 的 Keep-Alive 也叫 HTTP 长连接,该功能是由「应用程序」实现的,可以使得用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,减少了 HTTP 短连接带来的多次 TCP 连接建立和释放的开销。
TCP 的 Keepalive 也叫 TCP 保活机制,该功能是由「内核」实现的,当客户端和服务端长达一定时间没有进行数据交互时,内核为了确保该连接是否还有效,就会发送探测报文,来检测对方是否还在线,然后来决定是否要关闭该连接。
在 HTTP 1.0 中默认是关闭的,如果浏览器要开启 Keep-Alive,它必须在请求的包头中添加:
Connection: Keep-Alive
从 HTTP 1.1 开始, 就默认是开启了 Keep-Alive,如果要关闭 Keep-Alive,需要在 HTTP 请求的包头里添加:
Connection:close
现在大多数浏览器都默认是使用 HTTP/1.1,所以 Keep-Alive 都是默认打开的。一旦客户端和服务端达成协议,那么长连接就建立好了。
Connection: Keep-Alive
从 HTTP 1.1 开始, 就默认是开启了 Keep-Alive,如果要关闭 Keep-Alive,需要在 HTTP 请求的包头里添加:
Connection:close
现在大多数浏览器都默认是使用 HTTP/1.1,所以 Keep-Alive 都是默认打开的。一旦客户端和服务端达成协议,那么长连接就建立好了。
一般都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。
2. GET请求 和 POST请求 的区别?
都包含请求头请求行,POST多了请求Body
GET多用来查询,请求参数直接放到URL上。POST用来提交,一般放在请求体里面
GET可以在URL上看到内容,POST用户无法直接看到
GET提交的数据长度是有限的,URL长度有限制,POST没有
HTTP常用的请求方式,区别和用途?
POST
用于发送包含用户提价数据的请求
GET
对服务器资源获取的简单请求
PUT
向服务器提交数据,以修改数据
HEAD
请求页面的首部,获取资源的元信息
DELETE
删除服务器上的某些资源
CONNECT
用于ssl隧道的基于代理的请求
OPTIONS
返回所有可用的方法,常用于跨域
TRACE
追踪请求-响应的传输路径
3. HTTP常用状态码有哪些?forward 和 redirect 的区别?
状态码
1
服务器收到信息,需要请求者继续执行操作
100
继续。客户端应继续其请求
101
切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议
2
成功,操作被成功接受并处理
200
请求成功。一般用于GET与POST请求
3
重定向,需要进一步完成操作
301
永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
302
临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
307
临时重定向。与302类似。使用GET请求重定向
4
客户端错误
403
服务器理解请求客户端的请求,但是拒绝执行此请求
404
服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面
405
客户端请求中的方法被禁止
5
服务端错误
500
服务器内部错误,无法完成请求
501
服务器不支持请求的功能,无法完成请求
502
作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
503
由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中
forward 和 redirect 的区别?
转发是服务器行为,重定向是客户端行为
转发过程:客户浏览器发送http请求 --> web服务器接受此请求 --> 调用内部的一个方法在容器内部完成请求处理和转发动作 --> 将目标资源发送给客户;在这里,转发的路径必须是同一个web容器下的url,其不能转向到其他的web路径上去,中间传递的是自己的容器内的request。在客户浏览器路径栏显示的仍然是其第一次访问的路径,也就是说客户是感觉不到服务器做了转发的。转发行为是浏览器只做了一次访问请求。
重定向过程:客户浏览器发送http请求 --> web服务器接受后发送302状态码响应及对应新的location给客户浏览器 --> 客户浏览器发现是302响应,则自动再发送一个新的http请求,请求url是新的location地址 --> 服务器根据此请求寻找资源并发送给客户。在这里 location可以重定向到任意URL,既然是浏览器重新发出了请求,则就没有什么request传递的概念了。在客户浏览器路径栏显示的是其重定向的路径,客户可以观察到地址的变化的。重定向行为是浏览器做了至少两次的访问请求的。
简单说下你了解的端口及对应的服务
说下计算机网络体系结构
OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI 参考模型, ISO(国际标准化组织)组织在 1985 年研究网络互连模型,
OSI模型共分为七层
应用层:网络服务与最终用户的一个接口,常见的协议有:HTTP FTP SMTP SNMP DNS.
表示层:数据的表示、安全、压缩。确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。
会话层:建立、管理、终止会话,对应主机进程,指本地主机与远程主机正在进行的会话.
传输层:定义传输数据的协议端口号,以及流控和差错校验,协议有 TCP、UDP.
网络层:进行逻辑地址寻址,实现不同网络之间的路径选择,协议有 ICMP 、IGMP、 IP 等.
数据链路层:在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路。
物理层:建立、维护、断开物理连接。
OSI模型共分为七层
应用层:网络服务与最终用户的一个接口,常见的协议有:HTTP FTP SMTP SNMP DNS.
表示层:数据的表示、安全、压缩。确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。
会话层:建立、管理、终止会话,对应主机进程,指本地主机与远程主机正在进行的会话.
传输层:定义传输数据的协议端口号,以及流控和差错校验,协议有 TCP、UDP.
网络层:进行逻辑地址寻址,实现不同网络之间的路径选择,协议有 ICMP 、IGMP、 IP 等.
数据链路层:在物理层提供比特流服务的基础上,建立相邻结点之间的数据链路。
物理层:建立、维护、断开物理连接。
后面两种模型都是和前面这种的做对应的。
如何理解HTTP协议的无状态
当浏览器第一次发送请求给服务器时,服务器响应了;如果同个浏览器发第二次请求给服务器时,它还会响应,但是呢,服务器不知道你就是刚才的那个浏览器。简言之,服务器不会去记住你是谁,所以称为无状态协议。
什么是数字签名? 什么是数字证书?
加密
加密是通过使用某种算法将明文转变为不可读的密文,密文通过密钥还原出原来的明文,从而达到保护数据不被窃取泄漏的方法。
通常使用的数据编码算法有:AES、RSA、MD5、BASE64、SM4等。
其中AES、RSA 和 SM4 是较为常用的加密算法。MD5 是一种有损压缩的算法,无论数据量有多大,都会生成一个固定128位的散列值,由于其具有不可逆性、单向恒定性,通常用于验证文件完整性、口令加密以及数字签名。
BASE64 算法不需要使用密钥,任何人都可以使用该算法编码及解码数据,因此不用于数据加密中。
其中AES、RSA 和 SM4 是较为常用的加密算法。MD5 是一种有损压缩的算法,无论数据量有多大,都会生成一个固定128位的散列值,由于其具有不可逆性、单向恒定性,通常用于验证文件完整性、口令加密以及数字签名。
BASE64 算法不需要使用密钥,任何人都可以使用该算法编码及解码数据,因此不用于数据加密中。
对称加密
对称加密,通信双方使用相同的密钥进行数据加密及解密。双方线下约定好的密钥是一致的。
但是如果双方不能事先在线下约定好密钥,那么在传递密钥的的过程中因为需要使用明文传递,因此可能会被第三方截取并知悉密钥内容,所以并不是最安全的加密方式。
但是如果双方不能事先在线下约定好密钥,那么在传递密钥的的过程中因为需要使用明文传递,因此可能会被第三方截取并知悉密钥内容,所以并不是最安全的加密方式。
非对称加密
通信双方各生成一对公私钥,发信方使用收信方的公钥对内容进行加密,收信方使用自己的私钥即可解密。
由于公钥是公开的,因此可以使用明文传递,自己只要保存好私钥即可。
由于公钥是公开的,因此可以使用明文传递,自己只要保存好私钥即可。
但还是不能保障中途是否有人改了密文
数字签名
信息摘要:
一段信息,经过摘要算法得到一串哈希值,就是摘要(dijest)。
常见的摘要算法有MD5、SHA1、SHA256、SHA512等。
关于摘要,有几点需要你明白的:
摘要算法,是把任意长度的信息,映射成一个定长的字符串。
摘要算法,两个不同的信息,是有可能算出同一个摘要值的。
摘要算法与加密算法不同,不存在解密的过程。
摘要算法不用于数据的保密,而是用于数据的完整性校验。
一段信息,经过摘要算法得到一串哈希值,就是摘要(dijest)。
常见的摘要算法有MD5、SHA1、SHA256、SHA512等。
关于摘要,有几点需要你明白的:
摘要算法,是把任意长度的信息,映射成一个定长的字符串。
摘要算法,两个不同的信息,是有可能算出同一个摘要值的。
摘要算法与加密算法不同,不存在解密的过程。
摘要算法不用于数据的保密,而是用于数据的完整性校验。
摘要经过私钥的加密后,便有了一个新的名字 -- 数字签名。
签名 是在发送方,这是一个加密的过程。
验签 是在接收方,这是一个解密的过程。
签名 是在发送方,这是一个加密的过程。
验签 是在接收方,这是一个解密的过程。
第一个问题,有了信息摘要,为何还要有数字签名?
答:信息摘要,虽然也不可逆,但却容易却被伪造。所以信息摘要只用于校验完整性,而要保证信息摘要的正确性,就要依靠数字签名啦。
数字签名的签名和验签是非对称加密,其他人除非拿到私钥,不然没法伪造。
第二个问题,为什么不对内容直接加密,而是对摘要进行加密。
答:由上面我们知道了非对称加密的速度非常慢,如果传输的数据量非常大,那这个加密再解密的时间要远比网络传输的时间来得长,这样反而会得不偿失。
如果我们对传输的内容只有完整性要求,而安全性没有要求(意思是传输的内容被人知道了也没关系)。那就可以对摘要进行加密,到客户端这里解密后得到摘要明文,再用这个摘要明文与传输的数据二次计算的摘要进行比较,若一致,则说明传输的内容是完整的,没有被篡改。
答:信息摘要,虽然也不可逆,但却容易却被伪造。所以信息摘要只用于校验完整性,而要保证信息摘要的正确性,就要依靠数字签名啦。
数字签名的签名和验签是非对称加密,其他人除非拿到私钥,不然没法伪造。
第二个问题,为什么不对内容直接加密,而是对摘要进行加密。
答:由上面我们知道了非对称加密的速度非常慢,如果传输的数据量非常大,那这个加密再解密的时间要远比网络传输的时间来得长,这样反而会得不偿失。
如果我们对传输的内容只有完整性要求,而安全性没有要求(意思是传输的内容被人知道了也没关系)。那就可以对摘要进行加密,到客户端这里解密后得到摘要明文,再用这个摘要明文与传输的数据二次计算的摘要进行比较,若一致,则说明传输的内容是完整的,没有被篡改。
数字证书
数字证书是什么东西?其实它就是一个 .crt 文件
数字证书是谁颁发的?由权威证书认证机构颁发,一般我们简称为 CA 机构
数字证书如何申请的?或者说如何颁发的?
所谓数字证书,是一种用于电脑的身份识别机制。由数字证书颁发机构(CA)对使用私钥创建的签名请求文件做的签名(盖章),表示CA结构对证书持有者的认可。
(1) 数字证书拥有以下几个优点
使用数字证书能够提高用户的可信度;
数字证书中的公钥,能够与服务端的私钥配对使用,实现数据传输过程中的加密和解密;
在认证使用者身份期间,使用者的敏感个人数据并不会被传输至证书持有者的网络系统上;
使用数字证书能够提高用户的可信度;
数字证书中的公钥,能够与服务端的私钥配对使用,实现数据传输过程中的加密和解密;
在认证使用者身份期间,使用者的敏感个人数据并不会被传输至证书持有者的网络系统上;
(2) 证书类型
x509的证书编码格式有两种:
PEM(Privacy-enhanced Electronic Mail)是明文格式的,以 -----BEGIN CERTIFICATE-----开头,以-----END CERTIFICATE-----结尾。中间是经过base64编码的内容,apache需要的证书就是这类编码的证书.查看这类证书的信息的命令为: openssl x509 -noout -text -in server.pem。其实PEM就是把DER的内容进行了一次base64编码
DER是二进制格式的证书,查看这类证书的信息的命令为: openssl x509 -noout -text -inform der -in server.der
x509的证书编码格式有两种:
PEM(Privacy-enhanced Electronic Mail)是明文格式的,以 -----BEGIN CERTIFICATE-----开头,以-----END CERTIFICATE-----结尾。中间是经过base64编码的内容,apache需要的证书就是这类编码的证书.查看这类证书的信息的命令为: openssl x509 -noout -text -in server.pem。其实PEM就是把DER的内容进行了一次base64编码
DER是二进制格式的证书,查看这类证书的信息的命令为: openssl x509 -noout -text -inform der -in server.der
(3) 扩展名
.crt证书文件,可以是DER(二进制)编码的,也可以是PEM(ASCII (Base64))编码的),在类unix系统中比较常见;
.cer也是证书,常见于Windows系统。编码类型同样可以是DER或者PEM的,windows下有工具可以转换crt到cer;
.csr证书签名请求文件,一般是生成请求以后发送给CA,然后CA会给您签名并发回证书
.key一般公钥或者密钥都会用这种扩展名,可以是DER编码的或者是PEM编码的。查看DER编码的(公钥或者密钥)的文件的命令为: openssl rsa -inform DER -noout -text -in xxx.key。查看PEM编码的(公钥或者密钥)的文件的命令为: openssl rsa -inform PEM -noout -text -in xxx.key;
.p12证书文件,包含一个X509证书和一个被密码保护的私钥
.crt证书文件,可以是DER(二进制)编码的,也可以是PEM(ASCII (Base64))编码的),在类unix系统中比较常见;
.cer也是证书,常见于Windows系统。编码类型同样可以是DER或者PEM的,windows下有工具可以转换crt到cer;
.csr证书签名请求文件,一般是生成请求以后发送给CA,然后CA会给您签名并发回证书
.key一般公钥或者密钥都会用这种扩展名,可以是DER编码的或者是PEM编码的。查看DER编码的(公钥或者密钥)的文件的命令为: openssl rsa -inform DER -noout -text -in xxx.key。查看PEM编码的(公钥或者密钥)的文件的命令为: openssl rsa -inform PEM -noout -text -in xxx.key;
.p12证书文件,包含一个X509证书和一个被密码保护的私钥
(4) 证书的种类
安全证书主要分为DV、OV和EV三个种类,对应的安全等级为一般、较好和最高三个等级。三者的审核过程、审核标准和对应的域名数量也不同,所以价格在一两百元到几万元不等。
安全证书主要分为DV、OV和EV三个种类,对应的安全等级为一般、较好和最高三个等级。三者的审核过程、审核标准和对应的域名数量也不同,所以价格在一两百元到几万元不等。
(5) 证书在哪里
当你在下载并安装浏览器时,浏览器内部其实已经内嵌了全世界公认的根证书颁发机构的证书。
若一个网站的数字证书的证书颁发机构在浏览器中没有,则需要引导用户自行导入。
如果你想在 Chrome 中查看有哪些受信任的证书颁发机构,可以点击 设置 -> 隐私设置与安全性 -> 安全 -> 管理证书
当你在下载并安装浏览器时,浏览器内部其实已经内嵌了全世界公认的根证书颁发机构的证书。
若一个网站的数字证书的证书颁发机构在浏览器中没有,则需要引导用户自行导入。
如果你想在 Chrome 中查看有哪些受信任的证书颁发机构,可以点击 设置 -> 隐私设置与安全性 -> 安全 -> 管理证书
(6) 证书里的信息
在上图的位置里,随便双击点开一个证书,就可以查看证书里的内容。
内容非常多,最主要的有
证书是哪个机构的?
证书里的公钥是什么?
证书有效期是什么时候?
采用的哪种加解密的算法?
在上图的位置里,随便双击点开一个证书,就可以查看证书里的内容。
内容非常多,最主要的有
证书是哪个机构的?
证书里的公钥是什么?
证书有效期是什么时候?
采用的哪种加解密的算法?
(7) 证书吊销
证书是有生命周期的,如果证书的私钥泄漏了那这个证书就得吊销,一般有两种吊销方式:CRL和OCSP。
证书是有生命周期的,如果证书的私钥泄漏了那这个证书就得吊销,一般有两种吊销方式:CRL和OCSP。
在自己的服务器上生成一对公钥和私钥。然后将域名、申请者、公钥(注意不是私钥,私钥是无论如何也不能泄露的)等其他信息整合在一起,生成.csr 文件。
将这个 .csr 文件发给 CA 机构,CA 机构收到申请后,会通过各种手段验证申请者的组织信息和个人信息,如无异常(组织存在,企业合法,确实是域名的拥有者),CA 就会使用散列算法对.csr里的明文信息先做一个HASH,得到一个信息摘要,再用 CA 自己的私钥对这个信息摘要进行加密,生成一串密文,密文即是所说的 签名。签名 + .csr 明文信息,即是 证书。CA 把这个证书返回给申请人。
将这个 .csr 文件发给 CA 机构,CA 机构收到申请后,会通过各种手段验证申请者的组织信息和个人信息,如无异常(组织存在,企业合法,确实是域名的拥有者),CA 就会使用散列算法对.csr里的明文信息先做一个HASH,得到一个信息摘要,再用 CA 自己的私钥对这个信息摘要进行加密,生成一串密文,密文即是所说的 签名。签名 + .csr 明文信息,即是 证书。CA 把这个证书返回给申请人。
4. 如何生成 CSR 文件
(1) 使用 OpenSSL 生成
(2) 使用在线生成工具
(1) 使用 OpenSSL 生成
(2) 使用在线生成工具
TLS/SSL 保证信息的安全
信息的保密性
信息的完整性
身份识别
信息的保密性
信息的完整性
身份识别
问题
1.非对称加密中,公私钥都可以加密,那么什么时候使用公钥加密,什么时候使用私钥加密?
加密场景下,希望别人都可以加密,但只有我能解密,即公钥加密,私钥解密
签名场景下,希望只有我能签名,但别人都可以验签,即私钥签名,公钥验签
签名场景下,希望只有我能签名,但别人都可以验签,即私钥签名,公钥验签
2. 什么是数字签名,数字签名有什么作用?
数字签名是将数据的摘要通过私钥进行签名,并和加密后的数据一同发送
防篡改,防冒充
防篡改,防冒充
3. 为什么要对数据的摘要进行签名,而不是直接计算原始数据的数字签名?
数据量可能比较多,使用非对称加密比较复杂,增加时耗,因此对数据的摘要进行签名
同时也防止第三方拿到签名后使用公钥解出原始数据
同时也防止第三方拿到签名后使用公钥解出原始数据
4. 什么是数字证书,数字证书用来干什么?
数字证书是证书颁发机构使用自己的私钥对申请者的公钥进行签名认证
解决公钥的安全分发问题,也奠定了信任链的基础
解决公钥的安全分发问题,也奠定了信任链的基础
什么事CSRF攻击,如何避免?
CSRF,跨站请求伪造(英文全称 Cross-site request forgery),是一种
挟制用户在当前已登录的 Web 应用程序上执行非本意操作的攻击方法。
挟制用户在当前已登录的 Web 应用程序上执行非本意操作的攻击方法。
0. Tom 登陆银行,没有退出,浏览器包含了 Tom 在银行的身份认证信息。
0. 黑客 Jerry 将伪造的转账请求,包含在在帖子
0. Tom 在银行网站保持登陆的情况下,浏览帖子
0. 将伪造的转账请求连同身份认证信息,发送到银行网站
0. 银行网站看到身份认证信息,以为就是 Tom 的合法操作,最后造成 Tom
资金损失。
0. 黑客 Jerry 将伪造的转账请求,包含在在帖子
0. Tom 在银行网站保持登陆的情况下,浏览帖子
0. 将伪造的转账请求连同身份认证信息,发送到银行网站
0. 银行网站看到身份认证信息,以为就是 Tom 的合法操作,最后造成 Tom
资金损失。
怎么解决CSRF攻击?
检查Referer 字段。
添加校验 token。
检查Referer 字段。
添加校验 token。
DNS 的解析过程
下一道题目里面的连接也有
下一道题目里面的连接也有
首先会查找浏览器的缓存,看看是否能找到 www.baidu.com 对应的IP 地址,
找到就直接返回;否则进行下一步。
将请求发往给本地 DNS 服务器,如果查找到也直接返回,否则继续进行下一步;
本地 DNS 服务器向根域名服务器发送请求,根域名服务器返回负责.com 的顶
级域名服务器的IP 地址列表。
本地 DNS 服务器再向其中一个负责.com 的顶级域名服务器发送一个请求,返
回负责.baidu 的权威域名服务器的 IP 地址列表。
本地 DNS 服务器再向其中一个权威域名服务器发送一个请求,返回
www.baidu.com 所对应 IP 地址。
找到就直接返回;否则进行下一步。
将请求发往给本地 DNS 服务器,如果查找到也直接返回,否则继续进行下一步;
本地 DNS 服务器向根域名服务器发送请求,根域名服务器返回负责.com 的顶
级域名服务器的IP 地址列表。
本地 DNS 服务器再向其中一个负责.com 的顶级域名服务器发送一个请求,返
回负责.baidu 的权威域名服务器的 IP 地址列表。
本地 DNS 服务器再向其中一个权威域名服务器发送一个请求,返回
www.baidu.com 所对应 IP 地址。
说说 WebSocket 与socket 的区别
Socket 其实就等于 IP 地址 + 端口 + 协议。
WebSocket 是一个持久化的协议,它伴随 H5 而出的协议,用来解决 http 不
支持持久化连的问题。
Socket 一个网编编程的标准接口,而 WebSocket 则是应用层通信协议。
WebSocket 是一个持久化的协议,它伴随 H5 而出的协议,用来解决 http 不
支持持久化连的问题。
Socket 一个网编编程的标准接口,而 WebSocket 则是应用层通信协议。
具体来说,Socket 的一套标准,它完成了对 TCP/IP 的高度封装,屏蔽网络细
节,以方便开发者更好地进行网络编程。
节,以方便开发者更好地进行网络编程。
什么是 dos ddos drdos 攻击?
DOS: (Denial of Service),翻译过来就是拒绝服务,一切能引起 DOS 行为的攻击都被称为 DOS 攻击。
最常见的DoS 攻击就有计算机网络宽带攻击、连通性攻击。
DDoS: (Distributed Denial of Service),翻译过来的分布式拒绝服务。
是指处于不同位置的多个攻击者同时向一个或几个目标发动攻击,或者一个攻击者
控制了位于不同位置的多台机器并利用这些机器对受害者同时实施攻击。
常见的DDos 有 SYN Flood、Ping of Death、ACK Flood、UDP Flood等。
DRDoS: (Distributed Reflection Denial of Service),中文的分布式反射拒绝服务,该方式靠发送大量带有被害者 IP 地址的数据包给攻击主机,然后
攻击主机对 IP 地址源做出大量回应,从而形成拒绝服务攻击。
最常见的DoS 攻击就有计算机网络宽带攻击、连通性攻击。
DDoS: (Distributed Denial of Service),翻译过来的分布式拒绝服务。
是指处于不同位置的多个攻击者同时向一个或几个目标发动攻击,或者一个攻击者
控制了位于不同位置的多台机器并利用这些机器对受害者同时实施攻击。
常见的DDos 有 SYN Flood、Ping of Death、ACK Flood、UDP Flood等。
DRDoS: (Distributed Reflection Denial of Service),中文的分布式反射拒绝服务,该方式靠发送大量带有被害者 IP 地址的数据包给攻击主机,然后
攻击主机对 IP 地址源做出大量回应,从而形成拒绝服务攻击。
什么是xss攻击,如何避免?
XSS 攻击也是比较常见,XSS,叫跨站脚本攻击(Cross-Site Scripting),因为会与层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,因此有人将跨站脚本攻击缩写为 XSS。它指的是恶意攻击者往 Web 页面里插入恶意 html 代码,当用户浏览该页之时,嵌入其中 Web 里面的 html代码会被执行,从而达到恶意攻击用户的特殊目的。XSS 攻击一般分三种类型:
存储型 、反射型 、DOM 型 XSS
存储型 、反射型 、DOM 型 XSS
如何攻击?
如何解决?
首先,就不能相信用户的输入,对输入进行过滤,过滤标签等,只允许合法值。
HTML 转义
对于链接跳转,如 <a href="xxx"等,要校验内容,禁止以 script 开头的非法
链接。
限制输入长度等等
首先,就不能相信用户的输入,对输入进行过滤,过滤标签等,只允许合法值。
HTML 转义
对于链接跳转,如 <a href="xxx"等,要校验内容,禁止以 script 开头的非法
链接。
限制输入长度等等
聊一聊 SQL 注入
SQL 注入是一种代码注入技术,一般被应用于攻击 web 应用程序。它通过在
web 应用接口传入一些特殊参数字符,来欺应用服务器,执行恶意的SQL 命
令,以达到非法获取系统信息的目的。它目前是黑客对数据库进行攻击的最
常用手段之一。
web 应用接口传入一些特殊参数字符,来欺应用服务器,执行恶意的SQL 命
令,以达到非法获取系统信息的目的。它目前是黑客对数据库进行攻击的最
常用手段之一。
SQL 注入是如何攻击的?
sql 拼接查询 “or 1 = 1”
如何防止?
1). 使用#{}而不是 ${}
在 MyBatis 中,使用#{}而不是${},可以很大程度防止 sql 注入。
因为#{}的一个参数占位符,对于字符串类型,会自动加上"",其他类型不加。由
于 Mybatis 采用预编译,其后的参数不会再进行 SQL 编译,所以一定程度上防
止SQL 注入。
${}一个简单的字符串替换,字符串是什么,就会解析成什么,存在 SQL 注入
风险
在 MyBatis 中,使用#{}而不是${},可以很大程度防止 sql 注入。
因为#{}的一个参数占位符,对于字符串类型,会自动加上"",其他类型不加。由
于 Mybatis 采用预编译,其后的参数不会再进行 SQL 编译,所以一定程度上防
止SQL 注入。
${}一个简单的字符串替换,字符串是什么,就会解析成什么,存在 SQL 注入
风险
2). 不要暴露一些不必要日志或者安全信息,比如避免直接响应一些 sql 异
常信息。
如果 SQL 发生异常了,不要把这些信息暴露响应给用户,可以自定义异常进行
响应
常信息。
如果 SQL 发生异常了,不要把这些信息暴露响应给用户,可以自定义异常进行
响应
3). 不相信任何外部输入参数,过滤参数中含有一些数据库关键词关键词
可以加个参数校验过滤✁方法,过滤 union,or等数据库关键词
可以加个参数校验过滤✁方法,过滤 union,or等数据库关键词
4). 适当的权限控制
在你查询信息时,先校验下当前用户是否有这个权限。比如说,实现代码的时
候,可以让用户多传一个企业 Id 什么的,或者获取当前用户的 session 信息等,
在查询前,先校验一下当前用户是否是这个企业下的等等,是的话才有这个查
询员工的权限。
在你查询信息时,先校验下当前用户是否有这个权限。比如说,实现代码的时
候,可以让用户多传一个企业 Id 什么的,或者获取当前用户的 session 信息等,
在查询前,先校验一下当前用户是否是这个企业下的等等,是的话才有这个查
询员工的权限。
9. 从浏览器输入网址到页面加载需要经过那些步骤?
1、输入地址
当我们开始在浏览器中输入网址的时候,浏览器其实就已经在智能的匹配可能得 url 了,他会从历史记录,书签等地方,找到已经输入的字符串可能对应的 url,然后给出智能提示,让你可以补全url地址。对于 google的chrome 的浏览器,他甚至会直接从缓存中把网页展示出来,就是说,你还没有按下 enter,页面就出来了。
2、浏览器查找域名的 IP 地址
DNS
DNS
浏览器会首先查看本地硬盘的 hosts 文件,看看其中有没有和这个域名对应的规则,如果有的话就直接使用 hosts 文件里面的 ip 地址。
如果在本地的 hosts 文件没有能够找到对应的 ip 地址,浏览器会发出一个 DNS请求到本地DNS服务器 。
查询你输入的网址的DNS请求到达本地DNS服务器之后,本地DNS服务器会首先查询它的缓存记录,如果缓存中有此条记录,就可以直接返回结果,此过程是递归的方式进行查询。如果没有,本地DNS服务器还要向DNS根服务器进行查询。
根DNS服务器没有记录具体的域名和IP地址的对应关系,而是告诉本地DNS服务器,你可以到域服务器上去继续查询,并给出域服务器的地址。这种过程是迭代的过程。
本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。
本地DNS服务器继续向域服务器发出请求,在这个例子中,请求的对象是.com域服务器。.com域服务器收到请求之后,也不会直接返回域名和IP地址的对应关系,而是告诉本地DNS服务器,你的域名的解析服务器的地址。
最后,本地DNS服务器向域名的解析服务器发出请求,这时就能收到一个域名和IP地址对应关系,本地DNS服务器不仅要把IP地址返回给用户电脑,还要把这个对应关系保存在缓存中,以备下次别的用户查询时,可以直接返回结果,加快网络访问。
3、浏览器向 web 服务器发送一个 HTTP 请求
拿到域名对应的IP地址之后,浏览器会以一个随机端口(1024<端口<65535)向服务器的WEB程序(常用的有httpd,nginx等)80端口发起TCP的连接请求。这个连接请求到达服务器端后(这中间通过各种路由设备,局域网内除外),进入到网卡,然后是进入到内核的TCP/IP协议栈(用于识别该连接请求,解封包,一层一层的剥开),还有可能要经过Netfilter防火墙(属于内核的模块)的过滤,最终到达WEB程序,最终建立了TCP/IP的连接。
4、服务器的永久重定向响应
服务器给浏览器响应一个301永久重定向响应,这样浏览器就会访问http://www.google.com/而非http://google.com/。
5、浏览器跟踪重定向地址
现在浏览器知道了 http://www.google.com/ 才是要访问的正确地址,所以它会发送另一个http请求。
6、服务器处理请求
后端从在固定的端口接收到TCP报文开始,它会对TCP连接进行处理,对HTTP协议进行解析,并按照报文格式进一步封装成HTTP Request对象,供上层使用。
7、服务器返回一个 HTTP 响应
经过前面的6个步骤,服务器收到了我们的请求,也处理我们的请求,到这一步,它会把它的处理结果返回,也就是返回一个HTPP响应。
8、浏览器显示 HTML
在浏览器没有完整接受全部HTML文档时,它就已经开始显示这个页面了,不同浏览器可能解析的过程不太一样
9、浏览器发送请求获取嵌入在 HTML 中的资源(如图片、音频、视频、CSS、JS等等)
TCP 三次握手机制
TCP协议是一种可靠的,基于字节流的,面向连接的传输层双工协议。它有以下三个特征:
1、通信双方的数据传输是稳定的,即便是在网络不好的情况下,也能够保证数据传输到目标端。
2、TCP通信双方的数据包传输是通过字节流来实现传输的
3、数据传输之前,必须要建立一个连接,然后基于这个连接进行数据传输
1、通信双方的数据传输是稳定的,即便是在网络不好的情况下,也能够保证数据传输到目标端。
2、TCP通信双方的数据包传输是通过字节流来实现传输的
3、数据传输之前,必须要建立一个连接,然后基于这个连接进行数据传输
1)客户端向服务端发送连接请求并携带同步序列号SYN。
2)服务端收到请求后,发送SYN和ACK, 这里的SYN表示服务端的同步序列号,ACK表示对
前面收到请求的一个确认,表示告诉客户端,请求已收到。
3)客户端收到服务端的请求后,再次发送ACK,这个ACK是针对服务端连接的一个确认,表示
告诉服务端,请求已收到。
2)服务端收到请求后,发送SYN和ACK, 这里的SYN表示服务端的同步序列号,ACK表示对
前面收到请求的一个确认,表示告诉客户端,请求已收到。
3)客户端收到服务端的请求后,再次发送ACK,这个ACK是针对服务端连接的一个确认,表示
告诉服务端,请求已收到。
为什么要有三次握手呢?
TCP是可靠性通信协议,所以TCP协议的通信双方都必须要维护一个序列号,去标记已经发送出去的
数据包,哪些是已经被对方签收的。而三次握手就是通信双方相互告知序列号的起始值,为了确保这
个序列号被收到,所以双方都需要有一个确认的操作。
TCP协议需要在一个不可靠的网络环境下实现可靠的数据传输,意味着通信双方必须要通过某种手段
来实现一个可靠的数据传输通道,而三次通信是建立这样一个通道的最小值。当然还可以四次、五次,
只是没必要浪费这个资源。
防止历史的重复连接初始化造成的混乱问题,比如说在网络比较差的情况下,客户端连续多次发送建
立连接的请求,假设只有两次握手,那么服务端只能选择接受或者拒绝这个连接请求,但是服务端不
知道这次请求是不是之前因为网络堵塞而过期的请求,也就是说服务端不知道当前客户端的连接是有
效还是无效
TCP是可靠性通信协议,所以TCP协议的通信双方都必须要维护一个序列号,去标记已经发送出去的
数据包,哪些是已经被对方签收的。而三次握手就是通信双方相互告知序列号的起始值,为了确保这
个序列号被收到,所以双方都需要有一个确认的操作。
TCP协议需要在一个不可靠的网络环境下实现可靠的数据传输,意味着通信双方必须要通过某种手段
来实现一个可靠的数据传输通道,而三次通信是建立这样一个通道的最小值。当然还可以四次、五次,
只是没必要浪费这个资源。
防止历史的重复连接初始化造成的混乱问题,比如说在网络比较差的情况下,客户端连续多次发送建
立连接的请求,假设只有两次握手,那么服务端只能选择接受或者拒绝这个连接请求,但是服务端不
知道这次请求是不是之前因为网络堵塞而过期的请求,也就是说服务端不知道当前客户端的连接是有
效还是无效
tcp 四次挥手机制
TCP 四次挥手过程
1. 第一次挥手(FIN=1,seq=u),发送完毕后,客户端进入 FIN_WAIT_1 状态。
2. 第二次挥手(ACK=1,ack=u+1,seq =v),发送完毕后,服务器端进入
CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态。
3. 第三次挥手(FIN=1,ACK1,seq=w,ack=u+1),发送完毕后,服务器端进入
LAST_ACK 状态,等待来自客户端的最后一个 ACK。
4. 第四次挥手(ACK=1,seq=u+1,ack=w+1),客户端接收到来自服务器端的关
闭请求,发送一个确认包,并进入 TIME_WAIT 状态,等待了某个固定时间(两
个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没
有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,
进入 CLOSED 状态。服务器端接收到这个确认包之后,关闭连接,进入
CLOSED 状态
1. 第一次挥手(FIN=1,seq=u),发送完毕后,客户端进入 FIN_WAIT_1 状态。
2. 第二次挥手(ACK=1,ack=u+1,seq =v),发送完毕后,服务器端进入
CLOSE_WAIT 状态,客户端接收到这个确认包之后,进入 FIN_WAIT_2 状态。
3. 第三次挥手(FIN=1,ACK1,seq=w,ack=u+1),发送完毕后,服务器端进入
LAST_ACK 状态,等待来自客户端的最后一个 ACK。
4. 第四次挥手(ACK=1,seq=u+1,ack=w+1),客户端接收到来自服务器端的关
闭请求,发送一个确认包,并进入 TIME_WAIT 状态,等待了某个固定时间(两
个最大段生命周期,2MSL,2 Maximum Segment Lifetime)之后,没
有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,
进入 CLOSED 状态。服务器端接收到这个确认包之后,关闭连接,进入
CLOSED 状态
为什么需要等待 2MSL,才进入 CLOSED 关闭状态
2MSL,two Maximum Segment Lifetime,即两个最大段生命周期。
1.为了保证客户端发送的最后一个 ACK 报文段能够到达服务端。 这个 ACK 报
文段有可能丢失,因而使处在 LAST-ACK 状态的服务端就收不到对已发送的
FIN + ACK 文段的确认。服务端会超时重传这个 FIN+ACK 文段,而客
户端就能在 2MSL 时间内(超时 + 1MSL 传输)收到这个重传的 FIN+ACK
文段。接着客户端重传一次确认,重新启动 2MSL 计时器。最后,客户端和
服务器都正常进入到 CLOSED 状态。
2. 防止已失效的连接请求在文段出现在本连接中。客户端在发送完最后一个
ACK 的文段后,再经过时间 2MSL,就可以使本连接持续的时间内所产生的所
有的文段都从网络中消失。这样就可以使下一个连接中不会出现这种旧连接请求的文段。
2MSL,two Maximum Segment Lifetime,即两个最大段生命周期。
1.为了保证客户端发送的最后一个 ACK 报文段能够到达服务端。 这个 ACK 报
文段有可能丢失,因而使处在 LAST-ACK 状态的服务端就收不到对已发送的
FIN + ACK 文段的确认。服务端会超时重传这个 FIN+ACK 文段,而客
户端就能在 2MSL 时间内(超时 + 1MSL 传输)收到这个重传的 FIN+ACK
文段。接着客户端重传一次确认,重新启动 2MSL 计时器。最后,客户端和
服务器都正常进入到 CLOSED 状态。
2. 防止已失效的连接请求在文段出现在本连接中。客户端在发送完最后一个
ACK 的文段后,再经过时间 2MSL,就可以使本连接持续的时间内所产生的所
有的文段都从网络中消失。这样就可以使下一个连接中不会出现这种旧连接请求的文段。
说说tcp 如何确保可靠性呢?
首先,TCP 的连接是基于三次握手,而断开则是基于四次挥手。确保链接和断开的
可靠性。
其次,TCP 的可靠性,还体现在有状态;TCP 会记录哪些数据发送了,哪些数据被
回收了,哪些没有被接受,并且保证数据包按序到达,保证数据传输不出差错。
再次,TCP 的可靠性,还体现在可控制。它有数据包校验、ACK 应答、**超时重
传(发送方)**、失数据重传(接收方)、丢弃重复数据、流量控制(滑动窗口)
和拥塞控制等机制。
可靠性。
其次,TCP 的可靠性,还体现在有状态;TCP 会记录哪些数据发送了,哪些数据被
回收了,哪些没有被接受,并且保证数据包按序到达,保证数据传输不出差错。
再次,TCP 的可靠性,还体现在可控制。它有数据包校验、ACK 应答、**超时重
传(发送方)**、失数据重传(接收方)、丢弃重复数据、流量控制(滑动窗口)
和拥塞控制等机制。
说说 TCP 的文首部有哪些字段,其作用又分别是什么?
TCP 和 UDP 的区别?
连接性:
TCP是面向连接的协议。在数据传输之前,TCP建立连接,确保双方都准备好通信。这包括三次握手和四次挥手等步骤,以确保数据的可靠传输。
UDP是面向无连接的协议。它不建立连接,数据包可以直接发送,不会进行握手和挥手等过程。这使得UDP传输速度更快,但不保证数据的可靠性。
TCP是面向连接的协议。在数据传输之前,TCP建立连接,确保双方都准备好通信。这包括三次握手和四次挥手等步骤,以确保数据的可靠传输。
UDP是面向无连接的协议。它不建立连接,数据包可以直接发送,不会进行握手和挥手等过程。这使得UDP传输速度更快,但不保证数据的可靠性。
可靠性:
TCP提供可靠的数据传输。它使用确认机制、重传机制和流量控制等功能,确保数据包按顺序到达,没有丢失,没有错误。
UDP提供不可靠的数据传输。它不提供确认、重传或错误检测,数据包可能会丢失或乱序,因此用于那些要求速度高、实时性强的应用。
TCP提供可靠的数据传输。它使用确认机制、重传机制和流量控制等功能,确保数据包按顺序到达,没有丢失,没有错误。
UDP提供不可靠的数据传输。它不提供确认、重传或错误检测,数据包可能会丢失或乱序,因此用于那些要求速度高、实时性强的应用。
数据大小:
TCP通常用于传输大量数据,因为它可以将数据分割成多个数据包,然后在接收端重新组装。这适用于文件传输和Web页面下载等。
UDP通常用于传输小型数据包,如音频和视频流,DNS查询等,这些应用对实时性要求更高。
TCP通常用于传输大量数据,因为它可以将数据分割成多个数据包,然后在接收端重新组装。这适用于文件传输和Web页面下载等。
UDP通常用于传输小型数据包,如音频和视频流,DNS查询等,这些应用对实时性要求更高。
头部开销:
TCP的头部开销较大,约占数据包大小的10%。这是因为它包含了许多控制信息,如序号、确认、窗口大小等。
UDP的头部开销较小,只包含源端口、目标端口、长度和校验和等信息。
TCP的头部开销较大,约占数据包大小的10%。这是因为它包含了许多控制信息,如序号、确认、窗口大小等。
UDP的头部开销较小,只包含源端口、目标端口、长度和校验和等信息。
流量控制:
TCP有流量控制机制,用于避免拥塞。它根据接收端的处理能力来控制数据的发送速度。
UDP不具备流量控制功能,数据包可以随意发送。这可能导致网络拥塞,需要应用层自行处理。
TCP有流量控制机制,用于避免拥塞。它根据接收端的处理能力来控制数据的发送速度。
UDP不具备流量控制功能,数据包可以随意发送。这可能导致网络拥塞,需要应用层自行处理。
基于 TCP ✁应用层协议有:HTTP、FTP、SMTP、TELNET、SSH
HTTP:HyperText Transfer Protocol(超文本传输协议),默认端口 80
FTP: File Transfer Protocol (文件传输协议), 默认端口(20 用于传输数据,
21用于传输控制信息) SMTP: Simple Mail Transfer Protocol (简单邮件传输协议) ,默认端口 25
TELNET: Teletype over the Network (网络电传), 默认端口 23
SSH: Secure Shell(安全外壳协议),默认端口 22
HTTP:HyperText Transfer Protocol(超文本传输协议),默认端口 80
FTP: File Transfer Protocol (文件传输协议), 默认端口(20 用于传输数据,
21用于传输控制信息) SMTP: Simple Mail Transfer Protocol (简单邮件传输协议) ,默认端口 25
TELNET: Teletype over the Network (网络电传), 默认端口 23
SSH: Secure Shell(安全外壳协议),默认端口 22
基于UDP ✁应用层协议:DNS、TFTP、SNMP
DNS : Domain Name Service (域名服务),默认端口 53
TFTP: Trivial File Transfer Protocol (简单文件传输协议),默认端口 69
SNMP:Simple Network Management Protocol(简单网络管理协议),通
过 UDP 端口 161 ➓收,只有 Trap 信息采用UDP 端口 162。
DNS : Domain Name Service (域名服务),默认端口 53
TFTP: Trivial File Transfer Protocol (简单文件传输协议),默认端口 69
SNMP:Simple Network Management Protocol(简单网络管理协议),通
过 UDP 端口 161 ➓收,只有 Trap 信息采用UDP 端口 162。
TCP 重传机制
TCP 协议具有重传机制,也就是说,如果发送方认为发生了丢包现象,就重发这些数据包。很显然,我们需要一个方法来「猜测」是否发生了丢包。最简单的想法就是,接收方每收到一个包,就向发送方返回一个 ACK,表示自己已经收到了这段数据,反过来,如果发送方一段时间内没有收到 ACK,就知道很可能是数据包丢失了,紧接着就重发该数据包,直到收到 ACK 为止。
即使是超时了,这个数据包也可能并没有丢,它只是到的很晚而已。毕竟 TCP 协议是位于传输层的协议,不可能明确知道数据链路层和物理层发生了什么。但这并不妨碍我们的超时重传机制,因为接收方会自动忽略重复的包。
即使是超时了,这个数据包也可能并没有丢,它只是到的很晚而已。毕竟 TCP 协议是位于传输层的协议,不可能明确知道数据链路层和物理层发生了什么。但这并不妨碍我们的超时重传机制,因为接收方会自动忽略重复的包。
超时怎么确定:
在这里先引入两个概念:
RTT(Round Trip Time):往返时延,也就是数据包从发出去到收到对应 ACK 的时间。RTT 是针对连接的,每一个连接都有各自独立的 RTT。
RTO(Retransmission Time Out):重传超时,也就是前面说的超时时间。
在这里先引入两个概念:
RTT(Round Trip Time):往返时延,也就是数据包从发出去到收到对应 ACK 的时间。RTT 是针对连接的,每一个连接都有各自独立的 RTT。
RTO(Retransmission Time Out):重传超时,也就是前面说的超时时间。
基于计时器的重传
这种机制下,每个数据包都有相应的计时器,一旦超过 RTO 而没有收到 ACK,就重发该数据包。没收到 ACK 的数据包都会存在重传缓冲区里,等到 ACK 后,就从缓冲区里删除。
首先明确一点,对 TCP 来说,超时重传是相当重要的事件(RTO 往往大于两倍的 RTT,超时往往意味着拥塞),一旦发生这种情况,TCP 不仅会重传对应数据段,还会降低当前的数据发送速率,因为TCP 会认为当前网络发生了拥塞。
这种机制下,每个数据包都有相应的计时器,一旦超过 RTO 而没有收到 ACK,就重发该数据包。没收到 ACK 的数据包都会存在重传缓冲区里,等到 ACK 后,就从缓冲区里删除。
首先明确一点,对 TCP 来说,超时重传是相当重要的事件(RTO 往往大于两倍的 RTT,超时往往意味着拥塞),一旦发生这种情况,TCP 不仅会重传对应数据段,还会降低当前的数据发送速率,因为TCP 会认为当前网络发生了拥塞。
快速重传
快速重传机制「RFC5681」基于接收端的反馈信息来引发重传,而非重传计时器超时。
刚刚提到过,基于计时器的重传往往要等待很长时间,而快速重传使用了很巧妙的方法来解决这个问题:服务器如果收到乱序的包,也给客户端回复 ACK,只不过是重复的 ACK。就拿刚刚的例子来说,收到乱序的包 6,7,8,9 时,服务器全都发 ACK = 5。这样,客户端就知道 5 发生了空缺。一般来说,如果客户端连续三次收到重复的 ACK,就会重传对应包,而不需要等到计时器超时。
快速重传机制「RFC5681」基于接收端的反馈信息来引发重传,而非重传计时器超时。
刚刚提到过,基于计时器的重传往往要等待很长时间,而快速重传使用了很巧妙的方法来解决这个问题:服务器如果收到乱序的包,也给客户端回复 ACK,只不过是重复的 ACK。就拿刚刚的例子来说,收到乱序的包 6,7,8,9 时,服务器全都发 ACK = 5。这样,客户端就知道 5 发生了空缺。一般来说,如果客户端连续三次收到重复的 ACK,就会重传对应包,而不需要等到计时器超时。
带选择确认的重传
改进的方法就是 SACK(Selective Acknowledgment),简单来讲就是在快速重传的基础上,返回最近收到的报文段的序列号范围,这样客户端就知道,哪些数据包已经到达服务器了。
来几个简单的示例:
case 1:第一个包丢失,剩下的 7 个包都被收到了。
当收到 7 个包的任何一个的时候,接收方会返回一个带 SACK 选项的 ACK,告知发送方自己收到了哪些乱序包。注:Left Edge,Right Edge 就是这些乱序包的左右边界。
case 2:第 2, 4, 6, 8 个数据包丢失。
收到第一个包时,没有乱序的情况,正常回复 ACK。
收到第 3, 5, 7 个包时,由于出现了乱序包,回复带 SACK 的 ACK。
因为这种情况下有很多碎片段,所以相应的 Block 段也有很多组,当然,因为选项字段大小限制, Block 也有上限。
不过 SACK 的规范「RFC2018」有点坑爹,接收方可能会在提供一个 SACK 告诉发送方这些信息后,又「食言」,也就是说,接收方可能把这些(乱序的)数据包删除掉,然后再通知发送方。
最后一句是说,当接收方缓冲区快被耗尽时,可以采取这种措施,当然并不建议这种行为。。。
由于这个操作,发送方在收到 SACK 以后,也不能直接清空重传缓冲区里的数据,一直到接收方发送普通的,ACK 号大于其最大序列号的值的时候才能清除。另外,重传计时器也收到影响,重传计时器应该忽略 SACK 的影响,毕竟接收方把数据删了跟丢包没啥区别。
DSACK 扩展
DSACK,即重复 SACK,这个机制是在 SACK 的基础上,额外携带信息,告知发送方有哪些数据包自己重复接收了。DSACK 的目的是帮助发送方判断,是否发生了包失序、ACK 丢失、包重复或伪重传。让 TCP 可以更好的做网络流控。
改进的方法就是 SACK(Selective Acknowledgment),简单来讲就是在快速重传的基础上,返回最近收到的报文段的序列号范围,这样客户端就知道,哪些数据包已经到达服务器了。
来几个简单的示例:
case 1:第一个包丢失,剩下的 7 个包都被收到了。
当收到 7 个包的任何一个的时候,接收方会返回一个带 SACK 选项的 ACK,告知发送方自己收到了哪些乱序包。注:Left Edge,Right Edge 就是这些乱序包的左右边界。
case 2:第 2, 4, 6, 8 个数据包丢失。
收到第一个包时,没有乱序的情况,正常回复 ACK。
收到第 3, 5, 7 个包时,由于出现了乱序包,回复带 SACK 的 ACK。
因为这种情况下有很多碎片段,所以相应的 Block 段也有很多组,当然,因为选项字段大小限制, Block 也有上限。
不过 SACK 的规范「RFC2018」有点坑爹,接收方可能会在提供一个 SACK 告诉发送方这些信息后,又「食言」,也就是说,接收方可能把这些(乱序的)数据包删除掉,然后再通知发送方。
最后一句是说,当接收方缓冲区快被耗尽时,可以采取这种措施,当然并不建议这种行为。。。
由于这个操作,发送方在收到 SACK 以后,也不能直接清空重传缓冲区里的数据,一直到接收方发送普通的,ACK 号大于其最大序列号的值的时候才能清除。另外,重传计时器也收到影响,重传计时器应该忽略 SACK 的影响,毕竟接收方把数据删了跟丢包没啥区别。
DSACK 扩展
DSACK,即重复 SACK,这个机制是在 SACK 的基础上,额外携带信息,告知发送方有哪些数据包自己重复接收了。DSACK 的目的是帮助发送方判断,是否发生了包失序、ACK 丢失、包重复或伪重传。让 TCP 可以更好的做网络流控。
TCP 重传 滑动窗口 流量控制 拥塞控制
说说半连接队列和SYN Flood 攻击的关系
半连接队列
当客户端发送SYN到服务端,服务端收到以后回复ACK和SYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列,也就是半连接队列。
当客户端发送SYN到服务端,服务端收到以后回复ACK和SYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列,也就是半连接队列。
全连接队列
当客户端返回ACK, 服务端接收后,三次握手完成。这个时候连接等待被具体的应用取走,在被取走之前,它会被推入另外一个 TCP 维护的队列,也就是全连接队列(Accept Queue)。
当客户端返回ACK, 服务端接收后,三次握手完成。这个时候连接等待被具体的应用取走,在被取走之前,它会被推入另外一个 TCP 维护的队列,也就是全连接队列(Accept Queue)。
SYN Flood 攻击原理
SYN Flood 属于典型的 DoS/DDoS 攻击。其攻击的原理很简单,就是用客户端在短时间内伪造大量不存在的 IP地址,并向服务端疯狂发送SYN。对于服务端而言,会产生两个危险的后果:
处理大量的SYN包并返回对应ACK, 势必有大量连接处于SYN_RCVD状态,从而占满整个半连接队列,无法处理正常的请求。
由于是不存在的 IP,服务端长时间收不到客户端的ACK,会导致服务端不断重发数据,直到耗尽服务端的资源。
SYN Flood 属于典型的 DoS/DDoS 攻击。其攻击的原理很简单,就是用客户端在短时间内伪造大量不存在的 IP地址,并向服务端疯狂发送SYN。对于服务端而言,会产生两个危险的后果:
处理大量的SYN包并返回对应ACK, 势必有大量连接处于SYN_RCVD状态,从而占满整个半连接队列,无法处理正常的请求。
由于是不存在的 IP,服务端长时间收不到客户端的ACK,会导致服务端不断重发数据,直到耗尽服务端的资源。
如何应对 SYN Flood 攻击?
增加 SYN 连接,也就是增加半连接队列的容量。
减少 SYN + ACK 重试次数,避免大量的超时重发。
利用 SYN Cookie技术,在服务端接收到SYN后不立即分配连接资源,而是根据这个SYN计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复ACK的时候带上这个Cookie值,服务端验证Cookie 合法之后才分配连接资源。
增加 SYN 连接,也就是增加半连接队列的容量。
减少 SYN + ACK 重试次数,避免大量的超时重发。
利用 SYN Cookie技术,在服务端接收到SYN后不立即分配连接资源,而是根据这个SYN计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复ACK的时候带上这个Cookie值,服务端验证Cookie 合法之后才分配连接资源。
半连接队列和 SYN Flood 攻击的关系
三次握手前,服务端的状态从CLOSED变为LISTEN, 同时在内部创建了两个队列:
半连接队列和全连接队列,即SYN队列和ACCEPT队列。
半连接队列是当客户端发送SYN到服务端,服务端收到以后回复ACK和SYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列
SYN Flood在短时间内伪造大量不存在的 IP地址,并向服务端疯狂发送SYN。处理大量的SYN包并返回对应ACK, 势必有大量连接处于SYN_RCVD状态,从而占满整个半连接队列,无法处理正常的请求。
三次握手前,服务端的状态从CLOSED变为LISTEN, 同时在内部创建了两个队列:
半连接队列和全连接队列,即SYN队列和ACCEPT队列。
半连接队列是当客户端发送SYN到服务端,服务端收到以后回复ACK和SYN,状态由LISTEN变为SYN_RCVD,此时这个连接就被推入了SYN队列
SYN Flood在短时间内伪造大量不存在的 IP地址,并向服务端疯狂发送SYN。处理大量的SYN包并返回对应ACK, 势必有大量连接处于SYN_RCVD状态,从而占满整个半连接队列,无法处理正常的请求。
TCP 粘包和拆包
TCP是面向字节流的协议,就是没有界限的一串数据,本没有“包”的概念,“粘包”和“拆包”一说是为了有助于形象地理解这两种现象。
为什么UDP没有粘包?
粘包拆包问题在数据链路层、网络层以及传输层都有可能发生。日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。
粘包拆包问题在数据链路层、网络层以及传输层都有可能发生。日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。
粘包拆包发生场景
因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。
如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。
如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。
因为TCP是面向流,没有边界,而操作系统在发送TCP数据时,会通过缓冲区来进行优化,例如缓冲区为1024个字节大小。
如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题。
如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包。
正常的理想情况,两个包恰好满足TCP缓冲区的大小或达到TCP等待时长,分别发送两个包;
粘包:两个包较小,间隔时间短,发生粘包,合并成一个包发送;
拆包:一个包过大,超过缓存区大小,拆分成两个或多个包发送;
拆包和粘包:Packet1过大,进行了拆包处理,而拆出去的一部分又与Packet2进行粘包处理。
粘包:两个包较小,间隔时间短,发生粘包,合并成一个包发送;
拆包:一个包过大,超过缓存区大小,拆分成两个或多个包发送;
拆包和粘包:Packet1过大,进行了拆包处理,而拆出去的一部分又与Packet2进行粘包处理。
常见的解决方案
对于粘包和拆包问题,常见的解决方案有四种:
发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0或空等进行填充到指定长度;
发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,FTP协议;
将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
通过自定义协议进行粘包和拆包的处理。
对于粘包和拆包问题,常见的解决方案有四种:
发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0或空等进行填充到指定长度;
发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,FTP协议;
将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
通过自定义协议进行粘包和拆包的处理。
Netty对粘包和拆包问题的处理
Netty对解决粘包和拆包的方案做了抽象,提供了一些解码器(Decoder)来解决粘包和拆包的问题。如:
LineBasedFrameDecoder:以行为单位进行数据包的解码;
DelimiterBasedFrameDecoder:以特殊的符号作为分隔来进行数据包的解码;
FixedLengthFrameDecoder:以固定长度进行数据包的解码;
LenghtFieldBasedFrameDecode:适用于消息头包含消息长度的协议(最常用);
基于Netty进行网络读写的程序,可以直接使用这些Decoder来完成数据包的解码。对于高并发、大流量的系统来说,每个数据包都不应该传输多余的数据(所以补齐的方式不可取),LenghtFieldBasedFrameDecode更适合这样的场景。
Netty对解决粘包和拆包的方案做了抽象,提供了一些解码器(Decoder)来解决粘包和拆包的问题。如:
LineBasedFrameDecoder:以行为单位进行数据包的解码;
DelimiterBasedFrameDecoder:以特殊的符号作为分隔来进行数据包的解码;
FixedLengthFrameDecoder:以固定长度进行数据包的解码;
LenghtFieldBasedFrameDecode:适用于消息头包含消息长度的协议(最常用);
基于Netty进行网络读写的程序,可以直接使用这些Decoder来完成数据包的解码。对于高并发、大流量的系统来说,每个数据包都不应该传输多余的数据(所以补齐的方式不可取),LenghtFieldBasedFrameDecode更适合这样的场景。
小结
TCP协议粘包拆包问题是因为TCP协议数据传输是基于字节流的,它不包含消息、数据包等概念,需要应用层协议自己设计消息的边界,即消息帧(Message Framing)。如果应用层协议没有使用基于长度或者基于终结符息边界等方式进行处理,则会导致多个消息的粘包和拆包。
TCP协议粘包拆包问题是因为TCP协议数据传输是基于字节流的,它不包含消息、数据包等概念,需要应用层协议自己设计消息的边界,即消息帧(Message Framing)。如果应用层协议没有使用基于长度或者基于终结符息边界等方式进行处理,则会导致多个消息的粘包和拆包。
Nagle 算法与延迟确认
Nagle算法
1、如果没有『未确认的包』,立马发送当前包。这是首要原则
2、否则累积缓冲区的数据,直到没有『未确认的包』|| 累积的数据达到『MSS』的大小,立马发送数据
ps:||表示或者
总之重点就是不要一有数据就发送,我们要尽量的积攒一些数据然后发送。
1、如果没有『未确认的包』,立马发送当前包。这是首要原则
2、否则累积缓冲区的数据,直到没有『未确认的包』|| 累积的数据达到『MSS』的大小,立马发送数据
ps:||表示或者
总之重点就是不要一有数据就发送,我们要尽量的积攒一些数据然后发送。
ACK延迟确认
1、收到数据包之后不急着给ack,而是等待
2、如果一直收不到其他的数据包,等待一个『最大等待时间』,然后发送ack包
3、如果在等待的过程中,收到了其他数据包,立马发送ack包。
4、如果在等待的过程中,自己也有数据要发送给对方,那么顺便发送
总之重点就是不要一收到数据就发送确认,我们要尽量减少单纯的确认包。
1、收到数据包之后不急着给ack,而是等待
2、如果一直收不到其他的数据包,等待一个『最大等待时间』,然后发送ack包
3、如果在等待的过程中,收到了其他数据包,立马发送ack包。
4、如果在等待的过程中,自己也有数据要发送给对方,那么顺便发送
总之重点就是不要一收到数据就发送确认,我们要尽量减少单纯的确认包。
IP 地址有哪些分类?
一般可以这么认为,IP 地址=网络号+主机号。
1. 网络号:它标志主机所连接的网络地址表示属于互联网的哪一个网络。
2. 主机号:它标志主机地址表示其属于该网络中的哪一台主机。
IP 地址分为 A,B,C,D,E 五大类:
A 类地址(1~126):以 0 开头,网络号占前 8 位,主机号占后面 24 位。
B 类地址(128~191):以 10 开头,网络号占前 16 位,主机号占后面 16 位。
C 类地址(192~223):以 110 开头,网络号占前 24 位,主机号占后面 8 位。
D 类地址(224~239):以 1110 开头,保留位多播地址。
E 类地址(240~255):以 11110 开头,保留位为将来使用
1. 网络号:它标志主机所连接的网络地址表示属于互联网的哪一个网络。
2. 主机号:它标志主机地址表示其属于该网络中的哪一台主机。
IP 地址分为 A,B,C,D,E 五大类:
A 类地址(1~126):以 0 开头,网络号占前 8 位,主机号占后面 24 位。
B 类地址(128~191):以 10 开头,网络号占前 16 位,主机号占后面 16 位。
C 类地址(192~223):以 110 开头,网络号占前 24 位,主机号占后面 8 位。
D 类地址(224~239):以 1110 开头,保留位多播地址。
E 类地址(240~255):以 11110 开头,保留位为将来使用
ARP协议工作过程
ARP 协议的全称是 Address Resolution Protocol(地址解析协议),它是一个通过用于实现从 IP 地址到 MAC 地址的映射,即询问目标 IP 对应的 MAC 地址 的一种协议。ARP 协议在 IPv4 中极其重要。
注意:
ARP 只用于 IPv4 协议中,IPv6 协议使用的是 Neighbor Discovery Protocol,译为邻居发现协议,它被纳入 ICMPv6 中。
简而言之,ARP 就是一种解决地址问题的协议,它以 IP 地址为线索,定位下一个应该接收数据分包的主机 MAC 地址。如果目标主机不在同一个链路上,那么会查找下一跳路由器的 MAC 地址。
注意:
ARP 只用于 IPv4 协议中,IPv6 协议使用的是 Neighbor Discovery Protocol,译为邻居发现协议,它被纳入 ICMPv6 中。
简而言之,ARP 就是一种解决地址问题的协议,它以 IP 地址为线索,定位下一个应该接收数据分包的主机 MAC 地址。如果目标主机不在同一个链路上,那么会查找下一跳路由器的 MAC 地址。
IP地址将物理地址对上层隐藏起来,使Internet表现出统一的地址格式,但是在实际通讯时,IP地址不能被物理网络所识别物理网络所使用的依然是物理地址。因此必须实现IP地址对物理地址的映射。
对于以太网而言,当IP数据包通过以太网发送时,以太网链路并不识别32位的IP地址,它们是以48位的MAC地址表示该以太网节点。因此,必须在IP地址与MAC地址之间简历映射关系(MAP),而建立这种映射的过程咱们称之为地址解析(Resoloution)。
如上图中,假设HostA和HostB在同一个网段,HostA要向HostB发送IP包,其地址解析过程如下:
1.HostA首先查看自己的ARP表项,确定其中是否包含HostB的IP地址对应ARP表项。如果找到了对应的表项,则HostA直接理由ARP表项中的MAC地址对IP数据包封装成帧,并将帧发送给HostB。
2.如果HostA在ARP表中找不到对应的表项,则暂时缓存该数据包,然后以广播方式发送一个ARP轻轻。ARP请求报文中的发送端IP地址和发送端MAC地址为HostA的IP地址和MAC地址,目标IP地址HostB的IP地址,目标MAC地址为全0的MAC地址。
3.由于ARP请求报文以广播方式发送,该网段上的所有主机都可以接收到该请求。HostB比较自己的IP地址和ARP请求报文中的目标IP地址,由于两者相同,HostB将ARP请求报文中的发送端(HostA)IP地址和MAC地址存入自己的ARP表中,并以单播方式HostA发送ARP响应,其中包含了自己的MAC地址。其他主机发送请求的IP地址并非自己,于是都不做应答。
4.HostA收到ARP响应报文后,将HostB的MAC地址加入到自己的ARP表中,同时将IP数据包用此MAC地址为目的地址封装成帧并发送给HostB。
对于以太网而言,当IP数据包通过以太网发送时,以太网链路并不识别32位的IP地址,它们是以48位的MAC地址表示该以太网节点。因此,必须在IP地址与MAC地址之间简历映射关系(MAP),而建立这种映射的过程咱们称之为地址解析(Resoloution)。
如上图中,假设HostA和HostB在同一个网段,HostA要向HostB发送IP包,其地址解析过程如下:
1.HostA首先查看自己的ARP表项,确定其中是否包含HostB的IP地址对应ARP表项。如果找到了对应的表项,则HostA直接理由ARP表项中的MAC地址对IP数据包封装成帧,并将帧发送给HostB。
2.如果HostA在ARP表中找不到对应的表项,则暂时缓存该数据包,然后以广播方式发送一个ARP轻轻。ARP请求报文中的发送端IP地址和发送端MAC地址为HostA的IP地址和MAC地址,目标IP地址HostB的IP地址,目标MAC地址为全0的MAC地址。
3.由于ARP请求报文以广播方式发送,该网段上的所有主机都可以接收到该请求。HostB比较自己的IP地址和ARP请求报文中的目标IP地址,由于两者相同,HostB将ARP请求报文中的发送端(HostA)IP地址和MAC地址存入自己的ARP表中,并以单播方式HostA发送ARP响应,其中包含了自己的MAC地址。其他主机发送请求的IP地址并非自己,于是都不做应答。
4.HostA收到ARP响应报文后,将HostB的MAC地址加入到自己的ARP表中,同时将IP数据包用此MAC地址为目的地址封装成帧并发送给HostB。
聊聊保活计时器的作用
有了IP地址为什么还要有 MAC地址
IP地址:
IP地址是用于在互联网上唯一标识和寻址设备的地址。
它是网络层协议的一部分,允许数据在不同网络之间传输,因此它是全球范围内唯一的。
IP地址是逻辑地址,用于在网络中路由数据包。它们通常分为IPv4地址(32位)和IPv6地址(128位)。
IP地址是用于在互联网上唯一标识和寻址设备的地址。
它是网络层协议的一部分,允许数据在不同网络之间传输,因此它是全球范围内唯一的。
IP地址是逻辑地址,用于在网络中路由数据包。它们通常分为IPv4地址(32位)和IPv6地址(128位)。
MAC地址:
MAC地址是用于在局域网络(Local Area Network,LAN)中唯一标识和寻址网络设备的地址。
它是数据链路层(Data Link Layer)协议的一部分,用于在局域网络上直接传输数据包。
MAC地址通常是硬件地址,与设备的物理网络适配器(通常是网卡)相关。每个网络适配器都有唯一的MAC地址。
MAC地址是用于在局域网络(Local Area Network,LAN)中唯一标识和寻址网络设备的地址。
它是数据链路层(Data Link Layer)协议的一部分,用于在局域网络上直接传输数据包。
MAC地址通常是硬件地址,与设备的物理网络适配器(通常是网卡)相关。每个网络适配器都有唯一的MAC地址。
为什么需要同时使用IP地址和MAC地址:
IP地址用于在全球范围内唯一标识设备并进行路由,以确保数据能够从一个网络到另一个网络。
MAC地址用于在局域网络中唯一标识设备,以便在同一局域网络内直接传输数据帧。
在局域网络内,设备通常通过ARP(Address Resolution Protocol)将IP地址解析为MAC地址。ARP协议可以帮助设备找到其他设备的MAC地址,以便它们可以直接通信。所以,MAC地址是局域网络内的“物理地址”,而IP地址则是更高层次的逻辑地址,用于在全球互联网上唯一标识设备。
IP地址用于在全球范围内唯一标识设备并进行路由,以确保数据能够从一个网络到另一个网络。
MAC地址用于在局域网络中唯一标识设备,以便在同一局域网络内直接传输数据帧。
在局域网络内,设备通常通过ARP(Address Resolution Protocol)将IP地址解析为MAC地址。ARP协议可以帮助设备找到其他设备的MAC地址,以便它们可以直接通信。所以,MAC地址是局域网络内的“物理地址”,而IP地址则是更高层次的逻辑地址,用于在全球互联网上唯一标识设备。
4、AJAX 应用和传统 Web 应用有什么不同?
在传统的Js中,如果想发送客户端信息到服务器,需要建立一个HTML 表单然后GET或者POST数据到服务器端用户需要点击提交按钮来发送数据信息,然后等待服务器响应请求,页面重新加载使用AJAX技术,就可以使Javascript通过XMLHttpRequest对象直接与服务器进行交互
5、什么是 MVC?分别代表什么?
MVC开始是存在于Desktop程序中的,M是指数据模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现代码分离。C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。
视图是用户看到并与之交互的界面,视图没有真正的处理发生,不管这些数据是联机存储的还是一个雇员列表,作为视图来讲,它只是作为一种输出数据并允许用户操纵的方式。
模型表示企业数据和业务规则,模型返回的数据是中立的,就是说模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。
控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。
C设计模式考虑三种对象:模型对象、视图对象、和控制器对象。模型对象代表特别的知识和专业技能,它们负责保有应用程序的数据和定义操作数据的逻辑。视图对象知道如何显示应用程序的模型数据,而且可能允许用户对其进行编辑。控制器对象是应用程序的视图对象和模型对象之间的协调者。
6、拦截器和过滤器的区别?
过滤器和拦截器触发时机不一样:过滤器是在请求进入容器后,但请求进入 servlet 之前进行预处理的。请求结束返回也是,是在 servlet 处理完后,返回给前端之前。
拦截器是基于java的反射机制的,而过滤器是基于函数回调。
拦截器不依赖与servlet容器,过滤器依赖与servlet容器。
拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
拦截器可以获取 IOC 容器中的各个 bean ,而过滤器就不行,这点很重要,在拦截器里注入一个 service,可以调用业务逻辑。
7、Token、Cookie 和 Session 的区别?
token类似一个令牌,无状态,用户信息都被加密到token中,服务器收到token后解密就可知道是哪个用户。需要开发者手动添加。
session存储于服务器,可以理解为一个状态列表,拥有一个唯一识别符号sessionId,通常存放于cookie中。服务器收到cookie后解析出sessionId,再去session列表中查找,才能找到相应session。依赖cookie
cookie类似一个令牌,装有sessionId,存储在客户端,浏览器通常会自动添加。cookies是明文存储,安全性很低,只使用cookie的话盗取了cookie基本就获取了用户所有权限。
8、什么是跨域?有哪些解决方案?
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
10. 什么是反向代理,那正向代理是什么?
客户端本来可以直接通过HTTP协议访问某网站应用服务器,网站管理员可以在中间加上一个Nginx,客户端请求Nginx,Nginx请求应用服务器,然后将结果返回给客户端,此时Nginx就是反向代理服务器。
反向代理
是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。
优势
隐藏服务器真实IP
负载均衡
提高访问速度
提供安全保障
正向代理
是一个位于客户端和目标服务器之间的服务器(代理服务器),为了从目标服务器取得内容,客户端向代理服务器发送一个请求并指定目标,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端。
优势
突破访问限制
提高访问速度
隐藏客户端真实IP
区别
正向代理其实是客户端的代理,帮助客户端访问其无法访问的服务器资源。反向代理则是服务器的代理,帮助服务器做负载均衡,安全防护等。
正向代理一般是客户端架设的,比如在自己的机器上安装一个代理软件。而反向代理一般是服务器架设的,比如在自己的机器集群中部署一个反向代理服务器。
正向代理中,服务器不知道真正的客户端到底是谁,以为访问自己的就是真实的客户端。而在反向代理中,客户端不知道真正的服务器是谁,以为自己访问的就是真实的服务器。
正向代理和反向代理的作用和目的不同。正向代理主要是用来解决访问限制问题。而反向代理则是提供负载均衡、安全防护等作用。二者均能提高访问速度。
11. Rest规范
命名规则:URI本质上是一个资源,一个独立的URI地址对应一个独一无二的资源。如果需要对资源进行操作,采用get或者post请求。
MySQL
MySQL 基本架构 与日志模块
MySQL的基本架构
MySQL逻辑架构图
Server层
连接器
负责跟客户端建立连接、获取权限、维持和管理连接
mysql -h $ip -P $port -u $user -p
如果用户名密码认证通过,连接器会到权限表中获取用户权限
之后该连接的权限判断都依赖此时读到的权限,即使在连接成功后的这段时间内,管理员修改该用户的权限,也不会影响已存在连接的权限。
客户端长时间没有动静,连接器会自动断开,wait_timeout = 8
建立连接的过程比较复杂,使用中尽量减少建立连接动作
但是全部使用长连接会导致内存占用过大
但是全部使用长连接会导致内存占用过大
定期断开长连接,使用一段时间,程序判断执行过一个占用内存大的查询后断开连接
MySQL5.7以上版本,每次执行一个较大的操作后,通过执行mysql_reset_connection来重新出事化连接资源池。这个过程不需要重连和重新做权限验证,但是会将连接回复到刚创建时的状态。
查询缓存
mysql 拿到语句后会查看之前是否执行过这条语句。语句和结果会以key-value形式缓存在内存中,如果不在,会继续后面的查询阶段
不建议开启查询缓存,查询缓存的实效非常频繁,只要有对一个表的更新,这张表的缓存都会被清空,对于更新压力大的数据库来说,缓存的命中率会非常低,将query_cache_type 设置成demand 对于默认的SQL都不使用查询缓存。在8.0之后的版本直接将缓存功能删掉了。
分析器
首先进行词法分析,然后进行语法分析,根据语法规则判断输入的SQL是否满足MySQL语法
一般错误提示第一个出现错读的位置
优化器
在表中有多个索引时,决定使用哪个索引;在一个语句中有多个关联表时,决定各个表的连接顺序
执行计划的生成组件
执行器
执行前会判断表是否有权限(如果命中缓存的话,会在返回结果的时候做权限验证),如果不通过会抛出异常,通过后开始执行流程
row_examined 字段表示一条语句执行过程中扫描了多少行
所有内置函数(日期、时间、数学和加密函数)
扩存储引擎的功能
存储过程
触发器
视图
存储引擎层
负责数据存储和提取
插件式架构
不同的引擎共用相同的Server
引擎种类
InnoDB
MySQL5.5.5 默认存储引擎
MyISAM
Memory
查询语句执行顺序
连接器
先检查该语句是否有权限,如果没有权限,直接返回错误信息,
查询缓存
如果有权限会先查询缓存 (MySQL8.0 版本以前)。
分析器
如果没有缓存,分析器进行语法分析,提取 sql 语句中 select 等关键元素,然后判断 sql 语句是否有语法错误,比如关键词是否正确等等。
优化器
语法解析之后,MySQL 的服务器会对查询的语句进行优化,确定执行的方案。
执行器
完成查询优化后,按照生成的执行计划调用数据库引擎接口,返回执行结果。
更新语句执行顺序,两阶段提交
连接器
清除缓存
分析器
优化器
执行器
存储引擎
执行器先找引擎获取 ID=2 这一行。ID 是主键,存储引擎检索数据,找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
MySQL 在执行更新语句的时候,在服务层进行语句的解析和执行,在引擎层进行数据的提取和存储;同时在服务层对 binlog 进行写入,
在 InnoDB 内进行 redo log 的写入。
不仅如此,在对 redo log 写入时有两个阶段的提交,一是 binlog 写入之前prepare状态的写入,二是 binlog 写入之后commit状态的写入。
执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
MySQL 在执行更新语句的时候,在服务层进行语句的解析和执行,在引擎层进行数据的提取和存储;同时在服务层对 binlog 进行写入,
在 InnoDB 内进行 redo log 的写入。
不仅如此,在对 redo log 写入时有两个阶段的提交,一是 binlog 写入之前prepare状态的写入,二是 binlog 写入之后commit状态的写入。
细化
两阶段提交
引擎将数据更新到内存中,并将更新记录到redo log,redo log 处于prepare状态告知执行器完成。执行器生成这个操作的binlog,binlog写入磁盘。执行器调用引擎的提交事务接口,引擎将redo log 改成commit状态
如果未使用两阶段提交,可能会导致数据库状态和用它的日志恢复出来的状态不一致。
redo log 和 binlog 都可以用于表示事务提交的状态,而两阶段提交让这两个状态保持逻辑上的一致
该commit是事务提交的小步骤,MySQL在AB两时刻发生异常如何保证恢复
A时刻:写入redo log 处于prepare阶段后,写binlog之前发生崩溃
由于此时binlog还没写,redo log还没提交,崩溃恢复时事务会回滚,binlog还没写,索所以不会传到备库中
由于此时binlog还没写,redo log还没提交,崩溃恢复时事务会回滚,binlog还没写,索所以不会传到备库中
时刻B:binlog写完,redo log还没commit前发生crash
redo log里面事务完整,有commit标识,直接提交
redo log事务只有完整prepare,判断对应事务binlog是否存在并完整
是,提交事务
否,回滚事务
是,提交事务
否,回滚事务
MySQL如何知道binlog是完整的?
一个事务的binlog是有完整格式的:
statement格式的binlog最后会有COMMIT
row格式的binlog,最后会有一个XID event
statement格式的binlog最后会有COMMIT
row格式的binlog,最后会有一个XID event
5.6.2之后,引入了binlog-checksum参数,用于验证binlog内容正确性,如果日志在中间出错的情况,MySQL可以通过checksum结果来发现
redo log 和binlog如何关联
有共同的数据字段XID,崩溃恢复时,按照顺序扫描 redo log
如果碰到既有 prepare,又有commit的redo log 直接提交
如果碰到只有preoare没有commit的redo log,直接拿XID找binlog中对应的事务
处于prepare阶段的redo log加上完整的binlog,重启就能恢复,为什么要这样设计?
时刻B,binlog 写完以后MySQL发生崩溃,此时binlog已写入,只有从库会用来使用,所以主库也需要提交该事务,采用这样的策略可以保证主备数据一致
为什么要两阶段提交?
直接先redo log 写完,再写binlog。崩溃恢复时,两个日志全部完整再进行恢复
直接先redo log 写完,再写binlog。崩溃恢复时,两个日志全部完整再进行恢复
两阶段提交是,经典的分布式系统问题,不是MySQL独有
事务持久性问题:
对于InnoDB引擎,如果redo log提交完成,事务就不能归滚(如果允许回滚,可能覆盖掉别的事务的更新)
如果redo log直接提交,然后binlog写入时后失败了,InnoDB不能回滚,数据和binlog日志也不一致了
两阶段提交为了给所有的人一个机会,每个人都说 "我 ok"的时候,再一起提交
对于InnoDB引擎,如果redo log提交完成,事务就不能归滚(如果允许回滚,可能覆盖掉别的事务的更新)
如果redo log直接提交,然后binlog写入时后失败了,InnoDB不能回滚,数据和binlog日志也不一致了
两阶段提交为了给所有的人一个机会,每个人都说 "我 ok"的时候,再一起提交
日志模块
mysql 日志有哪些?分别介绍下作用?
MySQL 日志文件有很多,包括 :
错误日志(error log):错误日志文件对 MySQL 的启动、运行、关闭过程进行了记录,能帮助定位 MySQL 问题。
慢查询日志(slow query log):慢查询日志是用来记录执行时间超过 long_query_time 这个变量定义的时长的查询语句。通过慢查询日志,可以查找出哪些查询语句的执行效率很低,以便进行优化。
一般查询日志(general log):一般查询日志记录了所有对 MySQL 数据库请求的信息,无论请求是否正确执行。
二进制日志(bin log):关于二进制日志,它记录了数据库所有执行的 DDL 和 DML 语句(除了数据查询语句 select、show 等),以事件形式记录并保存在二进制文件中。
错误日志(error log):错误日志文件对 MySQL 的启动、运行、关闭过程进行了记录,能帮助定位 MySQL 问题。
慢查询日志(slow query log):慢查询日志是用来记录执行时间超过 long_query_time 这个变量定义的时长的查询语句。通过慢查询日志,可以查找出哪些查询语句的执行效率很低,以便进行优化。
一般查询日志(general log):一般查询日志记录了所有对 MySQL 数据库请求的信息,无论请求是否正确执行。
二进制日志(bin log):关于二进制日志,它记录了数据库所有执行的 DDL 和 DML 语句(除了数据查询语句 select、show 等),以事件形式记录并保存在二进制文件中。
还有两个 InnoDB 存储引擎特有的日志文件:
重做日志(redo log):重做日志至关重要,因为它们记录了对于 InnoDB 存储引擎的事务日志。
回滚日志(undo log):回滚日志同样也是 InnoDB 引擎提供的日志,顾名思义,回滚日志的作用就是对数据进行回滚。当事务对数据库进行修改,InnoDB 引擎不仅会记录 redo log,还会生成对应的 undo log 日志;如果事务执行失败或调用了 rollback,导致事务需要回滚,就可以利用 undo log 中的信息将数据回滚到修改之前的样子。
重做日志(redo log):重做日志至关重要,因为它们记录了对于 InnoDB 存储引擎的事务日志。
回滚日志(undo log):回滚日志同样也是 InnoDB 引擎提供的日志,顾名思义,回滚日志的作用就是对数据进行回滚。当事务对数据库进行修改,InnoDB 引擎不仅会记录 redo log,还会生成对应的 undo log 日志;如果事务执行失败或调用了 rollback,导致事务需要回滚,就可以利用 undo log 中的信息将数据回滚到修改之前的样子。
bin log
记录所有的逻辑操作,并且采用追加的形式
sync_binlog 设置成1时,每次事务的binlog都持久化到磁盘,可以保证MySQL异常重启后binlog不丢失
写入机制
事务执行过程中,先将日志写道binlog cache,事务提交的时候,再把binlog cache写到binlog中
一个事务的binlog是不能拆开的,需要确保一次性写入,涉及binlog cache的保存
系统会给binlog cache 分配一个内存,每个线程一个,参数binlog_cache_size用于控制单个线程内 binlog cache所占内存的大小。如果超过参数规定的大小需要暂存到磁盘上
提交事务时,执行器把binlog cache 里完整的事务写到binlog中,并清空cache
每个线程有自己的binlog cache,但是共用一份binlog文件
sync_binlog 机制
write:指的是把日志写入到文件系统的 page cache,并没有把数据持久化到磁盘,所以速度比较快
fsync:将数据持久化到磁盘的操作,一般情况下fsync站磁盘的IOPS
sync_binlog 参数控制持久化机制
sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync;
sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;
sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。
在出现IO瓶颈的场景里,可能sync_binlog设置有点问题。一般常见设置为 100~1000的某个数值,但是sync_binlog 设置为 N,对应的风险是:如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志。
redo log
使用了WAL技术 Write-Ahead Logging,先写日志,再写磁盘
当有一条更新语句时,InnoDB引擎会把记录写到redo log里,并更新内存。
InnoDB引擎会在何时的时候将操作记录更新到磁盘中,更新一般在系统比较空闲的时候做
redo log 是固定大小的,从头写到尾,又从头开始写。write pos是当前记录的位置,一边写一边后移,checkpoint 是当前要擦除的位置。如果write pos 赶上checkpoint时,不能再更新需要将checkpoint推进一下。
有了redo log,InnoDB可以保证数据库发生异常重启的时候,之前提交的记录不会丢失,称为crash-safe
将innodb_flush_log_at_trx_commit参数设置成1,每次事务的redo log会直接持久化到磁盘,保证MySQL异常重启后数据不会丢失
redo log 一般设置多大
redo log太小会导致很快被写满,不得不强行刷redo log,WAL机制能力发挥不出来
目前常用将redo log设置为 4 个文件,每个文件1GB
写入机制
redo log 的写入不是直接落到磁盘,而是在内存中设置了一片称之为redo log buffer的连续内存空间,也就是redo 日志缓冲区。
持久化机制
InnoDB有一个后台线程,每隔1秒,会将redo log buffer中的日志,调用write写到page cache,然后调用fsync持久化到磁盘
正常关闭服务器
触发checkpoint规则
redo log buffer 占用的空间即将到达innodb_log_buffer_size一半的时候,后台线程会主动写盘
并行事务提交的时候,顺带将这个事务的redo log buffer 持久化到磁盘
checkpoint 规则
重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),块的大小是固定的 512 字节。我们的 redo log 它是固定大小的,可以看作是一个逻辑上的 log group,由一定数量的log block 组成。
它的写入方式是从头到尾开始写,写到末尾又回到开头循环写。
其中有两个标记位置:
write pos是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到磁盘。
其中有两个标记位置:
write pos是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到磁盘。
当write_pos追上checkpoint时,表示 redo log 日志已经写满。这时候就不能接着往里写数据了,需要执行checkpoint规则腾出可写空间。
所谓的checkpoint 规则,就是 checkpoint 触发后,将 buffer 中日志页都刷到磁盘。
所谓的checkpoint 规则,就是 checkpoint 触发后,将 buffer 中日志页都刷到磁盘。
innodb_flush_log_at_trx_commit参数
0:表示每次提交事务时,都只是把redo log留在redo log buffer中
1:表示每次提交事务时,都将redo log 直接持久化到磁盘
2:表示每次事务提交时,都只是把redo log写到 page cache
组提交
持久化组提交
binlog_group_commit_sync_delay:表示延迟多少微秒后才调用fsync
binlog_group_commit_sync_no_delay_count:表示累积多少以后才调用fsync
三个状态
存在redo log buffer 中,物理上是在MySQL进程内存中
写到磁盘(write),但是没有持久化(fsync),物理上是在文件系统的page cache里面
持久化到磁盘,对应hard disk
WAL机制主要得益于
redo log 和binlog 都是顺序写,磁盘顺序写比随机写速度要块
组提交机制,可以大幅降低磁盘IOPS消耗
redo log 和 bin log 的不同点
redo log 是 InnoDB特有的日志模块,binlog是MySQL的Server层实现的,所有引擎都可以使用
redo log 是物理日志,记录某个数据页做了什么修改;binlog是逻辑日志,记录语句的原始逻辑
redo log 是循环写的,空间固定会用完;binlog是可追加写的,binlog文件写到一定大小后会切到下一个,不会覆盖以前日志
是否可以改成只用binlog
目前binlog是不支持的崩溃恢复的
InnoDB引擎使用WAL技术,执行事务时,写完内存和日志,事务就算完成,之后崩溃需要依赖日志恢复数据页,如果发生崩溃,可能导致数据页丢失
只使用redo log,不用binlog
这样没有两阶段提交了,但是系统依然是crash-safe,不过redo log和binlog的作用不同
redo log 是循环写,写到末尾要从开头继续写,日志没法保存,起不到归档作用
binlog 作为MySQL的归档日志,被用到的地方很多,MySQL系统的高可用技术,就是通过binlog复制的
刷脏页
当内存页和数据页内容不一致时,称内存页为"脏页",内存数据写道磁盘后,内存和磁盘的数据页内容变为一致。
一般情况下,更新操作时在写内存和写日志
触发刷脏页flush发的场景
redo log写满了,系统停止所有更新操作,将checkpoint向前推进,使redo log留出空间继续写,推进的这部分内容的脏页都需要刷入磁盘中
系统内存空间不足,当需要新的内存页,内存不够的时候,需要淘汰一些数据页。需要线将脏页写入磁盘
MySQL认为系统空闲的时候,进行flush
MySQL 正常关闭的时候
InnoDB缓冲池管理内存
InnoDB策略尽量使用内存,对一个长时间运行的库来说,未使用的页面很少
读入数据页没有在内存中时,需要在缓冲池中申请一个数据页,将最久不使用的数据页从内存中淘汰。
如果淘汰的是干净页,直接释放复用
如果是脏页,线刷到磁盘,变成干净页后再复用
影响性能的场景
一个查询淘汰的脏页数据太多,相应时间变长
日志写满,更新堵住,写性能跌为0,这种场景对于敏感业务不能接受
InnoDB刷脏页控制策略
通过设置innodb_io_capacity该参数可以使InnoDB引擎了解主机的IO能力
innodb_max_dirty_pages_pct 脏页比例上限 默认75%
谢谢他 完了 拖堂
脏页刷新速率计算方式
脏页刷新"连坐"机制:如果在准备刷新一个脏页的同时,旁边也是一脏页,会将这个"邻居"一起刷掉,同时该连带还会继续蔓延
InnoDB中 innodb_flush_neighbors参数用来控制这个行为,值为1的时候会有连坐机制,值为0时表示我刷我自己
在机械硬盘时代,找"邻居"的优化可以减少很多随机IO。SSD的话建议将innodb_flush_neighbors设置成0,此时IOPS一般不是瓶颈,可以减少SQL语句相应时间,MySQL8.0默认是0
数据写入后最终的落盘,从redo log 更新还是buffer pool更新
redo log 并没有记录数据页的完整数据,所以没有能力更新磁盘数据页,不存在 数据最终落盘由redo log更新
正常运行的什离,数据被修改后,磁盘的数据页不一致,称为脏页,最终落盘是将内存中的数据写到磁盘中,和redo log没有关系
崩溃恢复场景中,InnoDB判断数据页可能再崩溃恢复的时候丢失更新,就会将其读到内存,让redo log更新内存内容,更新之后,内存变成脏页,因此之后又回到了刷脏页的场景
先修改内存还是先写redo log 文件
redo log buffer是一块内存,先用来存redo 日志的,在执行insert的时候,数据内存被修改了,redo log buffer也写入了日志
真正将日志写道到redo log文件时,是执行commit语句的时候做的
MySQL事务隔离级别
简介
事务保证一组数据库操作,要么全部成功,要么全部失败。
事务支持是在引擎层实现的。
MyISAM引擎不支持事务,InnoDB支持
隔离性与隔离级别
事务的 ACID(Atomicity、Consistency、Isolation、Durability,原子性、一致性、隔离性、持久性)
数据库多个事务同时执行时,可能会出现 脏读、不可重复读、幻读 的情况,为了解决这些问题,出现了隔离级别
什么是幻读,脏读,不可重复读呢?
事务 A、B 交替执行,事务 A 读取到事务 B 未提交的数据,这就是脏读。
在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。
事务 A 查询一个范围的结果集,另一个并发事务 B 往这个范围中插入 / 删除了数据,并静悄悄地提交,然后事务 A 再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读。
不同的隔离级别,在并发事务下可能会发生的问题:
在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。
事务 A 查询一个范围的结果集,另一个并发事务 B 往这个范围中插入 / 删除了数据,并静悄悄地提交,然后事务 A 再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读。
不同的隔离级别,在并发事务下可能会发生的问题:
SQL标准的事务隔离级别包括:读未提交、读已提交、可重复读和串行化
读未提交
一个事务还没提交时,它做的变更能被别的事务看到。
采取的是读不加锁原理。
事务读不加锁,不阻塞其他事务的读和写
事务写阻塞其他事务写,但不阻塞其他事务读;
事务读不加锁,不阻塞其他事务的读和写
事务写阻塞其他事务写,但不阻塞其他事务读;
读已提交
一个事务提交之后,它做的变更才会被其他事务看到
每次读取数据前都生成一个 ReadView
可重复读
一个事务执行过程中看到的数据总是跟这个事务在启动时看到的数据是一致的。在可重复读隔离级别下,未提交的变更对其他事务是不可见的。
在第一次读取数据时生成一个 ReadView
串行化
写的时候会加写锁,读的时候会加读锁。当出现读写冲突时,后访问的事务必须等前一个事务执行完成,才可继续执行
串行化的实现采用的是读写都加锁的原理。
串行化的情况下,对于同一行事务,写会加写锁,读会加读锁。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
串行化的情况下,对于同一行事务,写会加写锁,读会加读锁。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
那ACID靠什么保证呢?
01、原子性(Atomicity),如果事务的所有操作都成功执行,则事务被提交;如果事务中的任何操作失败,所有事务中的操作都会被回滚,使数据库返回到事务开始前的状态。
undo log 是 InnoDB 存储引擎来确保事务原子性的关键机制,undo log 记录了事务发生之前的数据,如果事务失败,InnoDB 会根据 undo log 回滚数据。
当事务开始修改数据时,InnoDB 首先会在undo log中记录旧值(即修改前的值)。
如果事务顺利进行并最终提交,undo log会在某个时间点被清除。
如果事务中的某个操作失败或者事务被明确地回滚,InnoDB 会使用undo log中的信息来撤销所有更改,确保数据的原子性。
简而言之,undo log机制为 InnoDB 提供了一种在事务失败或被中断时恢复数据的手段,从而保证了事务的原子性。 buffer pool
undo log 是 InnoDB 存储引擎来确保事务原子性的关键机制,undo log 记录了事务发生之前的数据,如果事务失败,InnoDB 会根据 undo log 回滚数据。
当事务开始修改数据时,InnoDB 首先会在undo log中记录旧值(即修改前的值)。
如果事务顺利进行并最终提交,undo log会在某个时间点被清除。
如果事务中的某个操作失败或者事务被明确地回滚,InnoDB 会使用undo log中的信息来撤销所有更改,确保数据的原子性。
简而言之,undo log机制为 InnoDB 提供了一种在事务失败或被中断时恢复数据的手段,从而保证了事务的原子性。 buffer pool
02、一致性(Consistency),保证在事务开始之前和事务成功完成之后,数据库处于一个一致的状态。中间的任何阶段,即使事务失败,也不应该使数据库处于不一致的状态。
一致性是 ACID 的目的,也就是说,只要保证原子性、隔离性、持久性,自然也就保证了数据的一致性。
一致性是 ACID 的目的,也就是说,只要保证原子性、隔离性、持久性,自然也就保证了数据的一致性。
03、隔离性 (Isolation),MySQL 使用多种隔离级别来控制事务如何与其他并发事务隔离。InnoDB 存储引擎使用 MVCC (多版本并发控制) 机制来处理并发事务,确保每个事务都有自己的数据版本。
换句话说,事务查看数据时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
在 MVCC 中,每次更新记录时,都会生成记录的一个新版本,而不是覆盖老版本。每个版本都会有两个额外的属性:一个表示版本的创建时间(或事务ID),另一个表示版本的过期时间(或下一个版本的事务ID)。
当事务尝试读取记录时,它会看到该事务开始时有效的那个版本。
MVCC 通过提供数据版本来支持事务的隔离性。不同的事务会看到不同版本的数据行,这取决于事务的开始时间和它的隔离级别。
对于如 "读未提交"(READ UNCOMMITTED)这样的较低隔离级别,事务可能会看到其他未提交事务所做的更改。但在更高的隔离级别,如 "可重复读"(REPEATABLE READ)或 "串行化"(SERIALIZABLE),事务不会看到其他事务所做的更改,直到它们被提交。
换句话说,事务查看数据时,数据所处的状态要么是另一事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
在 MVCC 中,每次更新记录时,都会生成记录的一个新版本,而不是覆盖老版本。每个版本都会有两个额外的属性:一个表示版本的创建时间(或事务ID),另一个表示版本的过期时间(或下一个版本的事务ID)。
当事务尝试读取记录时,它会看到该事务开始时有效的那个版本。
MVCC 通过提供数据版本来支持事务的隔离性。不同的事务会看到不同版本的数据行,这取决于事务的开始时间和它的隔离级别。
对于如 "读未提交"(READ UNCOMMITTED)这样的较低隔离级别,事务可能会看到其他未提交事务所做的更改。但在更高的隔离级别,如 "可重复读"(REPEATABLE READ)或 "串行化"(SERIALIZABLE),事务不会看到其他事务所做的更改,直到它们被提交。
04、持久性 (Durability),由 MySQL 的存储引擎(如InnoDB)通过写入磁盘来确保。即使在系统崩溃之后,已提交事务的更改也不会丢失。
InnoDB 使用“redo log”来记录数据的更改,在系统崩溃后,redo log 可用于恢复数据。
redo log 是一种物理日志,记录了对数据页的物理更改。当事务进行写操作时,InnoDB 首先会写入 redo log,并不会立即修改数据文件。这种写入方式被称为“write-ahead logging”(先写日志)。
当 redo log 填满或在某些其他情况下,InnoDB 会异步将这些更改刷新到数据文件中。
系统崩溃时,由于数据可能还没有被真正写入数据文件,但已经在 redo log 中,因此系统可以在启动时使用这些日志来重新执行或“重做”这些更改,确保数据的持久性。
即使数据库在事务提交后立即崩溃,由于事务的更改已经记录在 redo log 中,这些更改在数据库恢复时仍然是安全的。
InnoDB 使用“redo log”来记录数据的更改,在系统崩溃后,redo log 可用于恢复数据。
redo log 是一种物理日志,记录了对数据页的物理更改。当事务进行写操作时,InnoDB 首先会写入 redo log,并不会立即修改数据文件。这种写入方式被称为“write-ahead logging”(先写日志)。
当 redo log 填满或在某些其他情况下,InnoDB 会异步将这些更改刷新到数据文件中。
系统崩溃时,由于数据可能还没有被真正写入数据文件,但已经在 redo log 中,因此系统可以在启动时使用这些日志来重新执行或“重做”这些更改,确保数据的持久性。
即使数据库在事务提交后立即崩溃,由于事务的更改已经记录在 redo log 中,这些更改在数据库恢复时仍然是安全的。
事务隔离的实现
同一条记录在数据库中可以存在多个版本,这就是数据库的多版本并发控制
对于read-view A,要得到 1,就需要将当前值一次执行途中所有的回滚得到。即使有另外一个事务将4改成5,其他事务也是不会改变的。
为什么尽量不要使用长事务?
长事务意味着系统里面会存在很老的事务视图,保证这些事务随时可能访问数据库里面的任何数据,所以在这个事务提交之前用到的回滚事务记录都必须保留,这样会导致大量占用存储空间。在MySQL5.5版本以前,回滚日志是跟数据字段放在ibdata文件里,长事务提交,回滚段被清理,文件也不会变小。
长事务占用锁资源,可能会拖垮整个库
长事务意味着系统里面会存在很老的事务视图,保证这些事务随时可能访问数据库里面的任何数据,所以在这个事务提交之前用到的回滚事务记录都必须保留,这样会导致大量占用存储空间。在MySQL5.5版本以前,回滚日志是跟数据字段放在ibdata文件里,长事务提交,回滚段被清理,文件也不会变小。
长事务占用锁资源,可能会拖垮整个库
事务的启动方式
显示启动事务语句
begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
自动提交
set autocommit = 0,会将这个线程的自动提交关掉,这样只执行一个select语句事务就启动了,持续到执行commit或者rollback语句
建议使用set autocommit = 1,通过显示语句启动长事务
事务的启动时机
begin/start transaction 命令不是一个事务的起点,执行他们之后第一个操作InnoDB表的语句才是真正事务的启动。
如果要马上启动一个事务,使用 start transaction with consistent snapshot 命令
一致性视图也就是在事务真正启动的时候进行创建的。
视图 / MVCC
视图 view。 用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建语法 create view ... 查询方法和表一样。
InnoDB实现MVcC时用到的一致性视图,用于RC(Read Committed)、RR(Repeatable Read) 隔离级别的实现
如何实现MVcC?
在可重复读隔离级别下 ,事务启动时就“拍了个快照”,基于整个库的。InnoDB咩每个事务都有一个唯一的事务ID,transaction id。在事务开始时想系统申请,严格递增。每行数据也是有多个版本的。每次事务更新数据时,都会生成新的数据版本,并将transaction id赋值给数据版本的事务ID,记为row trx_id.旧的数据版本需要保留,在新的数据版本中,保证能够有信息可以拿到它。数据表的一行数据可能有多个版本,每个版本都有自己的row trx_id
在可重复读隔离级别下 ,事务启动时就“拍了个快照”,基于整个库的。InnoDB咩每个事务都有一个唯一的事务ID,transaction id。在事务开始时想系统申请,严格递增。每行数据也是有多个版本的。每次事务更新数据时,都会生成新的数据版本,并将transaction id赋值给数据版本的事务ID,记为row trx_id.旧的数据版本需要保留,在新的数据版本中,保证能够有信息可以拿到它。数据表的一行数据可能有多个版本,每个版本都有自己的row trx_id
语句更新会产生undo log,找之前的版本时根据数据库事务的id以及undo log进行计算,从而找到数据正确的版本。
行状态变更
数据版本可见性规则
对于一个数据版本row trx_id
落在绿色部分,表示可见
落在红色部分,表示不可见
落在黄色部分,如果在数组中,表示还未提交,不可见
落在黄色部分,如果不在数组中,表示已提交,可见
InnoDB为每个事务构造了一个数组,用来保存事务启动的瞬间,当前正在“活跃”的所有事务ID。“活跃”指启动了还没有提交
为什么不直接保存已提交的最大事务id呢?
有可能小的事务id还在进行中,未提交,这部分数据也是不可见的。
有可能小的事务id还在进行中,未提交,这部分数据也是不可见的。
视图数组和高水位组成了当前事务的一致性视图(read-view),数据版本的可见性规则,基于数据的row trx_id和这个一致性视图的比对结果得到
InnoDB利用“所有数据都有多个版本”的特性,实现了“秒级创建快照”的能力。
一个数据版本,对于一个事务视图来讲,自己的更新总是可见的之外,其他事务的可见性
版本未提交,不可见
版本已提交,在视图创建后提交的,不可见
版本已提交,在视图创建前提交的,可见
可重复读/读提交 的实现
当前读
更新数据采用先读后写,这个读是“当前读”(current read),只能读当前值。
除了更新语句之外,select 语句如果加锁后,也是采用当前读
可重复读的核心是一致性读(consistent read);事务更新数据的时候,只能用当前读,如果当前记录的行锁被其他事务占用,需要进入锁等待。
读提交:每个语句执行前都会计算出一个新的视图。可以读到已提交的事务,但是读不到未提交的事务
MySQL的锁
数据库锁设计的初衷是处理并发问题。作为用户共享资源,当出现并发访问时,数据库需要合理控制资源的访问规则。锁是用来实现这些访问规则的重要数据结构
锁的分类/锁的粒度
表级锁
表锁
语法 lock tables ... read/write 使用 ublock tables 主动释放,或者客户端断开释放。
元数据锁(MDL metadata lock)
MySQL5.5版本引入。不需要显示使用,访问一个表的时候自动加上,保证读写完正确性。
当对一个表做增删改查操作时,加MDL读锁;对表结构更改时,加MDL写锁
读锁相互不互斥;读写/写之间是互斥的。保证更改表结构操作的安全性。
给表加字段,修改索引时需要全表扫描,所以需要对表的操作需要小心,防止写锁,阻塞后续的操作导致库的线程爆满影响后续热点数据读锁获取
安全给小表加字段:对热点表,在alter table语句设定等待时间,如果指定时间内可以拿到MDL写锁最好,拿不到也不要阻塞后续业务语句。后续通过重试重复该过程。
特点: 开销小,加锁快;锁定力度大,发生锁冲突频率高,并发度最低,不会出现死锁
行级锁
行锁是在引擎层实现的,并不是所有的引擎支持行级锁。MyISAM引擎不支持行锁,并发控制只能使用表锁,同一张表任何时刻只能有一个更新在执行,影响业务并发度
事务A需要更新一行,事务B也需要更新同一行,需要等待A操作完成后才能进行B操作
在InnoDB中,行锁在需要的时候才加上,不需要的时候需要等待事务结束才释放,这是两阶段协议。如果事务中需要锁多个行,要把最可能造成锁冲突的、最何能影响并发的锁尽量放后面
如果发生死锁,一帮有两种策略:直接进入等待超时,超时时间通过参数 innodb_lock_wait_timeout设置;发起死锁检测,发现死锁后主动回滚死锁链条中某一事务,使得其他事务得以继续。将参数innodb_deadlock_detect设置为on,表示开启该逻辑
场景:主动监测死锁需要外耗时。如果有1000个并发县城同时更新一行,死锁检测会很大,CPU利用率很高,但是执行不了几个事务。如何解决热点行更新导致性能问题?
1. 确保这个业务不会出现死锁,临时把死锁检测关掉(不建议,关掉死锁检测意味着可能会出现大量超时,业务有损)
2. 控制并发,在数据库服务端进行控制,中间件,修改MySQL源码,在相同行更新时,进入引擎前进行排队。将热点行变成多行考虑(需要详细的业务逻辑考虑)
1. 确保这个业务不会出现死锁,临时把死锁检测关掉(不建议,关掉死锁检测意味着可能会出现大量超时,业务有损)
2. 控制并发,在数据库服务端进行控制,中间件,修改MySQL源码,在相同行更新时,进入引擎前进行排队。将热点行变成多行考虑(需要详细的业务逻辑考虑)
InnoDB行级锁通过锁索引记录实现的,如果update的列没有建索引,更新时会走主键索引,逐行扫描满足条件的行,相当于主键索引所有的行加上了锁。考虑到如果会大量并发更改,需要让where走索引
特点:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
全局锁
MySQL提供了一个加全局读锁的方法,Flush tables with read lock(FTWRL)。当需要让整个库处于只读状态的时候,使用该命令。更新、定义语句会被阻塞
全局锁使用场景,全库的逻辑备份。如果不加锁的话,备份系统得到的库不是一个逻辑时间,视图是逻辑不一致的。如何得到一致性视图呢,需要数据库引擎支持
官方自带逻辑备份工具mysqldump,当使用single-transaction的时候,导数据之前会启动一个事务,确保拿到一致性视图MVCC支持,此时这个过程中,数据可以正常更新。对于MyISAM不支持事务的引擎,备份中有更新,总是只能拿到最新的数据,那么破坏了备份的一致性,所以需要使用FTWRL命令
single-transaction方法只适用于所有表使用事务引擎的库,如果有表使用了不支持事务的引擎,只能通过FTWRL的方式
为什么不使用 set global readonly = true 的方式
有些系统readonly的值会被用来做其他逻辑(判断是主库还是备库)。修改方式影响面更大,不建议使用
异常处理有差异。FTWRL命令后客户端发生异常断开,MySQL会自动释放全局锁,使库回到正常更新状态。设置readonly后,连接发生异常,数据库还是保持readyonly状态,导致整个库长时间处于不可写状态,风险高
页锁:开销和加锁速度介于表锁和行锁之间;会出现死锁,锁定力度介于表锁和行锁之间,并发度一般
锁模式
记录锁
Record Lock 记录锁
记录锁就是直接锁定某行记录。当我们使用唯一性的索引(包括唯一索引和聚簇索引)进行等值查询且精准匹配到一条记录时,此时就会直接将这条记录锁定。例如select * from t where id =6 for update;就会将id=6的记录锁定。
记录锁就是直接锁定某行记录。当我们使用唯一性的索引(包括唯一索引和聚簇索引)进行等值查询且精准匹配到一条记录时,此时就会直接将这条记录锁定。例如select * from t where id =6 for update;就会将id=6的记录锁定。
间隙锁
Gap Lock 间隙锁
间隙锁(Gap Locks) 的间隙指的是两个记录之间逻辑上尚未填入数据的部分,是一个左开右开空间。
间隙锁就是锁定某些间隙区间的。当我们使用用等值查询或者范围查询,并且没有命中任何一个record,此时就会将对应的间隙区间锁定。例如select * from t where id =3 for update;或者select * from t where id > 1 and id < 6 for update;就会将(1,6)区间锁定。
间隙锁(Gap Locks) 的间隙指的是两个记录之间逻辑上尚未填入数据的部分,是一个左开右开空间。
间隙锁就是锁定某些间隙区间的。当我们使用用等值查询或者范围查询,并且没有命中任何一个record,此时就会将对应的间隙区间锁定。例如select * from t where id =3 for update;或者select * from t where id > 1 and id < 6 for update;就会将(1,6)区间锁定。
next-key锁
Next-key Lock 临键锁
临键指的是间隙加上它右边的记录组成的左开右闭区间。比如上述的(1,6]、(6,8]等。
临键锁就是记录锁(Record Locks)和间隙锁(Gap Locks)的结合,即除了锁住记录本身,还要再锁住索引之间的间隙。当我们使用范围查询,并且命中了部分record记录,此时锁住的就是临键区间。注意,临键锁锁住的区间会包含最后一个 record 的右边的临键区间。例如select * from t where id > 5 and id <= 7 for update;会锁住(4,7]、(7,+∞)。mysql 默认行锁类型就是临键锁(Next-Key Locks)。当使用唯一性索引,等值查询匹配到一条记录的时候,临键锁(Next-Key Locks)会退化成记录锁;没有匹配到任何记录的时候,退化成间隙锁。
间隙锁(Gap Locks)和临键锁(Next-Key Locks)都是用来解决幻读问题的,在已提交读(READ COMMITTED)隔离级别下,间隙锁(Gap Locks)和临键锁(Next-Key Locks)都会失效!
上面是行锁的三种实现算法,除此之外,在行上还存在插入意向锁。
临键指的是间隙加上它右边的记录组成的左开右闭区间。比如上述的(1,6]、(6,8]等。
临键锁就是记录锁(Record Locks)和间隙锁(Gap Locks)的结合,即除了锁住记录本身,还要再锁住索引之间的间隙。当我们使用范围查询,并且命中了部分record记录,此时锁住的就是临键区间。注意,临键锁锁住的区间会包含最后一个 record 的右边的临键区间。例如select * from t where id > 5 and id <= 7 for update;会锁住(4,7]、(7,+∞)。mysql 默认行锁类型就是临键锁(Next-Key Locks)。当使用唯一性索引,等值查询匹配到一条记录的时候,临键锁(Next-Key Locks)会退化成记录锁;没有匹配到任何记录的时候,退化成间隙锁。
间隙锁(Gap Locks)和临键锁(Next-Key Locks)都是用来解决幻读问题的,在已提交读(READ COMMITTED)隔离级别下,间隙锁(Gap Locks)和临键锁(Next-Key Locks)都会失效!
上面是行锁的三种实现算法,除此之外,在行上还存在插入意向锁。
意向锁
意向锁是一个表级锁,不要和插入意向锁搞混。
意向锁的出现是为了支持 InnoDB 的多粒度锁,它解决的是表锁和行锁共存的问题。
当我们需要给一个表加表锁的时候,我们需要根据去判断表中有没有数据行被锁定,以确定是否能加成功。
假如没有意向锁,那么我们就得遍历表中所有数据行来判断有没有行锁;
有了意向锁这个表级锁之后,则我们直接判断一次就知道表中是否有数据行被锁定了。
有了意向锁之后,要执行的事务 A 在申请行锁(写锁)之前,数据库会自动先给事务 A 申请表的意向排他锁。当事务 B 去申请表的互斥锁时就会失败,因为表上有意向排他锁之后事务 B 申请表的互斥锁时会被阻塞。
意向锁的出现是为了支持 InnoDB 的多粒度锁,它解决的是表锁和行锁共存的问题。
当我们需要给一个表加表锁的时候,我们需要根据去判断表中有没有数据行被锁定,以确定是否能加成功。
假如没有意向锁,那么我们就得遍历表中所有数据行来判断有没有行锁;
有了意向锁这个表级锁之后,则我们直接判断一次就知道表中是否有数据行被锁定了。
有了意向锁之后,要执行的事务 A 在申请行锁(写锁)之前,数据库会自动先给事务 A 申请表的意向排他锁。当事务 B 去申请表的互斥锁时就会失败,因为表上有意向排他锁之后事务 B 申请表的互斥锁时会被阻塞。
插入意向锁
Insert Intention Lock 插入意向锁
一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了意向锁 ,如果有的话,插入操作需要等待,直到拥有 gap 锁 的那个事务提交。但是事务在等待的时候也需要在内存中生成一个 锁结构 ,表明有事务想在某个 间隙 中插入新记录,但是现在在等待。这种类型的锁命名为 Insert Intention Locks ,也就是插入意向锁 。
假如我们有个 T1 事务,给(1,6)区间加上了意向锁,现在有个 T2 事务,要插入一个数据,id 为 4,它会获取一个(1,6)区间的插入意向锁,又有有个 T3 事务,想要插入一个数据,id 为 3,它也会获取一个(1,6)区间的插入意向锁,但是,这两个插入意向锁锁不会互斥。
一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了意向锁 ,如果有的话,插入操作需要等待,直到拥有 gap 锁 的那个事务提交。但是事务在等待的时候也需要在内存中生成一个 锁结构 ,表明有事务想在某个 间隙 中插入新记录,但是现在在等待。这种类型的锁命名为 Insert Intention Locks ,也就是插入意向锁 。
假如我们有个 T1 事务,给(1,6)区间加上了意向锁,现在有个 T2 事务,要插入一个数据,id 为 4,它会获取一个(1,6)区间的插入意向锁,又有有个 T3 事务,想要插入一个数据,id 为 3,它也会获取一个(1,6)区间的插入意向锁,但是,这两个插入意向锁锁不会互斥。
加锁机制
乐观锁
乐观锁认为数据的变动不会太频繁。
乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,版本最为常用。
事务在从数据库中取数据时,会将该数据的版本也取出来(v1),当事务对数据变动完毕想要将其更新到表中时,会将之前取出的版本 v1 与数据中最新的版本 v2 相对比,如果 v1=v2,那么说明在数据变动期间,没有其他事务对数据进行修改,此时,就允许事务对表中的数据进行修改,并且修改时 version 会加 1,以此来表明数据已被变动。
如果,v1 不等于 v2,那么说明数据变动期间,数据被其他事务改动了,此时不允许数据更新到表中,一般的处理办法是通知用户让其重新操作。不同于悲观锁,乐观锁通常是由开发者实现的。
乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,版本最为常用。
事务在从数据库中取数据时,会将该数据的版本也取出来(v1),当事务对数据变动完毕想要将其更新到表中时,会将之前取出的版本 v1 与数据中最新的版本 v2 相对比,如果 v1=v2,那么说明在数据变动期间,没有其他事务对数据进行修改,此时,就允许事务对表中的数据进行修改,并且修改时 version 会加 1,以此来表明数据已被变动。
如果,v1 不等于 v2,那么说明数据变动期间,数据被其他事务改动了,此时不允许数据更新到表中,一般的处理办法是通知用户让其重新操作。不同于悲观锁,乐观锁通常是由开发者实现的。
悲观锁
悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能被改动,一个事务拿到悲观锁后,其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以执行。
数据库中的行锁,表锁,读锁,写锁均为悲观锁。
数据库中的行锁,表锁,读锁,写锁均为悲观锁。
兼容性
共享锁
也叫读锁(read lock),相互不阻塞。
排他锁
也叫写锁(write lock),排它锁是阻塞的,在一定时间内,只有一个请求能执行写入,并阻止其它锁读取正在写入的数据。
遇到过死锁吗?如何解决?
(1)查看死锁日志 show engine innodb status;
(2)找出死锁 sql
(3)分析 sql 加锁情况
(4)模拟死锁案发
(5)分析死锁日志
(6)分析死锁结果
(2)找出死锁 sql
(3)分析 sql 加锁情况
(4)模拟死锁案发
(5)分析死锁日志
(6)分析死锁结果
MySQL索引
简介
索引的出现是为了提高数据库的查询效率,就像树的目录一样
索引分类
基本使用
主键索引
InnoDB 主键是默认的索引,数据列不允许重复,不允许为 NULL,一个表只能有一个主键。
唯一索引
数据列不允许重复,允许为 NULL 值,一个表允许多个列创建唯一索引。
普通索引
基本的索引类型,没有唯一性的限制,允许为 NULL 值。
组合索引
多列值组成一个索引,用于组合搜索,效率大于索引合并
数据结构
哈希索引
B树索引
B+树索引
物理存储
聚簇索引
非聚簇索引
常见的索引模型
哈希表
以键值存储数据的结构
哈希索引做区间查询时速度会比较慢
适用场景:等值查询时,比如Memcached以及其他一些NoSQL引擎
有序数组
等值查询和范围查询的场景中性能也非常优秀
更新/插入数据时,需要将后面的所有记录进行移动,成本太高
适用于静态存储引擎
搜索树
父节点左子树所有节点的值小于父节点的值,右子树所有节点的值大于父节点的值
二叉搜索树查询效率O(log(N)),为了维持查询复杂度,需要保持这颗树时平衡二叉树,所以更新的时间复杂度为O(log(N))
二叉搜索树时搜索树效率最高的,但是实际上大多树数据库存储并不使用二叉树,索引不知存在内存中,还需要写到磁盘上。一颗树高20的二叉树,一次查询可能需要访问20个数据块,如果使用二叉树进行存储,查询会很慢
InnoDB的整数字段索引,N叉树的N大概为1200,当高为4时,可以存储17亿数据,树根节点一般在内存中,所以一个10亿行的表上一个整数字段索引,查询一个值最多需要访问3次。树的第二层大概也在内存中,耗时可能会更少。
N叉树由于现在读写上的性能优点,以及适配磁盘的访问模式,被广泛应用在数据库引擎中。数据库底层存储的核心是基于这些数据模型的。每当我们碰到一个数据库需要先关注它的数据模型,从理论上分析出数据库的适用场景。
InnoDB索引模型 —— 【B+树】
InnoDB使用B+树索引模型,数据存储在B+树中,每一个索引都在InnoDB中对应一颗B+树
根据索引的叶子结点内容,索引分为主键索引和非主键索引,主键索引也称为聚簇索引,非主键索引被称为二级索引。非主键索引在查询时,需要多扫描主键索引,所以尽量使用主键索引进行查询
为了维护索引的有序性,插入新值时,需要做必要的维护。数据页满了需要申请一个新数据页,挪动部分数据过去,该步骤称为叶分裂,性能会受一定的影响,空间的利用率也会有所降低。在空间利用率很低时,会做数据页的合并。
叶子结点会有一个双向链表进行维护,全表扫描的时候其实就是对叶子结点按照双向链表进行迭代扫描
自增主键,每次插入一条新纪录都是追加操作,不涉及挪动其他记录,不会触发叶子结点的分裂。业务字段做主键,不容易保证有序插入,写数据成本相对比较高
使用身份证做主键索引?
那么每个二级索引的叶子结点占用约20字节,如果用整型做主键,则只需要4个字节。主键长度越小,普通索引占用的空间也越小,从性能和存储空间方面考虑,自增主键往往是更合理的选择。
什么场景适合用业务字段做主键?
只有一个索引,该索引必须是唯一索引。因为没有其他索引所以不用考虑其他索引的叶子节点大小问题(典型的KV场景)
那么每个二级索引的叶子结点占用约20字节,如果用整型做主键,则只需要4个字节。主键长度越小,普通索引占用的空间也越小,从性能和存储空间方面考虑,自增主键往往是更合理的选择。
什么场景适合用业务字段做主键?
只有一个索引,该索引必须是唯一索引。因为没有其他索引所以不用考虑其他索引的叶子节点大小问题(典型的KV场景)
在 InnoDB 中 B+ 树深度一般为 1-3 层,它就能满足千万级的数据存储。
为什么用B+树,而不是用普通二叉树
为什么不用普通二叉树
普通二叉树存在退化的情况,如果它退化成链表,相当于全表扫描。平衡二叉树相比于二叉查找树来说,查找效率更稳定,总体的查找速度也更快。
为什么不用平衡二叉树
读取数据的时候,是从磁盘读到内存。如果树这种数据结构作为索引,那每查找一次数据就需要从磁盘中读取一个节点,也就是一个磁盘块,但是平衡二叉树可是每个节点只存储一个键值和数据的,如果是 B+ 树,可以存储更多的节点数据,树的高度也会降低,因此读取磁盘的次数就降下来啦,查询效率就快。
为什么用B+树而不用B树?
B+相比较 B 树,有这些优势:
· 它是 B Tree 的变种,B Tree 能解决的问题,它都能解决。
B Tree 解决的两大问题:每个节点存储更多关键字;路数更多
· 扫库、扫表能力更强
如果我们要对表进行全表扫描,只需要遍历叶子节点就可以 了,不需要遍历整棵 B+Tree 拿到所有的数据。
· B+Tree 的磁盘读写能力相对于 B Tree 来说更强,IO 次数更少
根节点和枝节点不保存数据区, 所以一个节点可以保存更多的关键字,一次磁盘加载的关键字更多,IO 次数更少。
· 排序能力更强
因为叶子节点上有下一个数据区的指针,数据形成了链表。
· 效率更加稳定
B+Tree 永远是在叶子节点拿到数据,所以 IO 次数是稳定的。
· 它是 B Tree 的变种,B Tree 能解决的问题,它都能解决。
B Tree 解决的两大问题:每个节点存储更多关键字;路数更多
· 扫库、扫表能力更强
如果我们要对表进行全表扫描,只需要遍历叶子节点就可以 了,不需要遍历整棵 B+Tree 拿到所有的数据。
· B+Tree 的磁盘读写能力相对于 B Tree 来说更强,IO 次数更少
根节点和枝节点不保存数据区, 所以一个节点可以保存更多的关键字,一次磁盘加载的关键字更多,IO 次数更少。
· 排序能力更强
因为叶子节点上有下一个数据区的指针,数据形成了链表。
· 效率更加稳定
B+Tree 永远是在叶子节点拿到数据,所以 IO 次数是稳定的。
B树存的数据量没有B+duo
hash索引和B+树索引区别是什么
B+ 树可以进行范围查询,Hash 索引不能。
B+ 树支持联合索引的最左侧原则,Hash 索引不支持。
B+ 树支持 order by 排序,Hash 索引不支持。
Hash 索引在等值查询上比 B+ 树效率更高。
B+ 树使用 like 进行模糊查询的时候,like 后面(比如 % 开头)的话可以起到优化的作用,Hash 索引根本无法进行模糊查询。
B+ 树支持联合索引的最左侧原则,Hash 索引不支持。
B+ 树支持 order by 排序,Hash 索引不支持。
Hash 索引在等值查询上比 B+ 树效率更高。
B+ 树使用 like 进行模糊查询的时候,like 后面(比如 % 开头)的话可以起到优化的作用,Hash 索引根本无法进行模糊查询。
聚簇索引与非聚簇索引的区别?
索引的数据结构是树,聚簇索引的索引和数据存储在一棵树上,树的叶子节点就是数据,非聚簇索引索引和数据不在一棵树上。
聚簇索引一种数据存储方式。聚簇表示数据行和相邻的键值紧凑地存储在一起。我们熟悉的两种存储引擎——MyISAM 采用的是非聚簇索引,InnoDB 采用的是聚簇索引。
一个表中只能拥有一个聚簇索引,而非聚簇索引一个表可以存在多个。
聚簇索引,索引中键值的逻辑顺序决定了表中相应行的物理顺序;索引,索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同。
聚簇索引:物理存储按照索引排序;非聚集索引:物理存储不按照索引排序;
创建索引有哪些要注意的点
1. 索引应该建在查询应用频繁的字段
在用于 where 判断、 order 排序和 join 的(on)字段上创建索引。
2. 索引的个数应该适量
索引需要占用空间;更新时候需要维护
3. 区分度低的字段,不要建索引
离散度太低的字段,扫描的行数降低的有限。
4. 频繁更新的值,不要作为主键或者索引
维护索引文件需要成本;还会导致页分裂,IO 次数增多。
5. 组合索引把散列性高(区分度高)的值放在前面
为了满足最左前缀匹配原则
6. 创建组合索引,而不是修改单列索引。
组合索引代替多个单列索引(对于单列索引,MySQL 基本只能使用一个索引,所以经常使用多个条件查询时更适合使用组合索引)
7. 过长的字段,使用前缀索引。当字段值比较长的时候,建立索引会消耗很多的空间,搜索起来也会很慢。我们可以通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。
8. 不建议用无序的值(例如身份证、UUID )作为索引
索引不是越多越好。
索引会占据磁盘空间
索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改操作,MySQL 不仅要保存数据,还有保存或者更新对应的索引文件。
索引会占据磁盘空间
索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改操作,MySQL 不仅要保存数据,还有保存或者更新对应的索引文件。
索引优化原则技巧
覆盖索引
如果需要查询的内容在索引里面,那么相当于索引覆盖了查询的需求,称为覆盖索引
覆盖索引可以减少搜索次数,显著提升查询性能,使用覆盖索引是常用的性能优化手段
最左前缀原则
建立联合索引时,考虑安排索引内的字段顺序,减少维护一个索引
联合索引中,索引命中按照最左匹配依次进行索引命中
索引下推
当使用(name, age)联合索引时,查询表中姓“张”且年龄为 10 的所有男孩,最左匹配找到张的第一个能满足条件的记录
MySQL5.6以前,最左匹配找到第一个张之后,就开始一个一个回表,然后找到数据行,再比对age字段值
MySQL5.6之后,引入索引下推,可以在索引遍历的过程中,对索引包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数
满足语句需求下,尽量少地访问资源是数据库设计的重要原则之一,使用数据库,设计表结构时,以减少资源消耗为目标
选择普通索引还是唯一索引
查询过程
如果在业务中已经校验了唯一性,字段选择普通索引还是唯一索引,选择唯一索引和普通索引在查询上的性能差距微乎其微
更新过程
更新一个数据页时,如果数据页在内存中,直接更新,如果数据页没有在内存中,不影响数据一致性的前提下,InnoDB会缓存在change buffer中。下次查询需要访问该数据页时,将数据页读入内存,然后执行change buffer中和这个页相关的操作。保证数据逻辑正确性
change buffer 使用的是buffer pool 的内存,不能无限增大,通过innodb_change_buffer_max_szie 来动态设置,设置为50时,表示最多占用buffer pool的50%
什么情况下可以使用change buffer?
对于唯一索引,所有的更新操作都要先判断操作是否违反唯一性约束,如果内存中有该数据页,直接更新不需要change buffer,如果不存在内存中,会将数据页读到内存再进行判断,也是不需要change buffer。
因此,唯一性索引不能使用change buffer, 只有普通索引可以使用 change buffer
对于唯一索引,所有的更新操作都要先判断操作是否违反唯一性约束,如果内存中有该数据页,直接更新不需要change buffer,如果不存在内存中,会将数据页读到内存再进行判断,也是不需要change buffer。
因此,唯一性索引不能使用change buffer, 只有普通索引可以使用 change buffer
change buffer 使用场景:
一个业务,更新模式是,写入之后马上要访问这个数据页,那么会直接触发merge过程,随机访问IO次数不会减少,反而增加了change buffer的维护代价。如果是账单、日志类型的系统,change buffer的效果最好,change buffer存的数据越多,收益越大
一个业务,更新模式是,写入之后马上要访问这个数据页,那么会直接触发merge过程,随机访问IO次数不会减少,反而增加了change buffer的维护代价。如果是账单、日志类型的系统,change buffer的效果最好,change buffer存的数据越多,收益越大
选择实践
两类索引的选择主要是考虑更新性能的影响,尽量选择 普通索引。
如果更新后面,立马有查询,那么应该关闭change buffer。其他情况下,使用change buffer都可以提升性能。在实际使用中,使用普通索引和 change buffer 配合使用,对于数据量大的表在更新优化上比较明显
如果更新后面,立马有查询,那么应该关闭change buffer。其他情况下,使用change buffer都可以提升性能。在实际使用中,使用普通索引和 change buffer 配合使用,对于数据量大的表在更新优化上比较明显
业务可以接受的情况下,从性能角度考虑,建议考虑非唯一索引
带 change buffer 的更新过程
change buffer 和 redo log区别
一条更新语句,涉及四个部分:内存、redo log、数据表空间、系统表空间
一条更新语句,涉及四个部分:内存、redo log、数据表空间、系统表空间
1. Page 1 在内存中,直接更新内存
2. Page 2 没有在内存中,在内存的change buffer区域,记录“向page 2 插入一行”
3. 将这两个动作记入redo log 中
如果后续要读数据,Page1 在内存中,可以直接读到更新后的数据;page 2 不在内存中,需要将数据读入内存,然后将change buffer 操作后,生成一个正确的版本后返回结果
redo log 主要是节省随机写磁盘的IO消耗(转成顺序写);change buffer 主要是节省随机读的磁盘IO消耗
MySQL什么时候会选错索引
选择索引是优化器的工作,优化器选择索引的目的是找到一个最优的执行方案,并用最小的代价去执行语句,在数据库中,扫描行数是影响执行计划的因素之一,扫描的行数越少,意味访问磁盘的次数越少,IO越少
show index 可以看到一个索引的基数,MySQL通过采样统计的方法,获得一个均值,然后乘以页面数获得索引的基数,当变更的数据超过1/M会触发重新做一次索引统计。
索引统计方式可以通过设置 innodb_stats_persistent的值来选择:
on 持久化存储统计信息,默认N为20,M为10
off内存存储统计信息,默认N为8,M为16
on 持久化存储统计信息,默认N为20,M为10
off内存存储统计信息,默认N为8,M为16
使用explain 可以获取MySQL预计执行SQL的扫描行数,explain有可能统计出的数量差距比较大,可以通过analyze table t 来重新统计索引信息。优化器会根据扫描行数进行判断,看是否选择该索引,会考虑回表的因素
大多数时候优化器可以选择正确的索引,如果碰到原本可以执行比较快的SQL执行速度却比预期慢
采用force index 强行选择一个索引。
考虑修改语句,引导MySQL使用期望的索引
有些场景可以新建一个更加合适的索引,优化索引提供优化器做选择,删除误用索引
索引哪些情况下会失效?
查询条件包含 or,可能导致索引失效
如果字段类型是字符串,where 时一定用引号括起来,否则会因为隐式类型转换,索引失效
like 通配符可能导致索引失效。
联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。
在索引列上使用 mysql 的内置函数,索引失效。
饮食转换函数?
对索引列运算(如,+、-、*、/),索引失效。
索引字段上使用(!= 或者 < >,not in)时,可能会导致索引失效。
索引字段上使用 is null, is not null,可能导致索引失效。
左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。
MySQL 优化器估计使用全表扫描要比使用索引快,则不使用索引。
哪些场景不是适用索引
数据量比较少的表不适合加索引
更新比较频繁的字段也不适合加索引
离散低的字段不适合加索引(如性别)
给字符串创建索引
使用整个字符串作为整个索引
只需要回主键索引取一次数据
使用前缀索引
可能会导致查询语句读数据的次数变多
节省索引空间
定义好长度可以做到既节省空间,又不用额外增加太多的查询成本。建立索引最主要的是关注区分度,区分度越高越好!
不可使用覆盖索引的性能优化,如果前缀索引的长度包括了所有的信息,还是会回表查询是否满足,系统并不知道前缀索引的定义是否截断了完整的信息
如果前缀区分度不高
倒序存储
新建hash字段,在hash字段上创建索引来保存对区分度不高的字读进行校验
会存在的问题:不支持范围查询,占用额外空间,CPU消耗
回表了解吗?
在 InnoDB 存储引擎里,利用辅助索引查询,先通过辅助索引找到主键索引的键值,再通过主键值查出主键索引里面没有符合要求的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表。
表空间回收
InnoDB中一个表包含两个部分:表结构定义,数据。8.0以前,表结构存放在frm后缀的文件里。8.0以后,已经允许将表结构定义放在系统数据库表中,表结构顶的占用的空间很小
表数据存放在共享表空间里还是单独的文件,由参数 innodb_file_per_table控制
OFF表示,表的数据放在系统空间,和数据字典放在一起
ON表示,每个InnoDB表数据村粗在一个.ibd为后缀的文件中
从MySQL5.6.6开始默认值为ON,建议设置为ON,一个表单独存储为一个问价更容易管理,不需要这张表时 drop table 系统会直接删除该文件,放在共享空间中,即使表删除了,空间页不会回收
InnoDB数据删除流程
InnoDB中数据都是用B+树结构组织的,如果要删除某条记录,InnoDB会将这条记录标记为删除,后续插入记录时会复用这条记录,磁盘文件不会缩小
如果删除一个数据页的所有数据,那么整个数据页都可以复用,这个页可以复用到任何空间
如果两个相邻的数据页,利用率都很小,系统会将两个页的数据整合到同一页上,将另一页标记为可复用。如果将表的所有数据都delete掉,所有的数据页都被标记为"可复用",但磁盘的大小不会改变。delete命令不能回收表空间
数据按照索引递增的顺序插入的,那么索引是紧凑的。数据随机插入,可能会造成索引的数据页分裂。此时页会留下"空洞"。
更新索引上的值,可以理解为删除一个旧值,再插入一个新值,这样也会造成空洞。所以,经过大量增删改的表,都会存在空洞。如果将这些空洞去掉,可以达到收缩表空间的目的。
重建表
5.5版本之前 改锁表DDL
新建一张与表A结构相同的表B,按照主键ID递增的顺序,将数据一行一行从表A读到表B
使用 alter table A engine = InnoDB 命令来重建表,和上述流程一致,MySQL自动完成数据的转化
在重建表的过程中,将数据读到B中这个过程是最耗时的,整个DDL过程中,表A不能更新。DDL不是Online的
5.6引入Online DDL
建立临时文件,扫描表A主键的所有数据页
用数据页中表A的记录生成B+树,存到临时文件中
生成临时文件的过程中,将所有对A的操作记录在一个日志文件中(row log),对应state2
临时文件生成后,将日志文件中的操作应用到临时文件,得到逻辑数据上与表A相同的数据文件,对应图中state3状态
用临时文件替换表A数据文件
由于日志文件记录和重放操作的存在,该方案在重建表的过程中,允许对表A做增删改操作,因此也称之为Online DDL
Online DDL
DDL之前需要拿到MDL写锁,alter语句在启动时需要获取MDL写锁,该写锁在真正拷贝数据之前就退化成读锁了,MDL读锁不会阻塞增删改操作,但是禁止其他线程对这个表做DDL
对于一个大表,Online DDL 最耗时的过程就是拷贝数据到临时表的过程,这个步骤的执行期间可以接受增删改操作。所以,相对于整个 DDL 过程来说,锁的时间非常短。
重建方法会扫描原表数据和构建临时文件,对于很大的表,该操作是很耗IO和CPU的,因此,如果线上服务,需要孝心控制操作时间。安全操作可以使用GitHub开源的gh-ost
Online 和 Inplace
5.5版本之前,表 A 中的数据导出来的存放位置叫作 tmp_table,是一个临时表,在server层创建的
5.6之后,A重建出来的数据放在"tmp_file"里,临时文件是InnoDB在内部创建处来的。整个DDL过程都在InnoDB内部完成,对于server层来说,没有将数据移动到临时表,因此是原地""操作,也称为"inplace"
重建语句alter table t engine=InnoDB
隐含意思为: alter table t engine=InnoDB,ALGORITHM=inplace
copy方式为5.5以前方式:alter table t engine=InnoDB,ALGORITHM=copy
隐含意思为: alter table t engine=InnoDB,ALGORITHM=inplace
copy方式为5.5以前方式:alter table t engine=InnoDB,ALGORITHM=copy
DDL过程是Online的一定是inplace
inplace 的DDL有可能不是Online的,截至到MySQL8.0,添加全文索引(FULLTEXT index)和空间索引(SPATIAL index)属于该情况
optimize table、analyze table、alter table 区别
MySQL5.6开始 alter table t engine = InnoDB 也就是recreate
analyze table t 不是建表,只是对表索引信息重新做统计,没有修改数据,过程中增加MDL读锁
optimize table 等于 recreate + analyze
实践
count() 实践
实现方式
MyISAM引擎将一个表的总行数存在磁盘上,执行count(*)时,会直接返回该数,效率很高
InnoDB引擎,执行count(1)的时候,需要将数据一行一行从引擎读出来然后进行累计
为什么InnoDB不和MyISAM一样将数字存起来?
同一时刻的查询,由于多版本并发控制(MVCC)的原因,InnoDB表返回多少行是不确定的。
InnoDB事务设计有关,可重复读是它的默认隔离级别,在代码上通过多版本并发控制MVCC实现。每一行的记录都要判断自己是否对这个绘画可见,因此count(*) InnoDB只好将数据一行一行读出来依次判断。
同一时刻的查询,由于多版本并发控制(MVCC)的原因,InnoDB表返回多少行是不确定的。
InnoDB事务设计有关,可重复读是它的默认隔离级别,在代码上通过多版本并发控制MVCC实现。每一行的记录都要判断自己是否对这个绘画可见,因此count(*) InnoDB只好将数据一行一行读出来依次判断。
InnoDB是索引组织表,主键索引树的叶子结点是数据,普通索引树的叶子结点是主键值。MySQL优化器会找到最小的那颗树来遍历。保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。
show status命令有一个字段会显示当前表有多少行。该字段一般通过采样来估算的,因此不够准确。官方文档中误差可能达到40%~50%
MyISAM count(*) 很快,但是不支持事务
show table status 命令返回会很快,但是不准确
InnoDB表直接count(*)会遍历全表,虽然准确,但是会导致性能问题
show table status 命令返回会很快,但是不准确
InnoDB表直接count(*)会遍历全表,虽然准确,但是会导致性能问题
统计计数设计
使用缓存系统保存计数
表每插入一行 Redis计数加1,删除一行 Redis计数减1
缓存系统可能会丢失,数据统计可能不精确,异常情况下可以执行count(*)重新获取真实数据
计数值在并发场景下可能导致逻辑上可能不精确,插入数据时,先插入还是先计数都可能导致数据不精确
把计数放在 Redis 里面,不能够保证计数和 MySQL 表里的数据精确一致的原因,是这两个不同的存储构成的系统,不支持分布式事务,无法拿到精确一致的视图。而把计数值也放在 MySQL 中,就解决了一致性视图的问题。
数据库保存计数
将计数值保存到数据库单独一张统计数表中,解决了崩溃丢失问题
数据统计不精确的问题可以使用事务的特性将问题解决
不同count() 的用法
count(*)
MySQL将其优化了下,不取值,并且count(*)肯定不为null,按行累加
count(1)
InnoDB遍历整张表,不取值,server层对于返回的每一行放入一个数字"1",判断不可能为空,按行累加
count(主键 id)
InnoDB引擎遍历整张表,把每一行的id值取出来,返回给sertver层,server层拿到id后判断不可能为空,就按行累加
count(字段)
如果"字段"定义为not null,一行行从记录读出字段,判断不能为null,按行累加
如果"字段"定义允许为null,执行时,判断有可能是null,还要把值取出后在判断一下不是null再累加
效率排序
count(字段)<count(主键 id)<count(1)≈count(*)
order by 实践
场景
查询城市是“杭州”的所有人名字,并且按照姓名排序返回前 1000 个人的姓名、年龄。
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`city` varchar(16) NOT NULL,
`name` varchar(16) NOT NULL,
`age` int(11) NOT NULL,
`addr` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `city` (`city`)
) ENGINE=InnoDB;
`id` int(11) NOT NULL,
`city` varchar(16) NOT NULL,
`name` varchar(16) NOT NULL,
`age` int(11) NOT NULL,
`addr` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `city` (`city`)
) ENGINE=InnoDB;
select city,name,age from t where city='杭州' order by name limit 1000 ;
在city加上索引后
使用explain 命令查看语句执行情况
Extra字段中,"Using filesort"表示需要排序,MySQL会给每个线程分配一块内存用于排序,称为sort_buffer
排序流程
全字段排序
语句流程
1. 初始化sort_buffer,确定name、city、age三个字段
2. 从索引city找到第一个满足 city = '杭州'
3. 到主键id索引取出整行,取name、city、age三个字段的值,存入sort_buffer中
4. 从索引city取下一个记录的主键id
5. 重复3、4到city的值不满足查询条件为止
6. 对 sort_buffer 中的数据按照字段name做快速排序
7. 按照排序结果取1000行返回给客户端
"按name排序"这个动作,可能在内存中完成,可能需要使用外部排序,取决于所需内存和参数sort_buffer_size
如果排序数据量太大,内存放不下,不得不使用磁盘临时文件辅助排序
判断是否使用临时文件
/* 打开optimizer_trace,只对本线程有效 */
SET optimizer_trace='enabled=on';
/* @a保存Innodb_rows_read的初始值 */
select VARIABLE_VALUE into @a from performance_schema.session_status where variable_name = 'Innodb_rows_read';
/* 执行语句 */
select city, name,age from t where city='杭州' order by name limit 1000;
/* 查看 OPTIMIZER_TRACE 输出 */
SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G
/* @b保存Innodb_rows_read的当前值 */
select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read';
/* 计算Innodb_rows_read差值 */
select @b-@a;
/* 打开optimizer_trace,只对本线程有效 */
SET optimizer_trace='enabled=on';
/* @a保存Innodb_rows_read的初始值 */
select VARIABLE_VALUE into @a from performance_schema.session_status where variable_name = 'Innodb_rows_read';
/* 执行语句 */
select city, name,age from t where city='杭州' order by name limit 1000;
/* 查看 OPTIMIZER_TRACE 输出 */
SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G
/* @b保存Innodb_rows_read的当前值 */
select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read';
/* 计算Innodb_rows_read差值 */
select @b-@a;
通过查看OPTIMIIZER_TRACE的结果来确认,可以从number_of_tmp_files中看到是否使用了临时文件
MySQL将需要排序的数据分成12份,每一份单独排序后存在这些临时问价中,然后将12个有序文件合并成一个有序大文件
如股票sort_buffer_size超过了需要排序的数据量大小,number_of_tmp_files就是0,表示排序可以在内存中完成,sort_buffer_size越小,需要分成的份数越多
examined_rows = 4000表示参与排序的行数为4000
sort_mode里面packed_additional_fields的意思是,排序过程对字符串做了"紧凑"处理,即使name字段定义是varchar(16),在排序过程中还是按照实际长度进行分配空间
select @b - @a的返回结果是4000,表示整个执行过程只扫描了4000行, 查询OPTIMIZER_TRACE时需要用到临时表,internal_tmp_disk_storage_engine默认是InnoDB,如果使用的是InnoDB引擎的话,从临时表取出来时,会让Innodb_rows_read的值加1
rowid排序
如果返回的字段很多,sort_buffer里面要放的字段数太多,内存能同时存放下的行数很少,分成多个临时文件,排序性能会很差
SET max_length_for_sort_data = 16; //max_length_for_sort_data是MySQL中专门控制用于排序的行数据长度参数,单行长度超过该值,MySQL判断需要换一个算法
city、name、age 这三个字段的定义总长度是 36, 设置max_length_for_sort_data=16,sort_buffer中的字字段包括,排序的列和主键id
执行流程
1. 初始化 sort_buffer,确定放入两个字段,name和id
2. 从索引city找到一个满足city = '杭州'
3. 找到主键id索引取出整行,取name、id两个字段,存入sort_buffer中
4. 从索引city取下一个记录的主键id
5. 从夫3、4直到不满足city = '杭州'条件的为止
6. 对sort_buffer中的数据按照字段name排序
7. 遍历排序结果取前1000行,按照id值回到原表中取出city、name和age三个字段返回客户端
OPTIMIZER_TRACE结果
对比全字段排序可以发现,rowid排序多了一次访问表t主键索引
此时查询时,examined_rows 还是4000,但是select @b-@a变成了5000
sort_mode变成了<sort_key,rowid>,表示参与排序只有name和id两个字段
number_of_tmp_files变成10,参与排序行数仍然时4000但是每行数据变小了,总数据量变小了,需要的临时文件也相应变小了
全字段排序 VS rowid 排序
如果内存足够,多利用内存,尽量减少磁盘访问
对于InnoDB来说,执行全字段排序会减少磁盘访问,因此会被优先选择。对于内存表,回表过程知识简单根据数行的位置,直接访问内存得到的数据,不会导致多访问磁盘,索引用于排序的行越小越好,因此此时MySQL会选择rowid排序
只有内存小,才会采用rowid排序算法,可以在内存中进行排序,但是需要增加一次回表的操作
对于 InnoDB 表来说,rowid 排序会要求回表多造成磁盘读,因此不会被优先选择
并不是所有的order by语句都需要排序,建立city和name的联合索引,如果city = '杭州'那么可以保证name就是一定是有序的,此时不用进行排序
根据覆盖索引的思想,索引如果满足查询请求,不再需要回表取数据,所以使用city、name、age联合索引,此时使用explain结果Extra字段是"Using index"表示使用了覆盖索引,性能会快很多
随机显示消息
https://time.geekbang.org/column/article/73795
https://time.geekbang.org/column/article/73795
场景
每个级别有对应的单词表,从单词表中随机选择三个单词,如何实现随机选择三个单词
CREATE TABLE `words` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`word` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
`id` int(11) NOT NULL AUTO_INCREMENT,
`word` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
使用order by rand()
select word from words order by rand() limit 3;
explain查看语句执行计划
Extra显示Using temporary 表示需要使用临时表;Using filesort 表示需要执行排序操作
因此需要临时表,并且需要在临时表上排序
因此需要临时表,并且需要在临时表上排序
内存临时表
对于内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘
语句执行流程
1. 创建一个临时表,使用memory引擎,表中两个字段,第一个是double类型R,第二个字段是varchar(64)W
2. 从words表中,按照主键顺序取出所有word,对于每个word调用rand()函数形成一个大于0小于1的随机小数,将随机小数和word分别存入临时表R和W中,扫面行数为10000
3. 临时表行数10000行,在没有索引的内存临时表上按照字段R排序
4. 初始化sort_buffer,sort_buffer中有两个字段,double和整型
5. 从内存临时表中取出R值和位置信息,分别存入sort_buffer中两个字段里,整个过程要对内存临时表做全表扫面,扫面行数增加10000,变成20000
6. 在sort_buffer中根据R进行排序,没有涉及表操作,不会增加扫描行数
7. 排序完成后取出前三个结果位置信息,依次到内存临时表中取出word值,返回给客户端,总扫描行数变成20003
MySQL表用什么方法定位一行数据
如果表中没有主键,InnoDB会自己生成一个长度6字节的rowid
对于有主键的InnoDB表,rowid是主键ID
对于没有主键的InnoDB表,rowid由系统生成
MEMORY引擎不是索引组织表。可以认为其是一个数组,rowid为数组的下标。
order by rand() 使用了内存临时表,内存临时表排序时,使用了rowid排序方法
磁盘临时表
tmp_table_size 配置限制内存临时表的大小,默认16M,临时表超过tmp_table_size,那么内存临时表会转成磁盘临时表
磁盘临时表使用的引擎是InnoDB,是由参数internal_tmp_disk_storage_engine控制
导致SQL性能的原因
索引相关
对索引字段做函数操作,可能会破坏索引值的有序性,优化器会决定放弃走树搜索功能
对于上述内容情况,可以根据业务场景在索引上使用范围查询,不使用函数,从而避免慢查询
类型转换
如果字段为 varchar,但是查询时,采用数字进行查询,会进行全表扫面
数据类型转换规则
如果字符串和数字相比较,会将字符串转成数字
CAST() 函数,转换类型
数据类型字符集不同
表A为 utf8 表B为 utf8mb4,所以表链接查询时,用不上关联字段的索引
utf8mb4 字符集 是 utf8的超集,所以这两个字符串比较的时候,会把utf8字符串转成 utf8mb4字符串,再做比较,所以会全表查询
CONVERT()函数 改变字符集类型
链接过程中,要求再被驱动表的索引自动福安加函数操作,是直接导致对被驱动表做全表扫描的原因
查询长时间不返回
表锁住了,session A通过lock table命令 持有t的MDL写锁,session B的查询需要获取MDL的读锁,session B会进入等待状态
解决方法: 找到持有MDL写锁,然后kill掉
设置 performance_schema 参数 为on 相比 off 会有10%的性能损失
查询 sys.schema_table_lock_waits 表 找出阻塞的的process id 然后kill掉
查询 sys.schema_table_lock_waits 表 找出阻塞的的process id 然后kill掉
等待行锁:A事务 启动了一个写命令,但是没有提交,B事务执行读需要读锁,会进行等待。
select * from t where id=1 lock in share mode;
查询一条记录导致的慢查询
MVVC一致性读,事务更新了100w次,导致undo logiu100w条,所以一致性读会搜索以往的状态,所以变慢了
提高性能的临时方案
短链接风暴
如果使用的是短链接,在业务高峰期,可能出现链接数突然暴增的情况。
建立链接的过程成本是很高的,除了正常的网络链接三次握手之外,还需要做登录权限判断,获得链接的读取权限
方案
max_connections 可以控制连接数量,一旦连接更多可能会导致系统的负载进一步加大,大量的资源消耗在权限验证等连接问题上
提前剔除一些连接着但是不工作的线程,主动剔除和设置timeout的效果差不多
减少连接过程中的消耗。跳过权限验证阶段
慢查询性能问题
索引没有设计好
紧急创建索引进行解决
1. 先在备库B上执行set sql_log_bin = off ,不写binlog,然后执行alter table语句加上索引
2. 主备切换
3. 在A库上执行1操作
2. 主备切换
3. 在A库上执行1操作
SQL语句没有写好
通过查询重写的方式进行解决
call query_rewrite.flush_rewrite_rules(); -- 存储方式
预先避免
测试环境,将慢查询日志打开,模拟线上数据,回归测试,留意慢查询日志里面的输出
MySQL选错索引
通过查询重写的方式进行解决
预先避免
测试环境,将慢查询日志打开,模拟线上数据,回归测试,留意慢查询日志里面的输出
QPS突增
场景:下掉一个功能,从数据库端处理
1. 全新业务导致的,在确定业务方会下掉该功能,从数据库端将白名单去掉
2. 新功能使用的是单独的数据库用户,用管理员账号将用户删掉,断开现有的连接
3. 新功能和主体服务在一起,通过语句重写的方式处理
如果其他业务也有相同的模板,会导致误伤
单独将一个语句可能会导致后续的逻辑失败
不建议
MySQL出现性能瓶颈,且瓶颈是在IO上,有什么方法
设置binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。
这个方法基于“额外的故意等待”来实现,因此可能会增加语句的相应时间,但没有丢失数据风险
这个方法基于“额外的故意等待”来实现,因此可能会增加语句的相应时间,但没有丢失数据风险
将sync_binlog设置为大于1的值(常见 100~1000)风险是 主机掉电时会丢失binlog日志
innodb_flush_log_at_trx_commit 设置为2。 风险是 主机掉电时会丢数据
不建议 把innodb_flush_log_at_trx_commit 设置成 0。 设置成0,如果MySQL本身异常重启也会丢数据,风险太大。redo log 写到文件系统的page cache 的速度也是很快的。所以将参数设置成2 和设置成0性能差不多,这样做MySQL异常重启时不会丢数据,相比之下风险会更小
慢SQL如何定位
慢查询日志:开启 MySQL 的慢查询日志,再通过一些工具比如 mysqldumpslow 去分析对应的慢查询日志,当然现在一般的云厂商都提供了可视化的平台。
服务监控:可以在业务的基建中加入对慢 SQL 的监控,常见的方案有字节码插桩、连接池扩展、ORM 框架过程,对服务运行中的慢 SQL 进行监控和告警。
慢sql 的优化方式
避免不必要的列
SQL 查询的时候,应该只查询需要的列,而不要包含额外的列,像slect * 这种写法应该尽量避免。
分页优化
延迟关联
先通过 where 条件提取出主键,在将该表与原数据表关联,通过主键 id 提取数据行,而不是通过原来的二级索引提取数据行
select a.* from table a,
(select id from table where type = 2 and level = 9 order by id asc limit 190289,10 ) b
where a.id = b.id
(select id from table where type = 2 and level = 9 order by id asc limit 190289,10 ) b
where a.id = b.id
书签方式
书签方式就是找到 limit 第一个参数对应的主键值,根据这个主键值再去过滤并 limit
select * from table where id >
(select * from table where type = 2 and level = 9 order by id asc limit 190
(select * from table where type = 2 and level = 9 order by id asc limit 190
索引优化
利用覆盖索引
InnoDB 使用非主键索引查询数据时会回表,但是如果索引的叶节点中已经包含要查询的字段,那它没有必要再回表查询了,这就叫覆盖索引
低版本避免使用or
避免使用 !=/<>
通过把不等于操作符改成 or,可以使用索引,避免全表扫描
把column<>’aaa’,改成column>’aaa’ or column<’aaa’,就可以使用索引了
适当使用前缀索引
适当地使用前缀所云,可以降低索引的空间占用,提高索引的查询效率。
比如,邮箱的后缀都是固定的“@xxx.com”,那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引
比如,邮箱的后缀都是固定的“@xxx.com”,那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引
alter table test add index index2(email(6));
PS:需要注意的是,前缀索引也存在缺点,MySQL 无法利用前缀索引做 order by 和 group by 操作,也无法作为覆盖索引
避免列上函数运算
要避免在列字段上进行算术运算或其他表达式运算,否则可能会导致存储引擎无法正确使用索引,从而影响了查询的效率
select * from test where id + 1 = 50;
select * from test where month(updateTime) = 7;
select * from test where month(updateTime) = 7;
正确使用联合索引
使用联合索引的时候,注意最左匹配原则。
join 优化
优化子查询
尽量使用 Join 语句来替代子查询,因为子查询是嵌套查询,而嵌套查询会新创建一张临时表,而临时表的创建与销毁会占用一定的系统资源以及花费一定的时间,同时对于返回结果集比较大的子查询,其对查询性能的影响更大
小表驱动大表
关联查询的时候要拿小表去驱动大表,因为关联的时候,MySQL 内部会遍历驱动表,再去连接被驱动表。
比如 left join,左表就是驱动表,A 表小于 B 表,建立连接的次数就少,查询速度就被加快了。
select name from A left join B ;
适当增加冗余字段
增加冗余字段可以减少大量的连表查询,因为多张表的连表查询性能很低,所有可以适当的增加冗余字段,以减少多张表的关联查询,这是以空间换时间的优化策略
避免join太多表
阿里巴巴 Java 开发手册》规定不要 join 超过三张表,第一 join 太多降低查询的速度,第二 join 的 buffer 会占用更多的内存。
如果不可避免要 join 多张表,可以考虑使用数据异构的方式异构到 ES 中查询。
如果不可避免要 join 多张表,可以考虑使用数据异构的方式异构到 ES 中查询。
排序优化
利用索引扫描做排序
MySQL 有两种方式生成有序结果:其一是对结果集进行排序的操作,其二是按照索引顺序扫描得出的结果自然是有序的
但是如果索引不能覆盖查询所需列,就不得不每扫描一条记录回表查询一次,这个读操作是随机 IO,通常会比顺序全表扫描还慢
因此,在设计索引时,尽可能使用同一个索引既满足排序又用于查找行
但是如果索引不能覆盖查询所需列,就不得不每扫描一条记录回表查询一次,这个读操作是随机 IO,通常会比顺序全表扫描还慢
因此,在设计索引时,尽可能使用同一个索引既满足排序又用于查找行
--建立索引(date,staff_id,customer_id)
select staff_id, customer_id from test where date = '2010-01-01' order by staff_id,customer_id;
select staff_id, customer_id from test where date = '2010-01-01' order by staff_id,customer_id;
只有当索引的列顺序和 ORDER BY 子句的顺序完全一致,并且所有列的排序方向都一样时,才能够使用索引来对结果做排序。
union优化
条件下推
MySQL 处理 union 的策略是先创建临时表,然后将各个查询结果填充到临时表中最后再来做查询,很多优化策略在 union 查询中都会失效,因为它无法利用索引
最好手工将 where、limit 等子句下推到 union 的各个子查询中,以便优化器可以充分利用这些条件进行优化。
此外,除非确实需要服务器去重,一定要使用 union all,如果不加 all 关键字,MySQL 会给临时表加上 distinct 选项,这会导致对整个临时表做唯一性检查,代价很高。
最好手工将 where、limit 等子句下推到 union 的各个子查询中,以便优化器可以充分利用这些条件进行优化。
此外,除非确实需要服务器去重,一定要使用 union all,如果不加 all 关键字,MySQL 会给临时表加上 distinct 选项,这会导致对整个临时表做唯一性检查,代价很高。
怎么看执行计划(explain),如何理解其中各个字段的含义?
explain 是 sql 优化的利器,除了优化慢 sql,平时的 sql 编写,也应该先 explain,查看一下执行计划,看看是否还有优化的空间。
直接在 select 语句之前增加explain 关键字,就会返回执行计划的信息。
直接在 select 语句之前增加explain 关键字,就会返回执行计划的信息。
id
MySQL 会为每个 select 语句分配一个唯一的 id 值
select_type
查询的类型,根据关联、union、子查询等等分类,常见的查询类型有 SIMPLE、PRIMARY
table
表示 explain 的一行正在访问哪个表。
type
system
当表仅有一行记录时(系统表),数据量很少,往往不需要进行磁盘 IO,速度非常快
const
表示查询时命中 primary key 主键或者 unique 唯一索引,或者被连接的部分是一个常量(const)值。这类扫描效率极高,返回数据量少,速度非常快。
eq_ref
查询时命中主键primary key 或者 unique key索引, type 就是 eq_ref。
ref_or_null
这种连接类型类似于 ref,区别在于 MySQL会额外搜索包含NULL值的行。
index_merge
使用了索引合并优化方法,查询使用了两个以上的索引。
unique_subquery
替换下面的 IN子查询,子查询返回不重复的集合。
index_subquery
区别于unique_subquery,用于非唯一索引,可以返回重复值。
range
使用索引选择行,仅检索给定范围内的行。简单点说就是针对一个有索引的字段,给定范围检索数据。在where语句中使用 bettween...and、<、>、<=、in 等条件查询 type 都是 range。
index
Index 与ALL 其实都是读全表,区别在于index是遍历索引树读取,而ALL是从硬盘中读取。
all
全表扫描。
possible_keys
显示查询可能使用哪些索引来查找,使用索引优化 sql 的时候比较重要。
keys
这一列显示 mysql 实际采用哪个索引来优化对该表的访问,判断索引是否失效的时候常用。
key_len
显示了 MySQL 使用
ref
ref 列展示的就是与索引列作等值匹配的值,常见的有:const(常量),func,NULL,字段名。
rows
这也是一个重要的字段,MySQL 查询优化器根据统计信息,估算 SQL 要查到结果集需要扫描读取的数据行数,这个值非常直观显示 SQL 的效率好坏,原则上 rows 越少越好。
extra
using index
表示 MySQL 将使用覆盖索引,以避免回表;直接访问索引就足够获取到所需要的数据不需要通过索引回表,一般是通过将带查询字段建立联合索引来实现的
using where
表示会在存储引擎检索之后再进行过滤; 也就是说,优化器需要通过索引回表查询数据
using temporaty
表示对查询结果排序时会使用一个临时表
using index condition
在5.6版本之后新加的特性, 也就是 索引下推, 是mysql 关于减少回表次数的重大优化
using filesort
文件排序,这个一般在order by 的时候数据量过大,mysql 会将所有数据找回内存中排序比较消耗资源
了解深度分页的问题吗?
问题描述: limit 遇到后面的查询性能会越来越差
select * from user where create_time>'2022-07-03' limit 100000,10;
翻到1000页的时候 效率急剧下降
问题分析
1. 需要做回表操作,而回表操作通常是IO操作
解决方案
1. 使用子查询
select * from user where id in (
select id from (
select id from user where create_time > '2022-07-03' limit 10000, 10
) as t
);
select id from (
select id from user where create_time > '2022-07-03' limit 10000, 10
) as t
);
2. 使用inner join 关联查询
select * from user inner join (
select id. from. user where create_time > '2022-07-03' limit 10000,10
) as t on user_id = t.id;
select id. from. user where create_time > '2022-07-03' limit 10000,10
) as t on user_id = t.id;
3. 使用分页游标
面试题
基础
什么是内连接、外连接、交叉连接、笛卡尔积呢?
内连接(inner join):取得两张表中满足存在连接匹配关系的记录。
外连接(outer join):不只取得两张表中满足存在连接匹配关系的记录,还包括某张表(或两张表)中不满足匹配关系的记录。
交叉连接(cross join):显示两张表所有记录一一对应,没有匹配关系进行筛选,它是笛卡尔积在 SQL 中的实现,如果 A 表有 m 行,B 表有 n 行,那么 A 和 B 交叉连接的结果就有 m*n 行。
笛卡尔积:是数学中的一个概念,例如集合 A={a,b},集合 B={0,1,2},那么 A✖️B={<a,o>,<a,1>,<a,2>,<b,0>,<b,1>,<b,2>,}。
外连接(outer join):不只取得两张表中满足存在连接匹配关系的记录,还包括某张表(或两张表)中不满足匹配关系的记录。
交叉连接(cross join):显示两张表所有记录一一对应,没有匹配关系进行筛选,它是笛卡尔积在 SQL 中的实现,如果 A 表有 m 行,B 表有 n 行,那么 A 和 B 交叉连接的结果就有 m*n 行。
笛卡尔积:是数学中的一个概念,例如集合 A={a,b},集合 B={0,1,2},那么 A✖️B={<a,o>,<a,1>,<a,2>,<b,0>,<b,1>,<b,2>,}。
那 MySQL 的内连接、左连接、右连接有有什么区别?
MySQL 的连接主要分为内连接和外连接,外连接常用的有左连接、右连接。
inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集
left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。
inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集
left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。
数据库的三范式
第一范式:数据表中的每一列(每个字段)都不可以再拆分。例如用户表,用户地址还可以拆分成国家、省份、市,这样才是符合第一范式的。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。例如订单表里,存储了商品信息(商品价格、商品类型),那就需要把商品 ID 和订单 ID 作为联合主键,才满足第二范式。
第三范式:在满足第二范式的基础上,表中的非主键只依赖于主键,而不依赖于其他非主键。例如订单表,就不能存储用户信息(姓名、地址)。
第二范式:在第一范式的基础上,非主键列完全依赖于主键,而不能是依赖于主键的一部分。例如订单表里,存储了商品信息(商品价格、商品类型),那就需要把商品 ID 和订单 ID 作为联合主键,才满足第二范式。
第三范式:在满足第二范式的基础上,表中的非主键只依赖于主键,而不依赖于其他非主键。例如订单表,就不能存储用户信息(姓名、地址)。
三大范式的作用是为了控制数据库的冗余,是对空间的节省,实际上,一般互联网公司的设计都是反范式的,通过冗余一些数据,避免跨表跨库,利用空间换时间,提高性能。
varchar 与 char 的区别?
char:
char 表示定长字符串,长度是固定的;
如果插入数据的长度小于 char 的固定长度时,则用空格填充;
因为长度固定,所以存取速度要比 varchar 快很多,甚至能快 50%,但正因为其长度固定,所以会占据多余的空间,是空间换时间的做法;
对于 char 来说,最多能存放的字符个数为 255,和编码无关
char 表示定长字符串,长度是固定的;
如果插入数据的长度小于 char 的固定长度时,则用空格填充;
因为长度固定,所以存取速度要比 varchar 快很多,甚至能快 50%,但正因为其长度固定,所以会占据多余的空间,是空间换时间的做法;
对于 char 来说,最多能存放的字符个数为 255,和编码无关
varchar:
varchar 表示可变长字符串,长度是可变的;
插入的数据是多长,就按照多长来存储;
varchar 在存取方面与 char 相反,它存取慢,因为长度不固定,但正因如此,不占据多余的空间,是时间换空间的做法;
对于 varchar 来说,最多能存放的字符个数为 65532
varchar 表示可变长字符串,长度是可变的;
插入的数据是多长,就按照多长来存储;
varchar 在存取方面与 char 相反,它存取慢,因为长度不固定,但正因如此,不占据多余的空间,是时间换空间的做法;
对于 varchar 来说,最多能存放的字符个数为 65532
日常的设计,对于长度相对固定的字符串,可以使用 char,对于长度不确定的,使用 varchar 更合适一些。
blob 和 text 有什么区别?
blob 用于存储二进制数据,而 text 用于存储大字符串。
blob 没有字符集,text 有一个字符集,并且根据字符集的校对规则对值进行排序和比较
blob 没有字符集,text 有一个字符集,并且根据字符集的校对规则对值进行排序和比较
DATETIME 和 TIMESTAMP 的异同?
相同点:
两个数据类型存储时间的表现格式一致。均为 YYYY-MM-DD HH:MM:SS
两个数据类型都包含「日期」和「时间」部分。
两个数据类型都可以存储微秒的小数秒(秒后 6 位小数秒)
两个数据类型存储时间的表现格式一致。均为 YYYY-MM-DD HH:MM:SS
两个数据类型都包含「日期」和「时间」部分。
两个数据类型都可以存储微秒的小数秒(秒后 6 位小数秒)
DATETIME 和 TIMESTAMP 的区别
日期范围:DATETIME 的日期范围是 1000-01-01 00:00:00.000000 到 9999-12-31 23:59:59.999999;TIMESTAMP 的时间范围是1970-01-01 00:00:01.000000 UTC 到 ``2038-01-09 03:14:07.999999 UTC
存储空间:DATETIME 的存储空间为 8 字节;TIMESTAMP 的存储空间为 4 字节
时区相关:DATETIME 存储时间与时区无关;TIMESTAMP 存储时间与时区有关,显示的值也依赖于时区
默认值:DATETIME 的默认值为 null;TIMESTAMP 的字段默认不为空(not null),默认值为当前时间(CURRENT_TIMESTAMP)
日期范围:DATETIME 的日期范围是 1000-01-01 00:00:00.000000 到 9999-12-31 23:59:59.999999;TIMESTAMP 的时间范围是1970-01-01 00:00:01.000000 UTC 到 ``2038-01-09 03:14:07.999999 UTC
存储空间:DATETIME 的存储空间为 8 字节;TIMESTAMP 的存储空间为 4 字节
时区相关:DATETIME 存储时间与时区无关;TIMESTAMP 存储时间与时区有关,显示的值也依赖于时区
默认值:DATETIME 的默认值为 null;TIMESTAMP 的字段默认不为空(not null),默认值为当前时间(CURRENT_TIMESTAMP)
MySQL 中 in 和 exists 的区别?
MySQL 中的 in 语句是把外表和内表作 hash 连接,而 exists 语句是对外表作 loop 循环,每次 loop 循环再对内表进行查询。我们可能认为 exists 比 in 语句的效率要高,这种说法其实是不准确的,要区分情景:
如果查询的两个表大小相当,那么用 in 和 exists 差别不大。
如果两个表中一个较小,一个是大表,则子查询表大的用 exists,子查询表小的用 in。
not in 和 not exists:如果查询语句使用了 not in,那么内外表都进行全表扫描,没有用到索引;而 not extsts 的子查询依然能用到表上的索引。所以无论那个表大,用 not exists 都比 not in 要快。
如果查询的两个表大小相当,那么用 in 和 exists 差别不大。
如果两个表中一个较小,一个是大表,则子查询表大的用 exists,子查询表小的用 in。
not in 和 not exists:如果查询语句使用了 not in,那么内外表都进行全表扫描,没有用到索引;而 not extsts 的子查询依然能用到表上的索引。所以无论那个表大,用 not exists 都比 not in 要快。
MySQL 里记录货币用什么字段类型比较好?
货币在数据库中 MySQL 常用 Decimal 和 Numric 类型表示,这两种类型被 MySQL 实现为同样的类型。他们被用于保存与货币有关的数据。
例如 salary DECIMAL(9,2),9(precision)代表将被用于存储值的总的小数位数,而 2(scale)代表将被用于存储小数点后的位数。存储在 salary 列中的值的范围是从-9999999.99 到 9999999.99。
DECIMAL 和 NUMERIC 值作为字符串存储,而不是作为二进制浮点数,以便保存那些值的小数精度。
之所以不使用 float 或者 double 的原因:因为 float 和 double 是以二进制存储的,所以有一定的误差。
例如 salary DECIMAL(9,2),9(precision)代表将被用于存储值的总的小数位数,而 2(scale)代表将被用于存储小数点后的位数。存储在 salary 列中的值的范围是从-9999999.99 到 9999999.99。
DECIMAL 和 NUMERIC 值作为字符串存储,而不是作为二进制浮点数,以便保存那些值的小数精度。
之所以不使用 float 或者 double 的原因:因为 float 和 double 是以二进制存储的,所以有一定的误差。
MySQL 怎么存储 emoji😊?
MySQL 可以直接使用字符串存储 emoji。
但是需要注意的,utf8 编码是不行的,MySQL 中的 utf8 是阉割版的 utf8,它最多只用 3 个字节存储字符,所以存储不了表情。那该怎么办?
需要使用 utf8mb4 编码。
但是需要注意的,utf8 编码是不行的,MySQL 中的 utf8 是阉割版的 utf8,它最多只用 3 个字节存储字符,所以存储不了表情。那该怎么办?
需要使用 utf8mb4 编码。
alter table blogs modify content text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci not null;
drop、delete 与 truncate 的区别?
UNION 与 UNION ALL 的区别?
如果使用 UNION,会在表链接后筛选掉重复的记录行
如果使用 UNION ALL,不会合并重复的记录行
从效率上说,UNION ALL 要比 UNION 快很多,如果合并没有刻意要删除重复行,那么就使用 UNION All
如果使用 UNION ALL,不会合并重复的记录行
从效率上说,UNION ALL 要比 UNION 快很多,如果合并没有刻意要删除重复行,那么就使用 UNION All
count(1)、count(*) 与 count(列名) 的区别?
执行效果:
count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为 NULL
count(1)包括了忽略所有列,用 1 代表代码行,在统计结果的时候,不会忽略列值为 NULL
count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者 0,而是表示 null)的计数,即某个字段值为 NULL 时,不统计。
执行速度:
列名为主键,count(列名)会比 count(1)快
列名不为主键,count(1)会比 count(列名)快
如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)
如果有主键,则 select count(主键)的执行效率是最优的
如果表只有一个字段,则 select count(*)最优。
count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为 NULL
count(1)包括了忽略所有列,用 1 代表代码行,在统计结果的时候,不会忽略列值为 NULL
count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者 0,而是表示 null)的计数,即某个字段值为 NULL 时,不统计。
执行速度:
列名为主键,count(列名)会比 count(1)快
列名不为主键,count(1)会比 count(列名)快
如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)
如果有主键,则 select count(主键)的执行效率是最优的
如果表只有一个字段,则 select count(*)最优。
一条 SQL 查询语句的执行顺序?
FROM:对 FROM 子句中的左表<left_table>和右表<right_table>执行笛卡儿积(Cartesianproduct),产生虚拟表 VT1
ON:对虚拟表 VT1 应用 ON 筛选,只有那些符合<join_condition>的行才被插入虚拟表 VT2 中
JOIN:如果指定了 OUTER JOIN(如 LEFT OUTER JOIN、RIGHT OUTER JOIN),那么保留表中未匹配的行作为外部行添加到虚拟表 VT2 中,产生虚拟表 VT3。如果 FROM 子句包含两个以上表,则对上一个连接生成的结果表 VT3 和下一个表重复执行步骤 1)~步骤 3),直到处理完所有的表为止
WHERE:对虚拟表 VT3 应用 WHERE 过滤条件,只有符合<where_condition>的记录才被插入虚拟表 VT4 中
GROUP BY:根据 GROUP BY 子句中的列,对 VT4 中的记录进行分组操作,产生 VT5
CUBE|ROLLUP:对表 VT5 进行 CUBE 或 ROLLUP 操作,产生表 VT6
HAVING:对虚拟表 VT6 应用 HAVING 过滤器,只有符合<having_condition>的记录才被插入虚拟表 VT7 中。
SELECT:第二次执行 SELECT 操作,选择指定的列,插入到虚拟表 VT8 中
DISTINCT:去除重复数据,产生虚拟表 VT9
ORDER BY:将虚拟表 VT9 中的记录按照<order_by_list>进行排序操作,产生虚拟表 VT10。11)
LIMIT:取出指定行的记录,产生虚拟表 VT11,并返回给查询用户
ON:对虚拟表 VT1 应用 ON 筛选,只有那些符合<join_condition>的行才被插入虚拟表 VT2 中
JOIN:如果指定了 OUTER JOIN(如 LEFT OUTER JOIN、RIGHT OUTER JOIN),那么保留表中未匹配的行作为外部行添加到虚拟表 VT2 中,产生虚拟表 VT3。如果 FROM 子句包含两个以上表,则对上一个连接生成的结果表 VT3 和下一个表重复执行步骤 1)~步骤 3),直到处理完所有的表为止
WHERE:对虚拟表 VT3 应用 WHERE 过滤条件,只有符合<where_condition>的记录才被插入虚拟表 VT4 中
GROUP BY:根据 GROUP BY 子句中的列,对 VT4 中的记录进行分组操作,产生 VT5
CUBE|ROLLUP:对表 VT5 进行 CUBE 或 ROLLUP 操作,产生表 VT6
HAVING:对虚拟表 VT6 应用 HAVING 过滤器,只有符合<having_condition>的记录才被插入虚拟表 VT7 中。
SELECT:第二次执行 SELECT 操作,选择指定的列,插入到虚拟表 VT8 中
DISTINCT:去除重复数据,产生虚拟表 VT9
ORDER BY:将虚拟表 VT9 中的记录按照<order_by_list>进行排序操作,产生虚拟表 VT10。11)
LIMIT:取出指定行的记录,产生虚拟表 VT11,并返回给查询用户
存储引擎
常见的存储引擎
存储引擎应该怎么选择?
大致上可以这么选择:
大多数情况下,使用默认的 InnoDB 就够了。如果要提供提交、回滚和恢复的事务安全(ACID 兼容)能力,并要求实现并发控制,InnoDB 就是比较靠前的选择了。
如果数据表主要用来插入和查询记录,则 MyISAM 引擎提供较高的处理效率。
如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存的 MEMORY 引擎中,MySQL 中使用该引擎作为临时表,存放查询的中间结果。
使用哪一种引擎可以根据需要灵活选择,因为存储引擎是基于表的,所以一个数据库中多个表可以使用不同的引擎以满足各种性能和实际需求。使用合适的存储引擎将会提高整个数据库的性能。
大多数情况下,使用默认的 InnoDB 就够了。如果要提供提交、回滚和恢复的事务安全(ACID 兼容)能力,并要求实现并发控制,InnoDB 就是比较靠前的选择了。
如果数据表主要用来插入和查询记录,则 MyISAM 引擎提供较高的处理效率。
如果只是临时存放数据,数据量不大,并且不需要较高的数据安全性,可以选择将数据保存在内存的 MEMORY 引擎中,MySQL 中使用该引擎作为临时表,存放查询的中间结果。
使用哪一种引擎可以根据需要灵活选择,因为存储引擎是基于表的,所以一个数据库中多个表可以使用不同的引擎以满足各种性能和实际需求。使用合适的存储引擎将会提高整个数据库的性能。
InnoDB 和 MylSAM 主要有什么区别?
存储结构:每个 MyISAM 在磁盘上存储成三个文件;InnoDB 所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB 表的大小只受限于操作系统文件的大小,一般为 2GB。
事务支持:MyISAM 不提供事务支持;InnoDB 提供事务支持事务,具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全特性。
最小锁粒度:MyISAM 只支持表级锁,更新时会锁住整张表,导致其它查询和更新都会被阻塞 InnoDB 支持行级锁。
索引类型:MyISAM 的索引为非聚簇索引,数据结构是 B 树;InnoDB 的索引是聚簇索引,数据结构是 B+树。
主键必需:MyISAM 允许没有任何索引和主键的表存在;InnoDB 如果没有设定主键或者非空唯一索引,**就会自动生成一个 6 字节的主键(用户不可见)**,数据是主索引的一部分,附加索引保存的是主索引的值。
表的具体行数:MyISAM 保存了表的总行数,如果 select count(*) from table;会直接取出出该值; InnoDB 没有保存表的总行数,如果使用 select count(*) from table;就会遍历整个表;但是在加了 wehre 条件后,MyISAM 和 InnoDB 处理的方式都一样。
外键支持:MyISAM 不支持外键;InnoDB 支持外键。
高性能/高可用
数据库读写分离了解吗
读写分离的基本原理是将数据库读写操作分散到不同的节点上,下面是基本架构图:
读写分离的基本实现是:
数据库服务器搭建主从集群,一主一从、一主多从都可以。
数据库主机负责读写操作,从机只负责读操作。
数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
数据库服务器搭建主从集群,一主一从、一主多从都可以。
数据库主机负责读写操作,从机只负责读操作。
数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
那读写分离的分配怎么实现呢?
将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。
(1)程序代码封装
程序代码封装指在代码中抽象一个数据访问层(所以有的文章也称这种方式为 "中间层封装" ) ,实现读写操作分离和数据库服务器连接的管理。例如,基于 Hibernate 进行简单封装,就可以实现读写分离:
(1)程序代码封装
程序代码封装指在代码中抽象一个数据访问层(所以有的文章也称这种方式为 "中间层封装" ) ,实现读写操作分离和数据库服务器连接的管理。例如,基于 Hibernate 进行简单封装,就可以实现读写分离:
目前开源的实现方案中,淘宝的 TDDL (Taobao Distributed Data Layer, 外号:头都大了)是比较有名的。
(2)中间件封装
中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供 SQL 兼容的协议,业务服务器无须自己进行读写分离。
对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。
其基本架构是:
中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供 SQL 兼容的协议,业务服务器无须自己进行读写分离。
对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。
其基本架构是:
主从复制原理了解吗?
master 数据写入,更新 binlog
master 创建一个 dump 线程向 slave 推送 binlog
slave 连接到 master 的时候,会创建一个 IO 线程接收 binlog,并记录到 relay log 中继日志中
slave 再开启一个 sql 线程读取 relay log 事件并在 slave 执行,完成同步
slave 记录自己的 binglog
master 创建一个 dump 线程向 slave 推送 binlog
slave 连接到 master 的时候,会创建一个 IO 线程接收 binlog,并记录到 relay log 中继日志中
slave 再开启一个 sql 线程读取 relay log 事件并在 slave 执行,完成同步
slave 记录自己的 binglog
主从同步延迟怎么处理?
主从同步延迟的原因
一个服务器开放N个链接给客户端来连接的,这样有会有大并发的更新操作, 但是从服务器的里面读取 binlog 的线程仅有一个,当某个 SQL 在从服务器上执行的时间稍长 或者由于某个 SQL 要进行锁表就会导致,主服务器的 SQL 大量积压,未被同步到从服务器里。这就导致了主从不一致, 也就是主从延迟。
主从同步延迟的解决办法
解决主从复制延迟有几种常见的方法:
1. 写操作后的读操作指定发给数据库主服务器
例如,注册账号完成后,登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定,对业务的侵入和影响较大,如果哪个新来的程序员不知道这样写代码,就会导致一个 bug。
2. 读从机失败后再读一次主机
这就是通常所说的 "二次读取" ,二次读取和业务无绑定,只需要对底层数据库访问的 API 进行封装即可,实现代价较小,不足之处在于如果有很多二次读取,将大大增加主机的读操作压力。例如,黑客暴力破解账号,会导致大量的二次读取操作,主机可能顶不住读操作的压力从而崩溃。
3. 关键业务读写操作全部指向主机,非关键业务采用读写分离
例如,对于一个用户管理系统来说,注册 + 登录的业务读写操作全部访问主机,用户的介绍、爰好、等级等业务,可以采用读写分离,因为即使用户改了自己的自我介绍,在查询时却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受。
1. 写操作后的读操作指定发给数据库主服务器
例如,注册账号完成后,登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定,对业务的侵入和影响较大,如果哪个新来的程序员不知道这样写代码,就会导致一个 bug。
2. 读从机失败后再读一次主机
这就是通常所说的 "二次读取" ,二次读取和业务无绑定,只需要对底层数据库访问的 API 进行封装即可,实现代价较小,不足之处在于如果有很多二次读取,将大大增加主机的读操作压力。例如,黑客暴力破解账号,会导致大量的二次读取操作,主机可能顶不住读操作的压力从而崩溃。
3. 关键业务读写操作全部指向主机,非关键业务采用读写分离
例如,对于一个用户管理系统来说,注册 + 登录的业务读写操作全部访问主机,用户的介绍、爰好、等级等业务,可以采用读写分离,因为即使用户改了自己的自我介绍,在查询时却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受。
你们一般怎么分库的?
垂直分库:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。
水平分库:以字段为依据,按照一定策略(hash、range 等),将一个库中的数据拆分到多个库中。
水平分表有哪几种路由方式?
范围路由:选取有序的数据列 (例如,整形、时间戳等) 作为路由的条件,不同分段分散到不同的数据库表中。
我们可以观察一些支付系统,发现只能查一年范围内的支付记录,这个可能就是支付公司按照时间进行了分表。
范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间,具体需要根据业务选取合适的分段大小。
范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。范围路由的一个比较隐含的缺点是分布不均匀,假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1000 条,而另外一个分段实际存储的数据量有 900 万条。
我们可以观察一些支付系统,发现只能查一年范围内的支付记录,这个可能就是支付公司按照时间进行了分表。
范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100 万至 2000 万之间,具体需要根据业务选取合适的分段大小。
范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。范围路由的一个比较隐含的缺点是分布不均匀,假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1000 条,而另外一个分段实际存储的数据量有 900 万条。
Hash 路由:选取某个列 (或者某几个列组合也可以) 的值进行 Hash 运算,然后根据 Hash 结果分散到不同的数据库表中。
同样以订单 id 为例,假如我们一开始就规划了 4 个数据库表,路由算法可以简单地用 id % 4 的值来表示数据所属的数据库表编号,id 为 12 的订单放到编号为 50 的子表中,id 为 13 的订单放到编号为 61 的字表中。
Hash 路由设计的复杂点主要体现在初始表数量的选取上,表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。而用了 Hash 路由后,增加子表数量是非常麻烦的,所有数据都要重分布。Hash 路由的优缺点和范围路由基本相反,Hash 路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布。
同样以订单 id 为例,假如我们一开始就规划了 4 个数据库表,路由算法可以简单地用 id % 4 的值来表示数据所属的数据库表编号,id 为 12 的订单放到编号为 50 的子表中,id 为 13 的订单放到编号为 61 的字表中。
Hash 路由设计的复杂点主要体现在初始表数量的选取上,表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。而用了 Hash 路由后,增加子表数量是非常麻烦的,所有数据都要重分布。Hash 路由的优缺点和范围路由基本相反,Hash 路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布。
配置路由:配置路由就是路由表,用一张独立的表来记录路由信息。同样以订单 id 为例,我们新增一张 order_router 表,这个表包含 orderjd 和 tablejd 两列 , 根据 orderjd 就可以查询对应的 table_id。
配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。
配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大(例如,几亿条数据) ,性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则又面临一个死循环式的路由算法选择问题。
配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。
配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大(例如,几亿条数据) ,性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则又面临一个死循环式的路由算法选择问题。
不停机扩容怎么实现
第一阶段:在线双写,查询走老库
建立好新的库表结构,数据写入久库的同时,也写入拆分的新库
数据迁移,使用数据迁移程序,将旧库中的历史数据迁移到新库
使用定时任务,新旧库的数据对比,把差异补齐
建立好新的库表结构,数据写入久库的同时,也写入拆分的新库
数据迁移,使用数据迁移程序,将旧库中的历史数据迁移到新库
使用定时任务,新旧库的数据对比,把差异补齐
第二阶段:在线双写,查询走新库
完成了历史数据的同步和校验
把对数据的读切换到新库
完成了历史数据的同步和校验
把对数据的读切换到新库
第三阶段:旧库下线
旧库不再写入新的数据
经过一段时间,确定旧库没有请求之后,就可以下线老库
旧库不再写入新的数据
经过一段时间,确定旧库没有请求之后,就可以下线老库
常用的分库分表中间件有哪些?
sharding-jdbc
Mycat
Mycat
那你觉得分库分表会带来什么问题呢?
从分库的角度来讲:
(1)事务的问题
使用关系型数据库,有很大一点在于它保证事务完整性。
而分库之后单机事务就用不上了,必须使用分布式事务来解决。
(2)跨库 JOIN 问题
在一个库中的时候我们还可以利用 JOIN 来连表查询,而跨库了之后就无法使用 JOIN 了。
此时的解决方案就是在业务代码中进行关联,也就是先把一个表的数据查出来,然后通过得到的结果再去查另一张表,然后利用代码来关联得到最终的结果。
这种方式实现起来稍微比较复杂,不过也是可以接受的。
还有可以适当的冗余一些字段。比如以前的表就存储一个关联 ID,但是业务时常要求返回对应的 Name 或者其他字段。这时候就可以把这些字段冗余到当前表中,来去除需要关联的操作。
还有一种方式就是数据异构,通过 binlog 同步等方式,把需要跨库 join 的数据异构到 ES 等存储结构中,通过 ES 进行查询。
(1)事务的问题
使用关系型数据库,有很大一点在于它保证事务完整性。
而分库之后单机事务就用不上了,必须使用分布式事务来解决。
(2)跨库 JOIN 问题
在一个库中的时候我们还可以利用 JOIN 来连表查询,而跨库了之后就无法使用 JOIN 了。
此时的解决方案就是在业务代码中进行关联,也就是先把一个表的数据查出来,然后通过得到的结果再去查另一张表,然后利用代码来关联得到最终的结果。
这种方式实现起来稍微比较复杂,不过也是可以接受的。
还有可以适当的冗余一些字段。比如以前的表就存储一个关联 ID,但是业务时常要求返回对应的 Name 或者其他字段。这时候就可以把这些字段冗余到当前表中,来去除需要关联的操作。
还有一种方式就是数据异构,通过 binlog 同步等方式,把需要跨库 join 的数据异构到 ES 等存储结构中,通过 ES 进行查询。
从分表的角度来看:
(1)跨节点的 count,order by,group by 以及聚合函数问题
只能由业务代码来实现或者用中间件将各表中的数据汇总、排序、分页然后返回。
(2)数据迁移,容量规划,扩容等问题
数据的迁移,容量如何规划,未来是否可能再次需要扩容,等等,都是需要考虑的问题。
(3)ID 问题
数据库表被切分后,不能再依赖数据库自身的主键生成机制,所以需要一些手段来保证全局主键唯一。
1. 还是自增,只不过自增步长设置一下。比如现在有三张表,步长设置为 3,三张表 ID 初始值分别是 1、2、3。这样第一张表的 ID 增长是 1、4、7。第二张表是 2、5、8。第三张表是 3、6、9,这样就不会重复了。
2. UUID,这种最简单,但是不连续的主键插入会导致严重的页分裂,性能比较差。
3. 分布式 ID,比较出名的就是 Twitter 开源的 sonwflake 雪花算法
(1)跨节点的 count,order by,group by 以及聚合函数问题
只能由业务代码来实现或者用中间件将各表中的数据汇总、排序、分页然后返回。
(2)数据迁移,容量规划,扩容等问题
数据的迁移,容量如何规划,未来是否可能再次需要扩容,等等,都是需要考虑的问题。
(3)ID 问题
数据库表被切分后,不能再依赖数据库自身的主键生成机制,所以需要一些手段来保证全局主键唯一。
1. 还是自增,只不过自增步长设置一下。比如现在有三张表,步长设置为 3,三张表 ID 初始值分别是 1、2、3。这样第一张表的 ID 增长是 1、4、7。第二张表是 2、5、8。第三张表是 3、6、9,这样就不会重复了。
2. UUID,这种最简单,但是不连续的主键插入会导致严重的页分裂,性能比较差。
3. 分布式 ID,比较出名的就是 Twitter 开源的 sonwflake 雪花算法
运维
百万级别以上的数据如何删除?
当线上的数据库数据量到达几百万、上千万的时候,加一个字段就没那么简单,因为可能会长时间锁表。
大表添加字段,通常有这些做法:
(1)通过中间表转换过去
创建一个临时的新表,把旧表的结构完全复制过去,添加字段,再把旧表数据复制过去,删除旧表,新表命名为旧表的名称,这种方式可能回丢掉一些数据。
(2)用 pt-online-schema-change
pt-online-schema-change是 percona 公司开发的一个工具,它可以在线修改表结构,它的原理也是通过中间表。
(3)先在从库添加 再进行主从切换
如果一张表数据量大且是热表(读写特别频繁),则可以考虑先在从库添加,再进行主从切换,切换后再将其他几个节点上添加字段。
大表添加字段,通常有这些做法:
(1)通过中间表转换过去
创建一个临时的新表,把旧表的结构完全复制过去,添加字段,再把旧表数据复制过去,删除旧表,新表命名为旧表的名称,这种方式可能回丢掉一些数据。
(2)用 pt-online-schema-change
pt-online-schema-change是 percona 公司开发的一个工具,它可以在线修改表结构,它的原理也是通过中间表。
(3)先在从库添加 再进行主从切换
如果一张表数据量大且是热表(读写特别频繁),则可以考虑先在从库添加,再进行主从切换,切换后再将其他几个节点上添加字段。
MySQL 数据库 cpu 飙升的话,要怎么处理呢?
其他情况:
也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等
排查过程:
(1)使用 top 命令观察,确定是 mysqld 导致还是其他原因。
(2)如果是 mysqld 导致的,show processlist,查看 session 情况,确定是不是有消耗资源的 sql 在运行。
(3)找出消耗高的 sql,看看执行计划是否准确, 索引是否缺失,数据量是否太大。
(1)使用 top 命令观察,确定是 mysqld 导致还是其他原因。
(2)如果是 mysqld 导致的,show processlist,查看 session 情况,确定是不是有消耗资源的 sql 在运行。
(3)找出消耗高的 sql,看看执行计划是否准确, 索引是否缺失,数据量是否太大。
处理:
(1)kill 掉这些线程 (同时观察 cpu 使用率是否下降),
(2)进行相应的调整 (比如说加索引、改 sql、改内存参数)
(3)重新跑这些 SQL。
(1)kill 掉这些线程 (同时观察 cpu 使用率是否下降),
(2)进行相应的调整 (比如说加索引、改 sql、改内存参数)
(3)重新跑这些 SQL。
其他情况:
也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等
Redis
1. 缓存更新策略有哪些?
什么是redis? 它主要是用来做什么的
Redis是一个基于Key-Value存储结构的Nosql开源内存数据库。
它提供了5种常用的数据类型,String、Map、Set、ZSet、List。
针对不同的结构,可以解决不同场景的问题。
因此它可以覆盖应用开发中大部分的业务场景,比如
1. 缓存:Redis最常用于作为缓存层,可以用来缓存数据库查询结果或其他计算密集型操作的中间结果。因为Redis将数据存储在内存中,读取速度非常快,可以显著减轻后端数据库的负担,提高应用性能。
2. 会话管理:Redis可以用来管理用户会话,存储用户登录状态、购物车数据和其他需要跨请求保持的信息。它比传统的基于文件或数据库的会话管理方式更高效。
3. 发布/订阅系统:Redis支持发布/订阅模式,允许应用程序发布信息给多个订阅者。这在构建实时消息传递系统和通知系统中非常有用。
4. 计数器:Redis可以用来创建计数器,例如网站的访问计数器、点赞计数器等。
5. 地理空间索引:Redis支持存储地理空间信息,可以用来构建位置相关的应用,如附近的人、地点搜索等。
6. 队列:Redis的列表数据结构可用于构建任务队列,支持异步任务处理。
7. 持久化:Redis提供了多种持久化选项,以确保数据在服务器重启后不会丢失。
8. 数据结构存储:Redis支持各种数据结构,如字符串、哈希、列表、集合、有序集合等。这使得它可以用于更复杂的应用,包括排行榜、消息队列等。
9. 分布式锁:Redis可以用于实现分布式锁,确保在分布式系统中的多个节点之间实现同步。
它提供了5种常用的数据类型,String、Map、Set、ZSet、List。
针对不同的结构,可以解决不同场景的问题。
因此它可以覆盖应用开发中大部分的业务场景,比如
1. 缓存:Redis最常用于作为缓存层,可以用来缓存数据库查询结果或其他计算密集型操作的中间结果。因为Redis将数据存储在内存中,读取速度非常快,可以显著减轻后端数据库的负担,提高应用性能。
2. 会话管理:Redis可以用来管理用户会话,存储用户登录状态、购物车数据和其他需要跨请求保持的信息。它比传统的基于文件或数据库的会话管理方式更高效。
3. 发布/订阅系统:Redis支持发布/订阅模式,允许应用程序发布信息给多个订阅者。这在构建实时消息传递系统和通知系统中非常有用。
4. 计数器:Redis可以用来创建计数器,例如网站的访问计数器、点赞计数器等。
5. 地理空间索引:Redis支持存储地理空间信息,可以用来构建位置相关的应用,如附近的人、地点搜索等。
6. 队列:Redis的列表数据结构可用于构建任务队列,支持异步任务处理。
7. 持久化:Redis提供了多种持久化选项,以确保数据在服务器重启后不会丢失。
8. 数据结构存储:Redis支持各种数据结构,如字符串、哈希、列表、集合、有序集合等。这使得它可以用于更复杂的应用,包括排行榜、消息队列等。
9. 分布式锁:Redis可以用于实现分布式锁,确保在分布式系统中的多个节点之间实现同步。
最后,作为企业级开发来说,它又提供了主从复制+哨兵、以及集群方式实现高可用在Redis集群里
面,通过hash槽的方式实现了数据分片,进一步提升了性能。
面,通过hash槽的方式实现了数据分片,进一步提升了性能。
说说redis的基本数据结构
String(字符串)
简介:String 是 Redis 最基础的数据结构类型,它是二进制安全的,可以存储图片
或者序列化的对象,值最大存储为 512M
简单使用举例: set key value、get key等
应用场景:共享 session、分布式锁,计数器、限流。
内部编码有 3 种,int(8字节长整型)/embstr(小于等于 39字节字符串)/
raw(大于 39个字节字符串)
C 语言的字符串是 char[]实现的,而 Redis 使用 SDS(simple dynamic
string) 封装,sds 源码如下:
struct sdshdr{
unsigned int len; // 标记buf的长度
unsigned int free; //标记buf中未使用的元素个数
char buf[]; // 存放元素的坑
}
Redis 为什么选择 SDS 结构,而 C 语言原生的 char[]不香吗?
举例其中一点,SDS 中,O(1)时间复杂度,就可以获取字符串长度;而 C 字
符串,需要遍历整个字符串,时间复杂度为 O(n)
或者序列化的对象,值最大存储为 512M
简单使用举例: set key value、get key等
应用场景:共享 session、分布式锁,计数器、限流。
内部编码有 3 种,int(8字节长整型)/embstr(小于等于 39字节字符串)/
raw(大于 39个字节字符串)
C 语言的字符串是 char[]实现的,而 Redis 使用 SDS(simple dynamic
string) 封装,sds 源码如下:
struct sdshdr{
unsigned int len; // 标记buf的长度
unsigned int free; //标记buf中未使用的元素个数
char buf[]; // 存放元素的坑
}
Redis 为什么选择 SDS 结构,而 C 语言原生的 char[]不香吗?
举例其中一点,SDS 中,O(1)时间复杂度,就可以获取字符串长度;而 C 字
符串,需要遍历整个字符串,时间复杂度为 O(n)
Hash(哈希)
简介:在 Redis 中,哈希类型是指 v(值)本身又是一个键值对(k-v)结构
简单使用举例:hset key field value、hget key field
内部编码:ziplist(压缩列表) 、hashtable(哈希表)
应用场景:缓存用户信息等。
注意点:如果开发使用 hgetall,哈希元素比较多的话,可能导致 Redis 阻塞,
可以使用 hscan。而如果只是获取部分 field,建议使用 hmget。
简单使用举例:hset key field value、hget key field
内部编码:ziplist(压缩列表) 、hashtable(哈希表)
应用场景:缓存用户信息等。
注意点:如果开发使用 hgetall,哈希元素比较多的话,可能导致 Redis 阻塞,
可以使用 hscan。而如果只是获取部分 field,建议使用 hmget。
解决哈希冲突链过长,进行rehash
rehash过程:
1、使用两个全局哈希表:hash1、hash2
2、开始只使用hash1、hash2没有分配空间
3、数据过多时,给hash2分配更多空间(2*hash1)
4、将hash1数据拷贝到hash2
5、释放hash1空间,等下一次rehash使用
渐进式rehash:
1、若一次性将值全部从hash1拷贝至hash2,会线程阻塞,无法服务其他请求,于是使用渐进式rehash
2、请求来时,将对应索引上的哈希链rehash到hash2上(惰性)
3、若没有请求也会定时执行渐进式rehash
rehash过程:
1、使用两个全局哈希表:hash1、hash2
2、开始只使用hash1、hash2没有分配空间
3、数据过多时,给hash2分配更多空间(2*hash1)
4、将hash1数据拷贝到hash2
5、释放hash1空间,等下一次rehash使用
渐进式rehash:
1、若一次性将值全部从hash1拷贝至hash2,会线程阻塞,无法服务其他请求,于是使用渐进式rehash
2、请求来时,将对应索引上的哈希链rehash到hash2上(惰性)
3、若没有请求也会定时执行渐进式rehash
List(列表)
简介:列表(list)类型是用来存储多个有序✁字符串,一个列表最多可以存储 2^32-1 个元素。
简单实用举例: lpush key value [value ...] 、lrange key start end
内部编码:ziplist(压缩列表)、linkedlist(链表)
应用场景: 消息队列,文章列表, 看懂 list 类型✁插入与弹出:
list 应用场景参考以下:
lpush+lpop=Stack(栈)
lpush+rpop=Queue(队列)
lpsh+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)
Set(集合)
简介:集合(set)类型也是用来保存多个字符串元素,但是不允许重复元素
简单使用举例:sadd key element [element ...]、smembers key
内部编码:intset(整数集合)、hashtable(哈希表)
注意点:smembers 和 lrange、hgetall 都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,可以使用 sscan 来完成。
应用场景: 用户标签,生成随机数抽奖、社交需求。
简单使用举例:sadd key element [element ...]、smembers key
内部编码:intset(整数集合)、hashtable(哈希表)
注意点:smembers 和 lrange、hgetall 都属于比较重的命令,如果元素过多存在阻塞Redis的可能性,可以使用 sscan 来完成。
应用场景: 用户标签,生成随机数抽奖、社交需求。
有序集合(zset)
简介:已排序的字符串集合,同时元素不能重复
简单格式举例:zadd key score member [score member ...],zrank key member
底层内部编码:ziplist(压缩列表)、skiplist(跳跃表)
应用场景:排行榜,社交需求(如用户点赞)。
简单格式举例:zadd key score member [score member ...],zrank key member
底层内部编码:ziplist(压缩列表)、skiplist(跳跃表)
应用场景:排行榜,社交需求(如用户点赞)。
Redis 的三种特殊数据类型
Geo:Redis3.2 推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作。
HyperLogLog:用来做基数统计算法的数据结构,如统计网站的 UV。
Bitmaps :用一个比特位来映射某个元素的状态,在 Redis 中,它的底层是基于字符串类型实现的 ,可以把 bitmaps 成作一个以比特位为单位的数组
Geo:Redis3.2 推出的,地理位置定位,用于存储地理位置信息,并对存储的信息进行操作。
HyperLogLog:用来做基数统计算法的数据结构,如统计网站的 UV。
Bitmaps :用一个比特位来映射某个元素的状态,在 Redis 中,它的底层是基于字符串类型实现的 ,可以把 bitmaps 成作一个以比特位为单位的数组
redis 为什么这么快
基于内存存储实现
我们都知道内存读写是比在磁盘快很多的,Redis 基于内存存储实现的数据库,
相对于数据存在磁盘的 MySQL 数据库,省去磁盘 I/O的消耗。
相对于数据存在磁盘的 MySQL 数据库,省去磁盘 I/O的消耗。
高效的数据结构
合理的数据编码
String:如果存储数字的话,是用 int 类型的编码;如果存储非数字,小于等于 39 字节的字符串,是 embstr;大于 39 个字节,则是 raw 编码。
List:如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节(默认),使用 ziplist 编码,否则使用 linkedlist 编码
Hash:哈希类型元素个数小于 512 个,所有值小于 64 字节的话,使用ziplist 编码,否则使用 hashtable 编码。
Set:如果集合中的元素都是整数且元素个数小于 512 个,使用 intset 编码,否则使用 hashtable 编码。
Zset:当有序集合的元素个数小于 128 个,每个元素的值小于 64 字节时,使用ziplist 编码,否则使用 skiplist(跳跃表)编码
List:如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节(默认),使用 ziplist 编码,否则使用 linkedlist 编码
Hash:哈希类型元素个数小于 512 个,所有值小于 64 字节的话,使用ziplist 编码,否则使用 hashtable 编码。
Set:如果集合中的元素都是整数且元素个数小于 512 个,使用 intset 编码,否则使用 hashtable 编码。
Zset:当有序集合的元素个数小于 128 个,每个元素的值小于 64 字节时,使用ziplist 编码,否则使用 skiplist(跳跃表)编码
合理的线程模型
多路 I/O 复用技术可以让单个线程高效的处理多个连接请求,而 Redis 使用epoll 作为 I/O 多路复用技术的实现。
并且,Redis 自身的事件处理模型将 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多的时间。
并且,Redis 自身的事件处理模型将 epoll 中的连接、读写、关闭都转换为事件,不在网络 I/O 上浪费过多的时间。
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
什么是IO多路复用
常见的IO多路复用机制的实现方式有: select 、poll、epoll
select 模型,它的基本原理是,采用轮询和遍历的方式。也就是说,在客户端操作服务器时,会创建三种文件描述符,简称FD。分别是writefds(写描述符)、readfds(读描述符)和 exceptfds(异常描述符)。
而select会阻塞监视这三种文件描述符,等有数据、可读、可写、出异常或超时都会返回;
返回后通过遍历fdset,也就是文件描述符的集合,来找到就绪的FD,然后,触发相应的IO操作。
优点:跨平台支持性好,几乎所有的平台上支持
它的缺点也很明显,由于select是采用轮询的方式进行全盘扫描,因此,随着FD数量增多而导致性能下降。
因此,每次调用select()方法,都需要把FD集合从用户态拷贝到内核态,并进行遍历。而操作系统对单个进程打开的FD数量是有限制的,一般默认是1024个。虽然,可以通过操作系统的宏定义FD_SETSIZE修改最大FD数量限制,但是,在IO吞吐量巨大的情况下,效率提升仍然有限。
而select会阻塞监视这三种文件描述符,等有数据、可读、可写、出异常或超时都会返回;
返回后通过遍历fdset,也就是文件描述符的集合,来找到就绪的FD,然后,触发相应的IO操作。
优点:跨平台支持性好,几乎所有的平台上支持
它的缺点也很明显,由于select是采用轮询的方式进行全盘扫描,因此,随着FD数量增多而导致性能下降。
因此,每次调用select()方法,都需要把FD集合从用户态拷贝到内核态,并进行遍历。而操作系统对单个进程打开的FD数量是有限制的,一般默认是1024个。虽然,可以通过操作系统的宏定义FD_SETSIZE修改最大FD数量限制,但是,在IO吞吐量巨大的情况下,效率提升仍然有限。
poll 模型的原理与select模型基本一致,也是采用轮询加遍历,唯一的区别就是 poll 采用链表
的方式来存储FD。
所以,它的优点点是没有最大FD的数量限制。
它的缺点和select一样,也是采用轮询方式全盘扫描,同样也会随着FD数量增多而导致性能下
降。
的方式来存储FD。
所以,它的优点点是没有最大FD的数量限制。
它的缺点和select一样,也是采用轮询方式全盘扫描,同样也会随着FD数量增多而导致性能下
降。
epoll模型
由于select和poll都会因为吞吐量增加而导致性能下降,因此,才出现了epoll模型。
epoll模型是采用时间通知机制来触发相关的IO操作。它没有FD个数限制,而且从用户态拷贝
到内核态只需要一次。它主要通过系统底层的函数来注册、激活FD,从而触发相关的 IO 操作,这样
大大提高了性能。主要是通过调用以下三个系统函数:
1、epoll_create()函数,在系统启动时,会在Linux内核里面申请一个B+树结构的文件系统,
然后,返回epoll对象,也是一个FD。
2、epoll_ctl()函数,每新建一个连接的时候,会同步更新epoll对象中的FD,并且绑定一个
callback回调函数。
3、epoll_wait()函数,轮询所有的callback集合,并触发对应的 IO 操作
由于select和poll都会因为吞吐量增加而导致性能下降,因此,才出现了epoll模型。
epoll模型是采用时间通知机制来触发相关的IO操作。它没有FD个数限制,而且从用户态拷贝
到内核态只需要一次。它主要通过系统底层的函数来注册、激活FD,从而触发相关的 IO 操作,这样
大大提高了性能。主要是通过调用以下三个系统函数:
1、epoll_create()函数,在系统启动时,会在Linux内核里面申请一个B+树结构的文件系统,
然后,返回epoll对象,也是一个FD。
2、epoll_ctl()函数,每新建一个连接的时候,会同步更新epoll对象中的FD,并且绑定一个
callback回调函数。
3、epoll_wait()函数,轮询所有的callback集合,并触发对应的 IO 操作
所以,epoll模型最大的优点是将轮询改成了回调,大大提高了CPU执行效率,也不会随FD数量
的增加而导致效率下降。当然,它也没有FD数量限制,也就是说,它能支持的FD上限是操作系统的最大文件句柄数。一般而言,1G 内存大概支持 10 万个句柄。分布式系统中常用的组件如Redis、
Nginx都是优先采用epoll模型。它的缺点是只能在Linux下工作。
的增加而导致效率下降。当然,它也没有FD数量限制,也就是说,它能支持的FD上限是操作系统的最大文件句柄数。一般而言,1G 内存大概支持 10 万个句柄。分布式系统中常用的组件如Redis、
Nginx都是优先采用epoll模型。它的缺点是只能在Linux下工作。
什么是热key问题? 如何解决热key问题
什么是热 Key 呢?在 Redis 中,我们把访问频率高的 key,称为热点key。
如果某一热点 key 的请求到服务器主机时,由于请求量特别大,可能会导致主机资源 不足,甚至宕机,从而影响正常的服务。
热点key是怎么产生的呢?主要原因有两个:
1. 用户消费的数据远大于生产的数据:秒杀,热点新闻,等读多写少的场景
2. 请求分片集中,超过单 Redi 服务器的性能,比如固定名称 key,Hash 落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点 Key 问题。
如果某一热点 key 的请求到服务器主机时,由于请求量特别大,可能会导致主机资源 不足,甚至宕机,从而影响正常的服务。
热点key是怎么产生的呢?主要原因有两个:
1. 用户消费的数据远大于生产的数据:秒杀,热点新闻,等读多写少的场景
2. 请求分片集中,超过单 Redi 服务器的性能,比如固定名称 key,Hash 落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点 Key 问题。
如何识别到热key?
凭经验判断哪些是热 Key;
客户端统计上报;
服务代理层上报
凭经验判断哪些是热 Key;
客户端统计上报;
服务代理层上报
如何解决热 key 问题?
Redis 集群扩容:增加分片副本,均衡读流量;
将热 key 分散到不同的服务器中;
使用二级缓存,即 JVM 本地缓存,减少 Redis 的读请求。
Redis 集群扩容:增加分片副本,均衡读流量;
将热 key 分散到不同的服务器中;
使用二级缓存,即 JVM 本地缓存,减少 Redis 的读请求。
2. Redis过期策略
定时过期
每个设置过期时间的 key 都需要创一个定时器,到过期时间就会立即对 key
进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量
的CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量
的CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
惰性过期
只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可
以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过
期 key 没有再次被访问,从而不会被清除,占用大量内存。
以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过
期 key 没有再次被访问,从而不会被清除,占用大量内存。
定期过期
每隔一定的时间,会扫描一定数量数据库的 expires 字典中一定数量的key,并清除其中已过期的key。
该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。
expires 字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value 是该键的毫秒精度是 UNIX 时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键。
该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。
expires 字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value 是该键的毫秒精度是 UNIX 时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键。
Redis 中同时使用了惰性过期和定期过期两种过期策略。
假设Redis 当前存放 30 万个 key,并且都设置了过期时间,如果你每隔 100ms
就去检查这全部的 key,CPU 负载会特别高,最后可能会挂掉。
因此,redis 采取的是定期过期,每隔 100ms 就随机抽取一定数量的key 来检查和删除。
但是呢,最后可能会有很多已经过期的key 没被删除。这时候,redis 采用惰性删除。
在你获取某个 key 的时候,redis 会检查一下,这个 key 如果设置了过期时间并且已经过期了,此时就会删除。
如果定期删除漏掉了很多过期的key,然后也没走惰性删除。就会有很多过期key 积在内存内存,直接会导致内存爆掉。
或者有些时候,业务量大起来了,redis 的key 被大量使用,内存直接不够了,运维小哥哥也忘记加大内存了。
难道 redis 直接这样挂掉?不会!
Redis 用 8 种内存淘汰策略保护
就去检查这全部的 key,CPU 负载会特别高,最后可能会挂掉。
因此,redis 采取的是定期过期,每隔 100ms 就随机抽取一定数量的key 来检查和删除。
但是呢,最后可能会有很多已经过期的key 没被删除。这时候,redis 采用惰性删除。
在你获取某个 key 的时候,redis 会检查一下,这个 key 如果设置了过期时间并且已经过期了,此时就会删除。
如果定期删除漏掉了很多过期的key,然后也没走惰性删除。就会有很多过期key 积在内存内存,直接会导致内存爆掉。
或者有些时候,业务量大起来了,redis 的key 被大量使用,内存直接不够了,运维小哥哥也忘记加大内存了。
难道 redis 直接这样挂掉?不会!
Redis 用 8 种内存淘汰策略保护
3. Redis内存淘汰策略
redis.conf中 有一行配置 # maxmemory-policy
noeviction : 当内存不足时,新写入的数据会报错(不推荐)
allkeys-lru : 当内存不足以容纳新写入数据时,在建空间中移除最近最少使用的key(推荐)
allkeys-random:当内存不足以写入新数据时,在键空间中随机移除某个key(不推荐)
volatile-lru:当内存不足时,在设置了过期时间的键空间中移除最近最少使用的key。(不推荐)
volatile-random:当内存不足时,在设置了过期时间的键空间中随机移除一个值(不推荐)
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中有更早过期时间的key优先移除(不推荐)
4. 缓存穿透/缓存雪崩/缓存击穿
缓存穿透
访问一个不存在的key,缓存不起作用,请求会穿透到DB,流量大时DB回挂掉
解决方案
采用布隆过滤器,使用一个趍够大的bitmap,用二存储可能访问的key,不存在的key直接被过滤;
访问key未在DB查询到值后,也将空值写到缓存,但可以设置较短期时间。
缓存雪崩
大量的key设置了相同的过期时间,导致缓存在同一时刻全部失效,造成瞬时DB请求量大,压力骤增,引起雪崩
解决方案
缓存过期时间加上个随机时间值
活动情况下,缓存预热
采用限流算法,限制流量
采用分布式锁,加锁访问
缓存击穿
缓存突然失效,并发大量的请求过来,导致数据量太大
解决方案
在缓存查询不到时,采用分布式锁获取数据,只让一个请求去获取数据写入缓存
其他请求,等待获取,并且设置获取次数阈值,防止异常场景导致服务器压力过大
说说redis几种常用的场景
缓存
不仅可以提升网站的访问速度,还可以
降低数据库 DB 的压力。并且,Redis 相比于 memcached,还提供了丰富的数据结构,并且提供 RDB 和 AOF 等持久化机制。
不仅可以提升网站的访问速度,还可以
降低数据库 DB 的压力。并且,Redis 相比于 memcached,还提供了丰富的数据结构,并且提供 RDB 和 AOF 等持久化机制。
排行榜
Redis 提供的 zset数据类型能够实现这些复杂的排行榜。
Redis 提供的 zset数据类型能够实现这些复杂的排行榜。
计数器应用
各大网站、APP 应用经常需要计数器的功能,如短视频的播放数、电商网站的浏览数。这些播放数、浏览数一般要求实时的,每一次播放和浏览都要做加1的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis 天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。
各大网站、APP 应用经常需要计数器的功能,如短视频的播放数、电商网站的浏览数。这些播放数、浏览数一般要求实时的,每一次播放和浏览都要做加1的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis 天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择。
共享Session
如果一个分布式 Web 服务将用户的 Session 信息保存在各自服务器,用户刷新一次可能就需要重新登录了,这样显然有问题。实际上,可以使用 Redis将用户的 Session 进行集中管理,每次用户更新或者查询登录信息都直接从Redis 中集中获取。
如果一个分布式 Web 服务将用户的 Session 信息保存在各自服务器,用户刷新一次可能就需要重新登录了,这样显然有问题。实际上,可以使用 Redis将用户的 Session 进行集中管理,每次用户更新或者查询登录信息都直接从Redis 中集中获取。
分布式锁
几乎每个互联网公司中都使用了分布式部署,分布式服务下,就会遇到对同一个资源的并发访问的技术难题,如秒杀、下单减库存等场景。
用 synchronize 或者reentrantlock 本地锁肯定是不行的。
如果是并发量不大话,使用数据库✁悲观锁、乐观锁来实现没啥问题。
但是在并发量高的场合中,利用数据库锁来控制资源的并发访问,会影响数据库的性能。
实际上,可以用 Redis 的setnx 来实现分布式锁。
几乎每个互联网公司中都使用了分布式部署,分布式服务下,就会遇到对同一个资源的并发访问的技术难题,如秒杀、下单减库存等场景。
用 synchronize 或者reentrantlock 本地锁肯定是不行的。
如果是并发量不大话,使用数据库✁悲观锁、乐观锁来实现没啥问题。
但是在并发量高的场合中,利用数据库锁来控制资源的并发访问,会影响数据库的性能。
实际上,可以用 Redis 的setnx 来实现分布式锁。
社交网络
赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适保存 这种类型的数据,Redis 提供的数据结构可以相对比较容易地实现这些功能。
赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适保存 这种类型的数据,Redis 提供的数据结构可以相对比较容易地实现这些功能。
消息队列
消息队列是大型网站必用中间件,如 ActiveMQ、RabbitMQ、Kafka 等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。
Redis 提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。
消息队列是大型网站必用中间件,如 ActiveMQ、RabbitMQ、Kafka 等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。
Redis 提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。
位操作
用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。腾讯 10 亿用户,要几个毫秒内查询到某个用户是否在线,能怎么做?这里要用到位操作——使用 setbit、getbit、bitcount 命令。原理是:redis 内构一个足够长的数组,每个数组元素只能是 0 和 1 两个值,然后这个数组的下标 index 用来表示用户id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标和元素值(0 和 1)来构成一个记忆系统。
用于数据量上亿的场景下,例如几亿用户系统的签到,去重登录次数统计,某用户是否在线状态等等。腾讯 10 亿用户,要几个毫秒内查询到某个用户是否在线,能怎么做?这里要用到位操作——使用 setbit、getbit、bitcount 命令。原理是:redis 内构一个足够长的数组,每个数组元素只能是 0 和 1 两个值,然后这个数组的下标 index 用来表示用户id(必须是数字哈),那么很显然,这个几亿长的大数组就能通过下标和元素值(0 和 1)来构成一个记忆系统。
redis持久化有哪几种方式?应该怎么选?
首先,Redis本身是一个基于Key-Value结构的内存数据库,为了避免Redis故障导致数据丢失的问题,所以提供了RDB和AOF两种持久化机制。
RDB
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。
RDB⽂件是⼀个压缩的⼆进制⽂件,通过它可以还原某个时刻数据库的状态。由于RDB⽂件是保存在硬盘上的,所以即使Redis崩溃或者退出,只要RDB⽂件存在,就可以⽤它来恢复还原数据库的状态。
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。
RDB⽂件是⼀个压缩的⼆进制⽂件,通过它可以还原某个时刻数据库的状态。由于RDB⽂件是保存在硬盘上的,所以即使Redis崩溃或者退出,只要RDB⽂件存在,就可以⽤它来恢复还原数据库的状态。
手动触发分别对应save和bgsave命令:
save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。
save命令:阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用。
bgsave命令:Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。
RDB | 优点
只有一个紧凑的二进制文件 dump.rdb,非常适合备份、全量复制的场景。
容灾性好,可以把RDB文件拷贝道远程机器或者文件系统张,用于容灾恢复。
恢复速度快,RDB恢复数据的速度远远快于AOF的方式
只有一个紧凑的二进制文件 dump.rdb,非常适合备份、全量复制的场景。
容灾性好,可以把RDB文件拷贝道远程机器或者文件系统张,用于容灾恢复。
恢复速度快,RDB恢复数据的速度远远快于AOF的方式
RDB | 缺点
实时性低,RDB 是间隔一段时间进行持久化,没法做到实时持久化/秒级持久化。如果在这一间隔事件发生故障,数据会丢失。
存在兼容问题,Redis演进过程存在多个格式的RDB版本,存在老版本Redis无法兼容新版本RDB的问题。
实时性低,RDB 是间隔一段时间进行持久化,没法做到实时持久化/秒级持久化。如果在这一间隔事件发生故障,数据会丢失。
存在兼容问题,Redis演进过程存在多个格式的RDB版本,存在老版本Redis无法兼容新版本RDB的问题。
以下场景会自动触发RDB持久化:
使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点
执行debug reload命令重新加载Redis时,也会自动触发save操作
默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。
使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点
执行debug reload命令重新加载Redis时,也会自动触发save操作
默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。
AOF
AOF(append only file)持久化:以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。
AOF的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load)
AOF(append only file)持久化:以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,目前已经是Redis持久化的主流方式。
AOF的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load)
流程如下:
1)所有的写入命令会追加到aof_buf(缓冲区)中。
2)AOF缓冲区根据对应的策略向硬盘做同步操作。
3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩 的目的。
4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。
1)所有的写入命令会追加到aof_buf(缓冲区)中。
2)AOF缓冲区根据对应的策略向硬盘做同步操作。
3)随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩 的目的。
4)当Redis服务器重启时,可以加载AOF文件进行数据恢复。
AOF | 优点
实时性好,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。
通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
实时性好,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。
通过 append 模式写文件,即使中途服务器宕机,可以通过 redis-check-aof 工具解决数据一致性问题。
AOF | 缺点
AOF 文件比 RDB 文件大,且 恢复速度慢。
数据集大 的时候,比 RDB 启动效率低。
AOF 文件比 RDB 文件大,且 恢复速度慢。
数据集大 的时候,比 RDB 启动效率低。
RDB和AOF如何选择?
(1)一般来说, 如果想达到足以媲美数据库的 数据安全性,应该 同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
(2)如果 可以接受数分钟以内的数据丢失,那么可以 只使用 RDB 持久化。
(3)有很多用户都只使用 AOF 持久化,但并不推荐这种方式,因为定时生成 RDB 快照(snapshot)非常便于进行数据备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此之外,使用 RDB 还可以避免 AOF 程序的 bug。
(4)如果只需要数据在服务器运行的时候存在,也可以不使用任何持久化方式。
(1)一般来说, 如果想达到足以媲美数据库的 数据安全性,应该 同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
(2)如果 可以接受数分钟以内的数据丢失,那么可以 只使用 RDB 持久化。
(3)有很多用户都只使用 AOF 持久化,但并不推荐这种方式,因为定时生成 RDB 快照(snapshot)非常便于进行数据备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此之外,使用 RDB 还可以避免 AOF 程序的 bug。
(4)如果只需要数据在服务器运行的时候存在,也可以不使用任何持久化方式。
怎么实现redis的高可用
为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不同的服务器上,其中一台挂了也可以继续提供服务。 Redis 实现高可用有三种部署模式:主从模式,哨兵模式,集群模式。
主从模式
Redis 高可用回答包括两个层面,一个就是数据不能丢失,或者说尽量减少丢失;另外一个就是保证 Redis 服务不中断。
1. 对于尽量减少数据丢失,可以通过 AOF 和 RDB 保证。
2.对于保证服务不中断的话,Redis 就不能单点部署,这时候我们先看下 Redis 主从。
Redis 主从同步过程
Redis 主从同步包括三个阶段。
第一阶段:主从库间建立连接、协商同步。
从库向主库发送 psync 命令,告诉它要进行数据同步。
主库收到 psync 命令后,响应 FULLRESYNC 命令(它表示第一次复制采用的是全量复制),并带上主库 runID 和主库目前的复制进度 offset。
第二阶段:主库把数据同步到从库,从库收到数据后,完成本地加载。
主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。
主库把数据同步到从库的过程中,新来的写操作,会记录到 replication buffer。
第三阶段,主库把新写的命令,发送到从库。
主库完成 RDB 发送后,会把 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样主从库就实现同步啦。
Redis 高可用回答包括两个层面,一个就是数据不能丢失,或者说尽量减少丢失;另外一个就是保证 Redis 服务不中断。
1. 对于尽量减少数据丢失,可以通过 AOF 和 RDB 保证。
2.对于保证服务不中断的话,Redis 就不能单点部署,这时候我们先看下 Redis 主从。
Redsi 主从概念
Redis 主从模式,就是部署多台 Redis 服务器,有主库和从库,它们之间通过主
从复制,以保证数据副本是一致的。
主从库之间采用的是读写分离✁方式,其中主库负责读操作和写操作,从库则负责
读操作。
如果 Redis 主库挂了,切换其中的从库成为主库。
Redis 主从模式,就是部署多台 Redis 服务器,有主库和从库,它们之间通过主
从复制,以保证数据副本是一致的。
主从库之间采用的是读写分离✁方式,其中主库负责读操作和写操作,从库则负责
读操作。
如果 Redis 主库挂了,切换其中的从库成为主库。
Redis 主从同步过程
Redis 主从同步包括三个阶段。
第一阶段:主从库间建立连接、协商同步。
从库向主库发送 psync 命令,告诉它要进行数据同步。
主库收到 psync 命令后,响应 FULLRESYNC 命令(它表示第一次复制采用的是全量复制),并带上主库 runID 和主库目前的复制进度 offset。
第二阶段:主库把数据同步到从库,从库收到数据后,完成本地加载。
主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。
主库把数据同步到从库的过程中,新来的写操作,会记录到 replication buffer。
第三阶段,主库把新写的命令,发送到从库。
主库完成 RDB 发送后,会把 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样主从库就实现同步啦。
注意的点:
(1)主从数据不一致
因为主从复制是异步进行的,如果从库滞后执行,则会导致主从数据不一致。
主从数据不一致一般有两个原因:
· 主从库网路延迟。
· 从库收到了主从命令,但是它正在执行阻塞性的命令(如 hgetall等)。
(2)一主多从,全量复制时主库压力问题
如果是一主多从模式,从库很多的时候,如果每个从库都要和主库进行全量复制的话,主库的压力是很大的。
因为主库 fork 进程生成 RDB,这个 fork的过程是会阻塞主线程处理正常请求。
同时,传输大的RDB 文件也会占用主库的网络宽带。可以使用主-从-从模式解决。
什么是主从从模式呢?
其实就是部署主从集群时,
选择硬件网络配置比较好的一个从库,让它跟部分从库再建立主从关系。
(3)主从网络断了怎么办?
当主从库断开连接后,主库会把断连期间收到读写操作命令,写入 replication buffer,同时也会把这些操作命令写入repl_backlog_buffer
这个缓冲区。repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。
主从库重连后,就是利用 repl_backlog_buffer 实现增量复制。
(1)主从数据不一致
因为主从复制是异步进行的,如果从库滞后执行,则会导致主从数据不一致。
主从数据不一致一般有两个原因:
· 主从库网路延迟。
· 从库收到了主从命令,但是它正在执行阻塞性的命令(如 hgetall等)。
(2)一主多从,全量复制时主库压力问题
如果是一主多从模式,从库很多的时候,如果每个从库都要和主库进行全量复制的话,主库的压力是很大的。
因为主库 fork 进程生成 RDB,这个 fork的过程是会阻塞主线程处理正常请求。
同时,传输大的RDB 文件也会占用主库的网络宽带。可以使用主-从-从模式解决。
什么是主从从模式呢?
其实就是部署主从集群时,
选择硬件网络配置比较好的一个从库,让它跟部分从库再建立主从关系。
(3)主从网络断了怎么办?
当主从库断开连接后,主库会把断连期间收到读写操作命令,写入 replication buffer,同时也会把这些操作命令写入repl_backlog_buffer
这个缓冲区。repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。
主从库重连后,就是利用 repl_backlog_buffer 实现增量复制。
如何解决主从不一致的问题:
1. 换更好的硬件配置,保证网络畅通
2.监控主从库间的复制进度
1. 换更好的硬件配置,保证网络畅通
2.监控主从库间的复制进度
如何保证主从数据一致性?
Redis主从数据一致性,主要是讲Redis主从数据是怎么同步的,主要有3种,分别是全量同步、增量同步、指令同步。每种同步方式的时机不一样。
**全量同步**
1.全量同步,一般发生在第一次建立主从关系、或者跟主断开时间比较久的场景。
它的步骤为:
1.slave向master发起同步指令,master收到以后,会通过bgsave指令生成一个快照RDB文件,同步给slave,slave拿到后,会通过master同步的快照文件进行加载。这个时候,主生成rdb文件时候的所有数据都以及同步给了slave。
2.但是bgsave指令是不会阻塞其他指令执行的,所以master在生成快照文件时,还是能接收新的指令执行。这些指令master会先保存到一个叫replication_buffer的内存区间,等slave加载完快照文件后会同步。
面试官:
那为什么还需要增量同步呢?
求职者:
数据同步,slave是定时会发起的,假如每次同步,都把主的所有数据都进行同步,那么性能会很慢,大部分时候,slave可能只跟master相差一部分数据。那么只需要同步这部分数据。slave发起同步的时候,还会带有上次同步的偏移量,然后跟master的最新的偏移量比较,如果相差的数据在master的积压缓存(一个专门存储master最新数据并且会覆盖的内存区间)能查询到的话,那么只需要把相差的数据同步给slave。这就叫做增量同步。
指令同步:master输入的指令会异步同步给slave。
Redis主从数据一致性,主要是讲Redis主从数据是怎么同步的,主要有3种,分别是全量同步、增量同步、指令同步。每种同步方式的时机不一样。
**全量同步**
1.全量同步,一般发生在第一次建立主从关系、或者跟主断开时间比较久的场景。
它的步骤为:
1.slave向master发起同步指令,master收到以后,会通过bgsave指令生成一个快照RDB文件,同步给slave,slave拿到后,会通过master同步的快照文件进行加载。这个时候,主生成rdb文件时候的所有数据都以及同步给了slave。
2.但是bgsave指令是不会阻塞其他指令执行的,所以master在生成快照文件时,还是能接收新的指令执行。这些指令master会先保存到一个叫replication_buffer的内存区间,等slave加载完快照文件后会同步。
面试官:
那为什么还需要增量同步呢?
求职者:
数据同步,slave是定时会发起的,假如每次同步,都把主的所有数据都进行同步,那么性能会很慢,大部分时候,slave可能只跟master相差一部分数据。那么只需要同步这部分数据。slave发起同步的时候,还会带有上次同步的偏移量,然后跟master的最新的偏移量比较,如果相差的数据在master的积压缓存(一个专门存储master最新数据并且会覆盖的内存区间)能查询到的话,那么只需要把相差的数据同步给slave。这就叫做增量同步。
指令同步:master输入的指令会异步同步给slave。
哨兵模式
哨兵模式的作用
哨兵其实是一个运行在特殊模式下的Redis 进程。它有三个作用,分别是:
监控、自动选主切换(简称选主)、通知。
哨兵进程在运行期间,监视所有的Redis 主节点和从节点。
它通过周期性给主从库发送 PING命令,检测主从库是否挂了。
如果从库没有在规定时间内响应哨兵的PING命令,哨兵就会把它标记为下线状态;
如果主库没有在规定时间内响应哨兵的PING命令,哨兵则会判定主库下线,然后开始切换到选主任务。
所谓选主,其实就是从多个从库中,按照一定规则,选出一个当做主库。
至于通知呢,就是选出主库后,哨兵把新主库的连接信息发给其他从库,让它们和新主库建立主从关系。
同时,哨兵也会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。
监控、自动选主切换(简称选主)、通知。
哨兵进程在运行期间,监视所有的Redis 主节点和从节点。
它通过周期性给主从库发送 PING命令,检测主从库是否挂了。
如果从库没有在规定时间内响应哨兵的PING命令,哨兵就会把它标记为下线状态;
如果主库没有在规定时间内响应哨兵的PING命令,哨兵则会判定主库下线,然后开始切换到选主任务。
所谓选主,其实就是从多个从库中,按照一定规则,选出一个当做主库。
至于通知呢,就是选出主库后,哨兵把新主库的连接信息发给其他从库,让它们和新主库建立主从关系。
同时,哨兵也会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。
哨兵模式简介
因为Redis 哨兵也是一个 Redis 进程,如果它自己挂了呢,那是不是就起不了监控的作用啦。
我们一起来看下 Redis 哨兵模式
哨兵模式,就是由一个或多个哨兵实例组成的哨兵系统,它可以监视所有的Redis 主节点和从节点,并在被监视的主节点进入下线状态时,自动将下线主服务器属下的某个从节点升级为新的主节点,一个哨兵进程对 Redis 节点进行监控,就可能会出现问题(单点问题)。
因此,一般使用多个哨兵来进行监控 Redis 节点,并且各个哨兵之间还会进行监控。
其实哨兵之间是通过发布订阅机制组成集群的,同时,哨兵又通过 INFO命令,获得了从库连接信息,也能和从库建立连接,从而进行监控。
我们一起来看下 Redis 哨兵模式
哨兵模式,就是由一个或多个哨兵实例组成的哨兵系统,它可以监视所有的Redis 主节点和从节点,并在被监视的主节点进入下线状态时,自动将下线主服务器属下的某个从节点升级为新的主节点,一个哨兵进程对 Redis 节点进行监控,就可能会出现问题(单点问题)。
因此,一般使用多个哨兵来进行监控 Redis 节点,并且各个哨兵之间还会进行监控。
其实哨兵之间是通过发布订阅机制组成集群的,同时,哨兵又通过 INFO命令,获得了从库连接信息,也能和从库建立连接,从而进行监控。
哨兵如何判定主库下线
哨兵是如何判断主库是否下线的呢?
我们先来了解两个基础概念哈:主观下线和客观下线。
哨兵进程向主库、从库发送 PING 命令,如果主库或者从库没有在规定的时间内响应 PING 命令,哨兵就把它标记为主观下线。
如果是主库被标记为主观下线,则正在监视这个主库的所有哨兵要以每秒一次的频率,以确认主库是否真正进入了主观下线。
当有多数的哨兵(一般少数服从多数,由 Redis 管理员自行设定的一个值)在指定的时间范围内确认主库的确进入了主观下线状态,则主库会被标记为客观下线。这样做的目的就是避免对主库的误判,以减少没有必要的主从切换,减少不必要的开销。
假设我们有 N 个哨兵实例,如果有 N/2+1 个实例判断主库主观下线,此时就可以把节点标记为客观下线,就可以做主从切换了。
我们先来了解两个基础概念哈:主观下线和客观下线。
哨兵进程向主库、从库发送 PING 命令,如果主库或者从库没有在规定的时间内响应 PING 命令,哨兵就把它标记为主观下线。
如果是主库被标记为主观下线,则正在监视这个主库的所有哨兵要以每秒一次的频率,以确认主库是否真正进入了主观下线。
当有多数的哨兵(一般少数服从多数,由 Redis 管理员自行设定的一个值)在指定的时间范围内确认主库的确进入了主观下线状态,则主库会被标记为客观下线。这样做的目的就是避免对主库的误判,以减少没有必要的主从切换,减少不必要的开销。
假设我们有 N 个哨兵实例,如果有 N/2+1 个实例判断主库主观下线,此时就可以把节点标记为客观下线,就可以做主从切换了。
哨兵模式如何工作
1. 每个哨兵以每秒钟一次的频率向它所知的主库、从库以及其他哨兵实例发送一个PING命令。
2. 如果一个实例节点距离最后一次有效回复 PING命令✁时间超过 down-after- milliseconds选项所指定✁值, 则这个实例会被哨兵标记为主观下线。
3. 如果主库被标记为主观下线,则正在监视这个主库的所有哨兵要以每秒一次的频率确认主库的确进入了主观下线状态。
4. 当有足够数量的哨兵(大于等于配置文件指定的值)在指定✁时间范围内确认主库的确进入了主观下线状态, 则主库会被标记为客观下线。
5. 当主库被哨兵标记为客观下线时,就会进入选主模式。
6. 若没有足够数量的哨兵同意主库已经进入主观下线, 主库的主观下线状态就会被移除;若主库重新向哨兵的 PING命令返回有效回复,主库的主观下线状态就会被移除。
2. 如果一个实例节点距离最后一次有效回复 PING命令✁时间超过 down-after- milliseconds选项所指定✁值, 则这个实例会被哨兵标记为主观下线。
3. 如果主库被标记为主观下线,则正在监视这个主库的所有哨兵要以每秒一次的频率确认主库的确进入了主观下线状态。
4. 当有足够数量的哨兵(大于等于配置文件指定的值)在指定✁时间范围内确认主库的确进入了主观下线状态, 则主库会被标记为客观下线。
5. 当主库被哨兵标记为客观下线时,就会进入选主模式。
6. 若没有足够数量的哨兵同意主库已经进入主观下线, 主库的主观下线状态就会被移除;若主库重新向哨兵的 PING命令返回有效回复,主库的主观下线状态就会被移除。
哨兵是如何选主的
哨兵选主包括两大过程,分别是:过滤和打分。其实就是在多个从库中,先按
照一定的筛选条件,把不符合条件的从库过滤掉。然后再按照一定的规则,给
剩下的从库逐个打分,将得分最高的从库选为新主库。
选主时,会判断从库的状态,如果已经下线,就直接过滤。
如果从库网络不好,老是超时,也会被过滤掉。看这个参数 down-after- milliseconds,它表示我们认定主从库断连的最大连接超时时间。
过滤掉了不适合做主库的从库后,就可以给剩下的从库打分,按这三个规则打分:
(1)从库优先级、从库复制进度以及从库 ID 号。
(2)从库优先级最高的话,打分就越高,优先级可以通过 slave-priority配置。
(3)如果优先级一样,就选与旧的主库复制进度最快的从库。如果优先级和从库进度都一样,从库ID 号小的打分高。
照一定的筛选条件,把不符合条件的从库过滤掉。然后再按照一定的规则,给
剩下的从库逐个打分,将得分最高的从库选为新主库。
选主时,会判断从库的状态,如果已经下线,就直接过滤。
如果从库网络不好,老是超时,也会被过滤掉。看这个参数 down-after- milliseconds,它表示我们认定主从库断连的最大连接超时时间。
过滤掉了不适合做主库的从库后,就可以给剩下的从库打分,按这三个规则打分:
(1)从库优先级、从库复制进度以及从库 ID 号。
(2)从库优先级最高的话,打分就越高,优先级可以通过 slave-priority配置。
(3)如果优先级一样,就选与旧的主库复制进度最快的从库。如果优先级和从库进度都一样,从库ID 号小的打分高。
由哪个哨兵执行主从切换呢?
一个哨兵标记主库为主观下线后,它会征求其他哨兵的意见,确认主库是否的确进入了主观下线状态。它向其他实例哨兵发送 is-master-down-by-addr命令。其他哨兵会根据自己和主库的连接情况,回应 Y或 N(Y 表示赞成,N 表示反对票)。如果这个哨兵获取得足够多的赞成票数(quorum配置),主库会被标记为客观下线。
标记主库客观下线的这个哨兵,紧接着向其他哨兵发送命令,再发起投票,希望它可以来执行主从切换。这个投票过程称为 Leader 选举。因为最终执行主
从切换的哨兵称为 Leader,投票过程就是确定 Leader。一个哨兵想成为Leader 需要满足两个条件:
(1)需要拿到 num(sentinels)/2+1的赞成票。
(2)并且拿到的票数需要大于等于哨兵配置文件中的 quorum值。
标记主库客观下线的这个哨兵,紧接着向其他哨兵发送命令,再发起投票,希望它可以来执行主从切换。这个投票过程称为 Leader 选举。因为最终执行主
从切换的哨兵称为 Leader,投票过程就是确定 Leader。一个哨兵想成为Leader 需要满足两个条件:
(1)需要拿到 num(sentinels)/2+1的赞成票。
(2)并且拿到的票数需要大于等于哨兵配置文件中的 quorum值。
哨兵下线故障转移
假设哨兵模式架构如下,有三个哨兵,一个主库 M,两个从库 S1 和 S2。
当哨兵检测到 Redis 主库 M1 出现故障,那么哨兵需要对集群进行故障转移。
假设选出了哨兵 3 作为 Leader。故障转移流程如下:
1. 从库 S1 解除从节点身份,升级为新主库
2. 从库 S2 成为新主库的从库
3. 原主节点恢复也变成新主库的从节点
4. 通知客户端应用程序新主节点的地址。
当哨兵检测到 Redis 主库 M1 出现故障,那么哨兵需要对集群进行故障转移。
假设选出了哨兵 3 作为 Leader。故障转移流程如下:
1. 从库 S1 解除从节点身份,升级为新主库
2. 从库 S2 成为新主库的从库
3. 原主节点恢复也变成新主库的从节点
4. 通知客户端应用程序新主节点的地址。
Redis Cluster 集群
概念
哨兵模式基于主从模式,实现读写分离,它还可以自动切换,系统可用性更高。但是它每个节点存储的数据是一样的,浪费内存,并且不好在线扩容。
因此,Reids Cluster 集群(切片集群的实现方案)应运而生,它在 Redis3.0 加入,实现了 Redis 的分布式存储。对数据进行分片,也就是说每台 Redis 节点上存储不同的内容,来解决在线扩容的问题。并且,它可以保存大量数据,即分散数据到各个 Redis 实例,还提供复制和故障转移的功能。
因此,Reids Cluster 集群(切片集群的实现方案)应运而生,它在 Redis3.0 加入,实现了 Redis 的分布式存储。对数据进行分片,也就是说每台 Redis 节点上存储不同的内容,来解决在线扩容的问题。并且,它可以保存大量数据,即分散数据到各个 Redis 实例,还提供复制和故障转移的功能。
比如你一个 Redis 实例保存 15G 甚至更大的数据,响应就会很慢,这是因为Redis RDB 持久化机制导致的,Redis 会 fork 子进程完成 RDB 持久化操作,
fork 执行的耗时与 Redis 数据量成正相关。这时候你很容易想到,把 15G 数据分散来存储就好了嘛。这就是 Redis 切片集群的初衷。切片集群是啥呢?来看个例子,如果你要用 Redis 保存 15G 的数据,可以用单实例 Redis,或者 3 台 Redis 实例组成切片集群,对比如下:
fork 执行的耗时与 Redis 数据量成正相关。这时候你很容易想到,把 15G 数据分散来存储就好了嘛。这就是 Redis 切片集群的初衷。切片集群是啥呢?来看个例子,如果你要用 Redis 保存 15G 的数据,可以用单实例 Redis,或者 3 台 Redis 实例组成切片集群,对比如下:
切片集群和 Redis Cluster 的区别:Redis Cluster 是从 Redis3.0 版本开始,官方提供的一种实现切片集群的方案。
既然数据是分片分布到不同 Redis 实例的,那客户端到底是怎么确定想要访问的数据在哪个实例上呢?我们一起来看下 Reids Cluster 是怎么做的哈。
既然数据是分片分布到不同 Redis 实例的,那客户端到底是怎么确定想要访问的数据在哪个实例上呢?我们一起来看下 Reids Cluster 是怎么做的哈。
哈希槽(Hash Slot)
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和实例之间的映射关系。
一个切片集群被分为 16384个 slot(槽),每个进入 Redis 的键值对,根据key 进行散列,分配到这 16384 插槽中的一个。
使用的哈希映射也比较简单,用CRC16算法计算出一个 16bit的值,再对 16384取模。数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点都可以处理这 16384 个槽。集群中的每个节点负责一部分的哈希槽,假设当前集群有 A、B、C3 个节点,每个节点上负责的哈希槽数 =16384/3,那么可能存在的一种分配:
节点A 负责 0~5460 号哈希槽
节点B 负责 5461~10922 号哈希槽
节点C 负责 10923~16383 号哈希槽
客户端给一个 Redis 实例发送数据读写操作时,如果这个实例上并没有相应的数据,会怎么样呢?MOVED 重定向和 ASK 重定向了解一下哈
一个切片集群被分为 16384个 slot(槽),每个进入 Redis 的键值对,根据key 进行散列,分配到这 16384 插槽中的一个。
使用的哈希映射也比较简单,用CRC16算法计算出一个 16bit的值,再对 16384取模。数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点都可以处理这 16384 个槽。集群中的每个节点负责一部分的哈希槽,假设当前集群有 A、B、C3 个节点,每个节点上负责的哈希槽数 =16384/3,那么可能存在的一种分配:
节点A 负责 0~5460 号哈希槽
节点B 负责 5461~10922 号哈希槽
节点C 负责 10923~16383 号哈希槽
客户端给一个 Redis 实例发送数据读写操作时,如果这个实例上并没有相应的数据,会怎么样呢?MOVED 重定向和 ASK 重定向了解一下哈
moved 重定向和 ask重定向
在 Redis cluster 模式下,节点对请求的处理过程如下:
1. 通过哈希槽映射,检查当前 Redis key 是否存在当前节点
2. 若哈希槽不是由自身节点负责,就返回 MOVED 重定向
3. 若哈希槽确实由自身负责,且 key 在 slot 中,则返回该 key 对应结果
4. 若 Redis key 不存在此哈希槽中,检查该哈希槽是否正在迁出(MIGRATING)?
5. 若 Redis key 正在迁出,返回ASK 错误重定向客户端到迁移的目的服务器上
6. 若哈希槽未迁出,检查哈希槽是否导入中?
7. 若哈希槽导入中且有 ASKING 标记,则直接操作,否则返回 MOVED 重定向
1. 通过哈希槽映射,检查当前 Redis key 是否存在当前节点
2. 若哈希槽不是由自身节点负责,就返回 MOVED 重定向
3. 若哈希槽确实由自身负责,且 key 在 slot 中,则返回该 key 对应结果
4. 若 Redis key 不存在此哈希槽中,检查该哈希槽是否正在迁出(MIGRATING)?
5. 若 Redis key 正在迁出,返回ASK 错误重定向客户端到迁移的目的服务器上
6. 若哈希槽未迁出,检查哈希槽是否导入中?
7. 若哈希槽导入中且有 ASKING 标记,则直接操作,否则返回 MOVED 重定向
Moved 重定向
客户端给一个 Redis 实例发送数据读写操作时,如果计算出来的槽不定在该节点上,这时候它会返回 MOVED 重定向错误,MOVED 重定向错误中,会将哈希槽所在的新实例的 IP 和 port 端口带回去。这就的 Redis Cluster 的 MOVED 重定向机制。流程图如下:
客户端给一个 Redis 实例发送数据读写操作时,如果计算出来的槽不定在该节点上,这时候它会返回 MOVED 重定向错误,MOVED 重定向错误中,会将哈希槽所在的新实例的 IP 和 port 端口带回去。这就的 Redis Cluster 的 MOVED 重定向机制。流程图如下:
ASK 重定向
Ask 重定向一般发生于集群伸缩的时候。集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用 Ask 重定向可
以解决此种情况。
Ask 重定向一般发生于集群伸缩的时候。集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用 Ask 重定向可
以解决此种情况。
Cluster 集群节点的通讯协议:Gossip
一个 Redis 集群由多个节点组成,各个节点之间是怎么通信的呢?通过Gossip 协议!
Gossip 是一种谣言传播协议,每个节点周期性地从节点列表中选择 k 个节点,将本节点存储的信息传播出去,直到所有节点信息一致,即算法收敛了。
Gossip 是一种谣言传播协议,每个节点周期性地从节点列表中选择 k 个节点,将本节点存储的信息传播出去,直到所有节点信息一致,即算法收敛了。
Gossip 协议基本思想:一个节点想要分享一些信息给网络中的其他的一些节点。
于是,它周期性的随机选择一些节点,并把信息传递给这些节点。这些收到信息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节点。一般而言,信息会周期性的传递给 N 个目标节点,而不只是一个。这个 N被称为 fanout
于是,它周期性的随机选择一些节点,并把信息传递给这些节点。这些收到信息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节点。一般而言,信息会周期性的传递给 N 个目标节点,而不只是一个。这个 N被称为 fanout
Redis Cluster 集群通过 Gossip 协议进行通信,节点之前不断交换信息,交换的信息内容包括节点出现故障、新节点加入、主从节点变更信息、slot 信息等
等。gossip 协议包含多种消息类型,包括 ping,pong,meet,fail,等等
等。gossip 协议包含多种消息类型,包括 ping,pong,meet,fail,等等
meet 消息:通知新节点加入。消息发送者通知接收者加入到当前集群,meet 消息通信正常完成后,接收节点会加入到集群中并进行周期性的 ping、pong 消息交换。
ping 消息:节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知两个节点的地址、槽、状态信息、最后一次通信时间等
pong 消息:当接收到 ping、meet 消息时,作为响应消息回复给发送方确认消息正常通信。消息中同样带有自己已知的两个节点信息。
fail 消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 fail 消息,其他节点接收到 fail 消息之后把对应节点更新为下线状态。
ping 消息:节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知两个节点的地址、槽、状态信息、最后一次通信时间等
pong 消息:当接收到 ping、meet 消息时,作为响应消息回复给发送方确认消息正常通信。消息中同样带有自己已知的两个节点信息。
fail 消息:当节点判定集群内另一个节点下线时,会向集群内广播一个 fail 消息,其他节点接收到 fail 消息之后把对应节点更新为下线状态。
特别地,每个节点的通过集群总线(cluster bus) 与其他的节点进行通信的。通讯时,使用特殊的端口号,即对外服务端口号加 10000。例如如果某个 node
的端口号是 6379,那么它与其它 nodes 通信的端口号是 16379。nodes 之间的通信采用特殊的二进制协议。
的端口号是 6379,那么它与其它 nodes 通信的端口号是 16379。nodes 之间的通信采用特殊的二进制协议。
故障转移
Redis 集群实现了高可用,当集群内节点出现故障时,通过故障转移,以保证集群正常对外提供服务。redis 集群通过 ping/pong 消息,实现故障发现。这个环境包括主观下线和客观下线。
主观下线:某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。
客观下线: 指标记一个节点真正✁下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。
假如节点A 标记节点B 为主观下线,一段时间后,节点 A 通过消息把节点 B的状态发到其它节点,当节点 C 接受到消息并解析出消息体时,如果发现节点 B的 pfail 状态时,会触发客观下线流程;当下线为主节点时,此时 Redis Cluster 集群为统计持有槽的主节点投票,看投票数是否达到一半,当下线报告统计数大于一半时,被标记为客观下线状态。
假如节点A 标记节点B 为主观下线,一段时间后,节点 A 通过消息把节点 B的状态发到其它节点,当节点 C 接受到消息并解析出消息体时,如果发现节点 B的 pfail 状态时,会触发客观下线流程;当下线为主节点时,此时 Redis Cluster 集群为统计持有槽的主节点投票,看投票数是否达到一半,当下线报告统计数大于一半时,被标记为客观下线状态。
故障恢复:故障发现后,如果下线节点的主节点,则需要在它的从节点中选一个替换它,以保证集群的高可用。流程如下:
资格检查:检查从节点是否具备替换故障主节点的条件。
准备选举时间:资格检查通过后,更新触发故障选举时间。
发起选举:到了故障选举时间,进行选举。
选举投票:只有持有槽的主节点才有票,从节点收集到足够✁选票(大于一半),触发替换主节点操作
准备选举时间:资格检查通过后,更新触发故障选举时间。
发起选举:到了故障选举时间,进行选举。
选举投票:只有持有槽的主节点才有票,从节点收集到足够✁选票(大于一半),触发替换主节点操作
redis分布式所以及要注意的哪些点
命令 setnx + expire 分开写
如果执行完 setnx加锁,正要执行 expire 设置过期时间时,进程 crash 掉或者要重启维护了,那这个锁就“长生不老”了,别的线程永远获取不到锁啦,所以分布式锁不能这么实现。
setnx + value 值的过期时间
过期时间如果是客户端自己生成的,分布式环境下,每个客户端的时间必须同步。
没有保存持有者的唯一标识,可能被别的客户端释放/解锁。
锁过期的时候,并发多个客户端同时请求过来,都执行了 jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。
没有保存持有者的唯一标识,可能被别的客户端释放/解锁。
锁过期的时候,并发多个客户端同时请求过来,都执行了 jedis.getSet(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。
set 的扩展命令(set ex px nx)(注意可能存在的问题)
这个方案可能存在这样的问题:
(1)锁过期释放了,业务还没执行完。
(2)锁被别的线程误删。
(1)锁过期释放了,业务还没执行完。
(2)锁被别的线程误删。
set ex px nx + 校验唯一随机值,再删除
在这里,判断当前线程加的锁和释放锁的不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端,会解除他人加的锁。
一般用lua 脚本代替
一般用lua 脚本代替
这种方式比较不错了,一般情况下,已经可以使用这种实现方式。但是存在锁过期释放了,业务还没执行完的问题(实际上,估算个业务处理的时间,一般
没啥问题了)。
没啥问题了)。
讲一下redission的原理
分布式锁可能存在锁过期释放,业务没执行完的问题。有些小伙伴认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。
当前开源框架Redisson 就解决了这个分布式锁问题。我们一起来看下
Redisson 底层原理是怎样的吧:
当前开源框架Redisson 就解决了这个分布式锁问题。我们一起来看下
Redisson 底层原理是怎样的吧:
只要线程一加锁成功,就会启动一个 watch dog看门狗,它的一个后台线程,会每隔 10 秒检查一下,如果线程 1 还持有锁,那么就会不断地延长锁 key
的生存时间。因此,Redisson 就是使用 Redisson 解决了锁过期释放,业务没执行完问题。
的生存时间。因此,Redisson 就是使用 Redisson 解决了锁过期释放,业务没执行完问题。
什么事redlock算法
Redis 一般都是集群部署的,假设数据在主从同步过程,主节点挂了,Redis 分布式锁可能会有哪些问题呢?一起来看些这个流程图:
如果线程一在 Redis 的master 节点上拿到了锁,但是加锁的 key 还没同步到 slave 节点。
恰好这时,master 节点发生故障,一个 slave 节点就会升级为master 节点。
线程二就可以获取同个 key 的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
为了解决这个问题,Redis 作者 antirez 提出一种高级的分布式锁算法:Redlock。
Redlock 核心思想是这样的:
搞多个Redis master 部署,以保证它们不会同时宕掉。并且这些 master 节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个
master 实例上,是与在 Redis 单实例,使用相同方法来获取和释放锁。
恰好这时,master 节点发生故障,一个 slave 节点就会升级为master 节点。
线程二就可以获取同个 key 的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。
为了解决这个问题,Redis 作者 antirez 提出一种高级的分布式锁算法:Redlock。
Redlock 核心思想是这样的:
搞多个Redis master 部署,以保证它们不会同时宕掉。并且这些 master 节点是完全相互独立的,相互之间不存在数据同步。同时,需要确保在这多个
master 实例上,是与在 Redis 单实例,使用相同方法来获取和释放锁。
按顺序向 5 个 master 节点请求加锁
根据设置的超时时间来判断,要不要跳过该 master 节点。
如果大于等于三个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
如果获取锁失败,解锁!
根据设置的超时时间来判断,要不要跳过该 master 节点。
如果大于等于三个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。
如果获取锁失败,解锁!
redis跳跃表
跳跃表
跳跃表是有序集合 zset 的底层实现之一
跳跃表支持平均 O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。
跳跃表实现由 zskiplist 和 zskiplistNode 两个结构组成,其中 zskiplist 用于 保存跳跃表信息(如表头节点、表尾节点、长度),而 zskiplistNode 则用于表示跳跃表节点。
跳跃表就是在链表的基础上,增加多级索引提升查找效率。
跳跃表是有序集合 zset 的底层实现之一
跳跃表支持平均 O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。
跳跃表实现由 zskiplist 和 zskiplistNode 两个结构组成,其中 zskiplist 用于 保存跳跃表信息(如表头节点、表尾节点、长度),而 zskiplistNode 则用于表示跳跃表节点。
跳跃表就是在链表的基础上,增加多级索引提升查找效率。
mysql 和 redis如何保证双写一致性
缓存延时双删
1. 先删除缓存
2. 再更新数据库
3. 休眠一会(比如 1 秒),再次删除缓存。
2. 再更新数据库
3. 休眠一会(比如 1 秒),再次删除缓存。
删除缓存重试机制
1. 写请求更新数据库
2. 缓存因为某些原因,删除失败
3. 把删除失败的 key 放到消息队列
4. 消费消息队列的消息,获取要删除的 key
5. 重试删除缓存操作
2. 缓存因为某些原因,删除失败
3. 把删除失败的 key 放到消息队列
4. 消费消息队列的消息,获取要删除的 key
5. 重试删除缓存操作
子主题
读取binlog异步缓存
重试删除缓存机制还可以吧,就会造成好多业务代码入侵。其实,还可以这样优化:通过数据库的 binlog 来异步淘汰 key。
(1)可以使用阿里的canal 将 binlog 日志采集发送到 MQ 队列里面
(2)然后通过 ACK 机制确认处理这条更新消息,删除缓存,保证数据缓存一致性
(1)可以使用阿里的canal 将 binlog 日志采集发送到 MQ 队列里面
(2)然后通过 ACK 机制确认处理这条更新消息,删除缓存,保证数据缓存一致性
为什么redis6.0 之后改成多线程
Redis6.0 之前,Redis 在处理客户端的请求时,包括读 socket、解析、执行、写socket 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。
Redis6.0 之前为什么一直不使用多线程?使用 Redis 时,几乎不存在 CPU 成为瓶颈的情况, Redis 主要受限于内存和网络。例如在一个普通的Linux 系统上,Redis 通过使用 pipelining 每秒可以处理 100 万个请求,所以如果应用程序主要使用 O(N)或 O(log(N))的命令,它几乎不会占用过多 CPU。
redis 使用多线程并非是完全摒弃单线程,redis 还在使用单线程模型来处理客户端的请求,只是使用多线程来处理数据来读写和协议解析,执行命令还是使用单线程。这样做的目的是因为 redis 的性能瓶颈在于网络 IO 而非 CPU,使用多线程能提升 IO 读写的效率,从而整体提高 redis 的性能。
redis事务机制
Redis 通过 MULTI、EXEC、WATCH 等一组命令集合,来实现事务机制。
事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
简言之,Redis 事务就是顺序性、一次性、排他性的执行一个队列中的一系列命令。
Redis 执行事务的流程如下:
开始事务(MULTI)
命令入队
执行事务(EXEC)、撤销事务(DISCARD )
事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
简言之,Redis 事务就是顺序性、一次性、排他性的执行一个队列中的一系列命令。
Redis 执行事务的流程如下:
开始事务(MULTI)
命令入队
执行事务(EXEC)、撤销事务(DISCARD )
EXEC 执行所有事务块内的命令
DISCARD 取消事务,放弃执行事务块内的所有命令
MULTI 标记一个事务块的开始
UNWATCH WATCH 命令对所有 key 的监视。
WATCH 监视key ,如果在事务执行之前,该 key 被其他命令所改动,那么事
务将被打断。
务将被打断。
redis出现了hash冲突怎么办
java中的hashmap
1.开放寻址法,包含线性探测、平方探测等等,就是从发生冲突的那个位置开始,按照一定的次序从
hash表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲位置中。ThreadLocal就用到
了线性探测法来解决hash冲突的。
2.链式寻址法,这是一种非常常见的方法,简单理解就是把存在hash冲突的key,以单向链表的方式
来存储。
3.再hash法,就是当通过某个hash函数计算的key存在冲突时,再用另外一个hash函数对这个key做
hash,一直运算直到不再产生冲突。这种方式会增加计算时间,性能影响较大。
4.建立公共溢出区, 就是把hash表分为基本表和溢出表两个部分,凡事存在冲突的元素,一律放入
到溢出表中。
HashMap就是通过链式寻址法来解决的,但是链式地址有个问题:当链表过长的时候,会影响查询
性能,所以在HashMap里,当链表长度大于8的时候,会转为红黑树,提升查询性能。但是树在添
加数据的时候也会有树的分裂合并,所以在树节点小于6的时候,又会转为链表
hash表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲位置中。ThreadLocal就用到
了线性探测法来解决hash冲突的。
2.链式寻址法,这是一种非常常见的方法,简单理解就是把存在hash冲突的key,以单向链表的方式
来存储。
3.再hash法,就是当通过某个hash函数计算的key存在冲突时,再用另外一个hash函数对这个key做
hash,一直运算直到不再产生冲突。这种方式会增加计算时间,性能影响较大。
4.建立公共溢出区, 就是把hash表分为基本表和溢出表两个部分,凡事存在冲突的元素,一律放入
到溢出表中。
HashMap就是通过链式寻址法来解决的,但是链式地址有个问题:当链表过长的时候,会影响查询
性能,所以在HashMap里,当链表长度大于8的时候,会转为红黑树,提升查询性能。但是树在添
加数据的时候也会有树的分裂合并,所以在树节点小于6的时候,又会转为链表
生成rdb期间,redis可以同时处理写请求吗
Redis 提供两个指令生成 RDB,分别是 save 和 bgsave。
如果是 save 指令,会阻塞,因为是主线程执行的。
如果是 bgsave 指令,fork 一个子进程来写入 RDB 文件,快照持久化完全交给子进程来处理,父进程则可以继续处理客户端的请求。
如果是 save 指令,会阻塞,因为是主线程执行的。
如果是 bgsave 指令,fork 一个子进程来写入 RDB 文件,快照持久化完全交给子进程来处理,父进程则可以继续处理客户端的请求。
redis底层使用的什么协议
RESP,英文全称是 Redis Serialization Protocol,它专门为 redis 设计的一套序列化协议. 这个协议其实在 redis 的1.2 版本时就已经出现了,但是到了redis2.0 才最终成为redis 通讯协议的标准。
RESP 主要有实现简单、解析速度快、可读性好等优点。
RESP 主要有实现简单、解析速度快、可读性好等优点。
redis存在线程安全问题吗?为什么
第一个,从Redis服务端层面。
Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Server上执行的指令,不需要任
何同步机制,不会存在线程安全问题。
虽然Redis 6.0里面,增加了多线程的模型,但是增加的多线程只是用来处理网络IO事件,对于指令
的执行过程,仍然是由主线程来处理,所以不会存在多个线程通知执行操作指令的情况。
Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Server上执行的指令,不需要任
何同步机制,不会存在线程安全问题。
虽然Redis 6.0里面,增加了多线程的模型,但是增加的多线程只是用来处理网络IO事件,对于指令
的执行过程,仍然是由主线程来处理,所以不会存在多个线程通知执行操作指令的情况。
为什么Redis没有采用多线程来执行指令,我认为有几个方面的原因。
Redis Server本身可能出现的性能瓶颈点无非就是网络IO、CPU、内存。但是CPU不是Redis的瓶颈
点,所以没必要使用多线程来执行指令。
如果采用多线程,意味着对于redis的所有指令操作,都必须要考虑到线程安全问题,也就是说需要
加锁来解决,这种方式带来的性能影响反而更大。
Redis Server本身可能出现的性能瓶颈点无非就是网络IO、CPU、内存。但是CPU不是Redis的瓶颈
点,所以没必要使用多线程来执行指令。
如果采用多线程,意味着对于redis的所有指令操作,都必须要考虑到线程安全问题,也就是说需要
加锁来解决,这种方式带来的性能影响反而更大。
第二个,从Redis客户端层面。
虽然Redis Server中的指令执行是原子的,但是如果有多个Redis客户端同时执行多个指令的时候,
就无法保证原子性。
假设两个redis client同时获取Redis Server上的key1,同时进行修改和写入,因为多线程环境下的
原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。
虽然Redis Server中的指令执行是原子的,但是如果有多个Redis客户端同时执行多个指令的时候,
就无法保证原子性。
假设两个redis client同时获取Redis Server上的key1,同时进行修改和写入,因为多线程环境下的
原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。
当然,对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能的使用Redis里面的原子指
令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等。
令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等。
redis多线程模型怎么理解有线程安全问题吗
首先,Redis在6.0支持的多线程,并不是说指令操作的多线程,而是针对网络IO的多线程支持。也就是Redis的命令操作,仍然是线程安全的。
其次, Redis本身的性能瓶颈,取决于三个纬度,网络、CPU、内存。而真正影响内存的关键问题是像内存和网络。
而Redis6.0的多线程,本质上解决网络IO的处理效率问题。
在Redis6.0之前。Redis Server端处理接受到客户端请求的时候,Socket连接建立到指令的读取、解析、执行、写回都是由一个线程来处理,这种方式,在客户端请求比较多的情况下,单个线程的网络处理效率太慢,导致客户端的请求处理效率较低。
于是在Redis6.0里面,针对网络IO的处理方式改成了多线程,通过多线程并行的方式提升了网络IO的处理效率。但是对于客户端指令的执行过程,还是使用单线程方式来执行。
最后,Redis6.0里面多线程默认是关闭的,需要在redis.conf文件里面修改io-threads-do-reads配置才能开启。
另外,之所以指令执行不使用多线程,我认为有两个方面的原因。
内存的IO操作,本身不存在性能瓶颈,Redis在数据结构上已经做了非常多的优化。
如果指令的执行使用多线程,那Redis为了解决线程安全问题,需要对数据操作增加锁的同步,不仅
仅增加了复杂度,还会影响性能,代价太大不合算。
其次, Redis本身的性能瓶颈,取决于三个纬度,网络、CPU、内存。而真正影响内存的关键问题是像内存和网络。
而Redis6.0的多线程,本质上解决网络IO的处理效率问题。
在Redis6.0之前。Redis Server端处理接受到客户端请求的时候,Socket连接建立到指令的读取、解析、执行、写回都是由一个线程来处理,这种方式,在客户端请求比较多的情况下,单个线程的网络处理效率太慢,导致客户端的请求处理效率较低。
于是在Redis6.0里面,针对网络IO的处理方式改成了多线程,通过多线程并行的方式提升了网络IO的处理效率。但是对于客户端指令的执行过程,还是使用单线程方式来执行。
最后,Redis6.0里面多线程默认是关闭的,需要在redis.conf文件里面修改io-threads-do-reads配置才能开启。
另外,之所以指令执行不使用多线程,我认为有两个方面的原因。
内存的IO操作,本身不存在性能瓶颈,Redis在数据结构上已经做了非常多的优化。
如果指令的执行使用多线程,那Redis为了解决线程安全问题,需要对数据操作增加锁的同步,不仅
仅增加了复杂度,还会影响性能,代价太大不合算。
redission
设计模式
创建型模式
单例模式
饿汉模式
类初始化时,会立即加载该对象,线程天生安全,调用效率高。
public class Demo1 {
// 类初始化时,会立即加载该对象,线程安全,调用效率高
private static Demo1 demo1 = new Demo1();
private Demo1() {
System.out.println("私有Demo1构造参数初始化");
}
public static Demo1 getInstance() {
return demo1;
}
public static void main(String[] args) {
Demo1 s1 = Demo1.getInstance();
Demo1 s2 = Demo1.getInstance();
System.out.println(s1 == s2);
}
}
// 类初始化时,会立即加载该对象,线程安全,调用效率高
private static Demo1 demo1 = new Demo1();
private Demo1() {
System.out.println("私有Demo1构造参数初始化");
}
public static Demo1 getInstance() {
return demo1;
}
public static void main(String[] args) {
Demo1 s1 = Demo1.getInstance();
Demo1 s2 = Demo1.getInstance();
System.out.println(s1 == s2);
}
}
懒汉模式
类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。
//懒汉式
public class Demo2 {
//类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象。
private static Demo2 demo2;
private Demo2() {
System.out.println("私有Demo2构造参数初始化");
}
public synchronized static Demo2 getInstance() {
if (demo2 == null) {
demo2 = new Demo2();
}
return demo2;
}
public static void main(String[] args) {
Demo2 s1 = Demo2.getInstance();
Demo2 s2 = Demo2.getInstance();
System.out.println(s1 == s2);
}
}
public class Demo2 {
//类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象。
private static Demo2 demo2;
private Demo2() {
System.out.println("私有Demo2构造参数初始化");
}
public synchronized static Demo2 getInstance() {
if (demo2 == null) {
demo2 = new Demo2();
}
return demo2;
}
public static void main(String[] args) {
Demo2 s1 = Demo2.getInstance();
Demo2 s2 = Demo2.getInstance();
System.out.println(s1 == s2);
}
}
静态内部类
静态内部方式:结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。
// 静态内部类方式
public class Demo3 {
private Demo3() {
System.out.println("私有Demo3构造参数初始化");
}
public static class SingletonClassInstance {
private static final Demo3 DEMO_3 = new Demo3();
}
// 方法没有同步
public static Demo3 getInstance() {
return SingletonClassInstance.DEMO_3;
}
public static void main(String[] args) {
Demo3 s1 = Demo3.getInstance();
Demo3 s2 = Demo3.getInstance();
System.out.println(s1 == s2);
}
}
public class Demo3 {
private Demo3() {
System.out.println("私有Demo3构造参数初始化");
}
public static class SingletonClassInstance {
private static final Demo3 DEMO_3 = new Demo3();
}
// 方法没有同步
public static Demo3 getInstance() {
return SingletonClassInstance.DEMO_3;
}
public static void main(String[] args) {
Demo3 s1 = Demo3.getInstance();
Demo3 s2 = Demo3.getInstance();
System.out.println(s1 == s2);
}
}
枚举单例模式
使用枚举实现单例模式 优点:实现简单、调用效率高,枚举本身就是单例,由jvm从根本上提供保障!避免通过反射和反序列化的漏洞, 缺点没有延迟加载。
//使用枚举实现单例模式 优点:实现简单、枚举本身就是单例,由jvm从根本上提供保障!避免通过反射和反序列化的漏洞 缺点没有延迟加载
public class Demo4 {
public static Demo4 getInstance() {
return Demo.INSTANCE.getInstance();
}
public static void main(String[] args) {
Demo4 s1 = Demo4.getInstance();
Demo4 s2 = Demo4.getInstance();
System.out.println(s1 == s2);
}
//定义枚举
private static enum Demo {
INSTANCE;
// 枚举元素为单例
private Demo4 demo4;
private Demo() {
System.out.println("枚举Demo私有构造参数");
demo4 = new Demo4();
}
public Demo4 getInstance() {
return demo4;
}
}
}
public class Demo4 {
public static Demo4 getInstance() {
return Demo.INSTANCE.getInstance();
}
public static void main(String[] args) {
Demo4 s1 = Demo4.getInstance();
Demo4 s2 = Demo4.getInstance();
System.out.println(s1 == s2);
}
//定义枚举
private static enum Demo {
INSTANCE;
// 枚举元素为单例
private Demo4 demo4;
private Demo() {
System.out.println("枚举Demo私有构造参数");
demo4 = new Demo4();
}
public Demo4 getInstance() {
return demo4;
}
}
}
双重检测锁方式
双重检测锁方式 (因为JVM本质重排序的原因,可能会初始化多次,不推荐使用)
//双重检测锁方式
public class Demo5 {
private static Demo5 demo5;
private Demo5() {
System.out.println("私有Demo4构造参数初始化");
}
public static Demo5 getInstance() {
if (demo5 == null) {
synchronized (Demo5.class) {
if (demo5 == null) {
demo5 = new Demo5();
}
}
}
return demo5;
}
public static void main(String[] args) {
Demo5 s1 = Demo5.getInstance();
Demo5 s2 = Demo5.getInstance();
System.out.println(s1 == s2);
}
}
public class Demo5 {
private static Demo5 demo5;
private Demo5() {
System.out.println("私有Demo4构造参数初始化");
}
public static Demo5 getInstance() {
if (demo5 == null) {
synchronized (Demo5.class) {
if (demo5 == null) {
demo5 = new Demo5();
}
}
}
return demo5;
}
public static void main(String[] args) {
Demo5 s1 = Demo5.getInstance();
Demo5 s2 = Demo5.getInstance();
System.out.println(s1 == s2);
}
}
保证一个类只有一个实例,并且提供一个访问该全局访问点
(1)ApplicationContext
ApplicationContext是Spring框架的核心容器,用于管理Bean的生命周期和依赖注入。ApplicationContext在初始化的过程中会创建多个Bean实例,但是ApplicationContext本身是以单例模式实现的。
ApplicationContext是Spring框架的核心容器,用于管理Bean的生命周期和依赖注入。ApplicationContext在初始化的过程中会创建多个Bean实例,但是ApplicationContext本身是以单例模式实现的。
(2)BeanFactory
BeanFactory是Spring框架中的一个接口,它定义了获取Bean的方法。在Spring中,可以通过ApplicationContext获取Bean,而ApplicationContext实际上是BeanFactory的一个实现类。
几乎所有的ApplicationContext实现都是以单例模式实现的,因为ApplicationContext的初始化代价较高,同时它被用来获取Bean,应保证整个应用中只有一个实例。
BeanFactory是Spring框架中的一个接口,它定义了获取Bean的方法。在Spring中,可以通过ApplicationContext获取Bean,而ApplicationContext实际上是BeanFactory的一个实现类。
几乎所有的ApplicationContext实现都是以单例模式实现的,因为ApplicationContext的初始化代价较高,同时它被用来获取Bean,应保证整个应用中只有一个实例。
(3)BeanPostProcessor
BeanPostProcessor是Spring框架中的一个接口,它定义了在Bean初始化前后进行自定义处理的方法。BeanPostProcessor提供了一种拦截器机制,允许我们在Bean被实例化和初始化的过程中进行额外的处理。
BeanPostProcessor通常是以单例模式实现的。因为在整个容器的生命周期中,BeanPostProcessor的实例很少发生变化,而且它们通常没有状态。
BeanPostProcessor是Spring框架中的一个接口,它定义了在Bean初始化前后进行自定义处理的方法。BeanPostProcessor提供了一种拦截器机制,允许我们在Bean被实例化和初始化的过程中进行额外的处理。
BeanPostProcessor通常是以单例模式实现的。因为在整个容器的生命周期中,BeanPostProcessor的实例很少发生变化,而且它们通常没有状态。
(4)DefaultListableBeanFactory
DefaultListableBeanFactory是Spring框架中的一个类,它是BeanFactory的一个默认实现。DefaultListableBeanFactory管理了一个Bean定义的注册表,并提供了获取Bean的功能。 DefaultListableBeanFactory使用单例模式实现,保证整个应用中只有一个实例。
DefaultListableBeanFactory是Spring框架中的一个类,它是BeanFactory的一个默认实现。DefaultListableBeanFactory管理了一个Bean定义的注册表,并提供了获取Bean的功能。 DefaultListableBeanFactory使用单例模式实现,保证整个应用中只有一个实例。
(5)AbstractAutowireCapableBeanFactory
AbstractAutowireCapableBeanFactory是Spring框架中的一个类,它是BeanFactory的抽象实现,并且提供了自动装配的能力。
在获取Bean的过程中,AbstractAutowireCapableBeanFactory会检查Bean定义的自动装配模式,然后通过反射机制实例化Bean,为其属性进行依赖注入。 AbstractAutowireCapableBeanFactory使用单例模式实现,确保整个应用中只有一个实例。
AbstractAutowireCapableBeanFactory是Spring框架中的一个类,它是BeanFactory的抽象实现,并且提供了自动装配的能力。
在获取Bean的过程中,AbstractAutowireCapableBeanFactory会检查Bean定义的自动装配模式,然后通过反射机制实例化Bean,为其属性进行依赖注入。 AbstractAutowireCapableBeanFactory使用单例模式实现,确保整个应用中只有一个实例。
(6)HandlerMapping
HandlerMapping是Spring框架中的一个接口,用于将请求映射到处理程序并返回处理程序对象。在Web应用中,HandlerMapping在容器启动时会被实例化,并设置到DispatcherServlet中。
HandlerMapping通常以单例模式实现。因为在整个应用的运行过程中,映射关系不会发生改变,只需要一个实例来进行请求映射即可。
HandlerMapping是Spring框架中的一个接口,用于将请求映射到处理程序并返回处理程序对象。在Web应用中,HandlerMapping在容器启动时会被实例化,并设置到DispatcherServlet中。
HandlerMapping通常以单例模式实现。因为在整个应用的运行过程中,映射关系不会发生改变,只需要一个实例来进行请求映射即可。
工厂方法模式
抽象工厂模式
建造者模式
原型模式
结构型模式
适配器模式
桥接模式
组合模式
装饰模式
外观模式
亨元模式
代理模式
行为型模式
访问者模式
模版模式
策略模式
状态模式
观察者模式
备忘录模式
中介者模式
迭代器模式
解释器模式
命令模式
责任链模式
每个节点 各司其职
设计模式的六大原则
开放封闭原则
Open Close Principle
Open Close Principle
原则思想:尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码来完成变化
描述:一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。
优点:单一原则告诉我们,每个类都有自己负责的职责,里氏替换原则不能破坏继承关系的体系。
描述:一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。
优点:单一原则告诉我们,每个类都有自己负责的职责,里氏替换原则不能破坏继承关系的体系。
里氏代换原则
Liskov Substitution Principle
Liskov Substitution Principle
原则思想:使用的基类可以在任何地方使用继承的子类,完美的替换基类。
大概意思是:子类可以扩展父类的功能,但不能改变父类原有的功能。子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法,子类中可以增加自己特有的方法。
优点:增加程序的健壮性,即使增加了子类,原有的子类还可以继续运行,互不影响。
大概意思是:子类可以扩展父类的功能,但不能改变父类原有的功能。子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法,子类中可以增加自己特有的方法。
优点:增加程序的健壮性,即使增加了子类,原有的子类还可以继续运行,互不影响。
依赖倒转原则
Dependence Inversion Principle
Dependence Inversion Principle
依赖倒置原则的核心思想是面向接口编程.
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,
这个是开放封闭原则的基础,具体内容是:对接口编程,依赖于抽象而不依赖于具体。
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,
这个是开放封闭原则的基础,具体内容是:对接口编程,依赖于抽象而不依赖于具体。
接口隔离原则
Interface Segregation Principle
Interface Segregation Principle
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。
例如:支付类的接口和订单类的接口,需要把这俩个类别的接口变成俩个隔离的接口
例如:支付类的接口和订单类的接口,需要把这俩个类别的接口变成俩个隔离的接口
迪米特法则(最少知道原则)
Demeter Principle
Demeter Principle
原则思想:一个对象应当对其他对象有尽可能少地了解,简称类间解耦
大概意思就是一个类尽量减少自己对其他对象的依赖,原则是低耦合,高内聚,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。
优点:低耦合,高内聚。
大概意思就是一个类尽量减少自己对其他对象的依赖,原则是低耦合,高内聚,只有使各个模块之间的耦合尽量的低,才能提高代码的复用率。
优点:低耦合,高内聚。
单一职责原则
Principle of single responsibility
Principle of single responsibility
原则思想:一个方法只负责一件事情。
描述:单一职责原则很简单,一个方法 一个类只负责一个职责,各个职责的程序改动,不影响其它程序。 这是常识,几乎所有程序员都会遵循这个原则。
优点:降低类和类的耦合,提高可读性,增加可维护性和可拓展性,降低可变性的风险。
描述:单一职责原则很简单,一个方法 一个类只负责一个职责,各个职责的程序改动,不影响其它程序。 这是常识,几乎所有程序员都会遵循这个原则。
优点:降低类和类的耦合,提高可读性,增加可维护性和可拓展性,降低可变性的风险。
分布式
CAP
CAP理论就是说在分布式存储系统中,最多叧能实现上面的两点。 而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。
AP:是目前大多数网站架构的选择
CP:Redis、MongoDB、Zookeeper
Zookeeper是通过ZAB算法实现的
BASE
基本可用(Basically Available)
软状态(Soft state)
最终一致(Eventually consistent)
它的思想是通过让系统放松对某一时刻数据一致性的要求来换取系统整体伸缩性和性能上的改观
RESTfull API
收藏
收藏
0 条评论
下一页