JAVA面试大纲
2024-03-22 15:22:07 0 举报
AI智能生成
用于初中高级Java程序猿精华面试大纲,斩获京东、飞猪、快手、某保险国企Offer
作者其他创作
大纲/内容
待学习
项目实战
分库分表
Kafka
RocketMQ
k8s
Docker
ES
MongoDB
Dubbo
zookpper
三种角色
Leader
一个 Zookeeper 集群同一时间只会有一个实际工作的 Leader,它会发起并维护与各 Follwer 及 Observer 间的心跳。
所有的写操作必须要通过 Leader 完成再由 Leader 将写操作广播给其它服务器。只要有超过 半数节点(不包括 observeer 节点)写入成功,该写请求就会被提交(类 2PC 协议)。
Follower
一个 Zookeeper 集群可能同时存在多个 Follower,它会响应 Leader 的心跳,
Follower 可直接处理并返回客户端的读请求,同时会将写请求转发给 Leader 处理,
并且负责在 Leader 处理写请求时对请求进行投票。
Observe
角色与 Follower 类似,但是无投票权。Zookeeper 需保证高可用和强一致性,为了支持更多的客 户端,需要增加更多 Server;Server 增多,投票阶段延迟增大,影响性能;引入 Observer, Observer 不参与投票; Observers 接受客户端的连接,并将写请求转发给 leader 节点; 加入更 多 Observer 节点,提高伸缩性,同时不影响吞吐率。
SpringCloud的各个组件
zuul
Ribbon
Fegin
Hystrix
sentienl
1.Java基础
1.1 语法基础
1.1.1 面向对象特性
封装
利用抽象数据类型和基于数据的操作封装在一起,隐藏内部的细节,只提供一些对外接口供外部调用
优点
减少耦合、提高代码可重用性、降低构建大型系统的风险
继承
实现IS-A关系,继承使得子类拥有和父类的属性和方法,可以重用父类的代码。
继承应遵循里氏替换原则,即子类必须能够替换掉所有的父类。比如:Animal animal=new Cat();
多态
编译时多态
主要指方法的重载
运行时多态
主要指在运行期间才能确定指向的具体类型
条件
继承
覆盖(重写)
向上转型
比如
Animal 有两个子类 Cat 和 Dog(继承),都覆盖了Animal 的 talk()方法,并且Cat 和 Dog 的对象都向上转型由 Animal
引用,在调用 talk()方法时,才知道调用的是哪个子类的方法。
引用,在调用 talk()方法时,才知道调用的是哪个子类的方法。
1.1.2 类图
泛化关系
用来描述继承关系,即 extends
实现关系
用来实现一个接口,即 implements
聚合关系
表示整体由部分组成,非强依赖,整体不存在了部分还是存在
组合关系
整体由部分组成,强依赖,比如整体不存在了部分也不存在,比如公司和部门
关联关系
表示不同对象或类之间有关联,1对1、1对多、多对多等
依赖关系
依赖关系实在运行过程中起作用,三种形式
A类是B类中的局部变量
A类是B类方法中的一个参数
A类向B类发送消息,影响B类发生变化
1.1.3 数据类型
基本类型
byte 1字节
boolean 1字节
char 2字节
short 2字节
int 4字节
float 4字节,精确到7位有效数字
long 8字节
double 8字节
引用类型4字节
缓存池
new Integer(123) 和 Integer.valueOf(123)的区别?
new Integer(123)会每次新建一个对象
Integer.valueOf(123)会使用缓存池的对象,多次调用会取同一个对象
valueOf()的实现?
先判断是否在缓存池,在的话直接返回缓存池中的对象
基本类型的缓存池
boolean
true/false
byte
all byte values
short
-128 to 127
int
-128 to 127
char
/u0000 to /uoo7F
String
String类被声明为final,不可被继承
内部使用char[]数组存储数据,且被声明为final,且没有提供相关方法,保证String线程安全,不可变
内部使用char[]数组存储数据,且被声明为final,且没有提供相关方法,保证String线程安全,不可变
不可变的好处
缓存hash值,比如HashMa的key
String Pool字符串缓存池的需要
安全,作为参数不可变
线程安全
intern()
保证相同内容的字符串变量引用同一个内存对象
1.1.4 Object
hashCode()
等价的两个对象散列值一定相同,散列值相同的两个对象不一定等价。
所以,要求在重写类的equals()方法的时候也要重写hashCode()方法。
clone()
Object类中被修饰符protected修饰,不能被对象直接调用,必须重写才能调用clone()方法
对象类必须实现Cloneable接口,重写父类Object的clone()方法才能调用,否则抛出CloneNotSupportedException异常
深拷贝
除了对象本身被拷贝外,对象包含的所有对象成员也将被拷贝,不共享引用。
浅拷贝
只拷贝对象本身和基本数据类型成员,对象类型成员仍然共享引用。
1.1.5 final
声明类不能被继承
声明方法不能被重写
方法被private声明,其隐式被指定为final
声明对象其引用不能被更改,但对象本身可以被改变
1.2 泛型
本质是参数化类型
在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型
如何理解Java中的泛型是“伪泛型”?
泛型中存在类型擦除,在JDK1.5版本加入,为了兼容之前的版本,所以语法上支持泛型,但是在编译阶段会进行
类型擦除操作,将所有的泛型替换为具体的类型。
类型擦除操作,将所有的泛型替换为具体的类型。
类型擦除:将泛型类型变为原始类型,原始类型就用第一个边界的类型变量类替换
如何理解泛型的多态?
泛型的桥接方法?
泛型的桥接方法?
类型擦除会造成多态的冲突
是指当子类继承父类时,@override重写父类的方法时,预计是使用重写实现多态,
但是由于类型擦除导致,重写变成重载,造成冲突
但是由于类型擦除导致,重写变成重载,造成冲突
JVM的解决方案就是桥接方法
即在编译时生成父类的方法和子类的重写方法,将调取的方法转换的接口交由JVM去做
泛型数组
Java 的泛型数组初始化时数组类型不能是具体的泛型类型,只能是通配符的形式,因为具体类型会导致可存入任意类型对象,在取出时会发生类型转换异常,会与泛型的设计思想冲突,而通配符形式本来就需要自己强转,符合预期。
如何正确创建
使用
获取泛型的参数类型
说明
小结
1.3 注解
作用
生成文档
@Document
编译检查
@SuppressWarnings 取消检查
编译时动态处理
lombok的注解
运行时动态处理
如权限注解等
注解+AOP最终的目的是实现模块的解耦
元注解
@Target
描述注解的使用范围
@Retention&@RetentionTarget
描述注解保留的时间范围
@Documented
描述在使用 javadoc 工具为类生成帮助文档时是否要保留其注解信息
@Inherited
被它修饰的Annotation将具有继承性
源码实现原理
参考博客一
参考博客二
1.4 异常
异常的底层
使用javac命令编译字节码后发现,底层维护了一个【异常表】Exception table 记录了 from、to、target、type
try-with-resource
自动释放的资源类需要时实现了AutoCloseable接口的类
建立一个异常对象,是建立一个普通Object耗时的约20倍,
而抛出、接住一个异常对象,所花费时间大约是建立异常对象的4倍。
而抛出、接住一个异常对象,所花费时间大约是建立异常对象的4倍。
1.5 反射
在运行状态中,对于任意一个类 ,都可以获得这类的所有属性和方法;
对于任意一个对象都能调用它的任意一个方法和属性
对于任意一个对象都能调用它的任意一个方法和属性
Class对象的获取
1.6 SPI 机制
Service Provider Interface,是JDK内置的一种服务提供发现机制,可以用来启用和替换组件或者扩展。
思想:接口的实现由provider实现,provider只用在提交的jar包里的META/services下根据平台定义的接口新建文件,
并添加相应的实现类内容就好
并添加相应的实现类内容就好
应用:JDBC DriverManager
1.7 Java引用类型
1. 强引用
Object strongReference = new Object()
gc
只要有引用就不会被GC回收
2. 软引用
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);
SoftReference<String> softReference = new SoftReference<String>(str);
gc
内存空间不足时被GC回收
应用场景
用于缓存的时候比较多
3. 弱引用
WeakReference<String> weakReference = new WeakReference<>(str);
gc
gc时直接回收
应用场景
ThreadLocal
场景
在进行对象跨层传递的时候,可以避免多次传递,打破层次间的约束
线程间数据隔离
进行事务操作,用于存储线程事务信息
数据库连接,Session会话管理
1. Spring中的@Transactional注解保证当前方法的所有操作的原子性的(要么全部成功,要么全部失败)
本质上:只有注解的方法获取到的都是同一个数据库的sessionConnection,才有可能实现,本质上就是通过TreadLocal实现的,因为它的特性是只属于一个线程的变量
底层实现(set)
源码线性:ThreadLocal对象.set()->Thread.currentThread(t)->getMap(t)->map.set
理解
1. set方法首先通过Thread.currentThread获取当前线程的对象(t)
2. 然后调用getMap(t)方法,将当前线程对象作为入参传入
3. 实现则是直接给当前线程(t)的成员变量ThreadLocal.ThreadLocalMap
4. 当前ThreaLocal作为Key 其中的反省对象作为value存入
5. 存入的过程中 将(key,value)组成了一个弱引用对象,所以这个key是一个弱引用对象(WeakReference)
为了防止内存泄漏
防止内存泄露
使用完ThreadLocal后执行remove操作防止内存泄露
WeakHashMap
4. 虚引用
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);
gc
不会决定对象的生命周期,任何时候都可能被回收
应用场景
主要用来跟踪对象被垃圾回收器回收的活动、管理堆外内存
NIO、Netty的零拷贝实现
1.8 集合
Collection
List
ArraryList
底层是数组,是一块连续的内存空间,查找快,方便寻址;但是插入删除慢,因为会发生数据迁移
底层实现
ArrayList的默认无参构造初始容量是0 (jdk1.7以后),第一次add元素的时候会初始化容量,
如果集合中要添加的元素小于10,ArrayList的容量就会设置为10,反之取最大值
如果集合中要添加的元素小于10,ArrayList的容量就会设置为10,反之取最大值
自动扩容
扩容为原来的1.5倍大小
不能无限制的扩容,最大的容量是2的31次方;扩容时会创建一个新数组,将旧数组中的数据复制到新数组(Arrays.copyOf())
Fail-Fast机制
ArrayList的快速失败发生的前提是必须是用了迭代器进行遍历
fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出
ConcurrentModificationException异常,产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出
ConcurrentModificationException异常,产生fail-fast事件。
如何解决线程不安全?
Vector
底层是数组,实现方式和ArrayList相似,不同的是方法上面都有Synchronized修饰
SynchronizedList
List list = Collections.synchronizedList(new ArrayList<>());
优点:使用同步代码块来为加锁,可以指定锁定的对象,锁的粒度更细
缺点:性能不如Vector,没有解决使用lterator迭代时的线程安全问题
适合不需要使用lterator、对性能要求也不高的情況
CopyOnWriteArrayList
List list = new CopyOnWriteArrayList<>();
CopyOnWrite容器是 “写时复制” 容器,在写的时候不对原集合进行修改,
而是重新复制一份,修改完之后,再移动指针(复制时加锁,此时读取则读旧数据)
而是重新复制一份,修改完之后,再移动指针(复制时加锁,此时读取则读旧数据)
优点:线程安全,解決了像ArravList、vector这种集合多线程遍历迭代的安全问题
缺点:耗内存 (集合复制) 且实时性不高
元素遍历与删除
普通for循环 list.size()
在删除的过程中,由于元素的移动导致相邻的相同元素无法删除
iterator迭代器
推荐使用,必须要使用迭代器的remore方法,否则会抛出并发修改异常
原理:迭代器内部会记录并对比修改次数,防止多个迭代器同时操作
原理:迭代器内部会记录并对比修改次数,防止多个迭代器同时操作
增强for循环
forEach语法糖
Vector
底层是数组,线程安全 ,实现方式和ArrayList相似,不同的是方法上面都有Synchronized修饰
LinkedList
底层实现
基于双向链表实现,只能顺序访问,但可以快速的在链表中间插入和删除元素。还可以用作栈、队列和双向队列。
包含头指针和尾指针,node中包含了上一个节点和下一个节点的引用,每个节点只能知道自己的前
驱pre和后继next,在查询指定位置时需要遍历链表一个一个找,效率不如ArrayList
驱pre和后继next,在查询指定位置时需要遍历链表一个一个找,效率不如ArrayList
头插法:插入节点的next指向frst节点,tirst节点的pre指向插入节点,插入节点的pre置为null
尾插法:插入节点的pre指向last节点,last节点的next指向插入节点,插入节点的next置为null
尾插法:插入节点的pre指向last节点,last节点的next指向插入节点,插入节点的next置为null
线程不安全,允许元素为null的有序序列
优点:新增快,用多少空间就申请多少,不浪费内存
缺点:查找慢,需要移动指针
Set
HashSet
基于HashMap实现,支持快速查找,不支持有序性操作。并且失去了元素的插入顺序信息。遍历得到的对象是不确定的
时间复杂度为O(1)
底层实现
底层为HashMap,排列无序,不可重复,可以放入一个null值
添加元素
当向HashSet中添加元素的时候,首先计算元素的hashcode值,然后通过扰动计算和按位与的方式计算出这个元素的存储位置
如果这个位置位空,就将元素添加进去,如果不为空,则用equals方法比较元素是否相等,相等就不添加,否则通过链表添加
为什么采用Hash算法?有什么优势?解决了什么问题?
解决的问题是唯一性,避免了元素很多时需要遍历查找这种效率低的方式
LinkedHashSet
基于哈希表实现,且内部使用双向链表维护元素的插入顺序。
时间复杂度O(1)
TreeSet
基于红黑树实现,支持有序性操作,例如一个范围查找元素的操作。
时间复杂度为O(logn)
底层实现
底层是TreeMap,基于二叉树(红黑树),排列有序,不可重复,不允许放入nul值
TreeMap是按key排序的,元素在插入Tree Set时compareTo()方法要被调用,所以TreeSet中的元素要实现Comparable接口
Treeset作为一种Set, 它不允许出现重复元素。TreeSet是用compareTo()来判断重复元素的
Queue(不属于集合的实现)
LinkedList可以用它来实现双向队列
PriorityQueue基于堆结构实现,可以用它来实现优先队列
BlockingQueue
ArrayBlockingQueue
LinkedBlockingQueue
SvnchronousQueue
PriorityBlockingQueue
LinkedTransferQueue
LinkedBlockingDeaue
DelayQueue
Map
TreeMap
基于红黑树实现
自然排序,实现Comparable接口
自定义排序,实现Comparator接口
HashMap
底层实现
1.7 使用数组+链表,采用头插法
头插法在多线程下扩容时可能会出现循环链表,再次get可能造成死循环
1.8 数组十链表+红黑树,采用尾插法
尾插法解决了死循环问题,引入红黑树
优化了长链表下的查询效率,复杂度为O(logn)
树化
两个判定条件
数组的容量大于等于64
链表的长度大于等于8
初始化
阿里规约里强调hashmap传入初始化容量,根本目的就是让其少扩容
扩容
影响因素
Capacity :容量,默认16
LoadFactor:扩载因子默认0.75
threshhold = Capacity * LoadFactor
扩容步骤
触发条件
元素个数>threshold 时,开始扩容
或 链表长度>8 且 数组长度<64 时,先不进行树化,而是优先考虑扩容
JDK1.7的扩容是需要重新hash分配的,并且会遍历hash表中所有的元素。
JDK1.8的扩容是不需要重新hash分配的,主要是利用的因为每次扩容都会翻倍,所以就产生了原来链表中的hash值在计算index的时候产生低位索引和高位索引,低位索引的位置不变,而高位索引则需要加旧容量的长度就可以了
put()源码
1、是否初始化:先判断 table 是否初始化,如果没有初始化(数组长度是0)则先初始化
2、计算哈希值:通过 hash(kev) 方法计算 key 的哈希值,如果key 是null则 Hashcode =0
底层的hash算法:使用【按位异或^ 以及无符号右移运算>>>】
(h=key.hashCode())^(h>>>16)
(h=key.hashCode())^(h>>>16)
3、计算下标:使用计算出的哈希值,通过按位与&操作来获取下标
【hash&(length-1)】等于【hash%length】的前提为length为2的n次幂
4、存入节点:获得下标后查找对应的节点位置,判断该位置是链表还是红黑树,插入数据
5、新增or覆盖:如果遇到hash值相同,则比较equals,都相同则覆盖原数据,否则新增
寻址算法
1.7 计算哈希值后进行9次扰动 (5次异或4次位移)再通过按位与&操作来获取下标
1.8 使用HashCode跟右移16位的哈希值进行异或,再通过按位与&操作来获取下标
遍历的四种方式
面试题
1. 为什么扩容2的幂次?
2. 加载因子为什么是0.75?
3. table的初始化是什么时候?
4. HashMap1.8相比于1.7的优化点?
5. HashMap和Hashtable有什么区别?
6.如何判断链表有环?
7. HashMap底层是怎么计算对应元素所在数组的位置的?
8. 从底层源码的角度介绍一下HashMap中的put操作?
LinkedHashMap
使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用LRU顺序
ConcurrentHashMap【放在此处作比较】
底层实现
1.7 数组+链表,1.8 引入红黑树
数据结构
1.7 使用HashEntry节点,使用segment分段锁,默认是16(并发级别),可以通过构造方法指定
1.8 采用Node节点,对每个node节点(数组元素)加锁,锁的粒度更小
锁种类
1.7 segment继承自ReentrantLock,可重入锁
1.8 CAS+synchronized, CAS失败了自旋保证成功,再失败就synchronized保证
put()源码
1.7
计算key的hash值,定位到对应的Segment对象
获取可重入锁,再次通过hash值定位到具体位置
插入或覆盖HashEntry对象,释放锁
1.8
先计算hash值,得到Node数组的下标
判断是否为空,如果为空,则用cas的操作来赋值首节点
如果失败,则因为自旋,会进入非空节点的逻辑
这个时候会用synchronize加锁头节点(保证整条链路锁定)
这个时候还会进行二次判断,是否是同一个首节点,在分首
节点到底是链表还是树结构,进行遍历判断
这个时候还会进行二次判断,是否是同一个首节点,在分首
节点到底是链表还是树结构,进行遍历判断
HashTable
遗留类 HashMap的线程安全版本,不再使用
面试题
1、JDK中Arrays里有个sort方法,是JDK为我们提供的一个排序方法,这个sort方法用的是什么排序算法?
2、在实际的使用中,你会如何选择ArraryList和LinkedList?
3、JDK中的快速排序,做了哪些方面的优化和改进,为什么要做这些改进?
1.9 设计模式
创建型
结构型
行为性
面试题
设计模式有哪些?
设计模式的六大原则
结构型-适配器模式
代理模式
策略模式
1.10 基本IO模型
BIO
同步阻塞IO模型,一个客户端连接对应一个处理线程
特性
IO操作里面的read操作是阻塞操作,如果客户端连接不做读写操作或者读操作耗时很长,会导致整个服务端阻塞,浪费系统资源
可以开启多个线程同时连接服务端,但是多个线程可以解决连接阻塞的问题,无法解决IO阻塞的问题,造成线程数量过多,压力巨大
适用场景
使用用于客户端连接数量小且固定的场景,对服务器资源要求比较高
AIO
异步非阻塞IO模型,由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用
适用场景
AIO方式适用于连接数目多且连接比较长(重操作) 的架构
NIO
同步非阻塞IO模型,实现一个服务端使用一个线程同时连接多个客户端连接请求(非阻塞),核心就是使用Selector多路复用器,
将客户端连接事件、read事件、write事件都注册到多路复用器(类似于一个监听机制),通过不断地轮询注册的多路复用器的SelectionKey的集合,根据不同的事件进行处理,这里是串行的
将客户端连接事件、read事件、write事件都注册到多路复用器(类似于一个监听机制),通过不断地轮询注册的多路复用器的SelectionKey的集合,根据不同的事件进行处理,这里是串行的
NIO非阻塞的体现
NIO的socket的accept()方法其实也是阻塞的,但是由于当执行到这个方法时,是因为确实已经有客户端注册了请求连接的事件,所以可以直接处理,不会阻塞住,主要体现在收集注册SocketChannel事件,然后轮询处理这些事件上
进一步说就是,BIO在客户端连接后,服务端会一直等着客户端发送消息(阻塞,不管客户端到底有没有发消息),而NIO是监听到了发送消息的事件才会处理
适用场景
NIO方式适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯,编程比较复杂
组件
Channel
类似于流,是客户端和服务端数据流通的通道
会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理
每个 channel 对应一个 buffer缓冲区
Selector
多路复用器,可以理解为一个监听器,适用select()方法,轮询注册到selector中的chennel和事件,进行处理
selector收集所有连接的事件并且转交给后端线程,线程连续执行所有事件命令并将结果写回客户端
Buffer
就是一个缓冲数组
实现
明显的区别是,由之前的主动去拉取数据,变为事件通知的方式
面试题
1、什么是反射?
2、反射的原理?
3、反射的优缺点?
4、反射的用途?
5、反射常用到类?
6、反射的使用
1、反射获取Class的方式?
2、判断是实例是某个类的
3、创建对象
4、通过反射获取构造方法并使用
5、获取成员变量并调用
6、获取成员方法并调用
7、利用反射创建数组
8、通过反射越过泛型检查
7、什么是hashCode 以及 hashCode()与equals()的联系
8、抽象类与接口的区别
分别在哪些地方使用抽象类和接口?
2. 计算机基础
2.1 网络基础
面试题
网络模型相关
1. OSI网络七层模型是什么?
2. TCP/IP 五层模型是什么?
0. TCP/IP模型和OSI模型的区别?
网络层相关
3. IP地址与物理地址的区别和联系?
4. ARP地址解析协议的工作原理?
5. RARP逆地址解析协议?
6. DHCP协议?
7. ICMP协议?
8. 交换机与路由器的区别?
9. 路由选择协议有哪些?
传输层相关
10. 传输控制协议TCP 和 用户数据报协议UDP的区别?
11. TCP 和 UDP 的适用场景?
12. TCP的首部字段结构?
13. 建立连接-TCP的三次握手过程?
14. 为什么不能用两次握手进行建立连接?
15. 三次握手过程中是否可以携带数据?
16. 断开连接-TCP的四次挥手过程?
17. 为什么需要四次挥手?
18. 为什么挥手过程需要TIME_WAIT 状态?
19. 什么是SYN洪泛?
20. TCP的拆包、粘包是什么?
21. 什么情况先会发生拆包粘包?
22. 拆包粘包问题的解决策略有哪些?
23. http1.1 是怎么解决拆包粘包的?
24. 网络层 - IP数据报分片是指什么?
25. TCP 如何保证可靠性传输?
26. TCP的流量控制?
27. TCP的拥塞控制?
拥塞避免算法
快重传算法
快恢复算法
28. 拥塞控制和流量控制的差别?
应用层相关
29. http协议是什么?
30. cookie 和 session 的区别?
31. 一个完整的http请求是怎么样?即从输入网址到获得页面的过程是什么样的?
第(2)步: DNS服务器查询对应IP地址的过程?
第(4)步:TCP链接链接建立起来后,浏览器向服务器发送http请求的过程?
第(6)步:浏览器解析视图的过程?
33. http的长连接和短连接?
34. http的断点续传是如何实现的?
35. http存在哪些问题?
36. https协议是什么?
37. https的认证加密过程?如何保证内容不会被篡改的?
38. 根证书如何保证签发的证书是安全有效的?
39. 为什么需要CA证书认证机构呢?
40. http的常见请求方式?
41. get和 post 请求的区别?
42. http报文头的理解?
43. http 常见的状态码?
44. http/1.1和http/2.0的区别?
45. http 和 https 的区别?
46. 应用层还有哪些协议?
47. RPC和TCP的区别?
2.2 操作系统基础
面试题
操作系统
1. 操作系统是什么?包括哪些部分?
2. 什么是用户态和内核态?
进程与线程
3. 进程与线程的区别?
4. 进程的五种状态?
5. 进程的调度算法?
6. 进程的通信方式?
7. 线程的七种状态?
8. 线程间同步的方式?
死锁
9. 死锁的必要条件?
10. 处理死锁的基本策略?
11. 活锁的概念是什么?
内存管理
12. 内存连续分配算法?
13. 虚拟内存的概念?
14. 虚拟内存的优点?
15. 页面置换算法?
16. 颠簸/抖动是什么概念?
17. 内存颠簸的解决策略有哪些?
文件系统
18. 磁盘的物理结构?
19. 磁盘调度算法?
系统IO
20. Unix 常见的IO模型有哪些?
21. select 、poll和epoll有什么区别?
2.3 应用基础
面试题
1、QPS和TPS的区别?
3.并发与多线程(JMM)
3.1 理论基础
进程和线程的理解
(1)一个程序至少有一个进程,一个进程至少有一个线程,线程是依赖于进程存在的,线程是一个进程中代码的不同的执行路线;
(2)调度和并发:进程是对运行时程序的封装,进程是操作系统进行资源调度和分配的最小单位,实现了操作系统的并发;
线程是程序执行的最小单位,是CPU调度和分派的基本的单位,实现进程内部的并发。
线程是程序执行的最小单位,是CPU调度和分派的基本的单位,实现进程内部的并发。
(3)资源与内存空间:进程是资源分配的基本单位,线程不拥有资源;进程之间拥有相互独立的内存单位,但是同一个进程下的各个线程之间共享程序的内存空间,(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
(4)系统开销:创建或销毁进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等;
而线程只需要堆栈指针以及程序计数器就可以了,开销远小于创建或撤销进程时的开销。
在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销小。
而线程只需要堆栈指针以及程序计数器就可以了,开销远小于创建或撤销进程时的开销。
在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销小。
(5)通信:线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助IPC。
为什么需要多线程?
CPU、内存、I/O设备的速度是有极大差异的,为了合理利用CPU的高性能,平衡这三者的速度差异,
计算机体系结构(硬件)、操作系统、编译程序都做出了贡献,主要体现如下
计算机体系结构(硬件)、操作系统、编译程序都做出了贡献,主要体现如下
多线程造成了什么问题?
可见性问题
CPU增加了缓存,以均衡与内存的速度差异
原子性问题
操作系统为增加了进程和线程,以分时复用CPU,进而均衡CPU和I/O设备的速度差异
有序性问题
JVM编译程序优化指令执行次序,使得缓存能够得到更加合理地利用
重排序的类型
编译器重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
处理器重排序
现代处理器采用指令集并行技术ILP,来将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行
面试题
1. 进程和线程的区别?
3.2 JMM
概述
Java内存模型即Java Memory Model,用来屏蔽掉各种硬件和操作系统的内存访问差异,
以实现让Java程序在各平台下都能够达到一致的内存访问效果
以实现让Java程序在各平台下都能够达到一致的内存访问效果
通俗来讲,每个线程都有自己的工作内存,线程的共享变量存储在主内存,
工作内存中存储了共享变量的副本,不同线程之间无法直接访问对方工作内存中的变量,
线程问的通信均需要在主内存完成
工作内存中存储了共享变量的副本,不同线程之间无法直接访问对方工作内存中的变量,
线程问的通信均需要在主内存完成
总结
工作内存=虚拟机栈
主内存=堆区+方法区
多级缓存
为什么要设置多级缓存?
为了解决CPU运算速度与内存读写速度不匹配的矛盾
工作原理
在CPU和内存之间,引入了L1高速缓存、L2高速缓存、L3高速缓存
每一级缓存中所存储的数据全部都是下一级缓存中的一部分
每一级缓存中所存储的数据全部都是下一级缓存中的一部分
当CPU需要数据时,先从缓存中取,加快读写速度,提高CPU利用率
CPU存储结构
寄存器 一L1缓存 一L2缓存 一L3缓存一主内存一本地磁盘一远程数据库
越往上访问速度越快、成本越高,空间更小。越往下访问速度越慢、成本越低,空间越大
问题解决
可见性
MESI,Java保证可见性可以通过volatile、synchronized、final来实现
原子性
Java内存模型通过read、load、assign、use、store、write来保证原子性操作,
此外还有lock和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令
此外还有lock和unlock,直接对应着synchronized关键字的monitorenter和monitorexit字节码指令
有序性
Java通过volatile、synchronized来保证
3.3 线程基础
1. 线程
线程的生命周期(6-7种)
新建 (NEW)
使用new 关键宇创建线程后进入新建状态,此时还没有调用 start()
可运行 (RUNNABLE)
线程调用 start()方法后,进入就绪状态,等待CPU分配时间片
操作系统中就绪(READY)和运行中(RUNNING)两种状态的统称
操作系统中就绪(READY)和运行中(RUNNING)两种状态的统称
运行态(RUNNING)
正在执行任务
阻塞 (BLOCKED)
当进入 synchronized 同步代码块或同步方法时,且没有获取到锁,
线程就进入了blocked,直到锁被释放,重新进入runnable 状态
线程就进入了blocked,直到锁被释放,重新进入runnable 状态
等待 (WAITING)
当线程调用 wait() 或者 join()时,会进入到 waiting 状态,当调用
notify 或notifyAll 时,或者 join 的线程执行结束后会进入 runnable
notify 或notifyAll 时,或者 join 的线程执行结束后会进入 runnable
超时等待 (TIMED_WAITING)
当线程调用 sleep (time)或者 wait (time)时,进入timed waiting
状态当休眠时问结束后,或者调用 notity 或notifyAll 时会重新进入runnable
状态当休眠时问结束后,或者调用 notity 或notifyAll 时会重新进入runnable
终止 (TERMINATED)
程序执行结束,线程进入 terminated 状态
创建线程的方式(5种)
1. 继承Thread类
重写run方法,没有返回值 | new MyThread().start();
缺点:Java是单继承,如果继承Thread就不能继承其他类
2. 实现Runnable接口
重写run方法,没有返回值 | 在创建Thread对象时传进去
优点:不受单继承的限制
3. 实现Callable接口
重写call方法,有返回值,可抛异常
需要用FutureTask在外部封装一下再传递给
Thread, FutureTask就是Runnable的实现类
Thread, FutureTask就是Runnable的实现类
FutureTask使用场景?
两件事或多件事同时完成
FutureTask两个构造函数?
第一个构造函数要求传入Callable对象
第二个构造函数要求传入Runnable对象和返回值类型
FutureTask的其他方法?
get():会一直等待子线程运行结束
get(5,TimeUnit.SECOND):传入等待时间5s,超时后会抛出TimeoutException异常。需要捕捉处理
isDone():询问子线程是否执行完成,返回值是boolean
优点
在主线程中可获取到子线程的返回值
直接调用FutureTask对象的get()方法
为什么可以获取到?如何获取的?
在主线程中可获取到子线程发生的异常
通过getCause()方法获取子线程的异常
4. 使用线程池创建
5. 使用lamba表达式创建
终止/退出线程的方式
run()方法运行结束
如何判断线程是否停止?
this.interrupted()
返回线程状态并停止线程
this.isInterrupted()
不具备清除状态功能
使用退出标志位/共享变量
定义全局变量,使用volatile修饰的boolean退出标志位来控制循环
调用interrupt()方法终止线程
区分阻塞和和非阻塞两种情况
区分阻塞和和非阻塞两种情况
调用interrupt()时会抛出异常,处于阻塞状态中的线程可捕获interruptedException异常,通过break跳出循环
调用interrupt()时会调用interrupted()函数,未阻塞的线程可使用isinterruptea()判断线程的中断标志来退出循环
守护线程
在java线程中有两种线程,一种是用户线程,一种是守护线程,典型得守护线程就是垃圾回收线程
守护线程是一种特殊得线程,当进程中不存在用户线程(非守护线程)时,守护线程自动销毀
可以使用setDaemon()设置线程为守护线程,注意不能把一个正在运行的线程设置为守护线程,
所以,setDaemon()方法必须在start()方法前面,并且守护线程中产生得线程也是守护线程
所以,setDaemon()方法必须在start()方法前面,并且守护线程中产生得线程也是守护线程
线程调度算法
1. 分片式调度
是指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程
占用的CPU的时间片
占用的CPU的时间片
2. 抢占式调度
是指优先让可运行池中优先级高的线程占用CPU如果可运行池中的线程优先级相同,
那么就随机选择一个线程,
那么就随机选择一个线程,
线程之间的通信方式
1. 临界区 CriticalSection
在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
2. 互斥量 Mutex
采用互斥对象机制。只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。当前拥有互斥对象的线程处理完任务后必须将线程交出,以便其他线程访问该资源。
互斥对象和临界区对象非常相似,但是互斥量允许在进程间使用,而临界区只限制于同一进程的各个线程之间使用,但是更节省资源,更有效率。
互斥对象和临界区对象非常相似,但是互斥量允许在进程间使用,而临界区只限制于同一进程的各个线程之间使用,但是更节省资源,更有效率。
3. 信号量 semophore
信号量其实就是一个计数器,限制了同一时刻访问同一资源的最大线程数。如果这个计数达到了零,则所有对这个Semaphore类对象所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零为止。
4. 事件 Event,wait/notify
事件机制,允许一个线程在处理完一个任务后,主动唤醒另一个线程执行任务,通过通知操作的方式来保持线程的同步。
面试题
1. 谈谈你对线程安全的理解?
当多个线程访问一个对象时,如果不进行额外的同步控制或其他的协调操作,
调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的
调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的
什么时候考虑线程安全?
多个线程访问同一个资源,但是资源是有状态的,比如字符串拼接
如何做到线程安全?
使用Synchronized关键字给代码块或者方法加锁
比如StringBuffer的源码中,方法上均添加了Synchronized
2. sleep()与wait()的区别?
3. 为什么wait()要定义在Object中而不定义在Thread中?
4. 为什么wait()必须写在同步代码块中?
5. notify()和notifyAll()有什么区别?
6. interrupted()和islnterrupted()的区别?
7. Java中用到的线程调度算法是什么?
8. 线程阻塞挂起会导致CPU升高么?
2. 线程池
线程池的创建方式(七种)
使用Executor创建
1. newFixedThreadPool
创建有固定线程数的线程池,可以控制并发数量,超过的线程在队列中等待
LinkedBlockingQueue
2. newCacheThreadPool
创建具有缓存功能的线程池,缓存一段时间后会回收,如果线程不够则会新建
SynchronousQueue
3. newSingleThreadPool
创建只有一个线程数的线程池,可以保证任务执行的顺序性
LinkedBlockingQueue
4. newScheduledThreadPool
创建一个可以执行延迟任务的线程池
DelayedWorkQueue
5. newSingleScheduledThreadPool
创建只有一个线程数且可以延迟执行任务的线程池
DelayedWorkQueue
6. newWorkStealingPool
创建可以抢占式执行任务的线程池,线程执行任务不固定
ForkJoinPool
使用ThreadPoolExecutor创建
7. new ThreadPoolExecutor()
创建自定义参数的线程池
七个参数
corePoolSize
最大核心线程数,核心线程数会一直保留
maximumPoolSize
线程最大数量,当前线程池可以创建的最大线程数量
keepAliveTime
当任务执行完之后,线程会保留的时间
TimeUnit
时间单位
BlockingQueue
当最大线程数都被占用时,任务放到任务队列中存储,
使用不同的队列就会产生不同类型的线程池
使用不同的队列就会产生不同类型的线程池
有界队列
SynchronousQueue
ArrayBlockingQueue
无界队列
LinkedBlockingQueue
PriorityBlockingQueue
ThreadFactory
线程工厂,用来产生线程的,可以自定义线程的类型,
比如我们可以定义线程组名称,在jstack问题排查时,非常有帮助
比如我们可以定义线程组名称,在jstack问题排查时,非常有帮助
RejectedExecutionHandler
拒绝策略(4种),当最大线程数和任务队列全部占满时,对新任务的处理策略
Abort:直接抛出异常
Discard: 直接扔掉最新的任务,不抛异常
DiscardOldest: 扔掉排队时间最久的任务,也就是最旧的任务。
CallerRuns: 调用者处理任务
线程池的工作流程
1. 当我们提交任务,线程池会根据corePoolSize大小创建若干任务数量线程执行任务
2. 当阻塞队列也满了之后,那么将会继续创建(maximumPoolSize-corePoolSize)个数量的线程来执行任务
3. 如果任务处理完成,maximumPoolSize-corePoolSize额外创建的线程等待keepAliveTime之后被自动销毁
4. 如果达到maximumPoolSize,阻塞队列还是满的状态,那么将根据不同的拒绝策略对应处理
有个volatile修饰的状态码
running
处理已添加的任务,接收新任务
shutdown
处理已添加的任务,不接收新任务
stop
停止处理已添加的任务,不接受新任务
tidying
所有的任务已终止,ctl记录的任务数为0
terminated
所有线程销毁
实际使用
商品详情界面
批处理
项目需求,更新所有商户的POI地址
ForkJoinPool
介绍
自java7开始,jvm提供的一个用于并行执行的任务框架,主要是使用分治思想实现。
思想是将大任务分成若干小任务,之后再并行对这些小任务进行计算,最终汇总这些任务的结果,得到最终的结果。
其广泛用在java8的stream中。
思想是将大任务分成若干小任务,之后再并行对这些小任务进行计算,最终汇总这些任务的结果,得到最终的结果。
其广泛用在java8的stream中。
工作原理
ForkJoinPool中内置了一个WorkQueue数组,一个WorkQueue对应着一个线程。
当一个线程执行完其对应的WorkQueue中的任务中时,会窃取其他WorkQueue中的任务来执行!
WorkQueue是一个双端队列,当前线程会从一端获取和添加任务,而其他线程会从队列的另一端来窃取任务,进一步降低了冲突。
当一个线程执行完其对应的WorkQueue中的任务中时,会窃取其他WorkQueue中的任务来执行!
WorkQueue是一个双端队列,当前线程会从一端获取和添加任务,而其他线程会从队列的另一端来窃取任务,进一步降低了冲突。
分治思想
将大任务分成若干小任务,之后再并行对这些小任务进行计算,最终汇总这些任务的结果,得到最终的结果
工作窃取
是指当某个线程的任务队列中没有可执行任务的时候,从其他线程的任务队列中窃取任务来执行,
以充分利用工作线程的计算能力,减少线程由于获取不到任务而造成的空闲浪费
以充分利用工作线程的计算能力,减少线程由于获取不到任务而造成的空闲浪费
问题
JDK8中的stream流导致线程吃满
内部使用根据CPU获取最大并行线程数量
默认使用ForkJoinPool.commonPool(),这是一个公用线程池,其它地方在使用的时候会被吃满
面试题
1. 为什么使用线程池?
线程池就相当于一种池化技术,所谓池化就是减少线程对象的创建和销毁的次数的一种技术。
因为创建和销毁线程是需要耗费系统资源的,为了避免性能损耗,所以使用线程池。
因为创建和销毁线程是需要耗费系统资源的,为了避免性能损耗,所以使用线程池。
线程池内部维护了两个集合,一个是线程的集合,一个是任务的集合
2. 为什么建议使用自定义的方式去生成线程池?
简单来说,就是线程池的实现【最大线程数】和【最大任务队列】
使用的Integer.MAX_VALUE,容易产生OOM
使用的Integer.MAX_VALUE,容易产生OOM
3. 说一说execute和submit的区别?
4. 如何知道一个线程池的任务是否全部执行完毕?
5. Thread可以设置什么?
3.4 并发/锁实现
底层算法[CAS/AQS]
AQS
介绍
AQS是全称是Abstract Queue Synchronizer,也就是多线程同步器,它是J.U.C包下面多个线程安全组件的底层实现。
比如Lock、CountDownLatch、Semaphore都用到了AQS。
本质上来说AQS提供了两种锁的机制,一种是排它锁,一种是共享锁。
比如CountDownLatch就用到了AQS的共享锁的功能。
比如Lock、CountDownLatch、Semaphore都用到了AQS。
本质上来说AQS提供了两种锁的机制,一种是排它锁,一种是共享锁。
比如CountDownLatch就用到了AQS的共享锁的功能。
底层实现
1. AQS采用了一个int类型的互斥变量state用来记录锁竞争的状态,它的值分为两种,0代表当前无锁状态,大于等于1(大于1时是锁重入的过程会累加)代表目前已经有线程持有锁资源,
一个线程来获得锁资源的时候首先会判断state的状态,如果是0,则采用CAS的方式将当前锁状态变更为1,并且把当前线程赋值(AQS的父类维护了一个存储线程信息的字段),则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。用CAS操作保证操作的原子性和安全性。
一个线程来获得锁资源的时候首先会判断state的状态,如果是0,则采用CAS的方式将当前锁状态变更为1,并且把当前线程赋值(AQS的父类维护了一个存储线程信息的字段),则代表加锁成功,一旦获取到锁,其他的线程将会被阻塞进入阻塞队列自旋,获得锁的线程释放锁的时候将会唤醒阻塞队列中的线程,释放锁的时候则会把state重新置为0,同时当前线程ID置为空。用CAS操作保证操作的原子性和安全性。
2. 线程的等待与唤醒
通过ubsafe方法中的park和unpark方法去阻塞和唤醒,
阻塞的线程会按照先进先出的原则添加到一个双向链表的结构中。
当获得锁资源的线程释放锁之后,会在双向链表的头部唤醒下一个阻塞的线程再去竞争锁。
通过ubsafe方法中的park和unpark方法去阻塞和唤醒,
阻塞的线程会按照先进先出的原则添加到一个双向链表的结构中。
当获得锁资源的线程释放锁之后,会在双向链表的头部唤醒下一个阻塞的线程再去竞争锁。
3. 锁竞争的公平性和非公平性
通过判断当前双向链表中是否有阻塞的线程,如果有则去排队等待,如果没有则直接获得锁,这是公平锁,
而非公平锁处理方式是,不管链表中是否存在阻塞线程都尝试去获得锁
通过判断当前双向链表中是否有阻塞的线程,如果有则去排队等待,如果没有则直接获得锁,这是公平锁,
而非公平锁处理方式是,不管链表中是否存在阻塞线程都尝试去获得锁
4. 非公平锁相对于公平锁的性能提升点:
在于非公平锁直接会以CAS的方式去获取锁,如果获得锁,则避免进入阻塞队列,进入阻塞队列会产生上下文的切换
在于非公平锁直接会以CAS的方式去获取锁,如果获得锁,则避免进入阻塞队列,进入阻塞队列会产生上下文的切换
实现锁类型
公平锁和非公平锁
重入锁
排它锁和共享锁
CAS
介绍
CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性
1. 变量内存地址,V表示
2. 旧的预期值,A表示
3. 准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
2. 旧的预期值,A表示
3. 准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
底层实现
使用【sum.misc.Unsafe】类,调用native方法实现在操作系统底层的指令支持:lock cmpxchg
指令
lock
类似于CPU级别锁总线
cmpxchg
CPU级别的CAS操作,非原子性
缺点
1. ABA问题
ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果
解决:
1. Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
2. 使用时间戳或者版本号机制
1. Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
2. 使用时间戳或者版本号机制
2. 循环时间长开销大
自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
3. 只能保证一个共享变量的原子操作
只对一个共享变量操作可以保证原子性,但是多个则不行
解决:多个可以通过AtomicReference来处理或者使用锁synchronized实现。
应用
Atomic包
ConcurrentHashmap底层的自旋操作都用到了
锁的实现
synchronized 关键字
锁的使用
锁的对象
不管是修饰的代码块还是方法,都是通过锁对象实现的
类
类实例
锁的语法使用
静态方法
由于静态方法属于类,所以锁的是类
普通方法
由于普通方法属于类的实例,所以锁的是类实例
代码块
直接使用synchronized(this){} 锁的是类的实例
声明一个类变量,synchronized(lockObj){} 是当前对象,粒度更小,用于优化
对象
对象头
MarkWord
存储对象的HashCode、分代年龄(4bit)、锁标志位(2bit)
KlassClass指针
对象指向类元信息的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
如果对象是数组,会记录数组的长度
实例数据
存储对象数据
对齐填充
以8个字节为单位,对数据进行填充
由于CPU读取缓存数据的时候是以 64 byte为一行进行读取的,所以,对齐填充实际8个字节为单位的
设置对象注解@Contended
添加一个JVM参数:+XX RestrictContended
添加一个JVM参数:+XX RestrictContended
工作原理
字节码实现
monitorenter
计数器+1
monitorexit
计数器-1
计数器
计算机底层实现
lock指令
类似于CPU级别锁总线
cmpxchg指令
CPU级别的CAS操作,非原子性
源码实现
1. synchronized实际上有两个队列waitSet和entryList
2. 当一个线程来竞争锁时,会先进入entryList
3. 当竞争到锁后,就赋值给当前线程(相当于写入对象头中的线程ID),并且计数器+1
4. 如果线程调用wait()方法,将释放锁,当前线程置为null,计数器-1,
同时进入【waitSet】等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
同时进入【waitSet】等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
5. 如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null
锁升级
无锁态
锁标志位00
偏向锁
锁标志位00
轻量级锁/自旋锁/无锁
锁标志位01
重量级锁
锁标志位10
锁的分类(了解)
自旋锁
由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,线程的等待和挂起操作是需要用户态和内核态的来回上下文切换的,会严重影响性能。
自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置
自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置
自适应锁
自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定
偏向锁
考虑到即使加了sync锁,大多数场景下其实是不存在锁竞争的,经常是一个线程直接持有锁,而直接加锁会引起线程的阻塞,所以引入偏向锁。
当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。
当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。
轻量级锁
由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,线程的等待和挂起操作是需要用户态和内核态的来回上下文切换的,会严重影响性能。
JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁
JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁
重量级锁
主要有操作系统的互斥量控制,切换到内核态进行操作
锁的优化机制
锁消除
锁消除指的是JVM检测到一些同步的代码块,完全不存在锁竞争的场景,也就是不需要加锁,就会进行锁消除
比如StringBuffer是基于sync实现的,连续的调用append方法,因为是局部变量(栈私有)不可能被其他线程引用,会自动消除内部的锁
锁粗化
锁粗化指的是多次对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。
比如说一个线程里面多次对一个对象进行加锁,就会将锁的同步范围扩大到只加一次锁
锁升级
volatile 关键字
工作原理
可见性
线程在对volatile修饰的变量执行写操作是会立刻吧写入的只刷新到主内存,并且是使工中作内存副本失效
缓存一致性协议(MESI)
各个cpu会对总线进行嗅探机制,如果发现某个线程修改了某个缓存的数据,
cpu就会使工作内存的缓存过期,再次从主内存加载最新的数据
cpu就会使工作内存的缓存过期,再次从主内存加载最新的数据
MESI(了解)
M 已修改
Modified
Modified
该缓存行的数据被修改了,和主存数据不一致
监听所有想要修改此缓存行对应的内存数据的操作,
该操作必须等缓存行数据更新到主内存中,状态变成 S (Shared)共享状态之后执行
该操作必须等缓存行数据更新到主内存中,状态变成 S (Shared)共享状态之后执行
E 独占
Exclusive
Exclusive
该缓存行和内存数据一致,数据只在本缓存中
监听所有读取此缓存行对应的内存数据的操作,
如果发生这种操作,Cache Line 缓存状态从独占转为共享状态
如果发生这种操作,Cache Line 缓存状态从独占转为共享状态
S 共享
Shared
Shared
该缓存行和内存数据一致,数据位于多个缓存中
监听其他缓存使该缓存行失效或者独享该缓存行的操作,如果检测到
这种操作,将该缓存行变成无效
这种操作,将该缓存行变成无效
I 失效
Invaild
Invaild
该缓存行的数据无效
没有监听,处于失效状态的缓存行需要去主存读取数据
happens-before原则
原理
执行写操作的时候JVM会给cpu发送一条lock前缀指令
执行写操作的时候JVM会给cpu发送一条lock前缀指令
cpu会立即将这个值写回主内存(锁总线)
同时使其它cpu缓存中的副本失效,触发MESI
有序性
内存屏障禁止指令重排
是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成
JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令来禁止特定的指令重排序
是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成
JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令来禁止特定的指令重排序
什么是指令重排?
计算机底层字节码的执行顺序并不是按照你写的顺序
因素
编译器优化的重排
指令并行也可能重排
内存系统也会重排
应用
DCL单例模式
防止对象半初始化
四种底层指令类型
LoadLoad:保证load1的读取操作在load2及后续读取操作之前执行
LoadStore:在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoad:保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行
在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
不能保证原子性
如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性
解决
加锁
Atomic包下的类
应用
基于DCL懒汉式的单例模式
适合只有一个线程修改,其它线程读取的场景
比如作为状态位,A线程调取了某个方法让B线程感知到
Lock 接口
ReentrantLock
工作原理
CAS+AQS
线程先通过CAS操作请求state互斥量来获取锁,
如果获取成功,则写入线程ID,进行业务操作。
如果没有获取成功,就加入AQS队列,并且被挂起,
当锁释放以后,排在队首的线程会被唤醒,CAS方式重新获取锁
如果获取成功,则写入线程ID,进行业务操作。
如果没有获取成功,就加入AQS队列,并且被挂起,
当锁释放以后,排在队首的线程会被唤醒,CAS方式重新获取锁
AQS
实现
共享锁
CountDownLatch | Semaphore | CyclicBarrier
独占锁
ReentrantLock
原理
AQS的实现依赖内部的同步队列,队列内部维护了—个state变量和一个FIFO的双向链表,
如果当前线程竞争锁(修改state状态)失败,那么AQS会把当前线程加入到同步队列中,同时阻塞该线程。
当获取锁的线程释放锁以后,会从队列中唤醒队首的阻塞节点。
如果当前线程竞争锁(修改state状态)失败,那么AQS会把当前线程加入到同步队列中,同时阻塞该线程。
当获取锁的线程释放锁以后,会从队列中唤醒队首的阻塞节点。
公平锁 和 非公平性锁
如果是非公平锁,新进来的线程会以CAS的方式尝试竞争锁,竞争不到才去排队;
如果是公平锁,新来的线程会直接排到队尾,由队首的线程获取到锁
非公平锁比公平锁的的性能优化在:直接尝试竞争锁,竞争成功不会产生上下文的切换
重入锁
概念
如果当前线程已经持有锁了,释放锁之前线程自己是可以重复获取此锁的(state会累加)这就是可重入的概念,
但要注意获取多少次就要释放多少次,最后保证state能回到零态
但要注意获取多少次就要释放多少次,最后保证state能回到零态
原理
AQS的父类维护了一个属性,用来存储当前持有锁的线程信息,每次获取锁的时候对比线程看是否可重入
LockSupport
用于创建锁和其他同步类的基本线程阻塞原语,方法park()和unpark()提供了阻止和解除阻塞线程的有效手段
park():禁止当前线程进行线程调度,除非许可证可用
unpark(Thread thread):为给定的线程提供许可证(如果尚未提供)
StampedLock(没了解)
ReadWriteLock 接口
ReentrantReadWriteLock
原理
读写锁维护着一对锁,一个读锁和一个写锁。
通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升
通过分离读锁和写锁,使得并发性比一般的排他锁有了较大的提升
特性
公平性
公平锁和非公平锁
重入性
可重入锁
锁降级
遵循获取写锁,再获取读锁,最后释放写锁的次序,如此写锁能够降级成为读锁
Condition 接口
—个Condition实例本质上绑定到一个锁来使用,相当于一个队列,要获得特定Condition实例要调用 lock.newcondition0);
await():使当前线程等待发信号或 interrupted
signal():唤醒当前队列的一个等待线程
signalAll():唤醒所有等待线程
CountDownLatch
CountDownLatch,闭锁,就是一个基于 AQS 共享模式的同步计数器,它内部的方法都是围绕 AQS 实现的。
主要作用是使一个或一组线程在其他线程执行完毕之前,一直处于等待状态,直到其他线程执行完成后再继续执行。
主要作用是使一个或一组线程在其他线程执行完毕之前,一直处于等待状态,直到其他线程执行完成后再继续执行。
CountDownLatch 利用 AQS 的 state 变量充当计数器(由 volatile 修饰并使用 CAS 进行更新的),计数器的初始值就是线程的数量,每当一个线程执行完成,计数器的值就会减一,当计数器的值为 0 时,表示所有的线程都已经完成任务了,那么接下来就唤醒在 CountDownLatch 上等待的线程执行后面的任务。
CountDownLatch 与 CyclicBarrier 区别
缓存行(cache line)
为什么是64字节?
设置太小,读取速度快,但命中率太低
设置太大,读取速度慢,但命中率高
伪共享问题
多核多线程并发场景下,如果多核要操作的共享变量处于同一缓存行,某CPU更新该缓存行中的数据
,会导致其他处理器缓存中的缓存行失效,每次用还要去主存重新加载
,会导致其他处理器缓存中的缓存行失效,每次用还要去主存重新加载
解决:字节填充
long在jeva中占用8字节,在其前后额外填充7个long类型的变量
将目标变量放入缓存行时,可以实现一个缓存行中只有目标变量
将目标变量放入缓存行时,可以实现一个缓存行中只有目标变量
ThreadLocal
ThreadLocal是一种在多线程场景下基于副本的变量隔离机制,来保证共享变量的修改安全性
实现原理
为每个线程创建一个线程专属的变量副本,存储在线程的ThreadLocalMap里面,
每个线程只针对自己的变量副本进行修改,而不影响其它线程的变量副本
每个线程只针对自己的变量副本进行修改,而不影响其它线程的变量副本
源码分析
每个线程(Thread类)都有一个ThreadLocalMap对象 (threadLocals)
,key为threadLocal对象(t)的弱引用,value为对应的副本值
,key为threadLocal对象(t)的弱引用,value为对应的副本值
ThreadlocalMap数据结构
类似 HashMap 的key-value 键值对,底层实现是tablel数组,没有链表结构,使用开放定址法解决hash冲突
set()方法
源码
map.set(this, value);
底层 new Entry(key, value) 给数组赋值
Entry类继承自WeakReferenceEntry类的构造方法
中有 super(key):创建弱引用对象指向key (tl对象)
中有 super(key):创建弱引用对象指向key (tl对象)
弱引用防止内存泄漏
ThreadLocal<Person> tI = new ThreadLocal<>(); tl指向ThreadLocal对象,强引用
tl.set(new Person);=>map.set( key, Person); key是指向ThreadLocal对象的 弱引用
get()方法
拿到当前线程,获取map对象,通过Entry中的key获取value后返回
remove()方法
拿到当前线程,获取map对象,将map 中对应的key移除
使用方式
ThreadLocal<Object> threadLocal=new ThreadLocal<>();// 声明
threadLocal.set("thread1"); // 存储
threadLocal.get(); // 获取
threadLocal.set("thread1"); // 存储
threadLocal.get(); // 获取
内存泄漏问题
①外部强引用消失时,ThreadLocal对象可能无法回收,导致内存泄漏(jdk优化:通过弱引用己解决)
②ThreadLocal对象被回收后,map中Entry的value就访问不到了,如果线程一直运行则导致内存泄漏
解决② ThreadLocal用完后,一定要调用remove()方法(线程池慎用ThreadLocal)
应用场景
数据库的连接管理,每个线程都有自己的sqlSession
Spring中@Transactional注解,使用ThreadLocal存储数据库连接connection,
保证事务方法每次拿到的都是同一个connection
保证事务方法每次拿到的都是同一个connection
Spring中Bean在singleton作用域时,使用ThreadLocal解决共享变量的线程安全问题
线程本地变量/存储,避免了将对象作为参数传递的麻烦
延伸:把变量设置为static不是也可以传递吗?
static有局限性,多线程访问时会有数据污染
ThreadLocal可以保证每个线程访问私有的变量
面试题
1. Lock和synchronized的区别?
2. 要求使用两个线程,交替打印出 1A2B3C4D5E6F7G?
方式一:使用Lock结合Condition完成
方式二:使用LockSupport完成
方式三:使用synchronized和CountDownLatch
3. ReentrantLock如何避免死锁?
1. 响应中断 locklnterruptibly()
2. 可轮询锁 tryLock()
3. 定时锁 tryLock(long time)
4. lock()和tryLock()的区别?
5. lock和locklnterruptibly的区别?
面试题
1、说几条你遵循的多线程最佳实践
2、说说 CountDownLatch 与 CyclicBarrier 区别?
5.JAVA虚拟机(JVM)
JVM学习路线
1. 类加载机制
类加载运行全过程
类加载器
双亲委派机制
类加载器初始化过程
打破双亲委派机制(Tomcat)
2.JVM整体结构
整体结构
内存模型
逃逸分析
3.垃圾收集机制与算法
JVM对象创建
内存分配机制
判断对象可回收
判断类可回收
垃圾收集算法
4.垃圾收集器
Serial 收集器
ParNew收集器
Parallel Scavenge收集器
CMS收集器
GC流程
优缺点
G1收集器
GC流程
使用场景
内存模型
GC类型
如何选择收集器
5.调优实战
调优工具
阿里巴巴Arthas
GCEasy
GC日志
类常量池
5.1 JVM虚拟机结构
类加载子系统
概念
加载编译好的.class文件到JVM内存模型中,运行(类使用到才加载)
类加载机制
流程
加载 - 验证 - 准备 - 解析 - 初始化 - 使用 - 卸载
1.加载:将类字节码文件加载到内存中
2.验证:验证类字节码文件是否正确
3.准备:给类的静态变量分配内存,并赋默认值值
4.解析:将符号引用替换为直接引用
该阶段会把静态方法(符号引用,比如main()方法替换为指向数据所存内存的指针或句柄等(直接引用),
这是所谓的静态链接过程(类加载期间完成)
这是所谓的静态链接过程(类加载期间完成)
5.初始化:对类的静态变量初始化为指定的值,执行静态代码块
6.使用
7.卸载
类加载器
启动类加载器(Bootstrap-C实现):负责加载JVM运行的位于JRE( /lib ) 核心类库
扩展类加载器(extention-Java):负责加载JVM运行的位于JRE( /lib/ext)扩展类库
应用程序类加载器(System-Java):负责加载classpath路径下的包,自己编写的类
自定义加载器
继承 ClassLoder
实现 findClass (指定class 文件 解析到jvm中 )
双亲委派机制(代理模式的一种)
概念
加载类时总是向上查找顶级父加载器,从顶级加载器 bootstarp 中依次向下查找,父加载器都不存在的话才会执行本级类加载器
设计目的
保证类不会重复被加载
保障java核心类库的安全(沙箱机制)
源码实现
loadClass方法,递归循环判断父加载器是否存在,存在即优先使用父加载器加载
打破双亲委派机制
例子
tomcat
1.部署两个应用,可能会依赖同一个第三方类库的不同版本,保证相互隔离
2.web容器也有自己依赖的类库,不能与应用程序的类库混淆
3.部署在同一个web容器中相同的类库相同的版本可以共享
Spring
底层也实现了大量自己的类加载器
怎么打破?
1.实现自定义类加载,继承ClassLoder,实现findClass
2.(重点)重写loadClass方法,去除关于递归循环判断父加载器是否存在的逻辑,直接使用自定义类加载器加载
全盘负责委托机制
当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类
所依赖及引用的类也由这个ClassLoder载入
所依赖及引用的类也由这个ClassLoder载入
初始化
主动初始化
new(实例化对象)/getstatic(读取静态字段)/putstatic(设置静态字段 非常量量)/invokestatic(调用静态方法)
使用java.lang.reflect 进行反射调用时 如:Class.forName
初始化子类时会先初始化父类
虚拟机启动时首先初始化启动类
jdk 动态语言支持,java.lang.invoke.MethodHandle 示例最后的解析结果为(REF_getStatic REF_putStatic REF_invokeStatic REF_newInvokeSpecial) 四种类型方法句柄,并且这个方法句柄对一个的类没有进行初始化,则需要先触发其初始化
当一个接口定义了 jdk8新加入的 默认方法(被default 修饰的接口方法)如果接口有实现类发生初始化,接口要在其之前被初始化
被动初始化
典型和特殊
初始化
运行时常量(final String a = UUID.randomUUID().toString)
不初始化
数组
子类调用父类静态变量
调用静态常量(常量池)
接口初始化时不要求全部父接口初始化(特殊;jdk8之后的默认方法)
区分初始化和实例化
实例化不一定要在类初始化结束之后才开始(准备 阶段给 静态常量赋值 可以实例变量了)
父类构造器<clinit> -> 子类构造器<clinit> -> 父类成员变量和实例代码块 -> 父类构造函数
->子类的成员变量和实例代码块 -> 子类构造函数<init>
->子类的成员变量和实例代码块 -> 子类构造函数<init>
字节码加载子系统(执行引擎)
概念
加载执行字节码文件
组成
解释器(Interpreter)
即时编译器(JIT Compiler)
逃逸分析
概念:逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。
示例
经过逃逸分析的变量很有可能会被分配到栈上
分析器(Profiler)
垃圾回收器(Garbage Collection)
运行模式
解释模式(Interpreted Mode)
只使用解释器(-Xint强制使用),执行一行JVM字节码就编译一行,然后执行编译成的机器码
编译模式(Compiled Mode)
只使用编译器(-Xcomp强制使用),将所有JVM字节码全部编译为机器码,一次性执行所有机器码
混合模式(Mixed Mode)
JVM默认模式,依然使用解释模式执行代码,对一些“热点”代码采用编译模式执行
运行时数据区(JVM内存模型)
堆
年轻代(1/3)
Eden(8/10)
Survivor(2/10)
Form(1/2)
To(1/2)
老年代(2/3)
栈(线程)
栈帧
局部变量表
方法参数和方法内部定义的局部变量(成员变量或方法外的引用在堆中)
基本类型
引用类型
操作数栈
动态链接
在程序运行期间将符号引用转化为直接引用
方法出口(returnAddress)
native 方法
方法区(元空间)
存储常量、静态变量、类元信息
本地方法栈
Java调取的C实现的方法,由native修饰
是线程私有的
程序计数器
每执行一行字节码,则计数器+1,由于线程是并行的,或者进行STW的时候,程序知道运行到第几行
程序计数器是线程私有的
5.2 垃圾收集机制
JVM内存分配机制
1. 对象优先在Eden区分配
2.大对象直接进入老年代
可设置参数-XX:PretenureSizeThreshold 只在Serial和ParNew收集器下生效
3.长期存活的对象进入老年代
一般在Survivor区经历过15次MinorGC后即15岁
4.MinorGC中存活下来的对象年轻代放不下,部分进入老年代
5.对象动态年龄判断
Minor GC后,如果年龄<=10的所有对象内存之和>=Survivor区的50%, 则年龄10以上的的对象进入老年代
6.老年代空间分配担保机制
Minor GC前,判断年轻代所有对象内存之和>老年代剩余可用空间50%
👇🏻
是否配置担保参数:-XX:-HandlePromotionFailure
👇🏻
判断每次MinorGC向老年代放入对象的平均值>老年代剩余可用空间
进行Full GC,否则进行MinorGC
👇🏻
是否配置担保参数:-XX:-HandlePromotionFailure
👇🏻
判断每次MinorGC向老年代放入对象的平均值>老年代剩余可用空间
进行Full GC,否则进行MinorGC
7.Eden区和Survivor区(from区、to区)的比例为8:1:1
判断对象是否可回收(gc)
1.可达性分析法
GC ROOTS (由线程栈指向堆内的引用)
虚拟机栈(栈帧中的本地变量表) 引用的对象
方法区中类静态属性引用的对象
方法区中常量变量引用的对象
本地方法栈中JNI(native)引用的对象
活跃线程(已启动且未停止)的引用对象
三色标记法
白色、灰色、黑色
漏标问题的产生
。。。。
结局方案
CMS
增量更新,将黑色记录下来重新扫描
G1
原始快照,将灰色引用的白色记录下来,重新扫描
2.引用计数法
对象头 维护计数器
缺陷
1.无法区分软,虚,弱,强引用类型
2.出现死锁 两个对象相互引用
3.引用类型
强引用(不回收)
Object strongReference = new Object()
gc 绝不会回收 除非显式置为null或超出生命周期
软引用(条件回收)
String str = new String("abc");
SoftReference<String> softReference = new SoftReference<String>(str);
内存空间不足时回收
用于缓存的时候比较多
弱引用(回收)
WeakReference<String> weakReference = new WeakReference<>(str);
gc时直接回收
虚引用(回收)
ReferenceQueue queue = new ReferenceQueue();
// 创建虚引用,要求必须与一个引用队列关联
PhantomReference pr = new PhantomReference(str, queue);
不会决定对象的生命周期,任何时候都可能被回收
主要用来跟踪对象被垃圾回收器回收的活动、管理堆外内存
4.finalize()最终判定对象是否存活
1.对象没有覆盖finalize()方法,直接被回收
2.是否有必要执行,不会立即回收,可以覆盖该方法实现自救
垃圾收集算法
标记-清除算法
原理
遍历标记所有引用的对象,标记完成后,清除被标记的无引用对象
缺点
1.效率不高
2.容易产生内存碎片,并且需要维护一张内存空闲列表
标记-整理算法
原理
遍历标记所有引用的对象,标记完成后,将存活的对象移动到一端,清除掉端边界以外的内存。
缺点
1.效率不高
复制算法
原理
将内存分为大小相同的两块,每次只使用其中一块,将存活对象复制到未使用的一边,将另外一边一次清除
缺点
1.内存利用率不高
分代收集算法
原理
年轻代和老年代使用不同的清除算法
年轻代
使用复制算法
由于年轻代大量对象是需要被清除的,效率高的同时可以减少引用对象的复制
老年代
使用标记-清除、标记-整理
由于老年代大量对象不需要清除,且没有额外的空间对它进行分配担保,所以只需要很少的标记操作即可完成收集
Full GC触发条件
1.System.gc
2.未指定老年代和新生代大小,堆伸缩时会产生fullgc,所以一定要配置Xmx(堆最大内存)、Xms(堆初始化大小)、Xmn(年轻代大小)
3.老年代空间不足
4.空间担保分配失败
5.方法区(元空间)空间不足-JDK1.7时为永久代
垃圾回收器
垃圾收集器分类
Serial 收集器
特点
串行化、单线程收集器
子主题
算法
年轻代:复制
老年代:标记-整理
老年代:标记-整理
ParNew 收集器
特点
串行化,多线程收集器(Serial的多线程版本)
子主题
算法
年轻代:复制
老年代:标记-整理
老年代:标记-整理
Parallel Scavenge 收集器
特点
多线程、并发、并行。多核多CPU下的默认收集器,注重高吞吐量
算法
年轻代:复制
老年代:标记-整理
老年代:标记-整理
ParallelOld 收集器
特点
Parallel Scavenge 收集器的老年代版本
CMS 收集器
特点
真正意义上的并发收集器,实现垃圾回收线程与应用程序线程一起工作
,通过减少STW的时间而提高用户的体验
,通过减少STW的时间而提高用户的体验
缺点
1.对CPU资源敏感,会跟应用程序争夺CPU资源
2.浮动垃圾,在并发清理阶段还会再产生垃圾,只有下次gc清理
3.如果本次GC没处理完,又一次出发GC(concurrentmodefailure),则直接STW,然后使用Serial 收集器串行收集
4.由于使用标记-清除算法,所以会产生内存空间碎片,但是可以通过设置(-XX:+UseCMSCompactAtFullCollection),清除完整理
算法
老年代:标记-清除
G1 收集器
特点
主要面向多核大内存的服务器,保证GC的停顿时间及高吞吐量。
算法
整体:标记-整理
局部:复制算法
局部:复制算法
GC分类
Young GC(标记-整理)
条件1:Eden区放满:条件2:接近或者达到所期望的GC停顿时间200ms(默认)【-XX:MaxGCPauseMills】,否则对Eden进行扩容,即增加Eden的Region数量。
Mixed GC(复制算法)
老年代所占的内存>整个堆内存的45%(默认)【-XX:InitiatingHeapOccupancyPercen】,采用复制算法,如果剩余Region不足以支持Mixed GC完成,则直接进行Full GC
Old GC(Serial收集器)
STW,然后采用单线程进行标记、清理和压缩整理,空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的
内存模型
将堆分为大小相同的Region区,最多为2048个;
Region'的类型可能动态改变,比如由年轻代转为老年代
Region'的类型可能动态改变,比如由年轻代转为老年代
Eden
Survivor
Old
Humongous
优势
1.并行与并发
G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。
其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
其他收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2.弱化分代收集概念
G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
3.空间整合
与CMS的“标记--清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器,不会产生空间碎片;
从局部上来看是基于“复制”算法实现的
从局部上来看是基于“复制”算法实现的
4.可预测的停顿
降低停顿时间是G1和CMS共同的关注点,但G1可以根据用户期待的gc停顿时间指定回收计划
(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。
(通过参数"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。
特点整理
吞吐量优先
Parallel Scavenge、Parallel Old
交互少,计算多,适合在后台运算的场景
停顿时间优先
CMS
交互多,对响应速度要求高
串行并行并发
串行
Serial , Serial Old
并行
ParNew , Parallel Scavenge , Parallel Old
并发
CMS ,G1
算法
复制算法(年轻代)
Serial , ParNew , Parallel Scavenge , G1
标记-清除(老年代)
CMS
标记-整理(老年代)
Serial Old , Parallel Old , G1
选择策略
新生代
Serial
单线程
ParNew
多线程
Parallel Scavenge
老年代
CMS
Serial Old
Parallel Old
整堆
G1
搭配使用
CMS/Serial Old 解决Concurrent Mode Failure
Parallel Scavenge / Serial Old
JVM调优
JVM常见参数
-Xms 初始堆大小 -Xmx 最大堆 -Xmn 年轻代
jdk1.7
-XX:PermSize:表示非堆区初始内存分配大小(方法区)
-XX:MaxPermSize:表示对非堆区分配的内存的最大上限(方法区)。
jdk1.8
-XX:MetaspaceSize:方法区元空间
-XX:MaxMetaspaceSize:方法区元空间内存上限
-XX:PermSize:表示非堆区初始内存分配大小(方法区)
-XX:MaxPermSize:表示对非堆区分配的内存的最大上限(方法区)。
jdk1.8
-XX:MetaspaceSize:方法区元空间
-XX:MaxMetaspaceSize:方法区元空间内存上限
-XX:NewRatio 年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)
-XX:NewRatio=4表示年轻代与年老代所占比值为1:4
-XX:SurvivorRatio Eden区与Survivor区的大小比值
设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10
分析诊断相关
-XX:+-HeapDumpOnOutOfMemoryError:当OOMError产生时,自动Dump堆内存
-XX:HeapDumpPath:与HeapDumpOnOutOfMemoryError搭配使用,指定内存溢出时Dump文件的目录,默认为启动Java程序的工作目录下
-XX:OnError:发生致命错误时执行的脚本
-XX:OnOutOfMemoryError:抛出OOMError错误是执行的脚本
-XX:ErrorFile=fileName:致命错误的日志文件名,绝对路径或者相对路径
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=1506:开启远程调试
-XX:+-HeapDumpOnOutOfMemoryError:当OOMError产生时,自动Dump堆内存
-XX:HeapDumpPath:与HeapDumpOnOutOfMemoryError搭配使用,指定内存溢出时Dump文件的目录,默认为启动Java程序的工作目录下
-XX:OnError:发生致命错误时执行的脚本
-XX:OnOutOfMemoryError:抛出OOMError错误是执行的脚本
-XX:ErrorFile=fileName:致命错误的日志文件名,绝对路径或者相对路径
-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=1506:开启远程调试
通用
CMS
G1
GC日志
问题分类
OOM
堆OOM
java.lang.OutOfMemoryError: Java heap space
通过dump+EMA可以轻松定位。(EMA虽功能强大,但对机器性能内存要求极高)
程序在垃圾回收上花费了98%的时间,却收集不回2%的空间,通常这样的异常伴随着CPU的冲高,具体的表现就是你的应用几乎耗尽所有可用内存,并且GC多次均未能清理干净。
gc 耗费时间过长
Java.lang.OutOfMemeoryError:GC overhead limit exceeded
方法区OOM(8之前)
Java.lang.OutOfMemoryError: PermGen space
持久代主要存储的是每个类的信息,比如:类加载器引用、运行时常量池(所有常量、字段引用、方法引用、属性)、字段(Field)数据、方法(Method)数据、方法代码、方法字节码等等。我们可以推断出,PermGen的大小取决于被加载类的数量以及类的大小。
元空间OOM(8之后)
java.lang.OutOfMemoryError:Metaspace
主要原因:太多的类或太大的类加载到元空间。
线程数量不足
java.lang.OutOfMemoryError:Unable to create new native thread
Java应用程序已达到其可以启动线程数量的极限了。
交换空间耗尽
java.lang.OutOfMemoryError:Out of swap space?
表示交换空间也将耗尽,并且由于缺少物理内存和交换空间,再次尝试分配内存也将失败。
最大数组超出
java.lang.OutOfMemoryError:Requested array size exceeds VM limit
意味着你的应用程序试图分配大于Java虚拟机可以支持的数组。
OOM killer
Out of memory:Kill process or sacrifice child
当可用虚拟虚拟内存(包括交换空间)消耗到让整个操作系统面临风险时,就会产生Out of memory:Kill process or sacrifice child错误。在这种情况下,OOM Killer会选择“流氓进程”并杀死它。
内存溢出/内存泄漏
内存溢出
运行大型程序时发生,当程序所需要的内存远远超出了JVM内存所承受大小,就会报出OutOfMemoryError异常(称为OOM异常)。
原因
大量循环产生新对象
是否存在死循环和方法的无线递归调用
集合类中有对对象的引用,使用完后未清空,GC不能回收(内存泄漏)
是否一次性在数据库中读取了过多的数据
内存泄露
内存泄漏是指本应该被GC回收的无用对象没有被回收,导致的内存空间的浪费,当内存泄露严重时会导致OOM。
Java内存泄露根本原因是:长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被GC回收。
Java内存泄露根本原因是:长生命周期的对象持有短生命周期对象的引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被GC回收。
原因
静态集合类引起内存泄露
集合(Hash算法的集合)里面的对象属性被修改后,再调用remove()方法时不起作用(对象 重写 hashCode方法会出现)
监听器
数据库连接,io连接,socket
单例
如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露。
STW
native方法可以执行 但是不能和jvm交互
VM Threads
安全点 safe point
循环体的结尾
方法返回前
调用方法的call之后
抛出异常的位置
wait或者join
OopMap
记录哪些位置存放着对象引用,以便于快速完成根节点枚举过程
检测工具
jdk 自带
jmap
jmap -histo 进程号
实例个数以及占用内存大小
jmap -dump 进程号
堆内存信息
jstack
jstack 进程号
查看当前应用程序线程信息,查找死锁
jinfo
jinfo -flags 进程号
查看当前进程JVM设置信息
jstat
jstat -gc 进程号
查看应用程序的堆信息及gc情况
jconsole
可视化调优工具
jvisualvm
可视化调优工具
jmc
可视化调优工具
dump 快照
#出现 OOME 时生成堆 dump:
-XX:+HeapDumpOnOutOfMemoryError
#生成堆文件地址:
-XX:HeapDumpPath=/home/liuke/jvmlogs/
jmap -dump:format=b,file=20170307.dump 120808
用jvisualvm 装入 分析
实战
性能调优
设置堆的最大最小值 -Xms -Xmx
调整老年代和年轻代比例
看是否存在更多持久对象和临时对象
看峰值老年代 不影响gc就加大年轻
配置好的机器可以用 并发收集算法
每个线程 默认会开启1M的堆栈 存放栈帧 可以改为500k够用
原则 减少 stw
-XX:PretenureSizeThreshold
大于这个值的参数直接在老年代分配。 防止频繁full gc
一次查询过大数据 放到list
硬件
死锁排查
如果CPU 100%打满,系统疯狂GC的话,大概率是死锁的问题
1. top -c
找出当前进程的运行列表 -> 按下P进行排序 -> 找到占用最高的进程
2. top -Hp [pid]
找到占用最高的线程
3. jstack -l [pid] > ./[pid].stack
导出检查死锁的文件
4. cat [pid].stack |grep '[pid转十六进制]' -C 8
查看死锁文件
内存占用泄漏
1. jps
获取异常进程的pid
2. jmap -heap [pid]
查看当前进程的堆栈信息-> 主要看最下面的元空间和老年代空间占用是否够用,是否异常
3. jstat -gc [pid] 1000 10
打印GC信息,每隔1秒打印一次,一共打印10次
4. jinfo -flags [pid]
查看jvm配置参数,根据GC信息,调整参数
5. jmap -dump:format=b,file=heap pid
导出dump堆栈信息,导入JVisualVM查看
6. jmap -histo pid | head -n20
查看堆内存中的存活对象,并按空间排序
堆外内存溢出
如果内存占用超过了所配置的堆的最大内存,那就是堆外内存的溢出
unsafe提供了分配堆外内存的方法
1. pmap -x pid | sort -n -k3
查找堆外的线程排序
补充-常量池
面试题
1. gc分代年龄的时候默认为15,cms垃圾收集器默认为6,是否可设置大于15?
2.CMS和G1的区别是什么?
3.为什么说minorGC比fullGC要快?
4.如何确定对象是否在栈上分配?
5. G1垃圾收集器会占用很大一部分内存空间,是用来做什么?
6. 为什么标记整理算法比较适合老年代回收?
7. 标记清除算法标记的是引用的对象,比较适合年轻代回收,为什么CMS垃圾收集器还是使用标记清除算法呢?
4.数据库
关系型
基础
数据库三范式
第一范式(1NF):指定的列不可再拆分,一个列不可能存储两个不同的字段值
第二范式(2NF):1. 每个表必须拥有一个主键 2. 表中的其它字段必须完全依赖于主键值,不能部分依赖
第三范式(3NF):消除非主键列对主键的传递依赖,非主键列必须直接依赖于主键。
BC范式(BCNF):在 3NF 的基础上,消除主属性对于码部分的传递依赖
MySQL
存储引擎
InnoDB
存储特性(聚簇)
.frm:存储表结构
.idb:存储索引和表数据信息。即索引和表数据在一个文件中存储(聚簇)
其它特性
支持:行级锁、MVCC、事务、外键
全文索引:MySQL 5.6 以后开始支持
MyISAM
存储特性(非聚簇)
.frm:存储表结构
.MYI:存储索引信息。存储行数据所在的内存地址
.MYD:存储数据信息
其它特性
支持:全文索引
不支持:行级锁、MVCC、事务、外键
其它引擎比较
索引结构
优缺点
优势
快速检索,减少I/O次数,加快检索速度
根据索引分组和排序,加快速度
索引可以让查询锁定更少的行,减少查询之间的锁争用及提高并发性
劣势
索引本身是表,占用存储空间,一般来说,索引表占用的空间是数据表的1.5倍
索引表的维护和创建需要时间成本,随数据量增大
降低数据表的增删改的效率,因为要同步更新索引表
创建索引时需要对表加锁,因此实际操作中需要在业务空闲期间进行
分类
主键索引
根据主键建立索引,不允许重复,不允许空值
ALTER TABLE 'table_name' ADD PRIMARY KEY pk_index('col');
唯一索引
用来建立索引的列的值必须是唯一的,允许空值
ALTER TABLE 'table_name' ADD UNIQUE index_name('col');
普通索引
用表中的普通列构建的索引,没有任何限制
ALTER TABLE 'table_name' ADD INDEX index_name('col');
全文索引
用大文本对象的列构建的索引(char varchar text)
ALTER TABLE 'table_name' ADD FULLTEXT INDEX ft_index('col');
组合索引
用多个列组合构建的索引,这多个列的值不允许有空值
ALTER TABLE 'table_name' ADD INDEX index_name('col1','col2','col3');
遵循"最左前缀"原则,把最常用作为检索或排序的列放在最左,依次递减,组合索引相当于建立了col1,col1col2,col1col2col3 三个索引,而col2和col3是不能使用索引的
*在使用组合索引的时候可能因为列名长度过长而导致索引的key太大,导致效率降低,在允许的情况下,可以只取col1和col2的前几个字符作为索引
ALTER TABLE 'table_name' ADD INDEX index_name(col1(4),col2(3));
表示使用col1的前4个字符和col2的前3个字符作为索引
聚簇索引
聚簇索引
索引中键值的逻辑顺序决定了表中相应行的物理顺序(索引中的数据物理存放地址和索引的顺序是一致的)
一张表只能有一个聚簇索引(数据的存储方式是唯一的)
InnoDB
主键作为聚簇索引
不指定主键,将第一个不为空的唯一索引作为聚簇索引
没有合适的唯一索引则用隐藏的"GEN_CLUST_INDEX"的聚簇索引
MyIsam
不支持聚簇索引
叶子结点存储
主键值
事务ID
用于事务和MVCC回滚的指针
行指针
页号+页偏移量
用于直接去表空间中的页文件查找物理地址
优缺点
优点
1.可以将相关连的数据保存在一起
2.数据行的数据访问速度很快
3.使用覆盖索引查询的数据行可以直接使用叶子结点的主键值查询
缺点
1.数据插入速度严重依赖于插入的顺序,按照主键的顺序插入是加载数据到InnoDB表中速度最快的方式。
2.更新聚簇索引列的代价很高。该操作会强制InnoDB将每个被更新的行移动到新的位置。
3.导致页分裂,占用更多的磁盘空间。基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,可能面临”页分裂“的问题。
4.可能导致全表扫描变慢,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。
非聚簇索引(二级索引)
索引的逻辑顺序与磁盘上的物理存储顺序不同
需要两次B-Tree查找,先通过二级索引的叶子节点获得主键值,然后根据主键值去聚簇索引中找行
实现原理
Hash索引
仅memory(内存)存储引擎支持hash索引,利用索引列计算hashCode散列分布,不支持范围查找和排序
btree索引(非主键索引)
b+tree索引(主键索引)
与b-tree区别 所有的叶子节点包含了全部关键字的信息,及指向含这些关键字的记录的指针,
聚集索引 与磁盘紧挨,不用二次遍历
全文索引
仅用于MyISAM和InnoDB(5.6+),再生成FULLTEXT索引时,会为文本生成一份单词的清单,在索引时即根据这个单词的清单来索引
特殊语法 SELECT * FROM table_name MATCH(ft_index) AGAINST('查询字符串');
不支持中文,5.7后通过ngram 插件支持
索引失效
like 以%开头,索引无效;当like前缀没有%,后缀有%时,索引有效
or语句前后没有同时使用索引,当or左右查询字段只有一个是索引,该索引失效,只有当or左右查询字段均为索引时,才会生效
组合索引,不是使用第一列索引,索引失效
数据类型隐式转化,如varchar不加单引号的话可能会自动转换为int型,使索引无效,产生全表扫描。
在索引列上使用 IS NULL 或 IS NOT NULL操作。索引是不索引空值的,所以这样的操作不能使用索引,可以用其他的办法处理,
例如:数字类型,判断大于0,字符串类型设置一个默认值,判断是否等于默认值即可。
例如:数字类型,判断大于0,字符串类型设置一个默认值,判断是否等于默认值即可。
在索引字段上使用not,<>,!=。不等于操作符是永远不会用到索引的,因此对它的处理只会产生全表扫描。
优化方法: key<>0 改为 key>0 or key<0。
优化方法: key<>0 改为 key>0 or key<0。
对索引字段进行计算操作、字段上使用函数。(索引为 emp(ename,empno,sal))
当全表扫描速度比索引速度快时,mysql会使用全表扫描,此时索引失效。
查询结果的记录数量小于表中记录一定比例的时候
索引优化
explain
主要 type 访问类型( ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好))
索引明显实效
当range出现 row扫描为1 ROW的时候
扫描的行数远远多于表本身的数据量
key 索引字段
排除缓存
SELECT SQL_NO_CACHE ...
更新索引
analyze table t
强制指定使用哪个索引 (不建议用)
force index()
覆盖索引避免回表 不要用*
联合索引 不能无限建
合理安排联合索引的顺序
索引字段 最好是字节小的 自增
让B+树 维持较低的树高
常见阈值
2500万条 基本失效
稀疏性
页分裂/页合并
索引下推
数据库事务
基本特性(ACID)
原子性(Atomi):一个操作单元要么完全成功,要么完全失败
一致性(Consistency):事务完成时,必须所有的数据都保持一致的状态
隔离性(Isolation):并发事务之间互相隔离,操作互不影响
持久性(Durability):事务一旦提交,则数据库的修改就是永久性的,即修改刷新到磁盘
事务隔离级别
隔离级别
未提交读(read uncommitted)
允许一个事务读取另外一个事务未提交的数据
特性
脏读、不可重复读、幻读
提交读(read committed)
一个事务能读取另外一个事务 正在操作 但 已经提交 的数据,不能读取到当前操作但未提交的数据
特性
避免脏读;出现不可重复读、幻读
锁行(读锁)
可重复读(repeatable read)
一个事务不能读取到另外一个事务正在操作且未提交的数据
特性
避免脏读、不可重复读;出现幻读
锁行(写锁)
串行化(serializable)
所有的SQL必须按照顺序执行
特性
避免脏读、不可重复度、幻读
锁(查询、更新、插入)范围内的行数据,任何操作都加锁
安全问题
隔离性
一个事务的执行,不应该受到其他事务的干扰
脏读
一个事务读到了另一个事务未提交的数据
不可重复读
一个事务读到了另一个事务的已经提交的【update】的数据,导致多次查询结果不一致
虚读/幻读
一个事务读到了另一个事务已经提交的【insert】的数据,导致多次查询结果不一致
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
默认隔离级别
1. MySQL默认隔离级别【可重复读】
2. Oracle默认隔离级别【提交读】
只支持【提交读】和【串行化】
3. 查询隔离级别select @@tx_isolation
事务传播行为(嵌套事务)
PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务。
默认
PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。
MVCC
Multi-Version Concurrency Control 多版本并发控制,主要适用于RC(读已提交)RR(可重复读)
读未提交存在脏读,由于MVCC的创建版本和删除版本在事务提交后产生 不适用
保存某个时间点的快照
基本特征
每行数据都存在一个版本,每次数据更新时都更新该版本
修改时Copy出当前版本随意修改,各个事务之前无干扰
保存时比较版本号,如果成功(commit),覆盖原纪录;失败则放弃copy(rollback)
innoDB
额外保存两个隐藏列 (当前行创建时的版本号和删除时的版本号(可能为空))
每个事务存在自己的版本号,通过版本号比较达到事物间控制
Read View (事务开启时记录的快照)
read commited
每进行一次select 查询都生成一个readView
repeatable read
只在第一次进行select操作前生成一个 readView,之后重复使用
数据库锁
预测锁
死锁
查看在锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
杀死进程id(就是上面命令的trx_mysql_thread_id列)
kill 线程ID
数据库中的锁机制
锁的分类
锁的粒度
表级锁
最大粒度的锁级别,发生锁冲突的概率最高,并发度最低,但开销小,加锁快,不会出现死锁;
意向锁
读意向锁
写意向锁
自增锁(Auto-inc Locks)
特殊的表级锁,产生于这样的场景:事务插入(inserting into )到具有 AUTO_INCREMENT 列的表中。
行级锁
最小粒度的所级别,发生锁冲突的概率最小,并发度最高,但开销大,加锁慢,会发生死锁;
InnoDB的行锁是通过给索引上的索引项加锁来实现的。只有通过索引检索数据,才能使用行锁,否则将使用表锁
间隙锁
当使用范围查询而不是精准查询进行检索数据,并请求共享或排它锁时,
InnoDB会给符合范围条件的已有数据记录的索引项加锁;
对于键值在条件范围内但并不存在的记录,叫做间隙(GAP)。
InnoDB会给符合范围条件的已有数据记录的索引项加锁;
对于键值在条件范围内但并不存在的记录,叫做间隙(GAP)。
记录锁
当使用唯一索引,且记录存在的精准查询时,使用记录锁
页级锁
锁粒度界于表级锁和行级锁之间,对表级锁和行级锁的折中,并发度一般。开销和加锁时间也界于表锁和行锁之间,会出现死锁
锁的类型
共享锁
多个事务可以对同一数据行共享一把S锁,但只能进行读不能修改;
select ... in share mode
排它锁
一个事务获取排它锁之后,可以对锁定范围内的数据行执行写操作,在锁定期间,其他事务不能再获取这部分数据行的锁(共享锁、排它锁),只允许获取到排它锁的事务进行更新数据。
select ... for update
锁的策略
乐观锁
悲观锁
数据库引擎
Innodb
支持表锁(不使用索引加表锁)、行锁(只有在使用索引的时候才会使用)
MyISAM、Memory
表锁
BDB
默认使用页级锁,也支持表锁
解决的并发问题
排它锁
脏读的问题
共享锁
不可重复读的问题
临键锁
幻读的问题
数据库日志
回滚日志(undo log)
保证事务原子性,保证事务中的所有操作要么全部成功执行,要么全部失败回滚,
保存了事务发生之前的数据的一个版本,可以用于回滚,
保存了事务发生之前的数据的一个版本,可以用于回滚,
同时可以提供多版本并发控制下的读(MVCC)
重做日志(redo log)
持久性:当数据库崩溃的时候redolog可以将已提交的事务持久化,未提交的事务进行回滚
崩溃恢复:通过重做Redo日志中的操作,可以实现数据库的崩溃恢复。
读一致性:依赖Redo日志实现读一致性,protect保证读取数据的一致性
二进制日志(bin log)
主要用于数据备份、数据恢复、主从节点的数据同步
错误日志(errorlog)
记录着mysqld启动和停止,以及服务起在运行过程中发生的错误的相关信息.
慢查询日志(slow query log)
记录执行时间过长和没有使用索引的查询语句,
一般查询日志(general log)
记录了服务器接收到的每一个查询或是命令,无论正确与否,默认关闭
中断日志(relay log)
SQL执行过程
MySQL总是从一个表开始一直嵌套循环、回溯完成所有表关关联。所以MySQL的执行计划是一棵左侧深度优先的树。
两阶段提交
1. 写redolog,此时处于prepare阶段,
2. 写完redolog,通知执行器可以提交事务,开始写binlog,binlog写完,将redolog状态修改为commit
最后 将 BufferPool里面的更改以随机IO的方式刷回到磁盘
SQL的执行顺序
from -> on -> join -> where -> group by -> having -> select -> distinct -> order by -> limit
详情
Mysql发生崩溃怎么恢复?
1、如果redo log 和 binlog都存在,逻辑上一致,那么提交事务;
2、如果redo log存在而binlog不存在,逻辑上不一致,那么回滚事务;
函数应用
join
驱动表(sql 优化永远是小表驱动大表 被驱动表建立索引)
即:A join B 在B表建索引
count()函数
1. count(1)
查询非聚簇索引(二级索引)
2. count(name)
如果name有索引,则走该索引,但只获取name!=null的值
如果name没有索引,则不走索引
3. count(*)
查询非聚簇索引(二级索引)
4. count(id)
5.7版本之前是查询主键索引
5.7版本之后查询非聚簇索引(二级索引)
总结:效率==> 1>2==3>4 【二级索引相对主键索引存储数据更少,检索性能更高】
MySQL库复制
复制流程(binlog)
1. 在主库上将数据更改记录到二进制日志文件上
2. 备库将主库的二进制日志复制到中继日志中
3. 备库读取中继日志二进制数据重放到备库中
复制方式
基于行复制
优点
1. 几乎支持任何复制数据操作
2. 会记录数据的变更,有一个更好地数据变更记录
3. 基于行的二进制日志还会记录发生改变之前的数据,因此有可能有利于数据的修复
缺点
1. 不支持修改 表结构 及 schema 等操作
2. 行复制不知道SQL语句,像一个黑盒子,定位问题难
3. 如果是使用基于语句的复制模式,在备库更新一个不存在的记录时不会报错,但是基于行的复制模式下则会报错并停止复制
基于语句复制
优点
1. 支持更灵活的复制操作,比如修改表数据、表结构、schema
2.直接执行sql,比较直观,问题好定位
缺点
1. 无法复制包含数据库函数的数据,比如CURRENT_USER()、时间戳等
复制特性
复制不可以像扩展读操作已经将读分发到其它服务器上,复制只能扩展读操作,不能扩展写操作
对数据进行分区是唯一可以扩展写操作的方式
应用
主从复制
binlog记录操作日志 传递到从库
主备延迟
备库消费中转日志的速度,比主库生产bin log的速度要慢。
解决方案
mysql5.7 之前单线程写入 主备都要是5.7之后 均支持多线程写入(并行复制)
业务初期选择合适的分库分表策略,避免单标单库过大
sql 优化
避免全表扫描,考虑在where及order by涉及的列上建立索引
避免在where 子句中对字段进行null值判断
索引失效
避免在where子句中使用!=或<>操作符,改为>,<
索引失效
尽量避免在where子句中使用or来连接条件,替换为union all
能用between就不要用in
避免在where子句中对字段进行数据运算或函数操作
索引失效
exists代替in
不要使用*,用具体字段
模糊查询like不要在关键词前加%
索引失效
分库分表
mycat
唯一主键
ZK自增id 根据key 的hash 分 保证每个分片自增 聚簇索引快
UUID 字符串 无法排序 效率低
雪花 计算规则中有服务器时间 不能完全保证每个分片是不重复自增
面试题
1. 为什么InnoDB表必须有主键?
2. 为什么InnoDB的主键推荐使用整型的自增主键?
3. 为什么非主键索引结构的叶子结点存储的是表的主键值?
4. 使用主键索引(聚簇索引)查询和非主键索引(二级索引)查询数据过程有什么区别?
5. 索引是怎么支撑千万级表查找的?
6.如何生成全局唯一ID?
7.介绍一下你理解的MVCC?
8. 怎么保证数据不丢失?
9. 索引结构为什么采用B+树,而不是B树和红黑树?
扩展:HashMap底层为什么不使用B+树而使用红黑树?
10. Innodb存储引擎,查询数据的过程是什么样子的?或者说寻址的方式?
11. Innodb存储引擎,新增一行数据的过程是什么样子的?
12. Innodb存储引擎,删除一条行数据的过程是什么样子的?
13. Innodb存储引擎,删除数据时行指针删除,存储在页上的物理数据会不会删除掉?
14. Innodb存储引擎,稀疏性是指什么?怎么造成的?
15. Innodb存储引擎,稀疏性有什么影响?如何避免?
16. Innodb存储引擎,页分裂的概念?怎么造成的?
17. Innodb存储引擎,页分裂的影响?怎避免页分裂?
18. MySql的索引添加有没有限制条件?
19. MySql的索引过多会导致什么问题?
20. Mysql发生崩溃怎么恢复?
21. MySql发生死锁?怎么解决?
22. 什么是索引下推?
23. MySQL服务层的优化器是怎么优化SQL的?
24. MySQL int(1) 和 int(11)有什么区别?
25. MySQL 中 char和varchar的主要区别是什么?
26. 在执行一条update语句的时候,从bufferpool里面将数据更改写回到磁盘的时机有哪些?
27. MySql中的快照读和当前读?如果要达到一个当前读的目的,怎么操作呢?
28. MySql中exists和in的查询原理和使用注意事项?
Oracle
SQLServer
键值对型
Redis
数据结构
String
二进制安全,可以存任何类型数据
常用操作
使用场景
计数器
INCR article:readcount:{文章id}
GET article:readcount:{文章id}
Web集群session共享
SpringSession+Redis
分布式全局ID生成
INCRBY orderId 1000
分布式锁
Hash
数量较小的时候采用类似一维数组的方式来紧凑存储,数量增大的时候会自动转换为真正的hashMap
常用操作
使用场景
电商购物车
优缺点
优点
1. 同类数据归类整合储存,方便数据管理,比如用户信息,视频信息等
2. 相比string操作消耗内存与cpu更小
3. 相比string储存更节省空间
2. 相比string操作消耗内存与cpu更小
3. 相比string储存更节省空间
缺点
1. 过期功能不能使用在field上,只能用在key上
2. Redis集群架构下不适合大规模使用
2. Redis集群架构下不适合大规模使用
List
常用操作
使用场景
实现数据结构:队列、栈、阻塞队列
微博和微信公众号消息流
Set
内部实现为一个value永远为null的HashMap,没通过计算hash实现排重(包括判断成员时候存在)
常用操作
运算操作
使用场景
抽奖小程序
微信微博点赞,收藏,标签
子主题
集合操作实现电商商品筛选
ZSet
使用HashMap和跳跃表(SkipList) 保证数据的存储和有序,hashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员.
跳跃表
1. 跳跃表基于单链表加索引的方式实现
2. 跳跃表以空间换时间的方式提升了查找速度
3. Redis有序集合在节点元素较大或者元素数量较多时使用跳跃表实现
4. Redis的跳跃表实现由 zskiplist和 zskiplistnode两个结构组成,其中 zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistnode则用于表示跳跃表节点
5. Redis每个跳跃表节点的层高都是1至32之间的随机数
6. 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
2. 跳跃表以空间换时间的方式提升了查找速度
3. Redis有序集合在节点元素较大或者元素数量较多时使用跳跃表实现
4. Redis的跳跃表实现由 zskiplist和 zskiplistnode两个结构组成,其中 zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistnode则用于表示跳跃表节点
5. Redis每个跳跃表节点的层高都是1至32之间的随机数
6. 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。
查询复杂度log(n)
常用操作
运算操作
使用场景
微博热点排行榜
线程模型
持久化存储
RDB
Redis 将内存数据库快照保存在名字为 dump.rdb 的二进制文件中
触发条件
N 秒内数据集至少有 M 个改动,自动保存一次
数据集【save 60 1000】
数据集【save 60 1000】
持久化方式
save
同步(阻塞)
bgsave
写时复制(COW)机制:在生成快照的同时,依然可以正常处理写命令。
bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。
bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件
bgsave 子进程是由主线程 fork 生成的,可以共享主线程的所有内存数据。
bgsave 子进程运行后,开始读取主线程的内存数据,并把它们写入 RDB 文件
异步(不阻塞)
对比
AOF
将修改的每一条指令记录到文件【appendonly.aof】中,先写入OS cache,每隔一段时间fsync到磁盘
触发条件
1. appendfsync always:每次有新命令追加到 AOF 文件时就执行一次 fsync ,非常慢,但非常安全。
2. appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
3. appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择
2. appendfsync everysec:每秒 fsync 一次,足够快,并且在故障时只会丢失 1 秒钟的数据。
3. appendfsync no:从不 fsync ,将数据交给操作系统来处理。更快,也更不安全的选择
AOF命令重写
会根据配置的策略触发数据命令重写
原因:由于很多命令是冗余的造成文件过于庞大
会fork出一个子进程去做(与bgsave命令类似),不会对redis正常命令处理有太多
影响
影响
混合模式(4.0+)
前提必须先开启AOF持久化方式
在进行AOF重写的时候会将此刻的内存数据生成RDB快照保存到.aof文件中,之后的新数据再按照aof的命令方式保存
总结
Redis备份策略(了解)
1. 写crontab定时调度脚本,每小时都copy一份rdb或aof的备份到一个目录中去,仅仅保留最近48
小时的备份
2. 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份
3. 每次copy备份的时候,都把太旧的备份给删了
4. 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏
小时的备份
2. 每天都保留一份当日的数据备份到一个目录中去,可以保留最近1个月的备份
3. 每次copy备份的时候,都把太旧的备份给删了
4. 每天晚上将当前机器上的备份复制一份到其他机器上,以防机器损坏
集群方式
主从模式(不符合高可用)
主数据库(master) + 多个 从数据库(slave)
特点
主数据库进行读写操作,读写操作导致变化时同步到从数据库
从数据库一般是只读
slave挂了不影响其他的,重启自动读master的
master挂了,可以读不可以写
缺点
master节点唯一,若挂掉,无法提供写服务
工作原理
断点续传
优化应用
避免一主多从造成主从复制风暴(从库大量同步请求到主库)
脑裂问题
哨兵模式(Sentinel)
sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点
特点
建立在主从模式基础上,如果只有一个节点没有意义
master挂了以后,sentinel会在slave中选择一个作为master,会修改其他节点的配置文件
master重启后作为新的slave
sentinel启动多个形成集群,多sentinel时互相监控
sentinel或sentinel集群可以管理多个主从redis,多个sentinel也可以监控同一个redis
sentinel不要和redis部署在一台机器
client第一次直接连接sentinel集群获取master的地址,后续client读写命令会直接找master
如果节点主从发生变更,client会收到sentinel的订阅通知
监控机制
每个sentinel以每秒钟一次的频率向它所知的master,slave以及其他sentinel发送一个ping命令
一个实例距离最后一次有效回复ping命令的时间超过 down-after-milliseconds选项所指定的值,则会被标记为主观下线
一个master被标记为主观下线,则正在监视这个master的所有sentinel要以每秒一次的频率确认master进入了主观下线状态
当有足够数量的sentinel(大于等于配置文件指定的值)在指定的时间范围内确认master的确进去主观下线状态,将master标记为客观下线状态
一般情况下,每个sentinel会以每10秒一次向它已知的所有master,slave发送INFO命令
当master被标记为客观下线,INFO命令频率变为一秒一次
若没有足够数量的sentinel同意master已经下线,master的客观下线状态就会被移除;
若master重新向sentinel的 PING 命令返回有效回复,master的主观下线状态就会被移除
架构图
集群模式(Cluster 3.0+)
工作原理
Redis Cluster 将所有数据划分为 16384 个 slots(槽位)
每个节点负责其中一部分槽位,槽位的信息存储于每个节点中
每个节点负责其中一部分槽位,槽位的信息存储于每个节点中
客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地
key槽位定位算法
HASH_SLOT = CRC16(key) mod 16384
CRC16哈希后对16384取模
hash一致性算法(数据倾斜问题)
解决:每个节点虚拟出多个点
节点通信机制
集中式
优点:在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节
点读取的时候立即就可以立即感知到
点读取的时候立即就可以立即感知到
缺点:所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力
gossip(默认)
meet:某个节点发送meet给新加入的节点,让新节点加入集群中,
然后新节点就会开始与其他节点进行通信
然后新节点就会开始与其他节点进行通信
ping:每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过
ping交换元数据(类似自己感知到的集群节点增加和移除,hash slot信息等)
ping交换元数据(类似自己感知到的集群节点增加和移除,hash slot信息等)
pong: 对ping和meet消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新
fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了
选举原理
1. slave发现自己的master变为FAIL
2. 将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST 信息
3. 其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个
epoch只发送一次ack
epoch只发送一次ack
4. 尝试failover的slave收集master返回的FAILOVER_AUTH_ACK
5. slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两
个,当其中一个挂了,只剩一个主节点是不能选举成功的)
个,当其中一个挂了,只剩一个主节点是不能选举成功的)
6. slave广播Pong消息通知其他集群节点
注意:从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待
FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票
FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票
脑裂问题
redis集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点对外提供写服务,一旦网络分区恢复,
会将其中一个主节点变为从节点,这时会有大量数据丢失
会将其中一个主节点变为从节点,这时会有大量数据丢失
解决:min‐replicas‐to‐write 1 //写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制配置,比如
集群总共三个节点可以配置1,加上leader就是2,超过了半数
集群总共三个节点可以配置1,加上leader就是2,超过了半数
过期策略 6种
对于持久化来说,进行持久化之前会先判断是否过期,所以过期key对持久化无影响
默认使用
惰性删除+定期删除
能很好地兼顾到EXPIRE的精确性和过期删除的负载问题,在大多数场景下都很合适
过期删除(expire)
在key过期时自动删除,可以使用expire命令设置过期时间
优缺点
优点:过期精准,不存在过期key堆积的问题
缺点:大量key同时过期可能导致负载飙升
应用
分布式锁场景,过期删除
惰性删除(lazy expire)
key过期后不删除,在下次查询时再删除key,redis默认采用惰性删除策略
优缺点
优点:避免了过期删除的负载问题
缺点:可能会存在大量过期key堆积,占用内存
定期删除(active expire)
每隔一段时间就删除已经过期的key,可以通过配置redis.conf中的active-expire相关参数实现
优缺点
优点:比较平滑地删除过期的key
缺点:deletes操作比较频繁的话,对CPU有一定的影响
定时删除(volatile-ttl)
在设置key时就确定一个固定的时间点,在到达那个时间点时删除key
优缺点
优点:可以表示固定未来时间点的key,比较精确
缺点:使用场景不多,不够灵活
设置交集过期(object expire)
当一个key所引用的对象过期时,这个key也过期,redis3.2版本引入
优缺点
优点:可以实现object和key过期时间统一
缺点:适用场景不多,比较复杂
永不过期(no expire)
key永远不会过期,需要手动删除,可以使用persist命令取消一个key的过期时间设置
优缺点
优点:数据绝不会丢失
缺点:可能会有很多垃圾数据堆积
基于Redis的分布式锁
自己实现
ILock接口
SimpleRedisLock实现ILock
解决判断【锁是不是自己】+【删除锁】原子性操作,使用lua脚本
分布式锁(Redisson)
代码实现
RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
hotCacheLock.lock();
// 业务实现
hotCacheLock.unlock();
hotCacheLock.lock();
// 业务实现
hotCacheLock.unlock();
流程图
存在的问题
高并发场景下主从节点锁会存在失效的情况
1. 假如client1刚对master加锁成功了,
2. 此时master的锁数据还没同步到salve,然后master挂了
3. salve成为新的master主节点(其中未包含client1的锁信息)
4. client2再次请求加锁会成功,存在锁失效的问题
2. 此时master的锁数据还没同步到salve,然后master挂了
3. salve成为新的master主节点(其中未包含client1的锁信息)
4. client2再次请求加锁会成功,存在锁失效的问题
结局方案
zookepper
原理:过半数的集群节点获得锁信息后才返回客户端加锁成功
RedLock
原理:同样存储相同信息的redis集群,过半数的节点加锁成功后才算成功,存在问题
应用
1. 大厂缓存大规模商品的冷热分离优化
2. 应用问题
缓存击穿
场景
一个热点key失效的,大流量直接打到DB层
解决
1.为这个热点key设置永不过期时间,之后进行手动过期
2.使用互斥锁进行热点缓存重建,单机使用synchronized或lock来处理,分布式环境采用分布式锁
缓存穿透
场景
持续查询一个必然不存在的缓存数据,导致数据库宕机
黑客入侵
解决
1. 为查询不存在的key缓存空json数据
2. 布隆过滤器
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);
不支持删除操作
特性
只能判定一定不存在
不能判断是否一定存在
误判率 默认0.03
缓存雪崩
场景
大量缓存缓存失效、缓存服务器宕机,流量直接打到数据库
比如:突发大V热点,导致百万流量直接打到缓存层,redis最高支持十万的并发,导致缓存服务器宕机
解决
1. 缓存服务器根据承载能力限流降级
2.部署高可用的redis集群
3.设置多级缓存,使用JVM级的缓存,有选择缓存热点数据到jvm缓存中(或者专门监控热点数据的访问系统,发消息通知客户端缓存)
4.缓存预热(在正式部署前,将可能的热点数据预先放置到缓存中)
5.尽量为每个内存设置不同的随机失效时间
热点缓存重建
场景
同一时间大量并发流量查询缓存层中不存在的数据,导致多次执行查询数据库并更新到缓存层操作
比如:大促时,某大促热点商品未在缓存中,开启时大量请求同时发起,查询数据库更新缓存层并发
解决
1.使用DCL机制双重检测锁机制进行缓存查询操作
1. 查询缓存,存在就返回
2.使用按照商品ID加分布式锁(tryLock),再次查询缓存
3.第二次查询缓存,存在就返回,不存在就查询DB,并更新到缓存
2.使用按照商品ID加分布式锁(tryLock),再次查询缓存
3.第二次查询缓存,存在就返回,不存在就查询DB,并更新到缓存
缓存与数据库双写不一致
场景
查询DB更新缓存层时发生并发,导致DB与缓存层数据不一致
解决
1.使用Redisson分布式读写锁(ReadWriteLock),针对读缓存的操作添加读锁,针对写缓存的操作添加写锁
2.延时双删
1. 先淘汰缓存
2. 再写数据库(这两步和原来一样)
3. 休眠1秒,再次淘汰缓存(异步增加吞吐量)
3.缓存更新
1. LRU/LFU/FIFO算法剔除
2. 超时剔除
3. 主动更新(消息,数据库触发器等)
4. MySql的binlog机制+MQ
bigkey
控制优化
String
大小控制在10k以下
hash、list、set、zset
数量控制在1W以下
查找bigkey
bigkeys
redis 自带的命令,对整个 key 进行扫描,统计 string,list,set,zset,hash 这几个常见数据类型中每种类型里的最大的 key,会造成线程阻塞
string 类型统计的是 value 的字节数
4 种复杂结构的类型统计的是元素个数
memory usage keyname
memory 命令查看 key 的大小
rdb tools 工具包
rdbtools 是 python写的 一个第三方开源工具,用来解析 redis 快照文
除了解析 rdb 文件,还提供了统计单个 key 大小的工具
解决方案
1. 数据结构优化
优化 redis 的数据结构,使用合适的数据结构来存储数据
哈希结构降低单key大小
哈希结构降低单key大小
2. 数据分片
将大量数据分片存储到多个 key 中,避免单个 key 的数据量过大
3. 数据压缩
对于存储的大数据,可以采用压缩算法来减少数据的大小,redis支持多种压缩算法,如 LZF、Snappy 等
4. 分布式存储
将数据分散到多个 redis 实例中,避免单个 redis 实例存储过多数据导致 redis 大 key 的问题
5. 消息队列分批操作大key、拆分大key
系统影响
1. 内存占用:大key会占用大量内存,造成内存浪费。特别是当bigkey并不是非常活跃使用的时候,它STATIC的内存占用就显得非常糟糕。
2. 阻塞问题:当bigkey在重新哈希、ttl检查或者内存回收时,会长时间阻塞Redis的主线程,从而导致其他请求的堆积和延迟。
3. 慢查询:对bigkey的访问查询速度会变慢,因为Redis需要遍历更多内存或更多链表元素来查找键值。
4. 传输开销:当bigkey作为热点被频繁访问时,大量网络带宽会被消耗在大key的传输上。
5. 复制延迟:大key的修改会导致主从复制的延迟,可能会引起数据不一致。
6. 删除:删除、清空或者刷新bigkey都会耗费很大的性能开销。
7. 运维困难:bigkey难于管理,分析,需要专门工具来检测。
8. 内存碎片:随着bigkey的释放,会导致大块内存碎片。
2. 阻塞问题:当bigkey在重新哈希、ttl检查或者内存回收时,会长时间阻塞Redis的主线程,从而导致其他请求的堆积和延迟。
3. 慢查询:对bigkey的访问查询速度会变慢,因为Redis需要遍历更多内存或更多链表元素来查找键值。
4. 传输开销:当bigkey作为热点被频繁访问时,大量网络带宽会被消耗在大key的传输上。
5. 复制延迟:大key的修改会导致主从复制的延迟,可能会引起数据不一致。
6. 删除:删除、清空或者刷新bigkey都会耗费很大的性能开销。
7. 运维困难:bigkey难于管理,分析,需要专门工具来检测。
8. 内存碎片:随着bigkey的释放,会导致大块内存碎片。
删除大key
1. 在系统低峰期,直接使用 del 命令删除 (会造成线程阻塞)
2. 使用 scan 命令删除 (会造成线程阻塞)
3. 使用 unlink 异步删除
其它知识点
Redis Lua
1. 减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器
上完成。使用脚本,减少了网络往返时延。这点跟管道类似
上完成。使用脚本,减少了网络往返时延。这点跟管道类似
2. 原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。管道不是原子的,不过
redis的批量操作命令(类似mset)是原子的
redis的批量操作命令(类似mset)是原子的
3. 替代redis的事务功能:redis自带的事务功能很鸡肋,而redis的lua脚本几乎实现了常规的事务功能,
官方推荐如果要使用redis的事务功能可以用redis lua替代
官方推荐如果要使用redis的事务功能可以用redis lua替代
管道(Pipeline)
客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响
应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一
次命令执行的网络开销
应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一
次命令执行的网络开销
淘汰机制
redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略 LRU
LRU 从设置过期时间的数据集(server.db[i].expires)中挑选出最近最少使用的数据淘汰。没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失
服务器应用类型
io密集型
系统cpu性能相对于硬盘内存好很多,大部分是cpu在等I/O(硬盘/内存)的读写操作
一般线程数 = cpu核心数 * 2
数据库连接池
cpu密集型
计算密集型,系统的硬盘,内存性能相对于cpu要好很多,
一般线程数 = cpu核心数 +1
Redis各个版本
面试题
1. Reids是单线程的么?
2.Redis为什么这么快?
3. 介绍下Redis的 I/O 多路复用机制?
4. Redis为什么使用单线程?
5. Redis6.0为什么引入的多线程?
6. Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?
7. 集群是否完整才能对外提供服务?
8. Redisson分布式锁lock和tryLock的区别?
9. Redisson分布式锁锁失效的问题?有什么解决方案?
10. 大促高并发场景下如何优化分布式锁的效率?
11.假设有海量key和value都比较小的数据,如何存储才更省内存?
12. Redis 的ZSET结构为什么使用跳跃表而不使用红黑树或者B+树?
13. Redis中的淘汰策略LRU和LFU是怎么实现的?
文档型
MangoDB
特点
MongoDB 是一个面向文档存储的数据库,操作起来比较简单和容易。
MongoDB 记录中设置任何属性的索引 (如:FirstName="Sameer",Address="8 Ga
ndhi Road")来实现更快的排序。
ndhi Road")来实现更快的排序。
通过本地或者网络创建数据镜像,这使得 MongoDB 有更强的扩展性。
如果负载的增加(需要更多的存储空间和更强的处理能力) ,它可以分布在计算机网络中的其 他节点上这就是所谓的分片。
Mongo 支持丰富的查询表达式。查询指令使用 JSON 形式的标记,可轻易查询文档中内嵌的 对象及数组。
MongoDb 使用 update()命令可以实现替换完成的文档(数据)或者一些指定的数据字段 。
Mongodb 中的 Map/reduce 主要是用来对数据进行批量处理和聚合操作。
Map 和 Reduce。Map 函数调用 emit(key,value)遍历集合中所有的记录,将 key 与 value 传 给 Reduce 函数进行处理。
Map 函数和 Reduce 函数是使用 Javascript 编写的,并可以通过 db.runCommand 或 mapre duce 命令来执行 MapReduce 操作。
GridFS 是 MongoDB 中的一个内置功能,可以用于存放大量小文件。
MongoDB 允许在服务端执行脚本,可以用 Javascript 编写某个函数,直接在服务端执行,也 可以把函数的定义存储在服务端,下次直接调用即可。
搜索引擎型
ElasticSearch
相关网址
官网
下载地址
搜索引擎排行网址
介绍
由于支持倒排索引、列存储等数据结构,ES提供非常灵活的搜索分析能力
支持交互式分析,即使在万亿级日志的情况下,ES搜索相应时间也是秒级
ES拥有一套完整的日志解决方案(ELK),实现秒级从采集到展示
ES是一个分布式、高扩展、高实时的搜索与数据分析引擎。可以很方便的使大量数据具有搜索、分析和探索的能力。
ES具有水平伸缩性,能使数据在生产环境变得更有价值
体系构成
基本概念
index->database
type->table
document->row
field->cloumn
mapping->schema
优劣势
优势
1、高可用性、高扩展性
分布式架构,简单地横向扩容。可以轻松的对资源进行横向纵向扩缩容,满足不同数量级及查询场景对硬件资源的需求。
实现百台乃至万台机器搭建满足PB级的快速搜索,也能搭建单机版
实现百台乃至万台机器搭建满足PB级的快速搜索,也能搭建单机版
2、查询速度快、性能佳
底层采用Lucene作为搜索引擎,并进行了优化,保证用户对大量数据查询的需求。可用于复杂数据分析,海量数据的近实时处理等
3、搜索结果高度匹配用户意图
内部提供了完善的额评分机制,会根据粉刺出现的频次等信息对文档进行相关性排序,保证相关性越高的文档排序越靠前。
还提供了包括模糊查询,前缀查询,通配符查询等在内的多种查询手段,帮助用户快速高效检索数据。
还提供了包括模糊查询,前缀查询,通配符查询等在内的多种查询手段,帮助用户快速高效检索数据。
4、生态圈丰富,社区活跃
ELK日志解决方案:ES(日志数据存储)+Logstash(日志筛选过滤)+Kibana(数据可视化)
劣势
1、优点在于快速查询,在被作为数据库来使用是,写完马上查询会有延迟
2、ClickHouse相比ES做亿级别数据深度聚合需求会更加合适
3、不支持包含频繁更新、事务的操作
4、权限不太完善
与Solr的对比
Solr 是第一个基于 Lucene 核心库功能完备的搜索引擎产品,诞生远早于 Elasticsearch。
当单纯的对已有数据进行搜索时,Solr更快。
当实时建立索引时, Solr会产生io阻 塞,查询性能较差, Elasticsearch具有明显的优势
当单纯的对已有数据进行搜索时,Solr更快。
当实时建立索引时, Solr会产生io阻 塞,查询性能较差, Elasticsearch具有明显的优势
1、Solr 利用 Zookeeper 进行分布式管理,而Elasticsearch 自身带有分布式协调管理功能。
2、Solr 支持更多格式的数据,比如JSON、XML、CSV,而 Elasticsearch 仅支持json文件格式。
3、Solr 在传统的搜索应用中表现好于 Elasticsearch,但在处理实时搜索应用时效率明显低于 Elasticsearch。
4、Solr 是传统搜索应用的有力解决方案,但 Elasticsearch更适用于新兴的实时搜索应用。
2、Solr 支持更多格式的数据,比如JSON、XML、CSV,而 Elasticsearch 仅支持json文件格式。
3、Solr 在传统的搜索应用中表现好于 Elasticsearch,但在处理实时搜索应用时效率明显低于 Elasticsearch。
4、Solr 是传统搜索应用的有力解决方案,但 Elasticsearch更适用于新兴的实时搜索应用。
应用场景
日志实时分析
ELK的日志解决方案可以天然的支持全栈的日志分析,实现秒级采集到展示
搜索服务
全文索引
商品搜索
时序分析
时序数据的特点是写入吞吐量特别高,ES支持的同事也提供了丰富的多维统计分析算子
典型场景是监控数据分析
物联网场景
数据分析
数据监控
查询服务
后端存储
ELK日志解决方案
问题背景
方案
相关组件
Filebeat
是一个日志文件托运工具,在你的服务器上安装客户端后,filebeat会监控日志目录或者指定的日志文件,
追踪读取这些文件(追踪文件的变化,不停的读)【Ruby语言】
追踪读取这些文件(追踪文件的变化,不停的读)【Ruby语言】
Logstash
【如果对日志做深加工就需要这个组件】是一根具备实时数据传输能力的管道,负责将数据信息从管道的输入端传输到管道的输出端;
与此同时这根管道还可以让你根据自己的需求在中间加上滤网,Logstash提供里很多功能强大的滤网以满足你的各种应用场景
与此同时这根管道还可以让你根据自己的需求在中间加上滤网,Logstash提供里很多功能强大的滤网以满足你的各种应用场景
ElasticSearch
输出存储、分布式多用户能力的全文搜索引擎
Kibana
数据可视化输出
Kafka
用于日志输出的消息中间件
在实际应用场景下,为了满足大数据实时检索的场景,利用Filebeat去监控日志文件,将Kafka作为Filebeat的输出端,Kafka实时接收到Filebeat后以Logstash作为输出端输出,到Logstash的数据也许还不是我们想要的格式化或者特定业务的数据,这时可以通过Logstash的一些过滤插件对数据进行过滤最后达到想要的数据格式以ElasticSearch作为输出端输出,数据到ElasticSearch就可以进行丰富的分布式检索了。Kibana可以将ElasticSearch里的数据很好的展示给用户使用
ES集群优化策略
1. 数据建模
尽量将数据先行计算,然后保存到Elasticsearch 中。尽量避免查询时的 Script
计算
计算
尽量使用Filter Context,利用缓存机制,减少不必要的算分
结合profile,explain API分析慢查询的问题,持续优化数据模型
避免使用*开头的通配符查询
2.优化分片
避免Over Sharing:一个查询需要访问每一个分片,分片过多,会导致不必要的查
询开销
询开销
控制单个分片的大小:Search: 20GB Logging: 40GB
Force-merge Read-only索引:使用基于时间序列的索引,将只读的索引进行force merge,
减少segment数量
减少segment数量
3.建模时的优化
只需要聚合不需要搜索,index设置成false
不要对字符串使用默认的dynamic mapping。字段数量过多,会对性能产生比
较大的影响
Index_options控制在创建倒排索引时,哪些内容会被添加到倒排索引中。
如果需要追求极致的写入速度,可以牺牲数据可靠性及搜索实时性以换取性能:
牺牲可靠性: 将副本分片设置为0,写入完毕再调整回去
牺牲搜索实时性︰增加Refresh Interval的时间
牺牲可靠性: 修改Translog的配置
不要对字符串使用默认的dynamic mapping。字段数量过多,会对性能产生比
较大的影响
Index_options控制在创建倒排索引时,哪些内容会被添加到倒排索引中。
如果需要追求极致的写入速度,可以牺牲数据可靠性及搜索实时性以换取性能:
牺牲可靠性: 将副本分片设置为0,写入完毕再调整回去
牺牲搜索实时性︰增加Refresh Interval的时间
牺牲可靠性: 修改Translog的配置
Splunk
Solr
6.消息中间件
6.0 MQ的作用
优点
异步通信
提高系统的响应速度、吞吐量
对于数据量大的操作,不需要客户端等待,减少客户端性能消耗
系统解耦
减少服务之间的影响,提高系统整体的稳定性、可扩展性、可扩展性
解耦后可以实现数据分发。生产者发送一个消息后可以由多个消费者消费,与消费者的增加减少无关
流量削峰
稳定系统资源应对突发的流量冲击
流量承接下来放到服务器上,处理完毕后再返回结果。添加消费者可以提高处理速度,达到保护应用和数据库的作用
缺点
系统复杂度增加
以前服务之间可以进行同步的服务调用,引入MQ后,会变为异步调用,数据的链路就会变得更复杂。
如何保证消费不会丢失?
不会被重复调用?
怎么保证消息的顺序性等问题。
不会被重复调用?
怎么保证消息的顺序性等问题。
系统可用性降低
系统引入的外部依赖增多,系统的稳定性就会变差。
一旦MQ宕机,对业务会产生影响。
需要考虑如何保证MQ的高可用。
一旦MQ宕机,对业务会产生影响。
需要考虑如何保证MQ的高可用。
消息一致性问题
A系统处理完业务,通过MQ发送消息给B、C系统进行后续的业务处理。
如果B系统 处理成功,C系统处理失败怎么办?
需要考虑如何保证消息数据处理的一致性。
如果B系统 处理成功,C系统处理失败怎么办?
需要考虑如何保证消息数据处理的一致性。
6.1 RabbitMQ
工作模型
Producer 生产者
消息生产者,比Broker建立TCP长连接
通过Channel链接,只进行创建或者释放Channel连接,不需要断开长连接,节省资源
Connection 连接
生产者和消费者必须跟Broker建立一个TCP长连接
Channel 信道
AMQP引入的概念,是虚拟链接,只需要创建和释放Channel,大大减少资源消耗
相互隔离,每个Channel有自己唯一的ID
原生API重要的编程接口,调用的都是Channel接口上的方法
Broker 主机
MQ的服务端,默认端口5672
VHost 虚拟主机
实现同一个服务器的复用,可以在一个RabbitMQ集群中划分出多个虚拟主机,每一个虚拟主机都有AMQP的全套基础组件,并且可以针对每个虚拟主机进行权限以及数据分配,并且不同虚拟主机之间是完全隔离的。
Exchange 交换机
路由消息,根据规则分发消息,实现多对多的绑定关系
路由方式
Direct 直连
使用RoutingKey完全匹配到接收队列中
适用于业务目的明确的场景
Topic 主题
使用支持通配符的绑定键
#:代表0个或者多个单词
*:代表1个单词
单词:使用“.”隔开 如 a.b.cdef
适用于根据业务主题过滤消息的场景
Fanout 广播
无需绑定键,将Exchange中所有的消息都发送到与Exchange绑定的Queue中
适用于通用类业务进行广播
Headers 头交换
通过匹配AMQP消息的header而非路由键
性能差,不实用
Binding 绑定
交换机与队列建立绑定关系
Queue 队列
作用
Broker有一个对象才存储消息,就是数据结构的队列
实际上用于数据库存储消息,Erlang开发
队列是生产者跟消费者之间的纽带,生产者发送消息到队列,消费者从队列消费消息
类型
Classic 经典队列
单机环境中,拥有比较高的消息可靠性。
可以选择是否持久化(Durability)和是否自动删除(Auto delete)属性
是否可持久化选择和拥有独占队列
Quorum 仲裁队列
介绍
3.8.0+版本
分布式环境下对消息的可靠性保证更高
基于Raft一致性协议实现的分布式消息队列,实现了持久化、多备份的FIFO队列,针对镜像模式设计
队列中的消息需要多半数节点确认后才会写入队列,类似于RocketMQ中的DLedger集群
固定持久化,没有独占队列,增加Poison Message(毒消息)
场景
适用
队列长期存在,并且对容错、数据安全严格,但是对性能要求稍低的情况
电商系统的订单,处理速度可以慢一点,但是订单不能丢失
不适用
临时使用的队列,比如transient临时队列,exclusive独占队列,或者经常 会修改和删除的队列。
对消息低延迟要求高,消息一致性要求会影响消息的延迟性
对数据安全性要求不高
队列消息积压严重的场景
Stream 队列
介绍
3.9.0+版本
持久化到磁盘并且具备分布式备份的,更适合于消费者多,读消息非常频繁的场景。
Stream队列的核心是以append-only只添加的日志来记录消息,
整体来说,就是消息将以append-only的方式持久化到日志文件中,
然后通过调整每个消费者的消 费进度offset,来实现消息的多次分发
整体来说,就是消息将以append-only的方式持久化到日志文件中,
然后通过调整每个消费者的消 费进度offset,来实现消息的多次分发
场景
适用
消费者多,读消息非常频繁的场景
特点
large fan-outs 大规模分发
Replay/Time-travelling 消息回溯
Throughput Performance 高吞吐性能
Large logs 大日志
Routing Key
路由键值
Consumer 消费者
消息的消费者
消费模型
Pull
消息存放在服务器端,消费者主动获取才能拿到消息
优点:根据消费者的能力决定获取消息的频率
缺点:每隔一段时间获取消息,消息试试性会比较低
Push
通过事件机制对队列进行监听,只要生产者发送消息到服务器,马上推送给消费者
优点:消息实时性高
缺点:不会根据消费者能处理的消息能力做判断,会造成消息积压
一个消费者可以监听多个队列,一个队列也可以被多个消费者监听
建议一个消费者只处理一个队列的消息,如果需要提升消息处理能力,可以增加多个消费者,消息进行轮询
工作流程
生产者
(1)Producer 先连接到 Broker,建立连接 Connection,开启一个信道 channel
(2)Producer 声明一个交换器并设置好相关属性
(3)Producer 声明一个队列并设置好相关属性
(4)Producer 通过绑定键将交换器和队列绑定起来
(5)Producer 发送消息到 Broker,其中包含路由键、交换器等信息
(6)交换器根据接收到的路由键查找匹配的队列
(7)如果找到,将消息存入对应的队列,如果没有找到,会根据生产者的配置丢弃或者退回给生产者。
(8)关闭信道
(2)Producer 声明一个交换器并设置好相关属性
(3)Producer 声明一个队列并设置好相关属性
(4)Producer 通过绑定键将交换器和队列绑定起来
(5)Producer 发送消息到 Broker,其中包含路由键、交换器等信息
(6)交换器根据接收到的路由键查找匹配的队列
(7)如果找到,将消息存入对应的队列,如果没有找到,会根据生产者的配置丢弃或者退回给生产者。
(8)关闭信道
消费者
(1)Producer 先连接到 Broker,建立连接 Connection,开启一个信道 channel
(2)向 Broker 请求消费相应队列中消息,可能会设置响应的回调函数。
(3)等待 Broker 回应并投递相应队列中的消息,接收消息。
(4)消费者确认收到的消息,ack。
(5)RabbitMQ从队列中删除已经确定的消息。
(6)关闭信道
(2)向 Broker 请求消费相应队列中消息,可能会设置响应的回调函数。
(3)等待 Broker 回应并投递相应队列中的消息,接收消息。
(4)消费者确认收到的消息,ack。
(5)RabbitMQ从队列中删除已经确定的消息。
(6)关闭信道
AMQP 协议
什么是AMQP协议
类似于JDBC协议,通过相同的API连接不同的数据库
本质上是一种进程间传递异步消息的网络协议,跨语言,跨平台,只要遵守AMQP协议,就可以实现消息的交互
AMQP结构
Module Layer:位于最高层,主要定义一些供客户端调用的命令,实现客户端的业务逻辑
Session Layer:位于中间层,负责将客户端的命令发送到服务端,再降客户端与服务端之间的通讯提供可靠性同步机制
Transport Layer:位于最底层,负责传输二进制数据流,提供帧的处理,信道复用、错误检测和数据表示
六种消息类型
简单模式
一个生产者,一个消费者,不需要设置交换机(使用默认的交换机)
工作队列模式 Work Queue
一个生产者、多个消费者(竞争关系),不需要设置交换机
发布订阅模式 Publish/Subscribe
需要设置类型为fanout类型的的交换机,并且交换机和队列进行绑定
当发送消息到交换机后,交换机会将消息发送到绑定的队列上
路由模式 Routing
需要设置类型为direct的交换机,交换机和队列进行绑定,并指定routing key
当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列
通配符模式 Topic
需要设置类型为topic的交换机,交换机和队列进行绑定,并指定通配符方式的routing key
当发送消息到交换机后,交换机会根据routing key将消息发送到对应队列
RPC模式
不常用
MQ进阶
延迟队列
实现方式
1. 消息过期+死信队列
消息设置过期
使用死信队列指定消息过期时间
xdead-letter-exchange交换机跟队列进行绑定
消息流转过程
1.生产者生产消息,通过默认交换机路由到默认队列,但是无消费者进行消费
2.设置x-dead-letter-exchange;消息到达TTL之后进入私信交换机
3.死信交换机与死信队列进行绑定,路由到死信队列,最后有消费者进行消费
队列设置过期
x-message-ttl:设置一个统一过期时间
缺点
如果消息也设置了过期时间,会选择过期时间较小的时间
实现不同阶梯(2分一次、4分一次)的消息投递,需要创建很多队列
2.RabbitMQ插件
通过rabbirmq-delayed-message-exchange 插件实现
创建x-delayed-message交换机,指定投递的时间
推荐使用,更方便
应用场景
订单在30分钟之内未支付,则自动取消订单
工单在60分钟之内未处理,则发送消息提醒
预定会议室,在预定时间钱10分钟,通知各参会人员
死信队列
条件
1. 队列达到长度的限制 x-max-length
2. 队列达到最大字节限制 x-max-length-bytes
3. 消息达到设置的TTL还未被消费
4. 消息被消费者拒绝,且未配置重回队列(requeue=false)
其它
死信在转移到死信队列时,他的Routing key 也会保存下来。
但是如果配置了x- dead-letter-routing-key这个参数的话,routingkey就会被替换为配置的这个值
但是如果配置了x- dead-letter-routing-key这个参数的话,routingkey就会被替换为配置的这个值
死信在转移到死信队列的过程中,是没有经过消息发送者确认的,所以并不能保证消息的安全性
如何确定一个消息是不是死信?
消息被作为死信转移到死信队列后,会在Header当中增加3个头信息,
这三个属性在以后的传递过程中都不会更改。
这三个属性在以后的传递过程中都不会更改。
应用
对头的消息会被丢入死信队列,不适合做秒杀
优先级队列
x-max-priority设置
惰性队列
队列有两种类型:default(默认) 和 lazy,可以设置x-queue-mode=lazy 为惰性队列
尽可能的将消息存入磁盘中,而在消费者消费消息时才会被加载到内存中,支持更多的消息存储
如何实现消息流量控制策略?
流量控制
服务端控流
设置队列长度
内存控制:默认40%
磁盘控制:默认30%
消费端限流
方法channel.basicQos(int prefetch_size, int prefetch_count, boolean global) 中
配置prefetch_count,最多同时处理的消息数量
配置prefetch_count,最多同时处理的消息数量
非自动确认消息的前提下,如果一定数目的消息未被确认,不进行新消息的推送
消费优先级
可以设置x-priority参数指定队列的优先级
默认先根据round-robin策略处理,添加上面配置后,先根据x-priority,后根据round-robin
prefetch count 和x-priority:两个参数实际上都是为了配置当前消费节点的消息吞吐量。
当消费者集群中的业务处理能力或者消息配置不一样时,可以通过给不同的消费节点配置不同的 prefetch_count,
再结合消费优先级的配置来实现流量控制策略。
当消费者集群中的业务处理能力或者消息配置不一样时,可以通过给不同的消费节点配置不同的 prefetch_count,
再结合消费优先级的配置来实现流量控制策略。
Federation Plugin 远程数据分发插件
应用问题
消息丢失)
丢消息的场景
1.生产者发送消息丢失
发消息的过程中,由于是网络传输,消息丢失
2.消息队列持久化存储丢失
MQ收到消息后消息存在内存中就挂了,未持久化
3.消费者消费消息丢失
自动应答模式下,消费者拿到消息后还没来及消费就挂了,MQ认为消息已经消费了
4..MQ集群主从同步时消息丢失
master消息未同步到slave时主节点挂了
解决方案
写在最前,任何用户态的应用程序都无法完全保证数据的完全安全,只是一个性能和可靠性的权衡
1.生产者发送消息丢失
a. 两种生产者确认机制
同步确认机制
普通确认模式
发送一条,确认一条
优点:可靠性高
缺点:效率太低
批量确认模式
多次发送,确认一次
优点:效率高
缺点:确认一旦失败无法确定是哪一条失败,只能重新发送
异步确认机制
一边发送,一边确认,维护一个标识,已确认的移除,留下的事未确认的
推荐使用此种方式
b. 事务模式
将channel设置成事务模式,需要手动提交事务或者回滚
缺点:事务模式是阻塞的,消息没发送完毕需要阻塞,导致MQ吞吐量下降
消息补偿机制
生产方发送消息时,要添加try catch机制,在catch中捕获异常,并将MQ发送的关键内容记录到日志中,
日志表中要有消息发送状态,若发送失败,则有定时任务定期扫描重新发送
日志表中要有消息发送状态,若发送失败,则有定时任务定期扫描重新发送
2.消息队列持久化丢失
对于Classic经典队列,将队列声明为持久化队列
对于Quorum和Stream队列本身就是持久化的
持久化配置可以和生产者的 confirm 机制配合使用,在消息持久化磁盘后,再给生产者发送一个Ack信号。
这样的话,如果消息持久化磁盘之前,即使 RabbitMQ 挂掉了,生产者也会因为收不到Ack信号而再次重发消息。
这样的话,如果消息持久化磁盘之前,即使 RabbitMQ 挂掉了,生产者也会因为收不到Ack信号而再次重发消息。
3.消费者消费消息丢失
指定消息自动应答(autoAck=true)
MQ将消息成功发送给消费者,自动应答,如果消费者异常或者直接宕机了,发生消息丢失
指定手动应答
业务处理完毕后发送ACK保证靠可靠性
SpringBoot
NONE:类似于MQ原生的autoACK,消息发送成功就应答,而不是不应答,会发生丢失
MANUAL:手动ACK
AUTO:消费者消息被成功处理,没有发生异常,会自动应答,不会发生丢失,会再次发送消息
4.MQ主从同步时消息丢失
使用镜像集群模式即可
或者开启Federation插件,联邦机制,给包含重要消息的队列建立远程备份
5. 其它保障措施
消息补偿机制
定义超时时间,超时重发,使用定时任务扫描业务表,或者消息表
最终一致性
约定标准,以核心系统数据为准,拿到文件,进行解析手工对比
幂等性(不重复消费)
场景
生产者,未收到MQ发送成功的ACK,重新发送了消息
消费者,在消费消息时发生异常或者处理超时,MQ端未收到ACK,重新发送消息给消费者
解决
主要针对消费端做好幂等性控制
对每个消息生成一个唯一的业务ID,当消费者消费消息时则将唯一业务ID setnx到redis中并设置过期时间
消费方的Message对象有个gerRedelivered()方法返回Boolean,为true就表示重复发送过来的
消息堆积
原因
消息的生产速率远远高于消息的消费速率
消费者宕机
消费者设置手动ACK,然后处理之后没有处理返回ACK,导致大量的NACK的消息在队列中堆积
解决
生产端
由于生产者生产消息的速率往往是根据业务决定的,所以无法过多优化
但是可以多采用批量消息的方式传入MQ,降低IO频率
MQ服务端
懒加载机制
创建Sharding分片队列
合适的场景使用Stream队列,致力于解决服务端消息堆积能力
消费端
查找消费端为什么消费慢的问题,如果是因为代码问题,则进行优化和修复
增加消费者数量
修改配置适当提高吞吐量
其它
紧急上线一个消费者组,用来将消息快速转录,保存到数据库或者Redis,慢慢处理,防止消息丢失
顺序性
场景
一个队列有多个消费者,消费速率不一致无法保证顺序
比如一个下单过程,需要先扣款,然后扣减库存,然后通知快递发货,
几个消费者同时监控一个队列消息,无法保证消息的处理顺序
几个消费者同时监控一个队列消息,无法保证消息的处理顺序
解决
一个队列只有一个消费者才能保证顺序消费(配置prefetch=1)
这种是以极度消耗性能为代价的,应当尽量避免这种场景
不同业务消息发送到不同的专用队列
最终一致性
用户态的应用程序,无法完全保证达到最终一致性,需要的话,需要程序记录,人工介入
高可用(集群)
问题
25672端口进行集群部署,无法在广域网搭建集群,需要插件
节点类型
内存节点:将元数据存放在内存中。优势:读写更快,用内存节点提供应用访问
磁盘节点:默认将元数据放在磁盘中,用磁盘节点做数据备份
集群模式
主备模式
不同节点之间只会相互同步元数据,而不会同步消息
读写都在主节点上面,备用节点不进行任何的读写操作,只用来实现当主节点宕机的情况下能顶上去
优点:速度快,只同步元数据
缺点:只同步元数据,不同步消息,节点失效造成消息丢失
镜像模式
节点之间会同步元数据及消息,保证100%数据不丢失,主要的目的在于保证数据的高可靠性
优点:稳定,可用性高,消息可靠性强
缺点:会降低系统性能,节点过多导致同步代价大
远程模式
远程模式能够实现双活的一种模式,又被叫做Shovel模式,它可以将消息进行不同数据中心的复制工作
,能够跨地域的让两个MQ集群互联
,能够跨地域的让两个MQ集群互联
多活模式
实现异地数据复制的主流模式,因为Shovel模式配置比较复杂,所以一般使用异地集群都是使用双活或者多活来实现
主要使用 (federation联邦插件)保证实现
多节点负载均衡
HAProxy+Keepalived
持久化机制
持久化分类
队列持久化
durable=true
消息持久化
BasicProperties中设置deliveryMode=2
交换机持久化
durable=true
注意
需要持久化的,会被保存到队列中,并且保存到内存,最后回写磁盘
不需要持久化的,会保存到内存,等内存到阈值,会被刷入磁盘
如果Exchange和Queue的持久化计划策略不一致,则不支持绑定
内存控制
当内存使用超过配置的阈值或者高于配置的阈值,会阻塞客户端连接,并停止接收从客户端,以免服务器崩溃
内存阈值为40%,超过阈值会产生警告并阻塞所有生产者连接
相对值设置,建议取值在40%-66% 不建议超过70%
绝对值,单位KB、MB、GB
内存换页
将队列中的消息,持久化到磁盘空间,达到释放内存空间,无论是否持久化,
都会被存储到磁盘,持久化的消息会从内存删除
都会被存储到磁盘,持久化的消息会从内存删除
默认内存达到内存阈值的50%会进行换页操作
内存阈值40%*换页阈值50%=20%会进行换页
内存8G ,内存阈值3.2G, 换页阈值50%,当有1.6G是会进行换页
可以进行修改,修改大于1是禁止换页功能
磁盘控制
当磁盘剩余空间低于确定的阈值,同样会阻塞生产者,避免服务器崩溃
默认50M
建议将磁盘阈值设置未操作系统的内存大小
内存16G 设置磁盘阈值16G
面试题
1. 消息队列的作用?
2. Channel的作用是什么?
3. 多个项目公用一个MQ服务器,怎么实现权限隔离?
4. RabbitMQ的消息有哪些路由方式?适合在什么业务场景下使用?
5. 交换机与队列、队列与消费者的绑定关系是什么样的?
6. 无法被路由的消息,会怎么样处理?
7. 消息什么时候回变成死信?
8. RabbitMQ如何实现延迟队列?
9. 消息丢失是怎么发生的?怎么解决?
10. 可以使用队列的x-max-length最大消息数来实现限流么?例如秒杀场景,应该怎么设计?
11. 如果一个项目要从多个服务器接收消息怎么做?如果一个项目要发送消息到多个服务器怎么做?
12. 一个队列最多可以存放多少条消息?
13. 如何提高消息的消费速率(避免消息堆积)?
14. AmqpTemplate和RabbitTemplate的区别?
15. 如何动态的创建队列和消费者?
16. SpringAMQP中消息怎么封装?用什么实现转换?
17. 如何保证集群的高可用?
18. 如何保证消息的顺序性?
19. 集群节点的类型?
20. 消息大量堆积怎么解决?
21. 如何确定一个消息是不是死信?
6.2 RocketMQ
分布式事务的实现
工作模型
NameServer
提供轻量级的Broker路由服务
Broker
实际处理消息存储、转发等服务的核心组件
Producer
RocketMQ提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。
同步和异步方式均需要Broker返回确认信息,单向发送不需要。
生产者中,会把同一类Producer组成一个集合,叫做生产者组。同一组的
Producer被认为是发送同一类消息且发送逻辑一致
同步和异步方式均需要Broker返回确认信息,单向发送不需要。
生产者中,会把同一类Producer组成一个集合,叫做生产者组。同一组的
Producer被认为是发送同一类消息且发送逻辑一致
Consumer
消息消费者集群。通常也是业务系统中的一个功能模块
拉取式消费:主动调用Consumer的拉消息方法从Broker服务器拉消
息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
息、主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
推动式消费:Broker收到数据后会主动推送给消费端,该消费模式一般实时
性较高
性较高
Topic
区分消息的种类;是RocketMQ进行消息订阅的基本单位;
一个发送者可以发送消息给一个或者多个Topic;
一个消息的接收者可以订阅一个或者多个Topic消息
一个发送者可以发送消息给一个或者多个Topic;
一个消息的接收者可以订阅一个或者多个Topic消息
Message Queue
相当于是Topic的分区;用于并行发送和接收消息
Topic只是一个逻辑概念,并不实际保存消息。同一个Topic下的消息,会分片保
存到不同的Broker上,而每一个分片单位,就叫做MessageQueue。
MessageQueue是一个具有FIFO特性的队列结构,生产者发送消息与消费者消费消
息的最小单位
存到不同的Broker上,而每一个分片单位,就叫做MessageQueue。
MessageQueue是一个具有FIFO特性的队列结构,生产者发送消息与消费者消费消
息的最小单位
生产问题及解决方案
1. 消息丢失
生产者使用事务消息机制
Broker配置同步刷盘+Dledger主从架构
消费者不要使用异步消费
整个MQ挂了之后准备降级方案
2.消息顺序消费
全局有序
将Topic配置成只有一个
MessageQueue队列(默认是4个)。这样天生就能保证消息全局有序了
MessageQueue队列(默认是4个)。这样天生就能保证消息全局有序了
局部有序
只需要将有序的一组消息都存入同一个MessageQueue
里,这样MessageQueue的FIFO设计天生就可以保证这一组消息的有序。
RocketMQ中,可以在发送者发送消息时指定一个MessageSelector对象,让这个
对象来决定消息发入哪一个MessageQueue。这样就可以保证一组有序的消息能够
发到同一个MessageQueue里
里,这样MessageQueue的FIFO设计天生就可以保证这一组消息的有序。
RocketMQ中,可以在发送者发送消息时指定一个MessageSelector对象,让这个
对象来决定消息发入哪一个MessageQueue。这样就可以保证一组有序的消息能够
发到同一个MessageQueue里
3.消息积压
如果Topic下的MessageQueue配置得是足够多的,那每个Consumer实际上会分
配多个MessageQueue来进行消费。这个时候,就可以简单的通过增加Consumer
的服务节点数量来加快消息的消费,等积压消息消费完了,再恢复成正常情况。最
极限的情况是把Consumer的节点个数设置成跟MessageQueue的个数相同。但是
如果此时再继续增加Consumer的服务节点就没有用了
配多个MessageQueue来进行消费。这个时候,就可以简单的通过增加Consumer
的服务节点数量来加快消息的消费,等积压消息消费完了,再恢复成正常情况。最
极限的情况是把Consumer的节点个数设置成跟MessageQueue的个数相同。但是
如果此时再继续增加Consumer的服务节点就没有用了
而如果Topic下的MessageQueue配置得不够多的话,那就不能用上面这种增加
Consumer节点个数的方法了。这时怎么办呢? 这时如果要快速处理积压的消息,
可以创建一个新的Topic,配置足够多的MessageQueue。然后把所有消费者节点
的目标Topic转向新的Topic,并紧急上线一组新的消费者,只负责消费旧Topic中
的消息,并转储到新的Topic中,这个速度是可以很快的。然后在新的Topic上,就
可以通过增加消费者个数来提高消费速度了。之后再根据情况恢复成正常情况
Consumer节点个数的方法了。这时怎么办呢? 这时如果要快速处理积压的消息,
可以创建一个新的Topic,配置足够多的MessageQueue。然后把所有消费者节点
的目标Topic转向新的Topic,并紧急上线一组新的消费者,只负责消费旧Topic中
的消息,并转储到新的Topic中,这个速度是可以很快的。然后在新的Topic上,就
可以通过增加消费者个数来提高消费速度了。之后再根据情况恢复成正常情况
6.3 Kafka
使用场景
1. 日志收集:一个公司可以用Kafka收集各种服务的log,通过kafka以统一接口服务的方式开放给各种
consumer,例如hadoop、Hbase、Solr等。
consumer,例如hadoop、Hbase、Solr等。
2. 消息系统:解耦和生产者和消费者、缓存消息等。
3. 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这
些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到
hadoop、数据仓库中做离线分析和挖掘。
些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到
hadoop、数据仓库中做离线分析和挖掘。
4. 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反
馈,比如报警和报告。
馈,比如报警和报告。
工作模型
Broker
消息中间件处理节点,一个Kafka节点就是一个broker,一
个或者多个Broker可以组成一个Kafka集群
个或者多个Broker可以组成一个Kafka集群
Topic
Kafka根据topic对消息进行归类,发布到Kafka集群的每条
消息都需要指定一个topic
消息都需要指定一个topic
Partition
物理上的概念,一个topic可以分为多个partition,每个
partition内部消息是有序的
partition内部消息是有序的
Producer
生产者将消息发送到topic中去,同时负责选择将message发送到topic的哪一个partition中。
通过round robin做简单的负载均衡。
也可以根据消息中的某一个关键字来进行区分。通常第二种方式使用的更多。
通过round robin做简单的负载均衡。
也可以根据消息中的某一个关键字来进行区分。通常第二种方式使用的更多。
1、写入方式
producer 采用 push 模式将消息发布到 broker,每条消息都被 append 到 patition 中,属于顺序写磁盘(顺序写磁盘
效率比随机写内存要高,保障 kafka 吞吐率)。
producer 采用 push 模式将消息发布到 broker,每条消息都被 append 到 patition 中,属于顺序写磁盘(顺序写磁盘
效率比随机写内存要高,保障 kafka 吞吐率)。
2、消息路由
producer 发送消息到 broker 时,会根据分区算法选择将其存储到哪一个 partition。其路由机制为:
1. 指定了 patition,则直接使用;
2. 未指定 patition 但指定 key,通过对 key 的 value 进行hash 选出一个 patition
3. patition 和 key 都未指定,使用轮询选出一个 patition
producer 发送消息到 broker 时,会根据分区算法选择将其存储到哪一个 partition。其路由机制为:
1. 指定了 patition,则直接使用;
2. 未指定 patition 但指定 key,通过对 key 的 value 进行hash 选出一个 patition
3. patition 和 key 都未指定,使用轮询选出一个 patition
Consumer
传统的消息传递模式有2种:队列( queue) 和(publish-subscribe)
queue模式:多个consumer从服务器中读取数据,消息只会到达一个consumer。
publish-subscribe模式:消息会被广播给所有的consumer
queue模式:多个consumer从服务器中读取数据,消息只会到达一个consumer。
publish-subscribe模式:消息会被广播给所有的consumer
Kafka基于这2种模式提供了一种consumer的抽象概念:consumer group。
queue模式:所有的consumer都位于同一个consumer group 下。
publish-subscribe模式:所有的consumer都有着自己唯一的consumer group
queue模式:所有的consumer都位于同一个consumer group 下。
publish-subscribe模式:所有的consumer都有着自己唯一的consumer group
ConsumerGroup
每个Consumer属于一个特定的Consumer Group,一条消
息可以被多个不同的Consumer Group消费,但是一个
Consumer Group中只能有一个Consumer能够消费该消息
息可以被多个不同的Consumer Group消费,但是一个
Consumer Group中只能有一个Consumer能够消费该消息
核心配置
生产问题及解决
1. 消息丢失情况
消息发送端
(1)acks=0: 表示producer不需要等待任何broker确认收到消息的回复,就可以继续发送下一条消息。性能最高,但是最容易丢消
息。大数据统计报表场景,对性能要求很高,对数据丢失不敏感的情况可以用这种。
(2)acks=1: 至少要等待leader已经成功将数据写入本地log,但是不需要等待所有follower是否成功写入。就可以继续发送下一条消
息。这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。
(3)acks=-1或all: 这意味着leader需要等待所有备份(min.insync.replicas配置的备份个数)都成功写入日志,这种策略会保证只要有一
个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。当然如果
min.insync.replicas配置的是1则也可能丢消息,跟acks=1情况类似。
息。大数据统计报表场景,对性能要求很高,对数据丢失不敏感的情况可以用这种。
(2)acks=1: 至少要等待leader已经成功将数据写入本地log,但是不需要等待所有follower是否成功写入。就可以继续发送下一条消
息。这种情况下,如果follower没有成功备份数据,而此时leader又挂掉,则消息会丢失。
(3)acks=-1或all: 这意味着leader需要等待所有备份(min.insync.replicas配置的备份个数)都成功写入日志,这种策略会保证只要有一
个备份存活就不会丢失数据。这是最强的数据保证。一般除非是金融级别,或跟钱打交道的场景才会使用这种配置。当然如果
min.insync.replicas配置的是1则也可能丢消息,跟acks=1情况类似。
消息消费端
如果消费这边配置的是自动提交,万一消费到数据还没处理完,就自动提交offset了,但是此时你consumer直接宕机了,未处理完的数据
丢失了,下次也消费不到了
丢失了,下次也消费不到了
2.消息重复消费
消息发送端
发送消息如果配置了重试机制,比如网络抖动时间过长导致发送端发送超时,实际broker可能已经接收到消息,但发送方会重新发送消息
消息消费端
如果消费这边配置的是自动提交,刚拉取了一批数据处理了一部分,但还没来得及提交,服务挂了,下次重启又会拉取相同的一批数据重
复处理
复处理
3.消息乱序
如果发送端配置了重试机制,kafka不会等之前那条消息完全发送成功才去发送下一条消息,这样可能会出现,发送了1,2,3条消息,第
一条超时了,后面两条发送成功,再重试发送第1条消息,这时消息在broker端的顺序就是2,3,1了
所以,是否一定要配置重试要根据业务情况而定。也可以用同步发送的模式去发消息,当然acks不能设置为0,这样也能保证消息从发送
端到消费端全链路有序。
一条超时了,后面两条发送成功,再重试发送第1条消息,这时消息在broker端的顺序就是2,3,1了
所以,是否一定要配置重试要根据业务情况而定。也可以用同步发送的模式去发消息,当然acks不能设置为0,这样也能保证消息从发送
端到消费端全链路有序。
kafka保证全链路消息顺序消费,需要从发送端开始,将所有有序消息发送到同一个分区,然后用一个消费者去消费,但是这种性能比较
低,可以在消费者端接收到消息后将需要保证顺序消费的几条消费发到内存队列(可以搞多个),一个内存队列开启一个线程顺序处理消
息。
低,可以在消费者端接收到消息后将需要保证顺序消费的几条消费发到内存队列(可以搞多个),一个内存队列开启一个线程顺序处理消
息。
4.消息积压
1. 线上有时因为发送方发送消息速度过快,或者消费方处理消息过慢,可能会导致broker积压大量未消费消息。
此种情况如果积压了上百万未消费消息需要紧急处理,可以修改消费端程序,让其将收到的消息快速转发到其他topic(可以设置很多分
区),然后再启动多个消费者同时消费新主题的不同分区。
此种情况如果积压了上百万未消费消息需要紧急处理,可以修改消费端程序,让其将收到的消息快速转发到其他topic(可以设置很多分
区),然后再启动多个消费者同时消费新主题的不同分区。
2. 由于消息数据格式变动或消费者程序有bug,导致消费者一直消费不成功,也可能导致broker积压大量未消费消息。
此种情况可以将这些消费不成功的消息转发到其它队列里去(类似死信队列),后面再慢慢分析死信队列里的消息处理问题
此种情况可以将这些消费不成功的消息转发到其它队列里去(类似死信队列),后面再慢慢分析死信队列里的消息处理问题
5.延时队列
实现思路:发送延时消息时先把消息按照不同的延迟时间段发送到指定的队列中(topic_1s,topic_5s,topic_10s,...topic_2h,这个一
般不能支持任意时间段的延时),然后通过定时器进行轮训消费这些topic,查看消息是否到期,如果到期就把这个消息发送到具体业务处
理的topic中,队列中消息越靠前的到期时间越早,具体来说就是定时器在一次消费过程中,对消息的发送时间做判断,看下是否延迟到对
应时间了,如果到了就转发,如果还没到这一次定时任务就可以提前结束了
般不能支持任意时间段的延时),然后通过定时器进行轮训消费这些topic,查看消息是否到期,如果到期就把这个消息发送到具体业务处
理的topic中,队列中消息越靠前的到期时间越早,具体来说就是定时器在一次消费过程中,对消息的发送时间做判断,看下是否延迟到对
应时间了,如果到了就转发,如果还没到这一次定时任务就可以提前结束了
6.消息回溯
如果某段时间对已消费消息计算的结果觉得有问题,可能是由于程序bug导致的计算错误,当程序bug修复后,这时可能需要对之前已消
费的消息重新消费,可以指定从多久之前的消息回溯消费,这种可以用consumer的offsetsForTimes、seek等方法指定从某个offset偏移
的消息开始消费
费的消息重新消费,可以指定从多久之前的消息回溯消费,这种可以用consumer的offsetsForTimes、seek等方法指定从某个offset偏移
的消息开始消费
7.分区数越多吞吐量越高吗
如果分区数设置过大,比如设置10000,可能会设置不成功,后台会报错"java.io.IOException : Too many open files"。
异常中最关键的信息是“ Too many open flies”,这是一种常见的 Linux 系统错误,通常意味着文件描述符不足,它一般发生在创建线
程、创建 Socket、打开文件这些场景下 。 在 Linux系统的默认设置下,这个文件描述符的个数不是很多 ,通过 ulimit -n 命令可以查
看:一般默认是1024,可以将该值增大,比如:ulimit -n 65535
异常中最关键的信息是“ Too many open flies”,这是一种常见的 Linux 系统错误,通常意味着文件描述符不足,它一般发生在创建线
程、创建 Socket、打开文件这些场景下 。 在 Linux系统的默认设置下,这个文件描述符的个数不是很多 ,通过 ulimit -n 命令可以查
看:一般默认是1024,可以将该值增大,比如:ulimit -n 65535
8. 消息幂等性
因为发送端重试导致的消息重复发送问题,kafka的幂等性可以保证重复发送的消息只接收一次,只需在生产者加
上参数 props.put(“enable.idempotence”, true) 即可,默认是false不开启。
具体实现原理是,kafka每次发送消息会生成PID和Sequence Number,并将这两个属性一起发送给broker,broker会将PID和
Sequence Number跟消息绑定一起存起来,下次如果生产者重发相同消息,broker会检查PID和Sequence Number,如果相同不会再
接收
上参数 props.put(“enable.idempotence”, true) 即可,默认是false不开启。
具体实现原理是,kafka每次发送消息会生成PID和Sequence Number,并将这两个属性一起发送给broker,broker会将PID和
Sequence Number跟消息绑定一起存起来,下次如果生产者重发相同消息,broker会检查PID和Sequence Number,如果相同不会再
接收
业务层面自己控制生成唯一的消息ID
面试题
1. 为什么要对Topic下数据进行分区存储?
2. Kafka高性能的原因?
6.4 ActiveMQ
P2P
Pub/Sub
持久化策略
KahaDB(默认) 存到日志文件中(恢复文件:db.redo)
JDBC存储
LevelDB存储
服务器宕机/消息丢失
不用非持久化消息
消息的不均匀消费
prefetch 设置每个消费者获取的条数
死信队列
关闭 AUTO_ACKNOWLEDGE 消息消费失败后将退回重试
退回重试六次后
分布式事务
重复消费
1、在生产者端添加消息id,作为消息唯一性。
2、在消费者消费消息之前,先进行判断消息是否被消费过。(可通过数据库或日志进行判断)
3、消息消费成功后,可把消息id存入数据库中,或者打印日志。
顺序及负载均衡策略
activeMq 里面有 messageGroups 属性,可以指定 JMSXGroupID,消费者会消费指定的 JMSXGroupID。即保证了顺序性,又解决负载均衡的问题。
6.5 几种主流MQ的对比
7. 开发框架
Spring
设计模式
工厂
BeanFactory
ApplicationContext
单例
bean默认作用域 singleton
代理
AOP
模板
jdbcTemplate,Redistemplate
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
观察者
event/listener
适配器
AOP中的增强或通知(Advice) AdvisorAdapter
mcv HandlerAdapter 适配器
装饰者
DataSource
模块特性
Bean
生命周期(源码步骤)
初始化 ApplicationContext
扫描类 invokeBeanFactoryPostProcessors
封装beanDefinition对象 各种信息
放到map
遍历map
验证
能不能实例化 需要实例化吗 根据信息来
是否单例
判断是不是factory bean
单例池 只是一个ConcurrentHashMap
正在创建的容器
得到class
推断构造方法
根据注入模型
默认
得到构造方法
反射 实例化这个对象
后置处理器合并beanDefinition
判断是否允许 循环依赖
提前暴露bean工厂对象
填充属性 自动注入
执行部分aware接口
继续执行部分aware接口 生命周期回调方法
完成代理AOP
beanProstprocessor的前置方法
实例化为bean
放到单例池
销毁
生命周期
阶段
实例化
属性赋值
初始化
销毁
部分源码
AbstractAutowireCapableBeanFactory
doCreateBean
createBeanInstance()->实例化
populateBean()->属性赋值
initializeBean()->初始化
销毁
ConfigurableApplicationContext#close()
扩展接口
影响多个bean的接口
InstantiationAwareBeanPostProcessor(继承BeanPostProcessor)
作用于实例化阶段的前后
postProcessBeforeInstantiation
在doCreateBean之前调用,也就是在bean实例化之前调用的,英文源码注释解释道该方法的返回值会替换原本的Bean作为代理,这也是Aop等功能实现的关键点。
postProcessAfterInstantiation
该方法在属性赋值方法内,但是在真正执行赋值操作之前。其返回值为boolean,返回false时可以阻断属性赋值阶段
BeanPostProcessor
作用于初始化阶段的前后
先于其他bean初始化
只调用一次的接口
Aware
生命周期
作用域
单例(singleton)
ioc中仅存在一个bean实例 默认
多例(prototype)
每次从ioc中调用bean时,返回一个新的实例
Request
每次http请求会创建一个新的bean,仅适用于WebApplicationContext
Session
不同session使用不同bean,仅适用于WebApplicationContext
Global Session
一个全局的HTTP
Bean的生命周期(加载流程)
IoC容器的初始化流程
IOC
由Spring IOC容器来负责对象的生命周期和对象之间的关系
注入方式
构造器注入
setter方法注入
接口注入
容器
BeanFactory
默认延迟初始化策略,懒加载
BeanDefinition 保存对象的所有必要信息
ApplicationContext
子类,功能更多(事件发布,消息),立即加载
AOP
静态代理
AspectJ
静态 AOP 实现, AOP 框架在编译阶段对程序源代码进行修改,生成了静态的 AOP 代理类(生成的 *.class 文件已经被改掉了,需要使用特定的编译器),比如 AspectJ。
Spring借鉴了AspectJ很多非常有用的做法,融合了AspectJ实现AOP的功能。但Spring AOP本质上底层还是动态代理,所以Spring AOP是不需要有专门的编辑器的~
动态代理
jdk动态代理
实现接口->java反射机制生成一个代理接口的匿名类->调用具体方法的时候调用invokeHandler
默认 通过接口
多例使用 每次创建的时候快
cglib
asm字节码编辑技术动态创建类->基于classLoad装载->修改字节码生成子类去处理
通过继承 生成动态代理对象为目标类的子类
单例使用 创建一次较慢,但是每次调用快
spring 对AOP的支持
基于代理的经典SpringAOP
实现接口,手动创建代理
纯POJO切面
xml配置,aop命名空间
@Aspectj注解驱动的切面
注解方式
@Aspect 指定一个类为切面类
@Pointcut("execution(* cn.itcast.e_aop_anno..(..))") 指定切入点表达式
@Before("pointCut_()") 前置通知: 目标方法之前执行
@After("pointCut_()") 后置通知:目标方法之后执行(始终执行)
@AfterReturning("pointCut_()") 返回后通知: 执行方法结束前执行(异常不执行)
@AfterThrowing("pointCut_()") 异常通知: 出现异常时候执行
@Around("pointCut_()") 环绕通知: 环绕目标方法执行
分支主题
分支主题
装饰者模式与代理模式的区别
装饰者模式
在运行时确定真实对象,在代理对象的构造中动态传入参数
代理模式
在编译器即确定真实对象,在代理对象的空参构造中直接new了真实的对象
应用
注解
@EnableAspectJAutoProxy
配置参数
proxyTargetClass
false(默认):表示使用标准的JDK动态代理,
true:使用CGLIB代理
true:使用CGLIB代理
exposeProxy
false(默认):表示不将当前代理对象暴露给切面,
true:可以通过 AopContext.currentProxy() 获取当前代理对象
true:可以通过 AopContext.currentProxy() 获取当前代理对象
作用
1. 启用代理机制:它启用了Spring容器的代理功能,允许Spring创建代理对象,以拦截和处理方法调用
2. 使用AspectJ注解:它启用了AspectJ注解,使您可以在Spring中使用 @Aspect、@Before、@After、@Around 等注解来定义切面。
@Aspect
循环依赖
类型
构造器循环依赖
无法解决
三级缓存中A实例化 发现B 寻找B 从 一级->二级->三级 未找到 实例化B 发现A 寻找A 在三级缓存中发现未实例化的A 抛异常
属性注入循环依赖
三级缓存中A实例化 完成 进入二级缓存 发现需要B 寻找B 一二三级缓存 未找到 去三级缓存工厂 实例化B 进入二级缓存 发现需要A 去找 发现二级缓存中存在A的实例(虽然未初始化完成 但是引用存在 ) B完成二级缓存中的依赖注入 进入一级缓存 初始化 完成 A二级缓存 注入完成 进入一级缓存 初始化完成 结束
缓存
一级缓存
/** 保存所有的singletonBean的实例 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);
单例池
二级缓存
/** 保存所有早期创建的Bean对象,这个Bean还没有完成依赖注入 */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
未赋值的实例(依赖注入 BI)
三级缓存
/** singletonBean的生产工厂*/
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
bean生产工厂
父子容器
web项目(spring+mvc)
mvc 子容器
spring 父容器
service不能调用controller
boot
一个容器 AnnotationConfigEmbeddedWebApplicationContext
事务实现原理
传播行为
REQUIRED, 如果当前线程已经在一个事务中,则加入该事务,否则新建一个事务。
SUPPORT, 如果当前线程已经在一个事务中,则加入该事务,否则不使用事务。
MANDATORY(强制的),如果当前线程已经在一个事务中,则加入该事务,否则抛出异常。
REQUIRES_NEW,无论如何都会创建一个新的事务,如果当前线程已经在一个事务中,则挂起当前事务,创建一个新的事务。
NOT_SUPPORTED,如果当前线程在一个事务中,则挂起事务。
NEVER,如果当前线程在一个事务中则抛出异常。
NESTED, 执行一个嵌套事务,有点像REQUIRED,但是有些区别,在Mysql中是采用SAVEPOINT来实现的。
事务不生效的原因
1.如果不是Innodb存储引擎,MyISAM不支持事务。
2.没有指定rollbackFor参数。
3. 没有指定transactionManager参数,默认的transactionManager并不是我期望的,以及一个事务中涉及到了多个数据库。
4. 如果AOP使用了JDK动态代理,对象内部方法互相调用不会被Spring的AOP拦截,@Transactional注解无效。
5. 如果AOP使用了CGLIB代理,事务方法或者类不是public,无法被外部包访问到,或者是final无法继承,@transactional注解无效
面试题
1、Spring是什么?
1.1、Spring 都有哪些模块?
2、Spring 的优点?
3、Spring的IoC理解?
什么是IOC?
什么是DI?
IOC的底层实现原理?
4、Spring的AOP理解?
什么是AOP?
AOP的底层实现?
5、Spring AOP里面的有几个基本概念?
6、Spring通知(Advice)有哪些类型?执行顺序是什么样子的?
7、Spring容器的启动流程?
8、BeanFactory和ApplicationContext有什么区别?
9、Spring Bean的生命周期?
10、Spring中bean的作用域?
11、Spring框架中的Bean是线程安全的么?如果线程不安全,那么如何处理?
12、Spring基于xml注入bean的几种方式?
13、Spring如何解决循环依赖的?
14、Spring的自动装配的原理?
15、Spring事务的实现方式和实现原理?
事务的种类?
事务的传播机制?
事物的隔离级别?
事务的失效场景?
16、Spring 框架中都用到了哪些设计模式?
17、Spring框架中有哪些不同类型的事件?
18、什么是注解?注解的原理?如何自定义注解?
什么是注解?
注解的原理?
如何自定义注解?
19、被Spring容器管理的bean实例和new 出来的实例有什么区别?
20、BeanFactory和FactoryBean的区别是什么?
21、Spring AOP动态代理的理解?
22、@Autowried和@Resource的区别?
SpringMVC
面试题
1、什么是Spring MVC ?简单介绍下你对springMVC的理解?
2、SpringMVC的流程?
3、SpringMVC的优点?
4、SpringMVC怎么样设定重定向和转发的?
5、SpringMVC常用的注解有哪些?
6、SpingMVC中的控制器的注解一般用哪个?有没有别的注解可以替代?
7、SpringMVC和struts2的区别有哪些?
8、如何解决POST请求中文乱码问题,GET的又如何处理呢?
9、SpringMvc里面拦截器是怎么写的?
10、注解是什么?怎么自定义注解?
11、SpringMVC怎么和AJAX相互调用的?
12、SpringMVC的异常处理 ?
13、SpringMvc的控制器是不是单例模式?如果是,有什么问题?怎么解决?
14、如果在拦截请求中,我想拦截get方式提交的方法,怎么配置?
16、怎样在方法里面得到Request,或者Session?
16、如果想在拦截的方法里面得到从前台传入的参数,怎么得到?
17、如果前端传入多个参数,并且参数都是同个对象的,如何快速得到这个对象?
18、SpringMVC中函数的返回值是什么?
19、SpringMVC用什么对象从后台向前台传递数据的?
20、怎么样把ModelMap里面的数据放入Session里面?
21. SpringMVC是线程安全的么?
SpringBoot
自动装配
@SpringBootApplication
@EnableAutoConfiguration
扫描的包@AutoConfigurationPackage
加载注解@Import(AutoConfigurationImportSelector.class)
AutoConfigurationImportSelector
selectImports
META-INF/spring.factories
子主题 2
1. 通过各种注解实现了类与类之间的依赖关系,容器在启动的时候Application.run,会调用EnableAutoConfigurationImportSelector.class的selectImports方法(其实是其父类的方法)
2. selectImports方法最终会调用SpringFactoriesLoader.loadFactoryNames方法来获取一个全面的常用BeanConfiguration列表
3. loadFactoryNames方法会读取FACTORIES_RESOURCE_LOCATION(也就是spring-boot-autoconfigure.jar 下面的spring.factories),获取到所有的Spring相关的Bean的全限定名ClassName,大概120多个
4. selectImports方法继续调用filter(configurations, autoConfigurationMetadata);这个时候会根据这些BeanConfiguration里面的条件,来一一筛选,最关键的是@ConditionalOnClass,这个条件注解会去classpath下查找,jar包里面是否有这个条件依赖类,所以必须有了相应的jar包,才有这些依赖类,才会生成IOC环境需要的一些默认配置Bean
5. 最后把符合条件的BeanConfiguration注入默认的EnableConfigurationPropertie类里面的属性值,并且注入到IOC环境当中
常用注解
@SpringBootApplication
@SpringBootApplication
@EnableAutoConfiguration
@ComponentScan
@WebServlet、@WebFilter、@WebListener
面试题
1、SpringBoot的自动装配原理?
SpringCloud
版本对应
spring boot 2.2.2 ->cloud Hoxton.SR1
生态图(待整理)
Spring Cloud Netflix
Eureka
服务注册和发现,它提供了一个服务注册中心、服务发现的客户端,
还有一个方便的查看所有注册的服务的界面。 所有的服务使用Eureka的服务发现客
户端来将自己注册到Eureka的服务器上
还有一个方便的查看所有注册的服务的界面。 所有的服务使用Eureka的服务发现客
户端来将自己注册到Eureka的服务器上
Zuul
网关,所有的客户端请求通过这个网关访问后台的服务。他可以使用一定
的路由配置来判断某一个URL由哪个服务来处理。并从Eureka获取注册的服务来转发
请求。
的路由配置来判断某一个URL由哪个服务来处理。并从Eureka获取注册的服务来转发
请求。
Ribbon
负载均衡,Zuul网关将一个请求发送给某一个服务的应用的时候,
如果一个服务启动了多个实例,就会通过Ribbon来通过一定的负载均衡策略来发送
给某一个服务实例
如果一个服务启动了多个实例,就会通过Ribbon来通过一定的负载均衡策略来发送
给某一个服务实例
Feign
服务客户端,服务之间如果需要相互访问,可以使用RestTemplate,也
可以使用Feign客户端访问。它默认会使用Ribbon来实现负载均衡
可以使用Feign客户端访问。它默认会使用Ribbon来实现负载均衡
Hystrix
监控和断路器。我们只需要在服务接口上添加Hystrix标签,就可以实
现对这个接口的监控和断路器功能
现对这个接口的监控和断路器功能
Hystrix Dashboard
监控面板,他提供了一个界面,可以监控各个服务上的服
务调用所消耗的时间等
务调用所消耗的时间等
Turbine
监控聚合,使用Hystrix监控,我们需要打开每一个服务实例的监控信
息来查看。而Turbine可以帮助我们把所有的服务实例的监控信息聚合到一个地方统
一查看。这样就不需要挨个打开一个个的页面一个个查看
息来查看。而Turbine可以帮助我们把所有的服务实例的监控信息聚合到一个地方统
一查看。这样就不需要挨个打开一个个的页面一个个查看
Spring Cloud Alibaba
Nacos
一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台
Sentinel
把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定
性。
性。
RocketMQ
开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息
发布与订阅服务
发布与订阅服务
Dubbo
在国内应用非常广泛的一款高性能 Java RPC 框架
Seata
阿里巴巴开源产品,一个易于使用的高性能微服务分布式事务解决方案
Arthas
开源的Java动态追踪工具,基于字节码增强技术,功能非常强大
核心组件
服务注册中心
Eureka(未维护了)
各个服务启动时,EurekaClient都会将服务注册到Eureka Servcer,并且Eureka Client 还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里
Zookeeper
Consul
Nacos(阿里)
客户端负载均衡
Ribbon
服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台
默认使用的是Round Robin轮训算法
LoadBalancer
服务调用
Feign
基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址
使用jdk的动态代理来针对FeignClient注解修饰的接口创建动态代理
服务消费者基于 Feign 调用服务提供者对外发布的接口,先对调用的本地接口加上注解@FeignClient,Feign会针对加了该注解的
接口生成动态代理,服务消费者针对 Feign 生成的动态代理去调用方法时,会在底层生成Http协议格式的请求,类似 /stock/deduct?
productId=100
Feign 最终会调用Ribbon从本地的Nacos注册表的缓存里根据服务名取出服务提供在机器的列表,然后进行负载均
衡并选择一台机器出来,对选出来的机器IP和端口拼接之前生成的url请求,生成调用的Http接口地址
接口生成动态代理,服务消费者针对 Feign 生成的动态代理去调用方法时,会在底层生成Http协议格式的请求,类似 /stock/deduct?
productId=100
Feign 最终会调用Ribbon从本地的Nacos注册表的缓存里根据服务名取出服务提供在机器的列表,然后进行负载均
衡并选择一台机器出来,对选出来的机器IP和端口拼接之前生成的url请求,生成调用的Http接口地址
Open Feign
断路器(服务隔离,降级,熔断)
Hystrix
发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,
实现了不通服务调用的隔离,避免了服务雪崩的问题
实现了不通服务调用的隔离,避免了服务雪崩的问题
resilience4j
sentienl
降级策略
1. 通过并发线程数进行限制
2. 通过响应时间对资源进行降级
工作流程
服务网关
Zuul
如果前端,移动端要调用后端系统,统一从Zuul网关进入,由zuul网关转发请求给对应的服务
gateway
服务配置
Config
Nacos
服务总线
Bus
Nacos
面试题
1. Nacos的服务注册表的结构是什么样子的?
2. Nacos和Eureka的区别?
111111111111
3. Nacos的注册原理?
4、Nacos的运行原理?
5. 常见的负载均衡算法?
Nacos
核心功能
服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如ip地址、端口等信
息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默
认5s发送一次心跳。
认5s发送一次心跳。
服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它
的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复
发送心跳则会重新注册)
的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复
发送心跳则会重新注册)
服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清
单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存
单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存
服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。
Mybatis
缓存
一级缓存(sqlsession 级别)
第一次发出一个查询 sql,sql 查询结果写入 sqlsession 的一级缓存中,缓存使用的数据结构是一 个 map。
key:MapperID+offset+limit+Sql+所有的入参
value:用户信息
同一个 sqlsession 再次发出相同的 sql,就从缓存中取出数据。
如果两次中间出现 commit 操作 (修改、添加、删除),本 sqlsession 中的一级缓存区域全部清空,
下次再去缓存中查询不到所 以要从数据库查询,从数据库查询到再写入缓存。
key:MapperID+offset+limit+Sql+所有的入参
value:用户信息
同一个 sqlsession 再次发出相同的 sql,就从缓存中取出数据。
如果两次中间出现 commit 操作 (修改、添加、删除),本 sqlsession 中的一级缓存区域全部清空,
下次再去缓存中查询不到所 以要从数据库查询,从数据库查询到再写入缓存。
二级缓存(mapper 基本)
二级缓存的范围是 mapper 级别(mapper 同一个命名空间),mapper 以命名空间为单位创建缓存数据结构,结构是 map。
mybatis 的二级缓存是通过 CacheExecutor 实现的。CacheExecutor其实是 Executor 的代理对象。
所有的查询操作,在 CacheExecutor 中都会先匹配缓存中是否存 在,不存在则查询数据库。
key:MapperID+offset+limit+Sql+所有的入参
mybatis 的二级缓存是通过 CacheExecutor 实现的。CacheExecutor其实是 Executor 的代理对象。
所有的查询操作,在 CacheExecutor 中都会先匹配缓存中是否存 在,不存在则查询数据库。
key:MapperID+offset+limit+Sql+所有的入参
工作原理
工作流程
RPC通信框架
基本IO模型
BIO
同步阻塞IO模型,一个客户端连接对应一个处理线程
特性
IO操作里面的read操作是阻塞操作,如果客户端连接不做读写操作或者读操作耗时很长,会导致整个服务端阻塞,浪费系统资源
可以开启多个线程同时连接服务端,但是多个线程可以解决连接阻塞的问题,无法解决IO阻塞的问题,造成线程数量过多,压力巨大
适用场景
使用用于客户端连接数量小且固定的场景,对服务器资源要求比较高
AIO
异步非阻塞IO模型,由操作系统完成后回调通知服务端程序启动线程去处理, 一般适用于连接数较多且连接时间较长的应用
适用场景
AIO方式适用于连接数目多且连接比较长(重操作) 的架构
NIO
同步非阻塞IO模型,实现一个服务端使用一个线程同时连接多个客户端连接请求(非阻塞),核心就是使用Selector多路复用器,
将客户端连接事件、read事件、write事件都注册到多路复用器(类似于一个监听机制),通过不断地轮询注册的多路复用器的SelectionKey的集合,根据不同的事件进行处理,这里是串行的
将客户端连接事件、read事件、write事件都注册到多路复用器(类似于一个监听机制),通过不断地轮询注册的多路复用器的SelectionKey的集合,根据不同的事件进行处理,这里是串行的
NIO非阻塞的体现
NIO的socket的accept()方法其实也是阻塞的,但是由于当执行到这个方法时,是因为确实已经有客户端注册了请求连接的事件,所以可以直接处理,不会阻塞住,主要体现在收集注册SocketChannel事件,然后轮询处理这些事件上
进一步说就是,BIO在客户端连接后,服务端会一直等着客户端发送消息(阻塞,不管客户端到底有没有发消息),而NIO是监听到了发送消息的事件才会处理
适用场景
NIO方式适用于连接数目多且连接比较短(轻操作) 的架构, 比如聊天服务器, 弹幕系统, 服务器间通讯,编程比较复杂
组件
Channel
类似于流,是客户端和服务端数据流通的通道
会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理
每个 channel 对应一个 buffer缓冲区
Selector
多路复用器,可以理解为一个监听器,适用select()方法,轮询注册到selector中的chennel和事件,进行处理
selector收集所有连接的事件并且转交给后端线程,线程连续执行所有事件命令并将结果写回客户端
Buffer
就是一个缓冲数组
实现
明显的区别是,由之前的主动去拉取数据,变为事件通知的方式
Netty
介绍
Netty是由JBOSS提供的一个基于NIO的java开源框架
子主题
线程模型
简述一下Netty的线程模型
详细的线程模型-组件
Bootstrap、ServerBootstrap
Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端
启动引导类,类似于SpingBoot的启动类,
启动引导类,类似于SpingBoot的启动类,
Future、ChannelFuture
Netty的所有IO操作都是异步的,可以注册一个监听,具体的实现就是通过 Future 和
ChannelFutures
ChannelFutures
Channel
网络通信的组件,能够用于执行网络 I/O 操作
比如绑定端口、是否连接
不同的协议对应不同的channel实现
NioServerSocketChannel,异步的服务器端 TCP Socket 连接
NioSocketChannel,异步的客户端 TCP Socket 连接
NioDatagramChannel,异步的 UDP 连接
NioSctpChannel,异步的客户端 Sctp 连接
NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文
件 IO
件 IO
Selector
Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel
事件。
事件。
当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册
的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单
地使用一个线程高效地管理多个 Channel
的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单
地使用一个线程高效地管理多个 Channel
NioEventLoop
维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用
NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务
NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务
I/O 任务:即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由
processSelectedKeys 方法触发
processSelectedKeys 方法触发
非 IO 任务:添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触
发
发
NioEventLoopGroup
主要管理 NioEventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,
每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应
于一个线程。
每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应
于一个线程。
ChannelHandler
ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业
务处理链)中的下一个处理程序
务处理链)中的下一个处理程序
自定义的处理类继承入站处理和出站处理类
ChannelInboundHandler 用于处理入站 I/O 事件。
ChannelOutboundHandler 用于处理出站 I/O 操作。
ChannelOutboundHandler 用于处理出站 I/O 操作。
ChannelInboundHandlerAdapter 用于处理入站 I/O 事件。
ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作
ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作
ChannelHandlerContext
保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象
ChannelPipline
保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作
服务端,read事件,类似InputStream,入站操作,从链表的head到tail
客户端,write事件,类似OutStream,出站操作,从链表的tail到head
使用场景
分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,比如Dubbo和RocketMQ底层都是Netty
游戏行业
大数据,Hadoop的Netty Service
案例(自己开发聊天室)
1. server端和client端都是固定的写法
2. 将自己的handler继承SimpleChannelInboundHandler类
3. 重写channelActive()方法,记录客户端注册的channel,并进行分发到各个客户端通知,比如xxx客户端上线了
4. 重写
5. 重写channelClose()方法,清除下线的客户端
6. 将handle添加到服务端的pipeline中
7.同样的方式写客户端的handler,只不过继承的是SimpleChannelOutboundHandler类
ByteBuf
通过readerindex和writerIndex和capacity,将buffer分成三个区域
已经读取的区域:[0,readerindex)
可读取的区域:[readerindex,writerIndex)
可写的区域: [writerIndex,capacity)
已经读取的区域:[0,readerindex)
可读取的区域:[readerindex,writerIndex)
可写的区域: [writerIndex,capacity)
组成
readerindex
writerindex
capacity
编解码器
当你通过Netty发送或者接受一个消息的时候,就将会发生一次数据转换
入站消息-解码:从字节转换为另一种格式(比如java对
象)
象)
出站消息-编码,它会被编码成字节
编解码器都继承顶层父类是ChannelHandler,本质上是一个ChannelHandler,也就意味着存放在ChannelPipeline中作为链式顺序执行
实现了ChannelInboundHadnler或者ChannelOutcoundHandler接口
也可以通过集成ByteToMessageDecoder自定义编解码器或者MessageToByteEncoder
粘包拆包
概念
原因
TCP 是面向连接的, 面向流的, 提供高可靠性服务。 收发两端(客户端和服务器端) 都要有成对的 socket,因此, 发送端为了将多个
发给接收端的包, 更有效的发给对方, 使用了优化方法(Nagle 算法),将多次间隔较小且数据量小的数据, 合并成一个大的数据块,
然后进行封包。 这样做虽然提高了效率, 但是接收端就难于分辨出完整的数据包了, 因为面向流的通信是无消息保护边界的
发给接收端的包, 更有效的发给对方, 使用了优化方法(Nagle 算法),将多次间隔较小且数据量小的数据, 合并成一个大的数据块,
然后进行封包。 这样做虽然提高了效率, 但是接收端就难于分辨出完整的数据包了, 因为面向流的通信是无消息保护边界的
解决方案
1. 发送消息的时候指定相应的分隔符(不好)
2. 发消息的时候,同时发一个长度(封装自定义协议包对象)
Netty心跳机制
概念
心跳, 即在 TCP 长连接中, 客户端和服务器之间定期发送的一种特殊的数据包, 通知对方自己还在线, 以确保 TCP 连接的有效性
关键构造方法
实现IdleStateHandler的构造方法,加入到ChannelPipeline中,处理
底层的实现实质就是根据心跳时间的设置设置一个schedule,定时去执行心跳检测
如果超时则会最终触发一个userEventTriggered()方法,可以实现userEventTriggered方法处理对应事件,比如超时3次断开链接节省资源
Dubbo
面试题
1. 简述一下NIO的线程模型?
2. 与BIO对比为什么NIO是非阻塞的线程模型?
3. 简述一下Netty的线程模型
4. 什么是粘包拆包?出现的原因是啥?
8. 分布式架构
8.1 分布式理论
CAP(三者不可兼得 取其二)
1.Consistency 强一致性
2.Availability 可用性
3.Partition tolerance 分区容错性
BASE理论
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
nacos临时实例集群是AP,永久实例是CP
8.2 分布式锁
8.1 特点
互斥性
在任意时刻,只有一个客户端能持有锁
安全性
锁只能被持有该锁的客户端删除,不能由其它客户端删除
高可用
当部分节点down机时,客户端仍然能够获取锁和释放锁
防死锁
防止客户端持有锁期间崩溃没有释放锁,设置过期时间t
8.2 基于Redis
注意问题
加锁命令和超时时问要同时设置【保证原子性】
value要具有唯一性(可用UUID)【防止误解锁】
使用定时任务定期检查锁的状态【超时续命】
每次检查锁还在的话,将锁的超时时问重制
每次检查锁还在的话,将锁的超时时问重制
加锁实现
手撸代码
SET key value [EX seconds] [PX milliseconds] [NXXX]
stringRedisTemplate.opsForValue().setIfAbsent(String key,String value,long timeout, TimeUnit unit)
jedis.set(String key, String value, String xxx, String expx, long time);
【存在风险】 如果是主从架构,主节点加锁后挂了,数据还没同步到从节点,
此时slave升级为master,如果再有线程加锁会出现多个客户端持有锁的情况
此时slave升级为master,如果再有线程加锁会出现多个客户端持有锁的情况
Redisson
拥有看门狗机制的锁续命功能 和 间歇性自旋加锁功能
代码实现
RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
hotCacheLock.lock();
// 业务实现
hotCacheLock.unlock();
hotCacheLock.lock();
// 业务实现
hotCacheLock.unlock();
流程图
存在的问题
高并发场景下主从节点锁会存在失效的情况
1. 假如client1刚对master加锁成功了,
2. 此时master的锁数据还没同步到salve,然后master挂了
3. salve成为新的master主节点(其中未包含client1的锁信息)
4. client2再次请求加锁会成功,存在锁失效的问题
2. 此时master的锁数据还没同步到salve,然后master挂了
3. salve成为新的master主节点(其中未包含client1的锁信息)
4. client2再次请求加锁会成功,存在锁失效的问题
结局方案
zookepper
原理:过半数的集群节点获得锁信息后才返回客户端加锁成功
RedLock
原理:同样存储相同信息的redis集群,过半数的节点加锁成功后才算成功,存在问题
8.3 基于MySql
基于表记录
直接在数据库中创建一张表,表里包含方法名等字段,
并且在方法名字段上面创建唯一索引使用时将方法名作为参数在表中插入一条记录,
插入成功即获取锁,释放锁时删除记录即可
并且在方法名字段上面创建唯一索引使用时将方法名作为参数在表中插入一条记录,
插入成功即获取锁,释放锁时删除记录即可
缺点
没有失效时间,会产生死锁,可以添加失效时间字段,靠定时任务去跑
获取锁失败会报错,建议使用队列排队
实现可重入锁需要添加字段累加
基于乐观锁
基于悲观锁
基于排它锁
select * xxxx for update
8.4 基于Zookepper
临时+有序节点
临时节点:避免死锁
有序节点:避免羊群效应
比如在/lock目录下,创建临时有序节点,创建成功后比较该目录下的所有节点,判断自己的序号
是不是最小的,如果是则获取分布式锁成功,否则对当前节点序号的前一个节点添加监听就ok了
是不是最小的,如果是则获取分布式锁成功,否则对当前节点序号的前一个节点添加监听就ok了
特点
相比于redis来说更加健壮,且简单易实现,不过redis性能更高
性能损耗小,没有获取锁时只需添加监听,不需要一直轮询
8.3 分布式事务
两阶段提交(2PC)
两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
问题
2.1 同步阻塞 所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
2.2 单点问题 协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
2.3 数据不一致 在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
2.4 太过保守 任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
补偿事务(TCC)
TCC 其实就是采用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作
TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。
本地消息表(异步确保)
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性
MQ事务消息
有一些第三方的MQ是支持事务消息的,比如RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。
8.4 分库分表
方案
垂直分表
将一个表按照字段分成多个表,每个表存储其中一部分字段。
一般会将常用的字段放到一个表中,将不常用的字段放到另一个表中。
一般会将常用的字段放到一个表中,将不常用的字段放到另一个表中。
优点
(1)避免IO竞争减少锁表的概率。因为大的字段效率更低,第一,大字段占用的空间更大,单页内存储的行数变少,会使得IO操作增多;第二数据量大,需要的读取时间长。
(2)可以更好地提升热门数据的查询效率。
(2)可以更好地提升热门数据的查询效率。
垂直分库
按照业务模块的不同,将表拆分到不同的数据库中,适合业务之间的耦合度非常低、业务逻辑清晰的系统
优点
(1)降低业务中的耦合,方便对不同的业务进行分级管理
(2)可以提升IO、数据库连接数、解决单机硬件存储资源的瓶颈问题
(2)可以提升IO、数据库连接数、解决单机硬件存储资源的瓶颈问题
水平分表
在同一个数据库内,把同一个表的数据按照一定规则拆分到多个表中。
优点
(1)解决了单表数据量过大的问题
(2)避免IO竞争并减少锁表的概率
(2)避免IO竞争并减少锁表的概率
水平分库
把同一个表的数据按照一定规则拆分到不同的数据库中,不同的数据库可以放到不同的服务器上。
优点
(1)解决了单库大数据量的瓶颈问题
(2)IO冲突减少,锁的竞争减少,某个数据库出现问题不影响其他数据库,提高了系统的稳定性和可用性
(2)IO冲突减少,锁的竞争减少,某个数据库出现问题不影响其他数据库,提高了系统的稳定性和可用性
问题
1. 事务的问题
① 方案一:使用分布式事务:
优点:由数据库管理,简单有效。
缺点:性能代价高,特别是shard越来越多。
优点:由数据库管理,简单有效。
缺点:性能代价高,特别是shard越来越多。
② 方案二:程序与数据库共同控制实现,原理就是将一个跨多个数据库的分布式事务分解成多个仅存在于单一数据库上面的小事务,并交由应用程序来总体控制各个小事务。
优点:性能上有优势;
缺点:需要在应用程序在事务上做灵活控制。如果使用了spring的事务管理,改动起来会面临一定的困难。
优点:性能上有优势;
缺点:需要在应用程序在事务上做灵活控制。如果使用了spring的事务管理,改动起来会面临一定的困难。
2.跨节点 Join 的问题
解决该问题的普遍做法是分两次查询实现:在第一次查询的结果集中找出关联数据的id,根据这些id发起第二次请求得到关联数据。
3. 跨节点count,order by,group by,分页和聚合函数问题
由于这类问题都需要基于全部数据集合进行计算。多数的代理都不会自动处理合并工作,解决方案:与解决跨节点join问题的类似,分别在各个节点上得到结果后在应用程序端进行合并。和 join 不同的是每个结点的查询可以并行执行,因此速度要比单一大表快很多。但如果结果集很大,对应用程序内存的消耗是一个问题。
表分区
分区就是将表的数据按照特定规则存放在不同的区域,也就是将表的数据文件分割成多个小块,在查询数据的时候,只要知道数据数据存储在哪些区域,然后直接在对应的区域进行查询,不需要对表数据进行全部的查询,提高查询的性能。同时,如果表数据特别大,一个磁盘磁盘放不下时,我们也可以将数据分配到不同的磁盘去,解决存储瓶颈的问题,利用多个磁盘,也能够提高磁盘的IO效率,提高数据库的性能。
在使用分区表时,需要注意分区字段必须放在主键或者唯一索引中、每个表最大分区数为1024;
在使用分区表时,需要注意分区字段必须放在主键或者唯一索引中、每个表最大分区数为1024;
分区类型
Range分区
按照连续的区间范围进行分区
List分区
按照给定的集合中的值进行选择分区
Hash分区
基于用户定义的表达式的返回值进行分区,该表达式使用将要插入到表中的这些行的列值进行计算。
这个函数可以包含MySQL中有效的、产生非负整数值的任何表达式。
这个函数可以包含MySQL中有效的、产生非负整数值的任何表达式。
Key分区
类似于按照HASH分区,区别在于Key分区只支持计算一列或多列,且key分区的哈希函数是由 MySQL 服务器提供。
优点
① 可伸缩性:
将分区分在不同磁盘,可以解决单磁盘容量瓶颈问题,存储更多的数据,也能解决单磁盘的IO瓶颈问题。
将分区分在不同磁盘,可以解决单磁盘容量瓶颈问题,存储更多的数据,也能解决单磁盘的IO瓶颈问题。
② 提升数据库的性能:
减少数据库检索时需要遍历的数据量,在查询时只需要在数据对应的分区进行查询。
避免Innodb的单个索引的互斥访问限制
对于聚合函数,例如sum()和count(),可以在每个分区进行并行处理,最终只需要统计所有分区得到的结果
减少数据库检索时需要遍历的数据量,在查询时只需要在数据对应的分区进行查询。
避免Innodb的单个索引的互斥访问限制
对于聚合函数,例如sum()和count(),可以在每个分区进行并行处理,最终只需要统计所有分区得到的结果
③ 方便对数据进行运维管理:
方便管理,对于失去保存意义的数据,通过删除对应的分区,达到快速删除的作用。比如删除某一时间的历史数据,直接执行truncate,或者直接drop整个分区,这比detele删除效率更高;
在某些场景下,单个分区表的备份很恢复会更有效率。
方便管理,对于失去保存意义的数据,通过删除对应的分区,达到快速删除的作用。比如删除某一时间的历史数据,直接执行truncate,或者直接drop整个分区,这比detele删除效率更高;
在某些场景下,单个分区表的备份很恢复会更有效率。
面试题
1、分库分表后主键值怎么处理?
面试题
1、说一下CAP理论和BASE理论?
2、说说你了解的集中分布式事务?
算法
贪心
分治
动态规划
排序
快排
子主题
堆排
二叉树
链表翻转
成环
环节点
跳楼梯
8. 项目
爱内控(1.0)
云平台 Devops
集群
docker k8s
单点
租户
内控
门户
微服务
租户
基础数据
数据解析
指标规则计算
业务流程
报告构建
文件库
OOM
场景
解决办法
死锁
场景
解决办法
cpu100%
场景
解决办法
幂等性
redis token(lua脚本 判断相等然后再删)
redis 集群问题
真实问题
for update 导致的 间隙锁 死锁
callable 多线程submit 未获取返回值 判断无异常则认为线程执行成功 不调用future.get 不会看到异常 (排查)
租户隔离 统计问题
默认租户 保留重要信息
qps 每秒查询次数
压测 最高800绝对并发
tps 每秒提交事务
线程池处理计算数据并落地
ThreadPoolExecutor pool = new ThreadPoolExecutor(// 自定义一个线程池
5, // coreSize
15, // maxSize
20, // 20s
TimeUnit.SECONDS, new ArrayBlockingQueue<>(50) // 有界队列,容量是3个);
拒绝策略 默认 直接抛异常
查询计算结果时发现未生成会重新计算 弹出友好提示
RedissonClient redissonClient = Redisson.create(config);
RList<Object> list = redissonClient.getList("");
list.clear();
List<Object> objects = list.readAll();
计算完成后 放入消息队列 点对点 单线程异步消费
入库失败 日志记录
大概500家单位 计算完成 两分钟左右
入库完成 五分钟左右 (innodb 入库略慢)
调优
jvm调优
参数
调整老年代年轻代比例
设置大对象内存阈值 防止都进入老年代 频繁gc
程序逻辑
内存泄漏
弱引用
sql查询list数据过大
硬件
索引调优
explain
key
row 扫描行数
type 看是不是all 全表扫描
索引失效的情况
数据库调优
分库分表
sql 调优
索引失效
sql 检索问题
oom排查
print dump filepath 打印dump文件
子主题 1
dump 快照
#出现 OOME 时生成堆 dump:
-XX:+HeapDumpOnOutOfMemoryError
#生成堆文件地址:
-XX:HeapDumpPath=/home/liuke/jvmlogs/
jmap -dump:format=b,file=20170307.dump 120808
用jvisualvm 装入 分析
jvisualvm 解析 dump
大小排序 能看到最大的对象类型
由对象看到引用 具体对应类名
死锁排查
jconsol 线程 会有个检查死锁
跟到具体代码
cpu100
top 根据占用率排序 拿到 pid 进程id
top -Hp pid 根据进程id拿到 线程id
根据线程id 转换成十六进制
jstack 十六进制
子主题 7
CPU满载 100%
查消耗cpu最高的进程Pid
top -c
按下P,进程按照Cpu使用率排序
根据Pid查出消耗cpu最高的线程号
top -Hp 3033
按下P,进程按照Cpu使用率排序
这是十进制的数据,转成十六进制为0Xbda
根据线程号查出对应的java线程(包含具体类信息),进行处理。
jstack -l 3033 > ./3033.stack
cat 3033.stack |grep 'bda' -C 8
技师系统
项目总结
保证抢单稳定性
业务逻辑
主要针对
订单状态的分布式事务保证
订单要点
保证订单的幂等性
后端处理幂等性
1. 前端进入下单页面,先调取生成唯一订单号的服务
2. 前端下单时带着订单号到后端创建订单
前端方案
前端防重复点击处理
后端
下单接口防重复点击处理
每5秒钟只允许下单一次
分布式全局订单号生成
1. 唯一前缀+毫秒的时间戳+5位随机数 ==> 3+13+5=21位
2. 美团的left
状态变更的ABA问题
1. 利用数据的乐观锁的机制,在数据表中添加version字段
读写分离优化
1. 利用MySql主从的机制,将读操作和写操作分离
数据归档
1. 订单是有时间属性的,可以按照三个月一归档的维度,归档订单数据
2. 将订单数据从原数据表抽离到MongoDB中存储
项目相关
订单总数量
1200W
订单每日数量
10W左右(最大)
2W左右(平均)
商户数量
5.2W左右
车主数量
30W左右
技师数量
8W左右
QPS
3000左右(大促峰值)
100左右(平时)
同时抢单最大数量
200左右
订单流程
问题
1. 在项目中使用SecureRandom实例获取随机数的时候,线上偶尔出现接口超时的情况
2.parallelStream引发的线程池无线程可用问题
3. 内存泄露的问题
项目优化
1. 基于aop封装的接口防止调用的注解
2. 基于aop+SpEL封装的分布式锁注解
面试题
1. 比如说你现在运行的系统,突然线上挂了?讲一下你的排查思路
2. 如果你们线上CPU突然飙高,达到了100%,你怎么排查呢?
3. 线上有没有JVM调优的经验?说一下调优的过程?
4. 排查的时候具体有哪些命令呢?
5. 说一下线上MySQL怎么调优的?
6. 有没有做过某个功能或者系统的重构或者优化?
封装了一个基于订单操作的分布式锁的注解
7. codereview的流程?还你会重点关注哪些点?
8. 你觉得你在工作中最大的优势是什么?
9. 你觉得你工作中还有哪些不足?
0 条评论
下一页