java
2024-10-31 23:28:30 2 举报
AI智能生成
java面试题
作者其他创作
大纲/内容
java基础
String
Comparable 和 Comparator的比较
Comparable是排序接口;若一个类实现了Comparable接口,就意味着“该类支持排序”。
而Comparator是比较器;我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
我们不难发现:Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
而Comparator是比较器;我们若需要控制某个类的次序,可以建立一个“该类的比较器”来进行排序。
我们不难发现:Comparable相当于“内部比较器”,而Comparator相当于“外部比较器”。
集合
map
hashMap
1.7
数组+链表
头插法
头插法造成环状结构
hash和length取模 hashcode&(length-1)
1.8
数组+链表+红黑树
尾插法
h = (h>>>16^h) h&(length-1)
为了保证在数组很小的时候,键值对的分配也比较均匀
扩容机制
LoadFactory 默认0.75
创建一个空数组 重新hash hash公式跟长度有关
扩容条件
达到阈值并且造成hash冲突
问题:不扩容情况下,hashmap能存多少对键值对? 27; 12个元素在一个链表上(红黑树),后来的15个各占一个数组上的位置
线程不安全
2的次幂
方便位运算
均匀分布
重写equals必须重写hashCode
为了避免equals相等而hashCode不等的情况
concurrentHashMap
安全失败
1.7
数组+链表
segment分段锁
继承reentrantLock
尝试获取锁存在并发竞争 自旋 阻塞
get高效 volatile修饰 不需要加锁
volatile修饰节点指针
HashEntry
1.8
数组+链表+红黑树
CAS+Synchronized
cas失败自旋保证成功
再失败就sync保证
node
hashTable
linkedHashMap
List
ArrayList
数组
查找访问速度快 增删效率低 线程不安全
linkedList
双向链表
适合插入删除频繁的情况 内部维护了链表的长度
vector
线程安全
set
基于hashMap实现
抽象类,接口
设计模式
1.工厂
2.简单工厂
3.静态工厂
4.单例
5.模板
6.代理
7.观察者
8.责任链
Spring拦截器链、servlet过滤器链等都采用了责任链设计模式。
9.策略
设计模式的八大原则
开闭
依赖倒置
单一
里氏替换
接口隔离
迪米特法则又叫作最少知识原则
合成复用
多线程
Happens-Before原则
Happens-Before八大规则
1.程序顺序规则
2.锁定规则
3.volatile变量规则
4.线程启动规则
5.线程结束规则
6.中断规则
7.终结器规则
8.传递性规则
Happens-Before规则的真正意义
volatile
指令重排
为了提高性能,编译器和处理器常常会对既定的代码执行顺序进行指令重排序。
重排序的类型有哪些? 源码到执行经过的重排序过程有哪些
volatile可以防止指令重排
volatile底层是如何基于内存屏障保证可见性和有序性的?
MESI 缓存一致性协议
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
怎么发现数据失效的呢?
嗅探: 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
总线风暴
由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。
所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。
所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。
CAS
多线程
实现方式
extends Thread
implements Runnable
implements Callable
线程池
为什么使用线程池
1.减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
2.可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
ThreadPoolExecutor核心参数
核心线程数
最大线程数
空闲时间
空闲时间单位
缓冲队列
LinkedBlockingQueue
无界 当心内存溢出
ArrayBlockingQueue
有界
加锁保证安全 一直死循环阻塞队列不满就唤醒
入队
阻塞调用方式 put(e) 或者 offer(e,timeout,unit) 阻塞调用时,唤醒条件为超时或者队列非满(因此要求在出队列时,要发起一个唤醒操作),在进行某项业务存储操作时,建议用offer进行添加,可及时获取boolean进行判断,如果用put要考虑阻塞情况(队列出队操作慢于进队操作),资源占用
线程工厂
拒绝策略
常用实现
newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newWorkStealingPool
不是ThreadPoolExecutor的扩展,它是新的线程池类ForkJoinPool的扩展,但是都是在统一的一个Executors类中实现,由于能够合理的使用CPU进行对任务操作(并行操作),所以适合使用在很耗时的任务中:
fork/join
Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行join 汇总。
常用队列
ArrayBlockingQueue:有界队列,内部实现是将对象放到一个数组里
LinkedBlockingQueue :内部是一个链式结构对元素进行存储.可以设定一个长度上限,如果没有定义的话,默认为Integer.MAX_VALUE
DelayBlockingQueue : 对元素进行持有直到一个特定的延迟到期,注入其中的元素必须实现java.util.concurrent,Delayed接口
PriorityBlockingQueue: 无界并发队列,无法插入null值,能插入的元素必须实现了java.long.Comparable接口,队列中元素顺序取决于自己的Comparable实现
SynchronousQueue:这是一个特殊的队列,内部同时只能够容纳单个元素.如果队列已有一元素的话,试图想该队列中插入一个新元素将会阻塞,直到另一个线程从队列中把元素抽走.
队列常用方法
插入:add(e) put(e) offer(e) 区别是add在超出队列长度的时候会直接抛异常(java.lang.IllegalStateException: Queue full),put方法会阻塞等待空间,offer方法会返回false;
从队列中取出并移除头元素的方法:remove,take,poll; 若对队列为空remove方法会抛NosuchElementException异常,take会阻塞等待,poll返回null;
返回队列头元素 不删除的方法有: element(),peek();区别是 如果队列为空,element()会抛NoSuchElementException异常,peek()方法只会返回null;
从队列中取出并移除头元素的方法:remove,take,poll; 若对队列为空remove方法会抛NosuchElementException异常,take会阻塞等待,poll返回null;
返回队列头元素 不删除的方法有: element(),peek();区别是 如果队列为空,element()会抛NoSuchElementException异常,peek()方法只会返回null;
自己实现一个队列
使用队列实现生产者-消费者
实际使用
商品详情页面
批处理
运行状态
有个Volatile的状态码
running
shutdown
stop
terminated
所有线程销毁
corePooSize maximumPoolSize largestPoolSize 有意思
largestPoolSize是记录线程池曾达到的最大线程数 <= maximumPoolSize
CompletableFuture
Future vs CompletableFuture
CompletableFuture的使用
故障
如何监控线程池
监控思路
无论是基于开源的监控体系,或者闭源的体系,我个人的思路大体类似。
大体可以分为以下步骤:
收拢线程池的创建入口,统一管理线程池对象
开启Schedule线程定时访问对象池,基于线程池对象api进行打点。
对接监控平台,采集第2步埋的keys,配置规则进行展示。
配置阀值预警
基于大家的使用方式不同,每一步可能各有差异,可自由修改。
比如第一步也可以用Spring管理以及查找。第二步也可以不用metrics api,用log解析的方式。
大体可以分为以下步骤:
收拢线程池的创建入口,统一管理线程池对象
开启Schedule线程定时访问对象池,基于线程池对象api进行打点。
对接监控平台,采集第2步埋的keys,配置规则进行展示。
配置阀值预警
基于大家的使用方式不同,每一步可能各有差异,可自由修改。
比如第一步也可以用Spring管理以及查找。第二步也可以不用metrics api,用log解析的方式。
1.使用API
ThreadPoolExecutor提供的监控api
int getCorePoolSize():核心线程数。
int getLargestPoolSize():历史峰值线程数。
int getMaximumPoolSize():最大线程数(线程池线程容量)。
int getActiveCount():当前活跃线程数,Tag:thread.pool.active.size。
int getPoolSize():当前线程池中运行的线程总数
当前任务队列中积压任务的总数,getQueue.size();
int getCorePoolSize():核心线程数。
int getLargestPoolSize():历史峰值线程数。
int getMaximumPoolSize():最大线程数(线程池线程容量)。
int getActiveCount():当前活跃线程数,Tag:thread.pool.active.size。
int getPoolSize():当前线程池中运行的线程总数
当前任务队列中积压任务的总数,getQueue.size();
除此之外我们还需要补充线程池名称,可以通过创建线程池时定义。
2.借助开源工具
在开源产品中,我们可以选择基于Grafana、Prometheus、Influxdb、MicroMeter等实现。
介绍比较常见的两种:
基于Grafana+Influxdb实现。
基于Grafana+Prometheus+MicroMeter实现。
介绍比较常见的两种:
基于Grafana+Influxdb实现。
基于Grafana+Prometheus+MicroMeter实现。
线程常用方法
线程状态
fork/join
死锁
锁
锁的分类
独享锁/共享锁
互斥锁/读写锁
悲观锁/乐观锁
公平锁/非公平锁
可重入锁/不可重入锁
分段锁
偏向锁/轻量级锁/重量级锁
自旋锁
synchronized
实现原理
锁升级过程
锁粗化
锁消除
对象
对象头Header
Mark Word(存储对象的HashCode,分带年龄和锁标志位信息)
Klass Point(对象执行它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的示例)
Monitor
Entrylist
Owner(会指向持有Monitor对象的线程)
WaitSet
实例数据
对其填充
方法
ACC_SYNCHRONIZED
锁住的是对象 this
代码块
monitorEnter
monitorExit
程序计数器count 加减
特性保证
有序性
as-if-serial
happens-before
可见性
内存强制刷新
原子性
单一线程持有
可重入性
计数器
重锁
用户态内核态切换
LOCK
常用方法
接口实现
ReentrantLock
NonfairSync
tryAcquire
acquireQueued
CAS
FairSync
hasQueuedPredecessors
如果是当前线程持有锁 可重入
公平锁和非公平锁
AQS(AbstractQueuedSynchronizer)
lock()过程
lock()获取锁的流程总结
unlock()过程
总结
AbstractQueuedSynchronizer(AQS)
入队 出队
头节点设计
共共享和独享的实现
CAS
实际应用
存在的问题
cpu开销
只能保证一个共享变量原子操作
AtomicReference
ABA
标志位 时间戳
ReentrantReadWriteLock
ReadLock
WriteLock
实现原理
Synchronized和Lock的区别
synchronized和lock用途区别
ThreadLocal
吊打面试官的ThreadLocal知识
1.ThreadLocal的主要作用
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,防止自己的变量被其它线程篡改。
2.ThreadLocal的隔离有什么用,会用在什么场景
a.Spring实现事务隔离级别
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring的事务主要是ThreadLocal和AOP去做实现的
3.自己有使用他的场景么?一般你会怎么用呢?
a.之前我们上线后发现部分用户的日期居然不对了,排查下来是SimpleDataFormat的锅,当时我们使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat?
所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。
其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat?
所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。
b.我在项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了ThreadLocal去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了。
使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了,所以我使用到了ThreadLocal去做了一下改造,这样只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了。
c.在Android中,Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。
4.底层实现的原理
a.使用方法
使用真的很简单,线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。
b.看一下它的set方法
set的源码很简单,主要就是ThreadLocalMap我们需要关注一下,而ThreadLocalMap呢是当前线程Thread一个叫threadLocals的变量中获取的。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
return t.threadLocals;
}
c.能够实现资源隔离的原因
hreadLocal数据隔离的真相,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。
5.ThreadLocalMap底层结构是怎么样子的呢?
a.既然有个Map那他的数据结构其实是很像HashMap的,但是看源码可以发现,它并未实现Map接口,而且他的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。
b.结构
子主题
c.为什么需要数组呢?没有了链表怎么解决Hash冲突呢?
1.用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。
2.解决hash冲突
看一下源码
从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。
后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
replaceStaleEntry(key, value, i);
return;
}
如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;
if (k == key) {
e.value = value;
return;
}
e.value = value;
return;
}
如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。
get 方法的源码
6.对象存放在哪里
在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
7.那么是不是说ThreadLocal的实例以及其值存放在栈上呢?
其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
8.想共享线程的ThreadLocal数据怎么办?
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
怎么传递的呀?
传递的逻辑很简单,我在开头Thread代码提到threadLocals的时候,你们再往下看看我刻意放了另外一个变量:
Thread源码中,我们看看Thread.init初始化创建的时候做了什么:
如果线程的inheritThreadLocals变量不为空,比如我们上面的例子,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。
9.ThreadLocal的问题了
内存泄露
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
解决办法
代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了
remove的源码很简单,找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。
10.为什么ThreadLocalMap的key要设计成弱引用
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
补充一点:ThreadLocal的不足,我觉得可以通过看看netty的fastThreadLocal来弥补,大家有兴趣可以康康
补充一点:ThreadLocal的不足,我觉得可以通过看看netty的fastThreadLocal来弥补,大家有兴趣可以康康
常见问题
线程间是怎么进行通信的
主要可以介绍一下wait/notify 机制 共享变量的synchronized或者Lock同步机制
volatile
CountDownLatch
只有一个构造方法,只会被赋值一次
没有别的地方可以修改count
CyclicBarrier
CountDownLatch和CyclicBarrier区别
con用于主线程等待其他子线程任务都执行完毕后再执行,cyc用于一组线程相互等待大家都达到某个状态后,再同时执行;
CountDownLatch是不可重用的,CyclicBarrier可重用
CountDownLatch是不可重用的,CyclicBarrier可重用
ThreadLocal用来解决什么问题
可以尽量减少临界区范围,使用ThreadLocal,减少线程切换,使用读写锁或copyonwrite等机制这些方面来回答
如何尽可能提高多线程并发性能
ThreadLocal是如何实现的
Thread不是用来解决线程共享变量的,而是用来解决线程数据隔离的
读写锁适用于什么场景
读写锁适用于读并发高,写并发少的场景。另外一种解决这种场景可以用coponwirte
如何实现一个生产者和消费者
可以通过信号量,阻塞队列,锁,线程通信等不同方式实现
JVM
组成
类加载
GC
JVM硬核 问题
1.yong gc,old gc,full gc,mixed gc 傻傻分不清楚
GC分为两类 Partial GC 和 Full GC
Partial GC分为
young gc
单单收集年轻代的gc
大致上可以认为在年轻代Eden区快要被占满的时候回触发young gc。这里说大致上,因为有一些垃圾收集齐实现是在 full gc之前会先执行 young gc 比如 Parallel Scavenge,不过有参数可以调整让其不进行young gc。正常情况下就当做Eden区快慢了即可。
Eden区快满的触发条件有两个,一个是为对象分配内存不够,一个是为TLAB分配内存不够
关于TLAB
old gc
单单收集年老代的gc
mixed gc
这个是G1 收集器特有的,指的是收集整个年轻代和年老代的GC
Full GC 整堆回收
包括年轻代,年老代,如果有永久代,还包括永久代
full gc的触发条件有点多
在要进行young gc的时候,根据之前统计数据发现年轻代平均晋升大小比现在老年代剩余空间要大那就触发full gc
有永久代的话,如果永久代满了 也会触发full gc
老年代空间不足,大对象直接在老年代申请分配,如果此时老年代空间不足则会触发full gc
担保失败,即promotion failure,新生代的to区放不下从eden区和from拷贝过来的对象,或者新生代对象gc年龄达到阈值需要晋升这两种情况,老年代如果放不下的话就会触发full gc
执行System.gc(),jmap -dump等命令会触发full gc
其实还有Major GC 这个名词,在《深入理解java虚拟机》中这个名词指代的是单单老年代的GC 也就是和Old gc等价的,不过也有很多资料认为其是和Full GC是等价的。 还有Minor GC 指的就是年轻代的GC
2.知道TLAB吗,来说说看
这个需要从内存申请说起,一般而言生成对象需要向堆中的新生代申请内存空间,而堆又是全局共享的,像新生代内存又是规整的,是通过一个指针来划分的。内存是紧凑的,新对象创建指针就右移对象大小size即可,这叫指针加法,可想而知如果多个内存在分配对象没那么指针就会成为热点资源,需要互斥,那分配的效率就低了。于是搞了个TLAB(Thread Local Allocation Buffer) 为一个线程分配的内存申请区域
这个区域只允许这一个线程申请分配对象,允许所有线程访问这块区域
TLAB的思想其实很简单,就是划一块区域给一个线程,每个线程只需要在自己的那块地申请对象内存,不需要争抢热点指针,当这块内存用完了之后,再去申请即可。
这种思想很常见,比如分布式发号器,每次不会一个一个号的取,会取一批,用完之后再去申请一批。
不过每次申请的大小不固定,会根据该线程启动到现在的历史信息来调整,比如这个线程一直在分配内存那么TLAB就大一些,如果这个线程不会申请分配内存那么TLAB就小一些
TLAB会浪费内存空间,TLAB内部如果剩余空间不足以分配给对象,这个时候需要申请一块TLAB,之前剩余的就浪费了,在HotSpot中会生成一个填充对象来填满这一块,因为堆需要线性遍历,遍历是通过对象头得知对象大小,然后跳过这个大小就能到下一个对象,所有不能有空洞。
TLAB自能分配小对象,大的对象还是需要在共享的Eden区分配
所以说TLAB是为了避免对象分配时的竞争而设计的
3.那PLAB知道吗?
Promotion Local Allocation Buffers
4.简单说下G1回收流程
G1从大局上分为两阶段,并发标记和对象拷贝
并发标记 基于STAB 可分为四个阶段
1.初始标记(init marking) 这个阶段是STW的,扫描根集合,标记根直接可达的对象即可,在G1中标记对象是利用外部的bitMap来记录,而不是对象头
2.并发阶段(concurrent marking) 这个阶段和应用线程并发,从上一步标记的根直达对象进行tracing,递归扫描所有可达对象,STAB也会在这个阶段巨鹿这变更的引用
3.最终标记(final marking) 这个阶段是STW的,处理STAB中的引用
4.清理阶段(cleanu) 根据标记的bitMap,统计每个region存活对象的多少,如果完全没存活的 region则整体回收
对象拷贝(evacuation)
这个阶段是STW的,根据标记结果选择合适的region组成收集集合(collection set即Cset),然后将CSet存活的对象拷贝到新的region中
G1的瓶颈在于对象拷贝阶段,需要花较多的瓶颈来转移对象
5.简单说下CMS回收流程
1.初始标记 initial marking 这个阶段是STW的,扫描跟集合,标记根直达的对象即可
2.并发标记 concurrent marking 这个阶段和应用线程并发,从上一步标记的根直达对象开始进行 tracing,递归扫描所有可达对象
3.并发清理 concurrent precleaning 这个阶段和应用线程并发,就是想帮重新标记阶段先做点工作,扫描一下卡表脏的区域和新晋升到老年代的对象等,因为重新标记是STW的 所以分担一点
4.可中断的预清理阶段 AbortablePreclean 这个和上一个阶段一致,就是为了分担重新标记的标记工作
5.重新标记 remark 这个阶段是STW的,因为并发阶段应用关系会发生变化,所以要重新遍历一遍新生代对象,GC ROOTS 卡表,来修正标记
6.并发清理 concurrent sweeping 这个阶段和应用线程并发,用于清理垃圾
7.并发重置 concurrent reset 这个阶段和应用线程并发,重置CMS内部状态
cms的瓶颈就在于重新标记阶段,需要花费时间来进行重新扫描
6.GC调优的两大目标是啥?
分别是最短暂停时间和吞吐量
最短暂停时间
因为GC会STW暂停所有应用线程,这个时候对用户而言等于卡顿,因此对于时延敏感的应用来说减少STW 的时间是关键
吞吐量
对于一些对时延不敏感的应用比如一些后台计算机应用来说,吞吐量是关注点,它们不关注每次GC 的停顿时间,只关注总的停顿时间,吞吐高。
7.GC如何调优化
思路
调优的思路就是尽量让对象在新生代就被回收,防止过多对象晋升到老年代,减少大对象分配
需要平衡分代的大小,垃圾回收的次数和停顿时间
对GC进行完整监控,监控各年代占用大小,YGC触发频率,Full gc触发频率,对象分配速率扥等
分布式锁
zookeeper
基于临时有序节点
watched 监控当前节点的上一个节点(如果上一个节点被移除 自己就可以获取锁)
死锁
羊群效应
性能没有redis高
redis
使用客户端redission 基于setNx 集成了java的Lock接口 容易上手 默认有效时间30s 锁会有自动续时机制(每10秒检查一次)
性能比较高
可能会出现重复加锁
数据库
基于数据表
死锁
设置一个失效时间 用定时任务去跑
数据库集群 主备同步
搞个死循环排队
可重入设计一个字段累加
排它锁
用数据库本身的锁就行了 索引
for update 或者 lock for share mode
记得提交
宕机数据库也会自动释放
缺点
更为复杂
消耗资源
特点
互斥
安全性
容错
死锁
java框架
spring
设计模式
单例
工厂
适配器
根据不同的商家适配
责任链
继承process链路执行
Bean
Bean生命周期
扫描类 InvokerBeanFactoryPostProcessors
封装BeanDef
放到map 各种信息
遍历map
验证
能不能实例化,需要实例化吗,根据信息来
是否单例等等
判断是不是factoryBean
单例池 只是一个ConcurrentHashMap而已
正在创建的容器
得到class
推断构造方法
根据注入模型
默认
反射 实例化这个对象
后置处理器beanDef
判断是否允许循环依赖
提前暴露bean工厂对象
填充属性 自动注入
执行部分aware接口
继续执行部分aware接口 生命周期回调方法
完成代理AOP
beanProstProcessor的前置方法
实例化为bean
放到单例池
销毁
作用域scope
单例 singleton
多例prototype 每次获取bean都是新的对象
request
session
循环依赖
情况
属性注入可以破解
构造器不行 三次缓存没自己 因二级之后去加载B了
三次缓存
去单例池拿
判断是不是正在被创建
判断是否支持循环依赖
二级缓存放到三级缓存
干掉二级缓存 GC
下次再来直接三级缓存中拿 缓存
缓存存放
一级缓存 单例Bean
二级缓存 工厂产生的bean 产生bean复杂
三级缓存 半成品
父子容器
事务的实现原理
采用不同的连接器
用AOP新建了一个链接 共享链接
ThreadLocal当前事务
关闭 autoCommit
AOP
静态代理
动态代理
jdk动态代理
实现接口 基于java生成代理接口的匿名类 调用具体方法的时候调用具体的invokeHanlder
CGlib
asm字节码编辑动态创建类 基于ClassLoad装载 修改字节码生成子类去处理
IOC
类加载机制
加载过程
加载
生成一个class对象
连接
验证
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
默认值
为static分配内存
解析
解析具体类的信息 引用等
初始化
父类没初始化 先初始化父类
加载方式
main
class.forName
ClassLoad.loadClass
类加载器
BootStrapClassLoader
ExtentionClassLoader
System/App ClassLoader
自定义类加载器
双亲委派
安全
避免重复加载
springMvc
执行过程
springMVC是由dispatchservlet为核心的分层控制框架。首先客户端发出一个请求web服务器解析请求url并去匹配dispatchservlet的映射url,如果匹配上就将这个请求放入到dispatchservlet,dispatchservlet根据mapping映射配置去寻找相对应的handel,然后把处理权交给找到的handel,handel封装了处理业务逻辑的代码,当handel处理完后会返回一个逻辑视图modelandview给dispatchservlet,此时的modelandview是一个逻辑视图不是一个正式视图,所以dispatchservlet会通过viewresource视图资源去解析modelandview,然后将解析后的参数放到view中返回到客户端并展现。
启动流程
应用根上下文(Root ApplicationContext)的启动
1.在一个web项目中使用SpringMVC时,需在web.xml中配置一个监听器:
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
mybatis
hibernate
SpringBoot
启动过程
SpringApplication的初始化过程
SpringApplication的run()方法
自动装配
SpringCloud
注册中心
Eureka
工作流程
心跳机制
Eureka集群
Nacos
注册中心
配置中心
Zookeeper
节点模式
几种注册中心的对比
远程调用
HttpClient
RestTemplate
feign/openfeign
原理
调用的重要过程
在微服务启动时,Feign会进行包扫描,对加@FeignClient注解的接口,按照注解的规则,创建远程接口的本地JDK Proxy代理实例。然后,将这些本地Proxy代理实例,注入到Spring IOC容器中。当远程接口的方法被调用,由Proxy代理实例去完成真正的远程访问,并且返回结果。
调用处理器 InvocationHandler
通过 JDK Proxy 生成动态代理类,核心步骤就是需要定制一个调用处理器,具体来说,就是实现JDK中位于java.lang.reflect 包中的 InvocationHandler 调用处理器接口,并且实现该接口的 invoke(…) 抽象方法。
为了创建Feign的远程接口的代理实现类,Feign提供了自己的一个默认的调用处理器,叫做 FeignInvocationHandler 类,该类处于 feign-core 核心jar包中。当然,调用处理器可以进行替换,如果Feign与Hystrix结合使用,则会替换成 HystrixInvocationHandler 调用处理器类,类处于 feign-hystrix 的jar包中。
为了创建Feign的远程接口的代理实现类,Feign提供了自己的一个默认的调用处理器,叫做 FeignInvocationHandler 类,该类处于 feign-core 核心jar包中。当然,调用处理器可以进行替换,如果Feign与Hystrix结合使用,则会替换成 HystrixInvocationHandler 调用处理器类,类处于 feign-hystrix 的jar包中。
默认的调用处理器 FeignInvocationHandler
默认的调用处理器 FeignInvocationHandler 是一个相对简单的类,有一个非常重要Map类型成员 dispatch 映射,保存着远程接口方法到MethodHandler方法处理器的映射。
代理实例的调用处理器 FeignInvocationHandler的dispatch 成员
默认的调用处理器 FeignInvocationHandle,在处理远程方法调用的时候,会根据Java反射的方法实例,在dispatch 映射对象中,找到对应的MethodHandler 方法处理器,然后交给MethodHandler 完成实际的HTTP请求和结果的处理。前面示例中的 DemoClient 远程调用接口,有两个远程调用方法,所以,其代理实现类的调用处理器 FeignInvocationHandler 的dispatch 成员,有两个有两个Key-Value键值对
FeignInvocationHandler源码节选
(1)根据Java反射的方法实例,在dispatch 映射对象中,找到对应的MethodHandler 方法处理器;
(2)调用MethodHandler方法处理器的 invoke(...) 方法,完成实际的HTTP请求和结果的处理。
补充说明一下:MethodHandler 方法处理器,和JDK 动态代理机制中位于 java.lang.reflect 包的 InvocationHandler 调用处理器接口,没有任何的继承和实现关系。MethodHandler 仅仅是Feign自定义的,一个非常简单接口。
源码很简单,重点在于invoke(…)方法,虽然核心代码只有一行,但是其功能是复杂的:
(1)根据Java反射的方法实例,在dispatch 映射对象中,找到对应的MethodHandler 方法处理器;
(2)调用MethodHandler方法处理器的 invoke(...) 方法,完成实际的HTTP请求和结果的处理。
MethodHandler源码
Feign的方法处理器 MethodHandler 是一个独立的接口,定义在 InvocationHandlerFactory 接口中,仅仅拥有一个invoke(…)
MethodHandler 的invoke(…)方法,主要职责是完成实际远程URL请求,然后返回解码后的远程URL的响应结果。Feign提供了默认的 SynchronousMethodHandler 实现类,提供了基本的远程URL的同步请求处理。有关 SynchronousMethodHandler类以及其与MethodHandler的关系,如图
补充说明一下:MethodHandler 方法处理器,和JDK 动态代理机制中位于 java.lang.reflect 包的 InvocationHandler 调用处理器接口,没有任何的继承和实现关系。MethodHandler 仅仅是Feign自定义的,一个非常简单接口。
过程分析
1.通过Spring IOC 容器实例,装配代理实例,然后进行远程调用
例子:DemoClient是@FeugnClient 的接口 UserController是测试用的Controller
Feign在启动时,会为加上了@FeignClient注解的所有远程接口(包括 DemoClient 接口),创建一个本地JDK Proxy代理实例,并注册到Spring IOC容器。在这里,暂且将这个Proxy代理实例,叫做 DemoClientProxy,稍后,会详细介绍这个Proxy代理实例的具体创建过程。
在本实例的UserController 调用代码中,通过@Resource/@AutoWired注解,按照名称/类型进行匹配(这里的类型为DemoClient接口类型),从Spring IOC容器找到这个代理实例,并且装配给@Resource/@AutoWired注解所在的成员变量,本实例的成员变量的名称为 demoClient。
在需要代进行hello()远程调用时,直接通过 demoClient 成员变量,调用JDK Proxy动态代理实例的hello()方法。
在需要代进行hello()远程调用时,直接通过 demoClient 成员变量,调用JDK Proxy动态代理实例的hello()方法。
2.执行 InvokeHandler 调用处理器的invoke(…)方法
JDK Proxy动态代理实例的真正的方法调用过程,具体是通过 InvokeHandler 调用处理器完成的。故,这里的DemoClientProxy代理实例,会调用到默认的FeignInvocationHandler 调用处理器实例的invoke(…)方法。
默认的调用处理器 FeignInvocationHandle,内部保持了一个远程调用方法实例和方法处理器的一个Key-Value键值对Map映射。FeignInvocationHandle 在其invoke(…)方法中,会根据Java反射的方法实例,在dispatch 映射对象中,找到对应的 MethodHandler 方法处理器,然后由后者完成实际的HTTP请求和结果的处理。
3.执行 MethodHandler 方法处理器的invoke(…)方法
feign默认的方法处理器为 SynchronousMethodHandler,其invoke(…)方法主要是通过内部成员feign客户端成员 client,完成远程 URL 请求执行和获取远程结果。
feign.Client 客户端有多种类型,不同的类型,完成URL请求处理的具体方式不同。
1)Client.Default类:默认的feign.Client 客户端实现类,内部使用HttpURLConnnection 完成URL请求处理;
(2)ApacheHttpClient 类:内部使用 Apache httpclient 开源组件完成URL请求处理的feign.Client 客户端实现类;
(3)OkHttpClient类:内部使用 OkHttp3 开源组件完成URL请求处理的feign.Client 客户端实现类。
(4)LoadBalancerFeignClient 类:内部使用 Ribben 负载均衡技术完成URL请求处理的feign.Client 客户端实现类。
(2)ApacheHttpClient 类:内部使用 Apache httpclient 开源组件完成URL请求处理的feign.Client 客户端实现类;
(3)OkHttpClient类:内部使用 OkHttp3 开源组件完成URL请求处理的feign.Client 客户端实现类。
(4)LoadBalancerFeignClient 类:内部使用 Ribben 负载均衡技术完成URL请求处理的feign.Client 客户端实现类。
实际上这一步可以分为2步
SynchronousMethodHandler 并不是直接完成远程URL的请求,而是通过负载均衡机制,定位到合适的远程server 服务器,然后再完成真正的远程URL请求。换句话说,SynchronousMethodHandler实例的client成员,其实际不是feign.Client.Default类型,而是 LoadBalancerFeignClient 客户端负载均衡类型。 因此,上面的第3步,如果进一步细分话,大致如下:(1)首先通过 SynchronousMethodHandler 内部的client实例,实质为负责客户端负载均衡 LoadBalancerFeignClient 实例,首先查找到远程的 server 服务端;(2) 然后再由LoadBalancerFeignClient 实例内部包装的feign.Client.Default 内部类实例,去请求server端服务器,完成URL请求处理
4.通过 feign.Client 客户端成员,完成远程 URL 请求执行和获取远程结果
熔断限流
Hystrix
流程
对于一次依赖调用,会被封装在一个HystrixCommand对象中,调用的执行有两种方式,一种是调用execute()方法同步调用,另一种是调用queue()方法进行异步调用。
执行时会判断断路器开关是否打开,如果断路器打开,则进入getFallback()降级逻辑;如果断路器关闭,则判断线程池/信号量资源是否已满,如果资源满了,则进入getFallback()降级逻辑;如果没满,则执行run()方法。再判断执行run()方法是否超时,超时则进入getFallback()降级逻辑,run()方法执行失败,则进入getFallback()降级逻辑,执行成功则报告Metrics。Metrics中的数据包括执行成功、超时、失败等情况的数据,Hystrix会计算一个断路器的健康值,也就是失败率,当失败率超过阈值后则会触发断路器开关打开。
getFallback()逻辑为:如果没有实现fallback()方法,则直接抛出异常,另外fallback降级也是需要资源的,在fallback时需要获取一个针对fallback的信号量,只有获取成功才能fallback,获取信号量失败,则抛出异常,获取信号量成功,才会执行fallback方法并且会响应fallback方法中的内容。
执行时会判断断路器开关是否打开,如果断路器打开,则进入getFallback()降级逻辑;如果断路器关闭,则判断线程池/信号量资源是否已满,如果资源满了,则进入getFallback()降级逻辑;如果没满,则执行run()方法。再判断执行run()方法是否超时,超时则进入getFallback()降级逻辑,run()方法执行失败,则进入getFallback()降级逻辑,执行成功则报告Metrics。Metrics中的数据包括执行成功、超时、失败等情况的数据,Hystrix会计算一个断路器的健康值,也就是失败率,当失败率超过阈值后则会触发断路器开关打开。
getFallback()逻辑为:如果没有实现fallback()方法,则直接抛出异常,另外fallback降级也是需要资源的,在fallback时需要获取一个针对fallback的信号量,只有获取成功才能fallback,获取信号量失败,则抛出异常,获取信号量成功,才会执行fallback方法并且会响应fallback方法中的内容。
中间件
Redis
数据结构
String
List
注意分页的坑
Set
Zset
结构
跳表 用随机函数维护平衡
近2分查找 时间复杂度O(log N)
score
随机层数 主需要调整前后节点的指针
不知比较score 还会比较value
场景
成绩
积分
排行榜
Hash
hash是String类型的field和value的映射表
每个hash可以存储2^32-1 约40亿的键值对
HyperLogLog
Geo
Pub/Sub
BitMap
底层
SDS
常见命令
keys
setnx
exprie
高可用
持久化
RDB
5分钟一次
冷备
恢复数据时比较快
快照文件生成时间久 消耗cpu
AOF
appendOnly
数据齐全
恢复慢 文件大
数据同步
主从复制 ex 指令 of
快照同步 RDB 缓冲区
哨兵
集群监控
消息通知
故障转移
配置中心
脑裂
主从切换过程
集群redis-cluster
一致性hash算法
多主多从
横向扩容 分片
锁
简单实现
Red Lock
Redisson
实现
Watch Dog机制
Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期。
如果自己设置了过期时间WathcDog是否还有效
如果有N把锁 是不是需要有N个WathcDog
https://www.cnblogs.com/keeya/p/14332131.html
https://blog.csdn.net/ice24for/article/details/86177152
Zookeeper
选举机制
过半机制
预提交 2pc
ZAB协议
全称 Zookeeper Atomic Broadcast
原子广播
崩溃修复
zk节点宕机怎么处理
如何实现分布式一致性
用途
注册中心
配置中心
分布式锁
常见客户端
zkClient
curator
分布式锁
Dubbo
Netty
零拷贝
BIO
同步阻塞IO
问题 : 资源 带宽 等
每个请求过来,开一个线程等待
NIO
同步不阻塞IO
等待时间过久 或响应时间太长会重试
IO多路复用,便捷的通知机制
selelct 遍历 判断事件是否可达,然后继续
poll 做了优化
epoll 有转态 会创建 文件描述指向的表 监听增删改查
通道channel buf 监听事件
AIO
异步非阻塞
架构设计思路
执行链路
初始化channel
注册channel 到selector 任务队列
轮训accept事件 处理这些channel的链接
注册channel到selector接收方
轮训写事件 开线程去处理 任务队列
线程组
boss 监听端口所所有准备的时间
work 监听准备工作
调用链路
服务暴露过程
IOC启动加载dubbo配置的标签
解析标签ServiceBean会生成一个Bean
实现了initializingBean afterPropertiesSet
get provider
set provider
各种信息保存在ServiceBean
IOC完成还实现了一个ApplicationListener监听器 回调onapplicationEvent 是否暴露 是否延迟等
暴露 信息校验 doexport
检查
doexportUrl 暴露URL
加载注册中心信息
循环协议 端口 代理工厂获取invoke封装
暴露invoke
根据spi来
本地暴露
打开服务器 exchangeServer
启动服务器netty监听端口
注册中心注册服务 注册表 执行器
暴露p和s两个invoker的map保存了地址
spi
服务引用
FactotyBean -->getObject --> get init
信息检查 创建代理对象 createProxy
远程引用 获取到zk 获取到信息 订阅 dubbo执行远程引用
创建netty客户端 返回invoke 注册到注册表中
成功
SPI
JAVA spi
dubbo的spi
容错机制
fail over 直接切换(默认的)
fail fast 快速失败 直接失败
fail salf 安全失败 适用于写入审计日志
fail back 后台记录失败失败请求,定时重发 通常用于消息通知操作
forking 并行调用多台服务器 只要有一个成功 就成功 可使用 forks=2 设定并行最大调动
broadcast 逐个调用 有一台失败 就失败 适用于通知所有提供者 更新配置 缓存 等
降级
return null
失败返回空
负载均衡
随机 权重
轮训
最少活跃
hash一致
选举算法
注册中心
协议
duubo
rmi
webservice
hassion
http
redis
thrif
rest
memcache
序列化
dubbo
hassion
json
java
MQ
RocketMQ
基础组成
NameServer
无状态模式 早期是zk 后来改了
broker向其发送心跳并带上topic信息
Broker
中转消息 消息持久化
底层基于netty
Producer
同步
异步
单向
Consumer
pull
push
支持集群模式
多master
多master 多slave 异步复制
多master多slave 双写
消费保证
发送成功后返回 consume_success
回溯消费
高可用
集群
刷盘
消息的主从复制
顺序消息
消息去重
分布式事务
最大努力
办消息
2pc
3pc
最终一致
完整的一个调用链
消息重试
顺序消息重试
可以获取消息重试次数
对一个消费者设置 组内都被设置
无需消息重试
死信队列
不再被正常消费
消息保留3天
面向消费者组 单独消费
事务消息
消息丢失
消息积压
决定是否放弃
判断吞吐量
加机器 加topic
RobbitMQ
常见问题
1.为什么使用mq
解耦
削峰
异步
2.为什么选rabbitmq
几种mq的对比
rabbitmq
rocketmq
kafka
3.如何保证消息不被重复消费(幂等)
4.如何保证消息不丢失
从生产者
从mq
从消费者
5.如何保证消息消费的顺序性
6.消息积压怎么解决
7.消息延时
8.死信队列
DB, MQ事务一致性
订阅DB的binglog日志
NSERT、UPDATE等都会产生binlog event,这些event会在本地事务提交成功后,才会生成。
通过一个binlog订阅组件,来订阅数据库中的记录变更。订阅到的binlog event,必然都是已经成功执行的本地事务的信息。可以放心的根据这些event解析出相应的信息,发送消息到MQ中即可。
目前开源的binlog订阅组件有很多,各种语言的实现都有:java、go、python等,首推的还是阿里巴巴开源的canal,服务端使用java编写,支持多语言客户端。
本地事务表
操作数据之前存入db后者redis,根据操作状态去修改数据状态
XXL-JOB
tx-lcn
kafka
原理 为什么这么快
1.顺序读写: kafka的消息是不断追加到文件中的,充分利用了磁盘的顺序读写性能,顺序读写不需要硬盘磁头的寻道时间,只需要很少的扇区时间,所以速度远快于随机读写。
2.零拷贝:Kafka高吞吐量Q的原因其中有个重要技术就是Zero-Copy(零拷贝)系统调用机制
3.批量发送:afka允许批量发送消息,producer发送消息的时候,可以将消息缓存在本地,等到固定条件再发
送到kafka
消息条数满足固定条数
一段时间发送一次数据压缩
kafka还支持对消息集合进行压缩,producer可以通过GZIP或Snappy格式对消息集合进行压缩,
压缩的好处就是减少传输数据量,减轻网络传输压力
4.分区
5.内存池复用
前面说过 Producer 发送消息是批量的,因此消息都会先写入 Producer 的内存中进行缓冲,直到多条消息组成了一个 Batch,才会通过网络把 Batch 发给 Broker。
当这个 Batch 发送完毕后,显然这部分数据还会在 Producer 端的 JVM 内存中,由于不存在引用了,它是可以被 JVM 回收掉的。
但是大家都知道,JVM GC 时一定会存在 Stop The World 的过程,即使采用最先进的垃圾回收器,也势必会导致工作线程的短暂停顿,这对于 Kafka 这种高并发场景肯定会带来性能上的影响。
有了这个背景,便引出了 Kafka 非常优秀的内存池机制,它和连接池、线程池的本质一样,都是为了提高复用,减少频繁的创建和释放。
具体是如何实现的呢?其实很简单:Producer 一上来就会占用一个固定大小的内存块,比如 64MB,然后将 64 MB 划分成 M 个小内存块(比如一个小内存块大小是 16KB)。
当需要创建一个新的 Batch 时,直接从内存池中取出一个 16 KB 的内存块即可,然后往里面不断写入消息,但最大写入量就是 16 KB,接着将 Batch 发送给 Broker ,此时该内存块就可以还回到缓冲池中继续复用了
2.零拷贝:Kafka高吞吐量Q的原因其中有个重要技术就是Zero-Copy(零拷贝)系统调用机制
3.批量发送:afka允许批量发送消息,producer发送消息的时候,可以将消息缓存在本地,等到固定条件再发
送到kafka
消息条数满足固定条数
一段时间发送一次数据压缩
kafka还支持对消息集合进行压缩,producer可以通过GZIP或Snappy格式对消息集合进行压缩,
压缩的好处就是减少传输数据量,减轻网络传输压力
4.分区
5.内存池复用
前面说过 Producer 发送消息是批量的,因此消息都会先写入 Producer 的内存中进行缓冲,直到多条消息组成了一个 Batch,才会通过网络把 Batch 发给 Broker。
当这个 Batch 发送完毕后,显然这部分数据还会在 Producer 端的 JVM 内存中,由于不存在引用了,它是可以被 JVM 回收掉的。
但是大家都知道,JVM GC 时一定会存在 Stop The World 的过程,即使采用最先进的垃圾回收器,也势必会导致工作线程的短暂停顿,这对于 Kafka 这种高并发场景肯定会带来性能上的影响。
有了这个背景,便引出了 Kafka 非常优秀的内存池机制,它和连接池、线程池的本质一样,都是为了提高复用,减少频繁的创建和释放。
具体是如何实现的呢?其实很简单:Producer 一上来就会占用一个固定大小的内存块,比如 64MB,然后将 64 MB 划分成 M 个小内存块(比如一个小内存块大小是 16KB)。
当需要创建一个新的 Batch 时,直接从内存池中取出一个 16 KB 的内存块即可,然后往里面不断写入消息,但最大写入量就是 16 KB,接着将 Batch 发送给 Broker ,此时该内存块就可以还回到缓冲池中继续复用了
选举机制
控制器Broker选举机制
集群中第一个启动的Broker会通过在Zookeeper中创建临时节点/controller来让自己成为控制器,其他Broker启动时也会在zookeeper中创建临时节点,但是发现节点已经存在,所以它们会收到一个异常,意识到控制器已经存在,那么就会在Zookeeper中创建watch对象,便于它们收到控制器变更的通知。
那么如果控制器由于网络原因与Zookeeper断开连接或者异常退出,那么其他broker通过watch收到控制器变更的通知,就会去尝试创建临时节点/controller,如果有一个Broker创建成功,那么其他broker就会收到创建异常通知,也就意味着集群中已经有了控制器,其他Broker只需创建watch对象即可。
如果集群中有一个Broker发生异常退出了,那么控制器就会检查这个broker是否有分区的副本leader,如果有那么这个分区就需要一个新的leader,此时控制器就会去遍历其他副本,决定哪一个成为新的leader,同时更新分区的ISR集合。
如果有一个Broker加入集群中,那么控制器就会通过Broker ID去判断新加入的Broker中是否含有现有分区的副本,如果有,就会从分区副本中去同步数据。
防止控制器脑裂
如果控制器所在broker挂掉了或者Full GC停顿时间太长超过zookeepersession timeout出现假死,Kafka集群必须选举出新的控制器,但如果之前被取代的控制器又恢复正常了,它依旧是控制器身份,这样集群就会出现两个控制器,这就是控制器脑裂问题。
解决方法:
为了解决Controller脑裂问题,ZooKeeper中还有一个与Controller有关的持久节点/controller_epoch,存放的是一个整形值的epoch number(纪元编号,也称为隔离令牌),集群中每选举一次控制器,就会通过Zookeeper创建一个数值更大的epoch number,如果有broker收到比这个epoch数值小的数据,就会忽略消息。
那么如果控制器由于网络原因与Zookeeper断开连接或者异常退出,那么其他broker通过watch收到控制器变更的通知,就会去尝试创建临时节点/controller,如果有一个Broker创建成功,那么其他broker就会收到创建异常通知,也就意味着集群中已经有了控制器,其他Broker只需创建watch对象即可。
如果集群中有一个Broker发生异常退出了,那么控制器就会检查这个broker是否有分区的副本leader,如果有那么这个分区就需要一个新的leader,此时控制器就会去遍历其他副本,决定哪一个成为新的leader,同时更新分区的ISR集合。
如果有一个Broker加入集群中,那么控制器就会通过Broker ID去判断新加入的Broker中是否含有现有分区的副本,如果有,就会从分区副本中去同步数据。
防止控制器脑裂
如果控制器所在broker挂掉了或者Full GC停顿时间太长超过zookeepersession timeout出现假死,Kafka集群必须选举出新的控制器,但如果之前被取代的控制器又恢复正常了,它依旧是控制器身份,这样集群就会出现两个控制器,这就是控制器脑裂问题。
解决方法:
为了解决Controller脑裂问题,ZooKeeper中还有一个与Controller有关的持久节点/controller_epoch,存放的是一个整形值的epoch number(纪元编号,也称为隔离令牌),集群中每选举一次控制器,就会通过Zookeeper创建一个数值更大的epoch number,如果有broker收到比这个epoch数值小的数据,就会忽略消息。
分区副本选举机制
在kafka的集群中,会存在着多个主题topic,在每一个topic中,又被划分为多个partition,为了防止数据不丢失,每一个partition又有多个副本,在整个集群中,总共有三种副本角色:
首领副本(leader):
也就是leader主副本,每个分区都有一个首领副本,为了保证数据一致性,所有的生产者与消费者的请求都会经过该副本来处理。
跟随者副本(follower):
除了首领副本外的其他所有副本都是跟随者副本,跟随者副本不处理来自客户端的任何请求,只负责从首领副本同步数据,保证与首领保持一致。如果首领副本发生崩溃,就会从这其中选举出一个leader。
首选首领副本:
创建分区时指定的首选首领。如果不指定,则为分区的第一个副本。
follower需要从leader中同步数据,但是由于网络或者其他原因,导致数据阻塞,出现不一致的情况,为了避免这种情况,follower会向leader发送请求信息,这些请求信息中包含了follower需要数据的偏移量offset,而且这些offset是有序的。
如果有follower向leader发送了请求1,接着发送请求2,请求3,那么再发送请求4,这时就意味着follower已经同步了前三条数据,否则不会发送请求4。leader通过跟踪 每一个follower的offset来判断它们的复制进度。
默认的,如果follower与leader之间超过10s内没有发送请求,或者说没有收到请求数据,此时该follower就会被认为“不同步副本”。而持续请求的副本就是“同步副本”,当leader发生故障时,只有“同步副本”才可以被选举为leader。其中的请求超时时间可以通过参数replica.lag.time.max.ms参数来配置。
我们希望每个分区的leader可以分布到不同的broker中,尽可能的达到负载均衡,所以会有一个首选首领,如果我们设置参数auto.leader.rebalance.enable为true,那么它会检查首选首领是否是真正的首领,如果不是,则会触发选举,让首选首领成为首领。
首领副本(leader):
也就是leader主副本,每个分区都有一个首领副本,为了保证数据一致性,所有的生产者与消费者的请求都会经过该副本来处理。
跟随者副本(follower):
除了首领副本外的其他所有副本都是跟随者副本,跟随者副本不处理来自客户端的任何请求,只负责从首领副本同步数据,保证与首领保持一致。如果首领副本发生崩溃,就会从这其中选举出一个leader。
首选首领副本:
创建分区时指定的首选首领。如果不指定,则为分区的第一个副本。
follower需要从leader中同步数据,但是由于网络或者其他原因,导致数据阻塞,出现不一致的情况,为了避免这种情况,follower会向leader发送请求信息,这些请求信息中包含了follower需要数据的偏移量offset,而且这些offset是有序的。
如果有follower向leader发送了请求1,接着发送请求2,请求3,那么再发送请求4,这时就意味着follower已经同步了前三条数据,否则不会发送请求4。leader通过跟踪 每一个follower的offset来判断它们的复制进度。
默认的,如果follower与leader之间超过10s内没有发送请求,或者说没有收到请求数据,此时该follower就会被认为“不同步副本”。而持续请求的副本就是“同步副本”,当leader发生故障时,只有“同步副本”才可以被选举为leader。其中的请求超时时间可以通过参数replica.lag.time.max.ms参数来配置。
我们希望每个分区的leader可以分布到不同的broker中,尽可能的达到负载均衡,所以会有一个首选首领,如果我们设置参数auto.leader.rebalance.enable为true,那么它会检查首选首领是否是真正的首领,如果不是,则会触发选举,让首选首领成为首领。
消费组选举机制
在kafka的消费端,会有一个消费者协调器以及消费组,组协调GroupCoordinator需要为消费组内的消费者选举出一个消费组的leader,那么如何选举的呢?
如果消费组内还没有leader,那么第一个加入消费组的消费者即为消费组的leader,如果某一个时刻leader消费者由于某些原因退出了消费组,那么就会重新选举leader,如何选举?
private val members = new mutable.HashMap[String, MemberMetadata]
leaderId = members.keys.headOption
上面代码是kafka源码中的部分代码,member是一个hashmap的数据结构,key为消费者的member_id,value是元数据信息,那么它会将leaderId选举为Hashmap中的第一个键值对,它和随机基本没啥区别。
对于整个选举算法的详情需要先了解Raft选举算法,kafka是基于该算法来实现leader选举的。
如果消费组内还没有leader,那么第一个加入消费组的消费者即为消费组的leader,如果某一个时刻leader消费者由于某些原因退出了消费组,那么就会重新选举leader,如何选举?
private val members = new mutable.HashMap[String, MemberMetadata]
leaderId = members.keys.headOption
上面代码是kafka源码中的部分代码,member是一个hashmap的数据结构,key为消费者的member_id,value是元数据信息,那么它会将leaderId选举为Hashmap中的第一个键值对,它和随机基本没啥区别。
对于整个选举算法的详情需要先了解Raft选举算法,kafka是基于该算法来实现leader选举的。
算法
贪心
分治
动态规划
快排
堆排
二叉树
链表反转
成环
环节点
跳楼梯
协议
CAP
Consistency 一致性
Availability 可用性
Partition tolerance分区容错性
分布式事务协议
2PC
阶段一
事务询问:
1. 协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作。并开始等待各参与者的响应。
执行事务:
1.各参与者节点执行事务操作,并将 undo 和 redo 信息写入事务日志。
2.各参与者向协调者反馈事务询问的响应(YES/NO)。
事务询问:
1. 协调者向所有的参与者发送事务内容,询问是否可以执行事务提交操作。并开始等待各参与者的响应。
执行事务:
1.各参与者节点执行事务操作,并将 undo 和 redo 信息写入事务日志。
2.各参与者向协调者反馈事务询问的响应(YES/NO)。
阶段二(执行事务提交)
如果所有参与者反馈都是 Yes,那就进行事务提交:
1.发送提交请求。
2,事务提交。
参与者接收到Commit请求之后,会正式执行事务提交操作,并在完成提交之后释放资源:
反馈事务提交结果(ack to 协调者)。
协调者完成事务。
阶段二(执行事务滚回)
如果有参与者反馈是NO 或等待超时 那就进行事务滚回:
发送滚回请求。
事务滚回。
参与者接收到Rollback请求之后,通过Undo 滚回事务,并在完成之后释放资源:
反馈事务滚回结果(ack to 协调者)。
协调者中断事务。
如果所有参与者反馈都是 Yes,那就进行事务提交:
1.发送提交请求。
2,事务提交。
参与者接收到Commit请求之后,会正式执行事务提交操作,并在完成提交之后释放资源:
反馈事务提交结果(ack to 协调者)。
协调者完成事务。
阶段二(执行事务滚回)
如果有参与者反馈是NO 或等待超时 那就进行事务滚回:
发送滚回请求。
事务滚回。
参与者接收到Rollback请求之后,通过Undo 滚回事务,并在完成之后释放资源:
反馈事务滚回结果(ack to 协调者)。
协调者中断事务。
2PC的问题
1.同步阻塞问题:执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。也就是说从投票阶段到提交阶段完成这段时间,资源是被锁住的。
2.单点故障:由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
3.数据不一致问题:在 2PC 最后提交阶段中,当协调者向参与者发送 commit 请求之后,发生了局部网络异常或者在发送 commit 请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了 commit 请求。而在这部分参与者接到 commit 请求之后就会执行 commit 操作。但是其他部分未接到 commit 请求的机器则无法执行事务提交,于是整个分布式系统便出现了数据不一致性的现象.
2.单点故障:由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。
3.数据不一致问题:在 2PC 最后提交阶段中,当协调者向参与者发送 commit 请求之后,发生了局部网络异常或者在发送 commit 请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了 commit 请求。而在这部分参与者接到 commit 请求之后就会执行 commit 操作。但是其他部分未接到 commit 请求的机器则无法执行事务提交,于是整个分布式系统便出现了数据不一致性的现象.
3PC
1.CanCommit 阶段(询问阶段):在这个阶段,事务协调者(Transaction Coordinator)向所有参与者(Transaction Participant)发出 CanCommit 请求,询问它们是否准备好提交事务。参与者执行所有必要的操作,并回复协调者它们是否可以提交事务。
2.PreCommit 阶段(准备阶段):如果所有参与者都回复可以提交事务,则协调者将向所有参与者发送PreCommit 请求,通知它们准备提交事务。参与者执行所有必要的操作,并回复协调者它们是否已经准备好提交事务。
3.DoCommit 阶段(提交阶段):如果所有参与者都已经准备好提交事务,则协调者将向所有参与者发送DoCommit 请求,通知它们提交事务。参与者执行所有必要的操作,并将其结果记录在持久性存储中。一旦所有参与者都已提交事务,协调者将向它们发送确认请求。如果任何参与者未能提交事务,则协调者将通知所有参与者回滚事务。
2.PreCommit 阶段(准备阶段):如果所有参与者都回复可以提交事务,则协调者将向所有参与者发送PreCommit 请求,通知它们准备提交事务。参与者执行所有必要的操作,并回复协调者它们是否已经准备好提交事务。
3.DoCommit 阶段(提交阶段):如果所有参与者都已经准备好提交事务,则协调者将向所有参与者发送DoCommit 请求,通知它们提交事务。参与者执行所有必要的操作,并将其结果记录在持久性存储中。一旦所有参与者都已提交事务,协调者将向它们发送确认请求。如果任何参与者未能提交事务,则协调者将通知所有参与者回滚事务。
与 2PC 协议相比,3PC 协议将 CanCommit 阶段(询问阶段)添加到协议中,使参与者能够在 CanCommit 阶段发现并解决可能导致阻塞的问题。这样,3PC 协议能够更快地执行提交或回滚事务,并减少不必要的等待时间。需要注意的是,与 2PC 协议相比,3PC 协议仍然可能存在阻塞的问题。
2PC vs 3PC
2PC 和 3PC 是分布式事务中两种常见的协议,3PC 可以看作是 2PC 协议的改进版本,相比于 2PC 它有两点改进:
1.引入了超时机制,同时在协调者和参与者中都引入超时机制(2PC 只有协调者有超时机制);
2.3PC 相比于 2PC 增加了 CanCommit 阶段,可以尽早的发现问题,从而避免了后续的阻塞和无效操作。
也就是说,3PC 相比于 2PC,因为引入了超时机制,所以发生阻塞的几率变小了;同时 3PC 把之前 2PC 的准备阶段一分为二,变成了两步,这样就多了一个缓冲阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
1.引入了超时机制,同时在协调者和参与者中都引入超时机制(2PC 只有协调者有超时机制);
2.3PC 相比于 2PC 增加了 CanCommit 阶段,可以尽早的发现问题,从而避免了后续的阻塞和无效操作。
也就是说,3PC 相比于 2PC,因为引入了超时机制,所以发生阻塞的几率变小了;同时 3PC 把之前 2PC 的准备阶段一分为二,变成了两步,这样就多了一个缓冲阶段,保证了在最后提交阶段之前各参与节点的状态是一致的。
数据一致性问题和解决方案
Paxos(帕克索斯)算法
柔性事务
允许一定时间内不同节点的数据不一致,但要求最终一致的机制。柔性事物有 TCC 补偿事务、可靠消息事务(MQ 事务)等。
TCC补偿事务
1.Tty
2.Confirm
3.Concel
2.Confirm
3.Concel
补偿机制是一种事务处理模式,用于确保分布式系统中的操作成功完成或在失败时进行补偿。TCC将一个事务拆分为三个阶段,即Try、Confirm和Cancel阶段。在Try阶段,业务系统尝试执行事务并锁定所需资源。如果Try阶段成功,业务系统将进入Confirm阶段并提交事务。如果Try阶段失败或出现其他异常情况,业务系统将回滚事务并释放所有锁定的资源。
TCC 采用了补偿机制,其核心思想:针对每个操作,都要注册一个与其对应确认和补偿(撤销)操作。
TCC是一种尝试性执行,若所有参与结点都有事务执行的条件,那么直接执行事务。否则Cancel回滚操作。
对业务的操作入侵比较大,耦合性高,对于Try和Cancel可能需要重试
需要业务系统保证操作的幂等性,从业务角度的实现方案,因此可以跨数据库、跨业务系统。
TCC 方案让应用可以自定义数据库操作粒度,降低了锁冲突,可以提升性能。
但应用侵入性强,try、confirm、cancel 三个阶段都需要业务逻辑实现。
TCC 采用了补偿机制,其核心思想:针对每个操作,都要注册一个与其对应确认和补偿(撤销)操作。
TCC是一种尝试性执行,若所有参与结点都有事务执行的条件,那么直接执行事务。否则Cancel回滚操作。
对业务的操作入侵比较大,耦合性高,对于Try和Cancel可能需要重试
需要业务系统保证操作的幂等性,从业务角度的实现方案,因此可以跨数据库、跨业务系统。
TCC 方案让应用可以自定义数据库操作粒度,降低了锁冲突,可以提升性能。
但应用侵入性强,try、confirm、cancel 三个阶段都需要业务逻辑实现。
TCC补偿机制的优点在于它提供了更高的可靠性和一致性。通过在事务执行过程中引入确认和取消机制,TCC确保了即使在出现错误或异常情况下,系统状态仍然保持一致。此外,TCC还提供了更好的资源管理和锁定控制,有助于提高系统的性能和效率。
2.可以灵活控制事务的边界:TCC模式通过明确的三个阶段来控制分布式事务的边界,可以灵活地控制事务的粒度和范围,减少事务的锁竞争和冲突。
3.可以提高系统的并发性能:TCC模式可以将事务的过程拆分成多个阶段,每个阶段可以并发执行,从而提高系统的并发性能和吞吐量。
4.可以降低分布式事务的复杂度:TCC模式可以将分布式事务的复杂度降低到可控的范围内,便于管理和维护。
5.可以保证数据的一致性:TCC补偿机制可以保证在分布式事务失败时,可以通过补偿操作将数据恢复到一致的状态,确保数据的正确性和完整性。
缺点:
1.实现复杂度较高:TCC模式相对于其他分布式事务解决方案,实现复杂度较高,需要对业务逻辑进行深入的理解和设计。
2.可能存在性能问题:TCC模式需要进行多次网络通信和状态转换,可能会对系统的性能产生一定的影响,尤其是在高并发场景下。
3.事务边界难以确定:TCC模式需要明确事务的边界和阶段,但在某些场景下,事务的边界难以确定,容易出现事务处理失败的情况。
4.补偿操作复杂性高:TCC补偿机制需要对每个参与者进行相应的补偿操作,补偿操作的复杂性取决于业务场景和实现方式,可能会带来额外的开销和复杂性。
5.然而,TCC也有其局限性。它需要更多的系统资源和处理时间,因为每个事务都需要经过Try、Confirm和Cancel三个阶段。此外,它也需要更复杂的事务管理逻辑和编程模型,增加了开发人员的工作量和难度。
综上所述,TCC补偿机制作为TCC模式中保证分布式事务一致性的关键机制之一,具有其优点和缺点。在选择分布式事务解决方案时,需要根据具体业务需求和场景来选择最合适的方案。
2.可以灵活控制事务的边界:TCC模式通过明确的三个阶段来控制分布式事务的边界,可以灵活地控制事务的粒度和范围,减少事务的锁竞争和冲突。
3.可以提高系统的并发性能:TCC模式可以将事务的过程拆分成多个阶段,每个阶段可以并发执行,从而提高系统的并发性能和吞吐量。
4.可以降低分布式事务的复杂度:TCC模式可以将分布式事务的复杂度降低到可控的范围内,便于管理和维护。
5.可以保证数据的一致性:TCC补偿机制可以保证在分布式事务失败时,可以通过补偿操作将数据恢复到一致的状态,确保数据的正确性和完整性。
缺点:
1.实现复杂度较高:TCC模式相对于其他分布式事务解决方案,实现复杂度较高,需要对业务逻辑进行深入的理解和设计。
2.可能存在性能问题:TCC模式需要进行多次网络通信和状态转换,可能会对系统的性能产生一定的影响,尤其是在高并发场景下。
3.事务边界难以确定:TCC模式需要明确事务的边界和阶段,但在某些场景下,事务的边界难以确定,容易出现事务处理失败的情况。
4.补偿操作复杂性高:TCC补偿机制需要对每个参与者进行相应的补偿操作,补偿操作的复杂性取决于业务场景和实现方式,可能会带来额外的开销和复杂性。
5.然而,TCC也有其局限性。它需要更多的系统资源和处理时间,因为每个事务都需要经过Try、Confirm和Cancel三个阶段。此外,它也需要更复杂的事务管理逻辑和编程模型,增加了开发人员的工作量和难度。
综上所述,TCC补偿机制作为TCC模式中保证分布式事务一致性的关键机制之一,具有其优点和缺点。在选择分布式事务解决方案时,需要根据具体业务需求和场景来选择最合适的方案。
Seata
XA模式
第一阶段:
Seata 实际上就是多了一个 TM 的概念, 也就是事务管理者. TM 一上来就是要去注册全局事务,作为分布式事务的入口,接下来就会去调用各个分支事务. 每个分支事务中又有一个 RM,RM 就回去 TC 里面注册分支事务,然后执行业务 sql,但是执行完后不能提交,只是去报告一下事务的状态. 到此,第一阶段结束.
第二阶段:
TM 这里的入口方法执行完了以后,就要去告诉 TC 说:“我这边执行完了,接下来就看你那边的情况了~”. 随后 TC 就会去检查各个分支事务报告的状态,然后向 RM 发送一个信号——如果第一阶段都执行成功了,那么就提交,反之,则进行回滚. RM 收到信号之后就可以进行 提交/回滚 事务了.
Seata 实际上就是多了一个 TM 的概念, 也就是事务管理者. TM 一上来就是要去注册全局事务,作为分布式事务的入口,接下来就会去调用各个分支事务. 每个分支事务中又有一个 RM,RM 就回去 TC 里面注册分支事务,然后执行业务 sql,但是执行完后不能提交,只是去报告一下事务的状态. 到此,第一阶段结束.
第二阶段:
TM 这里的入口方法执行完了以后,就要去告诉 TC 说:“我这边执行完了,接下来就看你那边的情况了~”. 随后 TC 就会去检查各个分支事务报告的状态,然后向 RM 发送一个信号——如果第一阶段都执行成功了,那么就提交,反之,则进行回滚. RM 收到信号之后就可以进行 提交/回滚 事务了.
优缺点
优点:
1. 支持强一致:执行完业务 sql 以后,不进行提交事务,而是等到 TC 协调完后,在进行 提交/回滚.
2. 实现起来简单:因为数据库本身就支持 XA 模式,Seata 只是在数据库的 XA 模式上做了一层简单的封装. 如果只看核心部分,就没什么差别了.
缺点:
1. 弱可用性,性能差:第一阶段不提交,等待第二阶段才提交,这个过程中会占用数据库锁(相当于对系统资源的一种浪费). 一旦分支事务特别多的情况下,业务耗时就会很久.
2. 依赖数据库底层实现:虽然用起来简单了,但是如果我用的不是 mysql 这种关系型数据库,而是 redis 这种非关系型数据库就不行了.
1. 支持强一致:执行完业务 sql 以后,不进行提交事务,而是等到 TC 协调完后,在进行 提交/回滚.
2. 实现起来简单:因为数据库本身就支持 XA 模式,Seata 只是在数据库的 XA 模式上做了一层简单的封装. 如果只看核心部分,就没什么差别了.
缺点:
1. 弱可用性,性能差:第一阶段不提交,等待第二阶段才提交,这个过程中会占用数据库锁(相当于对系统资源的一种浪费). 一旦分支事务特别多的情况下,业务耗时就会很久.
2. 依赖数据库底层实现:虽然用起来简单了,但是如果我用的不是 mysql 这种关系型数据库,而是 redis 这种非关系型数据库就不行了.
AT模式
第一阶段:
TM 去开启全局事务,作为分布式事务的入口,接着又会调用每一个分支事务 ,然后每个分支事务里的 rm 都会去注册分支事务,并执行本地的业务 sql,然后直接提交(这里就和 XA 不一样了). 因此 AT 模式就不需要像 XA 那样锁定资源,性能上要优于 XA 模式.
这里直接提交,万一有人失败了呢,没办法回滚不就导致状态不一致了吗?
实际上 AT 模式在执行 sql 之前,rm 会给当前数据生成一个快照,这个快照的名字叫 “undo log”,这就像是 redis 中 RDB 持久化时也会生成快照,那么即使服务重启,也可以根据快照恢复数据. 那么这样就即使有分支事务执行 sql 的时候失败了,也可以根据快照恢复如初,大胆提交就对了.
第二阶段:
TM 看到业务结束了,就会去通知 TC,那么 TC 就会判断是提交还是回滚. 如果分支事务的状态都是成功的,那就可以把第一阶段准备的快照给删了(删快照这个动作是异步的,因为第一阶段都成功了,也提交了,后面的事情就可以用一个线程独立去做,提高了效率). 如果第一阶段有人失败了,就要基于 undo log 恢复数据,恢复以后这个 log 也就没用了,最后也会删除.
TM 去开启全局事务,作为分布式事务的入口,接着又会调用每一个分支事务 ,然后每个分支事务里的 rm 都会去注册分支事务,并执行本地的业务 sql,然后直接提交(这里就和 XA 不一样了). 因此 AT 模式就不需要像 XA 那样锁定资源,性能上要优于 XA 模式.
这里直接提交,万一有人失败了呢,没办法回滚不就导致状态不一致了吗?
实际上 AT 模式在执行 sql 之前,rm 会给当前数据生成一个快照,这个快照的名字叫 “undo log”,这就像是 redis 中 RDB 持久化时也会生成快照,那么即使服务重启,也可以根据快照恢复数据. 那么这样就即使有分支事务执行 sql 的时候失败了,也可以根据快照恢复如初,大胆提交就对了.
第二阶段:
TM 看到业务结束了,就会去通知 TC,那么 TC 就会判断是提交还是回滚. 如果分支事务的状态都是成功的,那就可以把第一阶段准备的快照给删了(删快照这个动作是异步的,因为第一阶段都成功了,也提交了,后面的事情就可以用一个线程独立去做,提高了效率). 如果第一阶段有人失败了,就要基于 undo log 恢复数据,恢复以后这个 log 也就没用了,最后也会删除.
优缺点
优点:
1.高性能:第一阶段完成后直接提交,释放资源比较早,数据库的锁定时间短.
2.写隔离:利用全局锁机制,不仅实现了隔离,性能也依旧维持.
3.使用简单,无代码侵入:Seata 框架自动完成回滚和提交.
缺点:
1.软状态:由于第一阶段执行业务 sql 可能有人成功有人失败,并且已经提交,这种情况下,并没有达成一致,只有第二阶段快照恢复才最终一致.
2.影响性能:虽然相比 XA模式,性能要好很多,但是框架的快照功能多多少少都会影响一点性能
1.高性能:第一阶段完成后直接提交,释放资源比较早,数据库的锁定时间短.
2.写隔离:利用全局锁机制,不仅实现了隔离,性能也依旧维持.
3.使用简单,无代码侵入:Seata 框架自动完成回滚和提交.
缺点:
1.软状态:由于第一阶段执行业务 sql 可能有人成功有人失败,并且已经提交,这种情况下,并没有达成一致,只有第二阶段快照恢复才最终一致.
2.影响性能:虽然相比 XA模式,性能要好很多,但是框架的快照功能多多少少都会影响一点性能
XA模式 和 AT模式 的区别?
1. XA模式在第一阶段的时候不会进行事务的提交,而 AT 模式执行完业务 sql 之后会立即提交事务,不会锁定资源,因此性能会好一些.
2. XA 依赖于数据库的机制来做回滚,而 AT 模式因为已经提交了,就不能回滚,他是通过给自己生成快照的方式来实现数据的恢复.
3. XA 模式是强一致,而 AT 是最终一致,因为 AT 在业务 sql 执行之后直接提交了,有人失败,有人成功,那么这个时候状态肯定是不一致的,也就是一个软状态,只有在第二阶段,基于快照恢复了数据,才能到达一个最终一致的效果.
2. XA 依赖于数据库的机制来做回滚,而 AT 模式因为已经提交了,就不能回滚,他是通过给自己生成快照的方式来实现数据的恢复.
3. XA 模式是强一致,而 AT 是最终一致,因为 AT 在业务 sql 执行之后直接提交了,有人失败,有人成功,那么这个时候状态肯定是不一致的,也就是一个软状态,只有在第二阶段,基于快照恢复了数据,才能到达一个最终一致的效果.
AT 模式脏写问题
AT 模式在第一阶段执行完业务 sql 以后会直接提交,那么资源锁定的周期就比较短,效率高,但也正因为他提前释放了锁,就导致在高并发的场景下,会出现安全问题~
例如我这里有一个用户余额表,里面记录了 money 字段,表示余额.
现在有一个线程开启了事务1,首先他会去获取数据库锁,接着生成快照(money = 100),然后执行业务 sql(把 money 值改成 90),最后提交事务,锁也就释放了.
此时,另一个线程开启了 事务2 ,拿到锁以后保存快照(money = 90),执行业务 sql(把 money 值改成 80),最后提交释放锁.
到了第二阶段,假如说 事务1 要进行回滚(通过快照恢复数据),但是 事务1 的快照数据 money = 100!这个时候就出问题了,恢复了以后相当于 事务2 这哥们啥也没做. 这就是所谓的脏写问题.
例如我这里有一个用户余额表,里面记录了 money 字段,表示余额.
现在有一个线程开启了事务1,首先他会去获取数据库锁,接着生成快照(money = 100),然后执行业务 sql(把 money 值改成 90),最后提交事务,锁也就释放了.
此时,另一个线程开启了 事务2 ,拿到锁以后保存快照(money = 90),执行业务 sql(把 money 值改成 80),最后提交释放锁.
到了第二阶段,假如说 事务1 要进行回滚(通过快照恢复数据),但是 事务1 的快照数据 money = 100!这个时候就出问题了,恢复了以后相当于 事务2 这哥们啥也没做. 这就是所谓的脏写问题.
解决办法: 写隔离
上述过程出现的问题,归根结底还是隔离性的原因,如果第一阶段和第二阶段整体都是一个锁定状态,别人根本无法插入进来,因此 AT 模式就引入了一个东西叫 “全局锁”.
全局锁由 TC 来控制,用来记录当前哪个事务在操作哪种表的哪一行数据. 这张表了主要有 事务id、表名字、pk主键(记录这张表的哪一行数据).
有了全局锁,那么在刚刚上述栗子的 事务1中,执行完业务 sql 就会去获取全局锁,就表明这张表的这行数据是我 事务1 才能拥有的,此时就可以大胆的提交事务.
当 事务2 执行完业务 sql 之后,也尝试去获取全局锁,但是由于 事务1 已经获取了,因此 事务2 就只能阻塞等待 事务1 释放锁. 事务1 想要释放全局锁得等到第二阶段结束,但是现在 事务2 又持有 DB 锁,事务1 也拿不到 DB 锁,因此事务就互相等待,直到 事务2 等待超时,就会进行回滚并释放 DB 锁.
全局锁由 TC 来控制,用来记录当前哪个事务在操作哪种表的哪一行数据. 这张表了主要有 事务id、表名字、pk主键(记录这张表的哪一行数据).
有了全局锁,那么在刚刚上述栗子的 事务1中,执行完业务 sql 就会去获取全局锁,就表明这张表的这行数据是我 事务1 才能拥有的,此时就可以大胆的提交事务.
当 事务2 执行完业务 sql 之后,也尝试去获取全局锁,但是由于 事务1 已经获取了,因此 事务2 就只能阻塞等待 事务1 释放锁. 事务1 想要释放全局锁得等到第二阶段结束,但是现在 事务2 又持有 DB 锁,事务1 也拿不到 DB 锁,因此事务就互相等待,直到 事务2 等待超时,就会进行回滚并释放 DB 锁.
有人可能就会说,那这不跟 XA 模式一样了?也锁定资源,这样别人也无法访问,效率不久下降了么?
这里实际上是有一个 锁粒度 上的差别. XA 模式中是数据库锁不释放,意味着任何人都不能访问你这数据库的数据,而 AT 中的全局锁只是不能任何人操作这个表中的某一行数据,也就是说,同样是 account 表,你可以修改这张表其他所有不是余额的字段. 因此效率上还是高很多的.
这里实际上是有一个 锁粒度 上的差别. XA 模式中是数据库锁不释放,意味着任何人都不能访问你这数据库的数据,而 AT 中的全局锁只是不能任何人操作这个表中的某一行数据,也就是说,同样是 account 表,你可以修改这张表其他所有不是余额的字段. 因此效率上还是高很多的.
有人可能就会说,隔离不彻底啊,可能有一种极端情况,就是在修改 money 的过程中,有一个其他 非Seata 管理的事务 也来修改 money 字段,这时候人家又不用获取全局锁,也可能出现脏写的问题
确实有可能,但是可能性非常低,有以下两个原因:
1.全局事务大多数情况下都是成功的,比如我这边转账,大多数情况下我不能明知道钱不够还去转超额的钱吧,那么第二阶段几乎就不会回滚.
2.分布式事务耗时本来就长,或者说并发本来就比较低,像这种见缝插针的事务就更少见了.
但即使概论低,Seata 也还是考虑这种情况的. Seata 在管理的事务的时候,是保存了两个快照的,第一个是更新前的快照(money = 100),另一个是更新后的快照(money = 90). 在 Seata 管理的 事务1 执行到第二阶段的时候,他就会去对比数据库中的字段值 和 after-image(更新后的快照)是否一致,如果不一致,就知道其中有人动了手脚,此时他可能就会发一个短信、邮件、电话告诉你,需要人工介入了.
确实有可能,但是可能性非常低,有以下两个原因:
1.全局事务大多数情况下都是成功的,比如我这边转账,大多数情况下我不能明知道钱不够还去转超额的钱吧,那么第二阶段几乎就不会回滚.
2.分布式事务耗时本来就长,或者说并发本来就比较低,像这种见缝插针的事务就更少见了.
但即使概论低,Seata 也还是考虑这种情况的. Seata 在管理的事务的时候,是保存了两个快照的,第一个是更新前的快照(money = 100),另一个是更新后的快照(money = 90). 在 Seata 管理的 事务1 执行到第二阶段的时候,他就会去对比数据库中的字段值 和 after-image(更新后的快照)是否一致,如果不一致,就知道其中有人动了手脚,此时他可能就会发一个短信、邮件、电话告诉你,需要人工介入了.
一致性协议
NWR协议
NWR协议介绍
NWR协议是一种分布式存储系统中,用于控制一致性级别的一种策略。亚马逊的云存储中,就是用NWR来控制一致性。
N :在分布式存储系统中标识有多少份备份数据。可以理解为节点
W:代表一次成功的更新操作至少成功写入的数据份数
R:代表一次成功读取数据,至少要求读取的数份数
NWR协议是一种分布式存储系统中,用于控制一致性级别的一种策略。亚马逊的云存储中,就是用NWR来控制一致性。
N :在分布式存储系统中标识有多少份备份数据。可以理解为节点
W:代表一次成功的更新操作至少成功写入的数据份数
R:代表一次成功读取数据,至少要求读取的数份数
原理
NWR值的不同组合可以达到不同的一致性效果。
当W+R>N 系统数据一致性级别达到强一致性,也就是说一次成功读取数据一定会读取到最新的数据节点。因为R>N-W,N-W代表未写入更新的节点数量。
当 W+R ≤ N 系统无法达到强一致性,因为R无法完全覆盖未更新数据的节点。
NWR值的不同组合可以达到不同的一致性效果。
当W+R>N 系统数据一致性级别达到强一致性,也就是说一次成功读取数据一定会读取到最新的数据节点。因为R>N-W,N-W代表未写入更新的节点数量。
当 W+R ≤ N 系统无法达到强一致性,因为R无法完全覆盖未更新数据的节点。
Gossip协议
Gossip ( /ˈɡɒsɪp/ 流言) 协议也叫Epidemic协议(流行病协议)。原本用于分布式数据库中节点同步数据使用,后面又被用于数据库复制、信息扩散、集群成员身份确认、故障探测等。
Gossip 协议利用一种随机的方式将信息传播到整个网络中,并在一段时间内使得系统内所有节点数据一致。Gossip其实是一种去中心化思路的分布式协议,解决状态在集群中传播和状态一致性的保证两个问题。
Gossip 协议利用一种随机的方式将信息传播到整个网络中,并在一段时间内使得系统内所有节点数据一致。Gossip其实是一种去中心化思路的分布式协议,解决状态在集群中传播和状态一致性的保证两个问题。
原理
Gossip协议的消息传播方式有两种
1.反熵传播 以固定的概率传播所有的节点,所有参与的节点只有两种状态 Suspective(病原体)和Infective(感染)。过程是种子节点会把所有的数据都跟其他2.节点共享,以便消除节点之间数据的任何不一致,可以保证最终、完全的一致。缺点是消息数量非常庞大,且无限制;一般用于新加入节点的数据初始化!
谣言传播 以固定概率传播新到达的数据,所有参与节点有三种状态:Suspective(病原体)、Infective(感染)、Removed(愈除)。过程消息只包含最新update,谣言消息在某个时间点之后会被标记为removed,并且不再被传播。缺点是系统会有一定的概率不一致,通常用于节点间数据增量同步。
Gossip协议的消息传播方式有两种
1.反熵传播 以固定的概率传播所有的节点,所有参与的节点只有两种状态 Suspective(病原体)和Infective(感染)。过程是种子节点会把所有的数据都跟其他2.节点共享,以便消除节点之间数据的任何不一致,可以保证最终、完全的一致。缺点是消息数量非常庞大,且无限制;一般用于新加入节点的数据初始化!
谣言传播 以固定概率传播新到达的数据,所有参与节点有三种状态:Suspective(病原体)、Infective(感染)、Removed(愈除)。过程消息只包含最新update,谣言消息在某个时间点之后会被标记为removed,并且不再被传播。缺点是系统会有一定的概率不一致,通常用于节点间数据增量同步。
Gossip协议通信方式
Gossip协议最终目的是将数据分发到网络中的没一个节点,根据不同的具体应用场景,网络中两个节点间存在三种通信方式:推送模式、拉取模式、推/拉模式
Gossip协议最终目的是将数据分发到网络中的没一个节点,根据不同的具体应用场景,网络中两个节点间存在三种通信方式:推送模式、拉取模式、推/拉模式
Gossip协议优缺点
优点:
拓展性:允许节点的任意增加和减少,新增节点的状态最终会与其他节点一致。
容错性:任意节点的宕机和重启都不会影响Gossip消息的传播,具有天然的分布式系统容错性特性。
最终一致性:Gossip协议实现消息指数级的快速传播,因此在有新消息需要传播时,消息可以快速的传播到全局节点。
缺点:
消息延迟:节点随机向少数几个节点发送消息,消息最终通过多个轮次的散播而达到全网,不可避免的造成消息延迟。
消息冗余:节点定期随机选择周围节点发送消息,收到的消息节点也会重复改步骤,不可避免的引起同意节点消息多次接收,增大消息压力。
优点:
拓展性:允许节点的任意增加和减少,新增节点的状态最终会与其他节点一致。
容错性:任意节点的宕机和重启都不会影响Gossip消息的传播,具有天然的分布式系统容错性特性。
最终一致性:Gossip协议实现消息指数级的快速传播,因此在有新消息需要传播时,消息可以快速的传播到全局节点。
缺点:
消息延迟:节点随机向少数几个节点发送消息,消息最终通过多个轮次的散播而达到全网,不可避免的造成消息延迟。
消息冗余:节点定期随机选择周围节点发送消息,收到的消息节点也会重复改步骤,不可避免的引起同意节点消息多次接收,增大消息压力。
场景 适合AP(可用性、分区一致性)场景的数据一致性处理,例如 p2p网路通信,Redis Cluster 、Consul
Paxos协议
Paxos算法是基于消息传递且具有高度容错性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一。
大型分布式系统都采用了Paxos算法来解决分布式一致性问题,比如Chubby、Megastore、Spanner、Zookeeper、Mysql5.7的Group Replication
大型分布式系统都采用了Paxos算法来解决分布式一致性问题,比如Chubby、Megastore、Spanner、Zookeeper、Mysql5.7的Group Replication
Paxos的版本有:Basic Paxos、Multi Paxos、Fast-Paxos、ZAB 具体落地有Raft和zk的ZAB协议。zab是Paxos的工业实现。
其他的分布式一致性协议还有Raft协议
其他的分布式一致性协议还有Raft协议
Basic Paxos介绍
角色介绍
Client 客户端 :客户端向分布式系统发出请求,并且等待响应。例如对分布式文件服务器文件的更新请求
Proposer 提议者:提案者提倡客户端请求,尝试说服Acceptor达成一致,并在发生冲突时充当协调者以推动协议向前发展。
Acceptor 决策者:可以批提案,Acceptor可以接受提案,并且进行投票,投票结果是否通过以多数派为准,如果某个提案被选定,那么该提案的value就被选定了。
Learner 学习者,学习者充当该协议的复制因素 不参与投票
流程 basic paxos 流程分为四个步骤
Prepare proposer提出一个提案,编号为N,N大于这个proposer之前提出的所有编号,请求Accpetor的多数人接受这个提案
Promise 如果编号N大于此之前Accpetor之前接受过的所有提案编号就接受,否则就拒绝。
Accept 如果达到多数派,Proposer会发出accept请求,请求包含提案编号和对应的内容
Accepted 如果此Acceptor再次期间没有接受到任何大于N的提案,则接收此提案内容,否则就忽略。
角色介绍
Client 客户端 :客户端向分布式系统发出请求,并且等待响应。例如对分布式文件服务器文件的更新请求
Proposer 提议者:提案者提倡客户端请求,尝试说服Acceptor达成一致,并在发生冲突时充当协调者以推动协议向前发展。
Acceptor 决策者:可以批提案,Acceptor可以接受提案,并且进行投票,投票结果是否通过以多数派为准,如果某个提案被选定,那么该提案的value就被选定了。
Learner 学习者,学习者充当该协议的复制因素 不参与投票
流程 basic paxos 流程分为四个步骤
Prepare proposer提出一个提案,编号为N,N大于这个proposer之前提出的所有编号,请求Accpetor的多数人接受这个提案
Promise 如果编号N大于此之前Accpetor之前接受过的所有提案编号就接受,否则就拒绝。
Accept 如果达到多数派,Proposer会发出accept请求,请求包含提案编号和对应的内容
Accepted 如果此Acceptor再次期间没有接受到任何大于N的提案,则接收此提案内容,否则就忽略。
Basic Paxos存在的不足
流程复杂,实现困难
效率有点低,一轮一次性的达成,需要2轮RPC调用
流程复杂,实现困难
效率有点低,一轮一次性的达成,需要2轮RPC调用
Mulit-Paxos
Multi-Paxos 实施时会将Proposer、Acceptor、Learner合并,统称为服务器,这样到最后只有 客户端和服务器交互
Raft协议
Raft引入主节点,通过竞选确定主节点。节点类型有 Follower(追随者)、Candidate(竞选者)、Leader。
Leader节点会周期性的发送心跳包给 Follower 每个Follower都设置了一个随机的竞选超时时间,一般 150~300ms,这个时间内没有收到Leader的心跳包,就会变成Candidate进入竞选阶段。通过竞选阶段的投票多的就成为Leader
Leader节点会周期性的发送心跳包给 Follower 每个Follower都设置了一个随机的竞选超时时间,一般 150~300ms,这个时间内没有收到Leader的心跳包,就会变成Candidate进入竞选阶段。通过竞选阶段的投票多的就成为Leader
Leader 竞选流程
某个Flower状态节点在election timeout 时间内未收到来自Leader节点的心跳。这种情况可能是服务刚启动,还没有Leader节点,也有可能是Leader节点掉线了。
Flower 节点进入 Candidate 候选状态,此时生成一个新的 TermId 并且先算上自己的一票
Candidate 状态的节点向其他节点发起投票 RequestVote
收到投票请求的其他节点,如果在这个TermId任期号中还没有投票,那么就会将票投给该Candidate节点
当Candidate节点收到的票数超过半数,它就成为了新的Leader节点
称为Leader节点后,会给其他的Follower节点发送心跳包,Follower节点每次收到心跳包,就会重置他们的election timeout 计时。
某个Flower状态节点在election timeout 时间内未收到来自Leader节点的心跳。这种情况可能是服务刚启动,还没有Leader节点,也有可能是Leader节点掉线了。
Flower 节点进入 Candidate 候选状态,此时生成一个新的 TermId 并且先算上自己的一票
Candidate 状态的节点向其他节点发起投票 RequestVote
收到投票请求的其他节点,如果在这个TermId任期号中还没有投票,那么就会将票投给该Candidate节点
当Candidate节点收到的票数超过半数,它就成为了新的Leader节点
称为Leader节点后,会给其他的Follower节点发送心跳包,Follower节点每次收到心跳包,就会重置他们的election timeout 计时。
Leader节点接受请求流程
外部请求都由集群的Leader节点处理
Leader节点每收到一个更改就写入Leader节点的日志
Leader 节点写入日志后并未正式提交数据的修改,因为要提交修改必须要在复制给所有Follower节点之后才可以。
Leader节点将修改复制到Flower节点
Leader节点等待收到大多数Follower节点反馈写入完成
Leader 提交修改,并且通知其他的Follower节点,修改已提交。
Leader 节点返回Client操作结果
这个过程称之为 日志复制
外部请求都由集群的Leader节点处理
Leader节点每收到一个更改就写入Leader节点的日志
Leader 节点写入日志后并未正式提交数据的修改,因为要提交修改必须要在复制给所有Follower节点之后才可以。
Leader节点将修改复制到Flower节点
Leader节点等待收到大多数Follower节点反馈写入完成
Leader 提交修改,并且通知其他的Follower节点,修改已提交。
Leader 节点返回Client操作结果
这个过程称之为 日志复制
多个Candidate竞选
如过多个Follower节点同时超时进入Candidate状态
并且发起投票后收到了相同的票数,那么系统会开始等待超时重新投票
由于每个节点的超时时间不一样,所以出现多个Candidate的概率非常小
如过多个Follower节点同时超时进入Candidate状态
并且发起投票后收到了相同的票数,那么系统会开始等待超时重新投票
由于每个节点的超时时间不一样,所以出现多个Candidate的概率非常小
出现网络分区的一致性流程
1,.出现网络分区后,在多个分区可能都会出现对应的Leader节点
2.如果某个分区的Leader收到了客户端的更新条目,因为他无法将更新复制给大多数的节点,所以修改一致处于未提交状态。
3.如果某个分区的Leader收到了客户端更新条目,并且可以复制给大多数节点,那么修改会被正常提交。
4.如果网络分区消失,任期较小的leader将下台,并且回滚未提交的修改。会匹配随较大任期的Leader的日志数据记录
5.也就是说raft在发生网络分区的时候,是没有办法在半数以下的节点的网络分区中进行更改的
1,.出现网络分区后,在多个分区可能都会出现对应的Leader节点
2.如果某个分区的Leader收到了客户端的更新条目,因为他无法将更新复制给大多数的节点,所以修改一致处于未提交状态。
3.如果某个分区的Leader收到了客户端更新条目,并且可以复制给大多数节点,那么修改会被正常提交。
4.如果网络分区消失,任期较小的leader将下台,并且回滚未提交的修改。会匹配随较大任期的Leader的日志数据记录
5.也就是说raft在发生网络分区的时候,是没有办法在半数以下的节点的网络分区中进行更改的
Lease机制
Lease(租约) 机制是一种在分布式系统常用的协议,是维护分布式系统数据一致性的一种常用工具。
Lease机制有以下几个特点
1.Lease是颁发者对一段时间内数据一致性的承诺
2.颁发者发出Lease后,不管是否被接收,只要Lease租期内,颁发者都会按照协议遵守承诺
3.Lease的持有者只能在Lease有效期内使用承诺,一旦Lease超时,持有者需要放弃执行重新申请Leasedddd
Lease机制有以下几个特点
1.Lease是颁发者对一段时间内数据一致性的承诺
2.颁发者发出Lease后,不管是否被接收,只要Lease租期内,颁发者都会按照协议遵守承诺
3.Lease的持有者只能在Lease有效期内使用承诺,一旦Lease超时,持有者需要放弃执行重新申请Leasedddd
ZAB协议
ZAB(Zookeeper Atomic Broadcast) 协议是为分布式协调服务zookeeper专门设计的一种支持崩溃恢复的原子广播协议。在zookeeper中,主要依赖ZAB协议来实现分布式数据一致性,基于该协议,zookeeper实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。
原子广播
leader接收到消息请求后,将消息赋予一个全局唯一的64位自增id,叫:zxid,通过zxid的大小比较就可以实现因果有序这个特征。
2》leader为每个follower准备了一个FIFO队列(通过TCP协议来实现,以实现全局有序这一个特点)将带有zxid的消息作为一个提案(proposal)分发给所有的 follower。
3》当follower接收到proposal,先把proposal写到磁盘,写入成功以后再向leader回复一个ack。
4》当leader接收到合法数量(超过半数节点)的ack后,leader就会向这些follower发送commit命令,同时会在本地执行该消息。
5》当follower收到消息的commit命令以后,会提交该消息。
2》leader为每个follower准备了一个FIFO队列(通过TCP协议来实现,以实现全局有序这一个特点)将带有zxid的消息作为一个提案(proposal)分发给所有的 follower。
3》当follower接收到proposal,先把proposal写到磁盘,写入成功以后再向leader回复一个ack。
4》当leader接收到合法数量(超过半数节点)的ack后,leader就会向这些follower发送commit命令,同时会在本地执行该消息。
5》当follower收到消息的commit命令以后,会提交该消息。
崩溃恢复
ZAB协议的这个基于原子广播协议的消息广播过程,在正常情况下是没有任何问题的,但是一旦Leader节点崩溃,或者由于网络问题导致Leader服务器失去了过半的Follower节点的联系(leader失去与过半follower节点联系,可能是leader节点和 follower节点之间产生了网络分区,那么此时的leader不再是合法的leader了),那么就会进入到崩溃恢复模式。在ZAB协议中,为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的Leader。
假设1:Leader 在复制数据给所有 Follwer 之后,还没来得及收到Follower的ack返回就崩溃,怎么办?
假设2:Leader 在收到 ack 并提交了自己,同时发送了部分 commit 出去之后崩溃怎么办?
针对这些问题,ZAB 定义了 2 个原则:
ZAB 协议确保丢弃那些只在 Leader 提出/复制,但没有提交的事务。
ZAB 协议确保那些已经在 Leader 提交的事务最终会被所有服务器提交。
能够确保提交已经被 Leader 提交的事务,同时丢弃已经被跳过的事务。
针对这个要求,如果让 Leader 选举算法能够保证新选举出来的 Leader 服务器拥有集群中所有机器 ZXID 最大的事务,那么就能够保证这个新选举出来的 Leader 一定具有所有已经提交的提案。
而且这么做有一个好处是:可以省去 Leader 服务器检查事务的提交和丢弃工作的这一步操作
假设2:Leader 在收到 ack 并提交了自己,同时发送了部分 commit 出去之后崩溃怎么办?
针对这些问题,ZAB 定义了 2 个原则:
ZAB 协议确保丢弃那些只在 Leader 提出/复制,但没有提交的事务。
ZAB 协议确保那些已经在 Leader 提交的事务最终会被所有服务器提交。
能够确保提交已经被 Leader 提交的事务,同时丢弃已经被跳过的事务。
针对这个要求,如果让 Leader 选举算法能够保证新选举出来的 Leader 服务器拥有集群中所有机器 ZXID 最大的事务,那么就能够保证这个新选举出来的 Leader 一定具有所有已经提交的提案。
而且这么做有一个好处是:可以省去 Leader 服务器检查事务的提交和丢弃工作的这一步操作
数据同步
当崩溃恢复之后,需要在正式工作之前(接收客户端请求),Leader 服务器首先确认事务是否都已经被过半的 Follwer 提交了,即是否完成了数据同步。目的是为了保持数据一致。
当 Follwer 服务器成功同步之后,Leader 会将这些服务器加入到可用服务器列表中。
实际上,Leader 服务器处理或丢弃事务都是依赖着 ZXID 的,那么这个 ZXID 如何生成呢?
答:在 ZAB 协议的事务编号 ZXID 设计中,ZXID 是一个 64 位的数字,其中低 32 位可以看作是一个简单的递增的计数器,针对客户端的每一个事务请求,Leader 都会产生一个新的事务 Proposal 并对该计数器进行 + 1 操作。
而高 32 位则代表了 Leader 服务器上取出本地日志中最大事务 Proposal 的 ZXID,并从该 ZXID 中解析出对应的 epoch 值(leader选举周期),当一轮新的选举结束后,会对这个值加一,并且事务id又从0开始自增
高 32 位代表了每代 Leader 的唯一性,低 32 代表了每代 Leader 中事务的唯一性。同时,也能让 Follwer 通过高 32 位识别不同的 Leader。简化了数据恢复流程。
基于这样的策略:当 Follower 连接上 Leader 之后,Leader 服务器会根据自己服务器上最后被提交的 ZXID 和 Follower 上的 ZXID 进行比对,比对结果要么回滚,要么和 Leader 同步。
实际上,Leader 服务器处理或丢弃事务都是依赖着 ZXID 的,那么这个 ZXID 如何生成呢?
答:在 ZAB 协议的事务编号 ZXID 设计中,ZXID 是一个 64 位的数字,其中低 32 位可以看作是一个简单的递增的计数器,针对客户端的每一个事务请求,Leader 都会产生一个新的事务 Proposal 并对该计数器进行 + 1 操作。
而高 32 位则代表了 Leader 服务器上取出本地日志中最大事务 Proposal 的 ZXID,并从该 ZXID 中解析出对应的 epoch 值(leader选举周期),当一轮新的选举结束后,会对这个值加一,并且事务id又从0开始自增
高 32 位代表了每代 Leader 的唯一性,低 32 代表了每代 Leader 中事务的唯一性。同时,也能让 Follwer 通过高 32 位识别不同的 Leader。简化了数据恢复流程。
基于这样的策略:当 Follower 连接上 Leader 之后,Leader 服务器会根据自己服务器上最后被提交的 ZXID 和 Follower 上的 ZXID 进行比对,比对结果要么回滚,要么和 Leader 同步。
共识协议
POW共识机制
比特币采用的 POW 工作量证明共识机制,在生成区块时,系统让所有节点 公平地去计算一个随机数,最先寻找到随机数的节点即是这个区块的生产者,并 获得相应的区块奖励。由于哈希函数是散列函数,求解随机数的唯一方法在数学 上只能是穷举,随机性非常好,每个人都可以参与协议的执行。由于梅克尔树根的设置,哈希函数的解的验证过程也能迅速实现。因此,比特币的 POW 共识机 制门槛很低,无需中心化权威的许可,人人都可以参与,并且每一个参与者都无 需身份认证。
同时,中本聪通过工作量证明的机制破解了无门槛分布式系统的“女巫攻击” 问题。对系统发起攻击需要掌握超过 50%的算力,系统的安全保障较强。
POW 共识的优点可归纳为:
◼ 算法简单,容易实现,节点可自由进入,去中心化程度高。
◼ 破坏系统需要投入极大的成本,安全性极高。
◼ 区块生产者的选择通过节点求解哈希函数实现,提案的产生、验证到共识的最终达成过程是一个纯数学问题,节点间无需交换额外的信息即可达成共识,整个过程不需要人性的参与。
POW 共识算法也存在一些问题:
◼ 为了保证去中心化程度,区块的确认时间难以缩短。
◼ 没有最终性,需要检查点机制来弥补最终性,但随着确认次数的增加,达成共识的可能性也呈指数级地增长。 由于这两个方面的问题,一笔交易为了确保安全,要在 6 个新的区块产生后才能在全网得到确认,也就是说一个交易的确认延迟时间大概为 1 小时,这无法 满足现实世界中对交易实时性要求很高的应用场景。
另一方面,POW 共识算法带来了硬件设备的大量浪费。随着比特币价值的 增长,比特币算力竞赛经历了从 CPU 到 GPU,再到 ASIC 专用芯片的阶段。算力强大的 ASIC 芯片矿机将挖矿算法硬件化,而 ASIC 芯片矿机在淘汰后,没有 其他的用途,造成了大量的硬件浪费。
同时,中本聪通过工作量证明的机制破解了无门槛分布式系统的“女巫攻击” 问题。对系统发起攻击需要掌握超过 50%的算力,系统的安全保障较强。
POW 共识的优点可归纳为:
◼ 算法简单,容易实现,节点可自由进入,去中心化程度高。
◼ 破坏系统需要投入极大的成本,安全性极高。
◼ 区块生产者的选择通过节点求解哈希函数实现,提案的产生、验证到共识的最终达成过程是一个纯数学问题,节点间无需交换额外的信息即可达成共识,整个过程不需要人性的参与。
POW 共识算法也存在一些问题:
◼ 为了保证去中心化程度,区块的确认时间难以缩短。
◼ 没有最终性,需要检查点机制来弥补最终性,但随着确认次数的增加,达成共识的可能性也呈指数级地增长。 由于这两个方面的问题,一笔交易为了确保安全,要在 6 个新的区块产生后才能在全网得到确认,也就是说一个交易的确认延迟时间大概为 1 小时,这无法 满足现实世界中对交易实时性要求很高的应用场景。
另一方面,POW 共识算法带来了硬件设备的大量浪费。随着比特币价值的 增长,比特币算力竞赛经历了从 CPU 到 GPU,再到 ASIC 专用芯片的阶段。算力强大的 ASIC 芯片矿机将挖矿算法硬件化,而 ASIC 芯片矿机在淘汰后,没有 其他的用途,造成了大量的硬件浪费。
POS共识机制
POS(Proof of Stake)共识机制,是一种由系统权益代替算力决定区块记 账权的共识机制,拥有的权益越大则成为下一个区块生产者的概率也越大。POS 的合理假设是权益的所有者更乐于维护系统的一致性和安全性。如果说 POW 把 系统的安全性交给了数学和算力,那么 POS 共识机制把系统的安全性交给了人 性。人性问题,可以用博弈论来研究,POS 共识机制的关键在于构建适当的博弈 模型相应的验证算法,以保证系统的一致性和公平性。
POS 共识机制没有像 POW 那样耗费能源和硬件设备,缩短了区块的产生 时间和确认时间,提高了系统效率。但存在的缺点也有很多,包括:
◼ 实现规则复杂,中间步骤多,参杂了很多人为因素,容易产生安全漏洞。
◼ 与 POW 共识机制一样没有最终性,需要检查点机制来弥补最终性。
纯 POS 共识机制由节点所持权益(持有数量乘以持有时间)决定区块生产 者,权益比例越高,被选为区块生产者的概率也越大,区块生产者选举过程中没 有挖矿。这种机制的践行者有未来币(NXT)和量子链(QTUM)等。
纯 POS 共识机制没有引入外部资源,仅仅依靠自身的权益来维护网络安全, 因此其不需要消耗能源来进行计算;而且由于其没有引入外部的资源,因此不会 担心外部攻击,例如外界的算力攻击。但是,这种 POS 共识依然存在很多问题:
◼ 无利害关系攻击(Nothing-at-Stake attack)
基于权益的挖矿不需要像 POW 共识一样投入物理算力和能源的消耗,只需 要持有权益。假设系统中出现了两个分支链,那么对于持有币的“挖矿者”来讲, 矿工的最佳的操作策略就是同时在两个分支上进行“挖矿”,这样无论哪个分支 胜出,对币种持有者来讲,都会获得本属于他的利益,而不会有利益损失。 这导致的问题是,只要系统存在分叉,“矿工们”都会同时在这几个分支上 挖矿;因此在某种情况下,发起攻击的分叉链是极有可能成功的,因为所有人也 都在这个分叉链上达成了共识;而且甚至不用持有 51%的权益,就可以成功发 起分叉攻击。
◼ 马太效应
POS 共识机制下的权益累计由持币数量乘以持币时间得到,它势必形成赢 家通吃的局面。假设电力成本均为 3 币,大户持有 100 币天获得 100 利息币, 小户持有 1 币天,获得 1 利息币。这样大户会倾向于开机获得更多的币天,而 小户倾向于关机,(97,0)是最终博弈的选择。如此,大户获得的币越来越多, 造成富者愈富,贫者愈贫的局面。
POS 共识机制没有像 POW 那样耗费能源和硬件设备,缩短了区块的产生 时间和确认时间,提高了系统效率。但存在的缺点也有很多,包括:
◼ 实现规则复杂,中间步骤多,参杂了很多人为因素,容易产生安全漏洞。
◼ 与 POW 共识机制一样没有最终性,需要检查点机制来弥补最终性。
纯 POS 共识机制由节点所持权益(持有数量乘以持有时间)决定区块生产 者,权益比例越高,被选为区块生产者的概率也越大,区块生产者选举过程中没 有挖矿。这种机制的践行者有未来币(NXT)和量子链(QTUM)等。
纯 POS 共识机制没有引入外部资源,仅仅依靠自身的权益来维护网络安全, 因此其不需要消耗能源来进行计算;而且由于其没有引入外部的资源,因此不会 担心外部攻击,例如外界的算力攻击。但是,这种 POS 共识依然存在很多问题:
◼ 无利害关系攻击(Nothing-at-Stake attack)
基于权益的挖矿不需要像 POW 共识一样投入物理算力和能源的消耗,只需 要持有权益。假设系统中出现了两个分支链,那么对于持有币的“挖矿者”来讲, 矿工的最佳的操作策略就是同时在两个分支上进行“挖矿”,这样无论哪个分支 胜出,对币种持有者来讲,都会获得本属于他的利益,而不会有利益损失。 这导致的问题是,只要系统存在分叉,“矿工们”都会同时在这几个分支上 挖矿;因此在某种情况下,发起攻击的分叉链是极有可能成功的,因为所有人也 都在这个分叉链上达成了共识;而且甚至不用持有 51%的权益,就可以成功发 起分叉攻击。
◼ 马太效应
POS 共识机制下的权益累计由持币数量乘以持币时间得到,它势必形成赢 家通吃的局面。假设电力成本均为 3 币,大户持有 100 币天获得 100 利息币, 小户持有 1 币天,获得 1 利息币。这样大户会倾向于开机获得更多的币天,而 小户倾向于关机,(97,0)是最终博弈的选择。如此,大户获得的币越来越多, 造成富者愈富,贫者愈贫的局面。
DPOS共识机制
DPOS(Delegated Proof of Share),代理权益证明共识机制,是一种 基于投票选举的共识算法,类似代议制民主。在 POS 的基础上,DPOS 将区块 生产者的角色专业化,先通过权益来选出区块生产者,然后区块生产者之间再轮 流出块。
DPOS 共识由 BitShares(比特股)社区首先提出,它与 POS 共识的主要 区别在于节点选举若干代理人,由代理人验证和记账。DPOS 相比 POS 能大幅 度提升了选举效率,在牺牲一部分去中心化特性的情况下得到性能的提升。
DPOS 共识机制不需要挖矿,也不需要全节点验证,而是由有限数量的见 证节点进行验证,因此是简单、高效的。由于验证节点数量有限,DPOS 共识被 普遍质疑过于中心化,代理记账节点的选举过程中也存在巨大的人为操作空间。
DPOS 作为 POS 的变形,通过缩小选举节点的数量以减少网络压力,是一 种典型的分治策略:将所有节点分为领导者与跟随者,只有领导者之间达成共识 后才会通知跟随者。该机制能够在不增加计算资源的前提下有效减少网络压力, 在商业环境的实现中将会具有较强的应用价值。
DPOS 为了实现更高的效率而设置的代理人制度,背离了区块链世界里人 人可参与的基本精神,也是 EOS 一直被质疑的地方。
DPOS 共识由 BitShares(比特股)社区首先提出,它与 POS 共识的主要 区别在于节点选举若干代理人,由代理人验证和记账。DPOS 相比 POS 能大幅 度提升了选举效率,在牺牲一部分去中心化特性的情况下得到性能的提升。
DPOS 共识机制不需要挖矿,也不需要全节点验证,而是由有限数量的见 证节点进行验证,因此是简单、高效的。由于验证节点数量有限,DPOS 共识被 普遍质疑过于中心化,代理记账节点的选举过程中也存在巨大的人为操作空间。
DPOS 作为 POS 的变形,通过缩小选举节点的数量以减少网络压力,是一 种典型的分治策略:将所有节点分为领导者与跟随者,只有领导者之间达成共识 后才会通知跟随者。该机制能够在不增加计算资源的前提下有效减少网络压力, 在商业环境的实现中将会具有较强的应用价值。
DPOS 为了实现更高的效率而设置的代理人制度,背离了区块链世界里人 人可参与的基本精神,也是 EOS 一直被质疑的地方。
BFT共识机制
最常用的BFT共识机制是实用拜占庭容错算法PBFT(Practical Byzantine Fault Tolerance)。该算法是Miguel Castro和Barbara Liskov在1999年提出 来的,解决了原始拜占庭容错算法效率不高的问题,将算法复杂度由节点数的指 数级降低到节点数的平方级,使得拜占庭容错算法在实际系统应用中变得可行。
PBFT是针对状态机副本复制为主的分布式系统执行环境开发的算法,旨在 让系统中大部分的诚实节点来覆盖恶意节点或无效节点的行为。PBFT算法的节 点数量是固定的,节点身份提前确定,无法动态添加或删除,只能适用于节点数 目固定的联盟链或私有链场景中。
PBFT算法存在的问题:
◼ 计算效率依赖于参与协议的节点数量,不适用于节点数量过大的区块链
系统,扩展性差。
◼ 系统节点是固定的,无法应对公有链的开放环境,只适用于联盟链或私
有链环境。
◼ PBFT算法要求总节点数n>=3f+1(其中,f代表作恶节点数)。系统的失效节点数量不得超过全网节点的1/3,容错率相对较低。
PBFT是针对状态机副本复制为主的分布式系统执行环境开发的算法,旨在 让系统中大部分的诚实节点来覆盖恶意节点或无效节点的行为。PBFT算法的节 点数量是固定的,节点身份提前确定,无法动态添加或删除,只能适用于节点数 目固定的联盟链或私有链场景中。
PBFT算法存在的问题:
◼ 计算效率依赖于参与协议的节点数量,不适用于节点数量过大的区块链
系统,扩展性差。
◼ 系统节点是固定的,无法应对公有链的开放环境,只适用于联盟链或私
有链环境。
◼ PBFT算法要求总节点数n>=3f+1(其中,f代表作恶节点数)。系统的失效节点数量不得超过全网节点的1/3,容错率相对较低。
POW共识机制的回归
比特币是解决了拜占庭将军问题的分布式账本,在完全开放的环境中,实现 了数据的一致性和安全性。但比特币采用的POW 共识机制被广泛质疑为:
◼ 消耗大量能源和硬件设备;
◼ 区块同步时间长,扩展性弱,TPS 低。 于是效率更高、被认为更加节能环保的 POS、DPOS、BFT 等共识机制相继 问世,并得到广泛的应用。各种共识机制的特点:
◼ 在 POS 共识机制下,全网节点根据权益大小按照某种规则参与区块生 产者的选举,共识过程中节点系统开放。但选举过程效率低下,同时由 于选举过程复杂,伴随着许多安全问题。
◼ DPOS 共识通过代理人制度,大幅度提升了 POS 共识的选举效率。但 在共识过程中,节点系统是封闭的,而且去中心化程度低
◼ BFT 类的共识机制性能较高并具备良好的最终性,但其容错率低,且由 于节点的扩展性问题,更加适用于相对封闭的节点系统。
◼ 消耗大量能源和硬件设备;
◼ 区块同步时间长,扩展性弱,TPS 低。 于是效率更高、被认为更加节能环保的 POS、DPOS、BFT 等共识机制相继 问世,并得到广泛的应用。各种共识机制的特点:
◼ 在 POS 共识机制下,全网节点根据权益大小按照某种规则参与区块生 产者的选举,共识过程中节点系统开放。但选举过程效率低下,同时由 于选举过程复杂,伴随着许多安全问题。
◼ DPOS 共识通过代理人制度,大幅度提升了 POS 共识的选举效率。但 在共识过程中,节点系统是封闭的,而且去中心化程度低
◼ BFT 类的共识机制性能较高并具备良好的最终性,但其容错率低,且由 于节点的扩展性问题,更加适用于相对封闭的节点系统。
实战场景
秒杀系统怎么设计
1.设置单独服务 防止对其他服务造成影响(防止秒杀服务不可用而导致其他服务不可用)
2.缓存预热 (可以通过定时任务进行秒杀商品的缓存预热)
3.防止超卖 锁定库存 可以同个redis的redission的semaphore
4.加令牌token 防止有人提前通过链接进行抢购请求 可以到活动开始时间生成有效令牌 携带令牌的才能参加抢购
5.非法过滤 过滤恶意请求 防止非人为操作
6.做好熔断降级 比如hystrix 或者 sentinel
7.异步处理 抢购成功的 通过MQ异步生成订单
8.限流 前端可以通过 加入购物车操作进行缓冲 抢购按钮 点击之后2秒内不能点击之类的
遇到的坑
redis
sync遇到bgsave cpu飙升 bgsave之后做了一个EmpetyDB 这个时候bgsave的cow机制就没了 会重新加载整个rdb 然后swap
高并发场景下 无限同步 tps过高的时候 master复制缓存挤压区的时候 有个参数client-output-buf 大量请求会让值升高超出阈值 断开 重连
slave做RDB同步到的时候会导致tps过高 无法加载 阻塞久了 复制缓冲区的数据就被冲掉的 是个队列会冲掉之前的数据 slave重连 对比ofset 发现空挡 重新sync 预估体量参数动态调整
canal 并发修改
es自动机构建
redis es 深分页
JVM
cpu 飙升
cpu 100%
排查
1.定位耗费cpu的进程
top -c 显示进程列表,然后输入p,按照cpu使用率排序.会显示相关进程号,比如进程号为 43987
2.定位耗费cpu的线程
Top -Hp 43879,然后输入p,按照cpu使用率排序.会看到对应的线程号 等等 比如线程号为 16872
3.定位哪段代码耗费cpu过高
print “%x\n” 16872 把线程的pid转成16进制,比如 41e8
jstack 43987 | grep ‘0x41e8’ -C5 -color
这个就是用jstack打印进程堆栈信息,而且通过grep那个线程的16进制的pid,找到那个线程的相关东西,这个时候就可以在打印出的代码里,看到是哪个类的哪个方法导致cpu 100%的问题
top -c 显示进程列表,然后输入p,按照cpu使用率排序.会显示相关进程号,比如进程号为 43987
2.定位耗费cpu的线程
Top -Hp 43879,然后输入p,按照cpu使用率排序.会看到对应的线程号 等等 比如线程号为 16872
3.定位哪段代码耗费cpu过高
print “%x\n” 16872 把线程的pid转成16进制,比如 41e8
jstack 43987 | grep ‘0x41e8’ -C5 -color
这个就是用jstack打印进程堆栈信息,而且通过grep那个线程的16进制的pid,找到那个线程的相关东西,这个时候就可以在打印出的代码里,看到是哪个类的哪个方法导致cpu 100%的问题
full gc 频繁
druid造成的full gc
查看线上服务发现基本上都是获取数据库连接超时,而且影响时间只有3~4秒钟,服务又恢复了正常。隔了几分钟之后,又出现了大量的告警,还是影响3~4秒后又恢复正常
原本服务是有设置jvm监控告警的,理论上来说当内存使用率达到一定值时会有告警通知,但是由于一次服务迁移导致告警配置失效,没有提前发现问题。
1.大量请求延时,影响业务
2.考虑是不是上游服务调用过多 以及有一些复杂的sql查询 导致数据库连接不够用
分析了接口调用情况后发现异常前后的请求并没有明显的变化,排除突发流量造成的影响
3.考虑DB本身问题
查询DB情况,负载良好,无慢查询,排除DB造成的影响
4.考虑容器或JVM的影响?
排除了DB的影响之后,再往上排查容器的影响 我们再次回过头看异常告警,发现在每一波告警的时间段内,基本上都是同一个容器IP所产生,这个时候基本上已经有80%的概率是GC的问题了。 查询告警时间段内的容器CPU负载正常。再看JVM的内存和GC情况,发现整个内存使用曲线是像下面这样: 老年代内存占用大小呈线性增长
可以发现内存中存在长时间被引用,无法被YongGC所回收的对象,并且对象大小一直在增长。直到Old Gen被堆满之后触发Full GC后对象才会回收。
临时措施
现在问题已经找到了,到目前为止只是3台实例触发了FullGC,但是在查看其它实例内存使用情况时,发现基本上所有的实例Old Gen都快到达临界点了。所以临时解决方案是保留一台实例现场,滚动重启其它所有的实例,避免大量的实例同时进行FullGC。否则很可能导致服务雪崩
5.确定什么对象没有回收并解决
jmap -histo:live pid
jmap -dump:format=b,file=filename pid 内存快照
使用mat工具,很容易发现 class com.mysql.cj.jdbc.AbandonedConnectionCleanupThread 这个类占用了80%以上的内存
看类名就知道,应该是MySQL Driver中用来清理过期连接的一个线程,里面维护了一个Set
private static final Set<ConnectionFinalizerPhantomReference> connectionFinalizerPhantomRefs = ConcurrentHashMap.newKeySet();
对应我们上面看到的内存占用率排第二的HashMap$Node,基本上可以确定大概率是这里存在内存泄露了
在MAT上使用list_object确认一发
那么它里面存的是啥东西呢? 为什么一直增长且无法被YoungGC回收?看名字
ConnectionFinalizerPhantomReference 我们可以猜到它里面保存的应该是数据库连接的phantom引用
ConnectionFinalizerPhantomReference 我们可以猜到它里面保存的应该是数据库连接的phantom引用
根据源码知道果然是PhantomReference,里面存放的是创建的MySQL连接,看一下是在哪里被放进来的:
可以看到,每次创建一个新的数据库连接时,都会将创建的连接包装成PhantomReference后放入
connectionFinalizerPhantomRefs中,然后这个清理线程会在一个无限循环中,获取referenceQueue中的连接并关闭。
connectionFinalizerPhantomRefs中,然后这个清理线程会在一个无限循环中,获取referenceQueue中的连接并关闭。
只有在 connection对象 没有其它的引用,仅存在phantom reference时,才能够被GC,并且放入referenceQueue中
为什么Connection会无限增长?
现在问题找到了,数据库连接被创建之后,则会放入
connectionFinalizerPhantomRefs中,但是由于某种原因,连接前期正常使用,经过了多次minor GC都没有被回收,晋升到了老年代。但是一段时间过后,由于某种原因连接失效,导致连接池又新建了连接。
connectionFinalizerPhantomRefs中,但是由于某种原因,连接前期正常使用,经过了多次minor GC都没有被回收,晋升到了老年代。但是一段时间过后,由于某种原因连接失效,导致连接池又新建了连接。
druid配置了设置了keepAlive,且minEvictableIdleTimeMillis设置的是5分钟,连接初始化之后,在DB请求数没有频繁的波动时,连接池应该都是维护着最小的30个连接,且会在连接空闲时间超过5分钟时进行一次keepAlive操作:
理论上来说,连接池是不会频繁的创建连接的,除非有活跃连接很少,且存在波动,并且keepAlive操作没有生效,在连接池进行keepAlive操作时,MySQL连接就已经失效,那么则会丢弃这个无效连接,下次再重建。
先查看我们的活跃连接数,发现在大部分时候,单实例的数据库的活跃连接数都在3~20个左右波动,并且业务上还存在定时任务,每隔30分钟~1个小时会有大量的DB请求。 Druid既然有每隔5分钟有心跳行为,那为什么连接还会失效? 最大的可能是MySQL服务端的操作,MySQL默认服务端的wait_timeout是8小时,难道是有变更对应的配置?
show global variables like '%timeout%'
果然,数据库的超时时间被设置成了5分钟!那么问题就很明显了
结论:
1.空闲连接依赖于Druid的keepAlive定时任务来进行心跳检测和keepAlive,定时任务默认每60秒检测一次,并且只有当连接的空闲时间大于minEvictableIdleTimeMillis时才会进行心跳检测。
2.由于minEvictableIdleTimeMillis被设置为了5分钟,理论上空闲连接会在5分钟±60秒的时间区间内进行心跳检测。但是由于MySQL服务端的超时时间只有5分钟,所以大概率当Druid进行keepAlive操作时连接已经失效了。
3.由于数据库的活跃连接是波动的,且min-idle设置的是30,活跃连接处于波峰时,需要创建大量的连接,并且维护在连接池中。但是当活跃降到低谷时,大量的连接由于keepAlive失败,从连接池中被移除。周而复始。
4.每次创建连接时,又会将Connection对象放入入connectionFinalizerPhantomRefs中,并且由于创建完之后连接是处于活跃状态,短时间内不会被miniorGC所回收,直至晋升到老年代。导致这个SET越来越大。
2.由于minEvictableIdleTimeMillis被设置为了5分钟,理论上空闲连接会在5分钟±60秒的时间区间内进行心跳检测。但是由于MySQL服务端的超时时间只有5分钟,所以大概率当Druid进行keepAlive操作时连接已经失效了。
3.由于数据库的活跃连接是波动的,且min-idle设置的是30,活跃连接处于波峰时,需要创建大量的连接,并且维护在连接池中。但是当活跃降到低谷时,大量的连接由于keepAlive失败,从连接池中被移除。周而复始。
4.每次创建连接时,又会将Connection对象放入入connectionFinalizerPhantomRefs中,并且由于创建完之后连接是处于活跃状态,短时间内不会被miniorGC所回收,直至晋升到老年代。导致这个SET越来越大。
解决办法:知道问题的产生原因,要解决就很简单了,将minEvictableIdleTimeMillis设置为3分钟,保证keepAlive的有效性,避免一直重建连接即可。
子主题
Snowflake雪花算法 异常
锁相关问题
线上死锁如何排查
1.什么是死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
2.排查
jps+jstack
jps -l 查看java进程 PID
jstack -l PID
jconsole
打开Jconsole并连接到对应的java应用
点击线程-->检测死锁
死锁线程列表
Java Visual VM
打开Java Visual VM
连接到对应的java应用
点击线程
会检测并显示死锁线程
可生成一个线程的dump 显示更多信息
怀疑线上cpu 上下文切换频繁 如何排查
线上接口响应慢问题定位
方案1
https://blog.csdn.net/qq_33193972/article/details/136139161
1.确定影响范围 是某个接口还是全部接口
2.检查网络 使用ping traceroute或telnet等工具检查网络的连通性和延迟
3.查看服务器资源使用情况 CPU 磁盘IO 网络是否正常 使用top htop iostat等工具
4.查看应用日志 是否有异常或者错误信息
5.检查数据库 是否存在慢查询 一级负载和资源使用情况
6.检查缓存 命中率 考虑增加缓存或者优化缓存策略
7.检查代码 优化算法 减少循环
8.并发和线程
9.检查外部服务接口 响应时间
10.做压力测试 ApacheBench siege JMeter等工具。
2.检查网络 使用ping traceroute或telnet等工具检查网络的连通性和延迟
3.查看服务器资源使用情况 CPU 磁盘IO 网络是否正常 使用top htop iostat等工具
4.查看应用日志 是否有异常或者错误信息
5.检查数据库 是否存在慢查询 一级负载和资源使用情况
6.检查缓存 命中率 考虑增加缓存或者优化缓存策略
7.检查代码 优化算法 减少循环
8.并发和线程
9.检查外部服务接口 响应时间
10.做压力测试 ApacheBench siege JMeter等工具。
方案2
Arthas(阿尔萨斯)的trace命令可以分析具体的接口
cpu飙升
方案1 cpu 100%
top-c 命令查看cpu占用资源较高的PID
执行 jps -l 能够打印出所有的应用的PID,找到有一个PID和这个cpu使用100%一样的ID!!就知道是哪一个服务了。
Top -Hp PID 然后输入p,按照cpu使用率排序.会看到对应的线程号
找到cpu占用较高的线程TID
定位哪段代码耗费cpu过高
print “%x\n” 16 把线程的TID转成16进制 比如 0x41e8
print “%x\n” 16 把线程的TID转成16进制 比如 0x41e8
jstack 43987 | grep ‘0x41e8’ -C5 -color
这个就是用jstack打印进程堆栈信息,而且通过grep那个线程的16进制的pid,找到那个线程的相关东西,这个时候就可以在打印出的代码里,看到是哪个类的哪个方法导致cpu 100%的问题
这个就是用jstack打印进程堆栈信息,而且通过grep那个线程的16进制的pid,找到那个线程的相关东西,这个时候就可以在打印出的代码里,看到是哪个类的哪个方法导致cpu 100%的问题
线程的状态:
NEW,未启动的。不会出现在Dump中。
RUNNABLE,在虚拟机内执行的。
BLOCKED,受阻塞并等待监视器锁。
WATING,无限期等待另一个线程执行特定操作。
TIMED_WATING,有时限的等待另一个线程的特定操作。
TERMINATED,已退出的。
NEW,未启动的。不会出现在Dump中。
RUNNABLE,在虚拟机内执行的。
BLOCKED,受阻塞并等待监视器锁。
WATING,无限期等待另一个线程执行特定操作。
TIMED_WATING,有时限的等待另一个线程的特定操作。
TERMINATED,已退出的。
方案2 Arthas
1.thread命令 可以现实cpu占比
2.thread 线程id 快速定位
2.thread 线程id 快速定位
线上OOM内存溢出问题排查
方案1
1.导出内存快照 dump jmap -dump
2.使用mat工具
3.启动参数配置 -XX:+HeapDUmpOnOutOfMemoryError 内存溢出时自动生成dump
2.使用mat工具
3.启动参数配置 -XX:+HeapDUmpOnOutOfMemoryError 内存溢出时自动生成dump
排查内存泄漏
1.检查Full GC的情况 jstat -gc pid 间隔时间 显示次数
2.检查消耗cpu的线程 top
3.不建议导出dump文件,因为导出时,服务无法响应请求,如果文件很大则会造成服务长时间假死。
可以使用jmap -histo pid来查看哪些对象占用的内存大
2.检查消耗cpu的线程 top
3.不建议导出dump文件,因为导出时,服务无法响应请求,如果文件很大则会造成服务长时间假死。
可以使用jmap -histo pid来查看哪些对象占用的内存大
排查死锁
方案1 命令行
命令行检查死锁(可以使用jstack快速进行死锁检查)
jps -l
jstack -l 100460|grep "deadlock"
jstack -l 100460 导出堆栈信息,死锁信息就在最后
jps -l
jstack -l 100460|grep "deadlock"
jstack -l 100460 导出堆栈信息,死锁信息就在最后
方案2 Arthas
thread -b 打印死锁线程id
thread 线程id 可以打印具体的堆栈信息
thread 线程id 可以打印具体的堆栈信息
分布式事务
什么是分布式事务
可以简单的理解为牵扯到一次操作,牵扯到多个事务 (一次调用,牵扯到多个服务或者多个数据库,有多个事务)
XA协议
XA协议是一个基于数据库的分布式事务协议,其分为两部分:事务管理器和本地资源管理器。事务管理器作为一个全局的调度者,负责对各个本地资源管理器统一号令提交或者回滚。二阶提交协议(2PC)和三阶提交协议(3PC)就是根据此协议衍生出来而来。如今Oracle、Mysql等数据库均已实现了XA接口。
两阶段提交 2pc
两段提交顾名思义就是要进行两个阶段的提交:第一阶段,准备阶段(投票阶段) ; 第二阶段,提交阶段(执行阶段)
扩展知识
内存屏障
指令乱序
分支预测
NUMA
cpu亲和性
Code Review的好处
发现代码缺陷,找到需要重构的地方
找出非功能性缺陷(性能,安全,兼容性等)
帮助新员工提升代码能力
学习别人的优秀设计与代码
熟悉业务逻辑
建立互相学习和进步的范围
...
0 条评论
下一页