java知识框架图
2021-01-26 23:00:02 0 举报
AI智能生成
登录查看完整内容
2021.1.26整理
作者其他创作
大纲/内容
分布式锁
Zookeeper
死锁
羊群效应
临时节点顺序
性能没redis高
Redis
性能比较高
数据库
设置一个失效时间用定时任务去跑
数据库集群 主备同步
搞个死循环排队
可重入设计一个字段累加
排它锁
用数据库自身的锁就可以了 行锁 索引
select XX for update
记得 提交
宕机数据库也会自动释放锁
缺点
比其他的更消耗资源
复杂
特点
互斥
安全性
容错
算法
贪心
分治
动态规划
快排
堆排
二叉树
链表反转
成环
环节点
跳楼梯
扩展知识
内存屏障
指令乱序
分支预测
NUMA
CPU亲和性
遇到过的坑
sync遇到了bgsave
CPU彪升
bgsave之后会做一个emptyDB
这个时候做bgsave cow的机制就没了
会重新加载整个RDB
然后就swap
高并发场景下无限同步
tps过高的时候
master 复制缓存挤压区的时候 有个参数client-output-buffer-limit 默认1M
大量请求会让值升高 超出阈值断开
重连
slave做RDB同步的时候会导致TPS过高无法加载
阻塞久了复制缓冲区的数据就被冲掉了,是个队列会踢掉之前的数据
slave重连对比offset发现空档重新sync
预估体量参数动态调整
canal 并发修改
es 自动机构建
redis es 深分页
为什么换工作
想去阿里
有什么想问我的
我的回答有什么建议么
团队女生多吗
团队主要做的事情
项目
竞价服务
竞价策略
开关
频控
出价
监控服务
行为监控
获胜
点击
曝光
播放完成
物料
送审
合成
商品自动拼装
流量监控
自动化
个人项目
全局报警
分布式事务组件
拦截spring事务
设计方案
高并发下单
订单生成
Java
集合
HashMap
1.7
数组+链表
头插
用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题,线程不安全
根源在transfer函数中
在对table进行扩容到newTable后,需要将原来数据转移到newTable中,注意10-12行代码,这里可以看出在转移元素的过程中,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。
resize()方法负责扩容,inflateTable()负责创建表
键为null的情况
调用putForNullKey()方法
1.7在遍历的同时没有添加数据,而是另外调用了addEntry()方法,将节点添加到链表头部
1.8
数组+链表+红黑树
尾插
加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。
resize()方法在表为空时,创建表;在表不为空时,扩容为原来的2的n次幂
扩容时,利用 2 的次幂数值的二进制特点,既省去重新计算 hash 的时间,又把之前冲突的节点散列到了其他位置
没有区分键为null的情况
两个版本中如果键为null,那么调用hash()方法得到的都将是0,所以键为null的元素都始终位于哈希表table【0】中
1.8中的桶中元素处于链表的情况,遍历的同时最后如果没有匹配的,直接将节点添加到链表尾部
扩容机制
LoadFactory 默认0.75
创建一个空数组重新Hash
Hash公式跟长度有关
线程不安全
HashMap中put操作的主函数, 注意第6行代码,如果没有hash碰撞则会直接插入元素。如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入第6行代码中。假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。
2的幂次
方便位运算
均匀分布
重写equals必须重写HashCode
如果只重写hashcode()不重写equals()方法,当比较equals()时,其实调用的是Object中的方法,只是看他们是否为同一对象(即进行内存地址的比较)。
如果只重写equals()不重写hashcode()方法,在一个判断的时候就会被拦下HashMap认为是不同的Key。
在HashMap的“键”部分存放自定义的对象,一定要重写equals和hashCode方法。再来两句老生常谈的话!两个对象==相等,则其hashcode一定相等,反之不一定成立。两个对象equals相等,则其hashcode一定相等,反之不一定成立。
equals():默认情况下比较的是对象的 内存地址值,被重写后按照重写要求进行比较,一般是比较对象的 数据值
hashCode(): 默认情况下为对象的 内存地址值,被重写后按照重写要求生成新的值。
方法
equals 和 ==
equals
引用类型:默认情况下,对比它们的地址是否相等;如果equals()方法被重写,则根据重写的要求来比较。
String的equals()方法仅仅是对比它的 数据值,而不是对象的 内存地址
==
对于基本类型来说,== 比较的是它们的值
对于引用类型来说,== 比较的是它们在内存中存放的地址(堆内存地址)
put
get
扩容
扩容操作是新建了一个HashMap的底层数组,而后调用transfer方法,将就HashMap的全部元素添加到新的HashMap中(要重新计算元素在新的数组中的索引位置)。
ConcurrentHashMap
segment分段锁
继承了reentranLock
尝试获取锁存在并发竞争 自旋 阻塞
get高效 volatile修饰 不需要加锁
volatile修饰节点指针
HashEntry
Segment + HashEntry + ReentrantLock
CAS+synchronized
cas失败自旋保证成功
再失败就sync保证
node
Node + CAS + Synchronized
key 的 hashCode ,然后计算 table 的 index 位置,获取该 index 的值 x
x 为 null,说明还没有记录,调用 CAS 操作 该新的记录插入到table的index位置上去
x 不为 null,通过 synchronized 关键字 对 table 的 index 位置加锁;
然后判断table的index位置上的第一个节点的hashCode值,如果hashCode值小于0,那么就是一颗红黑树,如果不小于0,那么就还是一条链表;如果是一条链表,查找链表寻找是否有一个记录的 key 值和本次插入的 key 值相同,相同则将替换掉 value 值;不同的话直接添加到链表中;如果是一棵红黑树,调用 putTreeVal方法进行插入操作;插入完成,判断是不是更新操作,不是更新操作的话,size + 1。
计算hash值,定位到该table索引位置,如果是首节点符合就返回
get操作不会加锁,通过volatile关键字来保证读到的数据不是脏数据
迭代器是弱一致性
LinkedHashMap
继承自 HashMap
数组和链表或红黑树
双向链表的结构
afterNodeAccess保证有序
HashTable
Synchronize 线程安全
对对象加锁,锁住的是对象整体
HashTable效率低下
竞争这一把锁
HashMap和HashTable的区别
继承的父类不同 ,
线程安全
是否提供contains方法
HashTable不允许null值(key和value都不可以) ;HashMap允许null值(key和value都可以)
Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式
HashTable在不指定容量的情况下的默认容量为11,而HashMap为16
Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。
Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。
HashTable直接使用对象的hashCode; HashMap重新计算hash值,而且用与代替求模。
TreeMap
底层是红黑树
TreeMap继承AbstractMap
ArrayList
数组
动态数组
默认的容量大小 10
线程不安全
查找 访问速度快 增删效率低
System.arraycopy
将旧数组内容复制到新数组
插入元素
然后判断是否需要扩容
删除元素
删除指定位置的元素,先将指定位置后面的所有元素向前移动一位,
再将最后一个位置元素置为null,并修改size。
迭代器
定义了迭代器,可以双向迭代访问元素(next、previous)
序列化
elementData属性前有transient关键字,这表示elementData不会被序列化
ArrayList自定义了writeObject和readObject方法,实现了elementData序列化和反序列化的逻辑
它有可能存储了很多的null元素,所以为了避免序列化这些没必要的元素,就自定义了序列化逻辑。
快速失败(fail-fast)
modCount 用来记录 ArrayList 结构发生变化的次数
如果一个动作前后 modCount 的值不相等,说明 ArrayList 被其它线程修改了
迭代器将抛出 ConcurrentModificationException 异常
hashcode值随着它存储的元素改变而改变。ArrayList计算hashcode是通过它存储的元素来计算的,所以一般不要用ArrayList作为hasjmap的键、hashset的元素。
LinkedList
双向链表
适合插入删除频繁的情况 内部维护了链表的长度
LinkedList.Node
实际存储的元素
指向上一个节点的指针
指向下一个节点的指针
实现了Deque接口,可以将LinkedList当做队列使用
Offer:( 往队列尾部插入元素。直接返回false
Peek 查找队列头部元素并返回
Poll 移除并返问队列头部的元素。
add():往队列尾部插入元素。如果队列已满,则抛出一个IIIegaISlabEepeplian异常,让你处理
remove方法,没有元素时会抛出异常
LinkedList元素实际上是保存在LinkedList.Node.item上,自定义是为了避免序列化额外的东西(Node.next和Node.prev)。
Hashcode
在Java集合框架中,List实现类的hashcode值随着其存储的元素改变而改变,所以不要将List的实现类作为HashMap的键。
和ArrayList类似,不要在foreach循环中,对链表进行修改,否则会抛出ConcurrentModificationException异常。
HashSet
哈希表
数据是无序的,可以放入 null
但只能放入一个 null,两者中的值都不能重复,就如数据库中唯一约束
基本的操作都是有 HashMap 底层实现的
TreeSet
自动排好序的,不允许放入 null 值
底层是 TreeMap 的 keySet()
TreeMap 是基于红黑树实现的
网络基础
UDP
语音
视频
直播
TCP
三次握手
syn seq
第一次握手:客户端发送syn包(syn=j)到服务器,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_SEND 状态。等待服务器确认;
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
syn ack seq
服务端会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
ack seq
客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 established (已连接)状态。服务器收到 ACK 报文之后,也处于 established (已连接)状态,此时,双方已建立起了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
为什么连接的时候是三次握手?
第一次握手:客户端发送网络包,服务端收到了。这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。
第二次握手:服务端发包,客户端收到了。这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常。
第三次握手:客户端发包,服务端收到了。这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常。因此,需要三次握手才能确认双方的接收与发送能力是否正常。
试想如果是用两次握手,则会出现下面这种情况:如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一致等待客户端发送数据,浪费资源。
四次挥手
fin ack seq
第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。
即发出连接释放报文段(FIN=1,序号seq=u),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1(终止等待1)状态,等待服务端的确认。
第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。
即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号ack=u+1,序号seq=v),服务端进入CLOSE_WAIT(关闭等待)状态,此时的TCP处于半关闭状态,客户端到服务端的连接释放。客户端收到服务端的确认后,进入FIN_WAIT2(终止等待2)状态,等待服务端发出的连接释放报文段。
第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务端进入LAST_ACK(最后确认)状态,等待客户端的确认。
第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间2MSL后,客户端才进入CLOSED状态。
如何保证可靠
应用数据被分割成 TCP 认为最适合发送的数据块。
TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。
TCP 的接收端会丢弃重复的数据。
流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
拥塞控制: 当网络拥塞时,减少数据的发送。
停止等待协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就- 停止发送,等待对方确认。在收到确认后再发下一个分组。 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认;
什么是半连接队列?
服务器第一次收到客户端的 SYN 之后,就会处于SYN_REVD 状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列
全连接队列?
完成三次握手,建立起连接的就会放在全连接队列中。如果队列满了就有可能会出现丢包现象。
SYN-ACK 重传次数
服务器发送完SYN-ACK包,如果未收到客户确认包,服务器进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。
三次握手过程中可以携带数据吗?
第三次握手的时候,是可以携带数据的。但是,第一次、第二次握手不可以携带数据
假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
如果第三次握手丢失了,客户端服务端会如何处理?
服务端
客户端
第三次握手中的ACK包丢失的情况下,Client 向 server端发送数据,Server端将以 RST包响应,方能感知到Server的错误。
SYN攻击是什么?
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击。SYN攻击就是Client在短时间内伪造大量不存在的IP地址,并向Server不断地发送SYN包,Server则回复确认包,并等待Client确认,由于源地址不存在,因此Server需要不断重发直至超时,这些伪造的SYN包将长时间占用未连接队列,导致正常的SYN请求因为队列满而被丢弃,从而引起网络拥塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。
挥手为什么需要四次?
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。
四次挥手释放连接时,等待2MSL的意义?
报文段最大生存时间MSL(Maximum Segment Lifetime)
场景
网络会话
文件传输
发送接收邮件
远程登录
HTTP
通信使用明文(不加密),内容可能被窃听
无法证明报文的完整性,所以可能遭篡改
不验证通信方的身份,因此有可能遭遇伪装
HTTPS
TLS/SSL
散列函数
验证信息的完整性。
对称加密
采用协商的密钥对数据加密
非对称加密
身份认证和密钥协商
同步阻塞I/O(BIO)
服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。
阻塞等待链接
阻塞等待数据
开线程处理并发
耗资源
同步非阻塞I/O(NIO)
IO是面向流的,NIO是面向缓冲区的。
在NIO中,所有的数据都是用缓冲区处理
服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,
I/O多路复用模型
多路复用的优势并不是单个连接处理的更快,而是在于能处理更多的连接。
epoll
NIO的3个核心概念
缓冲区Buffer
NIO 的 Buffer 和 channel 都是既可以读也可以写
Buffer主要用于和NIO通道进行交互,数据是从通道读入到缓冲区的,然后从缓冲区中写入到通道中的。
通道Channel
channel 类似于流,每个 channel 对应一个 buffer缓冲区,buffer 底层就是个数组
多路复用器Selector
channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理
nio底层是怎么感知有事件发生的呢
引入了epoll基于事件响应机制来优化NIO。也就是如果有事件发生,客户端主动通知多路复用器请求事件,只处理发生事件请求的客户端,不会在次去全部循环处理。
异步非阻塞I/O(AIO)
服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂
粘包/拆包
在报文末尾增加换行符表明一条完整的消息,这样在接收端可以根据这个换行符来判断消息是否完整。将消息分为消息头、消息体。可以在消息头中声明消息的长度,根据这个长度来获取报文(比如 808 协议)。规定好报文长度,不足的空位补齐,取的时候按照长度截取即可。
多路复用
select
单个进程可监视的fd数量被限制,即能监听端口的大小有限。
对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低:
需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll
大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义
poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。
内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
neetty 非异步 阻塞 response trse id 感觉
OSI模型
应用层
OSI参考模型中最靠近用户的一层,是为计算机用户提供应用接口,也为用户直接提供各种网络服务。我们常见应用层的网络服务协议有:HTTP,HTTPS,FTP,POP3、SMTP等。
表示层
会话层
会话层就是负责建立、管理和终止表示层实体之间的通信会话。该层的通信由不同设备中的应用程序之间的服务请求和响应组成。
传输层
传输层建立了主机端到端的链接,传输层的作用是为上层协议提供端到端的可靠和透明的数据传输服务,包括处理差错控制和流量控制等问题。该层向高层屏蔽了下层数据通信的细节,使高层用户看到的只是在两个传输实体间的一条主机到主机的、可由用户控制和设定的、可靠的数据通路。我们通常说的,TCP UDP就是在这一层。端口号既是这里的“端”。
网络层
本层通过IP寻址来建立两个节点之间的连接,为源端的运输层送来的分组,选择合适的路由和交换节点,正确无误地按照地址传送给目的端的运输层。就是通常说的IP层。这一层就是我们经常说的IP协议层。IP协议是Internet的基础。
数据链路层
物理层
实际最终信号的传输是通过物理层实现的。通过物理介质传输比特流。规定了电平、速度和电缆针脚。
restful
REST(英文:Representational State Transfer,简称REST),表述性状态转移,指的是一组架构原则。
Restful: 遵守了rest 原则 的web服务或web应用。
资源路径(URI)、HTTP动词(Method)、过滤信息(query-string)、状态码(Status-code)、错误信息(Error)、返回结果(Result)
多线程
进程和线程
进程
程序的一次执行,系统进行资源分配和调度的独立单位
作用是程序能够并发执行
提高资源利用率和吞吐率
进程则占有堆、栈。
线程
线程是比进程更小的能独立运行的基本单位
是进程的一个实体
可以减少程序并发执行时的时间和空间开销,使得操作系统具有更好的并发性。
线程基本不拥有系统资源
程序计数器、寄存器和栈
synchronized
原子性内置锁
监视器锁
作用主要就是实现原子性操作和解决共享变量的内存可见性问题。
对象
对象头(Header)
Mark Word
具体的内容包含对象的hashcode、分代年龄、轻量级锁指针、重量级锁指针、GC标记、偏向锁线程ID、偏向锁时间戳
Klass Point(对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。)
Monitor
EntryList
Owner(会指向持有 Monitor 对象的线程)
WaitSet
实例数据
对其填充
ACC_SYNCHRONIZED
JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。
代码块
依赖操作系统底层互斥锁实现。
他依赖操作系统底层互斥锁实现
加锁的过程会清除工作内存中的共享变量,再从主内存读取
而释放锁的过程则是将工作内存中的共享变量写回主内存
过程
当多个线程进入同步代码块时,首先进入entryList
有一个线程获取到monitor锁后,就赋值给当前线程,并且计数器+1
如果线程调用wait方法,将释放锁,当前线程置为null,计数器-1,同时进入waitSet等待被唤醒,调用notify或者notifyAll之后又会进入entryList竞争锁
如果线程执行完毕,同样释放锁,计数器-1,当前线程置为null
锁优化
无锁->偏向锁->轻量级锁->重量级锁
无锁
偏向锁
mark Word 中有线程信息 cas 比较
对象头是由Mark Word和Klass pointer 组成,锁争夺也就是对象头指向的Monitor对象的争夺,一旦有线程持有了这个对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中。
当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁了,偏向锁会永远偏向第一个获得锁的线程,如果后续没有其他线程获得过这个锁,持有锁的线程就永远不需要进行同步,反之,当有其他线程竞争偏向锁时,持有偏向锁的线程就会释放偏向锁。可以用过设置-XX:+UseBiasedLocking开启偏向锁。
轻量级
轻量级锁主要就是通过CAS修改对象头锁记录和自旋来实现
复制了一份mark work 叫 Lock record 也是cas尝试改变指针
JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。
重量级
除了拥有锁的线程其他全部阻塞。
自旋
死循环
由于大部分时候,锁被占用的时间很短,共享变量的锁定时间也很短,所有没有必要挂起线程,用户态和内核态的来回上下文切换严重影响性能。自旋的概念就是让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。
自适应锁
自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
锁消除
锁消除指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
锁粗化
锁粗化指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。
特性保证
有序性
为了提高程序的执行效率,编译器和CPU会对程序中代码进行重排序。
as-if-serial
as-if-serial语义的意思是:不管编译器和CPU如何重排序,必须保证在单线程情况下程序的结果是正确的。
编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。例如:修改顺序后运算结果改变
happens-before
可见性
工作内存强制刷新主内存的数据
原子性
单一线程持有
在第一个线程获取到锁之后,在他执行完之前不允许其他的线程获取锁并操作共享数据,
可重入性
计数器
可重入的函数一定是线程安全的,反之则不一定成立
线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
若一个程序或子程序可以“在任意时刻被中断然后操作系统调度执行另外一段代码,这段代码又调用了该子程序不会出错”,则称其为可重入(reentrant或re-entrant)的。即当该子程序正在运行时,执行线程可以再次进入并执行它,仍然获得符合设计时预期的结果。与多线程并发执行的线程安全不同,可重入强调对单个线程执行时重新进入同一个子程序仍然是安全的
重锁
用户态内核态切换
sync 和 Lock的区别
通过Lock可以知道线程有没有拿到锁,而synchronized不能。synchronized能锁住方法和代码块,而Lock只能锁住代码块。
劣势
锁升级不可逆
synchronized锁是重量级锁,重量级体现在活跃性差一点。synchronized锁是内置锁,意味着JVM能基于synchronized锁做一些优化:比如增加锁的粒度(锁粗化)、锁消除。而Lock是一个接口,是JDK层面的有丰富的API。
在synchronized锁上阻塞的线程是不可中断的:线程A获得了synchronized锁,当线程B也去获取synchronized锁时会被阻塞。而且,线程B无法被其他线程中断(不可中断的阻塞),而ReentrantLock锁能实现可中断的阻塞
synchronized锁释放是自动的,当线程执行退出synchronized锁保护的同步代码块时,会自动释放synchronized锁。而ReentrantLock需要显示地释放:即在try-finally块中释放锁。
线程在竞争synchronized锁时是非公平的:假设synchronized锁目前被线程A占有,线程B请求锁未果,被放入队列中,线程C请求锁未果,也被 放入队列中,线程D也来请求锁,恰好此时线程A将锁释放了,那么线程D将跳过队列中所有的等待线程(即:线程B和线程C)并获得这个锁。而ReentrantLock能够实现锁的公平性。
synchronized锁是读写互斥并且 读读也互斥,ReentrantReadWriteLock 分为读锁和写锁,而读锁可以同时被多个线程持有,适合于读多写少场景的并发。
ThreadLocal
ThreadLocal是什么
线程变量
threadlocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据,
ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置
作为一个存储数据的类,关键点就在get和set方法。
在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
线程间数据隔离
进行事务操作,用于存储线程事务信息。
数据库连接,Session会话管理。
ThreadLocal怎么用
ThreadLocal的作用是每一个线程创建一个副本
频繁的使用数据库,那么就需要建立多次链接和关闭,我们的服务器可能会吃不消
ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能
ThreadLocal源码分析
set
从set方法我们可以看到,首先获取到了当前线程t,然后调用getMap获取ThreadLocalMap,如果map存在,则将当前线程对象t作为key,要存储的对象作为value存到map里面去。如果该Map不存在,则初始化一个。
首先获取当前线程,然后调用getMap方法获取一个ThreadLocalMap,如果map不为null,那就使用当前线程作为ThreadLocalMap的Entry的键,然后值就作为相应的的值,如果没有那就设置一个初始值。
remove方法
ThreadLocal内存泄漏问题
Thread中有一个map,就是ThreadLocalMap
ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。
ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收
突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
ThreadLocal 用来解决什么问题?
可以从尽量减少临界区范围,使用 ThreadLocal,减少线程切换、使用读写锁或 copyonwrite 等机制这些方面来回答。
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring的事务主要是ThreadLocal和AOP去做实现的
Lock和ReadWriteLock是两大锁的根接口
ReadWriteLock
ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。
ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。
ReentrantReadWriteLock
ReadLock
可以实现并发访问下的多读
WriteLock
可以实现每次只允许一个写操作。
Lock
Lock代表实现类是ReentrantLock(可重入锁)
Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。
ReentrantLock
有两个子类用于sync的初始化,FairSync和NonfairSync。这其实就是我们所熟知的公平锁和非公平锁。ReentrantLock默认情况下使用了非公平锁,当然也可以在创建ReentrantLock的时候显示指定。
NonfairSync
非公平锁一上来就先调用一把compareAndSetState(),尝试获取锁
state记录了锁重入的次数,如果为0,那么表示当前没有线程持有此锁,此时使用一个CAS操作即可快速完成锁的申请,这便是快速尝试。
当快速尝试失败之后,将会调用acquire()方法
原理是通过unsafe提供的CAS原子操作进行state的值更新。另外发现compareAndSetState()是位于AbstractQueuedSynchronizer类中的,继而发现,Sync继承了AbstractQueuedSynchronizer,我们需要更新的state也位于AbstractQueuedSynchronizer中。
非公平锁是tryAcquire()获取锁的
如果线程没有获取到锁,就只能去队列里等待锁了,也就是调用addWaiter()方法。
acquireQueued
FairSync
lock()里的tryAcquire()实现有所不同
hasQueuedPredecessors
hasQueuedPredecessors()很关键,它是公平性的核心体现
hasQueuedPredecessors()的作用是当满足以下两种条件中的一种时,线程就能获得抢锁的资格: 1. 锁同步队列里只有一个节点;2. 第二个节点属于当前线程。
如果是当前持有锁的线程 可重入
AbstractQueuedSynchronizer
入队 出队
头结点设计
共享和独享的实现
ReentrantLock通过使用AQS来实现加解锁
AQS 维护了一个基于双向链表的同步队列,线程在获取同步状态失败的情况下,都会被封装成节点,然后加入队列中。
AQS内部维护了一个双向链表的锁同步队列,并维护头节点head,尾节点tail和信号量state。每个节点是一个Node对象,对象中定义了prev,next分别指向它的上下游,还有一个waitStatus对象用于表示线程状态(等锁或已放弃)。当有新的线程需要抢锁时,新建一个和线程映射的Node,加入到锁同步队列的末尾。当然这里有个重点,在加入的时候会做判断,如果当前末尾节点处于放弃状态,那么会继续往前遍历,寻找一个可靠的节点作为上游。AQS内部的state为0时,资源未被占用,线程可进行CAS操作更新state,如果更新成功则代表加锁成功。如果state不为0,则意味着资源已经被线程占用。如果占用者是自己,那么可以进行重入,如果占用者不是自己,那么就老老实实等着。
CAS
实际应用
CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:变量内存地址,V表示旧的预期值,A表示准备设置的新值,B表示当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
存在的问题
cpu开销
自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
只能保证一个共享变量原子操作
AtomicReference
只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference来处理或者使用锁synchronized实现。
ABA
标志位 时间戳
ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果
Java中有AtomicStampedReference来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
StampedLock
6个方法
lock拿不到锁会一直等待
tryLock是去尝试,拿不到就返回false,拿到返回true。
volatile
volatile可以解决线程间变量可见性问题
volatile通过内存屏障禁止指令重排序
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
相比synchronized的加锁方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,他没有上下文切换的额外开销成本。使用volatile声明的变量,可以确保值被更新的时候对其他线程立刻可见。volatile使用内存屏障来保证不会发生指令重排,解决了内存可见性的问题。
MESI
当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取,CPU就会根据缓存一致性协议强制线程A重新从主内存加载最新的值到自己的工作内存,而不是直接用缓存中的值
锁bus
volitale会一直嗅探 cas 不断循环无效交互 导致带宽达到峰值
总线风暴
Java内存模型JMM
高速缓存
嗅探机制 强制失效
处理器嗅探总线
禁止指令重排序
lock 前缀指令 内存屏障
源代码->编译器优化重排序->指令级并行重排序->内存系统重排序->最终执行的指令序列
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
AtomicInteger
跳出死循环
volatile修饰之后会加入不同的内存屏障来保证可见性的问题能正确执行
线程池
newFixedThreadPool
(定长)固定线程数量的线程池,没有达到线程数上限时,会创建新的线程。否则,会缓存任务排队执行。
使用一个有界队列LinkedBlockingQueue缓存任务
避免任务堆积,newFixedThreadPool创建固定数目的线程,使用的任务队列是无界的,如果任务的处理速度跟不上入队的速度,就会导致任务大量堆积,占用大量系统内存,严重时导致OOM
newCacheThreadPool
缓存线程并定时回收的线程池
使用SynchronousQueue同步队列,这个队列容量为0,任务入队必须对应着一个出队操作,否则就会阻塞,反之亦然。提交的任务都会马上执行,线程空闲等待超时会回收,直到线程数收缩为0,长时间运行,不会消耗什么资源
响应优先:以响应优先的场景,需要任务到来时尽快执行,避免排队。
newSIngleTheadExecutor
一池只有一个线程。
线程数固定为1的线程池,使用一个有界阻塞队列LinkedBlockingQueue缓存任务,所有任务排队串行执行
newScheduleThreadPool
.实例化时工作队列使用延时队列(DelayedWorkQueue)
提交的任务装饰成ScheduledFutureTask类型,并把任务加入到工作队列
ScheduledFutureTask实现Delayed和Comparable接口,提交到工作队列中的任务是按照任务执行时间排序的(最早执行的任务在头部),因为工作队列是个小顶堆
只能从工作队列中获取已到执行时间的任务
可以定时或者周期性执行任务的线程池
在ThreadPoolExecutor之上扩展实现了定时调度的能力
newWorkStealingPool
ThreadPoolExecutor线程池实现类
参数
corePoolSize:核心线程池大小
吞吐量优先:应该设置队列去缓冲并发任务,调整合适的corePoolSize去设置处理任务的线程数。在这里,设置的线程数过多可能还会引发线程上下文切换频繁的问题,也会降低处理任务的速度,降低吞吐量。
maximumPoolSize: 线程池最大大小
默认没线程等任务来了才调用 除非调用了 预创建线程 一个或者全部
workQueue: 任务的缓存队列,必须是阻塞队列
ArrayBlockingQueue
\t底层实现是数组
有界队列
加锁保证安全 一直死循环阻塞 队列不满就唤醒
入队
在进行某项业务存储操作时,建议采用offer进行添加,可及时获取boolean进行判断,如用put要考虑阻塞情况(队列的出队操作慢于进队操作),资源占用。
LinkedBlockingDeque
底层实现是链表
无界 当心内存溢出
PriorityBlockingQueue
优先队列,本质是个小顶堆
DelayQueue
延时队列 (优先队列 & 元素实现Delayed接口),ScheduledThreadPoolExecutor实现的关键
SynchronousQueue
同步队列
变长线程池
keepAliveTime 非核心线程等待超时时间
存活时间,当线程的没执行任务时,空闲的时间超过了这个时间就会被标记为可回收,直到线程池的大小超过基本大小,被标记的线程就会被终止
没有执行任务多久会终止 当线程池的线程数大于核心线程才会起作用
调用allowCoreThreadTimeOut会起作用
RejectedExecutionHandler:拒绝策略(线程池无法执行该任务时的处理策略)
抛异常
AbortPolicy\t
默认的饱和策略,该策略抛出未检查(运行时异常)的RejectedExecutionException。
丢弃
DiscardPolicy\t
不执行任何操作,直接抛弃任务
重试
CallerRunsPolicy
在调用者线程中执行该任务
丢弃最早提交的
DiscardOldestPolicy
丢弃阻塞队列中的第一个任务, 然后重新将该任务交给线程池执行
threadFactory 控制线程创建的工作
线程工厂。线程池在创建线程时通过调用线程工厂的Thread newThread(Runnable r)来创建线程
unit 时间单位
keepAliveTime的单位,有DAYS、HOURS、MINUTES、SECONDS、MILLISECONDS、MICROSECONDS、NANOSECONDS7个单位可选
工厂方法
线程工厂(ThreadFactory)
线程池创建线程都是通过的ThreadFactory的Thread newThread(Runnable r)方法来创建的
使用Has表维护线程的引用
submit
调用线程可以根据返回的Future对象获取任务执行结果
实际使用
商品详情界面
批处理
执行过程
核心线程->队列->最大线程->拒绝策略
如果 线程数<核心线程数,创建新的线程,并执行任务
如果当前线程数>核心线程数,尝试把任务加入任务队列。
如果任务队列已满,入队失败,并且 当前线程数< 最大线程数,创建新线程执行任务
如果 线程数>最大线程数,执行拒绝策略
故障
注意线程泄漏,如果线程数目不断的增长,可能是因为任务逻辑出现问题,导致线程迟迟不能被释放
使用ThreadLocal导致的内存泄漏,ThreadLocal以自己的弱引用对象作为key,使用Thread中的ThreadLocalMap存储数据,ThreadLocal不再使用后,弱引用对象会被GC回收,对应的value永远不会被访问,但如果线程不销毁就会持有ThreadLocalMap进而持有value的强引用,导致内存泄漏。线程池中工作线程的生命周期一般比任务要长,核心线程更是长驻内存。所以在线程池中避免使用ThreadLocal或者使用后及时调用remove释放value的引用
优点
线程复用
提高响应速度
管理线程
线程次生命周期
RUNNING 运行,接受新任务,也可以处理任务队列中的任务
SHUTDOWN 中断,不接受新任务,但是可以处理任务队列中的任务
STOP 停止,不接受新任务,也不处理队列中的任务,并且会中断执行任务的线程
TIDYING tidying所有线程都已经中断、清理,有效线程数等于0
TERMINATED 终止,回调terinal后进入该状态,表示线程池已死
Worker线程管理
Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask
thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务
firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行workQueue中的任务,也就是非核心线程的创建。
线程池管理线程的生命周期,需要在线程长时间不运行的时候进行回收
线程池使用一张Hash表去持有线程的引用,这样可以通过添加引用、移除引用这样的操作来控制线程的生命周期。
判断线程是否在运行
Worker是通过继承AQS,使用AQS来实现独占锁这个功能
lock方法一旦获取了独占锁,表示当前线程正在执行任务中。则不应该中断线程
如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断。
线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;如果线程是空闲状态则可以安全回收。
Worker线程执行任务
Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。
当线程池决定哪些线程需要回收时,只需要将其引用消除即可
线程池中线程的销毁依赖JVM自动的回收
JUC
JUC就是java.util.concurrent下面的类包,专门用于多线程的开发。
常见问题
线程间是怎么进行通信的?
volatile关键字修饰变量,就是告知线程对该变量的访问必须重主内存中获取。而对它的改变必须同步刷新到主内存中。这样就能保证线程对变量访问的可见性。
关键字synchronized可以修饰方法或者代码块,使用synchronized可以确保多个线程在同一时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
(monitor),获取到该对象的监视器才能进入同步块,或者同步方法,而没有获取到监视器(monitor)的线程会阻塞在同步块或者同步方法的入口,进入BLOCKED状态
wait()/notify()
等待/通知机制是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify或者notifyAll方法。线程A收到通知后从对象O的wait()方法返回,进而执行后续的操作
notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notfifyAll()的线程释放对象的监视器(也就是执行monitorexit指令)后,等待线程才会有机会从wait()返回。
从wait()方法返回的前提是获得了调用对象的监视器(执行monitorenter指令成功)。
wait()/notifyAll
notify()/notifyAll()
一个有序的出队列的过程
Object
wait()
调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,线程调用wait()方法前,需要获得对象的监视器。当调用wait()方法后,会释放对象的监视器
wait(long)\t
\t调用该方法的线程进入TIMED_WAITING状态,这里的参数时间是毫秒,等待对应毫秒事件,如果没有收到其他线程通知,则超时返回
调用该方法的线程进入TIMED_WAITING状态,基本作用同wiat(long),第二个参数代表为纳秒,也就是等待时间为毫秒+纳秒。
notify()\t
通知一个在对象监视器上等待的线程,使其从wait()方法返回,而返回的前提是该线程获取到了对象的监视器。
notifyAll()\t
\t通知所有在监视器上等待的线程,具体唤醒那个线程由CPU决定
Lock下的等待/通知机制实现
courrent包下的Lock
CountDownLatch(闭锁)
CountDownLatch是一个同步的辅助类,允许一个或多个线程一直等待,直到其它线程完成它们的操作。
创建CountDownLatch并设置计数器值。
启动多线程并且调用CountDownLatch实例的countDown()方法。
主线程调用 await() 方法,这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务,count值为0,停止阻塞,主线程继续执行。
说白了就是通过count变量来控制等待,如果count值为0了(其他线程的任务都完成了),那就可以继续执行。
CyclicBarrier(栅栏)
CyclicBarrier允许一组线程互相等待,直到到达某个点。
叫做cyclic是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用(对比于CountDownLatch是不能重用的)
区别:CountDownLatch注重的是等待其他线程完成;CyclicBarrier注重的是:当线程到达某个状态后,暂停下来等待其他线程,所有线程均到达以后,继续执行。
如何尽可能提高多线程并发性能?
减少锁竞争手段
第一个是缩小锁的范围:将与锁无关代码移除同步代码块,尤其是那些可能发生阻塞的操作比如I/O;
第二个是减少锁的粒度:使用多个相互独立锁管理独立的状态变量,改变某个变量只用获取对应变量锁,而不用获取整体锁,其他线程仍然能使用其他变量。
第三个是锁分段:比如ConcurrentHashMap底层的链表数组,对数组中每一个数组元素进行加锁,数组长度是多少就有多少个锁,也就最大支持多少并发。
上下文切换
读写锁适用于什么场景?
可以回答读写锁适合读并发多,写并发少的场景,另外一个解决这种场景的方法是 copyonwrite。
copyonwrite机制
写时复制
在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,然后用现在的数组去替换成员变量的数组(就是get等读取操作读取的数组)。
比读写锁有改进的地方,那就是读取的时候可以写入的 ,这样省去了读写之间的竞争
如何实现一个生产者与消费者模型?
可以尝试通过锁、信号量、线程通信、阻塞队列等不同方式实现。
线程状态
新建(newThread)、就绪(runnable)、运行(running)、死亡(dead)、堵塞(blocked)
线程就绪
start
sleep休眠超时
线程池的execute方法和submit方法有什么区别?
execute
接受一个runnable,然后返回为空。也就是说,它接受任务之后,就静悄悄异步去运行了。
区别就是submit方法,会返回一个Future对象。显然它是比execute方法多了一些内容的。
wait和sleep的区别
sleep
使用sleep()方法时,线程在指定的时间间隔后启动,除非它被中断。
sleep()是一个可以从任何上下文调用的静态方法。Thread.sleep()暂停当前线程并且不释放任何锁。
wait
对于wait(),唤醒过程有点复杂。我们可以通过调用正在等待的监视器上的notify()或notifyAll()方法来唤醒线程。
wait()是一个用于线程同步的实例方法
JVM
JVM内存模型
堆
所有线程共享
存放对象实例
垃圾回收的主要场所
新生代
老年代
栈
Java虚拟机栈
存放局部变量的数据信息
java虚拟机栈,比如说当执行main方法的时候就会有一个main线程,用来存放main方法中定义的局部变量
后进先出的原则
局部变量是线程安全
虚拟机栈也叫栈内存,是在线程创建时创建,它的生命期是跟随线程的生命期,线程结束栈内存也就释放,对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就 Over,所以不存在垃圾回收。
方法区
存储类信息、常量、静态常量和即时编译后的代码
可以运行的.class文件
线程共享,也称为永久代,用来存储常量,静态变量,类信息,即时编译器编译后的机器码,运行时常量池等数据。
垃圾回收在这个区域会比较少出现,这个区域内存回收的目的主要针对常量池的回收和类的卸载。
运行时常量池
用于存储编译器产生的字面量和符号引用。这部分内容在类被加载后,都会存储到方法区中的RCP。值得注意的是,运行时产生的新常量也可以被放入常量池中,比如 String 类中的 intern() 方法产生的常量。
本地方法栈
与VM Strack相似,虚拟机栈为JVM提供执行JAVA方法的服务,本地方法栈则为JVM提供使用native 方法的服务。
程序计数器
当前线程所执行的字节码的行号指示器
类加载机制
加载->验证->准备->解析->初始化->使用->卸载
双亲委派原则
父类加载 不重复加载
加载类的时候优先加载jdk的核心类库(lib目录),再去加载jdk的扩展类库(lib/ext目录),最后再加载应该程序的类。
分代回收
年轻代
Eden/s1/s2
永久代/元空间
晋升机制
根据存活时间
垃圾回收机制
标记清除
适用场景
对象存活比较多的时候适用
提前GC
碎片空间
扫描了两次
标记存活对象
清除没有标记的对象
标记复制
适合场景
存活对象少 比较高效
扫描了整个空间(标记存活对象并复制异动)
适合年轻代
需要空闲空间
需要复制移动对象
引用计数
没办法解决循环引用的问题
标记整理
什么时候回收
会在cpu空闲的时候自动进行回收
在堆内存存储满了之后
主动调用System.gc()后尝试进行回收
垃圾回收器
CMS
分代
年轻
edan
s1
s2
minor gc
通过阈值晋升
老年
major gc 等价于 full gc
永久
对cpu资源敏感
无法处理浮动垃圾
基于标记清除算法 大量空间碎片
G1
分区概念 弱化分代
标记整理算法 不会产生空间碎片 分配大对象不会提前full gc
可以设置预设停顿时间
充分利用cpu 多核条件下 缩短stw
收集步骤
初始标记 stw 从gc root 开始直接可达的对象
并发标记 gc root 对对象进行可达性分析 找出存活对象
可达性分析算法
最终标记
筛选回收
根据用户期待的gc停顿时间指定回收计划
回收模式
young gc
回收所有的eden s区
复制一些存活对象到old区s区
mixed gc
GC模式
区别
g1分区域 每个区域是有老年代概念的 但是收集器以整个区域为单位收集
g1回收后马上合并空闲内存 cms 在stw的时候做
内存区域设置
XX:G1HeapRegionSize
复制成活对象到一个区域 暂停所有线程
full gc
老年代写满
system。gc
持久代空间不足
STW
实战
性能调优
设置堆的最大最小值 -xms -xmx
调整老年和年轻代的比例
-XX:newSize设置绝对大小
防止年轻代堆收缩:老年代同理
主要看是否存在更多持久对象和临时对象
观察一段时间 看峰值老年代如何 不影响gc就加大年轻代
配置好的机器可以用 并发收集算法
每个线程默认会开启1M的堆栈 存放栈帧 调用参数 局部变量 太大了 500k够了
原则 就是减少gc stw
FullGC 内存泄露排查
jasvism
dump
监控配置 自动dump
oom种类
逃逸分析
可达性
虚拟机栈(栈帧中的本地变量表)中引用的对象方法区中类静态属性引用的对象方法区中常量变量引用的对象本地方法栈中JNI(即一般说的Native方法)引用的对象活跃线程的引用对象
JVM调优
OOM
内存泄露
线程死锁
锁争用
Java进程消耗CPU过高
JVM性能检测工具
Jconsole
Jprofiler
jvisualvm
MAT
help dump
生产机 dump
mat
jmap
-helpdump
CPU100%
topc -c
top -Hp pid
jstack
进制转换
cat
Spring
设计模式
单例
工厂
适配器
根据不同商家适配
责任链
继承 process 链路执行
源码
Bean
生命周期
扫描类
invokeBeanFactoryPostProcessors
封装beanDefinition对象 各种信息
放到map
遍历map
验证
能不能实例化 需要实例化么 根据信息来
是否单例等等
判断是不是factory bean
单例池 只是一个ConcurrentHashMap而已
正在创建的 容器
得到 class
推断构造方法
根据注入模型
默认
得到构造方法
反射 实例化这个对象
后置处理器合并beanDefinition
判断是否允许 循环依赖
提前暴露bean工厂对象
填充属性
自动注入
执行部分 aware 接口
继续执行部分 aware 接口 生命周期回调方法
完成代理AOP
beanProstprocessor 的前置方法
实例化为bean
放到单例池
销毁
作用域
单例(singleton)
多例(prototype)
Request
Session
循环依赖
情况
属性注入可以破解
构造器不行
三级缓存没自己 因二级之后去加载B了
三级缓存
去单例池拿
判断是不是正在被创建的
判断是否 支持循环依赖
二级缓存 放到 三级缓存
干掉二级缓存
GC
下次再来直接 三级缓存拿 缓存
缓存 存放
一级缓存 单例Bean
二级缓存 工厂 产生baen
产生bean 复杂
三级缓存 半成品
父子容器
事务实现原理
采用不同的连接器
用AOP 新建立了一个 链接
共享链接
ThreadLocal 当前事务
前提是 关闭AutoCommit
普通事务
获取数据库连接 Connection con = DriverManager.getConnection()开启事务con.setAutoCommit(true/false);执行数据操作(crud)提交事务/回滚事务 con.commit() / con.rollback()关闭连接 conn.close()
AOP
静态代理
实现类
动态代理
JDK动态代理
实现接口
java反射机制生成一个代理接口的匿名类
调用具体方法的时候调用invokeHandler
cjlib
asm字节码编辑技术动态创建类 基于classLoad装载
修改字节码生成子类去处理
IOC
依赖注入(dependency injection)
依赖查找(dependency lookup)
加载
生成一个class对象
文件格式验证
元数据验证
字节码验证
符号引用验证
准备
默认值
static会分配内存
解析
解析具体类的信息
引用等
初始化
父类没初始化 先初始化父类
使用
卸载
加载方式
main()
class。forName
ClassLoader。loadClass
类加载器
Appclass Loade
Extention ClassLoader
Bootstrap ClassLoader
可以避免重复加载
安全
遵守BSD协议,是一个高性能的key-value非关系型数据库。
性能
我们在碰到需要执行耗时特别久、且结果不频繁变动的SQL时,就特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使得请求能够迅速响应
并发
在大并发的情况下,所有的请求直接访问数据库,数据库会出现连接异常。这个时候,就需要使用Redis做一个缓冲操作,让请求先访问到Redis,而不是直接访问数据库。
缓存和数据库双写一致性问题缓存雪崩问题缓存击穿问题缓存的并发竞争问题
数据结构
String
string是redis最基本的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个value
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象
string类型是Redis最基本的数据类型,一个redis中字符串value最多可以是512M
Hash
Redis hash 是一个键值对集合
hash是一个string类型的field和value的映射表,hash特别适合用于存储对象
Set是string类型的无序无重复集合。它是通过HashTable实现实现的
好友/关注/粉丝/感兴趣的人集合;set类型提供了一些很实用的命令用于直接操作这些集合
a. sinter命令可以获得A和B两个用户的共同好友
b. sismember命令可以判断A是否是B的好友
c. scard命令可以获取好友数量
d. 关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合
zset
随机层数
只需要调整前后节点指针
不止比较score
还会比较value
成绩
ZREVRANGE key start stop [WITHSCORES] 返回有序集中指定区间内的成员,通过索引,分数从高到低ZREVRANGEBYSCORE key max min [WITHSCORES] 返回有序集中指定分数区间内的成员,分数从高到低排序ZREVRANK key member 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序ZSCORE key member 返回有序集中,成员的分数值
积分
排行榜
List
分页的坑
HyperLogLog
Geo
Pub/Sub
BitMap
底层
SDS
键值的底层都是SDS
AOF缓存区
记录本身长度 C需要遍历
修改字符减少内存重新分配
空间预支配
惰性空间释放
二进制安全
C只能保存文本数据 无法保存图片等二进制数据
sds是使用长度去判断
杜绝缓冲区溢出
兼容部分C字符串函数
链表
保存多个客户端的状态信息
列表订阅发布 慢查询 监视器
字典
数据库 哈希键
Hash表节点
hash冲突用单向链表解决
渐进式 rehash
会逐渐rehash 新的键值对全部放到新的hash表
每个字典带 两个hash表
一个平时用 一个rehash时候用
压缩列表
整数集合
常见命令
Keys
setnx
setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
exprie
高可用
持久化
内存数据库,为防止事故,将数据保存到磁盘中,甚至是远程云服务中
RDB(默认打开)
5分钟一次
RDB文件就是定期生成数据的快照文件
。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。
冷备
RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备
Amazon的S3云服务
国内可以是阿里云的ODPS分布式存储
恢复的时候比较快
RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能
一旦redis进程宕机,那么会丢失最近5分钟的数据。
RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。
快照文件生成时间久,消耗cpu
AOF持久化开启
通过保存Redis服务器所执行的写命令来记录数据库状态。
redis每接受一条命令就会追加日志文件将日志文件写入到OS cache缓存中通过fsync将OS cache中的数据写入AOF文件
配置
appendonly yes
AOF和RDB都开启了,redis重启的时候,也是优先通过AOF进行数据恢复的,因为aof数据比较完整
数据齐全
回复慢文件大
数据初始化
从节点发送命令主节点做bgsave同时开启buffer
数据同步机制
主从同步
指令流
offset
Redis的主从同步机制可以确保redis的master和slave之间的数据同步。
全量复制和增量复制
全量拷贝
slave第一次启动时,连接Master,发送PSYNC命令,格式为psync {runId} {offset}
当master接收到psync ? -1时,知道slave是要全量复制,就会将自己的runId和offset告知slave,回复命令fullresync {runId} {offset}。同时,master会执行bgsave命令来生成rdb文件,期间的所有写命令将被写入缓冲区。
master bgsave执行完毕,向slave发送rdb文件。rdb文件发送完毕后,开始向slave发送缓冲区中的写命令。
slave收到rdb文件,丢弃所有旧数据,开始载入rdb文件。
rdb文件同步结束之后,slave执行从master缓冲区发送过来的所以写命令。
此后 master 每执行一个写命令,就向slave发送相同的写命令。
增量拷贝
如果出现网络闪断或者命令丢失等异常情况时,当主从连接恢复后,由于从节点之前保存了自身已复制的偏移量和主节点的运行ID。因此会把它们当作psync参数发送给主节点,要求进行部分复制操作,格式为psync {runId} {offset}。
主节点接到psync命令后首先核对参数runId是否与自身一致,如果一致,说明之前复制的是当前主节点;之后根据参数offset在自身复制积压缓冲区查找,如果偏移量之后的数据存在缓冲区中,则对从节点发送+continue响应,表示可以进行部分复制;否则进行全量复制。
主节点根据偏移量把复制积压缓冲区里的数据发送给从节点,保证主从复制进入正常状态。
问题
拷贝超时
如果总时间超过repl-timeout所配置的值(默认60秒),从节点将放弃接受rdb文件并清理已经下载的临时文件,导致全量复制失败
积压缓冲区拷贝溢出
如果主节点创建和传输RDB的时间过长,对于高流量写入场景非常容易造成主节点复制客户端缓冲区溢出。
slave全量同步的响应问题
slave节点接收完主节点传送来的全部数据后会清空自身旧数据,执行flash old data,然后加载rdb文件。对于较大的rdb文件,这一步操作依然比较耗时
快照同步
RDB
缓冲区
哨兵
集群监控
消息通知
故障转移
配置中心
脑裂
集群
多主
横向扩容
分片
缓存雪崩
设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩
针对很多Key
集群部署
双缓存。我们有两个缓存,缓存A和缓存B。缓存A的失效时间为20分钟,缓存B不设失效时间,自己做缓存预热操作。然后细分以下几个小点:a. 从缓存A读数据库,有则直接返回;b. A 没有数据,直接从B读数据,直接返回,并且异步启动一个更新线程;c. 更新线程同时更新缓存A和缓存B。
加随机值
使用互斥锁,但是该方案吞吐量明显下降了;
缓存击穿
key可能会在某些时间失效,被超高并发地访问
针对一个热点Key
互斥锁
互斥锁是一种简单的加锁的方法来控制对共享资源的访问
1. 原子性:把一个互斥量锁定为一个原子操作,这意味着操作系统(或pthread函数库)保证了如果一个线程锁定了一个互斥量,没有其他线程在同一时间可以成功锁定这个互斥量;
2. 唯一性:如果一个线程锁定了一个互斥量,在它解除锁定之前,没有其他线程可以锁定这个互斥量;
3. 非繁忙等待:如果一个线程已经锁定了一个互斥量,第二个线程又试图去锁定这个互斥量,则第二个线程将被挂起(不占用任何cpu资源),直到第一个线程解除对这个互斥量的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥量。
热点数据不失效
缓存穿透
拿一个不存在的id去海量查询数据库,数据库会因为巨大的压力而宕机
布隆过滤器
BloomFilter 类似于一个hbase set 用来判断某个元素(key)是否存在于某个集合中。
缓存空值
利用互斥锁,缓存失效的时候,先去获得锁,得到锁了,再去请求数据库,没得到锁,则休眠一段时间重试;
双写一致性
延时双删
先淘汰缓存;
再写数据库
休眠1秒,再次淘汰缓存。
目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
并发竞争
这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做Set操作即可,比较简单。如果对这个Key操作要求顺序假设有一个Key1,系统A需要将Key1设置为ValueA,系统B需要将Key1设置为ValueB,系统C需要将Key1设置为ValueC。期望按照Key1的Value值按照 ValueA→ValueB→ValueC的顺序变化。这种时候我们在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下:系统A Key 1 {ValueA 3:00}系统B Key 1 {ValueB 3:05}系统C Key 1 {ValueC 3:10}那么,假设这会系统B先抢到锁,将Key1设置为{ValueB 3:05}。接下来系统A抢到锁,发现自己的ValueA的时间戳早于缓存中的时间戳,那就不做Set操作了。以此类推。
过期策略
定时删除
消耗内存
创建一个定时器
惰性删除
可能存在大量key
不再是Redis去主动删除,而是在客户端要获取某个key的时候,Redis会先去检测一下这个key是否已经过期
定期删除
检查 删除 但是是随机的
Redis默认每隔100ms就随机抽取一些设置了过期时间的key,检测这些key是否过期,如果过期了就将其删掉。
淘汰机制
LUR
最少使用
数据的备份
Redis支持数据的备份,即master-slave模式的数据备份。
主从+哨兵+cluster
ecache+Hystrix+降级+熔断+隔离
备份
创建 redis 备份文件也可以使用命令 BGSAVE,该命令在后台执行。
恢复数据
如果需要恢复数据,只需将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可。获取 redis 目录可以使用 CONFIG 命令
限流
setnx ex
限流的主要目的就是为了在单位时间内,有且仅有N数量的请求能够访问我的代码程序
setnx的指令,在CAS(Compare and swap)的操作的时候,同时给指定的key设置了过期实践(expire)
窗口滑动
将请求打造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID生成,而score可以用当前时间戳表示,因为score我们可以用来计算当前时间戳之内有多少的请求数量。而zset数据结构也提供了range方法让我们可以很轻易的获取到2个时间戳内有多少请求
zset会越来越大
令牌
定时push
然后leftpop
空轮训
blpop
空连接异常
漏桶 funnel
输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了
make_space 灌水之前调用漏水 腾出空间 取决于流水速率
原子性有问题
Redis-Cell
redis cell
该模块提供漏斗算法,并提供原子的限流指令。
热key,大key
大Key
Redis使用过程中经常会有各种大key的情况, 比如单个简单的key存储的value很大。
由于redis是单线程运行的,如果一次操作的value很大会对整个redis的响应时间造成负面影响,导致IO网络拥
bigkey命令 找到干掉
Redis 4.0引入了memory usage命令和lazyfree机制
1、redis-rdb-tools工具。redis实例上执行bgsave,然后对dump出来的rdb文件进行分析,找到其中的大KEY。2、redis-cli --bigkeys命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key。3、自定义的扫描脚本,以Python脚本居多,方法与redis-cli --bigkeys类似。4、debug object key命令。可以查看某个key序列化后的长度,每次只能查找单个key的信息。官方不推荐。
热点key
多级缓存
(1)利用二级缓存
比如利用ehcache,或者一个HashMap都可以。在你发现热key以后,把热key加载到系统的JVM中。针对这种热key请求,会直接从jvm中取,而不会走到redis层。
现在假设,你的应用层有50台机器,OK,你也有jvm缓存了。这十万个请求平均分散开来,每个机器有2000个请求,会从JVM中取到value值,然后返回数据。避免了十万个请求怼到同一台redis上的情形。
将这些数据缓存到了服务端,那么还有一个问题。就是如何保证Redis和服务端热点Key的数据一致性。我这里想到的解决方案是利用Redis自带的消息通知机制,对于热点Key客户端建立一个监听,当热点Key有更新操作的时候,客户端也随之更新。
备份热点Key:
即将热点Key+随机数,随机分配至Redis其他节点中。这样访问热点key的时候就不会全部命中到一台机器上了。
扩展
跳跃表
漏桶
scan
先拿setnx来争抢锁,抢到之后,再用expire给锁加一个过期时间防止锁忘记了释放。
在setnx之后执行expire之前进程意外crash或者要重启维护
可以同时把setnx和expire合成一条指令来用的!
同步机制
Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将RDB文件全量同步到复制节点,复制节点接受完成后将RDB镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。后续的增量数据通过AOF日志同步即可,有点类似数据库的binlog。
为什么那么快
纯内存操作
单线程操作,避免了频繁的上下文切换
采用了非阻塞I/O多路复用机制
多路IO复用
Redis-client在操作的时候,会产生具有不同事件类型的Socket。在服务端,有一段I/O多路复用程序,将其置入队列之中。然后文件事件分派器依次去队列中取,转发到不同的事件处理器中。
read得读到很多才返回 为0会卡在那 直到新数据来或者链接关闭
写不会阻塞除非缓冲区满了
非阻塞的IO 提供了一个选项 no_blocking 读写都不会阻塞 读多少写多少 取决于内核的套接字字节分配
非阻塞IO也有问题 线程要读数据 读了一点就返回了 线程什么时候知道继续读?写一样
一般都是select解决 但是性能低 现在都是epoll
更新缓存
集群模式
主从(master-slave)
全量同步:slave会发送一个PSYNC命令给master,master接收到该命令后,会立即进行持久化操作,通过命令bgsava生成一个RDB快照文件,持久化期间,如果客户端仍在写入数据,这部分数据会被保存在内存缓冲区(repl buffer)中,持久化完成以后,master会将RDB文件发送给slave,slave 将数据加载到内存中,然后master会将缓冲区的命令发送给slave节点。
当slave发现自己的master节点 挂了(fail)将自己记录的集群 currentEpoch 加1,并广播 FAILOVER_AUTH_REQUEST 信息其他节点接收到消息后,由主节点回复 FAILOVER_AUTH_ACK 信息,每个master节点只能发送一次对应的slave节点,收集并统计 FAILOVER_AUTH_ACK 信息。当收集到超过半数的ACK信息后,该节点自动升级为master节点。master(新)会播Pong消息通知其他集群节点,”我已经成功上位,你们可以歇歇了,赶紧更新你们的配置“。
第1步结束后,第2两步并不是马上执行,中间会有一个延迟时间,每个slave节点的延迟时间不同。计算规则:500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000msSLAVE_RANK:表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新。这种方式下,持有最新数据的slave将会首先发起选举(理论上)。
RedisCluster
redis作者自己提供的redis集群解决方案。
一个节点发现某个节点失联了 (PFail),它会将这条信息向整个集群广播,其它节点也就可以收到这点失联信息。 如果一个节点收到了某个节点失联的数量 (PFail Count) 已经达到了集群的大多数,就可以标记该节点为确定下线状态 (Fail),然后向整个集群广播,强迫其它节点也接收该节点已经下线的事实,并立即对该失联节点进行主从切换。
edis-cluster集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点A1都宕机了,那么该集群的A和A1就无法再提供服务了,部分数据无法保存到redis了,集群不可用。
Redis集群预分好16384个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中。
MySQL
分库分表
唯一主键
事务隔离级别
数据库事务就是保证一组数据操作要么全部成功,要么全部失败。在 MySQL 中,事务是在引擎层实现的。
读未提交
没视图概念 都是返回最新的
如果用这种隔离级别,事务执行的时候会读到其他未提交事务的数据,我们称为脏读。
读已提交
不同的read view
如果用这种隔离级别,事务执行的时候会读到其他已提交事务的数据,我们称为不可重复读
可重复度
用一个read view
在同一个事务里,SELECT 语句获得的结果是基于事务开始时间点的状态,同一个事务中 SELECT 语句得到的结果是一样的,但是会有幻读现象。
串行化
在该事务级别下,事务都是串行顺序执行的,避免了脏读,不可重复读,幻读问题。
索引
数据库索引 ,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。
一是增加了数据库的存储空间,二是在插入和修改
在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引
B树/B+树
B树
B树是一棵多叉查找树树,一个节点可以拥有两个以上的节点。
B+树
B+树与B树的区别
B树的非叶子节点上也有完整的行数据,B+树只在叶子节点上有数据,非叶子节点上只有键,优点是内存中存放的键数量更多,查找更快。
B+树的叶子节点上包含所有的键,B树的叶子节点上不一定有所有的键。
B+树的叶子节点通过链表进行连接,范围查询更加快速。
B树和二叉树的对比
相比于二叉树而言,B树最大的优点在于中间层可以存放于内存中,且相同数据量的情况下,B树的高度低于二叉树,可以减少查找的过程,从而加快存储速度
hash索引
使hash索引的查找速度很快,在使用时却也不一定比B+Tree更好。
hash索引无法处理范围查询,范围查询速度较慢;
hash索引无法避免排序运算;
hash索引不能利用部分索引键查询,组合索引的场景下,hash索引需要将多个索引键进行合并计算hash值,无法单独利用;
hash索引不可避免地存在表扫描地情况,即使通过hash定位到数据,仍然需要对原始数据进行对比,才可以查找到正确地数据;
hash索引在大量hash值相等地情况下,性能并不一定比B-Tree索引高。
索引分类
普通索引
最基本的索引类型,没有唯一性之类的限制。
主键索引
数据库表经常有一列或列组合,其值唯一标识表中的每一行。该列称为表的主键。
。该索引要求主键中的每个值都唯一
使用主键索引时,它还允许对数据的快速访问。
唯一索引
唯一索引是不允许其中任何两行具有相同索引值的索引。
聚集索引
在聚集索引中,表中行的物理顺序与键值的逻辑(索引)顺序相同。一个表只能包含一个聚集索引。
非聚集索引
多扫描一次
减少回表
也称非聚集索引。在非聚集索引中,数据库表中记录的物理顺序与索引顺序可以不相同。一个表中只能有一个聚集索引,但表中的每一列都可以有自己的非聚集索引。
联合索引
联合索引是由多个字段组成的索引。其中,只有第一个字段是有序的,其他字段仅在前面字段相等的情况下有序,但从整体的角度而言,是无序的。
索引维护
页满了 页分裂 页利用率下降
数据删除 页合并
自增 只追加可以不考虑 也分页
索引长度
索引优化
在创建表的时候我们使用sql语句,Create table tableName () engine=myisam|innodb;
最左前缀原则
应用于联合索引的非常重要的原则,即查询时,会按照联合索引建立的顺序,从左到右进行匹配。
= 和 in 可以乱序,比如 a = 3 and b = 4 and c = 5 建立 (a,b,c)索引可以任意顺序。
如果建立的索引顺序是 (a,b)那么直接采用 where b = 5 这种查询条件是无法利用到索引的,这一条最能体现最左匹配的特性。
回表
在MySQL中,会自动为主键创建主键索引,同时,主键索引上包含所有的数据。而对于其他的所有非主键索引,非主键索引中只包含索引键和主键数据。因此在非主键索引上查询到主键后,需要通过该主键在主键索引上查找数据,这个过程称为“回表”。
覆盖索引
当select语句查询的内容,在非主键索引上就已经全部包含,不需要再进入主键索引查找时,称为“覆盖索引”。
索引下推
索引查询时时常带有where语句,当多个条件并列,且条件在索引数据中包含时,MySQL会使用“索引下推”在索引上进行匹配,过滤以减少数据,加快查询速度。
不需要多个回表 一边遍历 一边判断
索引空间问题
hash
导致索引失效
计算,如:+、-、*、/、!=、<>、is null、is not null、or
函数,如:sum()、round()等等
手动/自动类型转换,如:id = \"1\",本来是数字,给写成字符串了
索引不要放在范围查询右边
比如复合索引:a->b->c,当 where a=\"\" and b>10 and 3=\"\",这时候只能用到 a 和 b,c 用不到索引,因为在范围之后索引都失效(和 B+树结构有关)
索引更新
非唯一性索引
InnoDB会进行change buffering操作。将更改排入队列,之后再在后台将其合并到索引中。甚至,为了后续物理更新更加高效,会将变更进行合并。
这种特性不需要手动开启,而是默认开启的。在MySQL5.1版本,change buffering操作仅仅适用于insert。而在MySQL5.5版本之后,change buffering操作则扩展到update和delete里
更新操作来了 如果数据页不在内存 就缓存下来 下次来了 更新 在就直接更新
唯一索引 需要判断 所以 用不到change buffer
innodb的处理流程
记录在页内存
唯一索引 判断没冲突插入
普通索引 插入
记录不再页中
数据页读入内存 判断 插入
change buffer
数据读是随机IO 成本高
机械硬盘 change buffer 收益大 写多读少 marge
MVCC
多版本并发控制
版本链 在聚集索引中 有两个隐藏列 trx_id roll_pointer
直接读取最新版本
加锁
一致性视图(read-view)
当事务要查询某个记录的数据时,实际上就是拿该记录的事务ID(包括历史版本的事务ID)和这个一致性视图进行比较,直到某个版本的数据是可见的为止。
查询过程
读取的记录的事务ID小于低水位,说明这个版本的数据在开启本事务前已经提交,是可见的,直接返回这个数据
读取的记录的事务ID大于高水位,说明这个版本的数据在开启本事务后提交的,不可见,从记录中取出 DBROLLPTR 指向的记录并读取其事务 ID,开始下一轮的判断
读取的记录的事务ID介于低水位和高水位中间,此时判断事务ID是否在一致性视图的事务数组中:1,如果不在,说明这个版本的数据在开启本事务前已经提交,是可见的,直接返回这个数据2,如果在,说明这个版本的数据是由开启事务后的其他活跃事务提交的,对本事务是不可见的,因此需要从记录中取出 DBROLLPTR 指向的记录并读取其事务 ID,开始下一轮的判断
四种隔离级别,只有「读提交」和「可重复度」两个隔离级别能够使用 MVCC,因此也只有这两个隔离级别会创建一致性视图(read-view)。
每次读取前生成一个
第一次生成一个
锁
全局锁
全库逻辑备份
表锁
lock table read/write
MDL(metadata lock)
MySQL5.5引入 自动添加 读锁不互斥 写锁互斥
多个事务之前操作,如果查询的时候修改字段容易让线程池饱满
MySQL的information_schema 库的 innodb_trx 表 找到对应长事务 kill掉
alter table里面设定等待时间
Myisam是不支持表锁的
行锁
需要的时候才加上 并不是马上释放 等事务结束才释放 两阶段锁协议
超时时间
innodb_lock_wait_timeout
默认是50s太久 但是如果设置太短会误判 一般采用死锁监测
死锁机制 事务回滚
innodb_deadlock_detect = on
热点行
死锁消耗CPU
临时关闭
关掉死锁会出现大量重试
控制并发度
更多的机器 开启比较少的线程 消耗就少了
间隙锁
读写锁
读
lock in share mode
for update
写
innodb如何加锁
log
undo log
回滚 mvcc
redo log
物理日志 内存操作记录
sync_binlog 可以优化日志写入时机
binlog
组提交机制,可以大幅度降低磁盘的IOPS消耗。
两段式提交 redo 准备 binglog 提交
count1 *
mvcc影响
主备延迟
强制走主
join
驱动表
id用完
bigint
row_id 没设置主键的时候
thread_id
mysql io性能瓶颈
设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。将sync_binlog 设置为大于1的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢binlog日志。将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。
show processlist
查看空闲忙碌链接
wait_timeout
客户端空闲时间
定时断开链接
mysql_reset_connection 恢复链接状态
innodb_flush_log_at_trx_commit
redolog事务持久化
sync_binlog
binlog事务持久化
真实故障
数据库挂了 show processlist 一千个查询在等待 有个超长sql kill 但是不会引起flush table 周末 优化脚本 analyze 会导致 MySQL 监测到对应的table 做了修改 必须flush close reopen 就不会释放表的占用了
数据库的三大范式
第一范式(1NF):数据表中的每一列(字段),必须是不可拆分的最小单元,也就是确保每一列的原子性。
第二范式(2NF):满足1NF后要求表中的所有列,每一行的数据只能与其中一列相关,即一行数据只做一件事。
第三范式(3NF):满足2NF后,要求:表中的每一列都要与主键直接相关,而不是间接相关(表中的每一列只能依赖于主键)。
数据库的五大约束
1.主键约束(Primay Key Coustraint) 唯一性,非空性;
2.唯一约束 (Unique Counstraint)唯一性,可以空,但只能有一个;
3.默认约束 (Default Counstraint) 该数据的默认值;
4.外键约束 (Foreign Key Counstraint) 需要建立两表间的关系;
5.非空约束(Not Null Counstraint):设置非空约束,该字段不能为空。
主键 超键 候选键 外键
超键
在关系中能唯一标识元组的属性集称为关系模式的超键。
除此之外我们还可以把它跟其他属性组合起来,组成超键
主键
用户选择的候选键作为该元组的唯一标识,那么它就为主键。
候选键
不含多余属性的超键为候选键。
候选键是超键的子集
外键
外键是相对于主键的
删除
drop
直接删掉表
删除整个表(结构和数据)
delete
删除表中数据,可以加 where 字句。
DELETE 可以是 table 和 view
DELETE 语句每次删除一行,并在事务日志中为所删除的每行记录一项。
truncate
删除表中数据,再插入时自增长 id 又从 1 开始
TRUNCATE 只能对 TABLE
truncate table 在功能上与不带 WHERE 子句的 DELETE 语句相同
TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少。
TRUNCATE TABLE 通过释放存储表数据所用的数据页来删除数据,并且只在事务日志中记录页的释放
drop > truncate > delete
慢 SQL
定位慢 SQL
通过慢查询日志
MySQL的慢查询日志用来记录在MySQL中响应时间超过参数 long_query_time(单位秒,默认值 10)设置的值并且扫描记录数不小于 min_examined_row_limit(默认值 0)的语句,
开启慢查询日志
由参数 slow_query_log 决定是否开启
设置慢查询阀值
确定慢查询日志路径
慢查询日志的路径默认是 MySQL的数据目录
show global variables like \"datadir\";
确定慢查询日志的文件名
mysql> show global variables like \"slow_query_log_file\";
根据上面的查询结果,可以直接查看 /data/mysql/data/3306/mysql-slow.log 文件获取已经执行完的慢查询
[root@juran ~]# tail -n5 /data/mysql/data/3306/mysql-slow.log
使用 explain 分析慢查询
Explain可以获取MySQL中SQL语句的执行计划
优化流程
预发跑sql explain
排除 缓存 sql nocache
应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
)应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,
Where 子句替换 HAVING 子句 因为 HAVING 只会在检索出所有记录之后才对结果集进行过滤
看一下行数对不对 不对可以用analyze table t 矫正
添加索引 索引不一定是最优的 force index 强制走索引 不建议用
减少 select * 的使用
使用覆盖索引
select 查询字段和 where 中使用的索引字段一致。
like 模糊搜索
失效情况
like \"%张三%\" like \"%张三\"
使用复合索引,即 like 字段是 select 的查询字段,如:select name from table where name like \"%张三%\"
使用 like \"张三%\"
order by 优化
order by 进行排序时,如果没有使用索引进行排序,会出现 filesort 文件内排序,这种情况在数据量大或者并发高的时候,会有性能问题,需要优化
order by 排序算法
双路排序
Mysql4.1 之前是使用双路排序,字面的意思就是两次扫描磁盘,最终得到数据,读取行指针和 ORDER BY 列,对他们进行排序,然后扫描已经排好序的列表,按照列表中的值重新从列表中读取对数据输出。也就是从磁盘读取排序字段,在 buffer 进行排序,再从磁盘读取其他字段。。
文件的磁盘 IO 非常耗时的,所以在 Mysql4.1 之后,出现了第二种算法,就是单路排序。
单路排序
从磁盘读取查询需要的所有列,按照 orderby 列在 buffer 对它们进行排序,然后扫描排序后的列表进行输出, 它的效率更快一些,避免了第二次读取数据,并且把随机 IO 变成顺序 IO,但是它会使用更多的空间, 因为它把每一行都保存在内存中了。
联合索引 不能无限建 高频场景
白话来说,第一个字段相同,才会去比较字段
最左前缀原则 按照索引定义的字段顺序写sql
如果建立的是复合索引,索引的顺序要按照建立时的顺序,即从左到右,如:a->b->c(和 B+树的数据结构有关)
a->c:a 有效,c 无效b->c:b、c 都无效c:c 无效
5.6之后 索引下推 减少回表次数
给字符串加索引
前缀索引
倒序存储
数据库的flush的时机
redo log满了 修改checkpoint flush到磁盘
系统内存不足淘汰数据页
buffer pool
要知道磁盘的io能力 设置innodb_io_capacity 设置为磁盘的IOPS fio测试
innodb_io_capacity设置低了 会让innoDB错误估算系统能力 导致脏页累积
系统空闲的时候 找间隙刷脏页
MySQL正常关闭,会把内存脏页flush到磁盘
innodb刷盘速度
脏页比例
redolog 写盘速度
innodb_flush_neighbors 0
机械磁盘的随机io不太行 减少随机io性能大幅提升 设置为 1最好
现在都是ssd了 设置为0就够了 8.0之后默认是0
索引字段不要做函数操作,会破坏索引值的有序性,优化器会放弃走树结构
如果触发隐式转换 那也会走cast函数 会放弃走索引
字符集不同可能走不上索引
convert 也是函数所以走不上
binlog有有几种录入格式
statement
statement模式下,每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。
row
row级别下,不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。
mixed
mixed,一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row
MySQL存储引擎MyISAM与InnoDB区别
InnoDB
Innodb引擎:Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。
InnoDB索引是聚簇索引,MyISAM索引是非聚簇索引。
InnoDB的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
InnoDB非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。
MyIASM
MyIASM引擎(原本Mysql的默认引擎):不提供事务的支持,也不支持行级锁和外键。
MyISAM索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
zookeeper分布式服务框架是apache的一个子项目,它主要是用来解决分布式应用中常遇到的数据管理问题,比如同意命名服务,状态同步服务,集群管理,分布式应用配置项的管理等。
选举机制
当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的 Server都恢复到一个正确的状态。
fast paxos(系统默认)
某Server首先向所有Server提议自己要成为leader
basic paxos
过半机制
要使Leader获得多数Server的支持,则Server总数必须是奇数2n+1,且存活的Server的数目不得少于n+1.
获胜的Server获得n/2 + 1的Server票数
预提交 ack 2pc
ZAB协议?
ZK的核心是原子广播,这个机制保证了各个server之间的同步。实现这个机制的协议叫做Zab协议。
Zab协议有两种模式,它们分 别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和 leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。
zk节点宕机如何处理?
Zookeeper 本身也是集群,推荐配置不少于 3 个服务器。Zookeeper 自身也要保证当一个节点宕机时,其他节点会继续提供服务。如果是一个 Follower 宕机,还有 2 台服务器提供访问,因为 Zookeeper 上的数据是有多个副本的,数据并不会丢失;如果是一个 Leader 宕机,Zookeeper 会选举出新的 Leader。ZK 集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在 ZK节点挂得太多,只剩一半或不到一半节点能工作,集群才失效。
如何实现分布式一致性
ZooKeeper 保证 分布式系统数据一致性的核心算法就是 ZAB 协议(ZooKeeper Atomic Broadcast,原子消息广播协议)。
zookeeper 提供了什么?
1、文件系统
zookeeper 提供一个类似 unix 文件系统目录的多层级节点命名空间(节点称为 znode)。与文件系统不同的是,这些节点都可以设置关联的数据,而文件系统中只有文件节点可以存放数据而目录节点不行。zookeeper 为了保证高吞吐和低延迟,在内存中维护了这个树状的目录结构,这种特性使得zookeeper 不能用于存放大量的数据,每个节点的存放数据上限为 1M。
2、通知机制
client 端会对某个 znode 建立一个 watcher 事件,当该 znode 发生变化时,zk会主动通知 watch 这个 znode 的 client,然后 client 根据 znode 的变化来做出业务上的改变等。
watcher 的特点:
• 轻量级:一个 callback 函数。• 异步性:不会 block 正常的读写请求。• 主动推送:Watch 被触发时,由 Zookeeper 服务端主动将更新推送给客户端。• 一次性:数据变化时,Watch 只会被触发一次。如果客户端想得到后续更新的通知,必须要在 Watch 被触发后重新注册一个 Watch。• 仅通知:仅通知变更类型,不附带变更后的结果。• 顺序性:如果多个更新触发了多个 Watch,那 Watch 被触发的顺序与更新顺序一致
由于 watcher 是一次性的,所以需要自己去实现永久 watch
如果被 watch 的节点频繁更新,会出现“丢数据”的情况
watcher 数量过多会导致性能下降
应用场景
1、名字服务
命名服务是指通过指定的名字来获取资源或者服务的地址,利用 zk 创建一个全局的路径,即是唯一的路径,这个路径就可以作为一个名字,指向集群中的集群,提供的服务的地址,或者一个远程的对象等等。
2、配置管理
程序分布式的部署在不同的机器上,将程序的配置信息放在 zk 的 znode 下,当有配置发生改变时,也就是 znode 发生变化时,可以通过改变 zk 中某个目录节点的内容,利用 watcher 通知给各个客户端,从而更改配置。
3、集群管理
所谓集群管理无在乎两点:是否有机器退出和加入、选举 master。
4、分布式锁
有了 zookeeper 的一致性文件系统,锁的问题变得容易。锁服务可以分为两类,一个是保持独占,另一个是控制时序。 对于第一类,我们将 zookeeper 上的一个 znode 看作是一把锁,通过 createznode 的方式来实现。所有客户端都去创建 /distribute_lock 节点,最终成功创建的那个客户端也即拥有了这把锁。用完删除掉自己创建的 distribute_lock 节点就释放出锁。 对于第二类,/distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选 master 一样,编号最小的获得锁,用完删除,依次方便。
5、队列管理
1、同步队列,当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。
第一类,在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目
2、队列按照 FIFO 方式进行入队和出队操作。
第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列按编号。在特定的目录下创建 PERSISTENT_SEQUENTIAL节点,创建成功时 Watcher 通知等待的队列,队列删除序列号最小的节点用以消费。此场景下 Zookeeper 的 znode 用于消息存储,znode 存储的数据就是消息队列中的消息内容,SEQUENTIAL 序列号就是消息的编号,按序取出即可。由于创建的节点是持久化的,所以不必担心队列消息的丢失问题。
6、消息订阅
zk 是如何保证事物的顺序一致性
采用了递增的事务 Id 来标识,所有的 proposal(提议)都在被提出的时候加上了 zxid
zxid用来标识 leader 是否发生改变,如果有新的 leader产生出来,epoch 会自增,低 32 位用来递增计数。当新产生 proposal 的时候,会依据数据库的两阶段过程,首先会向其他的 server 发出事务执行请求,如果超过半数的机器都能执行并且能够成功,那么就会开始执行。
4 zk 集群下 server 工作状态
LOOKING:当前 Server 不知道 leader 是谁,正在搜寻
LEADING:当前 server 角色为 leader
FOLLOWING:当前 server 角色为 follower
OBSERVING:当前 server 角色为 observer
zk 宕机后的同步流程
. Leader 等待 Follower 和 Observer 连接;
Follower 连接 leader,将最大的 zxid 发送给 leader;
Leader 根据 follower 的 zxid 确定同步点;
完成同步后通知 follower 已经成为 uptodate 状态;
. Follower 收到 uptodate 消息后,又可以重新接受 client 的请求进行服务了。
zk 的 session 机制
zookeeper中session意味着一个物理连接,客户端连接服务器成功之后,会发送一个连接型请求,此时就会有session 产生。
session
SessionID:会话的唯一标识,由ZK来分配
TimeOut:会话超时时间。
Expiration Time:TimeOut是一个相对时间,而Expiration Time则是在时间轴上的一个绝对过期时间。
分桶机制
ZK服务端会维护着一个个\"桶\
Dubbo
Netty
零拷贝
bio
阻塞IO 读写都阻塞
问题 带宽 资源等
每个请求过来 开一个线程阻塞
nio
不阻塞来着不拒
但是可能等待时间太久 响应延迟大了 太短了 会重试
IO多路复用
便捷的通知机制
程序注册一组socket文件描述符给操作系统,表示“我要监视这些fd是否有IO事件发生,有了就告诉程序处理”
遍历 判断事件是否可达 然后继续
做了优化
有转态 会创建 文件描述符指向的表 监听增删改查
通道 buffer 多路复用
监听事件
aio
架构设计思路
执行链路
初始化channel
注册 channel到selector
任务队列
轮训accept事件 处理这些简历 channel的链接
注册 channel到selector 接收方
轮训写 事件 开线程去处理
线程组
boss
监听端口所有准备就绪的时间
work
监听准备工作
调用链路
应用层一般有三种类型的协议形式
固定长度形式
特殊字符隔断形式
header+body 形式(dubbo)
头部是固定长度的,然后头部里面会填写 body 的长度, body 是不固定长度的,这样伸缩性就比较好了,可以先解析头部,然后根据头部得到 body 的 len 然后解析 body。
Dubbo 协议
客户端发起调用,实际调用的是代理类,代理类最终调用的是 Client (默认Netty),需要构造好协议头,然后将 Java 的对象序列化生成协议体,然后网络调用传输。服务端的 NettyServer 接到这个请求之后,分发给业务线程池,由业务线程调用具体的实现方法。
服务暴露过程
IOC 启动加载dubbo配置的标签
服务的暴露起始于 Spring IOC 容器刷新完毕之后,会根据配置参数组装成 URL, 然后根据 URL 的参数来进行本地或者远程调用。
解析标签 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
dubbo 执行远程引用
会通过 proxyFactory.getInvoker,利用 javassist 来进行动态代理,封装真的实现类,然后再通过 URL 参数选择对应的协议来进行 protocol.export,默认是 Dubbo 协议。
在第一次暴露的时候会调用 createServer 来创建 Server,默认是 NettyServer。
然后将 export 得到的 exporter 存入一个 Map 中,供之后的远程调用查找,然后会向注册中心注册提供者的信息。
服务暴露顺序
容器初始化
服务本地暴露
服务网络暴露
服务注册中心暴露
服务的暴露起始于 Spring IOC 容器刷新完毕之后,会根据配置参数组装成 URL, 然后根据 URL 的参数来进行本地或者远程调用。会通过 proxyFactory.getInvoker,利用 javassist 来进行动态代理,封装真的实现类,然后再通过 URL 参数选择对应的协议来进行 protocol.export,默认是 Dubbo 协议。在第一次暴露的时候会调用 createServer 来创建 Server,默认是 NettyServer。然后将 export 得到的 exporter 存入一个 Map 中,供之后的远程调用查找,然后会向注册中心注册提供者的信息。
服务引用
factoryBean ->getObject ->get
init
信息检查
创建代理对象 createProxy
远程引用 获取到zk 获取到信息 订阅
创建 netty客户端
返回invoke
注册到注册表进去
成功
服务的引入时机有两种,第一种是饿汉式,第二种是懒汉式。饿汉式就是加载完毕就会引入,懒汉式是只有当这个服务被注入到其他类中时启动引入流程,默认是懒汉式。会先根据配置参数组装成 URL ,一般而言我们都会配置的注册中心,所以会构建 RegistryDirectory向注册中心注册消费者的信息,并且订阅提供者、配置、路由等节点。得知提供者的信息之后会进入 Dubbo 协议的引入,会创建 Invoker ,期间会包含 NettyClient,来进行远程通信,最后通过 Cluster 来包装 Invoker,默认是 FailoverCluster,最终返回代理类
SPI
java spi
Java 没ioc aop
Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。
具体的spi kv形式
为什么用 Javassist,很简单,就是快,且字节码生成方便。ASM 比 Javassist 更快,但是没有快一个数量级,而Javassist 只需用字符串拼接就可以生成字节码,而 ASM 需要手工生成,成本较高,比较麻烦。
SPI 是 Service Provider Interface,主要用于框架中,框架定义好接口,不同的使用者有不同的需求,因此需要有不同的实现,而 SPI 就通过定义一个特定的位置,Java SPI 约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。
Dubbo 就自己实现了一个 SPI,给每个实现类配了个名字,通过名字去文件里面找到对应的实现类全限定名然后加载实例化,按需加载。
容错机制
failover
直接切换
failfast
快速失败
快速失败,当请求失败之后快速返回异常结果,不做任何重试。改容错机制会对请求做负载均衡,通常使用在非幂等性的接口上。该机制受网络抖动的影响较大。
failsafe
当出现异常的时候直接忽略异常,会对请求做负载均衡。通常的使用场景是不关心调用是否成功,并且不想抛出异常影响外部调用,如某些不重要的日志同步,及时出现异常也无所谓。
failback
请求失败之后,会自动记录在失败队列中,并由一个定时线程池定时重试,适用一些异步或者最终以执行的请求。会对请求做负载均衡。
forking cluster
\t同时调用多个相同的服务,只要其中一个返回,就立即返回结果。用户可以配置forks=\"2\" 来设置最大并行数。通常用在对接口实时性较高的场景,但是也会浪费更多的资源。
broadcast cluster
广播调用所有可用服务,任意一个节点报错则报错。由于是广播,因此请求不需要做负载均衡。
整合 hystrix
失败回调
返回默认
降级
return null
失败返回空
负载均衡
随机 加权
轮训
最少活跃数
hash一致
选举算法
注册中心
协议
dubbo
默认 nio 单一长连接
二进制系列化 小数据量 100k
数据量中等 不适合文件传输
memcached
redis
webService
http
RPC
Consumer\t需要调用远程服务的服务消费方Registry\t注册中心Provider\t服务提供方Container\t服务运行的容器Monitor\t监控中心
Provider 启动然后向注册中心注册自己所能提供的服务。
服务消费者 Consumer 启动向注册中心订阅自己所需的服务。然后注册中心将提供者元信息通知给 Consumer, 之后 Consumer 因为已经从注册中心获取提供者的地址,因此可以通过负载均衡选择一个 Provider 直接调用 。
服务提供方元数据变更的话注册中心会把变更推送给服务消费者。
服务提供者和消费者都会在内存中记录着调用的次数和时间,然后定时的发送统计数据到监控中心。
实RPC的底层就是socket实现的,只要知道对方的主机和端口号,就可以通过网络连接上,而无需知道底层是怎么实现通讯的。
RocketMQ
基础组成
NameServer
无状态模式
broker向发心跳顺便带上所有的Topic信息
早期是zk后来改了
Broker
中转消息,消息持久化
底层通信基于Netty
Producer
三种消息发送方式
同步
运用在比较重要一点消息传递/通知等业务
异步
对发送消息响应时间要求更高/更快的场景:
单向
适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。只发送消息,不等待服务器响应,只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别
Consumer
pull
push
支持集群模式
在Broker的配置文件中,参数brokerId的值为0表明这个Broker是Master,大于0表明这个Broker是Slave
多master
一个集群无Slave,全是Master,例如2个Master或者3个Master
配置简单,单个Master宕机或重启维护对应用无影响,在磁盘配置为RAID10时,即使机器宕机不可恢复情况下,由于RAID10磁盘非常可靠,消息也不会丢(异步刷盘丢失少量消息,同步刷盘一条不丢),性能最高
单台机器宕机期间,这台机器上未被消费的消息在机器恢复之前不可订阅,消息实时性会受到影响。
多master多slave异步复制
每个Master配置一个Slave,有多对Master-Slave,HA采用异步复制方式,主备有短暂消息延迟(毫秒级)
即使磁盘损坏,消息丢失的非常少,且消息实时性不会受影响,同时Master宕机后,消费者仍然可以从Slave消费,而且此过程对应用透明,不需要人工干预,性能同多Master模式几乎一样;
Master宕机,磁盘损坏情况下会丢失少量消息。
多master多slave双写
每个Master配置一个Slave,有多对Master-Slave,HA采用同步双写方式,即只有主备都写成功,才向应用返回成功
数据与服务都无单点故障,Master宕机情况下,消息无延迟,服务可用性与数据可用性都非常高;
性能比异步复制模式略低(大约低10%左右),发送单个消息的RT会略高,且目前版本在主节点宕机后,备机不能自动切换为主机。
消费保证
发送成功后返回consume_success
回溯消费
主要使用场景就是重复消费一次指定时间后产生的相关topic的消息,需要在生产、消费时设置指定的参数,开启消息轨迹相关的配置,才能够使用。
broker.conf 添加配置项
# 开启消息轨迹traceTopicEnable=true
NameService 集群
Broker 主从 双主 双从
Consumer 自动切换
producer 链接两个Broker
刷盘
同步 超时会返回错误
消息被写入内存的PAGECACHE,返回写成功状态,当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入 。吞吐量高,当磁盘损坏时,会丢失消息
异步 不返回
消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,给应用返回消息写成功的状态。吞吐量低,但不会造成消息丢失
消息的主从复制
同步复制
master和slave均写成功,才返回客户端成功。maste挂了以后可以保证数据不丢失,但是同步复制会增加数据写入延迟,降低吞吐量
异步复制
master写成功,返回客户端成功。拥有较低的延迟和较高的吞吐量,但是当master出现故障后,有可能造成数据丢失
主从同步 异步刷盘
顺序消息
Hash取模
RocketMQ提供了MessageQueueSelector队列选择机制
顺序发送 顺序消费由 消费者保证
消费者是多线程
消息去重
幂等
去重
消息表主键冲突
分布式事务
最大努力
消息表 不断轮训 人工干预
半消息
发送半消息 发送成功 本地事务 觉得是否提交还是回滚 服务端没收到回查 检查本地事务 根据本地事务决定 提交
2/3pc
最终一致
预发 持久化 返回状态 发送处理结果 判断是否干掉持久化的 发送
完整的一个调用链路
producer 和NameService 节点建立一个长连接
定期从NameService获取Topic信息
并且向Broker Master 建立链接 发送心跳
发送消息给 Broker Master
consumer 从 Mater 和 Slave 一起订阅消息
消息重试
顺序消息重试
不断重试 16次 4小时46分钟 可以修改尝试次数
对一个消费者设置 组内都会设置
可以获取消息重试次数
无序消息重试
重试队列/死信队列
当消息消费失败,会被发送到重试队列
重试队列的命名为 %RETRY%消费组名称
当消息消费失败,并达到最大重试次数,rocketmq并不会将消息丢弃,而是将消息发送到死信队列
死信队列的命名为 %DLQ%消费组名称
保存3天
面向消费者组
控制台 重发 重写消费者 单独消费
一个死信队列包含了一个group id产生的所有消息,不管当前消息处于哪个topic。重试队列和死信队列只有在需要的时候才会被创建出来
事务消息
发送方向 MQ 服务端发送消息。
MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功,此时消息为半消息。
发送方开始执行本地事务逻辑。
发送方根据本地事务执行结果向 MQ Server 提交二次确认(Commit 或是 Rollback),MQ Server 收到 Commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 Rollback 状态则删除半消息,订阅方将不会接受该消息。
在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达 MQ Server,经过固定时间后 MQ Server 将对该消息发起消息回查。
发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
发送方根据检查得到的本地事务的最终状态再次提交二次确认,MQ Server 仍按照步骤4对半消息进行操作。
消息丢失
回查
由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,MQ 服务端通过扫描发现某条消息长期处于“半消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是 Rollback),该过程即消息回查
每60s会对Half(Prepare) Message的topic主题为RMQ_SYS_TRANS_HALF_TOPIC的消息进行check。
broker会调用自己实现TransactionListener接口的checkLocalTransaction方法。
消息积压
决定是否丢弃
判断吞吐量
停止消费 加机器 加topic
java8新特性
Lambda表达式
Java基础
树
0 条评论
回复 删除
下一页