超全的java后端体系
2024-05-04 22:02:58 15 举报
AI智能生成
Java思维导图
作者其他创作
大纲/内容
配置&注册中心
Nacos
配置中心
源码
NacosConfigService
构造方法
HttpAgent
负责网络请求等
ClientWorker
clientWorker主要就是每10ms检查是否需要有新的cacheData,有则发起长轮询(LongPollingRunnable)
LongPollingRunnable
CacheData
taskId
因为检查配置是分批的,taskId用来标记哪一批
dataId、group、tenant
content
配置内容
md5
contend通过md5算法得到的值,用于快速比较配置是否更新了
isUseLocalConfig
localConfigLastModified
上一次修改时间戳
CopyOnWriteArrayList<ManagerListenerWrap> listeners
监听配置是否改变
在run方法finally块里不停重复执行,周期默认30s(29.5s)
主要做了两件事
检查本地配置信息,更新cacheData的一些属性。如果配置更新了,还会回调该配置的listener
通过不断长轮询从服务端获取变化了的dataId列表,遍历dataIds去服务端查询最新配置,更新本地快照,更新cacheData
客户端感知配置变化:推还是拉?
push
tcp长连接可能会出现假死,需要通过心跳机制保证连接可用性
长连接会消耗大量的系统资源
pull
通过轮询的方式,缺点就是实时性低,如果降低轮询的间隔又会造成很大的系统压力
长轮询
客户端不断长轮询服务端,如果配置更改了则立即响应,否则会hold住请求(29.5s)再响应客户端,实现了服务端push的效果
服务端将客户端请求封装成ClientLongPolling任务放入到allSubs队列里,延时任务29.5s执行ClientLongPolling,如果期间接收到 LocalDataChangeEvent 则从allSubs里删除订阅关系,同时响应客户端配置变化了。否则到了29.5s则自动从队列删除,并响应客户端没有配置变化
配置变更的两个Event
ConfigDataChangeEvent
控制台修改或者新增配置发布该事件
监听ConfigDataChangeEvent事件,则会调用dumpService#dump方法,
dump方法就是往任务中心添加一个DumpTask(后台异步执行)
dump方法就是往任务中心添加一个DumpTask(后台异步执行)
dumpService#dump
ConfigService#dump
updateMd5
updateMd5方法如果md5改变了,则更新,
同时发布LoaclDataChangeEvent
同时发布LoaclDataChangeEvent
DumpTask主要做什么
保存磁盘中的配置信息
更新本地缓存的md5和修改时间戳
发布LocalDataChangeEvent
LocalDataChangeEvent
发布时机
ConfigDataChangeEvent触发dumpService#dump,发布LocalDataChangeEvent
后台定期检查配置有没有更改,更改则将数据库配置更新到磁盘,发布LocalDataChangeEvent
事件监听者LongPollingService
遍历allSubs队列,如果是当前配置,则从allSubs中删除,
同时响应客户端的长轮询请求通知配置变更
同时响应客户端的长轮询请求通知配置变更
客户端获取配置
NacosConfigService#getConfig
优先从本地获取
本地获取不到去Server获取
为了提升响应速度,服务端会将mysql的配置数据dump到磁盘,从而提升速度;
会有后台线程定期更新磁盘里的数据
会有后台线程定期更新磁盘里的数据
server请求异常从本地快照(snashot)获取
持久化
单机
Derby数据库(默认),可以配置MySQL
集群
MySQL
集群
CP
注册中心
服务分级模型
源码
NacosNamingService
服务注册
client
将服务名称,ip,端口等实例信息发送给server
server
收到client的注册请求,如果service还没有创建,则先创建,并为其添加一个
心跳检查的定时任务。然后封装成instance放入到缓存中
心跳检查的定时任务。然后封装成instance放入到缓存中
向其他nacos servers同步数据
如何同步
服务发现
client
订阅模式
直接从本地缓存获取服务实例列表,如果不存在则请求server,
并且订阅服务,拿到server返回的服务信息更新到本地缓存和磁盘文件中
并且订阅服务,拿到server返回的服务信息更新到本地缓存和磁盘文件中
tips:
1、磁盘里的服务信息会被后台任务写入到FailoverReactor的缓存中
2、正常情况是读取的HostReactor类的缓存。只有failover-mode = true才会
读取FailoverReactor的缓存
3、定时任务也会将HostReactor类的服务缓存写入到磁盘
1、磁盘里的服务信息会被后台任务写入到FailoverReactor的缓存中
2、正常情况是读取的HostReactor类的缓存。只有failover-mode = true才会
读取FailoverReactor的缓存
3、定时任务也会将HostReactor类的服务缓存写入到磁盘
非订阅模式
直接请求server获取服务的实例列表
server
接收client的请求,返回服务列表
如果订阅服务的话,则将其封装成pushClient放入到缓存,用于后期推送服务变化
服务续约
心跳续约,在服务端设置心跳时间戳,也就是更新instance的lastBeat为当前时间戳
服务下线
主动下线
故障下线
客户端发送心跳,发送心跳的周期默认是 5 秒,服务端会在 15 秒没收到心跳后将实例设置为不健康,
在 30 秒没收到心跳时将这个临时实例摘除,并通知订阅者
在 30 秒没收到心跳时将这个临时实例摘除,并通知订阅者
高可用
集群部署
一致性协议
自研Distro
AP
如果数据丢失的话,通过心跳续约机制作为数据补偿,达到最终一致性
临时节点
不健康则会删除
简化的Raft
CP
持久节点
持久化到nacos服务端,不健康实例不会
从服务端删除,只标记为不健康服务
从服务端删除,只标记为不健康服务
serverMode配置
AP(默认)、CP、MIXD
client存储服务列表
缓存、快照、failover
ZooKeeper
ZNode类型
持久节点
持久有序节点
临时节点
临时有序节点
Watcher机制
客户端可以在服务端注册一个watcher监听,当服务端的一些指定事件触发watcher,服务端会向客户端发送事件通知
注册的watcher监听是一次性的
EventType
NodeCreated
NodeChildrenChanged
NodeDataChanged
NodeDeleted
None
客户端的连接状态发生变更
集群
角色
leader
处理读写请求,处理完写请求会广播事务,当过半节点写入成功,则会提交事务
脑裂
集群中出现两个leader
避免
过半机制,集群机器数为2n+1
follower
只能处理读请求,当收到写请求会转发给leader处理。参与选举
observer
不参与选举投票,处理非事务请求
ZAB协议
崩溃恢复模式
当集群启动或者失去leader则会进入崩溃恢复模式,选举出leader,完成后退出该模式
原子广播模式
在leader正常工作时,加入一个新节点,则会进入原子广播模式,就会和leader进行数据同步
ZK的顺序一致性
比如leader事务提交,flower1已经同步到最新数据,follower2还没完成同步。在2为完成同步前,client连接到follower读取到某数据,并记录事务id(zxid:递增的数字),当由于某原因断开与flower1连接,下次重新连接,如果连接到follower2发现zxid小于自己记录的zxid则会连接失败。所以说client只要连接过一次zk,下次重新连接就不会读到比之前读的旧的数据
leader选举
源码中几个关键参数
Vote(myid, zxid,epoch)
myid
机器编号,在zk配置文件里指定的,数字越大,选举权重越大
zxid
事务id,数值越大,说明本机数据越新,选举权重越大
epoch-logicclock
逻辑时钟,也叫投票轮数。同一轮中,该值是一样的。每投完一轮则会递增
比较优先级:epoch>zxid>myid
节点状态
LOOKING
竞选状态,只有该状态的机器才能参与选举投票
FOLLOWING
随从状态,同步leader数据,参与投票
OBSERVING
观察状态,不参与投票
LEADING
领导状态
选举时机
启动选举
源码入口QuorumPeerMain#main(), zkServer.sh脚本执行该main方法
流程
开始每个节点状态都是LOOKING,接下来进行选举。每个server会发出投票,开始都是投自己,同时会接收其他server发送的投票,首先会判断投票的有效性。如果是有效投票,则会和自己的投票进行比较(优先级:epoch > zxid > myid),如果收到的投票优先级高于自己的,则更新自己的投票,并继续投票。每次投票后,会进行统计,如果有过半机器投票和自己当前投票一样,则选举出leader。最后如果是leader则更新自己为LEADING状态,如果是follower则更新自己为FOLLOWING
leader宕机后选举
和启动选举流程基本一致,当Leader挂了,余下的非Observer服务器都会更新自己状态为LOOKING
3.4.0后的Zookeeper的版本只保留了TCP版本的FastLeaderElection选举算法
客户端
Curator
ZkClient
应用
实现分布式锁
实现leader选举
注册中心
配置中心
分布式协调
Eureka
client定时去server拉取服务列表缓存到本地
自我保护机制
发现大量实例心跳失败,可能是网络问题,启动自我保护机制。
防止误杀下线服务
防止误杀下线服务
客户端恢复心跳,则退出自我保护机制
缓存
server、client都是缓存存储服务信息
部署
单点
集群
集群中的节点都是可以读写的
服务注册到其中一个节点,然后同步给其他节点
负载均衡:Ribbon
nacos & eureka & zk对比
CAP
zk是CP,leader宕机,会进行选主,期间服务不可用
eureka是AP,server节点宕机,依然可以从其他节点进行
服务注册和发现,保证可用性。等节点恢复后会进行数据
同步,保证最终一致性
服务注册和发现,保证可用性。等节点恢复后会进行数据
同步,保证最终一致性
nacos默认是AP,可CP
服务感知时效性
(推)nacos通过推的方式把服务变化实时推送给client
(拉)eureka采用定期拉取的方式,有较大的延迟
zk可以立即感知
存储容量
eureka不适合大规模服务实例数。因为单机要内存存储所有服务实例信息
zk 不适合大规模服务实例数。因为服务上下线的时候需要瞬间推送数据通知到其他
所有的实例,所以服务实例到几千个的时候,可能会导致网络带宽被打满
所有的实例,所以服务实例到几千个的时候,可能会导致网络带宽被打满
nacos支持百万级实例注册、十万级服务
访问协议
zk:TCP
eureka:HTTP
nacos:HTTP | DNS
分库分表
为什么分库分表
分库:1、单机连接数受限、减轻数据库压力;
2、单机存储容量受限
2、单机存储容量受限
分表:单表数据量大,索引和sql优化不能解决问题
分片键选择
算法
hash取模
一致性hash
range
带来问题
跨库导致无法使用本地事务
没有分片键作为条件的分页、范围查询、排序等
查询所有表内存中聚合处理
分片键和非分片键建立映射表,覆盖索引性能可以
分片键中含有非分片键信息
比如订单号中含有uid
数据倾斜
数据迁移
停机迁移
不停机迁移
双写法
旧表和新表双写
数据迁移
数据一致性校验
不一致以旧表为准
流量切换到新表
Sharding-proxy
扩容
方案
ShardingSphere
sharding-jdbc(JDBC层)
功能
数据分片
分库分表
读写分离
分布式主键
分布式事务
XA强一致事务
柔性事务
数据库治理
配置动态化
熔断&禁用
调用链路追踪
弹性伸缩
分片
数据源分片&数据表分片
分片键
支持单个和多个
分片算法
sharding-proxy(代理端)
Mycat
mybatis的拦截器改写sql
分布式&微服务
分布式理论
CAP
C:一致性
所有节点同一时间的数据完全一致
A:可用性
服务一直可用,正常响应
P:分区容错性
某节点故障或者网络分区故障时候,仍然能够对外提供满足一致性和可用性的服务
BASE
基本可用
分布式系统出现故障时候,允许损失部分可用性,保证核心可用
软状态
允许系统存在中间状态,该状态不会影响系统整体可用性。节点数据同步的延迟就是一种软状态
最终一致性
经过一定时间后,达到最终一性的状态
一致性协议
paxos
raft(redis哨兵间选主)
zab(zookeeper)
gossip(redis cluster)
distro(nacos)
分布式事务
相关理论
模型
RM(resource manager)
资源管理器,比如数据库
TM(transaction manager)
事务管理器,负责事务的提交和回滚等
AP(application)
应用程序
方案
2PC
过程
询问阶段
TM询问所有RM是否可以提交事务,并等待响应
RM执行本地事务,并未提交,根据是否成功返回yes或者no
提交阶段
TM根据RM的响应结果,告知RM是commit还是rollback
存在问题
单点故障
比如TM挂了,那么RM就会一直是prepare,不会commit也不会rollback
数据不一致
比如TM通知所有RM提交事务,由于某些原因只有一部分RM收到的commit,则出现了不一致性问题
3PC
针对2PC的问题做了改进,引入超时机制。
在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的
在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的
过程
canCommit
preCommit
doCommit
补偿事务TCC
Try阶段
对业务系统做检测以及资源预留
Confirm阶段
确认执行业务操作
Cancell阶段
取消执行业务操作
本地消息表
最大努力通知
比如调用支付系统,支付系统回调支付结果,有重试机制。也可以去支付系统查询支付状态
可靠消息最终一致性
RocketMQ事务性消息
事务消息发送到broker对消费者不可见,只有本地事务提交了,消息才对消费者可见
如果producer对broker上的消息确认失败了,rocket提供了回查机制,会主动查
询producer本地事务是否成功来决定是提交消息还是回滚消息
询producer本地事务是否成功来决定是提交消息还是回滚消息
不支持事务消息的MQ
建立事务消息表,消息记录到表中,定时器扫描消息表发送消息
Seata
事务模式
AT
TCC
SAGA
XA
分布式寻址算法
hash取模
key通过hash计算,取模
缺点
节点数变动时候,需要按照新的节点数全部重新取模计算,然后迁移,影响数据范围大
一致性hash
解决什么
解决在分布式环境下,hash 表中可能存在的动态扩容和缩容的问题。
造成数据需要全部重新hash取模,导致大量数据的迁移
造成数据需要全部重新hash取模,导致大量数据的迁移
原理
通过一个hash圆环,从0开始,到是 2^32-1结束
将存储节点的ip通过hash计算,在圆环上确定位置
要存储的值的key通过hash计算落在圆环的一个位置,通过顺时针查找,找到第一个节点存储
存储节点扩容缩容
只会影响临近节点少部分数据
存在缺陷
存储节点过少,导致数据倾斜
解决
虚拟节点
每个存储节点计算多个hash值,并且保存
虚拟节点和实际节点的映射关系
虚拟节点和实际节点的映射关系
分布式锁
Redis
原则
安全性(safety)
互斥性
同一时刻只有一个客户端可以持有锁
锁只能由加锁客户端释放
value设置一个随机值,删除key时比较value,一致才可以删
活性(liveness)
无死锁
设置过期时间
容错(只要大部分Redis节点都活着,客户端就可以获取和释放锁)
使用
加锁
setnx(lock,random_value)
expire(lock,expire_time)
通过lua脚本保证操作原子性
expire(lock,expire_time)
通过lua脚本保证操作原子性
set(key,random_value,NX,PX, expire_time)
释放锁
获取锁、判断random_value是否一致、一致则del(lock)
三步保证原子性,使用lua脚本
锁过期释放
Redisson
存储类型:hash
key为lock名称,field为线程id,value为重入次数
发布订阅
释放锁会发布消息通知订阅者
watch dog
如果lock没有设置过期时间,则默认是30s,有定时任务每10s
看锁是否还存在,存在则会续期重置过期时间为30s
看锁是否还存在,存在则会续期重置过期时间为30s
存在问题
单机
不能高可用
集群
在master上获取锁成功,还没有将key同步到slave,这时候master宕机,主从切换时,新主没有该key,
导致另一个客户端在新主上获得锁,违背了同一时刻只有一个客户端持有锁的原则
导致另一个客户端在新主上获得锁,违背了同一时刻只有一个客户端持有锁的原则
方案
RedLock算法
有 N 个 Redis Master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制
多master上获取锁,过半成功且在设定的时间内完成才算成功,否则向所有实例发送删除指令。当释放锁时,要向所有实例发送del
前提:各服务器之间较小的时间漂移
存在问题
如果在持有锁期间master挂了,比如5个实例,原来3个获取锁成功,这是其中一个挂了,
且没有持久化到磁盘,重启后,这时另一个客户端写入成功,这时有两个客户端持有锁
且没有持久化到磁盘,重启后,这时另一个客户端写入成功,这时有两个客户端持有锁
解决
开启aof备份,并且记录每条每次指令都立即备份
性能差
延时重启,延时的时间大于设置的key过期时间
如果大部分master挂了,延时重启会造成期间服务不可用
Java实现
org.redisson.RedissonRedLock
redis锁是AP模型
所以在集群架构下由于数据的一致性问题导致极端情况下出现多个线程抢占到锁的情况很难避免
Zookeeper
实现原理
临时有序节点 + watcher机制监听删除事件
流程
创建临时有序节点,并监听上一个节点的删除事件(序号最小的就是锁持有者)
Curator实现
InterProcessMutex
分布式可重入排它锁
InterProcessSemaphoreMutex
分布式排它锁
InterProcessReadWriteLock
分布式读写锁
两者对比
性能消耗
redis锁:需要不断尝试获取锁,消耗性能
zk锁:只需要创建临时有序节点排队,监听上一个节点的删除事件即可,性能损耗小
公平性
redis锁:非公平锁
zk锁:公平锁
锁释放
获取到redis锁的服务宕机,必须等到锁过期释放
zk只要客户端断开连接,就会删除节点,无需等待
CAP
zk是cp
保证锁不会被多方持有,基于这方面考量,zk锁更优
redis是ap
不能保证一致性,那么锁可能会被多个客户端持有
分布式ID
SnowFlake
分段说明
时间回拨
服务器时钟可能会因为各种原因发生不准,而网络中会提供 NTP 服务来做时间校准,
因此在做校准的时候,服务器时钟就会发生时钟的跳跃或者回拨问题
因此在做校准的时候,服务器时钟就会发生时钟的跳跃或者回拨问题
产生相同的id或者异常
解决
美团Leaf
根据最大容忍回拨时间,小于则等待时间回拨后再生成id,大于则异常
百度uid-generator
滴滴Tinyid
UUID
优点
生成速度快、简单易用
缺点
长度较大,无序,不太适合作为数据库主键
存在重复的可能
信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,暴露使用者的位置
Redis#incr
优点
性能不错,并且id自增
aof & 同步刷盘防止因为丢失数据生成重复id
分布式Session
集群模式下,在一台机器创建session,因为负载均衡请求别的机器没有session
解决
不使用session:JWT
spirng-session + redis
微服务组件
网关
配置中心
注册中心
服务调用
限流&降级
熔断器
负载均衡
链路追踪&监控
分布式事务
限流
限流
算法
固定时间窗口
优点:实现简单,易于理解
缺点:存在临界问题-两个窗口交界请求数可大于阈值而不被限流
滑动时间窗口
将固定窗口划分n个小窗口,每次大窗口向后滑动一个小窗口,并保证大的窗口内流量不会超出最大值
缺点:无法处理突发请求
漏桶算法
超出漏桶的请求被拒绝,漏桶内的请求以恒速从漏口流出
优点
恒定的速率处理请求,避免系统的过载和过度闲置
缺点
对请求进行缓存,消耗内存
恒定速率处理请求,对于突发流量影响用户体验,比如秒杀
难以动态调整桶的大小
令牌桶算法
定期往令牌桶中放进一定数量的令牌,请求进来从令牌桶中获取令牌,获取不到则拒绝
优点
因为桶中预留令牌,可以处理突发流量,适用于高并发场景比如秒杀
可动态调整令牌生成的速率
缺点
需要预热(预先在桶中放置令牌),否则会导致请求被误杀
被限流的请求可以排队、失败或者走降级逻辑
熔断
降级
组件
guava:限流工具类 RateLimiter基于令牌桶
sentinel
基本使用
步骤
定义资源
定义规则
hystrix
Nginx限流模块 limit_req_zone
运维
Linux
常用命令
CPU飙升,排查
- 定位进程:top找出占用cpu的pid;
- 定位线程:top -Hp pid找到cpu消耗最多的线程号;
- 打印线程号的16进制线程号:printf %x 线程号;
- 定位代码:jstack pid | grep -A 200 16进制线程号
查看磁盘空间
df -h
查看内存空间
free -m
查看端口占用
lsof -i:端口号
查看进程
ps -ef | grep xx
查看网络状况
netstat
搜索文件位置
find
find -name 文件名
比较慢
find 路径 -name 文件名
路径可以是一部分,加快查找速度
删除
rm 文件名
rm -r 文件名/文件夹
rm -f -r 文件名/文件夹
强制删除文件夹或文件,无需确认
vim
vim 文件名
G
跳转到最后一行
gg
跳转到第一行
page up/page down键
上一页/下一页
/关键字
搜索关键字
n
下一个搜索结果
N
上一个搜索结果
grep
grep 关键字 文件名
-C n 查看关键字前后n行
--color 颜色标注
tail
结束进程
kill pid
文件描述符(FD)
arthas
CI/CD工作流
Jenkins
是一个持续集成、交付、部署(软件/代码的编译、打包、部署)的基于web界面的平台
Docker
用于构建、分发、运行(Build, Ship and Run)容器的平台和工具
k8s
是一个使用 Docker 容器进行编排的系统,主要围绕 pods 进行工作。
Pods 是 k8s 生态中最小的调度单位,可以包含一个或多个容器
Pods 是 k8s 生态中最小的调度单位,可以包含一个或多个容器
网关
Nginx
轻量级 / 高性能的反向代理Web 服务器
功能
web服务器
用作静态资源(如HTML、CSS、JavaScript、图像等)的Web服务器
反向代理
请求nginx然后转发到目标服务器,对于用户来说目标服务器是无感知的
负载均衡
轮询
权重轮询
ip哈希计算
限流
令牌桶
漏桶
动静分离
解决跨域
配置SSL证书(https)
Kong
网络&操作系统
操作系统
内核态&用户态
为什么区分内核态&用户态
怎么切换
DMA
内核缓存
网络
OSI七层结构
应用层
表示层
会话层
传输层
网络层
数据链路层
物理层
http & https
http
什么是http协议
Hyper Text Transfer Protocol(超文本传输协议)
应用层协议,默认端口80
http报文
请求报文
请求行
请求头
常见请求头字段
Accept
可以接收的mime类型
Authorization
服务器可以对一些资源进行认证保护,如果你要访问这些资源,就要提供用户名和密码,
这个用户名和密码就是在Authorization头中附带的
这个用户名和密码就是在Authorization头中附带的
Cache-Control
可以指定是否使用缓存,缓存存活时间
取值
no-store
不使用缓存
no-cache
每次发送请求都要去服务器验证一下,如果服务器告诉可以使用缓存,才使用本地缓存
pulbic
private
只有发起请求的浏览器才能缓存
max-age=<seconds>
指定缓存存储时间
....
Connection
是否会关闭网络连接
取值
keep-alive
网络连接就是持久的,不会关闭,使得对同一个服务器的请求可以继续在该连接上完成
HTTP 1.1默认值
close
response后马上关闭连接
HTTP 1.0默认值
Keep-Alive
是一个通用消息头,允许消息发送者暗示连接的状态,还可以用来设置超时时长和最大请求数
Content-Type
请求体中的内容的mime类型
Cookie
User-Agent
用户的浏览器相关信息
请求正文
响应报文
状态行
常见响应码
2xx:成功响应
200
OK
服务器已成功处理了请求
3xx:重定向
4xx:指客户端错误
400
Bad Request
(错误请求) 服务器不理解请求的语法
401
Unauthorized
需要身份验证后才能获取所请求的内容
403
Forbidden
客户端没有权利访问所请求内容,服务器拒绝本次请求
404
Not Found
服务器找不到所请求的资源
5xx:服务端错误
500
Internal Server Error
服务器遇到未知的无法解决的问题
502
Bad Gateway
服务器作为网关且从上游服务器获取到了一个无效的HTTP响应
504
Gateway Timeout
服务器作为网关且不能从上游服务器及时的得到响应返回给客户端
响应头
常见响应头字段
Server
Cache-Control
响应正文
特点
支持B/S模式
简单快速
灵活
允许传输任意类型的数据对象。传输的类型由Content-Type加以标记
无连接
限制每次连接只处理一个请求,服务器处理完客户端的请求并收到客户端的应答后即断开连接
不利于客户端与服务端会话保持,通过cookie和session弥补这个不足
无状态
对事务处理没有记忆能力,意味着如果后续处理需要前面的信息,则必须被重传
存在问题
明文传输,数据可能被窃取
内容可能被篡改
不验证身份,导致身份可能被伪装
https
什么是https协议
http + SSL/TLS
在http的基础上通过传输加密和身份认证保证安全性
SSL
Secure Socket Layer(安全套接字层)
SSL 协议位于 TCP/IP 协议与各种应用层协议之间,为数据通讯提供安全支持
TLS
Transport Layer Security(传输层安全)
前身是 SSL,目前广泛使用的是TLS 1.1、TLS 1.2
通过 SSL证书来验证服务器的身份,并为浏览器和服务器之间的通信进行加密
默认端口443
https传输数据的流程
缺点
除了三次握手还有ssl握手,响应速度较http慢
连接缓存不如HTTP高效,会增加数据开销和功耗
CA证书一般不免费
SSL涉及到的安全算法会消耗 CPU 资源,对服务器资源消耗较大
tcp协议
一种面向连接的,可靠的,基于字节流(无界)的传输层通信协议
UDP
无需连接,不可靠的传输协议,支持一对多,多对多
性能好,适合视频通话,直播等允许少量丢包的场景
拆包粘包
只会发生在tcp中,不会发生在udp,因为tcp是无界字节流
拆包
把一个完整的数据包拆分成多个小包进行发送,
而接收端可能无法一次性接收到所有小包,导致
接收到的数据不完整
而接收端可能无法一次性接收到所有小包,导致
接收到的数据不完整
粘包
把多个数据包粘合在一起一次性发送,而接收端
可能无法正确区分每个数据包,导致接收到的数
据出现错位或混乱
可能无法正确区分每个数据包,导致接收到的数
据出现错位或混乱
解决方案
添加特殊字符标识包的开始和结束
自定义协议,协议头保存包的长度,接收方跟据长度来解析保证消息的完整性
基于定长消息,也就是发送端的消息长度是固定的(不足补0),服务端按照
固定长度来解析
固定长度来解析
Linux查看tcp连接状态
netstat -napt
linux下可同时建立多少个tcp连接?
取决于多个因素
进程允许的最大文件描述符数
内存大小
每个tcp连接都是占用内存的
服务器临时端口范围
服务器IP_TABLES限制
理想情况
总ip数 * 总端口数
2的32次方(ip数)×2的16次方(port数)
tcp的三次握手四次挥手
三次握手(建立连接)
过程
1.建立连接时,客户端发送SYN包(SYN=i)到服务器,并进入到SYN-SEND状态,等待服务器确认。
2.服务器收到 SYN 包,必须确认客户的 SYN ( ack=i+1 ) , 同时自己也发送一个 SYN 包( SYN=k ) , 即 SYN+ACK 包,此时服务器进入 SYN-RECV 状态。
3.客户端收到服务器的 SYN+ACK 包,向服务器发送确认报 ACK ( ack=k+1 ) , 此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手,客户端与服务器开始传送数据。
2.服务器收到 SYN 包,必须确认客户的 SYN ( ack=i+1 ) , 同时自己也发送一个 SYN 包( SYN=k ) , 即 SYN+ACK 包,此时服务器进入 SYN-RECV 状态。
3.客户端收到服务器的 SYN+ACK 包,向服务器发送确认报 ACK ( ack=k+1 ) , 此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手,客户端与服务器开始传送数据。
目的
保证双方都有发送和接收的能力
为什么不是两次、四次握手
没有第三次
避免历史连接
无法知道客户端具有接收能力
三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数
SYNC攻击
大量不同ip地址发送syn,服务端收到后发出ack+syn,但是客户端并不ack,导致服务端的syn接收队列占满,不能再接收正常请求
防范
缩短服务器接收SYN后的等待时间,但可能影响正常请求
四次挥手(断开连接)
过程
1.第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
2.第二次挥手: Server 收到 FIN 后,发送一个 ACK 给 Client ,确认序号为收到序号 +1 (与 SYN 相同,一个 FIN 占用一个序号), Server 进入 CLOSE_WAIT 状态。
3.第三次挥手: Server 发送一个 FIN ,用来关闭 Server 到 Client 的数据传送(数据传送完毕), Server 进入 LAST_ACK 状态。
4.第四次挥手: Client 收到 FIN 后, Client 进入 TIME_WAIT 状态,发送一个 ACK 给 Server ,确认序号为收到序号 +1 。等待2MS后,Server 进入 CLOSED 状态,完成四次挥手。
2.第二次挥手: Server 收到 FIN 后,发送一个 ACK 给 Client ,确认序号为收到序号 +1 (与 SYN 相同,一个 FIN 占用一个序号), Server 进入 CLOSE_WAIT 状态。
3.第三次挥手: Server 发送一个 FIN ,用来关闭 Server 到 Client 的数据传送(数据传送完毕), Server 进入 LAST_ACK 状态。
4.第四次挥手: Client 收到 FIN 后, Client 进入 TIME_WAIT 状态,发送一个 ACK 给 Server ,确认序号为收到序号 +1 。等待2MS后,Server 进入 CLOSED 状态,完成四次挥手。
为什么要四次挥手
没有第四次挥手ACK,client可能没收到FIN,而处于一直等待关闭状态。
有第四次的话,服务端没有收到ACK,会重发FIN
有第四次的话,服务端没有收到ACK,会重发FIN
为什么 TIME_WAIT 等待的时间是 2MSL
防止最后一次挥手(ACK)没有被server接收,server没收到ACK则会重新发送FIN
2MSL可以让此次连接中的报文段都消失,如果不等待就可能收到上一次连接的旧报文段,造成混乱
GET & POST请求区别
GET一般是用于获取资源,POST一般是用于提交表单
GET请求的参数是拼接在URL后面,而POST是放在请求报文的请求体里面
POST通过抓包还是可以看到请求参数的,也是不安全的。https加密安全
GET的URL是有长度限制的,POST没有限制
......
浏览器输入网址发生了什么
DNS域名解析,得到ip地址
浏览器向服务器建立TCP连接
三次握手
浏览器向服务器发送Http请求
服务器处理请求,将结果返回给浏览器
断开TCP连接
四次挥手
浏览器解析资源渲染页面
RPC & HTTP 的区别
前者主要是服务于不同计算机应用间的数据通信(屏蔽复杂的通信细节,像调用本地方法)。
后者主要是浏览器和服务器之间的数据通信
后者主要是浏览器和服务器之间的数据通信
前者是一个协议规范,rpc框架需要实现rpc协议,如dubbo。
后者是一个可以直接应用的应用层协议
后者是一个可以直接应用的应用层协议
rpc的底层通信协议可以用http实现,比如feign
Session & Cookie & Token
原因:http请求是无状态
作用:记录服务器和客户端会话状态的机制
Cookie
存储在浏览器,保存服务端
响应的数据,下次请求可以携带这些数据
响应的数据,下次请求可以携带这些数据
无法防止CSRF攻击
Session
存储在服务端
Token
JWT(Json Web Token)
jwt储存在客户端,一般存在localStorage。服务端不保存
解决CSRF攻击
格式:xxxxx.yyyyy.zzzzz,分别为头、荷载、签名
流程
秘钥保存在服务端
收到客户端的jwt,拿到头和荷载评接后使用秘钥生成签名,如果和jwt中签名一致则验证通过
IP
IP掩码/子网划分/nat/ARP/mac地址/VPN原理等基础概念
设计模式
七大原则
开闭原则
依赖倒置原则
单一职责原则
接口隔离原则
迪米特法则
里氏替换原则
合成复用原则
三大类型
创建型
简单工厂
if/else : 违背开闭原则
工厂方法
定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行
符合开闭原则
抽象工厂
与工厂方法的区别是生产的产品不是一个而是一系列产品
单例
好处
避免频繁的创建和销毁对象
避免频繁的gc
注意点
保证线程安全
私有构造函数
提供静态对外获取方法
实现方式
饿汉式
静态成员变量
枚举
懒汉式
静态内部类
双重检查锁
容器
Spring容器
ThreadLocal
线程级别单例
应用
Mybatis中ErrorContext
破坏
反射调用私有构造器
构造器null判断
反序列化
重写readResolve()
克隆
重写clone()方法,返回自身
不同类加载器
原型
定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象
复制,不会执行构造方法
实现 Cloneable 接口
场景
对象创建非常复杂,使用原型模式可以快速创建
建造者
结构型
代理
静态代理
编译期就生成了代理类
动态代理
运行时创建代理类
好处
增强被代理类
通过代理类去调用目标类,低耦合
装饰器
demo
特点
不改变原类文件
不使用继承
继承的替代模式
包装对象包裹原对象,动态拓展
优点
是继承的有力补充,比继承灵活,可以再不原有对象的情况下动态的给一个对象扩展功能,即插即用
使用不同的装饰类及这些装饰类的排列组合,可以实现不同的效果
完全符合开闭原则
适配器
不是最初设计,而是用于一种补救。代码复用
形式
类适配器
继承
Adapter extends Adaptee implements Target
优点
使用方便,代码简化
缺点
高耦合,灵活性低
对象适配器
组合
Adapter implements Target
Adapter持有Adaptee的引用
优点
灵活性高、低耦合
缺点
使用复杂(需要引入对象实例)
组合(Composite)
又名“部分整体模式”
定义:将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性(对象都拥有相同的接口)。
结构特点
整体和部分实现相同接口
父子文件夹,都可以删除。删除父也会同时删除子文件夹,子文件夹也可以单独删除
整体持有部分的集合
父文件夹含有子文件夹列表
DEMO
应用场景
如果你想表示“部分整体”的层次结构,可以使用组合模式
如果你想让客户端可以忽略复杂的层次结构,使用统一的方式去操作层次结构中的所有对象,也可以使用组合模式
门面
享元
桥接
行为型
模板
它在父类中定义一系列算法的步骤,而将具体的实现都推迟到子类
好处
代码复用
拓展性
策略
定义:定义了一系列的算法,并将每一个算法封装起来,而且它们还可以相互替换
使用场景
就是有一系列的可相互替换的算法的时候,我们就可以使用策略模式将这些算法做成接口的实现,
并让我们依赖于算法的类依赖于抽象的算法接口,这样可以彻底消除类与具体算法之间的耦合
并让我们依赖于算法的类依赖于抽象的算法接口,这样可以彻底消除类与具体算法之间的耦合
好处
开闭原则
避免if-else
应用
dubbo的SPI机制
责任链
为请求创建了一个接收者对象的链
好处
将请求的发送者和处理者解耦。客户端只认识一个Hanlder接口,降低了客户端(即请求发送者)与处理者的耦合度
客户端和处理者都不关心职责链的具体结构,而是交给职责链的创造者.也正因为如此,当在职责链中添加处理者的时候,
这对客户端和处理者来说,都是透明的,二者不知道也不必要知道职责链的变化
这对客户端和处理者来说,都是透明的,二者不知道也不必要知道职责链的变化
结构特点:每个处理对象实现相同接口,并且设置next处理对象
场景
有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定
在不明确指定接收者的情况下,向多个对象中的一个提交一个请求
可动态指定一组对象处理请求
观察者
对象间有一对多的依赖关系,对象发生变动需要通知依赖它的对象
优点
解耦
应用
事件驱动
Observer & Observable简单快速的实现观察者模式
状态
委派
迭代器
命令
备忘录
中介者
解析器
访问者
系统设计
秒杀系统设计
秒杀特点
高并发,瞬间请求量大
防止超卖
思路
系统隔离,防止影响其服务
尽可能的拦截掉大部分请求
通过缓存,减轻数据库压力
限流
消息队列异步削峰
......
具体实现
前端
页面静态化
数据缓存在客户端
限流削峰
开始前秒杀按钮置灰
限制点击秒杀按钮频率
过快给予友好提示
对恶意请求拦截
秒杀答题
基于时间片削峰
秒杀答题
支付宝咻一咻
微信摇一摇
后端
限流
集群部署
秒杀系统单独弄一个微服务,使用独立数据库,防止影响其它服务
库存预热到Redis
集群模式,哨兵,持久化
库存提前写入缓存,直接从缓存扣减库存
redis事务或者lua脚本,先判断库存再扣减,防止超卖
热点商品,超高tps,redis也扛不住
解决
应用层本地缓存库存
定时去redis拉取最新库存更新本地缓存
缓存不一致,是否导致超卖?
不会
读的场景可以允许一定的脏数据,因为这里的误判只会导致少量一些原本已经没有库存的下单请求误认为还有库存而已,等到真正写数据(查询redis中是否还有库存)时再保证最终的一致性
MQ
秒杀成功用消息队列异步削峰
数据库
读写分离
建立合适的索引
多级缓存架构设计
http缓存
CDN缓存
Nginx缓存
进程内缓存(JVM)
分布式缓存(redis)
接口超时处理
方案
调大timeout
重试
MQ
回滚
异步
注意
幂等
数据回滚
幂等
产生原因
网络抖动,可能重复请求
一些组件的重试机制:比如mq、nginx、rpc框架等
用户双击、浏览器回退重复提交、页面重复刷新等
解决
前端
页面控制:按钮置灰或加载中避免重复提交
提交后重定向到另一个页面
后端
唯一标识
状态检查
系统稳定性保证
高并发系统设计
MapReduce
算法
时间 & 空间复杂度
数据结构
链表
单向
数据结构
技巧
单向链表如何遍历
双指针
快慢指针
题目
反转单向链表
环形链表
合并两个有序链表
双向
二叉树
数据结构
遍历方式
前序遍历
递归
迭代
中序遍历
递归
迭代
后序遍历
递归
迭代
后序迭代
前序迭代(前右左)+ 翻转
层序遍历
技巧
深度优先搜索
广度优先搜索
题目
合并二叉树
递归
翻转二叉树
递归
对称二叉树
递归
迭代
二叉树最大深度
广度优先搜索
深度优先搜索
数组
技巧
Arrays.sort(int[] nums)
数组排序
快速排序
冒泡排序
插入排序(类似扑克)
选择排序
归并排序
堆排序
力扣
矩阵
int[][] matrix = new int[m][n]
m行n列
m = matrix.length
n = matrix[0].length
队列 & 栈
两个队列实现栈
两个栈实现队列
字符串
哈希表
堆
定义
完全二叉树
大顶堆
小顶堆
一种特殊二叉树,面向排序设计
查找平均时间复杂度o(n)
有序二叉树面向搜索设计,查找平均时间复杂度o(logn)
基础技巧
二分
二分查找
双指针
判断子序列
环形链表
贪心
跳跃游戏
递归
分治
滑动窗口
无重复字符的最长子串
长度最小的子数组
搜索算法
回溯
深度优先遍历
广度优先遍历
动态规划
最长子序和
其他
LRU
继承LinkedHashMap
Map + 双向链表
Map + 队列
top100
基础
八个基本数据类型
关键字
访问权限
public
所有都可以访问
protected
本类和子类,以及同一包的其他类可以访问
不写默认
本包内可以访问
private
本类可以访问
static
修饰内部类
修饰代码块
修饰成员变量
类变量,在内存中只有一个副本,被所有对象共享
修饰方法
final
修饰变量
基本数据类型值无法被修改。
对象、数组等引用无法被修改,但是可以改变其内容
对象、数组等引用无法被修改,但是可以改变其内容
修饰类
无法被继承,String就是final类
修饰方法
可以被子类继承,无法被重写
transient
被修饰的变量不会被序列化
finally块一定会执行吗?
不一定,当执行了System.exit(0)语句,导致 JVM 直接退出
抽象类 & 接口
抽象类
- 不能被实例化
- 子类可以是抽象类或者普通类
- 抽象方法不能有方法体,如果子类是普通类则必须实现抽象方法
接口
- 接口里所有方法都是抽象方法,必须是public类型,1.8后可以有default默认实现方法,静态方法
- 接口可以多继承
- 如果不是抽象类实现接口,则必须要实现接口中所有方法
- 接口中的变量会被隐式的加上public static final
重载 & 覆盖
重载
覆盖override
I/O
背景:用户进程无法操作I/O设备(磁盘),必须通过内核来完成完成,然后将内核读取到的数据拷贝到用户进程
模式
BIO
用户进程发起I/O操作,由用户空间转到内核空间,内核等待数据准备完成,然后拷贝到用户空间,这期间用户线程是阻塞的
同步阻塞。每个连接都会创建一个线程,造成资源极大浪费
NIO
所有连接(channel)注册到一个多路复用器(selector)上,通过轮询的方式
,检查就绪的channel,准备好则进行读写操作
,检查就绪的channel,准备好则进行读写操作
同步非阻塞。单个线程处理多个请求,节约系统资源
java层面的NIO封装了IO多路复用
I/O多路复用
通过单线程就可以监视多个文件描述符是否就绪(可读、可写、异常)
函数
select
进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这
样select可以帮我们检测多个fd是否可读写
样select可以帮我们检测多个fd是否可读写
优点
基本所有系统都支持(保底)
缺点
随着FD(文件描述符)数量增多导致性能下降,时间复杂度O(n)
操作系统对单进程FD有数量限制(默认1024),单机支持的tcp连接少
poll
将用户传入的数组拷贝到内核空间,然后查询每个fd是否可读写
优点
通过链表存储FD,所以没有最大FD数量限制
缺点
随着FD(文件描述符)数量增多导致性能下降,时间复杂度O(n)
epoll
优点
将轮询改为回调(事件通知机制),不会随着FD增多而性能下降,事件复杂度O(1)
fd是操作系统的最大文件句柄,远大于1024
缺点
只有linux支持
应用
redis
HTTP 2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求
AIO
用户进程发起read后,立即返回。内核收到read后,会等到数据准备完成,然后将数据拷贝到用户内存,完成后通知用户进程
异步非阻塞
零拷贝
深拷贝 & 浅拷贝
clone()是浅拷贝
如何深拷贝
序列化->反序列化
转Json,,然后转成对象
特殊类
Object
equals() & hashCode()
equals()
默认比较两个对象的地址是否相等
可以重写,属性一样就可以相等
hashCode()
返回一个对象的hash值
两个对象相等,hash值一定相等,但是hash值相等,两个对象不一定相等
==
基本类型比较值是否相等,引用类型比较引用地址是否相等
toString()
wait(),notify(),notifyAll()
getClass()
clone()
浅拷贝
finalize()
对象被gc的时候会调用此方法
String
String s = new String("abc")创建了几个对象
String/StringBuilder/StringBuffer
可变性
String内部value是final修饰,不可变类。所以每次修改都会产生新的对象
StringBuffer 和 StringBuilder 是可变类,字符串的变更不会产生新的对象
线程安全性
String因为不可变,所以是安全的
StringBuilder不安全
StringBuffer通过synchronized保证安全
性能
String每次拼接和修改都会产生新的对象,性能最差
其次是StringBuffer,因为有锁的开销
存储位置
String =》方法区
StringBuffer 和 StringBuilder =》堆
SimpleDateFormat
线程不安全
因为parse方法里的establish方法里cal.clear,cal.set,retrun cal不是原子操作
解决
每个线程new SimpleDateFormat(),缺点造成大量垃圾
线程隔离,放到ThreadLocal里,每次使用时候get
锁
在 Java8 里面引入了一些线程安全的日期 API,比如 LocalDateTimer、DateTimeFormatter 等
包装类对象
自动装箱
自动拆箱
缓存池设计
Integer/Long
缓存、自动装箱拆箱
在-128到127间的值,valueOf方法返回的都是缓存池的对象,是同一个对象。
范围之外是new一个新对象
范围之外是new一个新对象
Integer.valueOf(int i)
Byte
都是从缓存池获取,valueOf返回都是同一对象
作用
减少空间使用,提升性能
无缓存池设计
Float/Double
valueOf 每次都是new一个对象,没有缓存设计
为什么有的有缓存设计有的没有
小数无法确定缓存个数,整数可以确定
反射
通过获取的字节码文件然后将方法、属性、构造方法映射到Method、Field、Constructor等类
为什么性能差
优化
每次获取Method、Field、Constructor都是要新建对象的,所以如果频繁调用,可以将其缓存起来
获取Class对象
类.class
对象.getClass()
Class.forName("全限定名")
优点
增加程序的灵活性,可以在运行的过程中动态对类进行修改和操作
提高代码的复用率,比如动态代理,就是用到了反射来实现
可以在运行时轻松获取任意一个类的方法、属性,并且还能通过反射进行动态调用
缺点
性能相对直接调用差
反射可以绕过一些限制访问的属性或者方法
注解
反射
动态代理
在运行时动态地生成代理类,在不修改源代码的情况下,为原有的类提供额外的功能
技术
jdk
被代理类必须实现接口,运行期间生成字节码,实现目标类接口
cglib
运行期间使用ASM框架生成字节码,继承目标类,所以
不能代理final修饰的类和方法
不能代理final修饰的类和方法
javassist
是一个强大的字节码操作工具,可以运行时修改类的字节码,从而可以对目标类进行功能增强和修改
java8新特性
lambda
Stream流
stream()
有序
线程安全
parallelStream()
无序
多线程,非线程安全
forkJoin
并行不一定性能最好,要根据场景测试。比如子任务耗时短,频繁线程切换反而更耗时
原理
常用操作:foreach、filter、map、flatMap、sorted、distinct、count、min、max、skip、limit、collect、anyMatch、allMatch、reduce、findFirst、findAny
Option
方法与构造函数引用
接口中可以有静态、默认方法
元空间
@FunctionalInterface
一个接口只有一个未实现方法
Function 函数型接口:有一个输入参数,有一个输出
Predicate 断定型接口:有一个输入参数,返回值只能是 布尔值!
Consumer 消费型接口: 只有输入,没有返回值
Supplier 供给型接口: 没有参数,只有返回值
异常
Throwable
Error
系统环境问题引起的异常
Exception
CheckedException
是程序在编译阶段必须要主动捕获的异常,遇到该异常有两种处理
方法通过 try/catch 捕获该异常或者通过 throw 把异常抛出去
方法通过 try/catch 捕获该异常或者通过 throw 把异常抛出去
RuntimeException
运行时的错误,它可以被捕获并处理
必须try...catch/throw
CheckedException
Exception及其子类,但不包括RuntimeException及其子类
无须try...catch/throw
Error以及子类
RuntimeException以及子类
SPI
Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由
ServiceLoader读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类,从而给程序提供拓展功能
ServiceLoader读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类,从而给程序提供拓展功能
缺点:无法按需加载,会加载所有实现类
demo
定义接口,以及实现接口
META-INF/services 文件夹下创建一个文件,文件名就是接口全限定名
文件中输入接口实现类的全限定名
通过ServiceLoader.load(接口名.class)加载其实现类
泛型
用于类、接口、方法
作用
代码复用
类型安全
编译时类型检查
消除强制类型转换
泛型上限
<? extends Xxx>
只接收Xxx类型或者Xxx类型的子类
泛型下限
<? super Xxx>
只接收Xxx类型或者Xxx类型的父类
序列化&反序列化
序列化 & 反序列化
序列化
把对象转换为字节数组的过程
作用
网络传输对象
持久化对象
反序列化
把字节数组恢复为对象的过程
Serializable接口
为什么要实现该接口
标记一个类可以被序列化和反序列化,jvm
会自动给它提供序列化和反序列化的能力
会自动给它提供序列化和反序列化的能力
serialVersionUID
用来表示类的版本,用于在序列化和反序列化过程中检查
版本一致性。如果发送方和接收方的类版本不一致,
将引发`java.io.InvalidClassException`异常
版本一致性。如果发送方和接收方的类版本不一致,
将引发`java.io.InvalidClassException`异常
多个类定义同一个serialVersionUID有风险吗?
序列化的前提是保证通信双方对于对象的可识别性,
所以很多时候,我们会把对象先转化为通用的解析格式
所以很多时候,我们会把对象先转化为通用的解析格式
选型考虑因素
序列化之后的数据大小
序列化的性能(耗时)
是否跨平台、跨语言
技术成熟度
常见技术
JDK Serializable
只支持java,性能差
xml
json
fastjson、gson、jackson
protobuf
跨语言、速度快、体积小
Kryo
只支持java、体积小、速度快
线程不安全
线程安全方式
ThreadLocal
kryo提供的pool
FST
Java,不支持跨语言、体积小、速度快、线程安全
Hessian
跨语言、速度较慢
JDBC
Maven
冲突解决
发现
运行异常,一般类找不到,方法找不到等
依赖原则
路径最近者优先
第一声明者优先
排除依赖
maven helper插件,然后使用<exclusion>标签
<optional>true</optional>
加密算法
MDC
数据结构
Collection
List
线程不安全
ArrayList
动态数组
构造方法
new ArrayList()
{}。第一次add才初始化数组,默认容量是10
new ArrayList(n)
立即初始化数组
扩容
默认1.5倍,复制到扩容后的数组并指向原数组
删除|新增
其他元素需要移动,开销大且不固定
LinkedList
双向链表
Node通过prev和next指针构建
相对于单向链表优点
双向遍历,可以在任一节点向前向后遍历
任一节点的插入删除都是O(1)复杂度,
而单向链表需要从头开始遍历,复杂度为O(n)
而单向链表需要从头开始遍历,复杂度为O(n)
删除|新增
只需改变node的前后指针,开销小切固定
实现了Queue
线程安全
Vector
synchronized
Collections#synchronizedList(list)
对原list做了装饰,实际还是使用synchronized代码块,
可以通过参数传入指定的锁对象,否则默认当前对象作为锁
可以通过参数传入指定的锁对象,否则默认当前对象作为锁
CopyOnWriteArrayList
写操作会加锁,同时会复制一份原来的数据进行写操作,
然后再把原数组设置为新数组
然后再把原数组设置为新数组
读操作不会加锁
读多写少的场景
Set
HashSet
底层是Map,key为set的元素,value为Object对象
默认构造的HashSet的底层map就是HashMap
有一个构造器使用的是LinkedHashMap
TreeSet
底层是TreeMap
LinkedHashSet
继承了HashSet,用的构造器是HashSet的
LinkedHashMap,用双向链表维护插入顺序
LinkedHashMap,用双向链表维护插入顺序
Stack
Map
HashMap
java8
源码
数据结构
数组+链表+红黑树
为啥是红黑树不是平衡二叉树
红黑树插入和删除的性能要比平衡二叉树好
key & value 可以为null
put
过程
计算key的hash值
如果Node数组没有初始化,则初始化(resize())
计算key的数组下标,如果head Node为null,则将KV封装成Node插入
这里会有线程不安全问题
如果头结点不为空且key和待插入的key一致,则直接替换
如果当前是红黑树,则按照红黑树的方式插入
如果当前是链表,则遍历链表替换或者插入到尾部,如果链表长度达到8了,
那么看数组长度是否达到64,没达到则扩容,否则进行链表转红黑树
那么看数组长度是否达到64,没达到则扩容,否则进行链表转红黑树
最后,如果当前++size>threshold,则会进行resize()扩容
++size没有同步,也没有volatile+cas,线程不安全
get
过程
key计算hash值,然后定位到数组下标
如果头Node就是要查找的,返回
否则树查找或者链表遍历查找,返回
resize
初始化数组(第一次put)
扩容
链表长度达到8且数组长度小于64
size达到负载容量
Q & A
loadFactor = 0.75
负载因子越小,扩容越频繁,hash冲突越小,空间换时间
负载因子越大,扩容越不频繁,hash冲突越大,链表越长,时间换空间
0.75在时间和空间上取得一个平衡
为什么链表要达到8才树化
红黑树的时间复杂度为O(logn),而链表时间复杂度平均为O(n/2)
所以8链表查找长度平均为4,红黑树为3,所以有树化的必要。
6差不多,但是转换为树效率低,而且红黑树有更多的指针如
left、right、parent,color,更加耗内存
6差不多,但是转换为树效率低,而且红黑树有更多的指针如
left、right、parent,color,更加耗内存
树转链表是6,不是7是为了防止树和链表频繁转换
key的hash值如何计算
key在数组下标如何计算
index = key.hash & (数组长度-1)
等价于取模运算,性能高。前提是数组长度必须是2幂次方
容量为什么必须是2的幂次方
下标计算决定了必须是2幂次方
否则会造成key分布不均匀,而且有的index永远不会分配到key
容量最大值为2的30次方
modCount是干嘛用的
记录map修改次数,相当于乐观锁版本号
fast-fail: 进行迭代的时候,如果map被需改,抛出ConcurrentModificationException
扩容下标怎么移动,,如何判定是否需要移动
e.hash & oldTab.length
等于0则无需移动
原下标
否则需要移动
原下标+oldTab.length
为啥重写hashCode和equals方法
因为比较key的值相等,不是比较内存地址,所有重写equals(),
重写equals()则必须重写hashCode(),因为两个对象相等,其hashCode必须相等
重写equals()则必须重写hashCode(),因为两个对象相等,其hashCode必须相等
遍历方式
map.keySet()
map.values()
map.entrySet()
通过Iterator遍历
Iterator<Entry<String, String>> iterator = map.entrySet().iterator()
7和8区别
数据结构
后者比前者多了红黑树
节点类型
前者是Entry,后者是Node
插入方式
前者头插法,后者尾插
扩容时机
前者是先扩容再插入,后者是先插入后扩容
扩容后存储位置
前者是重新计算,后者是原位置or者原位置+旧容量
线程不安全
前者因为是头插法,在并发扩容的情况下,可能两个Entry的next指针互相指向,导致闭环
后者设置头Node和计算size不安全
HashTable
synchronized保证线程安全
数组+链表
节点是Entry,默认初始容量是11,容量没有2的幂次方限制
O(1)
key和value不能为null
因为HashTable用于多线程场景,ge(key) = null的话无法确定是不存在key还是存在key,value就是为null
头插法
扩容rehash()
先扩容后插入
新数组长度 = 原数组长度 * 2 + 1
遍历所有元素,重新计算hash,使用头插法插入到新数组
ConcurrentHashMap
java8源码
数据结构
结构图
构造方法
参数
volatile int sizeCtl
控制table的初始化和扩容
0 : 初始默认值;
-1 : 有线程正在进行table的初始化;
>0 : table初始化时使用的容量,或初始化/扩容完成后的threshold;
-(1 + nThreads) : 记录正在执行扩容任务的线程数;
-1 : 有线程正在进行table的初始化;
>0 : table初始化时使用的容量,或初始化/扩容完成后的threshold;
-(1 + nThreads) : 记录正在执行扩容任务的线程数;
put
put过程
看数组是否初始化了,没有则先初始化
根据key定位数组下标,看头Node是否为null,是则cas设置头Node
key和value都不能为null
头Node不是null且hash值=-1,说明正在扩容,则参与到协助迁移数据流程
头Node红黑树,则按照红黑树方式插入
否则头Node的hash大于0,则是链表,遍历链表,比较key如果一样则替换旧值,否则进行尾插
如果当前是链表,且长度大于等于8并且数组长度小于64,则会进行扩容迁移。如果大于等于64,则会将链表转为红黑树
最后会根据put结果是插入还是更新旧值计算当前map容量
扩容和数据迁移
安全性保证
volatile+cas
当table[i]为null,cas设置头Node
synchronized
当头Node不为null时候,会用头Node对象作为monitor,以桶为粒度,进行同步
get
过程
计算key的hash值h
如果数组没有初始化或者用h计算的数组下标处没有Node,则返回null
如果当前桶的头结点就是要查找的key,则返回
头结点hash小于0,说明当前正在扩容或者是红黑树结构,则调用当前Node类型的find方法
hash小于0
hash= -1则为TreeBin
(TreeNode的代理结点,负责红黑树的操作)
(TreeNode的代理结点,负责红黑树的操作)
红黑树
否则为ForwardingNode
正在扩容,则去新的table去查找
不是上面情况,则是链表,则遍历链表查找,返回
是否加锁
Node的val和next都是volatile修饰,所以节点修改和新增都具有可见性
数组是volatile修饰,所以扩容具有可见性
节点是红黑树的时候,如果树正在变色旋转并且要查询的值不是红黑树的头节点,会加一个读写锁
addCount和size()
baseCount + 数组方式计数,分散并发压力
size得到的可能不是准确值
TreeMap
红黑树
构造函数
传入比较器Comparator
key无需实现Comparable接口
不传入比较器Comparator
key必须实现Comparable接口,使用key默认的比较规则
数据插入
根据是否传入比较器,如果是则按照该比较规则,否则按照key默认的比较规则进行树搜索的遍历搜索,
如果存在key,则替换。否则插入,插入完成后,如果破坏红黑树的规则,则还需通过变色旋转的方式来达到平衡
如果存在key,则替换。否则插入,插入完成后,如果破坏红黑树的规则,则还需通过变色旋转的方式来达到平衡
可以实现一致性哈希
LinkedHashMap
继承HashMap,通过维护一条双向链表,实现了散列数据的有序排列,当我们希望顺序存取,可以使用
重写了HashMap的三个方法
afterNodeAccess(Node<K,V> p)
move node to last
afterNodeInsertion(boolean evict)
possibly remove eldest
removeEldestEntry(first)默认实现是返回false
继承LinkedHashMap重写该方法可以删除第一个元素实现LRU
afterNodeRemoval(Node<K,V> p)
WeakHashMap
Entry继承WeakReference,每次gc的时候,都会回收它,适合做缓存
Properties
继承Hashtable
Tree
二叉树
左子树小于根节点,右子树大于根节点
缺点:极端情况下,会造成左右两边不平衡,一边几乎成为链表,查找时间复杂度为O(n)
平衡二叉树(AVL)
左子树小于根节点,右子树大于根节点,且左右子树的高度差<=1
插入和删除
如果改变后左右子树高度差大于1,则破坏了平衡,会通过旋转达到平衡
O(log n)
红黑树
1.节点分为红色或者黑色;
2.根节点必为黑色;
3.叶子节点都为黑色,且为null;
4.不会出现相邻的红色节点;
5.从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;
6.新加入到红黑树的节点为红色节点
2.根节点必为黑色;
3.叶子节点都为黑色,且为null;
4.不会出现相邻的红色节点;
5.从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点;
6.新加入到红黑树的节点为红色节点
插入和删除
当插入或者删除破坏了上面的规则后,会进行变色来符合上述规则,
如果变色不行则通过左旋右旋来符合上述规则
如果变色不行则通过左旋右旋来符合上述规则
O(log n)
B-Tree
B+Tree
SkipList
因为链表没办法二分查找,只能遍历比较查找,所以有了跳跃表
从最高层开始查找,找到小于等于目标值的最大值,然后到下一层查找,然后以此内推,直到最后一层
时间复杂度O(log(n)),空间复杂度O(n),空间换时间
特征
- 由很多层结构组成
- 每一层都是一个”有序“链表(索引)
- 最底层(Level 1)的链表包含所有元素
- 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
- 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素
索引选取
如果用相同的间隔提取索引,插入数据会重建索引,导致性能差
随机函数
通过使用一个随机函数,来决定这个结点插入时,是否需要插入到
索引层、以及插入到第几级索引。时间复杂度O(log(n))
索引层、以及插入到第几级索引。时间复杂度O(log(n))
jdk实现ConcurrentSkipListMap
JVM
组成
运行时数据区
(JVM内存模型)
(JVM内存模型)
堆
对象一定在堆中分配吗?
不一定。如果对象在方法内创建且没有返回出去(方法逃逸)
或者没有被其他线程访问(线程逃逸),则会分配在栈里
或者没有被其他线程访问(线程逃逸),则会分配在栈里
逃逸分析
对象在方法内创建,没有逃逸到方法之外,则没有发生逃逸
(对象不是入参或者没有return出去)
(对象不是入参或者没有return出去)
会有性能损耗,要确保收益大于损耗
JDK7默认开启
+XX:+DoEscapeAnalysis
+XX:+DoEscapeAnalysis
栈上分配好处
随着方法执行结束销毁
提升对象访问速度,不需要访问堆
即使逃逸分析通过,大对象还是会分配到堆中
jdk8开始静态成员变量存放在堆中的Class对象里
方法区
常量池、类元信息
实现
jdk7:永久代
使用的是jvm的内存,所以存在上限,容易出现OOM。
通过-XX:PermSize调节大小
通过-XX:PermSize调节大小
Full GC,导致STW
jdk8:元空间MetaSpace
使用本地内存,默认是无限制使用的,可以通过参数调节。
因此不需要考虑GC的问题
因此不需要考虑GC的问题
栈
栈帧
局部变量表
操作数栈
主要用于保存计算过程的中间结果,同时
作为计算过程中变量临时的存储空间
作为计算过程中变量临时的存储空间
动态链接
运行时再确定具体类型
方法返回地址
OOM
基本上都是创建的了大量的线程导致的
本地方法栈
native方法
程序计数器
类加载子系统
类加载机制
Java虚拟机把Class文件加载到内存,并对数据进行校验、准备、解析和初始化,变成可被使用的java类型
Class文件获取
java类可以动态加载到内存
途径
zip包
网络
动态代理技术
.....
ClassLoader类
作用根据类全限定名找到Class文件,然后加载它并转换成java.lang.Class对象
生命周期
加载
通过类的全限定名来获取定义此类的二进制流,然后将其静态存储结构转化为方法区运行时数据结构,
在堆内存中生成代表此类的Class对象(ClassLoader的definClass方法将字节流转成Class对象),
作为方法区这个类的各种数据的访问入口
在堆内存中生成代表此类的Class对象(ClassLoader的definClass方法将字节流转成Class对象),
作为方法区这个类的各种数据的访问入口
验证
确保Class文件包含的信息符合《Java虚拟机规范》的约束
准备
为类的静态变量分配内存空间,设置零值。
非静态变量不会分配内存。
非静态变量不会分配内存。
解析
将常量池中的符号引用替换为直接引用
初始化
开始执行类中定义的java程序代码(字节码)
只有当类被直接引用的时候,才会触发类的初始化
使用
卸载
类加载器
同一个Class文件被同一个类加载器加载,这两个类才相等。
equals()、isAssignableFrom()、instanceof
equals()、isAssignableFrom()、instanceof
类型
启动类加载器:c++实现
<JAVA_HOME>/lib
被-Xbootclasspath指定的路径存放的
拓展类加载器: ExtClassLoader
<JAVA_HOME>/lib/ext目录下的jar
被java.ext.dirs指定的路径下类库
应用程序类加载器:AppClassLoader
classpath路径
自定义类加载器
why
类不在classplath路径下,可能来源于数据库,文件,网络等:需要我们自定义加载器去找这些类
代码加密:需要自定义类加载器去解密
.....
如何自定义
继承java.lang.ClassLoader,重写 findClass
拓展
tomcat为啥自定义类加载器
双亲委派模型
当一个类加载器收到类加载请求时,不会先自己加载,而是上抛给父加载器,一直到启动类加载器,如果父类加载不了,才会由子加载器去加载
好处
- 比如加载Object类时候,只有启动类加载器才可以加载,如果没有双亲委派,那么所有加载器都可以加载的话,那么系统中存在不同的Object;
- 安全性,保证系统的稳定运行
破坏双亲委派模型
java1.2双亲委派模型出现之前
SPI,父类加载器去请求子类加载器完成类加载
spi的接口由启动类加载器加载,而启动类加载器不能加载子类,也无法委托给应用类加载器,
所以必须打破双亲委派。ServiceLoader通过设置上下文类加载器为应用类加载器,从而可以加载子类
所以必须打破双亲委派。ServiceLoader通过设置上下文类加载器为应用类加载器,从而可以加载子类
如java.jdbc.Driver
热部署
Tomcat
如何破坏双亲委派模型
继承ClassLoader,重写loadClass方法
使用线程上下文加载器,可以通过 java.lang.Thread 类的
setContextClassLoader()方法来设置当前类使用的类加载器类型,比如SPI
setContextClassLoader()方法来设置当前类使用的类加载器类型,比如SPI
字节码执行引擎
执行字节码、修改程序计数器、执行垃圾收集线程
GC
内存溢出 & 内存泄露
内存溢出
当内存不够分配对象时候,就会OOM
内存泄露
当资源没有得到释放(比如流没有close)、循环引用等
原因,导致对象一直得不到释放回收,造成溢出
原因,导致对象一直得不到释放回收,造成溢出
强引用、软引用、弱引用和虚引用
垃圾判断
引用计数法
循环引用,无法被清除
可达性分析
GC Roots
- 虚拟机栈(栈帧中本地变量表)引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈引用的对象
- 被同步锁持有的对象
如果对象和gc roots之间没有可达路径,则被判断为垃圾对象
缺点:gc roots确定的过程中STW比较久
三色标记法
相比可达性分析好处
不发生STW,或者极短的STW
CMS、G1都采用
原理
三色
白:还没有被垃圾回收器扫描的对象
灰:已经被垃圾回收器扫描,但对象引用的其他对象没有被扫描
黑:已经被垃圾回收器扫描,对象以及引用的其他对象也是存活的
流程
初始阶段:gc roots是黑色,其他都是白色
初始标记阶段:跟gc roots直接关联的对象标记为灰色
并发标记阶段:当前节点标记为黑色,如果有子节点则标记灰色
重发并发标记阶段:直到灰色对象没有其它子节点引用时结束
扫描完成:黑色的为存活对象,白色为垃圾对象
缺陷
用户线程与 GC 线程同时
运行,可能产生多标、漏标
运行,可能产生多标、漏标
多标
产生浮动垃圾,留到下一次回收即可
漏标
会回收非垃圾对象,不可接受
解决
读屏障、写屏障、增量更新、原始快照
其他
跨代引用如何标记
GC算法
标记复制
新生代算法
新生代大部分对象是朝生夕灭,只需复制少量对象,
而老年代恰恰相反所以不适用
而老年代恰恰相反所以不适用
空间浪费
标记清除
使用空闲列表记录内存空间和大小,会产生内存碎片,虽然
有很多空间,但是每个空间分配不了大对象,提前触发GC
有很多空间,但是每个空间分配不了大对象,提前触发GC
因为产生内存碎片,内存的访问需要通过空闲列表,
所以更加耗时,吞吐量低
所以更加耗时,吞吐量低
标记整理
解决标记清除的内存碎片问题,会往一边移动存活对象,
所以gc停顿会持续更长的时间
所以gc停顿会持续更长的时间
分代算法
为什么要分代
分代可以将大内存分成多个小内存,提升回收效率
对象生命周期不一样,不同代可以选择不同的gc算法,提升速度以及减少gc停顿时间
新生代
Eden
对象第一次分配到Eden区域,大对象直接到老年代,当gc的时候,存活下来
的对象会被复制到survivor区
的对象会被复制到survivor区
对象内存分配线程安全
TLAB
Survivor from
Survivor to
老年代
对象进入老年代
熬过一定gc次数
-XX:MaxTenuringThreshold,默认15
为什么最大是15
大对象直接进入老年代
空间担保机制
动态对象年龄判定
垃圾收集器
Serial
新生代单线程垃圾收集器
Serial Old
是Serial的老年代版本,使用标记-整理算法
ParNew
Serial的多线程版本,是激活CMS后默认的年轻代垃圾回收器
Parallel Scavenge
新生代多线程收集器,复制算法,强调的是高吞吐量,高吞吐量可以最高效率的
利用处理器资源,适合后台运算不需要太多交互的任务
利用处理器资源,适合后台运算不需要太多交互的任务
最大gc停顿时间:-XX:MaxGCPauseMillis、设置吞吐量:-XX:GCTimeRatio
Parallel Old
Parallel Scavenge老年代版本,标记整理算法,吞吐量优先
CMS(Concurrent Mark Sweep)
低停顿老年代收集器
- 大部分工作可以和用户线程并发执行
- 使用标记清除算法,使用空闲列表管理内存
适用于与客户交互的应用,强调低延迟,但是吞吐量相对较低,适合多核的机器
优点
并发收集、低停顿
缺点
和用户线程并行期间占用了cpu时间,会降低吞吐量
处理器越多,影响越小(默认占用四分之一)
并发清除阶段会产生浮动垃圾,留到下一次GC
空间碎片
阶段
初始标记
STW,快速标记GC Roots能直达的对象
并发标记
和用户线程并发执行,从GC roos进行可达性分析,标记存活对象
最终标记
STW,标记并发标记阶段产生对象
并发清除
和用户线程同时执行,清理垃圾对象
concurrent mode failure
cms特有的错误,当清理线程和用户线程并发执行的时候,老年代不能容纳新的对象,则会抛出该错误
影响
退化为Serial Old回收器,暂停用户线程,停顿加长
原因以及避免
空间碎片太多
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=n
触发Full GC的内存阈值太高
-XX:CMSInitiatingOccupancyFraction=N调小
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+UseCMSInitiatingOccupancyOnly
垃圾产生速度超过清理速度
晋升阈值过小
Survivor空间过小
Eden区过小,导致晋升速率提高
存在大对象
Survivor空间过小
Eden区过小,导致晋升速率提高
存在大对象
G1(Garbage First)
设计目的
对gc暂停时间可预测、可配置,对延迟可控的情况下尽可能提高并发量
标记整理
把堆内存划分多个独立相等的Region,每个Region都可能是eden、survivor和old区域。
可以设置最大停顿时间,默认200ms,根据各个Region的回收价值进行选择性回收
可以设置最大停顿时间,默认200ms,根据各个Region的回收价值进行选择性回收
回收过程
初始标记
STW
并发标记
最终标记
STW
筛选回收
STW,这里不是并发的,因为gc停顿时间可控
适用
较大的堆空间,较短的停顿时间
ZGC
诊断/监控工具
命令行
jps
查看虚拟机中进程vmid
jstat
查看一些统计信息
jstat -gc vmid
可以查看各个分区内存占用大小,gc次数、时间
jstat -gcutil vmid
可以查看各个分区内存占比,gc次数、时间
jstat -gccause vmid
可以查看各个分区内存占比,gc次数、时间,以及上一次gc原因
jinfo
查看虚拟机信息以及启动参数等信息
jmap
查看堆信息:jmap -heap vmid
jmap -histo vmid | head -n20
查看堆内存中的存活对象,并按空间排序
转储:jmap -dump:live,format=b,file=文件名.dump vmid
jhat
jhat 文件名.dump
使用浏览器打开 ip:port->port就是jhat命令后输出的port
jstack
获取当前进程的所有线程堆栈信息,以及锁信息
死锁可以看到具体代码行数
jstack vmid
图形化工具
jvisualvm
输入jvisualvm命令即可打开
Visual GC插件
分析dump
MAT
jconsole
arthas
常用调优参数
Xms:初始堆大小
Xmx:最大堆大小
Xmn:新生代大小
-XX:+DisableExplicitGC:禁止调用Systerm.gc()
System.gc()
不会马上gc,也不一定会gc
触发Full GC
-XX:-HeapDumpOnOutOfMemoryError
生产排查
OOM快速定位
可能原因
内存分配过小
不断申请资源没有释放(内存泄露)
队列消费速度远远小于生产速度
不断创建线程,网络连接等
......
步骤
生成dump文件
jps找出进程号,jmap命令生成dump文件
缺点如果oom后进程结束了,就拿不到进程号了
OOM自动生成dump文件,指定存放目录
-XX:+HeapDumpOnOutOfMemoryError,
-XX:HeapDumpPath = xx
-XX:+HeapDumpOnOutOfMemoryError,
-XX:HeapDumpPath = xx
使用MAT(或jvisualvm)分析dump文件,找到占用空间大的对象,点开,定位代码
案例
订单导出重复点击,导致产生大量对象发生oom。
通过导出按钮置灰防止等响应之后再点击
通过导出按钮置灰防止等响应之后再点击
定时任务定时统计,导致周期性发生oom
频繁full gc排查
可能原因
手动触发System.gc()
一些资源对象没有close掉
频繁生成长生命周期对象
大对象
jvm参数不合理,如内存大小
频繁youg gc排查
J.U.C
并行 & 并发
并发:在同一时刻 CPU 能够处理的任务数量
并行:指在多核 CPU 架构下,同一时刻同时可以执行多个线程的能力
线程
分类
用户线程(默认)
jvm退出必须等待所有用户线程结束
守护线程
jvm退出不用等待守护线程结束
thread.setDaemon(true)
是一种特殊的线程,在后台默默地完成一些系统性的服务,比如垃圾回收线程
创建方式
继承Thread重写run、实现Runnable、实现Callable、线程池
线程状态
NEW(新建)
还没有调用start方法
RUNNABLE(可运行)
调用start方法后
RUNNING
获取时间片
BLOCKED(阻塞)
同步块阻塞或IO阻塞
WAITING(等待)
wait()、join()、LockSupport#park()
TIMED_WAITING(超时等待)
sleep(time)、wait(time)、join(time)、LockSupport#parkNanos
TERMINATED(终止)
执行完任务或者异常退出
线程交互
sleep
Thread.sleep(time)
被interupt中断后会清除中断标识
watit/notify/notifyAll
必须在同步代码块里,wait须在notify之前调用
LockSupport
使用了permit(许可证)的概念来做到阻塞和唤醒线程的功
能,每个线程都有一个许可(permit),permit只有两个值1和0
,默认是0
能,每个线程都有一个许可(permit),permit只有两个值1和0
,默认是0
park()
阻塞线程
消耗许可证
unpark(thread)
解除线程阻塞
释放许可证(执行多次也只有一个许可证)
join
等待调用该方法的线程结束
yield
静态方法:Thread.yield()
暗示调度器当前线程放弃cpu时间片,但是调度器可以忽略该暗示
应用
如果某一个线程是不太紧急的线程,那么我们可以在编写时调用yield()这样会让其它线程得到更多的执行机会
FutrueTask的awaitDone方法里有用到。如果任务还在运行中,将cpu让给其他线程
中断
thread.interrupt()
中断线程,只是标记中断,不会真的中断
想要真的中断,目标线程需要有判断中断的逻辑,然后中断
thread.isInterrupted()
返回是否被标记为中断
Thread.interrupted()
消除当前线程中断状态,并返回之前中断状态
锁
死锁
多线程情况下,互相等待对方释放锁资源导致死循环
排查
arthas
jstack打印线程状态信息
解决
顺序锁
使用相同的加锁顺序
轮询锁
while(true)里使用tryLock,获取失败适度休眠一会
超时锁
锁设置超时时间
乐观锁&悲观锁
volatile
可见性
为什么产生不可见性
cpu和内存的性能差距,使得cpu加了一个缓存来平衡这种差距。
而且现在都是多核的,就导致cpu之间共享变量的副本值不一致
而且现在都是多核的,就导致cpu之间共享变量的副本值不一致
JMM(Java内存模型)
线程对变量的所有操作(读,写)都必须在工作内存中进行,不能直接操作主内存中的数据
不同线程之间 也不能直接访问对方工作内存中的变量,线程间的变量值传递必须通过主内存进行中转传递
解决
一个线程对volatile的变量修改后,会写入主存同时立即
通知其他cpu缓存的变量副本失效。其他线程需要从主存
中读取最新的值
通知其他cpu缓存的变量副本失效。其他线程需要从主存
中读取最新的值
如何实现?
MESI(缓存一致性协议)
MESI(缓存一致性协议)
嗅探技术
各个cpu和主存通过总线进行通信。
嗅探总线检查本地缓存是否失效
嗅探总线检查本地缓存是否失效
产生问题:总线风暴(本地延迟)
当有大量线程共享一个对象或者有大量volatile
修饰的变量时候,由于总线通信能力的限制,
会产生一定的延迟
修饰的变量时候,由于总线通信能力的限制,
会产生一定的延迟
有序性
禁止重排序
内存屏障
编译器在适当位置插入指令禁止重排序
对volatile域写操作
前插入StoreStore屏障
后插入StoreLoad屏障
对volatile域读操作
后插入StoreLoad屏障
后插入StoreStore屏障
单例模式双重检查
为什么要valatile修饰?对象创建不是原子性的,包括分配内存,实例化,返回地址引用,
修饰后禁止重排序,这样就能保证在对象初始化完了之后才把singleton指向分配的内存空间
修饰后禁止重排序,这样就能保证在对象初始化完了之后才把singleton指向分配的内存空间
happens-before
volatile的变量写操作happen-before后面任何对此volatile变量的读操作
synchonized
用法
修饰代码块
反编译后再同步块开始和结束处分别加了 monitorenter 和 monitorexit 两个指令,
当线程执行到monitorenter处时候会获取对象的监视器,获取成功才可以执行代码块中的代码
当线程执行到monitorenter处时候会获取对象的监视器,获取成功才可以执行代码块中的代码
修饰实例方法
monitor即当前对象
修饰静态方法
monitor即当前类的Class对象,是该类的全局锁
特性
原子性
确保线程互斥的访问同步代码,不被其他线程打断
可见性
读取共享变量每次都是从主内存获取最新的值,写共享变量每次都会刷新到主内存
有序性
有效解决重排序问题,即 “一个unlock操作先行发生(happens-before)于后面对同一个锁的lock操作”
锁升级
对象内存布局
对象头
Mark Word
存储hashCode,gc分代年龄,锁状态标识,线程持有的锁,偏向线程id等信息
类型指针
通过它可以找到对象属于哪个类
数组长度(对象是数组才有)
实例数据
对齐填充
无锁->偏向锁->轻量级锁->重量级锁
(不可逆)
(不可逆)
锁消除(无锁)
JVM检测到共享数据不存在竞争,则会进行锁消除
依据逃逸分析
比如Vector,StringBuffer是线程安全的类,内部进行了同步操作,
但是如果对象没有逃逸到方法之外,那么则会锁消除
但是如果对象没有逃逸到方法之外,那么则会锁消除
偏向锁
默认开启,-XX:-UseBiasedLocking关闭
没有并发竞争的情况下,线程获取锁,将线程id写入到锁对象头mark word的偏向线程id上,
下次该线程再次要获取锁时,发现偏向线程id是当前线程话则直接获取到锁
下次该线程再次要获取锁时,发现偏向线程id是当前线程话则直接获取到锁
轻量级锁
自旋锁
默认开启,可以通过 -XX:+UseSpinning 开启,默认自旋10次,可以通过 -XX:PreBlockingSpin 调整
优点
发现锁被占用,通过自旋等待锁释放,而不是挂起切换到内核态,从而提升性能
缺点
如果未获取到锁,造成cpu时间浪费
当自旋到限定次数后,如果没有获取到锁,则会膨胀为重量级锁,从用户态
切换到内核态,相比直接使用重量级锁,多了自旋的cpu占用时间
切换到内核态,相比直接使用重量级锁,多了自旋的cpu占用时间
自适应自旋锁
会根据以往经验自动调节自旋次数。比如以往自旋9次获取锁了,那么这次很可能获取到锁,
则增加自旋次数。如果上次没有获取到,则这次很可能获取不到锁,则减少自旋次数甚至
跳过自旋直接碰撞为重量级锁
则增加自旋次数。如果上次没有获取到,则这次很可能获取不到锁,则减少自旋次数甚至
跳过自旋直接碰撞为重量级锁
总线风暴
重量级锁
依赖于操作系统的Mutex Lock实现,需要从用户态切换到内核态,性能较差
ThreadLocal
存储线程本地变量
应用场景
PageHelpler插件存放分页信息
Spring的事务
存储上下文信息
spring判断是否产生循环依赖
源码
ThreadLocalMap
Thread的属性threadLocals是ThreadLocalMap类
初始化是在ThreadLocal#set和get方法里发生的
是ThreadLocal的内部类
key是弱引用,类型是ThreadLocal,value是Object
只有数组,没有链表
如何解决哈希冲突
寻找数组下一个空位置,直到为空为止
set(T value)
get()
remove()
内存泄露
原因
当我们new一个ThreadLocal时候对其强引用,当线程执行结束后,不会对其强引用。
但是Entry(threadLocal,object)中key是弱引用,gc会清除,如果线程没有销毁比如线
程池,那么threadLocalMap则被强引用,则threadLocal对象会被清除,但是value
不会被清除,导致内存泄露
但是Entry(threadLocal,object)中key是弱引用,gc会清除,如果线程没有销毁比如线
程池,那么threadLocalMap则被强引用,则threadLocal对象会被清除,但是value
不会被清除,导致内存泄露
是否存在threadLocal还没有用完,就被gc回收了
不会:
因为我们new了一个threadLocal对象,是强引用。也就是说threadLoacl同时被强引用和弱引用,所以不会被gc。
只有代码逻辑执行结束,不再对其强引用,才会被gc。
因为我们new了一个threadLocal对象,是强引用。也就是说threadLoacl同时被强引用和弱引用,所以不会被gc。
只有代码逻辑执行结束,不再对其强引用,才会被gc。
避免
主动:使用完执行remove()
被动:源码自己做了优化,如果get,set,remove方法被调用如果发现key=null的话会把value也置为null
InheritableThreadLocal
可以将值传给子线程
当在父线程创建子线程的时候,会在init方法里将父线程的
inheritableThreadLocals属性传递给子线程
inheritableThreadLocals属性传递给子线程
必须在父线程里面new子线程,才会传递。
如果父子线程使用线程池则不会传递
如果父子线程使用线程池则不会传递
AQS
是什么
AbstractQueuedSynchronizer
volatile int state
竞争资源标识
exclusiveOwnerThread
记录持有锁的线程
内部类Node
volatile Thread thread
volatile int waitStatus
0
CANCELLED = 1
SIGNAL = -1
标识后继节点需要被唤醒,-1的值也是后继节点加入到阻塞队列时候改的
CONDITION=-2
标识当前node在条件队列中
PROPAGATE=-3
volatile Node prev
volatile Node next
Node nextWaiter
条件队列使用,形成单向链表
volatile Node head
头节点持有锁的线程,不包含在阻塞队列中
volatile Node tail
内部类ConditionObject
Node firstWaiter
Node lastWaiter
Node通过nextWaiter形成单向链表
5个未实现的方法
tryAcquire、tryRelease。
tryAcquireShared、tryReleaseShared。
isHeldExclusively(用于判断是否排它锁)
tryAcquireShared、tryReleaseShared。
isHeldExclusively(用于判断是否排它锁)
实现类
ReentrantLock
抽象类Sync extends AQS
NonfairSync
先通过CAS state值,如果成功则返回获取成功。否则将当前线程封装成Node节点,放入到阻塞队列的队尾,并挂起线程。
当持有锁的线程释放锁时候,则唤醒在阻塞队列里head节点下一个节点的线程,也就是从刚挂起的地方继续执行死循环,
如果CAS state成功,则抢占到锁,并把自己设置为head节点,退出循环
当持有锁的线程释放锁时候,则唤醒在阻塞队列里head节点下一个节点的线程,也就是从刚挂起的地方继续执行死循环,
如果CAS state成功,则抢占到锁,并把自己设置为head节点,退出循环
FairSync
不会先CAS state值,然后会看是否有前继结点(hasQueuedPredecessors()),如果有的话直接加入到队尾
公平锁 & 非公平锁优缺点对比
公平锁
所有线程都能获取到锁,不会出现饿死
吞吐效率低(每个线程都需要唤醒)
非公平锁
吞吐效率高(抢到锁的线程无需挂起再唤醒)
导致等待队列中的线程饿死或者等待太久
重写了tryAcquire、tryRelease和isHeldExclusively
ConditionObject
条件队列
是一个单向链表,当调用await方法会放入到同步队列,当signal/signalAll则会将同步队列的节点再放入到阻塞队列队尾
synchonized & ReentrantLock区别
前者是jvm层面的关键字,后者是java类
前者被动释放锁,后者主动释放
前者只能是非公平锁,后者皆可
前者是重量级锁,需要内核态和用户态切换,后者不需要
后者拥有更丰富的api
tryLock()、tryLock(time)、lockInterruptibly()等
前者不能知道是否获得了锁,后者可以知道
前者不可中断,后者可以
CountdownLatch
内部类Sync继承AQS实现了tryAcquireShared和tryReleaseShared两个方法。
构造方法设置state的值,latch.countDown()则state-1,
当state=0的时候,唤醒阻塞在latch.await()方法的线程
构造方法设置state的值,latch.countDown()则state-1,
当state=0的时候,唤醒阻塞在latch.await()方法的线程
CyclicBarrier
没有实现AQS,只是利用了Condition条件队列,调用await会调用Condition#awati()并count - 1,
当最后一个线程(count = 0)调用await会singnalAll(),同时会new Generation
当最后一个线程(count = 0)调用await会singnalAll(),同时会new Generation
相比CountdownLatch,有一个Generation的概念,对count重置然后重复使用,
而且可以在最后一个到达线程执行一个Runnable方法(构造方法传入)
而且可以在最后一个到达线程执行一个Runnable方法(构造方法传入)
Semaphore
内部类Sync继承AQS,有NonfairSync和FairSync模式,重写了tryAcquireShared和tryReleaseShared
acquire()和realease()
应用
拓展容器类,实现阻塞容器
ReentrantReadWriteLock
state的高16位表示读状态,低16位表示写状态
读锁重写tryAcquireShared 和 tryReleaseShared
每个线程维护一个计数器记录重入次数目的就是实现可重入锁
写锁重写tryAcquire 和 tryRelease
当线程获取读锁后,该线程不能再获取写锁。
当线程获取写锁后,可以继续获取读锁
当线程获取写锁后,可以继续获取读锁
ThreadPoolExecutor内部类Worker
Atomic类
AtomicInteger/AtomicLong
java7
java8
AtomicBoolean
AtomicIntegerArray
AtomicReference
可以把多个变量放在一个对象里来进行CAS操作
AtomicStampedReference
解决ABA问题,内部维护了对象和stamp
LongAdder
通过数组分担并发压力,提升性能(分治)
和AtomicLong性能对比测试
add(long x)
根据cells数组是否初始化,cas操作base是否成功来决定是否要走数组计算的逻辑
数组已经初始化或者casBase失败则走cells数组的逻辑
数组初始化或扩容(2幂次方,最大为cpu数)然后对cell元素进行cas累加
如果cells正在初始化则cas累加base值
sum()
base + 数组所有元素和
add方法没有返回值,想要获取当前值就得调用sum方法,得到的不是一个精确值
ThreadPoolExecutor
作用
提升响应速度
避免频繁的创建和销毁线程
防止创建过多线程
管理和监控线程
源码
构造参数
corePoolSize
prestartAllCoreThreads():线程池会提前创建并启动所有核心线程
allowsCoreThreadTimeOut() : 允许核心线程空闲回收
maximumPoolSize
空闲线程过多会占用内存,空闲销毁
为什么要队列满了才创建到最大线程数,而不是直接创建到最大线程数
BlockingQueue<Runnable> workQueue
阻塞队列主要成员
keepAliveTime + timeUnit
空闲存活时间,当达到设定的时间从队列里获取不到任务后则会销毁线程
0表示执行完任务非核心线程立即终止
ThreadFactory
可以设置线程名称、守护线程,线程优先级
RejectedExecutionHandler
AbortPolicy(default)
DiscardPolicy
DiscardOldestPolicy
CallerRunsPolicy
风险
自定义
AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0))
保证对运行状态和线程数量操作的原子性,无需使用锁同步,提升了性能
获取线程池状态
高3位记录线程池的运行状态
c & CAPACITY => c & 00011111111111111111111111111111 => 3个0+c的后29位二进制
获取线程数量
低29位记录线程数量
c & ~CAPACITY => c & 11100000000000000000000000000000 => c 保留前3位二进制 + 29个0
内部类Worker
实现Runnable
线程池里每个线程就是一个worker
职责
执行firstTask
getTask():从阻塞队列拉取任务执行
继承AQS
重写tryAcquire(),实现非重入锁
在执行runWorker方法里执行task逻辑时候,会先lock,执行完unlock,这个锁不可重入的
why
因为在比如shutdown、setCorePoolSize,setMaximumPoolSize等方法也会使用worker实现的不可重入锁,
原因就是当worker正在执行任务的时候,而这些方法会对线程进行中断(调用interruptIdleWorkers()),
那么就会影响到当前正在执行的任务
原因就是当worker正在执行任务的时候,而这些方法会对线程进行中断(调用interruptIdleWorkers()),
那么就会影响到当前正在执行的任务
shutdownNow()则不会tryLock,而是直接中断线程
提交任务流程
如果线程池中线程数量小于核心线程数(>0),则新建线程执行任务
核心线程数=0,则直接丢进队列,并且开启一个线程去队列拉取任务执行
如果线程池中数量大于等于核心线程数,且队列未满,则将任务放入到队列
如果队列也满了,则看线程池中线程数量是否小于最大线程数,如果没有则新建线程执行任务
如果已经达到最大线程数且队列已满,则执行拒绝策略
Executors工具类
静态工厂创建几种常用线程池
newFixedThreadPool
核心线程数=最大线程数,不会回收线程
LinkedBlockingQueue
因为核心线程不会被回收,所以内部类worker对象会一直持有外部类ThreadPoolExecutor(workQueue.take())引用,导致其不会被gc。
所以需要手动shutDown(),关闭线程池,否则无法被gc造成内存泄漏
所以需要手动shutDown(),关闭线程池,否则无法被gc造成内存泄漏
newSingleThreadExecutor
核心线程数=最大线程数=1,线程不会空闲关闭
LinkedBlockingQueue
线程不会被回收,但是也无需shutDown(),因为被FinalizableDelegatedExecutorService包装了,
gc会调用finalize(),从而隐式调用shutDown()
gc会调用finalize(),从而隐式调用shutDown()
单线程的线程池意义
保证任务按照提交的顺序执行
newCachedThreadPool
核心线程数=0,最大线程数不限制,60s空闲则回收掉
SynchronousQueue
不存储元素,每一个put必须等待take操作
因为线程会空闲被全部回收,无需手动shutDown()
场景
执行大量短暂的异步任务,提升性能
nacos长轮询线程池
newScheduledThreadPool
DelayedWorkQueue
ScheduledExecutorService#schedule(commod,delay,unit)
只执行一次
ScheduledExecutorService#scheduleAtFixedRate(commod,initialDelay,period,unit)
以固定的频率
ScheduledExecutorService#scheduleWithFixedDelay(commod,initialDelay,delay,unit)
以固定的延时
会考虑任务执行时间,进行顺延
使用
线程数设置
I/O密集型
cpu核数 * 2
CPU密集型
cpu核数 +1
动态化设置
监控线程池,通过配置中心快速调整参数,减少故障恢复时间
监听配置变化
setCorePoolSize(int)
setMaximumPoolSize(int)
线程空闲时间设置
可动态设置setKeepAliveTime()
队列设置
类型选择
大小设置
队列的容量
没有动态设置队列容量的方法
容量被final修饰
解决:自定义队列,可修改容量
Hippo4j
支持动态设置参数、自定义报警和运行监控
线程饥饿死锁
父子任务使用同一个线程池,父任务用完所有线程,子任务获取不到线程,导致等待死锁
分别定义不同的线程池
线程执行task发生异常,线程池会怎么处理这个线程
exectute提交会抛出堆栈异常,submit不会,可以通过future.get()获取异常
不影响其他线程执行任务
线程池会移除这个线程,并创建一个新线程放入到线程池
BlockingQueue
ArrayBlockingQueue
添加元素
add(e)
队列满则抛出IllegalStateException("Queue full")
offer(e)
返回true和或false
offer(e,time,unit)
如果队列满了,则阻塞,到了设定时间没有成功则返回false
put(e)
如果队列满了则阻塞
取出元素
remove(e)
返回true或false
remove()
没有则NoSuchElementException
poll()
有元素则返回元素,否则返回null
poll(time,unit)
如果不存在元素,则阻塞指定时间,没有则返回null
take()
队列为空则阻塞
源码
构造方法
底层是数组
添加和取出方法都使用了ReentrantLock进行同步
条件队列:notEmpty = lock.newCondition()
条件队列:notFull = lock.newCondition()
takeIndex
take获取元素的下标,然后++takeIndex,如果等于队列长度,则赋值为0
putIndex
put元素的下标,然后++putIndex,如果等于队列长度,则赋值为0
LinkedBlockingQueue
SynchronousQueue
不能存储元素, 每个put()都必须等到一个take(),才能解除阻塞, 反之亦然
PriorityBlockingQueue
其他
FutureTask
通过实现RunnableFuture间接
继承了Runnable和Future接口
继承了Runnable和Future接口
tips:接口可以多继承
构造方法
入参为Callable
入参为Runnalbe
通过适配器的方式将Runnable转化为Callable
runnable返回值为void,传入Result有何意义?
可以通过get方法知道call方法有没有执行完毕
可以通过get方法知道call方法有没有执行完毕
task的运行状态
get()
首先会判断任务状态,如果是<=completing则包装成waitNode放入到等待队列里,并且LockSupport.park当前线程。
否则根据任务是否是normal或者exceptional等返回任务运行结果或者异常
否则根据任务是否是normal或者exceptional等返回任务运行结果或者异常
总结
根据任务的状态值决定是阻塞、返回结果还是抛出异常
run()
执行run方法,最终会执行callable#call(),不管正常还是异常,都会设置结果,并且会设置
任务状态为Nomal或者Exceptional,最后LockSupport.unpark唤醒之前get里阻塞的线程
任务状态为Nomal或者Exceptional,最后LockSupport.unpark唤醒之前get里阻塞的线程
总结
执行任务,设置执行结果,唤醒阻塞的线程
CompletableFuture
Future局限性
get方法获取执行结果,是阻塞的
无法对多个任务进行链式调用
无法组合多个任务
没有异常处理
解决Future的局限性的问题
ForkJoin框架
用于并行执行任务的框架,把一个大任务分割成若干个小任务,
最终汇总每个小任务结果的后得到大任务结果的框架
最终汇总每个小任务结果的后得到大任务结果的框架
适合
计算密集型任务
流程
fork-任务分解
任务执行
工作窃取
当线程完成队列中任务空闲时,去别的队列获取任务执行
join-结果合并
开发框架
Spring
IOC
设计思想
控制反转,由使用者创建管理对象变成spring自动创建和管理对象,使用者只需要从容器中获取即可
DI,依赖注入,spring帮助我们自动管理bean的依赖关系
定位->加载->注册
AbstarctApplicationContext#refresh(),会通过解析xml或者扫描注解,然后生成BeanDefinition,
注册到beanDefinitionMap中,后面会进行实例化,属性注入以及初始化后放入到singletonObjects缓存中
注册到beanDefinitionMap中,后面会进行实例化,属性注入以及初始化后放入到singletonObjects缓存中
循环依赖
Spring中循环依赖几种情况
构造器的循环依赖
Spring解决不了,因为加入singletonFactories三级缓存的前提是执行了构造器,所以构造器的循环依赖没法解决
setter方式原型
Spring解决不了,检测到则会抛出异常BeanCurrentlyInCreationException
setter方式单例
三级缓存
一级:singletonObjects
存放实例化好的单例对象(成品)
二级:earlySingletonObjects
提前曝光的单例对象,还没有完全初始化好(半成品)
三级:singletonFactories
要被实例化对象的对象工厂
通过二级缓存提前暴露了不完整的依赖对象的引用,从而解决循环依赖问题
如何判定发生循环依赖
比如A依赖B,B依赖A,当创建A时候,会将A的beanName进行保存(ThreadLocal),标识A正在创建。
发现依赖B,会去创建B,而B又依赖A,发现A还在创建中,判定A和B循环依赖了
发现依赖B,会去创建B,而B又依赖A,发现A还在创建中,判定A和B循环依赖了
只要二级缓存可以吗?
为什么必须要有第三级缓存?
为什么必须要有第三级缓存?
不可以,第三级缓存实现aop的。
1、如果没有三级,那么二级只能存被代理的普通bean。
2、而三级缓存存放的是singletonFactory,这是一个函数式接口也就是内部类,当从三级获取的时候,
会调用singletonFactory#getObject方法会判断bean是否需要被代理,如果需要则会对其动态代理返回代理对象,否则返回普通bean。
1、如果没有三级,那么二级只能存被代理的普通bean。
2、而三级缓存存放的是singletonFactory,这是一个函数式接口也就是内部类,当从三级获取的时候,
会调用singletonFactory#getObject方法会判断bean是否需要被代理,如果需要则会对其动态代理返回代理对象,否则返回普通bean。
Bean生命周期
实例化-》依赖注入-》初始化-》销毁
平时对生命周期做了哪些拓展
ApplicationContextAware设置ApplicationContext
Dubbo
BeanFactory & FactoryBean & ApplicationContext
BeanFactory
BeanFactory是IOC顶层接口,负责生产和管理bean
ApplicationContext
继承了BeanFactory,对其进行功能增强:国际化、事件发布、资源访问等
FactoryBean
spring分为两种bean,一种是普通bean,一种的实现了FactoryBean的bean。
通过getBean(beanName)获取的是getObject()返回的对象,
想要获取FactoryBean本身,则通过getBean(& + beanNmae)获取
通过getBean(beanName)获取的是getObject()返回的对象,
想要获取FactoryBean本身,则通过getBean(& + beanNmae)获取
通常用来创建一些比较复杂的bean,比如dubbo的ReferenceBean、spring的MapperFactoryBean
@Autowired & @Resource
@Autowired
AutowiredAnnotationBeanPostProcessor在容器启动的时注册
AutowiredAnnotationBeanPostProcessor#postProcessProperties实现
当自动装配时,从容器中如果发现有多个同类型的属性时,@Autowired注解会先根据类型判断,然后根据@Primary、@Priority注解判断,
最后根据属性名与beanName是否相等来判断,如果还是不能决定注入哪一个bean时,就会抛出NoUniqueBeanDefinitionException异常
最后根据属性名与beanName是否相等来判断,如果还是不能决定注入哪一个bean时,就会抛出NoUniqueBeanDefinitionException异常
@Resource
CommonAnnotationBeanPostProcessor在容器启动时注册
CommonAnnotationBeanPostProcessor#postProcessProperties
区别
@Autowired 是 Spring 定义的注解,
@Resource 是 JSR 250 规范里面定义的注解,而 Spring 对 JSR 250 规范提供了支持
@Resource 是 JSR 250 规范里面定义的注解,而 Spring 对 JSR 250 规范提供了支持
@Autowired 是根据 type 来匹配。如果需要支持 name 匹配,就需要配合@Primary 或者@Qualifier
来实现
@Resource 可以根据 name 和 type 来匹配,默认是 name 匹配
来实现
@Resource 可以根据 name 和 type 来匹配,默认是 name 匹配
@Bean & @Component
前者作用于方法,后者作用于类
当需要把第三方库中的类装配到 Spring 容器时,通过 @Bean 来实现
Spring将配置文件解析成什么后注册到容器?
BeanDefinition
BeanFactoryPostProcessor & BeanPostProcessor
BeanFactoryPostProcessor
容器后处理器,spring预留给我们的拓展点,可以在所有bean注册之后(实例化之前),获取beanDefinition并做一些处理
BeanPostProcessor
可在bean初始化过程中,对bean做一些前置和后置处理
spring AOP、@Autowired等都是通过BeanPostProcessor实现
InstantiationAwareBeanPostProcessor
继承BeanPostProcessor
作用
定义了bean 实例化前后的方法,设置属性以及初始化前后方法的方法
实现
AutowiredAnnotationBeanPostProcessor
@Autowire、@Value等
AbstractAutoProxyCreator
spring AOP
scope
singleton、prototype、request、session、global-session
单例A依赖原型B
解决方案
A注入ApplicationContext,每次从其getBean(B.class)重新获取B对象
@Lookup
bean是线程安全的吗
spring没有提供线程安全策略
分析
protoype类型的bean
每次都会创建bean,不存在线程共享,所以是安全的
singleton类型的bean
无状态bean
只会查询成员变量,所以安全
有状态
会修改成员变量,不安全
解决
将成员变量保存在ThreaLocal中
对成员变量的修改使用锁或者使用线程安全的数据结构
作用域从singleton改为prototype
AOP
面相切面编程,将与业务无关的、通用的代码逻辑(比如日志、事务等等)抽取并封装起来,并通过动
态代理的方式作用于目标对象和方法,从而达到了避免重复代码,与目标对象解耦的目的
态代理的方式作用于目标对象和方法,从而达到了避免重复代码,与目标对象解耦的目的
概念
通知(Advice)
需要执行的操作,包括了 before、after、around 等多种类型
类型
前置通知
@Before
@Before
返回后通知
@AfterReturning
@AfterReturning
在目标方法正常执行并返回之后执行的通知,目标方法异常则不会执行
后置通知
@After
@After
在目标方法执行完成,目标方法正常&异常都会执行
环绕通知
@Around
@Around
异常通知
@AfterThrowing
@AfterThrowing
切点(Pointcut)
一组连接点的集合,它用来定义哪些方法需要被增强
切面(Aspect)
一个包含切点和通知的对象,它将切点和通知组合在一起
源码实现
在bean的初始化阶段,执行到一个AbstractAutoProxyCreator的BeanPostProcessor后置处理器的后置方法(postProcessAfterInitialization),
会调用wrapIfNessary方法判断是否需要被代理,需要则对其进行动态代理,织入切面,从而返回代理bean
会调用wrapIfNessary方法判断是否需要被代理,需要则对其进行动态代理,织入切面,从而返回代理bean
动态代理
jdk(spring默认)
被代理类实现接口
cglib(springboot默认)
被代理类不是接口
proxy-target-class = true
AOP
Spring AOP
动态织入(运行阶段),通过动态代理的方式实现织入
只能作用于 Spring 容器中的 bean。通过bean的生命周期拓展点的beanPostProcessor的后置方法对bean进行代理
获取被代理对象:Class<?> clazz = AopUtils.getTargetClass(Object candidate)
AspectJ
静态织入(编译阶段、编译后 或 类加载),基于修改字节码。
是通用型代理技术(功能更强大),使用复杂
是通用型代理技术(功能更强大),使用复杂
Spring MVC
请求流程
过滤器和拦截器区别
过滤器属于servlet,拦截器属于spring mvc
过滤器基于函数回调实现,拦截器基于反射实现
过滤器作用于request和response,拦截器作用于controller
统一异常处理
事务
7大传播行为
(PROPAGATION)
(PROPAGATION)
REQUIRED
NESTED
REQUIRES_NEW
SUPPORTS
NOT_SUPPOSRTED
MANDATORY
NEVER
实现方式
编程式
TransactionTemplate#execute
可实现更细粒度的事务控制,避免长事务
(比如方法中一些不需要在事务中逻辑,如果用声明式的话,就是长事务了)
(比如方法中一些不需要在事务中逻辑,如果用声明式的话,就是长事务了)
声明式
@Transactional失效
异常被catch并且没有再抛出
异常类型
被标注的方法不是public修饰(只能是public)
jdk动态代理是接口,接口不能定义私有方法
cglib的话,继承,私有方法不能被继承
(自调用)同一个Service里,普通方法调用被标注的方法,
因为事务是基于动态代理实现的,普通方法不会被代理
因为事务是基于动态代理实现的,普通方法不会被代理
所用数据源是否加载了事务管理器
数据库的存储引擎不支持事务
被标注的方法所在的类没有被Spring所管理
传播行为是否正确,比如NOT_SUPPORTED就会挂起事务
原理
因为通过aop实现,所以最终走到TransactionInterceptor拦截器执行invoke方法
获取事务的属性、事务管理器,然后根据事务传播行为是加入事务、挂起当前事务创建新事务等。
然后执行目标方法,发生异常根据异常类型判断是回滚还是提交,如果执行成功则提交事务。
然后执行目标方法,发生异常根据异常类型判断是回滚还是提交,如果执行成功则提交事务。
通过threadLocal保证获取的是同一个连接
TransactionSynchronization
同步器,事务提交前后会从threadLocal中获取注册的同步器执行对应的方法
注册
TransactionSynchronizationManager #
registerSynchronization(TransactionSynchronization synchronization)
registerSynchronization(TransactionSynchronization synchronization)
添加到threadLocal中列表中,
在事务节点中(事务完成,事务提交等)遍历列表执行对应方法
在事务节点中(事务完成,事务提交等)遍历列表执行对应方法
异步
使用
启动类或配置类+@EnableAsync开启异步
可以实现AsyncConfigurer来指定executor和可以实现AsyncConfigurer
来指定Executor和AsyncUncaughtExceptionHandler
来指定Executor和AsyncUncaughtExceptionHandler
方法或类+@Async
不要返回值直接void;需要返回值用AsyncResult或者CompletableFuture
可自定义执行器,@Async(”executorBeanName“)
动态代理,自调用会失效
解决
将异步方法放入到另一个类中
从容器中获取代理对象进行调用
原理
@Async注解的拦截器是AsyncExecutionInterceptor
拦截器拦截把需要异步执行的方法包装成task丢到线程池异步执行
如果不指定Executor(@Async)
且没有重写AsyncConfigurer
且没有重写AsyncConfigurer
默认会从spring容器中唯一的TaskExecutor bean,没有唯一的则搜索名为“taskExecutor”的Executor bean。
如果都没有则使用SimpleAsyncTaskExecutor来处理任务,每次都new线程,不会复用线程
如果都没有则使用SimpleAsyncTaskExecutor来处理任务,每次都new线程,不会复用线程
设计模式
单例模式
原型模式
代理模式
Spring aop
模板方法
JdbcTemplate
观察者模式
ContextRefreshEvent
工厂方法
FactoryBean
适配器模式
spring mvc中HandlerAdapter适配各种Controller
装饰器模式
Spring的BeanWrapper允许在不修改原始Bean类的情况下添加额外的功能
责任链
aop拦截链
策略模式
SpringBoot
@SpringBootApplication复合注解
@SpringBootConfiguration(用作@Configuration替代品)
说明被SpringBootApplication
所标识的Java类就是一个Java配置类
所标识的Java类就是一个Java配置类
@ComponentScan(加载内部组件到容器中)
可以从basePackageClasses或basePackages来定义要扫描的包。
如果没有定义,则从声明此注解的类的包中进行扫描
如果没有定义,则从声明此注解的类的包中进行扫描
@EnableAutoConfiguration(加载外部的组件到容器中)
打开自动装配功能,到META-INF/spring.factories文件中加载需要自
动注入的Java类到容器中 (SPI机制)
动注入的Java类到容器中 (SPI机制)
原理
@Import(
AutoConfigurationImportSelector.class
)
AutoConfigurationImportSelector.class
)
@import
作用:表示要导入的一个或多个组件类
常规组件,比如@service、@comment、@controller等
标记@Configuration类
ImportSelector实现类进行动态注入
ImportBeanDefinitionRegistrar实现类进行动态注入
AutoConfigurationImportSelector
1、扫描META-INF/spring-autoconfigure-metadata.properties
ConditonalOnClass、ConditonMissingClass等,根据条件判断是否加载
2、再扫描META-INF/spring.factories
SpringFactoriesLoader
在spring.factories 文件中,根据
org.springframework.boot.autoconfigure.EnableAutoConfiguration = xxxx
来读取需要加载的类的全路径xxxx
org.springframework.boot.autoconfigure.EnableAutoConfiguration = xxxx
来读取需要加载的类的全路径xxxx
3、用前者过滤后者数据,最后返回需要注入的类的全路径数组
自动装配流程
Starter
what
starter的作用是在META-INF目录下提供了一个spring.factories文件,
在该文件中我们添加了一个需要注入到Spring容器中的对应的配置类。
在该文件中我们添加了一个需要注入到Spring容器中的对应的配置类。
自定义Starter
实际应用
为什么能java -jar xx.jar启动
通过JarLaucher的main方法:
1、创建自定义类加载器(LaunchedURLClassLoader)
2、并设置为上下文的类加载器
3、最后执行真正的启动类的main方法,使用内置tomcat运行应用
1、创建自定义类加载器(LaunchedURLClassLoader)
2、并设置为上下文的类加载器
3、最后执行真正的启动类的main方法,使用内置tomcat运行应用
跨域
当一个请求url的 协议、域名、端口 三者之间任意一个与当前页面url不同即为跨域
三种解决方案
配置CorsFilter
重写WebMvcConfigurer
@CrossOrigin
定义在Controller类上
定义在方法上
和spring关系
MyBatis
源码
解析
解析配置文件封装成Confiuration类
Configuration
解析mapper.xml文件,放到knownMappers的map容器
knownMappers<mapper接口.class,MapperProxyFactory>
mapper为啥没有实现类
执行流程
SqlSessionFactory开启一个sqlSession,根据mapper接口从knownMappers中拿到代理工厂(mapperProxyFactory),并生成代理对象MapperProxy
调用MapperProxy的invoke方法,最终执行MapperMethod#execute
MapperMethod#execute,根据根据sql的增删改查的类型走相应的逻辑
查询执行sqlSession的select方法
根据是否开启二级和一级缓存执行相应逻辑
开启二级缓存
命中返回,否则查询数据库
二级缓存逻辑在CachingExecutor
未命中则继续走BaseExecutor,然后设置二级缓存
未开启二级缓存
直接走BaseExecutor
增删改执行sqlSession的update方法
会清空所有缓存
缓存
一级缓存
Session级别,维护在BaseExecutor
开启&关闭
默认开启(无法关闭)。
每次查询其实都会设置一级缓存
每次查询其实都会设置一级缓存
flushCache(默认false)
查询前,如果设置为ture则会删除一级缓存
localCacheScope(默认SESSION)
设置STATEMENT会删除一级缓存
hit condition
- flushCache=false
- localCacheScope=session
- 同一个sqlSession
- 相同statementId
- 相同的sql,参数
- 没有执行update操作
与Spring整合
spring每次都会new一个sqlSession,所以不是同一会话,一级缓存会失效,除非开启了事务,使用同一会话
二级缓存
应用级别,维护在CachingExecutor。因为不是会话级,所以是事务缓存
开启
mapper.xml配置<cache></cache>即可。在方法里配置useCache为false,即可关闭某个sql的二级缓存
hit condition
- 相同的statement id
- 相同的Sql与参数
- 没有使用ResultHandler来自定义返回数据
- 没有配置useCache=false 来关闭缓存
- 没有配置flushCache=true 来清空缓存
TransactionalCacheManager
query方法设置二级缓存时候并未提交,而是在会话结束时候,关闭会话里提交或者回滚事务缓存
集群模式下会出现一致性问题
mybatis-redis
四大组件
Executor
方法分为三类
执行
事务
缓存
BaseExecutor
SimpleExecutor(默认)
ReuseExecutor
BatchExecutor
CachingExecutor
开启二级缓存则装饰BaseExecutor
StatementHandler
负责处理Mybatis与JDBC之间Statement的交互
创建statement、sql参数赋值、和数据库交互等
配置:statementType
实现
SimpleStatementHandler
PreparedStatementHandler(默认)
CallableStatementHandler
RoutingStatementHandler
ParameterHandler
ResultSetHandler
Plugin
通过拦截四大组件进行相应的操作
PageHelper、sql监控、分表等
TypeHander
Java类型和JDBC类型的映射
可以自定义
实体属性和表字段不一致
指定别名
resultMap
ResultType/ResultMap区别
ResutType
只适用于基本类型以及简单pojo
ResultMap
可以映射表字段和实体属性,支持一对一(association)和一(多)对多(collection)
如何获取生成的主键
动态SQL
<if>、<where>、<set>、<foreach>等
设计模式
建造者
SqlSessionFactoryBuilder
工厂
SqlSessionFactory
模板方法
BaseExecutor子类有SimpleExecutor,BatchExecutor和ReuseExecutor
装饰器
CachingExecutor装饰BaseExecutor
PerpetualCache被FifoCache、LruCache、WeakCache等装饰
代理
MapperProxy
单例
ErrorContext,线程级别单列
责任链
插件
spring整合mybatis原理
微服务框架
Dubbo(2.6.x)
RPC & 服务治理框架
1、解决分布式系统中,服务之间的调用问题。
2、远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。
2、远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。
设计
设计层级(10层)
Service:业务层,就是咱们开发的业务逻辑层。
Config配置层:主要围绕 ServiceConfig 和 ReferenceConfig,初始化配置信息。
Proxy 代理层:服务提供者还是消费者都会生成一个代理类,使得服务接口透明化,代理层做远程调用和返回结果。
Register 注册层:封装了服务注册和发现
Cluster 路由和集群容错层:负责路由、负载均衡、容错等
Monitor 监控层:负责监控统计调用时间和次数。
Portocol 远程调用层:主要是封装 RPC 调用,主要负责管理 Invoker,Invoker代表一个抽象封装了的执行体
Exchange 信息交换层:用来封装请求响应模型,同步转异步
Transport 网络传输层:抽象了网络传输的统一接口,可以选择 Netty 或 Mina
Serialize 序列化层:序列化跟反序列化
调用链
源码
SPI
和原生SPI的区别
通过键值对的方式进行配置,可以按需加载指定的实现类
拓展点增强(AOP)
自动包装扩展点的 Wrapper 类。ExtensionLoader 在加载扩展点时,
如果加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类
如果加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类
通过装饰器模式对真正的拓展实现类进行增强,类似AOP
Wrapper类Demo
Wrapper类判断
拓展点自动注入(IOC)
一个拓展点可以自动setter注入其它拓展点
约定目录
拓展点的获取
getExtension(String name)
拓展点自适应
@Adaptive
@Adaptive
类上
在加载时候,直接赋值给cachedAdaptiveClass(只允许一个类加注解)
getAdaptiveExtension()获取的就是加了注解的拓展点
方法上
动态代理
getAdaptiveExtension()根据URL里配置的选择对应的拓展点,
否则使用默认的(@SPI("xxx")的xxx)
否则使用默认的(@SPI("xxx")的xxx)
扩展点自动激活
@Activate
@Activate
demo
定义接口和实现类,接口要加上@SPI注解
在META-INF/dubbo路径下创建文件名为接口全限定名
在上面文件里输入key=实现类全限定名
ExtensionLoader<Robot> extensionLoader =ExtensionLoader.getExtensionLoader(接口.class);
extensionLoader.getExtension(key)拿到某个实现类
extensionLoader.getExtension(key)拿到某个实现类
初始化过程
解析服务
基于 dubbo.jar 内的 META-INF/spring.handlers 配置,Spring 在遇到 dubbo 名称空间时,会回调 DubboNamespaceHandler#init()。
所有 dubbo 的标签,都统一用 DubboBeanDefinitionParser 进行解析,基于一对一属性映射,将 XML 标签解析为 Bean 对象
所有 dubbo 的标签,都统一用 DubboBeanDefinitionParser 进行解析,基于一对一属性映射,将 XML 标签解析为 Bean 对象
ServiceBean & ReferenceBean 在DubboNamespaceHandler里init()
服务暴露(ServiceBean)
Spring启动结束发布ContextRefreshedEvent
,ServiceBean监听到事件开始导出服务
,ServiceBean监听到事件开始导出服务
父类ServiceConfig#export()
主要是检查和更新配置,如果缺省则使用默认值。
然后根据delay的值,决定是立即导出还是延迟导出
然后根据delay的值,决定是立即导出还是延迟导出
获取注册中心地址,然后根据配置组装URL
分别开始在本地(injvm)和远程暴露服务,然后创建Invoker(ProxyFactory#getInvoker),
根据协议(默认DubboProtocl#export)转化为Exporter放入到exporterMap。启动服务,监听端口
根据协议(默认DubboProtocl#export)转化为Exporter放入到exporterMap。启动服务,监听端口
服务注册到注册中心
服务引用(ReferenceBean)
ReferenceBean#getObject()
父类ReferenceConfig#get()
返回代理对象
返回代理对象
检查和更新配置,组装URL
在注册中心consumer目录下注册新节点,同时订阅providers、configurators、routers节点数据(监听)
根据协议构建Invoker(默认DubboProtocol#refer),如果是服务有多提供者,那么是一个Invoker集合,通
过Cluster(默认FailoverCluster)进行一个合并成一个虚拟Invoker
过Cluster(默认FailoverCluster)进行一个合并成一个虚拟Invoker
通过ProxyFactory#getProxy(invoker)(默认javassist)对Invoker进行动态代理,返回代理对象
服务调用
调用三种方式
oneway:不关心请求是否发送成功,消耗最小
同步调用sync : 在 Dubbo 源码中就调用了 future.get,用户感觉方法被阻塞了,必须等结果后才返回
异步调用Async:客户端调用请求后将返回的ResponseFuture存到
上下文中,用户可以随时调用future.get获取结果
上下文中,用户可以随时调用future.get获取结果
异步调用通过唯一ID标识此次请求
流程总结
1. 首先客户端调用接口的某个方法,实际调用的是代理类,代理类会通过 cluster 从 directory 中获取一堆 invokers(如果有一堆的话),然后进行 router 的过滤(其中看配置也会添加 mockInvoker 用于服务降级),然后再通过 SPI 得到 loadBalance 进行一波负载均衡。默认的 cluster 是 FailoverCluster ,会进行容错重试处理
2. 现在我们已经得到要调用的远程服务对应的 invoker 了,此时根据具体的协议构造请求头,然后将参数根据具体的序列化协议序列化之后构造塞入请求体中,再通过 NettyClient 发起远程调用。
3. 服务端 NettyServer 收到请求之后,根据协议得到信息并且反序列化成对象,再按照派发策略派发消息,默认是 All,扔给业务线程池。
4. 业务线程会根据消息类型判断然后得到 serviceKey 从之前服务暴露生成的 exporterMap 中得到对应的 Invoker ,然后调用真实的实现类。
5. 最终将结果返回,因为请求和响应都有一个统一的 ID, 客户端根据响应的 ID 找到存储起来的 Future, 然后塞入响应再唤醒等待 future 的线程,完成一次远程调用全过程。
2. 现在我们已经得到要调用的远程服务对应的 invoker 了,此时根据具体的协议构造请求头,然后将参数根据具体的序列化协议序列化之后构造塞入请求体中,再通过 NettyClient 发起远程调用。
3. 服务端 NettyServer 收到请求之后,根据协议得到信息并且反序列化成对象,再按照派发策略派发消息,默认是 All,扔给业务线程池。
4. 业务线程会根据消息类型判断然后得到 serviceKey 从之前服务暴露生成的 exporterMap 中得到对应的 Invoker ,然后调用真实的实现类。
5. 最终将结果返回,因为请求和响应都有一个统一的 ID, 客户端根据响应的 ID 找到存储起来的 Future, 然后塞入响应再唤醒等待 future 的线程,完成一次远程调用全过程。
Cluster
简介
实现
failover(默认)
切换别的服务重试
failback
异常捕获返回默认结果,然后通过时间轮后台定时重试
failsafe
异常不抛出,只打印错误日志
failfast
只会进行一次调用,失败后立即抛出异常
forking
同时调用多个服务,第一个响应返回则结束,适合读请求
broadcast
会逐个调用每个服务提供者,如果其中一台报错,在循环
调用结束后会抛出异常。通常用于通知所有提供者更新缓存等
调用结束后会抛出异常。通常用于通知所有提供者更新缓存等
Directory(服务目录)
类似服务的目录,通过这个目录来查找远程服务,实际是一堆invoker集合。同样的服务可能有多个提供者。
还实现了监听注册中心的功能(指的是RegistryDirectory )
还实现了监听注册中心的功能(指的是RegistryDirectory )
静态:StaticDirectory
是静态的是因为多注册中心是写在配置里面的,不像服务可以动态变更
用在多注册中心的时候,它是一个静态目录,即固定的不会增减的
动态:RegistryDirectory
1、获取 invoker 列表
2、监听注册中心的变化,刷新 invoker列表
2、监听注册中心的变化,刷新 invoker列表
LoadBalance
配置
服务端
应用级别
方法级别
消费端
应用级别
方法级别
实现
加权Random(默认)
加权RoundRobbin
LeastActive
ConsistentHash
TreeMap<Long,Invoker> virtualInvokers
tailMap()
firstEntry()
相同参数的请求总是发到同一提供者
当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动
Registry
有zookeeper、redis、nacos、multicast、simple等
zk注册订阅流程
生产者在zk上使用临时节点注册服务url,在providers路径下
消费者订阅provider下节点,并且将自己url写入到consumers路径下
Protocol
dubbo
特性
采用NIO复用单一长连接,并使用线程池并发处理请求,减少握手和加大并发效率,性能较好(推荐使用)
在大文件传输时,单一连接会成为瓶颈
常见问题
http
特性
rest
hessian
特性
rmi
特性
redis
thrift
优点:跨语言、使用二进制编码进行数据传输,序列化和反序列
化速度快,并且数据量小,可以降低网络传输负担
化速度快,并且数据量小,可以降低网络传输负担
gRPC
webservice
Serialization
hessian2(默认)
跨语言高效二进制序列化方式
kryo
java
fst
java
dubbo
不成熟,不建议生产使用
json
java
JDK自带,性能较差
限流&熔断降级
限流
预先设定一个固定的限流值,集成Sentinel等
自动根据系统或集群负载情况执行限流
设计模式
代理模式
ProxyFactory#getProxy,对Invoker进行代理
抽象工厂
ProxyFactory实现类JdkProxyFactory和JavassistProxyFactory,
分别用来生产基于jdk代理机制和基于javassist代理机制的Proxy和Invoker
分别用来生产基于jdk代理机制和基于javassist代理机制的Proxy和Invoker
模板方法
负载均衡LoadBalance,具体实现子类实现
策略模式
SPI
责任链模式
Filter
装饰器模式
MockClusterInvoker装饰Invoker使其拥有mock功能
ProtocolFilterWrapper(ProtocolListenerWrapper(InjvmProtocol))
DelegateProviderMetaDataInvoker装饰Invoker,持有ServiceConfig对象
单例模式
容器注册式exporterMap
观察者模式
RegistryDirectory,服务上线下线会调用其notify接口
监听Spring的ContextRefreshedEvent后开始服务暴露
常见问题
Spring cloud
Netflix
网关
功能
通用非业务功能比如认证、监控、日志、限流、校验、黑白名单判断等,然后转发到目标服务。
否则每个服务都要重复写这些功能。
否则每个服务都要重复写这些功能。
对外唯一入口,用于接入微服务
Zuul(停更)
Gateway
三大核心概念
路由-route
断言-predicate
过滤器-filter
注册中心
Eureka
AP
适用于服务实例数量不大的服务注册中心
服务上下限感知时间久
Consul
服务调用
REST
Feign/OpenFeign
为什么Feign第一次调用耗时很长
负载均衡
Ribbon
消费端(客户端)负载均衡
默认轮询
使用
RestTemplate + Ribbon
Feign + Ribbon
饥饿加载
断路器
Hystrix
功能
熔断:阻止故障的连锁反应导致雪崩
降级:快速失败
资源隔离
线程隔离
给每个command分配一个单独线程池
信号量隔离
获取信号量才可以执行,否则进入fallback。
起到限流和防止雪崩的作用
起到限流和防止雪崩的作用
监控和告警
链路追踪
Spring Cloud Sleuth
作为链路追踪的一种组件,只提供了日志采集,日志打印的功能,并没有可视化的UI界面
结合
zipkin
提供了强大的日志追踪分析、可视化、服务依赖分析等相关功能
数据传输
默认http
推荐MQ
支持rabbit、kafka
持久化
数据存储在服务端
默认内存,可选择mysql、es(推荐)等
Skywalking
消息总线
Spring Cloud Bus
配置中心
Spring Cloud Config
Alibaba
包含netflix
Sentinel
Nacos
RocketMQ
Dubbo
Seata
Alibaba Cloud OSS
Alibaba Cloud SchedulerX
Alibaba Cloud SMS
MySQL
索引
类型
B+Tree
特点
非叶子节点(key),叶子节点(key+value)
即使非叶子节点查到也必须继续查到叶子节点才可以结束
叶子节点key从左到右有序排放,节点之间通过双向链表连接
B-Tree
叶子和非叶子节点都会存放key和value
查找到目标值则停止查找
innodb选择B+Tree
一个节点16kb,非叶子节点不存放value,所以可以存放更多的key,
那么树的分叉多,树高越低(层数),查询IO次数就越少
那么树的分叉多,树高越低(层数),查询IO次数就越少
一般树高三到四层,前两层大概率在buffer pool中,所以IO次数更少
数据都在叶子节点,查询次数一样,所以查询更加稳定
叶子节点间通过指针相连,范围查找时只要找到一个叶子节点后,通过指针就能找到其它数据
聚集索引
&
辅助索引(非聚集、二级)
&
辅助索引(非聚集、二级)
键值的逻辑顺序 = 表数据行的物理存储顺序
聚集索引(主键+行数据),实际就是存储数据的
辅助索引(key + 主键)
回表
因为辅助索引只存储主键,所以如果需要其他字段则需要回到聚集索引树去查找
覆盖索引
从辅助索引就可以得到需要查询的数据,无需回表(using index)
分裂
自适应哈希索引
O(1),不支持范围查询
innodb对一些常用索引会建立一些key为索引,value为索引记录所在的页的位置,来加速查询,将其放入到adaptive hash index buffer pool中
innodb自己控制的,我们不能建立。可以开启和关闭自适应hash索引:innodb_adaptive_hash_index,默认开启ON
全文索引
myisam支持,innodb 1.2.x版本开始支持全文索引
语法
select * from table where match(content) against ('xx' in natural language mode);
对非英文支持不好,可能搜索不到存在的记录,不建议使用
唯一索引 & 普通索引
Change Buffer只对普通索引生效
索引字段的选取
离散度高的字段
用于where、order、join的(on)、group by字段上创建索引
联合索引
离散度高的字段放在前面
index(a,b,c)
select * from t where a = xx and b = xx order by c;
select * from t where a = xx order by b;
可以利用叶子结点的有序性
select * from t where a = xx order by b;
可以利用叶子结点的有序性
select * from t where a = xx order by c;
不能利用有序性,需要filesort排序
不能利用有序性,需要filesort排序
过长的字段,建立前缀索引
过长导致树节点存储更少的数据从而树高越大
不建议用无序的值作为索引
插入容易导致非叶子节点频繁的分裂
频繁更新的字段
更新会维护索引,导致性能差,造成页分裂
失效
违背最左匹配原则
like '%xx'
联合索引a_b,使用b作为查询条件
为什么要设计最左匹配原则
索引列有函数计算
数字字符类型未使用 '',出现隐式转换(相当于使用CAST函数)
反向查询
not like、not in等
or 只要有一个字段没有索引就会全表扫描
查询优化器选择不走索引
强制使用指定索引
select * from t force index(a) where a = xx and b = xx
select * from t force index(a) where a = xx and b = xx
查看表索引
show index from table
关键参数
Non_unique
非唯一索引,1代表非唯一
Cardinality
非常关键的参数,索引中唯一值的估计值,如果Cardinality/count(1)
非常小(结果应该尽可能接近1)则可以考虑删除该索引
非常小(结果应该尽可能接近1)则可以考虑删除该索引
优化器会根据该值决定使不使用该索引,维护该值需要开销,所以
不是实时的是个大概值
不是实时的是个大概值
可以使用命令analyze table t更新该值
Sub_part
是否列的部分被索引,如果是整个则为null,否则为截取长度值
Null
索引列是否包含null,YES表示包含
Multi-Range Read(MRR)
适用于range,ref和eq_ref类型的查询,explain的extra信息Using MRR
好处
减少磁盘随机io,将随机访问转为较为顺序的的数据访问
非主键(条件必须有索引)的范围条件,开启MRR的话,会根据非主键索引查询到的主键id放入到
缓存(read_rnd_buffer)中进行排序,然后用排序后的id去聚集索引访问数据
缓存(read_rnd_buffer)中进行排序,然后用排序后的id去聚集索引访问数据
减少缓冲池中页被替换的次数
因为磁盘预读机制,离散读操作会频繁替换掉缓存池中的页,而主键顺序访问则减少这种情况
set optimizer_switch='mrr=on,mrr_cost_based=off'
Batched Key Access(BKA)
Index Nested-Loop Join(NLJ)中会拿驱动表的数据一条一条的去被驱动表中查找,
BKA则是将数据放入到 join buffer 中然后批量去被驱动表查找,提升了性能
BKA则是将数据放入到 join buffer 中然后批量去被驱动表查找,提升了性能
BKA 依赖于 MRR
set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on'
Index Conditon Pushdown(ICP)
支持range、ref、eq_ref类型的查询,只适用辅助索引
Using index condition
将原来server层的过滤下推到存储引擎,减少了回表访问行的数量,从而减少I/O
set optimizer_switch = 'index_condition_pushdown=on'
事务 & 锁 & MVCC
ACID
Atomicity
mvcc(undolog)
Consistency
Isolation
锁 & mvcc
Durability
redolog
并发带来问题
脏读
不可重复读
幻读
隔离级别
级别
读未提交
脏读
读已提交(Oracle默认)
解决脏读问题,但是不可重复读
可重复读(MySQL默认)
可重复读,但是会有幻读
innodb解决幻读
串行化
RC和RR的区别(选取)
RR通过锁区间相对于RC的行锁,增大了锁范围,所以并发度更低。
RC的“半一致性”读可以增加update操作的并发性(对于不满足更新条件的记录,可以提前释放锁)
隔离级别解决方案
锁
锁住的是索引
避免where条件没有索引,否则导致锁表
根据主键更新,则锁住主键
根据唯一索引更新,先锁唯一索引,然后锁主键
根据普通索引更新,先锁普通索引,然后锁记录对应的主键
锁粒度
全局锁(🔐库)
表锁
lock tables xxx read;
lock tables xxx write;
unlock tables;
lock tables xxx write;
unlock tables;
行锁
锁类别
共享锁(S Lock)
select …… lock in share mode
排它锁(X Lock)
1、增删改会自动加排它锁
2、select ... for update
2、select ... for update
意向锁
作用:提升加 表锁 的效率
引擎自己维护,无法手动使用
种类
意向共享锁(IS)
意向排它锁(IX)
提升加锁效率
锁算法
record lock
RC、RR都有
唯一性索引(唯一/主键)等值查询
gap lock
只在RR中存在,左开右开
未命中任何记录,则加间隙锁
next-key lock
只在RR中存在,左开右闭
record lock + gap lock
死锁
原因
多个事务因为夺取锁资源而造成的一个相互等待。比如事务1获得A资源,接下来要获取B资源。而事务2先获得了B,
再获取A,事务1等待事务2释放B,而事务2等待事务1释放A,就造成了一个互相等待,从而死锁
再获取A,事务1等待事务2释放B,而事务2等待事务1释放A,就造成了一个互相等待,从而死锁
SHOW ENGINE INNODB STATUS
应对策略
超时机制
被动方式,锁等待达到设定时间,则回滚其中小事务(更新,插入,删除行数少的事务,也就是undo量小的事务)
innodb_lock_wait_timeout
默认50s
设置过大业务不能容忍,过小造成误伤
主动死锁检测(默认)
主动检测死锁,每次事务请求发生等待时候会判断是否存在回路,有则回滚小事务
innodb_deadlock_detect
默认ON
并发高的情况,消耗很多cpu资源
设置为OFF则关闭主动检测,转为超时机制
避免或减少
操作不同的表或者同表不同行使用相同的顺序
隔离级别改为RC
将大事务改成小事务,减少事务的持续时间减少锁冲突概率
尽快提交事务,减少冲突
把耗时操作放在方法前面
SHOW ENGINE INNODB STATUS查看死锁原因,从而改进程序
打开innodb_print_all_deadLocks开关,会记录所有死锁日志。记录在错误日志里。用完记得关闭
mvcc
Multi Version Concurrent Control,多版本并发控制。
只在RC和RR下生效,用于事务隔离和提升并发访问性能
只在RC和RR下生效,用于事务隔离和提升并发访问性能
视频讲解
原理(版本链+视图)
版本链(unlog链)
是一条链表,链接的是每条数据曾经的修改记录
行数据隐含字段
trx_id:当前事务id
rol_pointer:回滚指针,记录上个版本
row_id
delete_flag
视图(ReadView)
包含信息
creator_trx_id:创建视图时的事务id
m_ids:创建该视图时,活跃且未提交的事务id集合
min_trx_id:m_ids中最小的事务id
max_trx_id:创建视图时应该给下个事务id
RC
每个select语句执行前都会创建一个新的视图
所以会读取到其他事务的更新
第二次读会开启新视图,而之前在旧的视图的m_ids里的事务
因为提交了,则不会在新视图的m_ids中,则可以读取到,所以
是不可重复读
因为提交了,则不会在新视图的m_ids中,则可以读取到,所以
是不可重复读
RR
事务开始时创建一个视图,提交前都是用这个视图
但是事务中更新语句,或者加锁的select也会读取到最新的数据
第二次读不会创建新视图,所以已经提交的事务id依然还在m_ids中,
所以认为没有提交,读取不到
所以认为没有提交,读取不到
查询流程
判
断
条
件
规
则
根据当前视图中的信息去版本链中遍历比较,符合条件则返回记录
判
断
条
件
规
则
SQL
执行流程
-client
->连接器
->缓存
->分析器
->优化器
->执行器
->存储引擎
->client
->连接器
->缓存
->分析器
->优化器
->执行器
->存储引擎
->client
连接器
管理连接、权限验证
缓存
命中缓存直接返回,默认关闭
8.0版本取消
解析器
拆分、检查语法是否正确
预处理器
检查字段、表是否存在
优化器
执行计划生成,索引选择
执行器
操作引擎返回结果
存储引擎
存储数据,提供读写接口
更新语句执行流程
慢sql发现
开启慢查询日志
mybatis拦截statementHandler,记录sql执行耗时,大于阈值报警或记录
explain查看执行计划
id
id相同,执行顺序从上往下,id不同,执行顺序从下往上
select_type
talbe
type
system
表中只有一条数据
const
通过索引一次找到,一般就是主键查询
eq_ref
唯一性索引扫描,表中只有一条数据匹配
ref
非唯一性索引扫描,返回匹配某个单独值的所有行
index_merge
index_merge是MySQL 5.1后引入的一项索引合并优化技术,它允许对同一个表同时使用多个
索引进行查询,并对多个索引的查询结果进行合并(取交集(intersect)、并集(union)等)后返回
索引进行查询,并对多个索引的查询结果进行合并(取交集(intersect)、并集(union)等)后返回
range
范围查询
index
只遍历树,通常比all快
all
全表扫描
possible_keys
key
key_len
索引中使用的字节数,可通过该列计算查询中使用的索引的长度,在不损失精确性的情况下,长度越短越好。
ref
rows
根据表统计信息及索引选用情况,大致估算出找到所需的记录所需要读取的行数
filtered
表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例(%)
extra
using index
using index condition
using where
using filesort
using join buffer
using temporary
关键字
order by
sort_buffer
一块用来排序的内存,每个查询线程都分配这么一块内存,会根据sql查询出来的数据放入到内存中进行排序然后返回给客户端
当explain的extra信息是using filesort表示使用了内存排序
参数
sort_buffer_size(默认256kb)
查询数据 <= sort_buffer:内存中排序
快排算法
查询数据 > sort_buffer:借助文件排序,导致io,性能变差
归并排序算法
max_length_for_sort_data
默认1024字节,代表select后字段的总长度,当没有大于该值则会使用全字段排序,否则则会自动选择rowid算法
全字段排序
内存保存select后所有字段,无需回表
rowid排序
内存只放入主键id和用来排序的字段,排序后还要拿到id进行回表查询
优化
配置优化
适当调大sort_buffer的size
max_length_for_sort_data调大一点,防止rowid排序,
不过1kb基本已经满足大部分情况了
不过1kb基本已经满足大部分情况了
sql方面优化
利用B+TREE叶子节点有序性/联合索引
如果有where条件,则筛选字段和排序字段建立联合索引
没有筛选的话,排序字段建立索引
如果where是in等,还是会filesort。可以分别查询然后内存处理排序
尽量避免select *
防止字段长度大于max_length_for_sort_data导致rowid排序
count
MyIsam会维护一个计数值,但是对于带有where条件的count查询就没用了
Innodb每次count查询都要实际计算
为什么不跟MyIsam一样
因为Innodb是有事务的,因为事务的隔离性以及mvcc机制,不同事务count查询结果可能不一样
count(*)
不取值,直接累加
count(1)
遍历每一行,并把1放进去,不存在null值情况
count(id)
count(字段)
如果字段可以为null,且有null值的话,会跳过不计算
in & exists
主结果集大,子结果集小
IN性能更高
SELECT * FROM `user` WHERE `user`.id IN (SELECT `order`.user_id FROM`order`)
先执行子查询,再执行主查询
主结果集小,子结果集大
EXISTS性能更高
SELECT `user`.* FROM `user` WHERE EXISTS (SELECT 1 FROM `order` WHERE `user`.id = `order`.user_id)
先执行主查询再执行子查询
主查询出来的记录一条条的执行子查询里的sql判断结果true|false
join
type
join
笛卡尔积
inner join
交集
left join
返回左表所有行,右表关联不上用null表示
right join
返回右表所有行,左表关联不上用null表示
union
全连接
算法
Index Nested-Loop Join(NLJ)
可以用到被驱动表的索引
Block Nested-Loop Join(BNL)
用不到被驱动表的索引,将驱动表数据放入到join_buffer中,然后将被驱动表的每一行取出来和
join_buffer的数据对比,满足条件的作为结果集返回
join_buffer的数据对比,满足条件的作为结果集返回
explain的extra信息中using join buffer(Bolck Nested Loop)
join_buffer
join_buffer_size 默认256k
当size不够大不能完全容下驱动表数据时候,则会放部分驱动表数据,然后全表扫描被驱动表,
跟buffer中数据对比。然后清空buffer,放入剩下的驱动表数据,再全表扫描一次被驱动表
跟buffer中数据对比。然后清空buffer,放入剩下的驱动表数据,再全表扫描一次被驱动表
会造成多次扫描被驱动表,所以可以调大join_buffer_size大小
优化
尽量使用到NLJ
小表作为驱动表
两个表按照各自条件过滤后,参数join个数据量小的表就是小表
调大join_buffer_size
union
union
无重复
代替or
union all
可重复
tips:内部select后列必须相同
where & having
where
从数据库的字段进行筛选
having
从select后面的字段(可以是函数计算,avg,max等)进行筛选
on duplicate key update
存在唯一索引情况下,插入或者更新
update会造成自增主键不连续
case when ... then
字段类型
char
长度固定,如果插入长度小于定义长度则用空格填充,检索值的时候删除空格
最多存放255个字符
varchar
长度可变,插入小于定义长度时,按实际长度存储
最多存放65532个字符
blob
存储二进制数据
date
存储日期,不存储时间,如YYYY-MM-DD
time
不存储日期,存储时间
datetime
YYYY-MM-DD HH:MM:SS
存储范围:
1000-01-01 00:00:00.000000~9999-12-31 23:59:59.999999
1000-01-01 00:00:00.000000~9999-12-31 23:59:59.999999
存什么就是什么,与时区无关
timestamp
YYYY-MM-DD HH:MM:SS
存储范围:
1970-01-01 00:00:01.000000~2038-01-19 03:14:07.999999
1970-01-01 00:00:01.000000~2038-01-19 03:14:07.999999
与时区有关,会随着时区变化而变化
主从复制
步骤
从库通过io线程去主库拉取binlog并写入relaylog
sql线程读取relaylog并执行sql
单线程,可以通过配置改为多线程执行
读写分离实现
代理方式
Mycat
MySQL Router(官方)
MySQL Proxy
组件方式
sharding-jdbc
主从延迟
查看延迟
备库执行show slave status命令,查看seconds_behind_master的值,精度为秒
原因&解决
从库配置比主库差,导致数据备份较慢
提升从库的硬件配置,最好大于等于主库配置
备库查询压力大,比如查询消耗大量CPU资源,从而影响同步速度
多接几个从库,分担读的压力
大事务&慢sql比较耗时,从库执行也耗时,导致延迟
尽量避免大事务,优化慢sql
SQL线程单线程写入慢
MySQL 5.7支持并行复制,也就是sql线程由单线程变为多线程
异步复制
5.5后增加半同步复制
写入立马读取场景(强一致性、金融),如何解决
强制主库
适当休眠后再查
架构
连接池组件
管理服务和工具组件
SQL接口组件
查询分析器组件
优化器组件
存储引擎
innodb
事务、行锁、外键、mvcc、解决幻读、有主键
B+Tree
叶子节点存放是行记录
myisam
表锁、不支持事务、可以没有主键
B+Tree
叶子节点存放的是行记录所在磁盘地址,根据地址再去查询数据
索引(存放键值和行记录磁盘地址)和数据分开存放,分别是MYI和MYD文件
archive
只支持insert和select。目标是高速插入和压缩功能(压缩比1:10),适合日志等存储归档数据
memory
数据存于内存,使用哈希索引
csv
......
缓存
Buffer pool
磁盘读取是按页读取(预读机制),默认16K。
当读取一条数据时,会把其附近的数据也一起预读到缓存池。
下次读取,就直接从缓存读取,无需从磁盘读取,提升查询性能
当读取一条数据时,会把其附近的数据也一起预读到缓存池。
下次读取,就直接从缓存读取,无需从磁盘读取,提升查询性能
innodb_buffer_pool_size
LRU
传统LRU
新数据或者访问页中存在的数据,都会移动到队头,如果空间不够,则移除队尾的数据
存在问题
预读失效
可能预读的数据被没有被使用,所以预读失效了
缓冲池污染
比如一个查询扫描大量树数据,把缓冲池中的大量页都替换出去了,导致大量热数据被移出
MySQL基于传统的改造
划分成新生代&老生代
老生代的头连接者新生代的尾部。新页先放入到老生代,只有当读取到了,才移动到新生代,
这样如果没有被读到,则提前被淘汰了,解决预读失效问题
这样如果没有被读到,则提前被淘汰了,解决预读失效问题
innodb_old_blocks_pct,老生代占lru链的百分比,默认37
设置老生代停留时间
数据被访问了,且在老生代停留时间达到设定时间,才会移动到新生代,解决缓存污染问题
innodb_old_blocks_time.老生代停留时间,单位ms,默认1000
Change buffer
如果没有change buffer
修改的数据在缓存中,直接修改缓存中的数据。否则从磁盘读取,写入缓存再修改
有change buffer
先把变更记录到change buffer中,等未来读取到该数据再并入缓存池中
减少了一次磁盘随机操作
作用
如果buffer pool不存在要修改的数据,则在changeBuffer中记录变更内容,
然后写redolog(这时候内存中和磁盘数据不一致,叫脏页),从而提升了速度。在
然后写redolog(这时候内存中和磁盘数据不一致,叫脏页),从而提升了速度。在
- 数据库空闲
- 缓存池不够用
- 数据库正常关闭
- redo log写满的时候
第二个和第四个可能会影响到正在执行的sql执行速度,
也就是同一个sql有时很快有时很慢的原因
也就是同一个sql有时很快有时很慢的原因
适用
非唯一普通索引
如果是唯一索引,必须要到磁盘校验唯一性,所以,缓存就没意义了
读多写少或者不是写后立即读取
立即读取会访问磁盘,change buffer的初衷就是不访问磁盘
innodb_change_buffer_max_size
占用整个缓存池百分比,最大50,默认25
Log buffer
redo log buffer
Adaptive hash index buffer
InnoDB后台线程
多线程模型
线程类别
master thread
负责将缓冲池中的数据异步刷新到磁盘,合并change buffer等
IO thread
负责处理IO请求
insert buffer thread(1个)
log IO thread(4个)
read IO thread(4个)
write IO thread(4个)
show eninge innodb status可以看到上面线程,线程数量可以调整
purge thread
负责清理undo log
page cleaner thread
负责脏页的刷新
......
文件
参数文件
my.cnf
socket文件
使用unix套接字方式进行连接需要的文件
pid文件
mysql实例进程PID文件
表结构文件
存储引擎文件
包括各引擎相关的数据,索引等文件
日志文件
binlog
默认开启,二进制格式的日志,所属server层、逻辑日志,非幂等
作用:主从复制 & 数据恢复
格式
statement(不合理的设置)
记录的是sql
如果是带有limit,now()等,从库执行的话可能会造成不一致
row
记录具体修改的信息
缺点:占用更多存储空间,耗费io资源,影响执行速度
mixed
MySQL 自己会判断这条 SQL 语句是否可能引起主备不一致,
如果有可能,就用 row 格式,否则就用 statement 格式
如果有可能,就用 row 格式,否则就用 statement 格式
8.0 以上版本默认mixed格式
刷盘策略
- sync_binlog=0 的时候,表示每次提交事务都只 write,不 fsync,由系统决定fsync
- sync_binlog=1 的时候,表示每次提交事务都会执行 fsync
- sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync
redolog
innodb特有、物理日志,幂等的,记录的是对每个页的做了哪些改动。
事务进行中不断写入,是循环写入,空间固定会用完,然后覆盖
事务进行中不断写入,是循环写入,空间固定会用完,然后覆盖
作用:崩溃恢复
流程
将修改结果记录到redolog,这里是mysql的日志缓存中
刷盘
触发时机
master thread每秒触发
提交事务,根据配置的策略来决定刷盘策略
innodb_flush_log_at_trx_commit
innodb_flush_log_at_trx_commit
0(性能好)
提交不写入磁盘,写入磁盘由mater thread去完成,每1s
执行一次fsync操作,所以mysql宕机会最多丢失一秒的数据
执行一次fsync操作,所以mysql宕机会最多丢失一秒的数据
1 (默认)
每次事务提交,都会fsync,写入磁盘,不会丢失数据
2(折中)
提交时写入redo log到操作系统的缓存中,由操作系统后台
每秒fsync,mysql宕机不会丢失数据,操作系统宕机则会丢失数据
每秒fsync,mysql宕机不会丢失数据,操作系统宕机则会丢失数据
阿里云配置的是2,最佳实践
undolog
不是文件,存放在数据库内部一个undo段(undo segment)中
慢查询日志
默认关闭
开启
非永久
SET GLOBAL slow_query_log = 1
永久
my.cnf文件,设置slow_query_log=ON
设置阈值
long_query_time,单位s,默认10。
大于该值的sql会被记录
大于该值的sql会被记录
消耗性能,使用完建议关闭
查询日志
记录MySQL的请求信息
错误日志
记录错误信息,比如启动失败等
其它
三范式
1NF
字段都是单一属性,不可再拆分
2NF
1NF的基础上,要求表中的每个非主键列完全依赖于主键列,而不是依赖于其他非主键列
3NF
2NF基础上,要求表中的每个非主键列之间不应该存在传递依赖关系
连接池
Druid
支持sql级监控,支持扩展,防止SQL注入等功能
HikariCP
SpringBoot2默认
获取和关闭连接性能更好
CPU飙升排查
show processlist
大表添加字段
Redis
数据类型
string
bitmap
签到、登录状态等
hash
list
set
zset
应用场景
商品筛选
限流
验证码
底层数据结构
dictEntry
Redis里每一个键值对都是一个dictEntry,里面指向了key和value
key<SDS>
SDS
Redis使用c写的,c中字符串是用char[]数组表示,redis并没有使用这个,而是自己定义了SDS
char[]缺点
必须先给目标变量分配足够内存,否则会内存溢出
获取字符串长度必须遍历数组,时间复杂度为O(n)
长度变更会对字符串数组做内存重分配
通过从字符串开始到结尾碰到的第一个'\0'来标记字符串的结束,
因此不能保 存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全
因此不能保 存图片、音频、视频、压缩文件等二进制(bytes)保存的内容,二进制不安全
SDS优点
按需扩容,无需担心内存溢出
记录的字符串长度len,无需遍历,复杂度为o(1)
空间预分配和惰性空间释放,防止多次重分配内存
判断是否结束的标志是 len 属性,二进制安全
value<RedisObject>
redisObject
数据类型
查看key的数据类型:type key
内部编码
查看编码命令:object encoding key
五大类型中,每种类型可能存在着不同的编码
不同编码的目的
为了在节约内存和提高性能之间做平衡
当数据量小的时候,会采用紧凑(性能偏低)的数据结构
当数据量达到一定阈值的时候,会从紧凑型的结构转成高效率的数据结构
LRU:最近一次被访问时间
查看key的空闲时间:object idletime key
引用计数
查看key被引用次数:object refcount key
实际对象指针
当值是 string 类型,并且编码是 int 时,保存的就是这个整数值,而不是指针
string
编码
int
字符串是整数值,用int编码。redisobject的ptr保存的就是实际值,而不是指针
好处
节省了sds和指针的内存空间
embstr
字符串的长度小于等于 44 采用 embstr 编码,真实数据由sds存储
好处
redisObject 跟 sds 的内存是挨在一起,从而分配和释放内存都只要一次。而raw是分开的,需要两次
raw
字符串长度超过 44 用raw编码,真实数据由sds存储
list
quicklist
双向链表,链表中每个元素(node)是ziplist
双向链表浪费内存,通过ziplist减少链表节点数量。同时ziplist插入和删除复杂度高,所以通过参数控制ziplist的长度
hash
ziplist
节省内存空间,适合少键值对个数,kv体积小的场景
转化为hastable条件
key或value大于64byte(一个字母为1byte)
键值对个数大于512个
hashtable
数组+链表
set
intset
元素都是整数类型且个数<=512则使用inset存储
hashtable
元素有不是整数类型或者个数超过512个
key是元素的值,value为null
zset
ziplist
节省内存空间
元素个数小于128并且每个元素长度小于64字节
skiplist
查询值使用字典
字典key存储的是元素,value存储的是score
根据score查询使用的skiplist
为什么不用红黑树
增删查复杂度跟跳表是一样的,但是范围查询跳表更好
插入元素是否建立索引判断逻辑
为什么快
纯内存
请求单线程
网络请求模块使用了单线程,其他模块是多线程
单线程为啥快
Redis的瓶颈不是计算,而是IO能力
避免创建和销毁线程带来的开销
避免了多线程上下文切换
避免线程竞争问题,比如加锁,死锁等
I/O多路复用
linux: epoll
专门设计的数据结构
skiplist、SDS.....
C语言,不要经过JVM翻译
持久化
RDB(快照模式-默认)
保存当前数据
触发
手动
save
整个过程都会阻塞(同步)
bgsave
fork一个子进程(阻塞),持久化操作由子进程执行(非阻塞)(异步)
自动
redis.conf配置规则
save <seconds> <changes>
关闭:save "",并且注释掉其他 save m n
主从复制,生成rdb文件发送给从
shutdown
lastsave指令
数据恢复快,但是容易丢数据
AOF
记录每次写命令
开启aof:appendonly yes
流程
1、写命令都会追加到aof_buf(缓冲区)中
2、根据策略将缓存刷盘到磁盘
策略参数:appendfsync
no
不执行fsync(),由操作系统保证数据同步到磁盘,速度最快,最不安全
always
每次写入都fsync(),速度慢不丢失数据(最多丢失一条命令)
everysec(默认)
每秒执行fsync(),可能丢失1s(可能大于1s)的数据
AOF阻塞
每次fsync会记录完成时间T,主线程会对比当前时间和T,如果发现距离T小于等于2s则直接返回,否则阻塞主线程。
fsync执行比较慢的情况下,则会丢失2s的数据
fsync执行比较慢的情况下,则会丢失2s的数据
定位
reids日志
info persistence
参数:aof_delayed_fsync,fsync延迟超过2s则计数+1
监控服务器磁盘io高的进程
如工具iotop
3、随着aof文件不断增大,会触发重写机制
当aof文件越来越大,达到阈值则会将文件压缩,只保留可以恢复数据的最小指令集
触发
手动
bgrewriteaof
自动
auto-aof-rewrite-min-size
运行AOF重写时文件最小体积,默认 为64MB
auto-aof-rewrite-percentage
代表当前AOF文件空间 (aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值
触发阈值
aof_current_size>auto-aof-rewrite-min- size
&&
aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewrite- percentage
&&
aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewrite- percentage
注意
也会fork子进程,fork过程中会阻塞
不是在原文件上压缩,而是读取服务器现有键值对,生成新的文件替换原文件
no-appendfsync-on-rewrite
no(默认)
重写过程中,(appendfsync为always或者everysec)会阻塞新的写入
yes
重写过程不会阻塞新的写入,等到重写完成再这些新的写入指令写入到aof文件
如果有延迟问题可以设置为yes,但可能会丢失数据
恢复慢,但是基本不会丢数据
同时开启
重启选择aof恢复数据
坑
第一次开启aof,重启。因为aof为空文件,所以会丢失所有数据
解决
先备份rdb文件,同时动态开启aof,再手动触发生成aof文件。
再修改conf文件,开启aof,修改aof文件名(可不修改)
再修改conf文件,开启aof,修改aof文件名(可不修改)
混合持久化
Redis 4.0 版本的混合持久化功能默认是关闭。
aof-use-rdb-preamble 为 yes 开启此功能
aof-use-rdb-preamble 为 yes 开启此功能
重写aof文件时候会同时生成rdb文件,然后将rdb作为aof文件一部分。然后就追加新的写入命令
数据恢复时候先加载rdb,然后加载aof的命令。这样结合了两者的优点
内存
maxmemory
info memory
关键信息
used_memory
used_memory_rss
mem_fragmentation_ratio
used_memory_peak
内存回收
过期键删除策略
惰性删除
key 在过期之后,没有立即删除,而是在读写 key 的时候,才对过期的 key 进行删除
缺点:key一直没有被访问的话就一直不会被删除
定期删除
expires名字的字典,key保存的key名称,value保存的是过期的时间。定时从字典中
获取若干key,判断是否过期,过期则删除
获取若干key,判断是否过期,过期则删除
8个淘汰策略(内存达到maxmemory上限)
noeviction(default)
拒绝写入,抛异常
设置了过期时间
volatile-lru
volatile-lfu
volatile-random
volatile-ttl
所有的key
allkeys-lru
allkeys-lfu
allkeys-random
集群
作用
性能、扩展、高可用
模式
主从Replication
配置
开启
从服务redis.conf文件配置:slaveof ip port
从服务启动命令加上slaveof ip port
关闭:slaveof no one
数据同步
全量复制
slave连接到master会发送psync命令,maseter收到后执行bgsave生产rdb文件,同时将此
后的写命令写入到缓冲区,将rdb发送给slave,同时将期间新的写命令发送给slave
后的写命令写入到缓冲区,将rdb发送给slave,同时将期间新的写命令发送给slave
slave收到rdb文件会删除本地的文件,然后进行新rdb文件的内存载入,完成后会执行master发送的写命令
增量复制
slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器
缺点
主宕机,不能自动切换,需要人工介入去修改主节点,同时还要更改客户端配置
哨兵Sentinel
故障发现和自动故障转移的高可用方案
客户端连接到哨兵节点集合,哨兵负责转发
工作原理
哨兵间建立连接
利用redis的发布订阅模式
哨兵同从库建立连接
向主库发送info命令,返回从库列表信息,然后可以同从库建立连接
故障发现
哨兵会监控主、从和其他哨兵是否正常(ping/s)。
当发现master在超过时间(down-after-milliseconds,默认30s)没有响应,
则标记为主观下线
当发现master在超过时间(down-after-milliseconds,默认30s)没有响应,
则标记为主观下线
当有达到一定数量(配置指定quorum)哨兵标记主观下线,则会标记其为客观下线
自动故障转移
从哨兵集群中选出leader(raft)
哨兵leader向某个slave发送slave of no one命令,让其成为master
选取策略
如果slave和哨兵断开时间比较紧久,超过阈值则失去选举权
优先级:数值越小越优先(配置中配置replica-priority 100)
复制数量:优先级相同,选取从master中复制的数据多的
进城id:如果复制量也相同,则选择进程id小的
向其他节点发送replicaof命令,让他们成为选取的master的slave节点,
原master节点变为新master的从节点
原master节点变为新master的从节点
通知客户端主节点已更换
Cluster(分片)模式
作用
可拓展
数据分片,解决单节点容量限制问题
高可用
主从切换
高并发
至少3个master,3个slave
特点
客户端可以连接任何一个主节点进行读写
支持在线增加、删除节点
不支持同时处理多个key(如MSET/MGET)
不支持事务
key寻址(寻址算法)
哈希算法
节点变动造成大量数据迁移
一致性哈希
不适合 少量数据节点 的分布式方案
圆环分布不均,导致部分节点压力大
当一个节点挂点,数据迁移到临近节点,导致临近节点压力大
哈希槽
16384个slot,每个node分配一个区间的slot
crc16(key)%16384 => slot => node
Q:如何让不同的key落在同一个节点?
A:key里加入{tag},用tag寻址
扩容缩容
key 与 slot 的关系是永远不会变的,会变的只有 slot 和 node 的关系
slot和node关系改变的过程中服务是可用的,所以可以动态扩缩容
扩容则从每个node中分出一部分slot给新node。
缩容则把当前闹得的slot分给剩下的node
缩容则把当前闹得的slot分给剩下的node
数据分布均匀
codis
存在问题
双写一致性
不一致情景
先更新数据库,再更新缓存
更新缓存失败,存在不一致性
并发带来的问题,a线程先更新数据库,此时b线程更新数据库,更新缓存,最后a线程继续更新缓存,存在不一致
先更新数据库,再删除缓存
删除缓存失败,存在不一致
先删除缓存,再更新数据库
并发带来问题,a线程先删除,此时b线程未命中缓存,写入缓存,最后a更新数据库,产生不一致
数据库主从带来问题,未来得及同步到从库,未命中缓存,写入缓存,读取的是旧数据
不一致原因
并发况下线程不安全
操作数据库或者缓存其中之一失败了
方案
延时双删
1、删除缓存
2、更新数据库
3、rocketMQ延时消息删除
2、更新数据库
3、rocketMQ延时消息删除
删除缓存重试机制
删除失败丢进mq重试
订阅binlog日志(canal),监听到数据变化,通过mq删除
更新缓存,mq消息异步更新数据库
缓存雪崩
大量缓存失效(或者Redis宕机),导致大量请求落到数据库
解决方案
设置随机过期时间,防止同一时刻大量key过期
保证redis高可用
缓存穿透
请求数据库中不存在的数据,导致无法缓存,后续请求全部打在数据库
解决方案
参数校验,非法参数直接拒绝
缓存‘null’,并设置过期时间
如果恶意攻击,不断请求不同的不存在的key,还是解决不了问题
布隆过滤器
位图/hash计算
位图实际是二进制数组,只存储0和1,分别表示不存在和存在。位图占用内存空间非常小
key经过n次hash计算得到n个hash值,映射到数组上,把0变为1
n个下标的值都是1表示存在,只有有0表示不存在
缺点
不能删除
误判
判断不存在肯定不存在
判断存在可能不存在
hash冲突
降低误判率
更长的二进制数组
需要更多内存空间
更多次的hash计算
需要更多cpu资源
实现
Reddison
缓存击穿
某一个热点key失效,导致大量请求落到数据库
解决方案
分布式锁
只有一个请求可以访问数据库,缺点是会导致其它请求排队。而且都会打到数据库
优化:zk锁获取锁成功之后,双重检查一下是否存在,存在说明更新了直接返回。否则查询db然后构建缓存
热点key永不过期,value添加逻辑过期时间,第一个抢到锁的更新缓存,其他线程直接返回旧值
存在数据不一致
双key
设置两个key
key
保存业务数据,过期时间较长
lock_key
过期时间较短
流程
第一个线程setnx(lock_key,value)成功,说明lock_key之前已经过期了,则取读取db后set(key,value)
其他线程setnx失败,直接返回get(key)
hot key
发现
redis-cli --hotkeys
代理层统计
饿了么开源框架Samaritan
客户端统计
hot key访问方案
增加一层本地缓存,如Map,Guava cache或Ehcache
热点key分散成多个子key,在集群多个机器上设置相同的值。请求进来通过一定的算法(比如一致性hash),选择一台机器访问,分散压力
读写分离:多个slave
big key
value占用空间大或者元素个数多
影响
请求耗时或超时
占用带宽和cpu
造成集群数据倾斜
删除导致阻塞
发现
redis-cli --bigkeys
应用
事务
实现方式
MULTI 、 EXEC 、 DISCARD 、WATCH
使用lua脚本
ACID
原子性
保证一组命令不被分割
不支持回滚
因为发生错误是编程错误(只会是语法错误或者命令使用在不对的键值类型上),不应该发生在生产环境上
不支持回滚可以使其简单快速
一致性
隔离性
redis是单线程的
持久性
开启aof备份,并且appendfsync always
lua
保证命令间不会插入别的命令
发布/订阅
实现延时队列
异步队列
list作为队列,lpush生产消息,brpop消费消息
发布订阅
不可靠,订阅者不一定能收到消息
管道
一次发送多个命令,节省往返时间(RTT,round trip time)
分布式锁
拓展
客户端
序列化协议
RESP
发送命令格式
返回结果格式
Java客户端
Jedis
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持
Redisson
实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。
宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上
Redis变慢的原因(阻塞)
内在原因
时间复杂度高的命令
定位
慢查询日志
slowlog-log-slower-than num
记录执行时间大于num微秒的命令
slowlog-max-len num
保留最近num条慢日志
slowlog get num
获取指定数量的记录
查看命令统计信息
info commandstats
big key
危害
慢查询
占用带宽
数据倾斜(集群)
定位
redis-cli --bigkeys
分类
比如hash、set、list等元素很多
String类型的value占用内存很大
优化
lazy-free机制
删除大key的内存释放在后台异步线程执行,
而不阻塞主线程
而不阻塞主线程
主动删除
UNLINK KEY
DEL KEY是阻塞删除
key的元素大于64才会异步释放,否则同DEL命令
淘汰删除,过期删除等开启lazy-free
key集中过期
内存达到上限
写入数据同时要淘汰数据
CPU饱和
指Redis把单核CPU使用率跑到接近100%
top
定位
redis-cli --stat
1s打印一次,可以看到每秒请求数
解决
集群水平拓展,读写分离
持久化阻塞
fork阻塞
bgsave或者aof重写
定位
info stats查看latest_fork_usec指标:上一次fork耗时多少微秒
优化
不要使用过大的redis内存
降低aof重写的触发频率
AOF阻塞
AOF阻塞,fsync慢导致阻塞主进程
定位
info persistence查看aof_delayed_fsync
fsync调用超过2s则计数+1
优化
redis单独部署,不要跟高磁盘负载的服务部署在一起
使用sds硬盘
外在原因
CPU竞争
内存交换
如果操作系统把Redis使用的部分内存 换出到硬盘,由于内存与硬盘读写速度差几个数量级,会导致发生交换后的 Redis性能急剧下降
定位
info server查看process_id
cat /proc/{process_id}/smaps | grep Swap
r如果值基本都是0K,则表示没有内存交换
预防
保证足够可用内存
设置合适maxmemory
Linux>3.5,vm.swappiness建议为1,否则建议为0
宁可OOM killer杀死进程,也不内环交换
网络问题
scan & keys
scan
info
info server:服务器信息
info clients:客户端信息
info memory:内存信息
info persistence:持久化信息
info stats:全局统计信息
info replication:复制信息
info cpu:cpu消耗信息
info commmandstats:命令统计信息
info cluster:集群信息
info keyspace:键统计信息
memory
memory usage key
memory stats
MQ
总览
作用
异步处理
异步线程是在一个进程,mq可以跨进程
宕机也不会丢失消息
应用解耦
流量削峰
缺点
系统可用性降低
比如MQ服务挂了
系统复杂度增加
比如重复消费、消息丢失、顺序消费等问题
运维成本增加
问题
重复消费
原因
消息发送重复
因为网络抖动等原因没有收到broker的确认导致消息重发
消息投递重复
因为网络导致没有收到ack,为保证至少消费一次而重新投递
负载均衡时候重复
消费者宕机没有提交位移、扩容导致的reblance等
解决
业务幂等
通过消息的唯一id进行去重
消息积压
消息发送过快
消费者消费过慢
消费耗时
IO耗时
数据库读写慢
批量消费,批量更新
索引优化
分库分表
CPU耗时
递归或者循环
下游系统调用耗时
消费者是否消费异常
消费并发度
增加消费者
增加机器配置
队列扩容,分区扩容
消息丢失
从发送、存储、消费等维度保证消息不丢失
顺序消费
全局顺序
分区顺序
高可用
MQ对比选型
RabbitMQ
核心概念
Broker
VHost
一个broker可以有多个VHost,起到隔离作用
可以新建一个vhost,本地测试使用该vhost
Exchange
消息先投递到交换机上
4种类型
direct:直连交换机
根据消息携带的路由键将消息投递给对应队列
简单说就是精准匹配
fanout:扇形交换机
将消息路由给绑定到它身上的所有队列
简单说就是广播
topic:主题交换机
对路由键进行模式匹配后进行投递
简单说就是模糊匹配
headers:头交换机
Binding key
交换机通过绑定键和队列绑定
*: 1 个
#:0 个或多个
#:0 个或多个
Routing key
消息发送到指定交换机,消息设置路由键通过路由键投递到到对应的队列
Queue
消息默认存在内存,开启持久化模式则持久化到磁盘
Connection
TCP长连接
Channel
在tcp长连接里创建和释放channel,减少connection的频繁创建和销毁的开销
TTL
队列
x-message-ttl
消息
死信
消息变成死信的条件
(NACK || Reject) && requeue = false
消息过期
队列达到设定的最大消息条数或者字节数,最先入队的会变成死信
使用步骤
定义死信交换机和死信队列
绑定死信队列和死信交换机
定义死信队列的消费者
定义普通交换机和普通队列,并且普通队列设置死信交换机属性为前面定义的死信交换机
绑定普通交换机和普通队列
延迟队列
RabbitMQ不支持
实现
数据库+定时器
消息TTL + 死信队列
流转流程
生产者—>原交换机—>原队列(超过 TTL 之后)—>死信交换机—>死信队列—>最终消费者
缺点
可能有时间误差
rabbitmq-delayed-message-exchange 插件
流控
消息发送速度远大于消费速度,造成消息积压,所以要流控
措施
服务端限流
queue
x-max-length
队列中最大消息条数,超过队头的被丢弃
x-max-length-bytes
队列中存储的最大消息容量(bytes),超过队头的被丢弃
内存控制
默认MQ占用内存达到40%则会抛出警告,阻塞所有连接
磁盘控制
磁盘空余低于指定的值则触发流控
消费者限流
默认是消息push给消费者(consumer在本地缓存所有的message),如果来不及消费会导致OOM或者其他影响
prefetch count
autoAck为false才生效
在服务端设置,在channel或者connection上设置,未ack的最大消息数。
比如该值为2,当有两个消费者,一个消费者有两条消息没有ack,那么接下来消息push给另一个消费者
比如该值为2,当有两个消费者,一个消费者有两条消息没有ack,那么接下来消息push给另一个消费者
集群模式
普通集群模式
普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。
你创建 的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据
可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时
候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来
你创建 的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据
可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时
候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来
主要是用来提高吞吐量,非高可用
镜像集群模式
你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,
每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每
次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上
每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每
次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上
高可用,无法拓展queue
可靠性投递
消息丢失
生产者
使用事务机制,是同步的会阻塞
使用confirm机制,是异步非阻塞的。broker回调发送结果
Broker
消息持久化
默认消息只存在内存,不会持久化到磁盘
交换机、队列设置为持久化,发送消息设置发送模式deliveryMode=2,代表持久化消息
镜像集群模式
消费者
手动Ack
幂等性
顺序消息
需要顺序消费的消息发送到同一个queue,
并且只有一个消费者去消费,并且是单线程消费
并且只有一个消费者去消费,并且是单线程消费
步骤
需要顺序消费的消息发送到同一个queue
集群模式下也只有一个消费者消费:
设置单活消费者
设置单活消费者
队列的参数
x-single-active-consumer设置为true
x-single-active-consumer设置为true
手动ack,prefetchCount=1
优缺点
优点
单机吞吐量高,达万级
延迟低,达微秒级
社区成熟活跃
提供较为友好的后台管理页面
支持多种语言
缺点
吞吐量(单机万级)对于一些场景不够用
不支持水平扩容
消息ack后就会删除,无法回查消息
当产生消息堆积 ,性能下降明显
erlang语言,不利于二次开发和维护
Spring AMQP核心组件
ConnectionFactory
用于创建连接
RabbitAdmin
是AmqpAdmin的实现,主要是队交换机、队列、绑定的声明和创建
Message
消息的封装
RabbitTemplate
目前是AmqpTemplate的唯一实现,用来简化消息的收发。封装了创建连接、创建channel、
收发消息、消息格式转换、关闭channel、关闭连接等操作
收发消息、消息格式转换、关闭channel、关闭连接等操作
MessageListener
处理消息
MessageListenerContainer
messageListener的容器,一个container只有一个listener,但是可以生成多个线程使用相同的listener同时消费消息。
可以管理listener生命周期,队消费者进行配置。比如动态添加移除队列、队消费者设置包括并发、消费者数量、消息确认模式等
可以管理listener生命周期,队消费者进行配置。比如动态添加移除队列、队消费者设置包括并发、消费者数量、消息确认模式等
MessageListenerContainerFactory
需要监听多个rabbit服务器,则可以指定不同factory
MessageConvetor
消息的序列化,默认SimpleMessageConverter
RocketMQ
核心概念
组件
NameServer
注册中心
支持broker、topic路由信息的动态注册和发现
broker管理
topic路由信息管理
故障发现
每10s检查broker的心跳信息,超过120s没有心跳,则移除该broker的路由信息
可集群部署
多实例部署,互不通信,互相独立
为啥不用zk
Broker
消息投递、存储、查询以及服务高可用保证
路由注册
启动会在nameserver上注册路由信息
每30s发送心跳,心跳包含ip、port、topic信息
多master/slave
角色
ASYNC_MASTER(异步主机)
SYNC_MASTER(同步主机)
SLAVE(从机)
数据分片,实现横向拓展
master负责读写数据
brokerId=0的为master
slave复制mater数据
可靠性,防止数据丢失
只有brokerId=1的slave在master读取慢的情况才参与到读
读写分离
master负责读写
正常情况slave不参与读
当消息堆积大于物理内存40%则brokerId=1的
slave可以读取数据
slave可以读取数据
Producer/Producer Group
Consumer/Consumer Group
topic
生产者将消息发到主题(实际是队列),消费者订阅主题获取消息
创建topic会指定队列数量
写队列数量(默认4)
决定message queue的数量
如果m个broker,n个写队列,那么总队列数就是m*n
consumequeue/{topic}/路径下就有多少个目录(目录按照0、1、2、...n命名)
MessageQueue
包含topic、brokerName和queueId信息
可以知道消息往哪个broker,哪个topic和哪个queue发送
读队列数量(默认4)
决定几个线程消费这些message queue
>=写队列数量,否则会有message queue的消息不能被消费
tag
可以用来过滤消息
key
相当于消息的索引
业务层面的设置唯一标识
message
必须包含topic,可以没有tag,有唯一 Message ID,可以是具有业务标识的值
可以根据 Message ID & Key查询消息
特性&原理
生产者
发送方式
单向发送
同步发送
异步发送
选择 message queue
选择broker,选择topic,选择queue
策略
轮询(默认)
随机
自定义实现MessageQueueSelector接口
事务消息
保证了本地事务和消息发送的原子性
消息状态
TransactionStatus.Unknown
中间状态,对消费者不可见
中间状态,对消费者不可见
TransactionStatus.CommitTransaction
提交,消费者可以拉取消费
提交,消费者可以拉取消费
TransactionStatus.RollbackTransaction
回滚,删除消息
回滚,删除消息
流程
消息发送到broker,状态unknown对消费者不可见,生产者执行本地事务,成功则commit消息,对消费者可见
补偿阶段(回查)
如果一直没有收到生产者的commit/rollback消息,则发起回查,根据本地事务状态来commit/rollback消息
默认回查15次,如果还是无法得知事务状态,则默认回滚消息
延时消息
开源版本只支持18个级别的延迟消息
原理
场景:订单超时取消
Broker
消息存储
存储架构
commitlog
消息顺序写入commiltlog,单个broker实例所有topic共用一个commitlog存储
每个文件默认大小1G,文件名长度20位,左边补零,数字为消息起始偏移量,写满就写入下一个文件
consumequeue
consumequeue相当于索引,不同topic的消息在commitlog中的offset放入到对应的consumerque中,这样消费者
只需去对应的consumeque获取物理偏移量offset去commitlog中拉取消息
只需去对应的consumeque获取物理偏移量offset去commitlog中拉取消息
提高查询消息性能
indexFile
可以通过key或者时间区间查询消息
顺序读写
commitlog顺序写
consumequeue顺序读
page cache(页缓存)
写入缓存异步刷盘提升写性能
预读机制,将邻近数据预读写入缓存,提升查询性能
零拷贝:mmap(内存映射)
对文件的操作转化成对内存地址的操作,减少内核缓冲区和用户缓冲区的拷贝的开销
消息刷盘
在broker.conf配置flushDiskType
ASYNC_FLUSH(默认)
消息先写入缓存中,达到一定程度则刷盘
吞吐量高,性能好,会丢数据
SYNC_FLUSH
消息写入缓存成功后,立刻刷盘
性能差,不会丢失数据
消息查询
MessageId
解析MessageId得到存储消息broker的ip、port、commitlog offset,然后向目标broker发起rpc请求查询
Message key
基于IndexFile索引文件实现
文件清理
commitlog清理
条件
默认每天凌晨4点删除超过72小时的文件
磁盘使用超过85%,没有到凌晨4点也会清除过期文件,直到低于85%
达到90%拒绝写入
consumerqueue清理
主从同步&故障转移
集群模式
2m-2s-async
2m-2s-sync
2m-noslave
消费者(组)
消费模式
集群模式
一条消息只会被消费者组里一个消费者消费
消费位移存放在broker
广播模式
一条消息被消费者组所有消费者消费
消费位移存放在客户端
负载均衡
平均分配(默认)
一致性哈希
重平衡机制
对topic进行扩容(增加consumequeue个数)或者消费者扩容缩容则会触发消费队列重新分配
消费模型
并发消费
对一个队列中消息,每一个消费者内部都会创建一个线程池,对队列中的消 息多线程处理,
即偏移量大的消息比偏移量小的消息有可能先消费
即偏移量大的消息比偏移量小的消息有可能先消费
该模式下消费失败默认会16次衰减重试
顺序消费
该模式消息消费失败,会一直重试,直到成功
重试 & 死信队列
重试16次失败则丢进死信队列
RocketMQ 控制台提供对死信消息的查询、导出和重发的功能
支持 push & pull
push
实时性高
消费端没有做好流控会导致积压甚至崩溃
pull
普通轮询
每隔一段时间去拉取消息
缺点
大部分时间没有消息,很多无效请求,浪费服务器资源
定时请求,造成消息延迟
长轮询
如果没有消息,会hold住请求直到有消息或者超时返回,然后发起下一次长轮询
broker端属性 longPollingEnable 标记是否开启长轮询。默认开启
缺点
耗费内存
其他
保证消息不丢失(可靠性)
producer
同步或异步 + 失败重试
broker
同步刷盘 + 主从同步复制
consumer
手动ack
顺序消息
生产者
同步发送
根据sharding key把消息发送到同一个messageQueue(定义MessageQueueSlector)
消费者
设置为有序消费模式,使用顺序监听器
消息积压
原因
解决
messageQueue数量 < 消费者数量
增加消费者
messageQueue数量 >= 消费者数量
messageQueue扩容
并发消费下位移提交
比如msg1,msg2,msg3三条消息并发消费,msg3先消费完成ack如何提交位移呢,是提交msg1的位移还是msg3的位移
提交最小offset
不会丢失消息
宕机重启会重复消费,幂等性处理
实现原理
消息存放在key为offset的treeMap中,消费完一个则从map中删除,返回最小的key并提交位移
broker宕机,nameserver自检有
延时,这段窗口期如何规避
延时,这段窗口期如何规避
重试机制,默认两次
sendLatencyFaultEnable
Broker故障延迟机制
Broker故障延迟机制
false默认值
如果broker不可用,重试选择其他broker,可能还是不可用broker(浪费一次重试机会)
true
故障规避
判断broker是否可用,不可用则轮询下个broker(在一次重试中切换)
如何判断是否可用
优缺点
优点
高性能:单机吞吐十万级
高拓展:支持弹性伸缩
高可用:分布式架构
合理的参数可以保证消息0丢失
支持10亿消息堆积,且不影响性能
支持消息回查、重新消费、事务消息、延时消息、顺序消息等
java编写,易于二次改造
经过双十一实战考验
缺点
支持语言少(java\go\c++)
社区活跃度一般
Kafka
思维导图
0 条评论
下一页