价值30K-----高级JAVA面试大纲
2024-02-02 21:37:26 0 举报
AI智能生成
这里有你想要的一切
作者其他创作
大纲/内容
分布式
CAP原则
概念:分布式系统, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性)
一致性(C):分布式系统,所有数据备份,同时刻同值。(访问所有节点最新数据副本,事务强一致性)
可用性(A):集群部分节点故障,集群能响应客户端读写请求。(具备高可用性,响应要求及时)
分区容忍性(P):分区备份,可多节点提供服务,提高系统容错。系统如不能在时限内达成数据一致,
在C和A间做选择。
在C和A间做选择。
CP:zookeeper,Consul,Hbase,Nacos(要配置,持久节点),Kafka(replication.factor = 3、min.insync.replicas = 3、acks = all)
AP:eureka,redis,Nacos(默认,临时节点),Kafka(replication.factor = 3、min.insync.replicas = 3、acks = 1)
CA:关系型数据库(mysql关系型数据库)、Kafka(Kafka的开发人员申明Kafka是CA系统,因为运行在一个数据中心,网络分区问题基本不会发生,如果发生了,就有AP和CP之分)
Base理论
BASE对CAP延伸,无法强一致性(Strong Consistency),达到最终一致性(Eventual Consitency)。
1,基本可用(Basically Available):分布式系统出现不可预知故障,允许损失部分节点,部分时间段,部分业务模块可用性
2,软状态( Soft State):允许系统数据存在中间状态,不会影响系统整体可用性。
3,最终一致( Eventual Consistency):强调所有数据变更,一段时间同步(重试,负载均衡,分区同步,主从复制,消息同步),最终一致。
2,软状态( Soft State):允许系统数据存在中间状态,不会影响系统整体可用性。
3,最终一致( Eventual Consistency):强调所有数据变更,一段时间同步(重试,负载均衡,分区同步,主从复制,消息同步),最终一致。
BASE大型,高可用,可扩展,分布式系统,牺牲ACID强一致性,获得可用性,允许数据在一段时间内不一致,终达到一致状态。
一致性协议
二阶段提交(2PC)
实现
1,协调者向参与者发送事务执行指令,等待参与者反馈事务执行结果 2,参与者收到请求,执行事务不提交,记录事务日志 3,参与者将事务执行情况给协调者,阻塞等待,协调者后续指令
第一阶段,协调者收集参与者事务执行情况
参与者回复正常执行
1协调者向参与者发commit 2参与者执行事务,释放占有资源 3参与者给协调者commit结果
1或多执行失败
1,协调者向参与者发rollback 2,参与者收到事务回滚后,执行rollbacak,释放占用资源3参与者给协调者返rollback结果
等待超时
优点
- 实现简单
- 实现简单
缺点
- 1,同步阻塞,机器出现性能,网络故障:执行中,参与节点都事务阻塞
- 2,协调者,单点故障:协调者发生故障,参与者一直阻塞
- 3,丢失消息,导致数据不一致:发Commit时,局部网络异常,只有部分参与者收请求
- 4,过于保守:无完善容错机制,任意节点失败,都会导致整个事务失败
- 1,同步阻塞,机器出现性能,网络故障:执行中,参与节点都事务阻塞
- 2,协调者,单点故障:协调者发生故障,参与者一直阻塞
- 3,丢失消息,导致数据不一致:发Commit时,局部网络异常,只有部分参与者收请求
- 4,过于保守:无完善容错机制,任意节点失败,都会导致整个事务失败
三阶段提交(3PC)
优点
-3PC,协调者(Coordinator)和参与者(Participant)设置超时预估值,中断机制,
2PC协调者才有超时机制。
- 加了事务询问过程,设置中断逻辑,减少了阻塞范围
-3PC,协调者(Coordinator)和参与者(Participant)设置超时预估值,中断机制,
2PC协调者才有超时机制。
- 加了事务询问过程,设置中断逻辑,减少了阻塞范围
缺点
- 未解决数据不一致问题
- 未解决数据不一致问题
三阶段提交, 协调者,参与者引入超时机制 ,把第一个阶段拆分成两步:询问,后发布执行指令,后提交事务执行,最后提交事务。
分别为:cancommit,precommit,do_commit
分别为:cancommit,precommit,do_commit
Paxos
执行流程
优缺点
Paxos为分布式强一致性提供理论基础,Paxos协议理解困难,实现复杂
Paxos 协议是解决分布式系统,多节点对值(提案)达成一致(决议)通信协议。能处理少数节点离线情况,多数节点仍能够达成一致。
ZAB
1、变更状态。Leader 挂后,非 Observer 服务器,会设自己服务器状态 LOOKING,开始 Leader 选举。
2、Server发出投票。运行期间,每个服务器ZXID 可不同,假定 ZK1 节点 ZXID :124,ZK3 的 ZXID : 123;第一轮投票,ZK1 和 ZK3 产生投票(1, 124),(3, 123),都将投票发送给集群中所有机器
3、接收各服务器投票。启动时过程相同
4、处理投票。与启动相同, ZK1 事务 ID 大,ZK1 成 Leader。
5、统计投票。与启动时过程相同。
6、改变服务器状态。与启动时过程相同。
运用:Zookeeper
2、Server发出投票。运行期间,每个服务器ZXID 可不同,假定 ZK1 节点 ZXID :124,ZK3 的 ZXID : 123;第一轮投票,ZK1 和 ZK3 产生投票(1, 124),(3, 123),都将投票发送给集群中所有机器
3、接收各服务器投票。启动时过程相同
4、处理投票。与启动相同, ZK1 事务 ID 大,ZK1 成 Leader。
5、统计投票。与启动时过程相同。
6、改变服务器状态。与启动时过程相同。
运用:Zookeeper
Raft
执行流程
- Leader(主节点):接受 client 请求,写入本地,同步到副本
- Follower(从节点):从 Leader 中接受请求,写入本地日志文件。对客户端提供读服务
- Candidate(候选节点):follower 一段时间内,未收到 leader 心跳。leader 可能故障,发起选主提议。节点 Follower 变 Candidate 状态,到选主结束
,组内收到大多数选票,称为leader,可发送心跳确立自己地位。(每server(follower)在term内投一张选票,按照先到先得原则投出)
- Follower(从节点):从 Leader 中接受请求,写入本地日志文件。对客户端提供读服务
- Candidate(候选节点):follower 一段时间内,未收到 leader 心跳。leader 可能故障,发起选主提议。节点 Follower 变 Candidate 状态,到选主结束
,组内收到大多数选票,称为leader,可发送心跳确立自己地位。(每server(follower)在term内投一张选票,按照先到先得原则投出)
优缺点
分布式事务
两阶段提交方案XA 方案
- 效率低,适合单体, 多数据库应用,不适合互联网
- 效率低,适合单体, 多数据库应用,不适合互联网
阻塞,效率低,依赖分配者,存在消息丢失,没有容错,试错方案
TCC 方案(常用)
- 用于保证数据正确性分布式事务场景,全成功,或者全回滚,如资金转账等
- TCC 依赖回滚和补偿代码,适用场景较少
- 用于保证数据正确性分布式事务场景,全成功,或者全回滚,如资金转账等
- TCC 依赖回滚和补偿代码,适用场景较少
本地消息表
- 依赖数据库消息表,无法支持高并发场景
- 依赖数据库消息表,无法支持高并发场景
MQ事务消息方案(常用)
- 只保证发送端与MQ间事务性,不保证消费端事务
- 消费端事务执行失败,1,不断重试直到成功,2,通知发送端回滚 3,人工补偿
- 支持高并发,大多数公司选择,rocketmq解决方案
- 只保证发送端与MQ间事务性,不保证消费端事务
- 消费端事务执行失败,1,不断重试直到成功,2,通知发送端回滚 3,人工补偿
- 支持高并发,大多数公司选择,rocketmq解决方案
最大努力通知方案
- 最大努力通知服务消费 MQ ,写入数据库中记录下来
- 最大努力通知系统B,反复 N 次,最后还是不行就放弃。
- 最大努力通知服务消费 MQ ,写入数据库中记录下来
- 最大努力通知系统B,反复 N 次,最后还是不行就放弃。
分布式框架
Netty
zookeeper
什么是zookeeper?
分布式协调框架,Apache Hadoop子项目,解决分布式应用遇到数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置管理等。存储少量数据到内存数据库,两个核心概念:文件系统数据结构+监听通知机制。
zookeeper的znode(目录节点)有哪些?
PERSISTENT(持久化目录节点)
客户端与zookeeper断开连接,节点依旧存在,不手动删除节点,永远存在
PERSISTENT_SEQUENTIAL(持久化顺序编号目录节点)
客户端与zookeeper断开连接,节点依旧存在,Zookeeper给该节点名称,进行顺序编号
EPHEMERAL(临时目录节点)
客户端与zookeeper断开连接后,节点被删除
EPHEMERAL_SEQUENTIAL(临时顺序编号目录节点)
客户端与zookeeper断开连接,节点被删除,只是Zookeeper给该节点名称,进行顺序编号
Container节点
3.5.3 版本新增,Container节点下面没有子节点,Container节点
在未来会被Zookeeper自动清除,定时任务默认,60s 检查一次
在未来会被Zookeeper自动清除,定时任务默认,60s 检查一次
TTL 节点
默认禁用,配置 zookeeper.extendedTypesEnabled=true 开启,不稳定
zookeeper的watch监听通知机制
客户端注册,监听任意节点,目录节点及递归子目录节点。
对节点监听,节点被删除,被修改时,客户端将被通知
对目录监听,目录有子节点被创建,删除,客户端将被通知
对目录递归子节点进行监听,目录下面任意子节点,有目录结构变化(子节点被创建,或被删除)根节点有数据变化时,客户端将被通知。
注意:通知都是一次性,对节点,目录及递归子集进行监听,一旦触发,对应监听被移除。每节点下面事件只被触发一次。
对节点监听,节点被删除,被修改时,客户端将被通知
对目录监听,目录有子节点被创建,删除,客户端将被通知
对目录递归子节点进行监听,目录下面任意子节点,有目录结构变化(子节点被创建,或被删除)根节点有数据变化时,客户端将被通知。
注意:通知都是一次性,对节点,目录及递归子集进行监听,一旦触发,对应监听被移除。每节点下面事件只被触发一次。
服务器角色
Leader
1,处理事务请求和非事务请求
2,集群内部各服务器调度者
2,集群内部各服务器调度者
Follower
1,处理客户端非事务请求,转发事务给Leader
2,参与事务请求Proposal(提案)投票
3,参与Leader选举投票
2,参与事务请求Proposal(提案)投票
3,参与Leader选举投票
Observer
1,处理客户端非事务请求,转发事务请求给Leader
2,不参与leader选举,投票。写操作。
3,数据没有持久化到硬盘。Observer只会把数据加载到内存。
2,不参与leader选举,投票。写操作。
3,数据没有持久化到硬盘。Observer只会把数据加载到内存。
leader选举
选举相关的概念
Zookeeper 节点状态
LOOKING:寻找 Leader 状态,需进入选举流程
LEADING:领导者状态
FOLLOWING:跟随者状态,Leader 已选举出来,当前节点角色 follower
OBSERVER:观察者状态,当前节点角色是 observer
LEADING:领导者状态
FOLLOWING:跟随者状态,Leader 已选举出来,当前节点角色 follower
OBSERVER:观察者状态,当前节点角色是 observer
事务ID(zxid)
ZooKeeper 状态变化生成 ZXID(ZooKeeper 事务 id)形式标记。ZXID 是64 位数字, Leader 统一分配,全局唯一,不断递增。ZXID 展示了所有ZooKeeper 变更顺序。每次变更有唯一 zxid, zxid1 小于 zxid2 说明 zxid1 在 zxid2 之前发生。
值越大,数据越新,选举算法中权重越大
值越大,数据越新,选举算法中权重越大
服务器ID(myid)
如有三台服务器,编号1,2,3 。
编号越大在选择算法权重越大。
编号越大在选择算法权重越大。
逻辑时钟(epoch logicalclock)
投票次数,同一轮投票过程,逻辑时钟值相同。投完,数据会增加,与接收到其它服务器返回投票信息数值相比,不同值,做出不同判断
集群初始化启动时 Leader 选举
3台机器,启动到第2台时候,选举过程开始:
1、每Server一个投票。初始情况,ZK1 和 ZK2 将自己作 Leader 进行投票,投票包含推举服务器 myid 和 ZXID, ZK1 (1, 0),ZK2 (2, 0),投票发给集群其他机器。
2、接受各服务器投票。服务器收到投票,判断投票有效性,如:是否本轮投票、是否 LOOKING 状态服务器。
3、处理投票。服务器都需要将别人的投票和自己投票进行比较:
ZXID 较大服务器优先作 Leader
ZXID 相同,比较 myid。myid 较大服务器作为Leader服务器
ZK1 更新投票为(2, 0),将投票重新发送 ZK2。
4、服务器统计投票信息,是否有过半机器接受相同投票,如统计出集群接受了(2, 0)投票信息,选出 ZK2 作为Leader
5、改服务器状态。确定 Leader,服务会更新状态,Follower-》 FOLLOWING, Leader-》 LEADING。节点 ZK3 启动,不再选举,状态 LOOKING-》 FOLLOWING
1、每Server一个投票。初始情况,ZK1 和 ZK2 将自己作 Leader 进行投票,投票包含推举服务器 myid 和 ZXID, ZK1 (1, 0),ZK2 (2, 0),投票发给集群其他机器。
2、接受各服务器投票。服务器收到投票,判断投票有效性,如:是否本轮投票、是否 LOOKING 状态服务器。
3、处理投票。服务器都需要将别人的投票和自己投票进行比较:
ZXID 较大服务器优先作 Leader
ZXID 相同,比较 myid。myid 较大服务器作为Leader服务器
ZK1 更新投票为(2, 0),将投票重新发送 ZK2。
4、服务器统计投票信息,是否有过半机器接受相同投票,如统计出集群接受了(2, 0)投票信息,选出 ZK2 作为Leader
5、改服务器状态。确定 Leader,服务会更新状态,Follower-》 FOLLOWING, Leader-》 LEADING。节点 ZK3 启动,不再选举,状态 LOOKING-》 FOLLOWING
集群运行期间 Leader 故障
1、变更状态。Leader 挂后,非 Observer 将状态变为 LOOKING,开始 Leader 选举。
2、Server会发投票。运行期间,每服务器上 ZXID可不同,假定 ZK1 (ZXID: 124),ZK3(ZXID: 123)投票中,ZK1 和 ZK3 都会投自己,产生投票(1, 124),(3, 123),将投票发送给集群中所有机器。
3、接收来自各个服务器的投票。与启动时过程比较。
4、处理投票。与启动时过程相同 ZK1 事务 ID 大,ZK1 成为 Leader。
5、统计投票。启动时过程相同。
6、改变服务器状态。启动时过程相同。
2、Server会发投票。运行期间,每服务器上 ZXID可不同,假定 ZK1 (ZXID: 124),ZK3(ZXID: 123)投票中,ZK1 和 ZK3 都会投自己,产生投票(1, 124),(3, 123),将投票发送给集群中所有机器。
3、接收来自各个服务器的投票。与启动时过程比较。
4、处理投票。与启动时过程相同 ZK1 事务 ID 大,ZK1 成为 Leader。
5、统计投票。启动时过程相同。
6、改变服务器状态。启动时过程相同。
zookeeper的经典使用场景?
分布式配置中心
基于Zookeeper的实现方式:
数据存储:将数据(配置信息)存储到Zookeeper节点
数据获取:启动初始化节点,从Zookeeper数据节点读取数据,节点上注册一个数据变更Watcher
数据变更:变更数据时,更新Zookeeper对应节点数据,Zookeeper会将数据变更通知,通过watch事件,发各客户端,接到通知后,变更数据
设计模式
Push 模式
Pull 模式
发布与订阅
数据存储:将数据(配置信息)存储到Zookeeper节点
数据获取:启动初始化节点,从Zookeeper数据节点读取数据,节点上注册一个数据变更Watcher
数据变更:变更数据时,更新Zookeeper对应节点数据,Zookeeper会将数据变更通知,通过watch事件,发各客户端,接到通知后,变更数据
设计模式
Push 模式
Pull 模式
发布与订阅
分布式注册中心
注册中心
1、服务提供方(user-service,N个实例)zk中创建临时节点,比如/user-service/1,...,/user-service/n
2、服务调用方(order-serivce)监听/user-service,有变化,新增或删除实例,order-serivce会收到通知,重新获取数据,并重新设置监听
1、服务提供方(user-service,N个实例)zk中创建临时节点,比如/user-service/1,...,/user-service/n
2、服务调用方(order-serivce)监听/user-service,有变化,新增或删除实例,order-serivce会收到通知,重新获取数据,并重新设置监听
分布式互斥锁(公平锁)
临时有序节点特性分布式锁,属于公平锁。
1、请求进来,直接在/lock节点下创建临时有序节点,比如/order_id
2、判断是不是/lock节点下面最小节点
a.是,获取锁
b.否,对前面节点进行监听(watch)
3、获得锁,处理完任务,释放锁,delete节点,后继第一节点会得到通知,判断。
临时有序节点好处,避免锁释放后,引发多个节点并发竞争锁(羊群效应),缓解服务器压力。
1、请求进来,直接在/lock节点下创建临时有序节点,比如/order_id
2、判断是不是/lock节点下面最小节点
a.是,获取锁
b.否,对前面节点进行监听(watch)
3、获得锁,处理完任务,释放锁,delete节点,后继第一节点会得到通知,判断。
临时有序节点好处,避免锁释放后,引发多个节点并发竞争锁(羊群效应),缓解服务器压力。
分布式共享锁(读锁)
获取锁原理:
1、客户端 create() 持久节点临时有序子节点,返回带序号节点名称;
2、客户端getChildren() 获取持久节点子节点列表;
3、判断自己在子节点顺序,是第一个子节点(序号最小),获取到了锁;否,判断自己获取节点类型,是读节点,判断是否有比自己小的写节点,没有,则获读锁,有则通过 exist() 向比自己小最后写节点,注册Watcher监听;如果是写节点,那么通过 exist() 接口向比自己小最后节点注册一个Watcher监听;
4、等Watcher通知,客户端创建的是读节点,则获取到了读锁;否在继续进行第二步
1、客户端 create() 持久节点临时有序子节点,返回带序号节点名称;
2、客户端getChildren() 获取持久节点子节点列表;
3、判断自己在子节点顺序,是第一个子节点(序号最小),获取到了锁;否,判断自己获取节点类型,是读节点,判断是否有比自己小的写节点,没有,则获读锁,有则通过 exist() 向比自己小最后写节点,注册Watcher监听;如果是写节点,那么通过 exist() 接口向比自己小最后节点注册一个Watcher监听;
4、等Watcher通知,客户端创建的是读节点,则获取到了读锁;否在继续进行第二步
分布式队列
集群选举
负载均衡
原理
将启动服务注册到zk注册中心上,采用临时节点。
从zk节点上获取最新服务节点信息,本地使用负载均衡算法,分配服务器
将启动服务注册到zk注册中心上,采用临时节点。
从zk节点上获取最新服务节点信息,本地使用负载均衡算法,分配服务器
集群使用几台服务器好?
不是越多越好,选举和事务提交前询问会更耗时。
如果是3、4台服务器哪个更好?3台更好,只能支持宕机1台服务,4台增加选举耗时。将其中1台换成Observer不一样,不参与投票,又提供服务。
如果是3、4台服务器哪个更好?3台更好,只能支持宕机1台服务,4台增加选举耗时。将其中1台换成Observer不一样,不参与投票,又提供服务。
ZAB协议
消息广播
Leader 收到客户端请求后,将请求封成个事务,给事务分配全局递增唯一 ID,称为事务ID(ZXID),ZAB 协议需保证事务顺序,必须将事务按照 ZXID 后排序后处理,通过消息队列实现, Leader 和 Follwer 间还有消息队列,用来解耦他们之间耦合,解除同步阻塞
zookeeper集群中为保证进程能够有序顺序执行, Leader 服务器接受写请求,Follower 服务器接受到客户端写请求,转发到Leader 服务器进行处理,Follower只能处理读请求
ZAB协议规定,事务在一台机器上被处理(commit)成功,所有机器上都被处理成功
zookeeper集群中为保证进程能够有序顺序执行, Leader 服务器接受写请求,Follower 服务器接受到客户端写请求,转发到Leader 服务器进行处理,Follower只能处理读请求
ZAB协议规定,事务在一台机器上被处理(commit)成功,所有机器上都被处理成功
崩溃恢复
ZAB 定义 2 个原则:
丢弃在 Leader 提出/复制,没有提交事务。
确保 Leader 提交事务最终会被所有服务器提交
丢弃在 Leader 提出/复制,没有提交事务。
确保 Leader 提交事务最终会被所有服务器提交
数据同步
崩溃恢复后,需在正式工作前(接收客户端请求),Leader 确认事务是否已被过半 Follwer 提交,完成数据同步。为保持数据一致。Follwer 服务器成功同步后,Leader 将这些服务器加到可用服务器列表中。
Zookeeper数据同步为四类
1,直接差异化同步(DIFF同步)
2,先回滚再差异化同步(TRUNC+DIFF同步)
3,仅回滚同步(TRUNC同步)
4,全量同步(SNAP同步)
数据同步前,Leader服务器会完成数据同步初始化:
peerLastZxid:从learner(follower或observer)注册发送ACKEPOCH消息中提取* lastZxid(Learner服务器最后处理ZXID)
minCommittedLog:Leader服务器Proposal缓存队列committedLog中最小ZXID
maxCommittedLog:Leader服务器Proposal缓存队列committedLog中最大ZXID
直接差异化同步(DIFF同步)
场景:peerLastZxid介于minCommittedLog和maxCommittedLog间
先回滚,再差异化同步(TRUNC+DIFF同步)
场景:新Leader服务器发现Learner服务器含自己没有事务记录,就需要Learner服务器进行事务回滚--回滚到Leader服务器同步,也是最接近peerLastZxid的ZXID
仅回滚(TRUNC同步)
场景:peerLastZxid 大于 maxCommittedLog
全量同步(SNAP同步)
场景一:peerLastZxid 小于 minCommittedLog
场景二:Leader服务器上没有Proposal缓存队列,peerLastZxid不等于lastProcessZxid
Zookeeper数据同步为四类
1,直接差异化同步(DIFF同步)
2,先回滚再差异化同步(TRUNC+DIFF同步)
3,仅回滚同步(TRUNC同步)
4,全量同步(SNAP同步)
数据同步前,Leader服务器会完成数据同步初始化:
peerLastZxid:从learner(follower或observer)注册发送ACKEPOCH消息中提取* lastZxid(Learner服务器最后处理ZXID)
minCommittedLog:Leader服务器Proposal缓存队列committedLog中最小ZXID
maxCommittedLog:Leader服务器Proposal缓存队列committedLog中最大ZXID
直接差异化同步(DIFF同步)
场景:peerLastZxid介于minCommittedLog和maxCommittedLog间
先回滚,再差异化同步(TRUNC+DIFF同步)
场景:新Leader服务器发现Learner服务器含自己没有事务记录,就需要Learner服务器进行事务回滚--回滚到Leader服务器同步,也是最接近peerLastZxid的ZXID
仅回滚(TRUNC同步)
场景:peerLastZxid 大于 maxCommittedLog
全量同步(SNAP同步)
场景一:peerLastZxid 小于 minCommittedLog
场景二:Leader服务器上没有Proposal缓存队列,peerLastZxid不等于lastProcessZxid
zookeeper是如何保证事务的顺序一致性的?
zookeeper采用了全局递增事务Id来标识,所有proposal(提议)都在被提出时加上zxid,zxid是一个64位数字,高32位epoch(时期; 纪元; 世; 新时代)用来标识leader周期,有新的leader产生出来,epoch会自增,低32位用来递增计数。当新生proposal时,会依据数据库两阶段过程,先会向其他server发出事务执行请求,超过半数机器都能执行,且能够成功,那么就会开始执行。
集群支持动态添加机器吗?
水平扩容了:
全部重启:关闭所有Zookeeper服务,修改配置后启动。不影响之前客户端会话。
逐个重启:过半存活原则下,一台重启不影响整个集群对外提供服务。比较常用的方式。
3.5版本开始支持动态扩容。
全部重启:关闭所有Zookeeper服务,修改配置后启动。不影响之前客户端会话。
逐个重启:过半存活原则下,一台重启不影响整个集群对外提供服务。比较常用的方式。
3.5版本开始支持动态扩容。
ZAB和Paxos算法的联系与区别?
相同点:
1,都存在类似于Leader角色,负责协调多Follower进程运行
2,Leader等待超过半数Follower正确反馈后,才会提交提案
3,存在Leader周期的属性,ZAB协议epoch,Paxos中Ballot
不同点:
应用场景不同,ZAB构建高可用分布式数据主备系统,Paxos构建分布式一致性状态机系统。
1,都存在类似于Leader角色,负责协调多Follower进程运行
2,Leader等待超过半数Follower正确反馈后,才会提交提案
3,存在Leader周期的属性,ZAB协议epoch,Paxos中Ballot
不同点:
应用场景不同,ZAB构建高可用分布式数据主备系统,Paxos构建分布式一致性状态机系统。
ZooKeeper分布式锁的优点和缺点?
ZooKeeper分布式锁:
(1)优点:ZooKeeper分布式锁(如InterProcessMutex),有效的解决分布式系统资源竞争,不可重入问题,使用起来也较为简单。
(2)缺点:ZooKeeper实现分布式锁,性能并不太高。为啥呢?
创建锁和释放锁,要动态创建、销毁临时节点来。ZK中创建和删除节点,只能通过Leader服务器来执行,Leader还需要将数据同步到所有Follower机器上,这样频繁网络通信,性能短板,非常突出。
(1)优点:ZooKeeper分布式锁(如InterProcessMutex),有效的解决分布式系统资源竞争,不可重入问题,使用起来也较为简单。
(2)缺点:ZooKeeper实现分布式锁,性能并不太高。为啥呢?
创建锁和释放锁,要动态创建、销毁临时节点来。ZK中创建和删除节点,只能通过Leader服务器来执行,Leader还需要将数据同步到所有Follower机器上,这样频繁网络通信,性能短板,非常突出。
dubbo
ShardingSphere
分库分表
一致性算法
ZAB
设计初衷
专为Apache ZooKeeper设计,用于实现一致性的状态机复制服务。
特点
强调顺序一致性,更新在有副本上,以相同顺序执行。
两个关键阶段:崩溃恢复和消息广播。
1,领导者选举过程与数据同步相结合,保证新领导者拥有最新事务状态。
2,简化了Paxos的部分复杂性,并针对ZooKeeper的服务特性进行了优化。
两个关键阶段:崩溃恢复和消息广播。
1,领导者选举过程与数据同步相结合,保证新领导者拥有最新事务状态。
2,简化了Paxos的部分复杂性,并针对ZooKeeper的服务特性进行了优化。
优点
ZAB为Apache ZooKeeper专门设计,高度契合ZooKeeper的需求,确保了数据的强一致性。
强调顺序一致性,保证所有更新操作在所有服务器上按全局一致的顺序进行。
针对崩溃恢复进行了优化,能够快速重新选举并同步集群状态。
强调顺序一致性,保证所有更新操作在所有服务器上按全局一致的顺序进行。
针对崩溃恢复进行了优化,能够快速重新选举并同步集群状态。
缺点
主要应用于ZooKeeper这样的协调服务中,通用性不如Raft和Paxos广泛。
对于非ZooKeeper应用场景,需要更多的定制化工作来适应具体需求。
对于非ZooKeeper应用场景,需要更多的定制化工作来适应具体需求。
使用场景
主要用于构建高可用的服务协调系统(如ZooKeeper),处理分布式锁、配置管理等任务。
raft
设计初衷
设计目标:易于理解和实现,是Paxos的一种更具体且工程化的变体。
特点
明确强领导者概念,每任期只有一个领导者,处理客请求,管理日志复制。
详细定义领导人选举、日志复制、安全状态转换内,所有操作流程。
成员变更(如集群扩容或缩容)清晰的操作指导。
对于Paxos,Raft通过更直观的设计降低实现难度,提高可理解性。
详细定义领导人选举、日志复制、安全状态转换内,所有操作流程。
成员变更(如集群扩容或缩容)清晰的操作指导。
对于Paxos,Raft通过更直观的设计降低实现难度,提高可理解性。
优点
设计目标是易于理解与实现,相对于Paxos更直观清晰,适合教学和工程实践。
明确定义了领导者选举机制和日志复制流程,简化了分布式共识过程。
提供了详细的操作步骤和清晰的状态转换图,有利于开发者准确地实现算法。
缺点
虽然相比Paxos更易于理解和实现,但在某些复杂场景下可能略显保守,性能上的优势并不总是明显。
在节点数量较多的情况下,其网络通信开销可能会增加。
在节点数量较多的情况下,其网络通信开销可能会增加。
使用场景
广泛应用于分布式存储系统、数据库、云原生应用以及各种要求强一致性的分布式系统中。
paxos
设计初衷
最早的实用分布式共识算法之一,由Leslie Lamport提出
特点
专注解决“拜占庭将军”问题,网络不稳定情况下,达成一致决策。
基本思想通过提案、投票、接受来达到一致性。
Paxos没有规定领导者角色和选举机制,抽象描述提议和批准过程,直接实现,需要更多工程化努力。
虽然理论基础强大,因其表述相对抽象,实际实现起来相对复杂,不如Raft直观易懂。
基本思想通过提案、投票、接受来达到一致性。
Paxos没有规定领导者角色和选举机制,抽象描述提议和批准过程,直接实现,需要更多工程化努力。
虽然理论基础强大,因其表述相对抽象,实际实现起来相对复杂,不如Raft直观易懂。
优点
是分布式系统中最基础且经过严格理论证明的一致性算法,具有非常高的容错性和稳定性。
理论上可以解决任何分布式一致性问题,提供了强大的理论支撑。
理论上可以解决任何分布式一致性问题,提供了强大的理论支撑。
缺点
Paxos的原始描述较为抽象和复杂,直接从学术论文中理解并实现起来相对困难。
对于实际工程应用,需要开发人员深入理解并根据具体场景进行适当的优化和简化
对于实际工程应用,需要开发人员深入理解并根据具体场景进行适当的优化和简化
使用场景
常用于构建大型分布式数据库、分布式文件系统以及其他要求极高一致性和容错性的系统底层技术。
消息中间件
Kafka
概念
Broker
Kafka集群包含一个或多个服务器,这种服务器被称为broker。broker端不维护数据的消费状态,提升了性能。直接使用磁盘进行存储,线性读写,速度快:避免了数据在JVM内存和系统内存之间的复制,减少耗性能的创建对象和垃圾回收
Kafka集群包含一个或多个服务器,这种服务器被称为broker。broker端不维护数据的消费状态,提升了性能。直接使用磁盘进行存储,线性读写,速度快:避免了数据在JVM内存和系统内存之间的复制,减少耗性能的创建对象和垃圾回收
Producer
消息生产者,负责发布消息到Kafka broker
消息生产者,负责发布消息到Kafka broker
Consumer
消息消费者,向Kafka broker读取消息的客户端,consumer从broker拉取(pull)数据并进行处理
消息消费者,向Kafka broker读取消息的客户端,consumer从broker拉取(pull)数据并进行处理
Topic
每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic,属于逻辑上的概念
每条发布到Kafka集群的消息都有一个类别,这个类别被称为Topic,属于逻辑上的概念
Partition
Parition是物理上的概念,每个Topic包含一个或多个Partition
Parition是物理上的概念,每个Topic包含一个或多个Partition
Consumer Group
每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)
每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)
生产端
消息分发策略?
1、指定具体partition
2、没有指定partition但有key的情况下,将key的hash值与topic的partition数进行取余得到partition
3、没有指定partition且无key的情况下,会随机选择一个分区并一直使用,直到设定的时间到期后(metadata.max.age.ms,默认10分钟),
会重新随机另外一个分区
1、指定具体partition
2、没有指定partition但有key的情况下,将key的hash值与topic的partition数进行取余得到partition
3、没有指定partition且无key的情况下,会随机选择一个分区并一直使用,直到设定的时间到期后(metadata.max.age.ms,默认10分钟),
会重新随机另外一个分区
重要参数
acks
0:生产者不会等待任何来自服务器响应
1(默认):leader写日志成功,才会返回成功
-1或者all:所有ISR的副本全部写日志成功才返回成功
0:生产者不会等待任何来自服务器响应
1(默认):leader写日志成功,才会返回成功
-1或者all:所有ISR的副本全部写日志成功才返回成功
retries
重试次数(默认0),生产者从服务器收到的错误有可能是临时性的错误(比如分区找不到首领),需要重试,
重试间隔默认100ms,可以通过retry.backoff.ms来设置
重试次数(默认0),生产者从服务器收到的错误有可能是临时性的错误(比如分区找不到首领),需要重试,
重试间隔默认100ms,可以通过retry.backoff.ms来设置
buffer.memory
生产端用来缓存消息的缓冲区大小,默认32M
生产者发送消息不是直接发到broker,而是放在缓冲区队列里面,再由后台线程不断从队列中取出消息发送,
发送成功后会触发callback。如果缓冲区不足,会先阻塞一段时间(max.block.ms),再抛出异常
生产端用来缓存消息的缓冲区大小,默认32M
生产者发送消息不是直接发到broker,而是放在缓冲区队列里面,再由后台线程不断从队列中取出消息发送,
发送成功后会触发callback。如果缓冲区不足,会先阻塞一段时间(max.block.ms),再抛出异常
batch.size
当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里,满了就发送。
默认16K,该值过小的话,会降低吞吐量,过大的话,会带来较大的内存压力
当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里,满了就发送。
默认16K,该值过小的话,会降低吞吐量,过大的话,会带来较大的内存压力
linger.ms
该参数指定了生产者在发送批次消息之前等待更多消息加入批次的时间。
KafkaProducer会在批次填满或linger.ms达到上限时把批次发送出去。
默认0,表示不等待,这种情况会导致批次消息过少就被发送了
该参数指定了生产者在发送批次消息之前等待更多消息加入批次的时间。
KafkaProducer会在批次填满或linger.ms达到上限时把批次发送出去。
默认0,表示不等待,这种情况会导致批次消息过少就被发送了
max.block.ms
默认60秒,KafkaProducer.send() and KafkaProducer.partitionsFor() 方法最大的阻塞时间,
当buffer.memory满了或者集群的Metadata不可用时,这两个方法会被阻塞。
默认60秒,KafkaProducer.send() and KafkaProducer.partitionsFor() 方法最大的阻塞时间,
当buffer.memory满了或者集群的Metadata不可用时,这两个方法会被阻塞。
request.timeout.ms
Producer向Broker发送请求以后,等待响应的最长时间,默认30秒
Producer向Broker发送请求以后,等待响应的最长时间,默认30秒
消息发送方式
发送并忘记
调用send()方法发送消息,但并不关心它是否正常到达。
producer.send(new ProducerRecord<Integer, String>(topic,msg))
调用send()方法发送消息,但并不关心它是否正常到达。
producer.send(new ProducerRecord<Integer, String>(topic,msg))
同步发送
调用send()方法发送消息,它会返回一个Future对象,调用get()方法进行等待,就可以知道消息是否发送成功。
producer.send(new ProducerRecord<Integer, String>(topic,msg)).get();
调用send()方法发送消息,它会返回一个Future对象,调用get()方法进行等待,就可以知道消息是否发送成功。
producer.send(new ProducerRecord<Integer, String>(topic,msg)).get();
异步发送
调用send()方法发送消息,并指定一个回调函数,服务器在返回响应时调用该函数
调用send()方法发送消息,并指定一个回调函数,服务器在返回响应时调用该函数
数据的传递性语义
至少一次(AtLeastOnce)
默认,可以保证数据不丢失,但是不能保证数据不重复
默认,可以保证数据不丢失,但是不能保证数据不重复
最多一次(AtMostOnce)
可以保证数据不重复,但是不能保证数据不丢失
可以保证数据不重复,但是不能保证数据不丢失
精确一次(Exactly Once)
不会漏传输也不会重复传输
不会漏传输也不会重复传输
消费端
消费者消费原理?
1、一个partition里面的消息,只能被同一consumer group中的某一个consumer来消费
2、想要多个consumer来消费同一个partition,那这多个consumer必须要在不同的consumer group中
3、如果consumer多于partition,则多出的consumer处于空闲浪费状态
1、一个partition里面的消息,只能被同一consumer group中的某一个consumer来消费
2、想要多个consumer来消费同一个partition,那这多个consumer必须要在不同的consumer group中
3、如果consumer多于partition,则多出的consumer处于空闲浪费状态
消费者分区分配策略?
1、Range(范围)默认:分区数/消费者数,多的按顺序分摊到前面几个消费者上面
2、RoundRobin(轮询):轮询分区策略是把所有partition和所有consumer线程都列出来,然后按照hashcode进行排序。
最后通过轮询算法分配partition给消费线程。
3、Stricky(粘性):它主要有两个目的(分区的分配尽可能的均匀分区的分配,尽可能和上次分配保持相同),初始采用轮询,后面变化。
1、Range(范围)默认:分区数/消费者数,多的按顺序分摊到前面几个消费者上面
2、RoundRobin(轮询):轮询分区策略是把所有partition和所有consumer线程都列出来,然后按照hashcode进行排序。
最后通过轮询算法分配partition给消费线程。
3、Stricky(粘性):它主要有两个目的(分区的分配尽可能的均匀分区的分配,尽可能和上次分配保持相同),初始采用轮询,后面变化。
consumer和partition的数量建议?
1、最好partition和consumer数量一致,或者partition是consumer的整数倍
2、consumer大于partition,会导致consumer浪费
3、consumer远小于partition,会导致吞吐量降低
consumer从多个partition读取数据,不保证数据间的顺序性,kafka只保证在一个partition
上数据是有序的
1、最好partition和consumer数量一致,或者partition是consumer的整数倍
2、consumer大于partition,会导致consumer浪费
3、consumer远小于partition,会导致吞吐量降低
consumer从多个partition读取数据,不保证数据间的顺序性,kafka只保证在一个partition
上数据是有序的
什么是rebalance机制?什么时候会触发该机制?
Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 consumer 如何达成一致,来分配订阅 Topic 的每个分区。
1、同一个consumer group内新增了消费者
2、消费者离开当前所属的consumer group,比如主动停机或者宕机
3、topic新增了分区(也就是分区数量发生了变化)
Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 consumer 如何达成一致,来分配订阅 Topic 的每个分区。
1、同一个consumer group内新增了消费者
2、消费者离开当前所属的consumer group,比如主动停机或者宕机
3、topic新增了分区(也就是分区数量发生了变化)
什么是coordinator?coordinator又是如何协调consumer group消费的(rebalance过程)?
coordinator是用来统筹管理consumer group消费的
1、确定coordinator:broker集群会将集群中负载最小的broker定为coordinator
2、获取coordinator:consumer会向集群中任意一个broker发送一个GroupCoordinatorRequest来获取coordinator
3、确定consumer leader:所有的consumer向coordinator发送joinGroup请求,coordinator会从中挑选一个来担任
consumer leader角色(如果没有leader,那么第一个请求的就是Leader;如果leader退出了消费组,则随机选举一个)
4、确定分区分配策略:每个consumer都可以有自己的分区分配策略,在上述joinGroup的过程中,consumer也会将自己的分区分配策略
发给coordinator,由coordinator组成一个侯选集,然后每个consumer都从中选举一个自己支持的策略并投票,最终票数多的为最终策略
5、确定消费partition方案:leader选举出来后,会根据最终策略,产生一个消费partition的方案,然后向coordinator发送一个
SyncGroupRequest请求,将方案同步给coordinator
6、同步方案给消费者:coordinator拿到方案后,将方案同步给其它consumer,除非发生rebalance,否则consumer只会消费对应的Partition
Rebalance期间,整个消费组无法消费消息
coordinator是用来统筹管理consumer group消费的
1、确定coordinator:broker集群会将集群中负载最小的broker定为coordinator
2、获取coordinator:consumer会向集群中任意一个broker发送一个GroupCoordinatorRequest来获取coordinator
3、确定consumer leader:所有的consumer向coordinator发送joinGroup请求,coordinator会从中挑选一个来担任
consumer leader角色(如果没有leader,那么第一个请求的就是Leader;如果leader退出了消费组,则随机选举一个)
4、确定分区分配策略:每个consumer都可以有自己的分区分配策略,在上述joinGroup的过程中,consumer也会将自己的分区分配策略
发给coordinator,由coordinator组成一个侯选集,然后每个consumer都从中选举一个自己支持的策略并投票,最终票数多的为最终策略
5、确定消费partition方案:leader选举出来后,会根据最终策略,产生一个消费partition的方案,然后向coordinator发送一个
SyncGroupRequest请求,将方案同步给coordinator
6、同步方案给消费者:coordinator拿到方案后,将方案同步给其它consumer,除非发生rebalance,否则consumer只会消费对应的Partition
Rebalance期间,整个消费组无法消费消息
服务端
概念
Controller(控制器)
在Kafka集群中会有一个broker会被选举为控制器(KafkaController),它负责管理整个集群中所有分区和副本的状态
1、监听broker相关的变化
2、监听topic相关的变化
3、从Zookeeper中读取获取当前所有与topic、partition以及broker有关的信息并进行相应的管理
4、更新集群的元数据信息,同步到其他普通的broker节点中
在Kafka集群中会有一个broker会被选举为控制器(KafkaController),它负责管理整个集群中所有分区和副本的状态
1、监听broker相关的变化
2、监听topic相关的变化
3、从Zookeeper中读取获取当前所有与topic、partition以及broker有关的信息并进行相应的管理
4、更新集群的元数据信息,同步到其他普通的broker节点中
Leader
partition中的Leader副本,提供读写服务,并且同步数据给Follower副本
partition中的Leader副本,提供读写服务,并且同步数据给Follower副本
Follower
partition中的Follower副本,不提供读写服务,用于备份Leader副本中的数据,与Leader是一主多从关系
partition中的Follower副本,不提供读写服务,用于备份Leader副本中的数据,与Leader是一主多从关系
AR(Assigned Repllicas)
partition中的所有副本集合
partition中的所有副本集合
ISR(In-Sync Replicas)
与leader副本保持一定程度同步(replica.lag.time.max.ms时间内有同步)的副本集合(ISR中的第一个副本就是Leader副本),ISR是AR的子集
Leader副本负责维护和跟踪ISR集合中所有的follower副本的滞后状态,当follower副本落后太多或者失效时,leader副本会吧它从ISR集合中剔除
与leader副本保持一定程度同步(replica.lag.time.max.ms时间内有同步)的副本集合(ISR中的第一个副本就是Leader副本),ISR是AR的子集
Leader副本负责维护和跟踪ISR集合中所有的follower副本的滞后状态,当follower副本落后太多或者失效时,leader副本会吧它从ISR集合中剔除
OSR(Out-Sync Relipcas)
与leader副本同步滞后过多的副本集合,AR = ISR + OSR
与leader副本同步滞后过多的副本集合,AR = ISR + OSR
LEO(Log End Offset)
标识当前日志文件中下一条待写入的消息的offset。LEO 的大小相当于当前日志分区中最后一条消息的offset值加1。
标识当前日志文件中下一条待写入的消息的offset。LEO 的大小相当于当前日志分区中最后一条消息的offset值加1。
HW(High Watermark)
俗称高水位,它标识了一个消息偏移量(offset),消费者只能拉取到这个offset之前的消息。
HW为ISR中所有副本的最小LEO
俗称高水位,它标识了一个消息偏移量(offset),消费者只能拉取到这个offset之前的消息。
HW为ISR中所有副本的最小LEO
LW(Low Watermark)
俗称低水位,AR集合中最小的Log Start Offset
俗称低水位,AR集合中最小的Log Start Offset
Controller选举
优先到zookeeper上面创建/controller临时节点的broker为Controller
优先到zookeeper上面创建/controller临时节点的broker为Controller
Partition副本Leader选举
Controller会从ISR列表(参数unclean.leader.election.enable=false的前提下)里挑第一个broker作为Leader(第一个broker最先放进ISR列表,可能是同步数据最多的副本),如果参数unclean.leader.election.enable=true,代表在ISR列表里所有副本都挂了的时候可以在ISR列表以外的副本中选Leader
Controller会从ISR列表(参数unclean.leader.election.enable=false的前提下)里挑第一个broker作为Leader(第一个broker最先放进ISR列表,可能是同步数据最多的副本),如果参数unclean.leader.election.enable=true,代表在ISR列表里所有副本都挂了的时候可以在ISR列表以外的副本中选Leader
Kafka数据同步方式?
Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制,而是半同步复制。
Kafka使用这种ISR的方式有效的权衡了数据可靠性和性能之间关系
Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制,而是半同步复制。
Kafka使用这种ISR的方式有效的权衡了数据可靠性和性能之间关系
Kafka如何保存消费端的消费位置?
Kafka是通过offset来保存消费者的位置的。在kafka中,提供了一个consumer_offsets_* 的一个topic,把offset信息写入到这个topic中。
offset不跨分区,只保证同一分区的offset的顺序性
Kafka是通过offset来保存消费者的位置的。在kafka中,提供了一个consumer_offsets_* 的一个topic,把offset信息写入到这个topic中。
offset不跨分区,只保证同一分区的offset的顺序性
常见问题汇总
Kafka为什么速度这么快?
1、利用 Partition实现数据分区存储,提高并发处理效率
2、利用页缓存技术,提高IO效率
3、顺序写,在刷盘时,将数据追加到文件的末尾,性能不比写内存差多少
4、零拷贝(读):减少了从os cache拷贝到应用缓存和从应用缓存拷贝到socket缓存。
Socket缓存中仅仅会拷贝一个描述符过去,不会拷贝数据到Socket缓存
5、消息批量发送,提高吞吐量
6、数据压缩:Kafka还支持对消息集合进行压缩,Producer可以通过GZIP、Snappy、LZ4格式对消息集合进行压缩
操作系统本身有一层缓存,叫做page cache,是在内存里的缓存,我们也可以称之为os cache,意思就是操作系统自己管理的缓存。
1、利用 Partition实现数据分区存储,提高并发处理效率
2、利用页缓存技术,提高IO效率
3、顺序写,在刷盘时,将数据追加到文件的末尾,性能不比写内存差多少
4、零拷贝(读):减少了从os cache拷贝到应用缓存和从应用缓存拷贝到socket缓存。
Socket缓存中仅仅会拷贝一个描述符过去,不会拷贝数据到Socket缓存
5、消息批量发送,提高吞吐量
6、数据压缩:Kafka还支持对消息集合进行压缩,Producer可以通过GZIP、Snappy、LZ4格式对消息集合进行压缩
操作系统本身有一层缓存,叫做page cache,是在内存里的缓存,我们也可以称之为os cache,意思就是操作系统自己管理的缓存。
Kafka如何快速读取指定offset的消息?
1、Partition数据是分段(Segment)存储,每个分段都包含log、index、timeindex三个文件。
2、offset根据二分法定位到index稀疏索引文件
3、然后根据索引文件中的[offset,position](position为物理偏移地址)去log中获取指定offset的message数据。
1、Partition数据是分段(Segment)存储,每个分段都包含log、index、timeindex三个文件。
2、offset根据二分法定位到index稀疏索引文件
3、然后根据索引文件中的[offset,position](position为物理偏移地址)去log中获取指定offset的message数据。
Kafka如何保证消息不丢失?
1、设置 ack=-1,保证ISR所有副本全部写日志成功才返回;producer.type=sync,设置成同步模式
2、设置 replication.factor>=3,并且 min.insync.replicas>=2,保证ISR中副本集至少大于等于2
3、设置 unclean.leader.election.enable=false,关闭跟不上Leader的follower副本的选举
4、设置 enable.auto.commit=false,消费者改为手动提交
第1点是生产端,第2、3点是服务端,第4点为消费端
-- min.insync.replicas:ISR里面的最小副本数
-- replication.factor:这个参数用来表示分区的副本数
1、设置 ack=-1,保证ISR所有副本全部写日志成功才返回;producer.type=sync,设置成同步模式
2、设置 replication.factor>=3,并且 min.insync.replicas>=2,保证ISR中副本集至少大于等于2
3、设置 unclean.leader.election.enable=false,关闭跟不上Leader的follower副本的选举
4、设置 enable.auto.commit=false,消费者改为手动提交
第1点是生产端,第2、3点是服务端,第4点为消费端
-- min.insync.replicas:ISR里面的最小副本数
-- replication.factor:这个参数用来表示分区的副本数
Kafka 是怎么去实现负载均衡的?
1、生产者:消息分发策略
2、消费者:分区分配策略
1、生产者:消息分发策略
2、消费者:分区分配策略
为什么Kafka不支持读写分离?
1、读写分离(主写从读)会存在数据一致性和延时问题
2、Kafka本身采取了分区的策略,天然提高了并发效率,不需要进一步采用读写分离来提升效率
1、读写分离(主写从读)会存在数据一致性和延时问题
2、Kafka本身采取了分区的策略,天然提高了并发效率,不需要进一步采用读写分离来提升效率
Kafka 分区数可以增加或减少吗?为什么?
1、支持分区增加,增加后会重新rebalance就好了
2、不支持分区减少,因为会丢失上面的消息
1、支持分区增加,增加后会重新rebalance就好了
2、不支持分区减少,因为会丢失上面的消息
Kafka消息是采用Pull模式,还是Push模式?
1、Push:当消息生产速率远大于消费速率,消费者容易崩溃
2、Pull:可以根据自己的消费能力拉取数据,但是如果没有消息时,会不断轮询,可以在消费端设置轮询间隔
1、Push:当消息生产速率远大于消费速率,消费者容易崩溃
2、Pull:可以根据自己的消费能力拉取数据,但是如果没有消息时,会不断轮询,可以在消费端设置轮询间隔
Kafka的分区数是不是越多越好? -- 不是
1、生产者缓存占用内存会更多(一个分区会有一个batch.size的开销)
2、消费者线程数也会更多(一个分区需要一个线程来处理)
3、服务端的很多组件在内存中维护分区级别缓存,比如controller,FetcherManager等,因此分区数越多,这种缓存的成本就越大
4、文件句柄的开销会更多(.index和.log文件数量会更多)
1、生产者缓存占用内存会更多(一个分区会有一个batch.size的开销)
2、消费者线程数也会更多(一个分区需要一个线程来处理)
3、服务端的很多组件在内存中维护分区级别缓存,比如controller,FetcherManager等,因此分区数越多,这种缓存的成本就越大
4、文件句柄的开销会更多(.index和.log文件数量会更多)
谈谈你对Kafka生产者幂等性的了解?
Kafka精确一次性(Exactly-once)保障之一
为了实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。
Kafka精确一次性(Exactly-once)保障之一
为了实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。
- PID。新的Producer在初始化时,分配一个PID,这个PID用户是不可见的。
- Sequence Numbler。对PID,Producer发送数据对应从0单调递增Sequence Number
- Broker端,在缓存保存这seq number,对接收消息,如序号比Broker缓存中序号大于1,则接受,
否,丢弃,这样就可以实现消息重复提交。只能保证单个Producer
Kafka如何保证消息的有序性?
单分区可以保证有序性,多分区不可以
1、将max.in.flight.requests.per.connection设置为1,消息队列只允许一个请求,
消息失败,第一时间发送,不会乱序,会降低网络吞吐量。
2、开启生产者幂等性设置(Exactly-once)
单分区可以保证有序性,多分区不可以
1、将max.in.flight.requests.per.connection设置为1,消息队列只允许一个请求,
消息失败,第一时间发送,不会乱序,会降低网络吞吐量。
2、开启生产者幂等性设置(Exactly-once)
Kafka缺点?
- 由于是批量发送,数据并非真正的实时;
- 对于mqtt协议不支持;
- 不支持物联网传感数据直接接入;
- 支持同分区内消息有序,无法实现全局消息有序;
- 监控不完善,需安装插件;
- 依赖zookeeper进行元数据管理;3.0版本去除
RocketMQ
概念
Broker
-- RocketMQ集群包含一个或多个服务器,服务器被称Broker。Broker是存储和转发消息, 单机大约能承受 10 万 QPS 请求。
-- RocketMQ集群Broker保存总数据一部分,可实现横向扩展。为提升可靠性(防止数据丢失), Broker 可有自己副本(slave)
-- 默认情况下,读写都发生在 master。slaveReadEnable=true 的情况下,slave 也可以参与读负载。只有 Brokerld=1的 slave 才参与读负载
注:Broker 会向所有的Name Server注册
-- RocketMQ集群包含一个或多个服务器,服务器被称Broker。Broker是存储和转发消息, 单机大约能承受 10 万 QPS 请求。
-- RocketMQ集群Broker保存总数据一部分,可实现横向扩展。为提升可靠性(防止数据丢失), Broker 可有自己副本(slave)
-- 默认情况下,读写都发生在 master。slaveReadEnable=true 的情况下,slave 也可以参与读负载。只有 Brokerld=1的 slave 才参与读负载
注:Broker 会向所有的Name Server注册
Master
-- 主节点,配置来设置,不会改变,Master挂了后,从节点不会选举变成主节点
注:4.5以后,基于dledger技术可以进行选举,推举一个slave成master。
-- 主节点,配置来设置,不会改变,Master挂了后,从节点不会选举变成主节点
注:4.5以后,基于dledger技术可以进行选举,推举一个slave成master。
Slave
-- 从节点,用于数据备份,配置来设置,4.5前,不会改变,Master挂了后,从节点不会选举变成主节点
-- 从节点,用于数据备份,配置来设置,4.5前,不会改变,Master挂了后,从节点不会选举变成主节点
NameServer
-- NameServer RocketMQ路由中心
-- Producer 和 Consumer通过NameServer拿到Topic相关信息
-- NameServer间不做数据通信,每个NameServer 节点都保存着全量路由信息
-- NameServer RocketMQ路由中心
-- Producer 和 Consumer通过NameServer拿到Topic相关信息
-- NameServer间不做数据通信,每个NameServer 节点都保存着全量路由信息
Producer
-- Producer 跟 Name Server 任意节点(随机)建立长连接,定期从Name Server拉取 Topic 路由信息
-- 根据Topic路由信息与指定Broker 建立TCP长连接
-- 发送逻辑一致Producer 可组成一个 Group
-- RocketMQ生产者支持批量发送,List要自己传进去
-- Producer写数据只能操作 master 节点
-- Producer 跟 Name Server 任意节点(随机)建立长连接,定期从Name Server拉取 Topic 路由信息
-- 根据Topic路由信息与指定Broker 建立TCP长连接
-- 发送逻辑一致Producer 可组成一个 Group
-- RocketMQ生产者支持批量发送,List要自己传进去
-- Producer写数据只能操作 master 节点
Consumer
-- Consumer 跟 Name Server 任意节点(随机)建立长连接,定期从 Name Server 拉取 Topic 路由信息
-- 根据Topic路由信息与指定的Master和Slave 建立 TCP长连接
-- 消费逻辑一致 Consumer可组成一个Group
-- 在Brokerld=1 slaveReadEnable=true 的情况下,slave 也可以参与读负载
-- Consumer 跟 Name Server 任意节点(随机)建立长连接,定期从 Name Server 拉取 Topic 路由信息
-- 根据Topic路由信息与指定的Master和Slave 建立 TCP长连接
-- 消费逻辑一致 Consumer可组成一个Group
-- 在Brokerld=1 slaveReadEnable=true 的情况下,slave 也可以参与读负载
Topic
在 RocketMQ中,Topic是一个逻辑概念,消息不是按 Topic 划分存储的
在 RocketMQ中,Topic是一个逻辑概念,消息不是按 Topic 划分存储的
Message Queue
类似Kafka里面的Partition分片概念,topic的数据是分片存储
类似Kafka里面的Partition分片概念,topic的数据是分片存储
生产端
发送方式
同步发送
消息发送后,producer等到broker回应后,才能继续发送下一个消息
消息发送后,producer等到broker回应后,才能继续发送下一个消息
异步发送
发出数据后,不等borker响应,发送下数据包
注:有一个异步发送回调接口(SendCallback)来处理返回信息
发出数据后,不等borker响应,发送下数据包
注:有一个异步发送回调接口(SendCallback)来处理返回信息
单向发送(Oneway)
只发送请求,不等待应答,效率最高
只发送请求,不等待应答,效率最高
顺序发送(OrderProducer)
顺序发送比较复杂,支持局部顺序消息,要保证发送端,服务端,消费端顺序保持一致
顺序发送比较复杂,支持局部顺序消息,要保证发送端,服务端,消费端顺序保持一致
服务端
消息的存储结构是怎么样的?
-- CommitLog:存储消息元数据。消息顺序存到CommitLog文件。CommitLog
多个文件组成,每个文件固定大小1G。以第条消息偏移量为文件名。
-- ComsumeQueue:存储消息CommitLog的索引。记录当前
MessageQueue被存到了哪一条CommitLog。
-- IndexFile:为消息查询提供了一种通过key或时间区间,来查询消息的方法,这种通过IndexFile来
查找消息的方法,不影响发送与消费消息主流程
-- CommitLog:存储消息元数据。消息顺序存到CommitLog文件。CommitLog
多个文件组成,每个文件固定大小1G。以第条消息偏移量为文件名。
-- ComsumeQueue:存储消息CommitLog的索引。记录当前
MessageQueue被存到了哪一条CommitLog。
-- IndexFile:为消息查询提供了一种通过key或时间区间,来查询消息的方法,这种通过IndexFile来
查找消息的方法,不影响发送与消费消息主流程
刷盘机制是怎么样的?
-- 同步刷盘,消息写入内存PAGECACHE后,立刻通知线程刷盘, 然后等待刷盘完成,
执行完成后唤醒等待线程,返回消息写成功状态
-- 异步刷盘,消息可能写入内存PAGECACHE就返回消息成功,何时刷盘,操作系统自己决定
注:配置方式,Broker配置文件里的flushDiskType(SYNC_FLUSH、ASYNC_FLUSH)
-- 同步刷盘,消息写入内存PAGECACHE后,立刻通知线程刷盘, 然后等待刷盘完成,
执行完成后唤醒等待线程,返回消息写成功状态
-- 异步刷盘,消息可能写入内存PAGECACHE就返回消息成功,何时刷盘,操作系统自己决定
注:配置方式,Broker配置文件里的flushDiskType(SYNC_FLUSH、ASYNC_FLUSH)
消息主从复制是怎么样的?
-- 同步复制,Master和Slave都写消息成功后,才反馈给客户端写成功状态
-- 异步复制,只要master写消息成功,就反馈给客户端写成功。再异步将消息复制给
Slave节点。
注:配置方式,Broker配置文件里的brokerRole(ASYNC_MASTER、 SYNC_MASTER、SLAVE)
-- 同步复制,Master和Slave都写消息成功后,才反馈给客户端写成功状态
-- 异步复制,只要master写消息成功,就反馈给客户端写成功。再异步将消息复制给
Slave节点。
注:配置方式,Broker配置文件里的brokerRole(ASYNC_MASTER、 SYNC_MASTER、SLAVE)
消费端
消息模式
集群消费(Clustering)
相同Consumer Group每Consumer实例协作 分摊消息
相同Consumer Group每Consumer实例协作 分摊消息
广播消费(Broadcasting)
相同Consumer Group的每Consumer实例接收全量的消息
相同Consumer Group的每Consumer实例接收全量的消息
消费形式
拉取式消费(pull)
Consumer主动从Broker服务器拉消息
Consumer主动从Broker服务器拉消息
推动式消费(push)
Broker收到数据,主动推送给消费端,实时性比较高,底层同样是pull
Broker收到数据,主动推送给消费端,实时性比较高,底层同样是pull
路由端(NameServer)
NameServer 作为路由中心到底怎么工作的呢?
-- 节点在启动时,根据配置NameServer列表,与每个NameServer建立TCP长连接,并注册自己信息
-- Broker(包含master和slave)每30s发送心跳信息,表示自己存活
-- NameServer每10s检查Broker 最近一次心跳时间,发现Broker超过120s都没发送心跳,
认为Broker 挂掉,将其从路由信息里移除。
-- 节点在启动时,根据配置NameServer列表,与每个NameServer建立TCP长连接,并注册自己信息
-- Broker(包含master和slave)每30s发送心跳信息,表示自己存活
-- NameServer每10s检查Broker 最近一次心跳时间,发现Broker超过120s都没发送心跳,
认为Broker 挂掉,将其从路由信息里移除。
NameServer 之间是互相不通信的,也没有主从之分,它们是怎么保持一致性的?
-- 服务注册,Broker都会向所有NameServer进行注册,并且每隔30s发送心跳
-- 服务剔除,Broker正常关闭(连接断开),Netty通道关闭,监听器监听到连接断开事件;
Broker异常关闭,NameServer每10s检查,某Broker120s都没有发送心跳,可剔除
-- 路由发现,生产者在发送第一条消息时,根据从NameServer 获取路由信息,
消费者一般订阅固定Topic,启动时候就要获取 Broker 信息
注:
-- NameServer 不会主动推送服务信息给客户端(指生产端和消费端),
-- 客户端不会发送心跳到Nameserver
-- 客户端每隔30s到NameServer拉取最新信息,缓存在本地
-- NameServer 是AP,只保证最终一致性,Zookeeper是CP
-- 服务注册,Broker都会向所有NameServer进行注册,并且每隔30s发送心跳
-- 服务剔除,Broker正常关闭(连接断开),Netty通道关闭,监听器监听到连接断开事件;
Broker异常关闭,NameServer每10s检查,某Broker120s都没有发送心跳,可剔除
-- 路由发现,生产者在发送第一条消息时,根据从NameServer 获取路由信息,
消费者一般订阅固定Topic,启动时候就要获取 Broker 信息
注:
-- NameServer 不会主动推送服务信息给客户端(指生产端和消费端),
-- 客户端不会发送心跳到Nameserver
-- 客户端每隔30s到NameServer拉取最新信息,缓存在本地
-- NameServer 是AP,只保证最终一致性,Zookeeper是CP
如果作为路由中心的 NameServer 全部挂掉了,而且暂时没有恢复呢?
-- 客户端会缓存路由信息在本地,不完全依赖于 NameServer
-- 客户端会缓存路由信息在本地,不完全依赖于 NameServer
如果 Broker 刚挂,怎么处理数据?
-- 重试
-- 把无法连接Broker 隔离掉,不再连接
-- 优先选择延迟小节点,就能避免连接到容易挂 Broker 了
-- 重试
-- 把无法连接Broker 隔离掉,不再连接
-- 优先选择延迟小节点,就能避免连接到容易挂 Broker 了
常见问题汇总
RocketMQ磁盘保存文件慢吗?
-- 顺序写,保证了消息存储的速度。高性能磁盘顺序写速度可以达到600MB/s
-- 零拷贝,减少了2次数据在内存中的复制(内核空间到用户空间及用户空间到内核socket)
-- 页缓存,利用页缓存技术,提高IO效率
注:
-- RocketMQ采用的是mmap技术,Kafka采用的是sendfile技术
-- 关于零拷贝,JAVA的NIO中提供了两种实现方式,mmap和sendfile,其中mmap适合比较小的文
件(1.5~2G),而sendfile适合传递比较大的文件
-- 顺序写,保证了消息存储的速度。高性能磁盘顺序写速度可以达到600MB/s
-- 零拷贝,减少了2次数据在内存中的复制(内核空间到用户空间及用户空间到内核socket)
-- 页缓存,利用页缓存技术,提高IO效率
注:
-- RocketMQ采用的是mmap技术,Kafka采用的是sendfile技术
-- 关于零拷贝,JAVA的NIO中提供了两种实现方式,mmap和sendfile,其中mmap适合比较小的文
件(1.5~2G),而sendfile适合传递比较大的文件
RocketMQ延时消息是怎么实现的?
1、producer 要将一个延迟消息发送到某个 Topic 中
2、Broker 判断这是一个延迟消息后,将其通过临时存储进行暂存
3、Broker 内部通过一个延迟服务(delay service)检查消息是否到期,将到期的消息投递到目标 Topic 中
4、消费者消费目标 topic 中的延迟投递的消息
注:
对外非商业的版本,延时设置有18个级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
1、producer 要将一个延迟消息发送到某个 Topic 中
2、Broker 判断这是一个延迟消息后,将其通过临时存储进行暂存
3、Broker 内部通过一个延迟服务(delay service)检查消息是否到期,将到期的消息投递到目标 Topic 中
4、消费者消费目标 topic 中的延迟投递的消息
注:
对外非商业的版本,延时设置有18个级别:1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
RocketMQ事务消息实现原理?即怎么保证数据一致性?
注:half消息对消费者是不可见的,服务端和broker间的通信
注:half消息对消费者是不可见的,服务端和broker间的通信
RocketMQ如何保证消息不丢失?
1、同步发送(等待返回结果)+重试机制,尽可能减小消息丢失的可能性(或者生产者使用事务消息机制)
2、Broker配置,分区容错性,同步刷盘+Dledger主从架构 + 多个master节点
3、消费者不用异步消费 + 消费消息重试机制
1、同步发送(等待返回结果)+重试机制,尽可能减小消息丢失的可能性(或者生产者使用事务消息机制)
2、Broker配置,分区容错性,同步刷盘+Dledger主从架构 + 多个master节点
3、消费者不用异步消费 + 消费消息重试机制
RocketMQ如何保证消息顺序消费?
1、发送端:同步发送(保证消息发送顺序) + 自定义投放策略(保证消息存储在同个MessageQueue)
2、消费端:使用有序消费模式MessageListenerOrderly,MessageListenerOrderly是通过加分布式锁和本地锁,
保证同时只有一条线程,去消费一个队列上数据。
1、发送端:同步发送(保证消息发送顺序) + 自定义投放策略(保证消息存储在同个MessageQueue)
2、消费端:使用有序消费模式MessageListenerOrderly,MessageListenerOrderly是通过加分布式锁和本地锁,
保证同时只有一条线程,去消费一个队列上数据。
消息消费失败,消费端处理机制?
1,消费消息出现异常,RocketMQ默认多次重试。重试次数和重试间隔时间客户端配置,通过consumer的retryTimesWhenSendFailed设置重试次数,通过consumeMessageBatchMaxSize与consumeConcurrentlyMaxSpan等参数调整消费批次大小,并发度
2,若干次重试仍无法成功消费,RocketMQ可将消息发延时队列,后再尝试消费,自定义延时消息或者死信队列来实现。
3,最大重试次数后仍未正确处理,RocketMQ将其存入死信队列。监听死信队列来排查问题、修复错误,并对死信进行特殊处理。
4,对至少被消费一次的场景,RocketMQ提供事务消息支持。当事务消息二次确认失败,服务端会进行回查,根据业务结果决定是回滚,再次投递。
5,在某些高级使用场景下,消费者可手动控制消息ACK(确认收到并处理完成)或NACK(拒绝接收或处理失败)。如NACK,认为该消息未被正确处理,并触发重试机制。
1,消费消息出现异常,RocketMQ默认多次重试。重试次数和重试间隔时间客户端配置,通过consumer的retryTimesWhenSendFailed设置重试次数,通过consumeMessageBatchMaxSize与consumeConcurrentlyMaxSpan等参数调整消费批次大小,并发度
2,若干次重试仍无法成功消费,RocketMQ可将消息发延时队列,后再尝试消费,自定义延时消息或者死信队列来实现。
3,最大重试次数后仍未正确处理,RocketMQ将其存入死信队列。监听死信队列来排查问题、修复错误,并对死信进行特殊处理。
4,对至少被消费一次的场景,RocketMQ提供事务消息支持。当事务消息二次确认失败,服务端会进行回查,根据业务结果决定是回滚,再次投递。
5,在某些高级使用场景下,消费者可手动控制消息ACK(确认收到并处理完成)或NACK(拒绝接收或处理失败)。如NACK,认为该消息未被正确处理,并触发重试机制。
常见问题
为什么使用消息中间件?
-- 解耦、异步、削峰
引入消息队列会存在什么问题?
-- 1,系统实际可用性降低(业务系统正常,但消息中间件挂了,或者堆积不消费)
-- 2,系统复杂性提高(1,消息丢失,2,重复消费、3,顺序消费、4,数据一致性问题等)
Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么优点和缺点?
-- Kafka:高可用(分布式),高吞吐(10W级),低延时(ms级别),消息0丢失,功能简单(适合大数据计算及数据采集)
-- RocketMQ:高可用(分布式),高吞吐(10W级),低延时(ms级别),消息0丢失,MQ功能完善(适合业务需求)
-- RabbitMQ:高可用(主从),高吞吐(1W级),超低延时(微秒级别),消息基本不丢失,MQ功能完善
-- ActiveMQ:高可用(主从),高吞吐(1W级),低延时(ms级别),消息较低丢失,MQ功能完善
-- 解耦、异步、削峰
引入消息队列会存在什么问题?
-- 1,系统实际可用性降低(业务系统正常,但消息中间件挂了,或者堆积不消费)
-- 2,系统复杂性提高(1,消息丢失,2,重复消费、3,顺序消费、4,数据一致性问题等)
Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么优点和缺点?
-- Kafka:高可用(分布式),高吞吐(10W级),低延时(ms级别),消息0丢失,功能简单(适合大数据计算及数据采集)
-- RocketMQ:高可用(分布式),高吞吐(10W级),低延时(ms级别),消息0丢失,MQ功能完善(适合业务需求)
-- RabbitMQ:高可用(主从),高吞吐(1W级),超低延时(微秒级别),消息基本不丢失,MQ功能完善
-- ActiveMQ:高可用(主从),高吞吐(1W级),低延时(ms级别),消息较低丢失,MQ功能完善
如何保证消息队列的高可用?
以Kafka为例,
1、数据持久化到磁盘 sendFile
2、数据分布式存储,每个分片数据都有其备份, 分区容错
3、partiton leader的选举机制(ISR) 选举leader提供服务
注:Kafka读写数据都发生在leader上面,且leader的选举机制,保证了数据的一致性
以Kafka为例,
1、数据持久化到磁盘 sendFile
2、数据分布式存储,每个分片数据都有其备份, 分区容错
3、partiton leader的选举机制(ISR) 选举leader提供服务
注:Kafka读写数据都发生在leader上面,且leader的选举机制,保证了数据的一致性
如何保证消息不丢失? -- Kafka
1、设置 ack=-1,保证ISR所有副本全部写日志成功才返回;producer.type=sync,设置成同步模式,retries=MAX无限重试
2、设置 replication.factor>=3,并且 min.insync.replicas>=2,保证ISR中副本集至少大于等于2
3、设置 unclean.leader.election.enable=false,关闭跟不上Leader的follower副本的选举
4、设置 enable.auto.commit=false,消费者改为手动提交
第1点是生产端,第2、3点是服务端,第4点为消费端
-- min.insync.replicas:ISR里面的最小副本数
-- replication.factor:这个参数用来表示分区的副本数
1、设置 ack=-1,保证ISR所有副本全部写日志成功才返回;producer.type=sync,设置成同步模式,retries=MAX无限重试
2、设置 replication.factor>=3,并且 min.insync.replicas>=2,保证ISR中副本集至少大于等于2
3、设置 unclean.leader.election.enable=false,关闭跟不上Leader的follower副本的选举
4、设置 enable.auto.commit=false,消费者改为手动提交
第1点是生产端,第2、3点是服务端,第4点为消费端
-- min.insync.replicas:ISR里面的最小副本数
-- replication.factor:这个参数用来表示分区的副本数
如何保证消息不被重复消费(幂等性)?
1、消费者消费完消息,但还未提交到kafka更新offset,宕机了,导致offset未变化
2、通过数据库唯一约束或者redis唯一键来判断
1、消费者消费完消息,但还未提交到kafka更新offset,宕机了,导致offset未变化
2、通过数据库唯一约束或者redis唯一键来判断
如何保证消息的顺序性? -- Kafka
-- 一个消费者开多个线程来处理一个Partition里面的顺序消息时,会出现顺序错乱的问题
解决方案:写N个queue,相同的Key的数据放到同一个queue,然后开N个线程,每个线程分别消费其中一个queue。
这样就保证了同一个key的消息是顺序消费的,而且也兼顾了吞吐量
-- 一个消费者开多个线程来处理一个Partition里面的顺序消息时,会出现顺序错乱的问题
解决方案:写N个queue,相同的Key的数据放到同一个queue,然后开N个线程,每个线程分别消费其中一个queue。
这样就保证了同一个key的消息是顺序消费的,而且也兼顾了吞吐量
有几百万消息持续积压几小时,说说怎么解决? -- Kafka扩容
1、先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
2、新建一个 topic,partition 是原来的 10 倍
3、写一个临时分发的程序,将积压的消息转发到新的topic里面来
4、部署10倍的 consumer 来消费新的topic
5、消费完后,切换成原有的架构
注意保证消息幂等性
mq 写满了丢失数据怎么处理?
如消息积压,上面扩容方案没有来得及实施,mq就满了,这时只能丢失部分数据,
后面再从业务系统中找出数据后写入mq
1、先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。
2、新建一个 topic,partition 是原来的 10 倍
3、写一个临时分发的程序,将积压的消息转发到新的topic里面来
4、部署10倍的 consumer 来消费新的topic
5、消费完后,切换成原有的架构
注意保证消息幂等性
mq 写满了丢失数据怎么处理?
如消息积压,上面扩容方案没有来得及实施,mq就满了,这时只能丢失部分数据,
后面再从业务系统中找出数据后写入mq
如果让你写一个消息队列,该如何进行架构设计?
1、可伸缩(快速扩容)、高吞吐量:Kafka分区机制
2、数据安全性,针对服务器宕机,断电,服务异常重启:持久化磁盘,顺序读写(rocketmq,kafka)
3、高可用:Kafka多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务
4、数据0丢失:kafka 数据零丢失方案
1、可伸缩(快速扩容)、高吞吐量:Kafka分区机制
2、数据安全性,针对服务器宕机,断电,服务异常重启:持久化磁盘,顺序读写(rocketmq,kafka)
3、高可用:Kafka多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务
4、数据0丢失:kafka 数据零丢失方案
存储中间件
redis
什么是redis?
基于内存运行非关系型(NoSql)数据库。
redis特性?
1,丰富数据类型;
2,进程内与跨进程;3,单机与分布式;
4,功能丰富:持久化机制、过期策略;
5,支持多种编程语言;
6,高可用,集群;
2,进程内与跨进程;3,单机与分布式;
4,功能丰富:持久化机制、过期策略;
5,支持多种编程语言;
6,高可用,集群;
redis是单线程的吗?
- 单线程「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」一个线程(主线程)来完成
- Redis 启动时,会创建3个BIO线程「1,关闭文件(BIO_CLOSE_FILE)、2,AOF 刷盘(BIO_AOF_FSYNC)、3,释放内存(BIO_LAZY_FREE)」。
- Redis6.0后,创建3个线程,协助主线程处理写请求(默认,也可以通过修改配置,读请求用多线程IO)
//读请求使用io多线程
io-threads-do-reads yes
// io-threads N,表示启用 N-1 个 I/O 多线程(主线程也算一个 I/O 线程)
io-threads 4
- 单线程「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」一个线程(主线程)来完成
- Redis 启动时,会创建3个BIO线程「1,关闭文件(BIO_CLOSE_FILE)、2,AOF 刷盘(BIO_AOF_FSYNC)、3,释放内存(BIO_LAZY_FREE)」。
- Redis6.0后,创建3个线程,协助主线程处理写请求(默认,也可以通过修改配置,读请求用多线程IO)
//读请求使用io多线程
io-threads-do-reads yes
// io-threads N,表示启用 N-1 个 I/O 多线程(主线程也算一个 I/O 线程)
io-threads 4
redis支持的数据类型?
String 字符串
存储字符串、整数、浮点数。
应用场景:
1 缓存
如:热点数据缓存(如报表,热点新闻等),对象缓存,全页缓存。 可提升热点数据访问速度
2 数据共享分布式
String 类型, Redis 分布式独立服务,可多应用间共享 如:分布式 Session
3 分布式锁
String 类型 setnx 方法,不存时,才添加成功,返回 true
4 全局 ID
int 类型,INCRBY,原子性(incrby userid 1000)(分库分表场景,一次性拿一段)
5 计数器
INT 类型,INCR 方法。如:文章阅读量,微博点赞数,允许一定延迟,先写 Redis ,定时同步到 数据库
应用场景:
1 缓存
如:热点数据缓存(如报表,热点新闻等),对象缓存,全页缓存。 可提升热点数据访问速度
2 数据共享分布式
String 类型, Redis 分布式独立服务,可多应用间共享 如:分布式 Session
3 分布式锁
String 类型 setnx 方法,不存时,才添加成功,返回 true
4 全局 ID
int 类型,INCRBY,原子性(incrby userid 1000)(分库分表场景,一次性拿一段)
5 计数器
INT 类型,INCR 方法。如:文章阅读量,微博点赞数,允许一定延迟,先写 Redis ,定时同步到 数据库
Hash 哈希
键值对,无序散列表。value 是字符串,不能嵌套其他类型。
hset key name tx
应用场景:
1 存储对象类型数据
如对象或表的数据,比 String 节省 key 空间,便于集中管理
2 购物车
key:用户 id;field:商品 id;value:商品数量。
+1:hincr。-1:hdecr。删除:hdel。全选:hgetall。商品数:hlen。
hset key name tx
应用场景:
1 存储对象类型数据
如对象或表的数据,比 String 节省 key 空间,便于集中管理
2 购物车
key:用户 id;field:商品 id;value:商品数量。
+1:hincr。-1:hdecr。删除:hdel。全选:hgetall。商品数:hlen。
List 列表
存储有序字符串(从左到右),元素可重复。可充当队列和栈角色。
lpush queue a
应用场景:
1、 用户消息时间线 timeline
List 是有序,可用来做用户时间线
2、 消息队列
List 提供两个阻塞弹出:BLPOP/BRPOP,置超时时间。
BLPOP:BLPOP key1 timeout 移出,获取列表第一元素, 列表没有元素会阻塞列表,等待超时,发现可弹出元素为止。
BRPOP:BRPOP key1 timeout 移出,获取列表最后一元素, 没有元 素,会阻塞列表,直到等待超时,发现可弹出元素为止。
队列:先进先出:rpush blpop,左头右尾,右进队列,左出队列。
栈:先进后出:rpush brpop。
lpush queue a
应用场景:
1、 用户消息时间线 timeline
List 是有序,可用来做用户时间线
2、 消息队列
List 提供两个阻塞弹出:BLPOP/BRPOP,置超时时间。
BLPOP:BLPOP key1 timeout 移出,获取列表第一元素, 列表没有元素会阻塞列表,等待超时,发现可弹出元素为止。
BRPOP:BRPOP key1 timeout 移出,获取列表最后一元素, 没有元 素,会阻塞列表,直到等待超时,发现可弹出元素为止。
队列:先进先出:rpush blpop,左头右尾,右进队列,左出队列。
栈:先进后出:rpush brpop。
Set 集合
String 类型无序集合,最大存储数量 2^32-1(40 亿左右)。
sadd myset a b c d e f g
应用场景:
1 抽奖:随机获取元素
spop mykey
2 点赞、签到、打卡
这条微博的 ID 是 t1001,用户 ID 是 u3001。
用 like:t1001 来维护 t1001 这条微博的所有点赞用户。
点赞了这条微博:sadd like:t1001 u3001
取消点赞:srem like:t1001 u3001
是否点赞:sismember like:t1001 u3001
点赞的所有用户:smembers like:t1001
点赞数:scard like:t1001
比关系型数据库简单许多。
3 商品标签
用 tags:i5001 来维护商品所有的标签。
sadd tags:i5001 画面清晰细腻
sadd tags:i5001 真彩清晰显示屏
sadd tags:i5001 流畅至极
4 商品筛选
获取差集
sdiff set1 set2
获取交集(intersection )
sinter set1 set2
获取并集
sunion set1 set2
筛选商品,苹果的,iOS 的,屏幕在 60-624 之间的,屏幕材质是 LCD 屏幕
sinter brand:apple brand:ios screensize:60-624 screentype:lcd
5 用户关注、推荐模型
sadd myset a b c d e f g
应用场景:
1 抽奖:随机获取元素
spop mykey
2 点赞、签到、打卡
这条微博的 ID 是 t1001,用户 ID 是 u3001。
用 like:t1001 来维护 t1001 这条微博的所有点赞用户。
点赞了这条微博:sadd like:t1001 u3001
取消点赞:srem like:t1001 u3001
是否点赞:sismember like:t1001 u3001
点赞的所有用户:smembers like:t1001
点赞数:scard like:t1001
比关系型数据库简单许多。
3 商品标签
用 tags:i5001 来维护商品所有的标签。
sadd tags:i5001 画面清晰细腻
sadd tags:i5001 真彩清晰显示屏
sadd tags:i5001 流畅至极
4 商品筛选
获取差集
sdiff set1 set2
获取交集(intersection )
sinter set1 set2
获取并集
sunion set1 set2
筛选商品,苹果的,iOS 的,屏幕在 60-624 之间的,屏幕材质是 LCD 屏幕
sinter brand:apple brand:ios screensize:60-624 screentype:lcd
5 用户关注、推荐模型
ZSet 有序集合
子主题
sorted set,有序的 set,每个元素有个 score。score 相同时,按照 key 的 ASCII 码排序。
zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
应用场景:
1 排行榜
id 为 6001 新闻点击数加 1
zincrby hotNews:20190926 1 6001
今天点击最多的 15 条
zrevrange hotNews:20190926 0 15 withscores
zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python
应用场景:
1 排行榜
id 为 6001 新闻点击数加 1
zincrby hotNews:20190926 1 6001
今天点击最多的 15 条
zrevrange hotNews:20190926 0 15 withscores
redis线程模型
redis持久化
RDB(Redis DataBase)
RDB触发规则
自动触发
注意:满足任意,都会触发,使用bgsave。
手动触发
重启服务,迁移数据,手动触 RDB 快照保存:
a. save:save生成快照, 阻塞当前redis服务器,不能处理其它命令,生产环境不要使用该命令。
b. bgsave:bgsave执行fork操作,创建子进程来处理快照,不影响接收redis命令。
bgsave:fork() + CopyOnWrite
a. save:save生成快照, 阻塞当前redis服务器,不能处理其它命令,生产环境不要使用该命令。
b. bgsave:bgsave执行fork操作,创建子进程来处理快照,不影响接收redis命令。
bgsave:fork() + CopyOnWrite
bgsave流程
Redis父进程判断:当前是否执行save,bgsave/bgrewriteaof子进程,执行则bgsave直接返回。bgsave/bgrewriteaof(aof) 子进程不能同时执行,基于性能方面考虑:两个并发子进程同时执行大量磁盘写操作,可能引起严重性能问题。
父进程执行fork操作创建子进程,中父进程是阻塞的,Redis不能执行,客户端任何命令
父进程fork后,bgsave返回”Background saving started”并不再阻塞父进程,并可响应其他命令
子进程创建RDB文件,根据父进程内存快照, 生成临时快照文件,对原有文件进行原子替换
子进程发送信号给父进程, 完成父进程更新统计信息
父进程执行fork操作创建子进程,中父进程是阻塞的,Redis不能执行,客户端任何命令
父进程fork后,bgsave返回”Background saving started”并不再阻塞父进程,并可响应其他命令
子进程创建RDB文件,根据父进程内存快照, 生成临时快照文件,对原有文件进行原子替换
子进程发送信号给父进程, 完成父进程更新统计信息
fork()
fork()创建子进程,注意是子进程,不是子线程。fork()进程共享父类内存数据。仅是共享fork()出子进程那一刻的内存数据,后期主进程修改,对子进程不可见,同理,子进程修改数据,对主进程也不可见。如:A进程fork()子进程B,A进程为主进程,主进程,子进程,指向内存空间是同一个,他们的数据是一致的。A修改了内存上数据,B是看不到,A新增数据,删除数据,B都看不到。子进程B出问题,主进程A完全没影响,依然可对外提供服务,主进程挂了,子进程也跟随挂。像守护线程概念。Redis用fork()来完成RDB持久化操作
CopyOnWrite
主进程fork()子进程后,内核把主进程中内存页的权限设为read-only,子进程地址空间指向主进程。共享了主进程内存,其中某个进程写内存时(是主进程写,子进程rdb文件持久化工作,不参与客户端请求),CPU硬件检测到内存页read-only,触发页异常中断(page-fault),内核中断例程。内核会把触发的异常页复制一份(仅复制异常页,所修改数据页,不是内存中全部数据),主子进程各自持有独立同样的数据
AOF(Append Only File)
AOF文件格式
1、resp协议格式数据
2、* 后面数字,代表命令有多少参数
3、$ 号后面的数字,代表参数几个字符
4、带过期时间set命令,记录key过期时间戳
2、* 后面数字,代表命令有多少参数
3、$ 号后面的数字,代表参数几个字符
4、带过期时间set命令,记录key过期时间戳
开启策略
重写策略
bgRewriteAof流程
redis内存回收
过期策略
定时过期(主动)
-- 设key过期时间,同时,为该key创建定时器,让定时器在key过期时间来临前,对key进行删除
对内存友好、对cpu不友好(新增定时器,消耗cpu资源)
-- 设key过期时间,同时,为该key创建定时器,让定时器在key过期时间来临前,对key进行删除
对内存友好、对cpu不友好(新增定时器,消耗cpu资源)
惰性过期(被动)
-- key过期,不删除,通过key获取value,检查是否过期,过期,则删除,返回null。
对cpu友好、对内存不友好(无查询,长期占用内存资源);get,setnx时,先检查key是否过期,过期,则先删除,再操作
-- key过期,不删除,通过key获取value,检查是否过期,过期,则删除,返回null。
对cpu友好、对内存不友好(无查询,长期占用内存资源);get,setnx时,先检查key是否过期,过期,则先删除,再操作
定期过期
-- 每隔一段时间(默认100ms),扫描expires 字典(默认20)key,并清除其中已过期的 key
该策略是折中方案
-- 每隔一段时间(默认100ms),扫描expires 字典(默认20)key,并清除其中已过期的 key
该策略是折中方案
- 遍历每个数据库(默认16个)
- 随机获取当前库设置过期时间key,已过期,删除,默认循环20次
- 判断定期删除操作,是否已经达指定时长,已达到,直接退出
- 定期删除程序中全局变量,currentdb记录下个将要遍历的库
淘汰策略
LRU策略
建议使用All-keys Lru
建议使用All-keys Lru
LRU是Least Recently Used,最近很少用,可理解成最久没有用
1、链表满,把链表尾部数据丢弃掉,新加的缓存,直接加到链表头。
2、链表缓存被命中,直接把数据移到链头,原头节点缓存就向表尾移动。
1、链表满,把链表尾部数据丢弃掉,新加的缓存,直接加到链表头。
2、链表缓存被命中,直接把数据移到链头,原头节点缓存就向表尾移动。
LFU策略
随机策略
TTL策略
noeviction策略(默认)
Redis的LRU
1、Redis维护大小16候选池,第一次随机选取采用数据,把数据放到候选池中,候选池中数据根据key空闲进行排序。
2、第二次后取数据时,大于候选池内最小空闲时间key,放进候选池。
3、候选池数据满后,空闲时间最大key,就会被挤出候选池。执行淘汰时,从候选池,选取空闲时间最大key进行淘汰。
2、第二次后取数据时,大于候选池内最小空闲时间key,放进候选池。
3、候选池数据满后,空闲时间最大key,就会被挤出候选池。执行淘汰时,从候选池,选取空闲时间最大key进行淘汰。
redis高可用架构
主从架构
主从数据是如何同步的?
1、slave连上master后(stocket长连接),会发送一个PSYNC命令给master请求复制数据
2、master通过bgsave来生成rdb文件,并在开始生成文件的时候,将后面的写命令都缓存到buffer中
3、rdb文件生成好后,master将文件send到slave
4、slave收到rdb文件后,先清空自己的旧文件,再将新的rdb内容加载到内存中
5、master发送buffer,slave执行buffer中的写命令
6、master通过stocket长连接,将写命令持续发到slave中,保持数据一致
1、slave连上master后(stocket长连接),会发送一个PSYNC命令给master请求复制数据
2、master通过bgsave来生成rdb文件,并在开始生成文件的时候,将后面的写命令都缓存到buffer中
3、rdb文件生成好后,master将文件send到slave
4、slave收到rdb文件后,先清空自己的旧文件,再将新的rdb内容加载到内存中
5、master发送buffer,slave执行buffer中的写命令
6、master通过stocket长连接,将写命令持续发到slave中,保持数据一致
哨兵架构
服务下线判断
1、Sentinel 默认以每秒钟 1 次的频率向 Redis 服务节点发送 PING 命令
2、如果在down-after-milliseconds 内都没有收到有效回复,Sentinel 会将该服务器标记为下线(主观下线)
3、Sentinel 节点会继续询问其他的 Sentinel 节点,确认这个节点是否下线,如果多数 Sentinel 节点都认为 master 下线,
master 才真正确认被下线(客观下线),这个时候就需要重新选举 master。
1、Sentinel 默认以每秒钟 1 次的频率向 Redis 服务节点发送 PING 命令
2、如果在down-after-milliseconds 内都没有收到有效回复,Sentinel 会将该服务器标记为下线(主观下线)
3、Sentinel 节点会继续询问其他的 Sentinel 节点,确认这个节点是否下线,如果多数 Sentinel 节点都认为 master 下线,
master 才真正确认被下线(客观下线),这个时候就需要重新选举 master。
Sentinel 集群的 Leader 选举(Raft算法)
1、master 客观下线触发选举
2、每个sentinel每次选举都会自增配置纪元(选举周期),每个纪元中只会选择一个sentinel(先到先得)的leader
3、超过一半的sentinel选举某sentinel作为leader
注:sentinel是没有主从是分的,只是在客观下线后,有个leader选举,leader来处理故障转移
1、master 客观下线触发选举
2、每个sentinel每次选举都会自增配置纪元(选举周期),每个纪元中只会选择一个sentinel(先到先得)的leader
3、超过一半的sentinel选举某sentinel作为leader
注:sentinel是没有主从是分的,只是在客观下线后,有个leader选举,leader来处理故障转移
故障转移
1、选出 Sentinel Leader 之后,由 Sentinel Leader 从所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器
2、让其它从服务器改为从新的主服务器复制数据
3、将原主服务器也改为从新的主服务器复制数据
1、选出 Sentinel Leader 之后,由 Sentinel Leader 从所有从服务器里面,挑选出一个从服务器,并将其转换为主服务器
2、让其它从服务器改为从新的主服务器复制数据
3、将原主服务器也改为从新的主服务器复制数据
这么多从节点,选谁成为新的主节点?
1、如果与哨兵断开连接时长的比较久,超过了某个阈值,就直接失去了选举权
2、如果拥有选举权,那就看谁的优先级高(replica-priority 100),数值越小优先级越高
3、优先级相同,就看谁从 master 中复制的数据最多(复制偏移量最大),选最多的那个
4、复制数量也相同,就选择进程 id 最小的那个
1、如果与哨兵断开连接时长的比较久,超过了某个阈值,就直接失去了选举权
2、如果拥有选举权,那就看谁的优先级高(replica-priority 100),数值越小优先级越高
3、优先级相同,就看谁从 master 中复制的数据最多(复制偏移量最大),选最多的那个
4、复制数量也相同,就选择进程 id 最小的那个
集群架构
数据怎么相对均匀地分片?
1、hash取模:节点变化后,需要对所有数据进行重新分布
2、一致性hash:解决节点变化后,全部数据都要重新分布的问题。引入虚拟节点,解决数据分布不均匀问题
3、hash slot:对 key 用 CRC16 算法计算再%16384,得到一个 slot的值
注意:key 与 slot 的关系是永远不会变的,会变的只有 slot 和 Redis 节点的关系
1、hash取模:节点变化后,需要对所有数据进行重新分布
2、一致性hash:解决节点变化后,全部数据都要重新分布的问题。引入虚拟节点,解决数据分布不均匀问题
3、hash slot:对 key 用 CRC16 算法计算再%16384,得到一个 slot的值
注意:key 与 slot 的关系是永远不会变的,会变的只有 slot 和 Redis 节点的关系
怎么让相关的数据落到同一个节点上?
-- 在 key 里面加入{hash tag}即可。Redis 在计算槽编号的时候只会获取{}之间的字符串进行槽编号计算
如:127.0.0.1:7293> set a{qs}a 1
-- 在 key 里面加入{hash tag}即可。Redis 在计算槽编号的时候只会获取{}之间的字符串进行槽编号计算
如:127.0.0.1:7293> set a{qs}a 1
客户端连接到哪一台服务器?访问的数据不在当前节点上,怎么办?
-- 客户端操作的key不在当前连接的Redis服务器上,会返回一个MOVED,且含有Key所有服务器地址,重定向连接新服务器即可
如:127.0.0.1:7291> set qs 1
(error) MOVED 13724 127.0.0.1:7293
Jedis 等客户端会在本地维护一份 slot——node 的映射关系,大部分时候不需要重定向,所以叫做 smart jedis(需要客户端支持)。
-- 客户端操作的key不在当前连接的Redis服务器上,会返回一个MOVED,且含有Key所有服务器地址,重定向连接新服务器即可
如:127.0.0.1:7291> set qs 1
(error) MOVED 13724 127.0.0.1:7293
Jedis 等客户端会在本地维护一份 slot——node 的映射关系,大部分时候不需要重定向,所以叫做 smart jedis(需要客户端支持)。
新增或下线了 Master 节点,数据怎么迁移(重新分配)?
-- 需要把原有的 slot分配给新的节点负责,并且把相关的数据迁移过来
如:redis-cli --cluster add-node 127.0.0.1:7291 127.0.0.1:7297
redis-cli --cluster reshard 127.0.0.1:7291
-- 需要把原有的 slot分配给新的节点负责,并且把相关的数据迁移过来
如:redis-cli --cluster add-node 127.0.0.1:7291 127.0.0.1:7297
redis-cli --cluster reshard 127.0.0.1:7291
Redis集群选举原理分析
当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,
从而存在多个slave竞争成为master节点的过程,其过程如下:
1、slave发现自己的master变为FAIL
2、将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST信息
3、其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
4、尝试failover的slave收集master返回的FAILOVER_AUTH_ACK
5、slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)
6、slave广播Pong消息通知其他集群节点
当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,
从而存在多个slave竞争成为master节点的过程,其过程如下:
1、slave发现自己的master变为FAIL
2、将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST信息
3、其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
4、尝试failover的slave收集master返回的FAILOVER_AUTH_ACK
5、slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)
6、slave广播Pong消息通知其他集群节点
生产环境部署情况?
1、集群部署,3主3从,32G 内存+ 8 核 CPU + 1T 磁盘,redis只占用10g
2、用户数据,商品数据,库存数据
1、集群部署,3主3从,32G 内存+ 8 核 CPU + 1T 磁盘,redis只占用10g
2、用户数据,商品数据,库存数据
常见问题
为什么要用缓存?
-- 高性能(响应,存储,容错,稳定,如果访问多,变动小,可以放缓存)
-- 高并发(内存,支持高并发)
用了缓存之后会有什么不良后果?
-- 缓存雪崩、缓存穿透、缓存击穿、缓存与数据库双写不一致
-- 高性能(响应,存储,容错,稳定,如果访问多,变动小,可以放缓存)
-- 高并发(内存,支持高并发)
用了缓存之后会有什么不良后果?
-- 缓存雪崩、缓存穿透、缓存击穿、缓存与数据库双写不一致
Redis为什么会这么快?
1、纯内存操作
2、数据接入单线程(避免创建,销毁线程开销、避免上下文切换、避免多线程资源竞争,引发锁问题)
3、IO多路复用
注意:
-- 官方数据Redis 的 QPS 可以达到 10 万左右(每秒请求数)。
-- cpu不会成为redis瓶颈,redis瓶颈最有可能是机器内存,网络带宽。
1、纯内存操作
2、数据接入单线程(避免创建,销毁线程开销、避免上下文切换、避免多线程资源竞争,引发锁问题)
3、IO多路复用
注意:
-- 官方数据Redis 的 QPS 可以达到 10 万左右(每秒请求数)。
-- cpu不会成为redis瓶颈,redis瓶颈最有可能是机器内存,网络带宽。
如何保证缓存与数据库双写时的数据一致性?
-- 先更新数据库,再删除缓存,再配合删除缓存,重试机制(mq或者binlog)(短暂数据不一致)
1、先更新数据库,再更新缓存(多写高并发,无法保证顺序,存在数据库缓存数据不一致情况;存在浪费性能)
2、先删除缓存,再更新数据库(读写高并发时,存在数据库,缓存数据不一致)
3、先删除缓存,再更新数据库,最后,延时删除缓存(存在延长时间不好确认情况)
-- 先更新数据库,再删除缓存,再配合删除缓存,重试机制(mq或者binlog)(短暂数据不一致)
1、先更新数据库,再更新缓存(多写高并发,无法保证顺序,存在数据库缓存数据不一致情况;存在浪费性能)
2、先删除缓存,再更新数据库(读写高并发时,存在数据库,缓存数据不一致)
3、先删除缓存,再更新数据库,最后,延时删除缓存(存在延长时间不好确认情况)
什么是缓存穿透?
--缓存、数据库均不存在
解决方案是什么?
-- 缓存空对象,返回空对象,设过期时间(浪费内存,存储空对象,导致数据库与缓存数据不一致)
-- 布隆过滤器(无法删除元素,会影响key)
注:布隆过滤器,判断不存在,一定不存在,判断存在,不一定存在
--缓存、数据库均不存在
解决方案是什么?
-- 缓存空对象,返回空对象,设过期时间(浪费内存,存储空对象,导致数据库与缓存数据不一致)
-- 布隆过滤器(无法删除元素,会影响key)
注:布隆过滤器,判断不存在,一定不存在,判断存在,不一定存在
什么是缓存击穿?
-- 单一热点key,缓存数据失效
解决方案是什么?
-- 设key永不过期
-- 用分布式锁
查key,失效,对key进行加锁,只允许一个查询拿到锁,查询数据库(其它自旋等待),
结果查询出来存储到缓存,释放锁,其它查询,从缓存获取
-- 单一热点key,缓存数据失效
解决方案是什么?
-- 设key永不过期
-- 用分布式锁
查key,失效,对key进行加锁,只允许一个查询拿到锁,查询数据库(其它自旋等待),
结果查询出来存储到缓存,释放锁,其它查询,从缓存获取
什么是缓存雪崩?
-- 大量缓存数据同时失效
解决方案是什么?
--1, 设key永不过期
-- 2,将key过期时间打乱
-- 3,redis前,加本地缓存
-- 4,限流组件,限制每秒请求数,未通过请求走降级
-- 5,高可用集群
-- 大量缓存数据同时失效
解决方案是什么?
--1, 设key永不过期
-- 2,将key过期时间打乱
-- 3,redis前,加本地缓存
-- 4,限流组件,限制每秒请求数,未通过请求走降级
-- 5,高可用集群
monogdb
数据存储方式使用B树
ES(ElasticSearch)
概念
index(索引)
es 存储数据基本单位是索引,类似于表
es 存储数据基本单位是索引,类似于表
document(文档)
document就相当于表中一条记录
document就相当于表中一条记录
field(列)
field相当于表字段
field相当于表字段
mapping(映射)
表结构
表结构
shard(分片)
类似于kafka中partition分片,shard最好在同机房,减少网络带宽消耗
类似于kafka中partition分片,shard最好在同机房,减少网络带宽消耗
replica(副本)
类似于kafka中的follow副本
类似于kafka中的follow副本
primary(主副本)
类似于kafka中的leader副本
类似于kafka中的leader副本
node(节点)
类似于kafka中的节点服务器broker
类似于kafka中的节点服务器broker
master
类似于kafka中的leader,选举出来的,master选举采用Raft算法
类似于kafka中的leader,选举出来的,master选举采用Raft算法
中文分词器
IK分词器
HanL分词器
什么是倒排索引?倒排索引是如何查找到数据的?
-- 通过value 找 key
-- 通过value 找 key
es 写数据的流程?
1、写请求任意节点,节点成coordinating node(协调节点)
2、协调节点路由(hash取模),将请求转发primary shard的datanode(数据节点)
3、node 上 primary shard 请求,后将数据同步到 replica node(副本节点)
4、协调节点,发现 primary node , replica node 保存好后,返回响应结果,给客户端
1、写请求任意节点,节点成coordinating node(协调节点)
2、协调节点路由(hash取模),将请求转发primary shard的datanode(数据节点)
3、node 上 primary shard 请求,后将数据同步到 replica node(副本节点)
4、协调节点,发现 primary node , replica node 保存好后,返回响应结果,给客户端
es 读取数据的过程?
1、读请求任意节点,节点成coordinating node(协调节点)
2、协调节点对doc id 路由(hash),转到对应node(随机轮询算法,将请求转发primary shard或replica shard)
3、node 处理读请求,结果返协调节点
4、协调节点将数据返给客户端
注意:primary shard负责读写,replica shard负责读
1、读请求任意节点,节点成coordinating node(协调节点)
2、协调节点对doc id 路由(hash),转到对应node(随机轮询算法,将请求转发primary shard或replica shard)
3、node 处理读请求,结果返协调节点
4、协调节点将数据返给客户端
注意:primary shard负责读写,replica shard负责读
es 搜索数据过程?
1、请求任意节点,节点成coordinating node(协调节点)
2、协调节点广播到所有数据节点,节点分片处理请求
3、分片查询结果(文档ID,节点信息,分片信息)放队列,返给协调节点
4、协调节点将结果汇总,排序
5、根据文档ID,去各个分片拉取实际文档数据,返回给客户端
1、请求任意节点,节点成coordinating node(协调节点)
2、协调节点广播到所有数据节点,节点分片处理请求
3、分片查询结果(文档ID,节点信息,分片信息)放队列,返给协调节点
4、协调节点将结果汇总,排序
5、根据文档ID,去各个分片拉取实际文档数据,返回给客户端
es 写数据底层原理?
-- master选举采用Raft算法
-- master选举采用Raft算法
es 性能优化方案?
-- 加filesystem cache大小
-- 数据预热,热点数据预热filesystem cache中
-- 冷热分离,冷数据写一个索引,热数据写另一个索引,让热点数据保留filesystem cache中
-- 加filesystem cache大小
-- 数据预热,热点数据预热filesystem cache中
-- 冷热分离,冷数据写一个索引,热数据写另一个索引,让热点数据保留filesystem cache中
常见问题
Redis与MySQL双写一致性如何保证?
延时双删策略
1 先删除缓存
2 更新数据库
3 休眠一会(比如1秒),再删除缓存。
缺点: 休眠时间不确定,过短导致第二次缓存删除后,另一个读到旧数据,将旧数据写入到缓存中
2 更新数据库
3 休眠一会(比如1秒),再删除缓存。
缺点: 休眠时间不确定,过短导致第二次缓存删除后,另一个读到旧数据,将旧数据写入到缓存中
删除缓存重试机制
1 写,更新数据库
2 缓存删除失败
3 把删除失败key放到消息队列
4 消费消息队列消息,获取要删除的key
5 重试删除缓存操作
2 缓存删除失败
3 把删除失败key放到消息队列
4 消费消息队列消息,获取要删除的key
5 重试删除缓存操作
读取biglog异步删除缓存
1 读取缓存中是否有数据
2 缓存有相关数据value,则返回
3 缓存无相关数据,从数据库读取数据,放缓存,再返回
4 有更新数据,先更新数据库,再删缓存
5 为保证删成功,用binlog异步删除
6 主从数据库,binglog取从库
7 一主多从,从库采集binlog,消费端收到所有binlog数据,才删缓存,为简单,收一次更新log,删除一次缓存
Canal+RocketMQ同步MySQL到Redis/ES
2 缓存有相关数据value,则返回
3 缓存无相关数据,从数据库读取数据,放缓存,再返回
4 有更新数据,先更新数据库,再删缓存
5 为保证删成功,用binlog异步删除
6 主从数据库,binglog取从库
7 一主多从,从库采集binlog,消费端收到所有binlog数据,才删缓存,为简单,收一次更新log,删除一次缓存
Canal+RocketMQ同步MySQL到Redis/ES
为什么要用三级缓存架构?
Ngnix缓存
本地缓存
本地缓存只读、数据小、高频率访问数据。如秒杀商品数据。
优点:访问速度快
缺点:
1 占JVM空间,不能存储大量数据
2 本地缓存更新复杂度高
3 应用重启后,缓存数据丢失,缓存雪崩,数据库造成巨大压力
4 多应用节点,无法共享缓存数据
优点:访问速度快
缺点:
1 占JVM空间,不能存储大量数据
2 本地缓存更新复杂度高
3 应用重启后,缓存数据丢失,缓存雪崩,数据库造成巨大压力
4 多应用节点,无法共享缓存数据
Redis缓存
1 热点数据大量访问,对系统造成各种网络开销,影响系统性能
2 集中缓存雪崩,缓存被击穿,造成数据库的压力增大,造成数据库挂机状态,进而造成服务宕机
2 集中缓存雪崩,缓存被击穿,造成数据库的压力增大,造成数据库挂机状态,进而造成服务宕机
秒杀超卖 解决方案?
设计一个线程安全且带有过期时间的LRU缓存?
LRU,全称Least Recently Used,最近最少使用缓存。
-- ConcurrentHashMap 存储数据(K, V)
-- ConcurrentLinkedQueue 存储key,FIFO,尾插法,头近未使用的key,尾最新数据
-- ScheduledExecutorService 定时器实现到期清除key
注:HashMap + 链表 + 定时器
LRU,全称Least Recently Used,最近最少使用缓存。
-- ConcurrentHashMap 存储数据(K, V)
-- ConcurrentLinkedQueue 存储key,FIFO,尾插法,头近未使用的key,尾最新数据
-- ScheduledExecutorService 定时器实现到期清除key
注:HashMap + 链表 + 定时器
微服务
Dubbo
原理
核心组件
服务注册中心 (Registry)
服务提供者启动时会将自己的服务信息(如接口名、版本号、IP地址、端口号等)注册到注册中心。
服务消费者在启动时或需要时向注册中心订阅所需的服务,从注册中心获取可用的服务提供者列表。
服务消费者在启动时或需要时向注册中心订阅所需的服务,从注册中心获取可用的服务提供者列表。
服务提供者 (Provider)
提供者实现了服务接口并对外暴露服务,通过Dubbo框架将其服务注册到注册中心。
服务消费者 (Consumer)
消费者引用服务接口并通过注册中心发现和调用远程服务,Dubbo会根据配置创建代理对象,隐藏了网络通信和协议转换等细节。
监控中心 (Monitor, 可选)
监控中心用于收集统计信息,如服务调用次数、耗时等,便于运维人员对系统性能进行监控和优化。
配置管理
配置层允许开发者以集中化的方式管理服务的元数据信息,包括接口定义、服务版本、路由规则、负载均衡策略等。
运行机制
服务发布与订阅
提供者启动时将服务信息发布到注册中心,消费者则订阅相应的服务,注册中心负责通知消费者的变更情况。
服务调用
消费者通过本地代理对象发起远程服务调用,Dubbo基于指定的协议(如dubbo、rmi、http等)进行远程通讯。
负载均衡
在消费者端,Dubbo支持多种负载均衡策略,如随机、轮询、最少活跃调用数、一致性Hash等,确保请求能均匀地分发给多个服务提供者实例。
容错与失败恢复
如果服务提供者发生故障,Dubbo能够通过不同的容错模式(如快速失败、失败安全、失败自动恢复等)保证服务调用的健壮性。
扩展机制
Dubbo采用SPI(Service Provider Interface)扩展机制,使得开发者可以自定义过滤器、适配器、序列化方式等多种功能模块。
网络通信
传输协议
支持TCP、HTTP等不同网络传输协议。
数据编解码
支持Hessian2、JSON、Java原生序列化等序列化方式,实现跨语言调用的可能性。
集群与高可用
集群容错
通过集群层的支持,Dubbo能够处理多实例的路由和负载均衡,实现服务的高可用。
面试
架构组件
问题:请简述Dubbo的架构及各组件的功能。 答案:Dubbo包含服务提供者(Provider)、服务消费者(Consumer)、注册中心(Registry,如Zookeeper、Nacos等)、监控中心(Monitor)和容器(Container)。Provider发布服务,Consumer通过Registry订阅并调用服务;Monitor负责收集统计信息,进行运维监控;Container为服务运行提供环境支持。
服务注册与发现
问题:如何实现服务的注册与发现? 答案:服务提供者启动时,向注册中心注册服务信息,接口名、版本、地址等;服务消费者从注册中心订阅所需服务,并根据注册中心返回的信息,找到可用服务提供者。
服务调用过程
问题:描述一次Dubbo服务调用完整流程。 答案:Consumer通过代理对象,发起远程方法调用;根据负载均衡策略,选择一个Provider节点;用指定协议(如dubbo、rmi、http等)将请求发送给选定Provider;Provider收到请求后,执行业务逻辑并返回结果;Consumer接收到响应,反序列化得到返回值。
负载均衡策略
问题:列举几种Dubbo的负载均衡策略并解释其工作原理。 答案:Dubbo支持多种负载均衡策略,例如:
轮询(Round Robin):按顺序依次分配请求到提供者。
随机(Random):随机选择一个提供者进行调用。
最少活跃调用数(Least Active):优先分配给当前活跃调用数最少提供者。
一致性Hash(Consistent Hash):基于Key做一致性哈希,相同Key请求分发至同一提供者。
轮询(Round Robin):按顺序依次分配请求到提供者。
随机(Random):随机选择一个提供者进行调用。
最少活跃调用数(Least Active):优先分配给当前活跃调用数最少提供者。
一致性Hash(Consistent Hash):基于Key做一致性哈希,相同Key请求分发至同一提供者。
集群容错机制
问题:简述Dubbo提供的集群容错模式。 答案:Dubbo提供了多种容错模式,包括:
快速失败(Failfast):调用失败立即报错,用于非核心,且快速响应服务。
失败重试(Retry):出现异常时多次重试,直到成功或达最大重试次数。
失败安全(Failsafe):无论调用成功与否不抛出异常,用写入日志等操作。
并行调用(Forking):一次发起所有提供者调用,第一个返回成功,返回结果。
广播调用(Broadcast):向所有提供者,发起调用,不关心结果。
快速失败(Failfast):调用失败立即报错,用于非核心,且快速响应服务。
失败重试(Retry):出现异常时多次重试,直到成功或达最大重试次数。
失败安全(Failsafe):无论调用成功与否不抛出异常,用写入日志等操作。
并行调用(Forking):一次发起所有提供者调用,第一个返回成功,返回结果。
广播调用(Broadcast):向所有提供者,发起调用,不关心结果。
扩展机制
问题:Dubbo如何实现自定义扩展点? 答案:Dubbo用SPI(Service Provider Interface)机制,可自定义Filter、Invoker、Protocol等扩展点,只要实现对应接口,在META-INF/services目录下放置配置文件
配置方式
问题:Dubbo支持哪些配置方式? 答案:Dubbo支持XML和注解,也支持API编程式配置和服务治理规则,动态推送更新。
监控与运维
问题:Dubbo如何实现服务的监控? 答案:Dubbo内置监控统计功能,可统计服务调用次数、耗时等信息。可对接第三方监控系统,如Zipkin、Prometheus等,以实现更全面的监控和性能分析。
Spring Boot
基础
加载流程
1启动 Spring Boot 应用
通过调用 SpringApplication.run(Application.class, args) 启动应用。
2创建 SpringApplication 实例
初始化 SpringApplication 类实例,并设置其基础属性如主配置类等。
3初始化环境 (Environment)
加载系统属性和命令行参数,合并成应用的运行环境。
4加载外部配置
从多个来源读取配置,包括:application.properties 或 application.yml 文件、Profile特定的配置文件、命令行参数、系统属性等。
5 监听器与初始化器准备
注册并初始化 ApplicationContextInitializer 和 ApplicationListener 实现类。
6 资源加载与Bean定义
从META-INF/spring.factories中加载并注册所有的ApplicationContextInitializer实现。
执行所有自定义的初始化器来进一步定制上下文。
使用ClassPathScanningCandidateComponentProvider进行组件扫描,找出需要管理的bean定义。
执行所有自定义的初始化器来进一步定制上下文。
使用ClassPathScanningCandidateComponentProvider进行组件扫描,找出需要管理的bean定义。
7自动配置 (Auto-Configuration)
根据当前环境和已有的Bean定义情况,自动检测并配置合适的Spring Beans。
检查spring-boot-autoconfigure模块下的META-INF/spring.factories中的EnableAutoConfiguration条目,按需启用相应的自动配置类
检查spring-boot-autoconfigure模块下的META-INF/spring.factories中的EnableAutoConfiguration条目,按需启用相应的自动配置类
8创建并刷新 ApplicationContext
创建 ApplicationContext 的子类(通常是 AnnotationConfigServletWebServerApplicationContext),然后刷新上下文以完成Bean的创建和初始化过程。
在此过程中,会执行@PostConstruct注解的方法,以及初始化所有的单例Bean。
在此过程中,会执行@PostConstruct注解的方法,以及初始化所有的单例Bean。
9启动嵌入式服务器
如果是web应用,Spring Boot将根据配置自动启动嵌入式的Tomcat、Jetty或Undertow服务器。
10 启动完成后处理
调用ApplicationRunner和CommandLineRunner接口的实现类,这些类在应用完全启动后可以执行一些初始化逻辑。
组件
自动配置
Spring Boot的核心特性是其智能的自动配置机制。当Spring Boot启动时,会根据项目中引入的依赖库和类路径中的Bean定义,自动配置相关组件并初始化它们。这意味着无需大量的XML配置或注解,就可以快速搭建出一个具备基本功能的Spring应用。
启动依赖
Spring Boot提供了一系列预定义的起步依赖(Starters),这些依赖模块以group ID org.springframework.boot 开头,并且通常命名为 spring-boot-starter-xxx。通过引入合适的starter,可以一次性导入一组兼容且功能相关的依赖包,如数据库连接、Web服务、安全性等。
嵌入式容器
Spring Boot内嵌了Tomcat、Jetty或Undertow等Servlet容器,使得开发人员不再需要单独部署服务器环境,只需运行Spring Boot应用的主类即可启动一个独立运行的Web应用程序。
springBoot Cli
Spring Boot Command Line Interface (CLI) 提供了一种命令行工具,可以通过脚本编写快速创建和运行Spring Boot应用,特别适合快速原型开发。
Actuator端点
Spring Boot Actuator包含一系列生产就绪的监控和管理端点,可以用来查看应用信息、健康状况、审计日志、JVM指标等,对运维管理和故障排查极其有用。
简化配置
Spring Boot支持通过application.properties或application.yml文件进行集中化的配置管理,同时提供了属性绑定和外部化配置等功能,便于在不同环境中切换配置。
starter parent POM
Spring Boot提供了标准的父级POM(Maven Project Object Model),简化了Maven项目的构建配置,统一了版本管理和依赖管理。
主类(Spring Boot Application Class)
通过使用@SpringBootApplication注解标记的Java类作为应用的入口点,该注解包含了@Configuration、@EnableAutoConfiguration和@ComponentScan三个注解的功能,使得整个Spring Boot应用能够被正确地引导和自动配置。
RestTemplate和WebClient
Spring Boot为HTTP客户端请求提供了RestTemplate以及响应式编程模型下的WebClient,方便微服务之间的通信调用。
面试
Spring Cloud Alibaba
Nacos 服务注册与发现
服务注册
服务提供者(Nacos Client)Rest请求,向Nacos Server注册自己服务
服务提供者(Nacos Client)Rest请求,向Nacos Server注册自己服务
服务心跳
Nacos Client维护定时心跳(默认5秒)通知Nacos Server,更新Nacos Server服务实例时间为当前时间
临时节点适用,非临时节点不上报
Nacos Client维护定时心跳(默认5秒)通知Nacos Server,更新Nacos Server服务实例时间为当前时间
临时节点适用,非临时节点不上报
服务健康检查
临时节点(默认):Nacos Client每5秒上报健康,Nacos Server开启定时任务(5秒)检查注册服务实例健康情况,超过15秒未更新,就设成不健康状态(消费者看不到该实例),超30秒就剔除服务
非临时节点:Nacos Server主动探测(TCP),每20秒探测一次,探测不到,认为服务异常,不会剔除服务。
临时节点(默认):Nacos Client每5秒上报健康,Nacos Server开启定时任务(5秒)检查注册服务实例健康情况,超过15秒未更新,就设成不健康状态(消费者看不到该实例),超30秒就剔除服务
非临时节点:Nacos Server主动探测(TCP),每20秒探测一次,探测不到,认为服务异常,不会剔除服务。
服务发现
服务调用者(Nacos Client)调用服务时,发送REST请求Nacos Server,获取注册服务清单,缓存在Nacos Client本地,
在Nacos Client本地开启定时任务(默认30秒)拉取服务端最新注册表信息,更新到本地缓存。
Nacos Server发现服务异常,通过UDP方式,推送给服务调用者
服务调用者(Nacos Client)调用服务时,发送REST请求Nacos Server,获取注册服务清单,缓存在Nacos Client本地,
在Nacos Client本地开启定时任务(默认30秒)拉取服务端最新注册表信息,更新到本地缓存。
Nacos Server发现服务异常,通过UDP方式,推送给服务调用者
服务数据同步(一致性)
cp模式:采用raft协议进行同步
ap模式:进行异步批量同步,1,实例数达到1000条 2,当前时间跟上次同步大于2秒,进行同步
cp模式:采用raft协议进行同步
ap模式:进行异步批量同步,1,实例数达到1000条 2,当前时间跟上次同步大于2秒,进行同步
Nacos是AP架构还是CP架构?
AP架构(默认)
1、集群节点间,数据同步是异步同步
2、服务列表信息,不会持久化到本地,只会存在内存中
临时节点
1、集群节点间,数据同步是异步同步
2、服务列表信息,不会持久化到本地,只会存在内存中
临时节点
优点:1、就算其中一台机器宕机了,其他几台机器照样可以接收请求
缺点:1、机器正好注册,没同步,宕机,新注册服务,客户端还是调用不了
缺点:1、机器正好注册,没同步,宕机,新注册服务,客户端还是调用不了
CP架构
1、raft协议,保证集群节点间数据一致性,节点之间选举
2、数据会持久化到本地,防止数据丢失
非临时节点
1、raft协议,保证集群节点间数据一致性,节点之间选举
2、数据会持久化到本地,防止数据丢失
非临时节点
优点:
1、节点间数据一致,客户端获取数据都是一样
缺点:
1、一台机器宕机,集群会进行选举,选举过程,服务对外不可用
2、宕机半数以上,所有服务都不可用,无法产生master
3、机器是偶数台,防止脑裂,导致整个服务不可用
1、节点间数据一致,客户端获取数据都是一样
缺点:
1、一台机器宕机,集群会进行选举,选举过程,服务对外不可用
2、宕机半数以上,所有服务都不可用,无法产生master
3、机器是偶数台,防止脑裂,导致整个服务不可用
Nacos的AP和CP,针对注册中心来说,配置中心存储在mysql,不存AP和CP
Nacos的服务注册表结构是怎样的?
1、Nacos用双层Map存储
2、Map<nameSpaceId, Map<group::serverName, serviceInstance>>
1、Nacos用双层Map存储
2、Map<nameSpaceId, Map<group::serverName, serviceInstance>>
Nacos如何支撑阿里内部数十万服务注册压力?
Nacos接收到注册请求,不立即写数据,将服务注册任务放入阻塞队列,立即响应给客户端。
用线程池读取阻塞队列中任务,异步完成实例更新,提高并发写能力。
Nacos接收到注册请求,不立即写数据,将服务注册任务放入阻塞队列,立即响应给客户端。
用线程池读取阻塞队列中任务,异步完成实例更新,提高并发写能力。
Nacos如何避免并发读写冲突问题?
Nacos更新实例列表,采用CopyOnWrite技术,将旧实例列表拷贝一份,更新拷贝实例列表,覆盖旧实例列表。
1、复制旧注册表List实例数据,形成新注册表List
2、新增,修改,删除操作在新List完成
3、操作完成后,用新List覆盖旧List
Nacos更新实例列表,采用CopyOnWrite技术,将旧实例列表拷贝一份,更新拷贝实例列表,覆盖旧实例列表。
1、复制旧注册表List实例数据,形成新注册表List
2、新增,修改,删除操作在新List完成
3、操作完成后,用新List覆盖旧List
- 读写锁是遵循 写写互斥、读写互斥、读读不互斥 原则,
- 而copyOnWrite 则是写写互斥、读写不互斥、读读不互斥原则。
Nacos中的保护阈值的作用是什么?
1、保护阈值:健康实例数/总实例数,0-1间浮点数
2、Nacos返回健康实例。健康实例占比低时,请求让剩余健康实例处理,
导致压力过大,可能宕机,造成雪崩。保护阈值,为防止出现这种情况,设置的。
3、到保护阈值以下,Nacos返回所有实例给消费者(健康和不健康),部分请求
会请求到不健康实例上,牺牲部分请求,避免服务不可用
1、保护阈值:健康实例数/总实例数,0-1间浮点数
2、Nacos返回健康实例。健康实例占比低时,请求让剩余健康实例处理,
导致压力过大,可能宕机,造成雪崩。保护阈值,为防止出现这种情况,设置的。
3、到保护阈值以下,Nacos返回所有实例给消费者(健康和不健康),部分请求
会请求到不健康实例上,牺牲部分请求,避免服务不可用
Nacos与Eureka的区别有哪些?
- 接口方式:对外暴露Rest API,实现服务注册、发现等功能
- 实例类型:Nacos的实例有永久和临时实例;Eureka只支持临时实例
- 健康检测:Nacos对临时实例,用5秒检测定时上报心跳模式检测,永久实例,用TCP(20秒)主动请求来检测;Eureka 心跳模式
- 服务发现:Nacos支持 client 定时拉取 和 server UDP 订阅推送两种模式;Eureka定时拉取模式
相比Nacos1.X,Nacos2.X为什么这么快?解决了哪些问题?
Nacos1.X存在的问题:
1、心跳数量多,导致 TPS 高
2、心跳续约 ,感知服务变化,时延长
3、server UDP 推送不可靠,导致QPS 高(主动推送,配置中心)
4、HTTP 短连接模型,TIME_WAIT 状态连接多(客户端请求)
5、配置模块30 秒长轮询, 引起频繁 GC(配置中心实时感知变化)
Nacos2.X gRPC实现长连接,解决了上面问题, 底层NIO
Nacos1.X存在的问题:
1、心跳数量多,导致 TPS 高
2、心跳续约 ,感知服务变化,时延长
3、server UDP 推送不可靠,导致QPS 高(主动推送,配置中心)
4、HTTP 短连接模型,TIME_WAIT 状态连接多(客户端请求)
5、配置模块30 秒长轮询, 引起频繁 GC(配置中心实时感知变化)
Nacos2.X gRPC实现长连接,解决了上面问题, 底层NIO
Nacos 配置中心
1 eureka 2.0不开源。
2 nacos 注册实例数大于eureka
3 nacos用raft协议,nacos集群一致性大于eureka集群
4 nacos可动态变更配置项
Nacos Server 的配置数据是存在哪里呢?
单机:Derby 嵌入式的小型数据库derby
集群:Mysql
配置的隔离,在实际的应用中,存在着以下几种环境隔离的要求:
1、开发环境、测试环境、准生产环境和生产环境需要隔离
2、不同项目需要隔离
3、同一项目,不同的模块需要隔离
可以通过三种方式来进行配置隔离:Nacos服务器、namespace命名空间、group分组,在bootstrap.yml文件中可以通过配置Nacos的server-addr、namespace和group来区分不同的配置信息。
- Nacos的服务器 spring.cloud.nacos.config.server-addr
- Nacos的命名空间 spring.cloud.nacos.config.namespace,注意,命名空间ID不是名称
- Nacos的分组 spring.cloud.nacos.config.group
2 nacos 注册实例数大于eureka
3 nacos用raft协议,nacos集群一致性大于eureka集群
4 nacos可动态变更配置项
Nacos Server 的配置数据是存在哪里呢?
单机:Derby 嵌入式的小型数据库derby
集群:Mysql
配置的隔离,在实际的应用中,存在着以下几种环境隔离的要求:
1、开发环境、测试环境、准生产环境和生产环境需要隔离
2、不同项目需要隔离
3、同一项目,不同的模块需要隔离
可以通过三种方式来进行配置隔离:Nacos服务器、namespace命名空间、group分组,在bootstrap.yml文件中可以通过配置Nacos的server-addr、namespace和group来区分不同的配置信息。
- Nacos的服务器 spring.cloud.nacos.config.server-addr
- Nacos的命名空间 spring.cloud.nacos.config.namespace,注意,命名空间ID不是名称
- Nacos的分组 spring.cloud.nacos.config.group
LoadBalancer 客户端负载均衡器
Ribbon 客户端负载均衡
OpenFeign 声明式服务调用
Sentinel 限流降级熔断
限流算法
固定窗口算法
滑动窗口算法
漏桶算法
令牌桶算法
Sentinel 对比 Hystrix?
隔离策略
sentinel基于信号量隔离
Hystrix基于信号量、线程池
熔断降级策略
基于响应时间、失败比率
基于失败比例
实现方式
滑动窗口
滑动窗口(RxJava)
配置规则
支持多种数据源
支持多种数据源
扩展性
多个扩展点
插件形式
基于注解支持
支持
支持
限流
基于QOS,基于关系
不支持
支持模式
支持慢启动,匀速器模式
不支持
系统负载保护
支持
不支持
控制台
可配置规则,秒级监控,机器发现
不完善
框架适配
servlet,spring cloud dubbo ,grpc
servlet,spring cloud netflix
Seata 微服务分布式事务
Gateway 统一网关
原理
背景框架
Spring Cloud Gateway 是一个基于Spring框架和Project Reactor构建的微服务API网关。它作为Spring Cloud生态体系的一部分,承担着客户端请求路由、过滤以及与后端服务交互的核心作用
路由机制
Spring Cloud Gateway提供了一种声明式的路由配置方式,允许用户根据路径、HTTP方法、Header、Query参数等规则来定义路由规则。
每个路由都会关联一个或多个过滤器,这些过滤器会按照一定的顺序对经过该路由的请求进行处理
每个路由都会关联一个或多个过滤器,这些过滤器会按照一定的顺序对经过该路由的请求进行处理
过滤链接
过滤器是Gateway实现功能扩展的关键组件,包括预处理器(Pre-filter)、路由过滤器(Route Filter)、后处理器(Post-filter)等类型。
过滤器链是在请求到达网关时执行的一系列操作,如身份验证、限流、熔断、日志记录、请求转换等。内置了多种过滤器,同时也支持自定义过滤器以满足特定业务需求。
过滤器链是在请求到达网关时执行的一系列操作,如身份验证、限流、熔断、日志记录、请求转换等。内置了多种过滤器,同时也支持自定义过滤器以满足特定业务需求。
异步非阻塞IO
Spring Cloud Gateway基于Reactor项目实现了异步非阻塞的网络通信模型,能够有效提高系统在高并发场景下的性能表现。
动态路由和服务发现
Gateway可以通过集成服务注册中心(例如Eureka、Consul),自动获取并更新服务实例列表,从而实现动态路由和负载均衡。
当客户端请求到达时,Gateway通过匹配预先定义的路由规则找到目标服务,并转发至可用的服务实例。
当客户端请求到达时,Gateway通过匹配预先定义的路由规则找到目标服务,并转发至可用的服务实例。
请求处理流程
客户端发起请求到Spring Cloud Gateway。
Gateway首先根据路由配置找到匹配的路由及其对应的过滤器链。
然后依次执行过滤器链中的各个过滤器,每个过滤器都可以决定是否继续传递请求给下一个过滤器或者直接返回响应给客户端。
最终,经过所有过滤器处理后的请求被发送到下游服务,待收到响应后再经由过滤器链逆序执行后处理逻辑,并将最终响应返回给客户端。
Gateway首先根据路由配置找到匹配的路由及其对应的过滤器链。
然后依次执行过滤器链中的各个过滤器,每个过滤器都可以决定是否继续传递请求给下一个过滤器或者直接返回响应给客户端。
最终,经过所有过滤器处理后的请求被发送到下游服务,待收到响应后再经由过滤器链逆序执行后处理逻辑,并将最终响应返回给客户端。
安全性和监控
Spring Cloud Gateway可以整合OAuth2、JWT等认证机制,在网关层实现安全性控制。
同时,它可以与Zipkin、Sleuth等工具集成,实现全链路跟踪和分布式监控,方便问题定位和性能分析。
同时,它可以与Zipkin、Sleuth等工具集成,实现全链路跟踪和分布式监控,方便问题定位和性能分析。
子对比zuul
gateway网关
优点
性能优化
基于Spring 5和Project Reactor构建,支持异步非阻塞IO模型,提高了系统的吞吐量和响应速度
功能强大
提供了丰富的过滤器机制,用户可以根据需求自定义各种预处理、路由和后处理逻辑。内置了限流、熔断、重试等高级特性。
集成度高
与Spring Boot、Spring Cloud生态完美融合,配置更加简洁灵活,并且可以无缝对接如Eureka、Consul等服务发现组件。
高性能路由
利用Route Predicate Factory和Filter Factory的组合,能够实现更细粒度的路由控制
缺点
相对而言,学习曲线稍陡峭,对于不熟悉Reactor编程模型的开发者来说可能需要更多时间去适应
对于较早版本的Spring Cloud环境,Gateway可能是更新的技术,社区支持和相关文档在初期可能不如Zuul成熟。
zuul
优点
成熟稳定
作为Netflix开源的一部分,Zuul较早应用于生产环境,历经多代迭代,相对成熟稳定。
易于上手
基于Servlet API构建,对于大多数Java开发者来说较为亲切,学习成本相对较低。
预先封装的功能
提供了一些预定义的过滤器,比如身份验证、安全、监控等功能。
缺点
性能瓶颈
由于Zuul基于传统的同步阻塞I/O模型,因此在高并发场景下可能存在性能瓶颈。
扩展性限制
虽然也支持自定义过滤器,但在复杂路由策略和高级功能的支持上相比Gateway略显不足。
未来维护情况
随着Netflix停止对Zuul 2.x的开发,社区支持力度逐渐减弱,而Spring Cloud Gateway已成为Spring Cloud官方推荐的API网关解决方案。
Skywalking 链路追踪组件
Spring Security OAuth2 微服务安全
Spring Cloud
Eureka 服务注册与发现
服务注册
服务提供者(Eureka Client)HTTP请求,向Eureka Server注册自己服务
服务提供者(Eureka Client)HTTP请求,向Eureka Server注册自己服务
服务续约与剔除
Eureka Client默认每隔30秒心跳请求Eureka Server,Eureka Server90秒还未收到请求,该实例从注册表中剔除
Eureka Client默认每隔30秒心跳请求Eureka Server,Eureka Server90秒还未收到请求,该实例从注册表中剔除
服务自我保护机制
Eureka Server (默认15分钟)丢失(默认85%)实例连接时(如网络故障,频繁启动,关闭客户端)节点进入自我保护模式。
Eureka能够接受新服务注册和查询请求,不再删除,注册数据,不会同步到其他节点(高可用),网络稳定时,自动退出自我保护模式,新注册信息会被同步到其他节点(最终一致性)。
Eureka Server (默认15分钟)丢失(默认85%)实例连接时(如网络故障,频繁启动,关闭客户端)节点进入自我保护模式。
Eureka能够接受新服务注册和查询请求,不再删除,注册数据,不会同步到其他节点(高可用),网络稳定时,自动退出自我保护模式,新注册信息会被同步到其他节点(最终一致性)。
服务下线
Eureka Client正常,主动关闭时,向Eureka Server发下线请求,Eureka Server从注册表中剔除服务实例
做灰度发布,正常关闭服务的操作时,服务剔除,有一定延时时间,用服务下线
Eureka Client正常,主动关闭时,向Eureka Server发下线请求,Eureka Server从注册表中剔除服务实例
做灰度发布,正常关闭服务的操作时,服务剔除,有一定延时时间,用服务下线
服务发现
1、Eureka Client启动时从Eureka Server获取注册表信息,缓存在本地
2、Eureka Client每隔30秒去Eureka Server拉取最近注册表变化,保存在本地注册表中
1、Eureka Client启动时从Eureka Server获取注册表信息,缓存在本地
2、Eureka Client每隔30秒去Eureka Server拉取最近注册表变化,保存在本地注册表中
Eureka为什么能够抗住高并发?
一、发生在内存里面,维护注册表、拉取注册表、更新心跳时间
注册表,双层map结构:new ConcurrentHashMap(String, Map<String, Lease<InstanceInfo>)
1、ConcurrentHashMap的key是服务名称,如“inventory-service”
2、value:Map<String, Lease<InstanceInfo>代表一个服务多个服务实例
3、里层Map的key是服务实例id
4、InstanceInfo服务实例具体信息,机器ip地址、hostname及端口号
5、Lease,会维护每个服务,最近一次发送心跳时间
二、Eureka采用多级缓存机制,提升服务请求响应速度(具体见下)
一、发生在内存里面,维护注册表、拉取注册表、更新心跳时间
注册表,双层map结构:new ConcurrentHashMap(String, Map<String, Lease<InstanceInfo>)
1、ConcurrentHashMap的key是服务名称,如“inventory-service”
2、value:Map<String, Lease<InstanceInfo>代表一个服务多个服务实例
3、里层Map的key是服务实例id
4、InstanceInfo服务实例具体信息,机器ip地址、hostname及端口号
5、Lease,会维护每个服务,最近一次发送心跳时间
二、Eureka采用多级缓存机制,提升服务请求响应速度(具体见下)
Eureka是如何避免并发读写冲突的? -- 多级缓存
Eureka Server为了避免读写内存并发冲突,用多级缓存机制来进一步提升服务请求响应速度。
拉取注册表:
1、从ReadOnlyCacheMap查缓存注册表
2、无,从ReadWriteCacheMap里缓存注册表
3、无,从内存中获取实际注册表数据
注册表变更:
1、内存中更新变更注册表数据,过期掉ReadWriteCacheMap
2、默认30秒,各服务注册表,读ReadOnlyCacheMap
3、30秒后,Eureka Server后台线程发现ReadWriteCacheMap清空,清空ReadOnlyCacheMap缓存
4、服务拉取注册表,会从内存中获取最新的数据了,同时填充各个缓存
Eureka Server为了避免读写内存并发冲突,用多级缓存机制来进一步提升服务请求响应速度。
拉取注册表:
1、从ReadOnlyCacheMap查缓存注册表
2、无,从ReadWriteCacheMap里缓存注册表
3、无,从内存中获取实际注册表数据
注册表变更:
1、内存中更新变更注册表数据,过期掉ReadWriteCacheMap
2、默认30秒,各服务注册表,读ReadOnlyCacheMap
3、30秒后,Eureka Server后台线程发现ReadWriteCacheMap清空,清空ReadOnlyCacheMap缓存
4、服务拉取注册表,会从内存中获取最新的数据了,同时填充各个缓存
Eureka服务注册中心不保证数据一致性(保存最终一致性),属于AP,数据同步存在延时,全部对外提供服务
,让客户端必须定时间发送心跳到Eureka Server,属集中式心跳机制
集群中每Eureka实例是对等。每Eureka实例都包含全部服务注册表
,让客户端必须定时间发送心跳到Eureka Server,属集中式心跳机制
集群中每Eureka实例是对等。每Eureka实例都包含全部服务注册表
Eureka存在哪些缺点?
1、超大规模集群时,存在单机内存不足情况
2、超大规模集群时,服务注册中心无法保证数据一致性
3、Eureka Client每30秒Eureka Server发送一次心跳请求,Eureka Server压力太大
1、超大规模集群时,存在单机内存不足情况
2、超大规模集群时,服务注册中心无法保证数据一致性
3、Eureka Client每30秒Eureka Server发送一次心跳请求,Eureka Server压力太大
Eureka和ZooKeeper都可以提供服务注册与发现的功能,请说说两个的区别?
1、ZooKeeper CP,Eureka AP
2、ZooKeeper选举期间,注册服务瘫痪,服务最终会恢复,选举期间不可用
3、Eureka各节点平等关系,只要有一台Eureka可保证服务可用,查询到数据不是最新
4、ZooKeeper有Leader和Follower角色,Eureka各节点平等
5、ZooKeeper用过半数存活原则,Eureka采用自我保护机制,解决高可用问题
1、ZooKeeper CP,Eureka AP
2、ZooKeeper选举期间,注册服务瘫痪,服务最终会恢复,选举期间不可用
3、Eureka各节点平等关系,只要有一台Eureka可保证服务可用,查询到数据不是最新
4、ZooKeeper有Leader和Follower角色,Eureka各节点平等
5、ZooKeeper用过半数存活原则,Eureka采用自我保护机制,解决高可用问题
Eureka如何保证高可用与数据一致?
1、Eureka通过集群保证高可用,AP保证最终一致性
2、Eureka区分正常客户端,服务端发起数据同步请求,对于服务端发起数据同步请求,
Eureka不再进行其他同步操作,避免数据同步,出现死循环
3、Eureka用版本号(时间戳)来区分新旧数据
4、客户端定时拉取数据,会检测数据与本地数据冲突,会重新发起注册请求
1、Eureka通过集群保证高可用,AP保证最终一致性
2、Eureka区分正常客户端,服务端发起数据同步请求,对于服务端发起数据同步请求,
Eureka不再进行其他同步操作,避免数据同步,出现死循环
3、Eureka用版本号(时间戳)来区分新旧数据
4、客户端定时拉取数据,会检测数据与本地数据冲突,会重新发起注册请求
Consul服务注册与发现
Consul注册中心架构设计
1、每个服务的机器部署Consul Agent
2、在多台机器上部署Consul Server,核心服务注册中心,要求部署3~5台机器,保证高可用
3、Consul Agent收集服务信息,发给Consul Server,还会对服务请求,检查健康
4、Consul Agent转发给Consul Server,查询其他服务所在机器,方便直接调用服务
5、Consul Server集群,自动选举出一台机器作leader,对外提供服务,其他Consul Server是follower,leader自动同步数据给follower
1、每个服务的机器部署Consul Agent
2、在多台机器上部署Consul Server,核心服务注册中心,要求部署3~5台机器,保证高可用
3、Consul Agent收集服务信息,发给Consul Server,还会对服务请求,检查健康
4、Consul Agent转发给Consul Server,查询其他服务所在机器,方便直接调用服务
5、Consul Server集群,自动选举出一台机器作leader,对外提供服务,其他Consul Server是follower,leader自动同步数据给follower
Consul通过Raft协议实现数据一致性的?
1、Leader有最新数据,只有Leader对外提供服务
2、Leader收到注册信息后,将数据同步到大部分follower后才算成功
3、Leader服务宕机后,其它follower有最新数据,重新选举出Leader,对外提供服务
1、Leader有最新数据,只有Leader对外提供服务
2、Leader收到注册信息后,将数据同步到大部分follower后才算成功
3、Leader服务宕机后,其它follower有最新数据,重新选举出Leader,对外提供服务
Consul 是如何进行leader选举的?
1、Create Session:参与选举应用,创建Session,Session存活状态,关联到健康检查
2、Acquire KV:多应用Session去锁定一个KV,只能有一个锁定住,锁定成功应用,就是leader。
1、Create Session:参与选举应用,创建Session,Session存活状态,关联到健康检查
2、Acquire KV:多应用Session去锁定一个KV,只能有一个锁定住,锁定成功应用,就是leader。
Consul如何通过Agent实现分布式健康检查?
每机器上Consul Agent不断发送请求,检查服务健康,服务宕机,通知Consul Server。可大幅度减小Server端压力。
每机器上Consul Agent不断发送请求,检查服务健康,服务宕机,通知Consul Server。可大幅度减小Server端压力。
Ribbon 客户端负载均衡
Fegin 声明式服务调用
Hystrix 实现服务限流,降级,熔断
Zuul 统一网关详解,服务路由,过滤器使用
Config 分布式配置中心
Sleuth 分布式链路跟踪
相关工具
Docker
架构
命令
Docker run流程图
Docker底层原理
Docker 挂载
Docker 网络
Dockerfile
Docker Compose
Nginx
k8s
Jenkins
数据库
mysql
索引
数据结构
二叉数
红黑树
B树
B+树
hash表
Explain执行计划字段列
mysql索引的最佳实践?
1、全值匹配
2、最左前缀法则(联合索引)
3、索引列不做函数
4、尽量索引覆盖,减少 select * 语句
5、不使用!=,not in,is null , is not null等
6、字符串要加单引号
1、全值匹配
2、最左前缀法则(联合索引)
3、索引列不做函数
4、尽量索引覆盖,减少 select * 语句
5、不使用!=,not in,is null , is not null等
6、字符串要加单引号
mysql为什么采用B+树而不是B树?
1、B+树更少层高(单一节点存储更多元素),减少IO次数
2、叶子节点存储数据,非叶子节点不存储数据,使用缓存页,一次可以加载更多数据
3、叶子节点有序链表(mysql对B+树再次优化,双向有序链表),可高效地支持范围查找
1、B+树更少层高(单一节点存储更多元素),减少IO次数
2、叶子节点存储数据,非叶子节点不存储数据,使用缓存页,一次可以加载更多数据
3、叶子节点有序链表(mysql对B+树再次优化,双向有序链表),可高效地支持范围查找
mysql为什么不使用哈希索引、平衡二叉树索引、B树索引?
1、hash索引支持等值查询,不适用范围查询
2、平衡二叉树插入,更新操作,维护平衡代价太大,层级太高,IO次数多
3、B+树相对于B树更具有优势
1、hash索引支持等值查询,不适用范围查询
2、平衡二叉树插入,更新操作,维护平衡代价太大,层级太高,IO次数多
3、B+树相对于B树更具有优势
什么是聚簇索引、非聚簇索引、联合索引、索引覆盖、索引下推?
1、索引覆盖:在索引树中拿到结果,不回表查询聚簇索引情况(减少回表次数,减少IO)
2、索引下推:在使用联合索引,通过减少回表的次数,来提高数据库的查询效率
1、索引覆盖:在索引树中拿到结果,不回表查询聚簇索引情况(减少回表次数,减少IO)
2、索引下推:在使用联合索引,通过减少回表的次数,来提高数据库的查询效率
聚集索引非聚集索引比较
聚集索引表中只有一个,非聚集索引可有多个
聚集索引是物理上连续存在,非聚集索引逻辑上连续,并非物理上
聚集索引是索引组织形式,索引键值逻辑顺序决定表数据行的物理存储顺序,非聚集索引是普通索引,对数据列创建相应索引,不影响整表物理存储顺序
二叉树的数据结构来描述的,聚簇索引叶节点就是数据节点。非聚簇索引的叶节点仍然索引节点,有一个指针指向对应数据块
聚集索引非聚集索引优缺点
查询速度上来说,聚集优于非聚集
插入数据来说,非聚集要比聚集快
数据库锁
按操作的粒度分类
表锁(MYISAM与INNODB支持)
优点:表锁开销小,加锁快,无死锁,缺点:锁定粒度大,发生锁冲突概率最高,并发低
优点:表锁开销小,加锁快,无死锁,缺点:锁定粒度大,发生锁冲突概率最高,并发低
行锁(InnoDB默认)
优点:锁定粒度最小,发生锁冲突的概率最低,并发度也最高,缺点:行锁开销大,加锁慢;会死锁;
-- InnoDB是基于索引构建行锁
-- 索引未生效,查询条件未建立索引,行锁变表锁
优点:锁定粒度最小,发生锁冲突的概率最低,并发度也最高,缺点:行锁开销大,加锁慢;会死锁;
-- InnoDB是基于索引构建行锁
-- 索引未生效,查询条件未建立索引,行锁变表锁
页锁(BDB支持)
开销和加锁时间,界于表锁和行锁间:会死锁;锁定粒度界于表锁和行锁间,并发度一般
开销和加锁时间,界于表锁和行锁间:会死锁;锁定粒度界于表锁和行锁间,并发度一般
行锁的算法
Record Lock(记录锁)
锁住一条索引记录
例:10,20
锁住一条索引记录
例:10,20
Gap Lock(间隙锁) -- 解决幻读问题
注:生在事务隔离级别为RR(Repeatable Read)的情况下,RC才会有幻读的问题
不包含索引本身的开区间范围 (index1,index2)
例:(negative infinity, 10),(10, 20),(20, positive infinity)
注:生在事务隔离级别为RR(Repeatable Read)的情况下,RC才会有幻读的问题
不包含索引本身的开区间范围 (index1,index2)
例:(negative infinity, 10),(10, 20),(20, positive infinity)
Next-key Lock(临键锁)-- 解决当前读的幻读问题
InnoDB默认,Record Lock + Gap Lock
注:只发生在事务隔离级别为RR(Repeatable Read)的情况下,所以RC才会有幻读的问题
左开右闭 ,除了最后一个锁区间,是全开区间
例:(negative infinity, 10],(10, 20],(20, positive infinity)
InnoDB默认,Record Lock + Gap Lock
注:只发生在事务隔离级别为RR(Repeatable Read)的情况下,所以RC才会有幻读的问题
左开右闭 ,除了最后一个锁区间,是全开区间
例:(negative infinity, 10],(10, 20],(20, positive infinity)
唯一索引等值查询
- 当查询记录存在,临键锁退化成记录锁
select * from t_index where id = 16 for update;
加锁范围:(8, 16] -> 16
- 当查询的记录是不存在的,临键锁退化成间隙锁
select * from t_index whereid = 10 for update;
加锁范围:(8, 16] -> (8, 16)
- 当查询记录存在,临键锁退化成记录锁
select * from t_index where id = 16 for update;
加锁范围:(8, 16] -> 16
- 当查询的记录是不存在的,临键锁退化成间隙锁
select * from t_index whereid = 10 for update;
加锁范围:(8, 16] -> (8, 16)
唯一索引范围查询
select * from t_index where id >= 8 and id < 10 for update;
加锁范围:(4, 8] -> 8 && (8, 16)
select * from t_index where id >= 8 and id < 10 for update;
加锁范围:(4, 8] -> 8 && (8, 16)
非唯一索引等值查询
非唯一索引范围查询
按操作的类型分类
读锁(共享锁,Share Lock)
若事务T对数据对象A加上读锁,则事务T只能读A;其他事务只能再对A加读锁,不能加写锁,直到事务T释放A上的读锁
若事务T对数据对象A加上读锁,则事务T只能读A;其他事务只能再对A加读锁,不能加写锁,直到事务T释放A上的读锁
写锁(排它锁,Exclusive Lock)
若事务T对数据对象A加上写锁,只允许事务T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁
若事务T对数据对象A加上写锁,只允许事务T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁
按锁思想分类
悲观锁
-- 一种用来解决 读-写冲突 和 写-写冲突 的加锁并发控制(利用数据库锁机制)
-- 可以解决脏读,幻读,不可重复读,更新丢失的问题
-- 一种用来解决 读-写冲突 和 写-写冲突 的加锁并发控制(利用数据库锁机制)
-- 可以解决脏读,幻读,不可重复读,更新丢失的问题
乐观锁
-- 是一种用来解决 写-写冲突 无锁并发控制
-- 方式一:使用数据版本(version)实现
-- 方式二:使用时间戳(timestamp)实现
-- 无法解决脏读,幻读,不可重复读,可以解决更新丢失问题
-- 是一种用来解决 写-写冲突 无锁并发控制
-- 方式一:使用数据版本(version)实现
-- 方式二:使用时间戳(timestamp)实现
-- 无法解决脏读,幻读,不可重复读,可以解决更新丢失问题
MVCC(多版本并发控制)
-- 解决 读-写冲突 无锁并发控制
-- 可以解决脏读,幻读(RR级别),不可重复读事务问题,不可以解决更新丢失问题
-- 解决 读-写冲突 无锁并发控制
-- 可以解决脏读,幻读(RR级别),不可重复读事务问题,不可以解决更新丢失问题
MVCC + 悲观锁
MVCC解决读写冲突,悲观锁解决写写冲突
MVCC解决读写冲突,悲观锁解决写写冲突
MVCC + 乐观锁
MVCC解决读写冲突,乐观锁解决写写冲突
MVCC解决读写冲突,乐观锁解决写写冲突
MVCC
3个隐式字段(DB_ROW_ID,DB_TRX_ID,DB_ROLL_PTR)
undo日志版本链
Read View(一致性视图)
数据库事务
隔离级别
读取未提交(READ-UNCOMMITTED)
含义:允许读取尚未提交的数据变更
与锁关系:读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突
问题:脏读、幻读,不可重复读
含义:允许读取尚未提交的数据变更
与锁关系:读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突
问题:脏读、幻读,不可重复读
读取已提交(READ-COMMITTED)
含义:允许读取并发事务已经提交的数据
与锁关系:读操作需要加共享锁,但是在语句执行完以后释放共享锁
问题:幻读,不可重复读
含义:允许读取并发事务已经提交的数据
与锁关系:读操作需要加共享锁,但是在语句执行完以后释放共享锁
问题:幻读,不可重复读
可重复读(REPEATABLE-READ)
含义:对同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改
与锁关系:读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
问题:幻读
InnDB默认隔离级别,通过MVCC来实现可重复读,
在RR级别下,通过MVCC解决快照读的幻读问题,通过临间锁(next-key-lock)解决当前读的幻读问题
含义:对同⼀字段的多次读取结果都是⼀致的,除⾮数据是被本身事务⾃⼰所修改
与锁关系:读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。
问题:幻读
InnDB默认隔离级别,通过MVCC来实现可重复读,
在RR级别下,通过MVCC解决快照读的幻读问题,通过临间锁(next-key-lock)解决当前读的幻读问题
可串⾏化(SERIALIZABLE)
含义:所有的事务依次逐个执⾏,这样事务之间就完全不可能产⽣⼲扰
与锁关系:该级别锁定整个范围的键,并一直持有锁,直到事务完成
问题:性能差
含义:所有的事务依次逐个执⾏,这样事务之间就完全不可能产⽣⼲扰
与锁关系:该级别锁定整个范围的键,并一直持有锁,直到事务完成
问题:性能差
并发事务处理带来的问题
脏读(DirtyReads)
事务对数据进⾏修改未提交,另⼀个事务访问数据,还没有提交,另⼀个事务读到是“脏数据“
更新丢失(LostUpdate)或脏写
事务读取数据时,另⼀个事务也访问了该数据,第⼀个事务修改数据后,第⼆个事务修改数据。第⼀个事务内修改丢失,丢失修改。
不可重复读(Unrepeatableread)
事务内两次读到的数据,属性是不⼀样的情况,因此称为不可重复读。
幻读(Phantom read)
幻读与不可重复读类似。事务(T1)读取⼏⾏数据,事务(T2)插⼊⼀些数据。后查询中,事务(T1)发现不存在的记录,称为幻读。
注:不可重复读和幻读区别: 不可重复读重点修改多次读取⼀条记录,某些列的值被修改,幻读的重点在于新增或者删除,⽐如多次读取,发现记录增多减少
注:不可重复读和幻读区别: 不可重复读重点修改多次读取⼀条记录,某些列的值被修改,幻读的重点在于新增或者删除,⽐如多次读取,发现记录增多减少
mysql读写分离
mysql如何实现读写分离?
基于主从复制架构,主库,多个从库,只是写主库,
然后主库自动把数据给同步到从库上去。
基于主从复制架构,主库,多个从库,只是写主库,
然后主库自动把数据给同步到从库上去。
mysql 主从复制原理的是啥?
1、主库写表操作后,写入binlog日志文件(需要打开bin-log功能)
2、从库(IO线程)从主库拉取binlog日志(主库操作是IO线程-binlog dump线程完成的),将内容写relay log的末端,读到binlog文件名和位置记录master-info文件中,下一次读取时候,知道从哪里开始读取。主库返回信息,除日志内容外,有binlog文件名称和binlog位置。
3、从库SQL线程,检测到relay日志有新增后,将新增内容执行,写入从库中
1、主库写表操作后,写入binlog日志文件(需要打开bin-log功能)
2、从库(IO线程)从主库拉取binlog日志(主库操作是IO线程-binlog dump线程完成的),将内容写relay log的末端,读到binlog文件名和位置记录master-info文件中,下一次读取时候,知道从哪里开始读取。主库返回信息,除日志内容外,有binlog文件名称和binlog位置。
3、从库SQL线程,检测到relay日志有新增后,将新增内容执行,写入从库中
如何解决主从库同步延时问题?
1、半同步复制:主库必须要等到数据同步到从库(至少一个)relay log才算成功
2、并行复制:从库多SQL线程同时读取relay,写入从库中
3、分库:将主库分成多个库,减少单个库并发量
1、半同步复制:主库必须要等到数据同步到从库(至少一个)relay log才算成功
2、并行复制:从库多SQL线程同时读取relay,写入从库中
3、分库:将主库分成多个库,减少单个库并发量
分库分表
为什么要分库分表?
分库:减少数据库的并发压力和减少磁盘使用率
分表:是为了提高SQL执行效率
分库:减少数据库的并发压力和减少磁盘使用率
分表:是为了提高SQL执行效率
有哪些分库分表中间件?各自优缺点?
sharding-jdbc
优点:不用部署,运维成本低,不需要代理层二次转发,性能很高
缺点:需要各个系统都重新升级版本再发布,各系统需要耦合sharding-jdbc 依赖
中小公司使用
优点:不用部署,运维成本低,不需要代理层二次转发,性能很高
缺点:需要各个系统都重新升级版本再发布,各系统需要耦合sharding-jdbc 依赖
中小公司使用
mycat
优点:对于各个项目是透明的,升级之类,自己中间件那里搞就行了
缺点:需要部署,自己运维一套中间件,运维成本高
大公司使用
优点:对于各个项目是透明的,升级之类,自己中间件那里搞就行了
缺点:需要部署,自己运维一套中间件,运维成本高
大公司使用
如何对数据库进行分库分表?
垂直拆分
把一个有很多字段的表给拆分成多个表,或者是多个库上去。一般来说,访问频率高的字段放一张表,访问频率低的字段放一张表
把一个有很多字段的表给拆分成多个表,或者是多个库上去。一般来说,访问频率高的字段放一张表,访问频率低的字段放一张表
水平拆分
把表数据多个库多个表里去,每库的表结构都一样,每个库表放的数据是不同,所有库表数据加起来,就是全部数据
把表数据多个库多个表里去,每库的表结构都一样,每个库表放的数据是不同,所有库表数据加起来,就是全部数据
水平拆分 -- range
按照range 来分,每个库一段连续的数据,按时间范围来。
优点:扩容简单,只要预备好,给每个月都准备个库就可以,到一个新的月份,写新库了。
缺点:容易产生热点问题,大部分请求,访问最新的数据
按照range 来分,每个库一段连续的数据,按时间范围来。
优点:扩容简单,只要预备好,给每个月都准备个库就可以,到一个新的月份,写新库了。
缺点:容易产生热点问题,大部分请求,访问最新的数据
水平拆分 -- hash
按字段hash 散列,较为常用。
优点:分摊每个库的数据量和请求压力
缺点:扩容起来比较麻烦,会有一个数据迁移过程,之前数据重新计算hash 值,再分配不同的库或表
按字段hash 散列,较为常用。
优点:分摊每个库的数据量和请求压力
缺点:扩容起来比较麻烦,会有一个数据迁移过程,之前数据重新计算hash 值,再分配不同的库或表
如何将未分库分表的系统切换成分库分表的系统?
停机迁移方案
不建议使用在线上系统
双写迁移方案
1、写表地方,老库和新库(通过中间件)都写
2、同步历史数据,不允许覆盖新库的数据,除非旧库的数据比新库更新
3、程序切换成,分库分表最新代码
1、写表地方,老库和新库(通过中间件)都写
2、同步历史数据,不允许覆盖新库的数据,除非旧库的数据比新库更新
3、程序切换成,分库分表最新代码
分库分表如何动态扩容
分库分表的主键ID如何生成?
利用单台数据库自增主键
可适用:并发不高,数据量大
不适用:并发高,数据量大
可适用:并发不高,数据量大
不适用:并发高,数据量大
利用Redis生成ID
-- 使用Redis集群(如5台),每台步长一样,使用原子操作INCR来增加
优点:性能高,单机顺序递增
缺点:不能再扩展,5台已经够使用了
-- 使用Redis集群(如5台),每台步长一样,使用原子操作INCR来增加
优点:性能高,单机顺序递增
缺点:不能再扩展,5台已经够使用了
Twitter的snowflake算法
生成一个64位long型ID
-- 1 bit 恒为0,保证生成ID为正整数
-- 41 bit 作为日期毫秒数 - 41位的长度可用69年
-- 10 bit 作为机器编号 (5个bit是数据中心,5个bit的机器ID) - 10位的长度支持部署1024个节点
-- 12 bit 作为毫秒内序列号。12位的计数顺序号支持每个节点每毫秒4096ID序号
优点:分布式,性能高,单机递增
缺点:分布式环境,可能由于服务器时间不一致,导致生成ID不是全局递增;时钟回拨,服务器上时间突然倒退回时间
注:时钟回拨解决方案
1、回拨时间间隔小时,不生成 ID,循环等待到时间点到达
2、提前机器ID,序列号留出拓展位置0,出现时钟回拨时,拓展位置1,可保证生成ID唯一性。
生成一个64位long型ID
-- 1 bit 恒为0,保证生成ID为正整数
-- 41 bit 作为日期毫秒数 - 41位的长度可用69年
-- 10 bit 作为机器编号 (5个bit是数据中心,5个bit的机器ID) - 10位的长度支持部署1024个节点
-- 12 bit 作为毫秒内序列号。12位的计数顺序号支持每个节点每毫秒4096ID序号
优点:分布式,性能高,单机递增
缺点:分布式环境,可能由于服务器时间不一致,导致生成ID不是全局递增;时钟回拨,服务器上时间突然倒退回时间
注:时钟回拨解决方案
1、回拨时间间隔小时,不生成 ID,循环等待到时间点到达
2、提前机器ID,序列号留出拓展位置0,出现时钟回拨时,拓展位置1,可保证生成ID唯一性。
常见问题
MyISAM与InnoDB 的区别?
1 InnoDB支持事务,MyISAM不支持,InnoDB每条SQL都默认封装成事务,自动提交,会影响速度,多条SQL语言begin和commit间,组成个事务;
2 InnoDB支持外键,MyISAM不支持。对包含外键InnoDB表,转MYISAM会失败;
3 聚集索引 VS 非聚集索引
InnoDB是聚集索引,用B+作为索引结构,数据文件是和(主键)索引绑在一起的(表数据文件本身就是按B+Tree组织的一个索引结构),有主键,主键索引效率高。辅助索引需要两次查询,先查询到主键,再通过主键查询到数据。主键不应过大,主键大,其他索引都很大
4 InnoDB支持表、行(默认)级锁,MyISAM支持表级锁
5 InnoDB表必须有唯一索引(如主键)(自己找/生产一个隐藏列Row_id充当主键),Myisam可没有
2 InnoDB支持外键,MyISAM不支持。对包含外键InnoDB表,转MYISAM会失败;
3 聚集索引 VS 非聚集索引
InnoDB是聚集索引,用B+作为索引结构,数据文件是和(主键)索引绑在一起的(表数据文件本身就是按B+Tree组织的一个索引结构),有主键,主键索引效率高。辅助索引需要两次查询,先查询到主键,再通过主键查询到数据。主键不应过大,主键大,其他索引都很大
4 InnoDB支持表、行(默认)级锁,MyISAM支持表级锁
5 InnoDB表必须有唯一索引(如主键)(自己找/生产一个隐藏列Row_id充当主键),Myisam可没有
mysql的查询SQL执行过程?
1、发查询语句给服务器。
2、检查缓存存在该查询,存在,返回缓存中存在结果。不存在就进行下一步。
3、服务器进行SQL解析、语法检测和预处理,由优化器生成执行计划。
4、Mysql执行器执行计划,调用存储引擎接口进行查询。
5、服务器将查询结果返回客户端。
1、发查询语句给服务器。
2、检查缓存存在该查询,存在,返回缓存中存在结果。不存在就进行下一步。
3、服务器进行SQL解析、语法检测和预处理,由优化器生成执行计划。
4、Mysql执行器执行计划,调用存储引擎接口进行查询。
5、服务器将查询结果返回客户端。
InnoDB更新语句执行流程?
-- undo log:保证事务的一致性(事务回滚,MVCC),随机读写(说不定要回滚)
-- redo log:重做日志,保证事务持久性,顺序写
-- bin log:归档日志,顺序写
注:执行顺序 undoLog -> redoLog -> binLog -> redoLog
-- undo log:保证事务的一致性(事务回滚,MVCC),随机读写(说不定要回滚)
-- redo log:重做日志,保证事务持久性,顺序写
-- bin log:归档日志,顺序写
注:执行顺序 undoLog -> redoLog -> binLog -> redoLog
架构设计问题
秒杀系统设计
设计要点:
1 按钮及时设为不可用,禁止用户重复提交
2 用本地缓存,缓存一些商品信息,等变动不大数据
3 将库存信息,提前刷到Redis缓存,在Redis缓存,进行库存锁定,扣减操作
4 允许场景下,使用异步模式,等缓存都落库后,再回调返回结果
5 如果允许,增加类似答题校验,等验证措施,来减少并发
6 注意缓存雪崩,缓存击穿,缓存穿透等问题
7 根据库存数量,设置允许通过请求数量,比如扩大几倍。
其它手段:
1 业务隔离:把秒杀做成一种促销,需要报名报名,归纳,整合用户数据
2 系统隔离:秒杀模块,单独部署
3 数据隔离:启动单独Mysql支持
1 按钮及时设为不可用,禁止用户重复提交
2 用本地缓存,缓存一些商品信息,等变动不大数据
3 将库存信息,提前刷到Redis缓存,在Redis缓存,进行库存锁定,扣减操作
4 允许场景下,使用异步模式,等缓存都落库后,再回调返回结果
5 如果允许,增加类似答题校验,等验证措施,来减少并发
6 注意缓存雪崩,缓存击穿,缓存穿透等问题
7 根据库存数量,设置允许通过请求数量,比如扩大几倍。
其它手段:
1 业务隔离:把秒杀做成一种促销,需要报名报名,归纳,整合用户数据
2 系统隔离:秒杀模块,单独部署
3 数据隔离:启动单独Mysql支持
秒杀超卖,少卖,及解决方案
Lua + Redis + MQ
1、先将库存数据,预热到Redis
2、用Lua脚本,依靠redis批量操作命令行原子性的特性,做库存扣减(判断库存充足,扣减操作,属于原子操作),成功返回订单号
3、Lua脚本将信息发MQ(失败,重试,重试不成功,得持久化,再做补偿处理,保证订单系统,可以拿到相关信息)
4、订单系统MQ信息,创建订单
5、客户端,通过轮询订单,来获取订单数据
注:用户未支付,取消订单,要对Redis库存进行添加
Lua + Redis + MQ
1、先将库存数据,预热到Redis
2、用Lua脚本,依靠redis批量操作命令行原子性的特性,做库存扣减(判断库存充足,扣减操作,属于原子操作),成功返回订单号
3、Lua脚本将信息发MQ(失败,重试,重试不成功,得持久化,再做补偿处理,保证订单系统,可以拿到相关信息)
4、订单系统MQ信息,创建订单
5、客户端,通过轮询订单,来获取订单数据
注:用户未支付,取消订单,要对Redis库存进行添加
高并发的红包系统
1 接入层,垂直切分,根据红包ID,发红包、抢红包、拆红包、查详情等都部署在同台机器上处理。
2 请求进行排队,到数据库时是串行,不涉及抢锁问题
3 为防止队列太长,过载导致队列被降级,直接打到数据库,数据库前面加上个缓存,用CAS自增,控制并发,太高并发,直接返回失败。
4 红包冷热数据分离,按时间分表。
2 请求进行排队,到数据库时是串行,不涉及抢锁问题
3 为防止队列太长,过载导致队列被降级,直接打到数据库,数据库前面加上个缓存,用CAS自增,控制并发,太高并发,直接返回失败。
4 红包冷热数据分离,按时间分表。
分布式ID
1 预先分片,如十台机器,每台先分一千个ID,一号机从0开始,二号从1000开始等等。
2 类似雪花算法,每个机器有个id,然后基于时间算一个id,再加上一个递增id。如美团的方案。缺点时间戳不能回拨,回拨可能出现问题。
2 类似雪花算法,每个机器有个id,然后基于时间算一个id,再加上一个递增id。如美团的方案。缺点时间戳不能回拨,回拨可能出现问题。
分布式限流
1 固定窗口计数器:按照时间段划分窗口,有请求就+1,最为简单的算法,这个算法有时会让通过请求量,允许为限制的两倍。
2 滑动窗口计数器:通过将窗口再细分,并且按照时间“滑动”解决突破限制问题,时间区间的精度越高,算法所需的空间容量就越大。
3 漏桶:请求类似水滴,先放桶里,服务提供方,按照固定速率,从桶里面取出请求执行。缺陷,短时间内有大量的突发请求,服务器没有任何负载,每个请求,队列中等待一段时间,才能被响应。
4 令牌桶:往桶里面发放令牌,每个请求拿走一个令牌,有令牌的请求。多余的令牌会直接丢弃。令牌桶算法,将请求平均分布到时间区间,能够承受范围内的突发请求,较为广泛的一种限流算法。
2 滑动窗口计数器:通过将窗口再细分,并且按照时间“滑动”解决突破限制问题,时间区间的精度越高,算法所需的空间容量就越大。
3 漏桶:请求类似水滴,先放桶里,服务提供方,按照固定速率,从桶里面取出请求执行。缺陷,短时间内有大量的突发请求,服务器没有任何负载,每个请求,队列中等待一段时间,才能被响应。
4 令牌桶:往桶里面发放令牌,每个请求拿走一个令牌,有令牌的请求。多余的令牌会直接丢弃。令牌桶算法,将请求平均分布到时间区间,能够承受范围内的突发请求,较为广泛的一种限流算法。
基础
并发
锁
synchronize
Synchronization Order
JVM 同步规范:
监视器m解锁,对后续加锁同步
volatile变量v写入,后续线程,对v读同步
启动线程与线程操作同步。
属性写,默认值(0, false,null)对其线程同步。(创建对象前,对象属性默认值,程序启动时,默认值,初始化来创建)
T1 操作,T2 发现T1 结束( T2 通过 T1.isAlive() 或 T1.join() 来判断 T1 是否已终结)
T1 中断 T2,T1 中断,其他线程发现 T2 被中断,同步(抛出 InterruptedException 异常,调用 Thread.interrupted 或 Thread.isInterrupted )
监视器m解锁,对后续加锁同步
volatile变量v写入,后续线程,对v读同步
启动线程与线程操作同步。
属性写,默认值(0, false,null)对其线程同步。(创建对象前,对象属性默认值,程序启动时,默认值,初始化来创建)
T1 操作,T2 发现T1 结束( T2 通过 T1.isAlive() 或 T1.join() 来判断 T1 是否已终结)
T1 中断 T2,T1 中断,其他线程发现 T2 被中断,同步(抛出 InterruptedException 异常,调用 Thread.interrupted 或 Thread.isInterrupted )
Happens-before Order
两个操作用 happens-before 确定执行顺序,操作 x happens-before 操作 y (记作: hb(x,y)),前操作于后操作可见。
同线程操作 x ,操作 y, x 先于 y ,hb(x, y)
解释: 单线程代码,顺序执行
对象构造方法最后一行指令, happens-before 于 finalize() 第一行指令。
解释: 构造函数先于 finalize ()
操作 x ,操作 y 构成同步,记 hb(x, y)。
解释: 是 synchronization order 规则
hb(x, y) 和 hb(y, z),推断出 hb(x, z)
同线程操作 x ,操作 y, x 先于 y ,hb(x, y)
解释: 单线程代码,顺序执行
对象构造方法最后一行指令, happens-before 于 finalize() 第一行指令。
解释: 构造函数先于 finalize ()
操作 x ,操作 y 构成同步,记 hb(x, y)。
解释: 是 synchronization order 规则
hb(x, y) 和 hb(y, z),推断出 hb(x, z)
synchronized
线程 a 进 synchronized 前, synchronized 对共享变量操作,后续持同监视器锁(monitor)(读锁,共享锁)线程 b 可见。
synchronized 保障变量可见性:
线程获取监视器锁,才 synchronized 代码块,进入代码块,从主存中重新读取共享变量值,退出代码块时,线程写缓冲区中数据,刷到主内存。
Java对象可作为锁, synchronized 实现同步基础:
普通方法,锁实例对象(synchronized method)
静态方法,锁类 class 对象(synchronize static method, synchronized(Obj.class))
同步方法块,锁括号里面对象(synchronized(this), synchronized(obj))
synchronized 保障变量可见性:
线程获取监视器锁,才 synchronized 代码块,进入代码块,从主存中重新读取共享变量值,退出代码块时,线程写缓冲区中数据,刷到主内存。
Java对象可作为锁, synchronized 实现同步基础:
普通方法,锁实例对象(synchronized method)
静态方法,锁类 class 对象(synchronize static method, synchronized(Obj.class))
同步方法块,锁括号里面对象(synchronized(this), synchronized(obj))
原理
对象头
对象在堆中内存布局分: 对象头,实例数据,对齐填充
Hotspot虚拟机对象头:Mark Word、Klass Pointer。Klass Point对象指向其类元数据指针,Mark Word存储对象运行时标志数据,是实现轻量级锁和偏向锁的关键。
JVM 用2个字(jvm 字等于位数)存储对象头(对象是数组则会分配3个字,多1个字记录数组长度)
Mark Word在默认存储着对象HashCode、分代年龄、锁标记位以下是32位JVM的Mark Word存储结构:32 = 25(23+2 可存储对象hashCode)+4(分代年龄)+3(1(是否偏向锁(0:非,1:是))+2(01:无锁,00:轻量级锁,10:重量级锁,11:GC,01:偏向锁))
Hotspot虚拟机对象头:Mark Word、Klass Pointer。Klass Point对象指向其类元数据指针,Mark Word存储对象运行时标志数据,是实现轻量级锁和偏向锁的关键。
JVM 用2个字(jvm 字等于位数)存储对象头(对象是数组则会分配3个字,多1个字记录数组长度)
Mark Word在默认存储着对象HashCode、分代年龄、锁标记位以下是32位JVM的Mark Word存储结构:32 = 25(23+2 可存储对象hashCode)+4(分代年龄)+3(1(是否偏向锁(0:非,1:是))+2(01:无锁,00:轻量级锁,10:重量级锁,11:GC,01:偏向锁))
monitor
monitor 对象(监视器锁),是 MarkWord 重量级锁指向,每对象存在 monitor 与之关联, 当monitor 被某线程持有后,处于锁定状态。
monitor是由ObjectMonitor实现的,其主要数据结构如下: ObjectMonitor中有两个队列,_WaitSet 和 _EntryLis,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象).
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_owner = NULL; // 指向持有锁线程
_WaitSet = NULL; // 处于wait状态线程,会加入到_WaitSet
_EntryList = NULL ; // 处于等待锁block状态线程,会加入到列表
}
多线程访问一段同步代码,先会进 _EntryList 集合
线程获取到对象 monitor ,把monitor的owner设当前线程,monitor计数器count加1
线程调用 wait() ,将释放持有monitor,owner恢复为null,count减1, 线程进 WaitSet集合,等待被唤醒
当前线程执行完毕,释放monitor(锁)并复位变量的值,线程进入获取monitor(锁)
monitor是由ObjectMonitor实现的,其主要数据结构如下: ObjectMonitor中有两个队列,_WaitSet 和 _EntryLis,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象).
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_owner = NULL; // 指向持有锁线程
_WaitSet = NULL; // 处于wait状态线程,会加入到_WaitSet
_EntryList = NULL ; // 处于等待锁block状态线程,会加入到列表
}
多线程访问一段同步代码,先会进 _EntryList 集合
线程获取到对象 monitor ,把monitor的owner设当前线程,monitor计数器count加1
线程调用 wait() ,将释放持有monitor,owner恢复为null,count减1, 线程进 WaitSet集合,等待被唤醒
当前线程执行完毕,释放monitor(锁)并复位变量的值,线程进入获取monitor(锁)
底层
同步代码块
public void syncTask(){
//同步代码库
synchronized (this){
doSomething....
}
}
对同步代码块,实现是monitorenter和monitorexit指令,其中monitorenter指向同步代码块开始位置,monitorexit指明同步代码块结束位置。执行到moniterenter指令,获取monitor。
monitorenter // 进入同步方法
........ // 执行逻辑
monitorexit // 退出同步方法块
同步方法
public synchronized void syncTask(){
doSomething......
}
方法同步是隐式,通过字节码指令来控制,在方法调用和返回操作中。JVM从方法常量池的方法表结构(method_info Structure) 的 ACC_SYNCHRONIZED 访问标志,是否同步方法。当方法调用时,指令检查方法 ACC_SYNCHRONIZED 访问标志,设置了,去获取monitor
public void syncTask(){
//同步代码库
synchronized (this){
doSomething....
}
}
对同步代码块,实现是monitorenter和monitorexit指令,其中monitorenter指向同步代码块开始位置,monitorexit指明同步代码块结束位置。执行到moniterenter指令,获取monitor。
monitorenter // 进入同步方法
........ // 执行逻辑
monitorexit // 退出同步方法块
同步方法
public synchronized void syncTask(){
doSomething......
}
方法同步是隐式,通过字节码指令来控制,在方法调用和返回操作中。JVM从方法常量池的方法表结构(method_info Structure) 的 ACC_SYNCHRONIZED 访问标志,是否同步方法。当方法调用时,指令检查方法 ACC_SYNCHRONIZED 访问标志,设置了,去获取monitor
锁的分类
无锁
没有对资源锁定,所有的线程能访问,修改同个资源,只有一个线程修改成功。
无锁特点修改操作会在循环内进行,线程不断尝试修改共享资源。没有冲突修改成功退出,否则循环尝试。多线程修改同个值,必定有个线程修改成功,而其修改失败线程不断重试,直到修改成功。
CAS原理,应用是无锁的实现
无锁特点修改操作会在循环内进行,线程不断尝试修改共享资源。没有冲突修改成功退出,否则循环尝试。多线程修改同个值,必定有个线程修改成功,而其修改失败线程不断重试,直到修改成功。
CAS原理,应用是无锁的实现
偏向锁
大多数情况,锁总是同线程多次获得,不存在多线程竞争,出现偏向锁。在一个线程执行同步代码块,提高性能 。
线程访问同步代码块,获取锁时,Mark Word存储锁偏向线程ID。线程进入和退出同步块,不通过CAS加锁和解锁,检测Mark Word存储指向当前线程的偏向锁。偏向锁在无多线程竞争,减少不必要轻量级锁执行,轻量级锁获取,释放依赖多次CAS原子指令,偏向锁只要在置换ThreadID时,依赖一次CAS原子指令
遇到其他线程尝试竞争偏向锁时,持有偏向锁线程才会释放锁,偏向锁撤销,需等待全局安全点(时间点上没有字节码正在执行),先暂停拥有偏向锁线程,判断锁对象,处于被锁定状态。撤销偏向锁,恢复到无锁(标志位为“01”)轻量级锁(标志位为“00”)的状态。
线程访问同步代码块,获取锁时,Mark Word存储锁偏向线程ID。线程进入和退出同步块,不通过CAS加锁和解锁,检测Mark Word存储指向当前线程的偏向锁。偏向锁在无多线程竞争,减少不必要轻量级锁执行,轻量级锁获取,释放依赖多次CAS原子指令,偏向锁只要在置换ThreadID时,依赖一次CAS原子指令
遇到其他线程尝试竞争偏向锁时,持有偏向锁线程才会释放锁,偏向锁撤销,需等待全局安全点(时间点上没有字节码正在执行),先暂停拥有偏向锁线程,判断锁对象,处于被锁定状态。撤销偏向锁,恢复到无锁(标志位为“01”)轻量级锁(标志位为“00”)的状态。
轻量级锁
偏向锁,被另外线程所访问,偏向锁升级为轻量级锁,其他线程自旋的形式尝试获取锁,不会阻塞,提高性能。
进同步块时,同步对象锁状态无锁状态(锁标志位“01”状态,是否为偏向锁“0”),虚拟机将线程栈帧建立一个锁记录(Lock Record)空间,存储锁对象Mark Word拷贝,拷贝对象头Mark Word到锁记录。
拷贝后,虚拟机用CAS操作,尝试将对象Mark Word更新指向Lock Record指针,将Lock Record里的owner指针,指向对象Mark Word。
更新动作成功,线程就拥有对象的锁,对象Mark Word的锁标志设“00”,对象处于轻量级锁定状态。
更新操作失败,虚拟机先检查对象Mark Word指向当前线程栈帧,是,线程已有对象的锁,可进同步块继续执行,否则,多个线程竞争锁。
当只有一个等待线程,自旋进行等待。自旋超过一定的次数,一个线程持有锁,一个自旋,第三个来时,轻量级锁升级重量级锁。
进同步块时,同步对象锁状态无锁状态(锁标志位“01”状态,是否为偏向锁“0”),虚拟机将线程栈帧建立一个锁记录(Lock Record)空间,存储锁对象Mark Word拷贝,拷贝对象头Mark Word到锁记录。
拷贝后,虚拟机用CAS操作,尝试将对象Mark Word更新指向Lock Record指针,将Lock Record里的owner指针,指向对象Mark Word。
更新动作成功,线程就拥有对象的锁,对象Mark Word的锁标志设“00”,对象处于轻量级锁定状态。
更新操作失败,虚拟机先检查对象Mark Word指向当前线程栈帧,是,线程已有对象的锁,可进同步块继续执行,否则,多个线程竞争锁。
当只有一个等待线程,自旋进行等待。自旋超过一定的次数,一个线程持有锁,一个自旋,第三个来时,轻量级锁升级重量级锁。
重量级锁
升级为重量级锁,锁标志的状态值“10”,Mark Word存储指向重量级锁指针,等待锁的线程,进入阻塞状态。
锁升级
偏向锁: 只有一个线程进入临界区
轻量级锁: 多个线程交替进入临界区
重量级锁: 多个线程同时进入临界区
理解思路
情况一:只有Thread1会进入临界区;
情况二:Thread#1和Thread#2交替进入临界区;
情况三:Thread#1和Thread#2同时进入临界区。
Thread#1进临界区,JVM将lockObject对象头Mark Word锁标志设为“01”,用CAS把Thread#1线程ID记录Mark Word中,偏向模式。所谓“偏向”,锁会偏向Thread#1,没有线程进入临界区,Thread#1出入临界区,无需同步操作
Thread#2尝试进入临界区。Thread#2尝试进入,Thread#1已退出临界区,lockObject处未锁定状态,偏向锁上发生竞争(对应情况二),会撤销偏向,Mark Word中不再存放偏向线程ID,而是存放hashCode和GC分代年龄,锁标识位变为“01”(表示未锁定),Thread#2获取lockObject轻量级锁。Thread#1和Thread#2交替进临界区,偏向锁无法满足需求,需要膨胀到轻量级锁。
一直是Thread#1和Thread#2交替进临界区,没有问题,轻量锁hold住。轻量级锁上发生竞争,出现“Thread#1和Thread#2同时进入临界区”,轻量级锁hold不住。 (根本原因轻量级锁没有足够空间存储,若不膨胀为重量级锁,所有等待轻量锁的线程,只能自旋,可能会损失很多CPU时间)
情况一:只有Thread1会进入临界区;
情况二:Thread#1和Thread#2交替进入临界区;
情况三:Thread#1和Thread#2同时进入临界区。
Thread#1进临界区,JVM将lockObject对象头Mark Word锁标志设为“01”,用CAS把Thread#1线程ID记录Mark Word中,偏向模式。所谓“偏向”,锁会偏向Thread#1,没有线程进入临界区,Thread#1出入临界区,无需同步操作
Thread#2尝试进入临界区。Thread#2尝试进入,Thread#1已退出临界区,lockObject处未锁定状态,偏向锁上发生竞争(对应情况二),会撤销偏向,Mark Word中不再存放偏向线程ID,而是存放hashCode和GC分代年龄,锁标识位变为“01”(表示未锁定),Thread#2获取lockObject轻量级锁。Thread#1和Thread#2交替进临界区,偏向锁无法满足需求,需要膨胀到轻量级锁。
一直是Thread#1和Thread#2交替进临界区,没有问题,轻量锁hold住。轻量级锁上发生竞争,出现“Thread#1和Thread#2同时进入临界区”,轻量级锁hold不住。 (根本原因轻量级锁没有足够空间存储,若不膨胀为重量级锁,所有等待轻量锁的线程,只能自旋,可能会损失很多CPU时间)
轻量级锁升级为重量级锁原因
自旋失败:当多个线程尝试获取同一个已被轻量级锁定的对象时,除了持有锁的线程外,其他线程会首先通过自旋CAS操作尝试获取锁。如果自旋多次后(即自旋循环达到一定次数),依然无法获得锁,那么表明当前存在较高的锁竞争。在这种情况下,为了防止过多CPU资源被无效地消耗在自旋上,JVM会选择将轻量级锁升级为重量级锁,这样等待锁的线程就会进入阻塞状态,并由操作系统进行调度
锁竞争加剧:当系统检测到有多个线程频繁争夺同一把轻量级锁时,意味着出现了较为激烈的锁竞争状况,继续维持轻量级锁可能导致性能恶化。因此,为了避免这种过度的线程上下文切换和CPU利用率过低的问题,JVM会将轻量级锁升级为重量级锁,让后续线程排队等候,从而保证更公平的锁分配机制。
锁膨胀条件满足:在特定条件下,例如锁从栈上逸出(即将锁对象作为方法返回值或者保存到其他不受控制的地方),使得轻量级锁的安全性模型失效,这时也会触发锁升级过程。
锁优化
锁消除
锁粗化
适用锁
不能保证重排序
以单例模式的 double check 检查问题, 看一下如下单例的写法:
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // 1. 第一次检查
synchronized (Singleton.class) { // 2
if (instance == null) { // 3. 第二次检查
instance = new Singleton(); // 4
}
}
}
return instance;
}
}
复制
这种写法是有问题的, 假设有两个线程 a 和 b 调用 getInstance() 方法
假设 a 先运行, 首先到步骤 4 处。instance = new Singleton() 这句代码首先会申请一段空间,然后将各个属性初始化为零值(0/null),执行构造方法中的属性赋值[1],将这个对象的引用赋值给 instance[2]。在这个过程中,[1] 和 [2] 可能会发生重排序。
此时,线程 b 刚刚进来执行到步骤 1,就有可能会看到 instance 不为 null,然后线程 b 也就不会等待监视器锁,而是直接返回 instance。问题是这个 instance 可能还没执行完构造方法(线程 a 此时还在 4 这一步),所以线程 b 拿到的 instance 是不完整的,它里面的属性值可能是初始化的零值(0/false/null),而不是线程 a 在构造方法中指定的值。
想解决这个问题, 使用 volatile 关键字修饰 instance 即可
另外: 如果对象所有的属性都被 final 关键字修饰, 那么不需要加 volatile 也可以保证不发生重排序。
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) { // 1. 第一次检查
synchronized (Singleton.class) { // 2
if (instance == null) { // 3. 第二次检查
instance = new Singleton(); // 4
}
}
}
return instance;
}
}
复制
这种写法是有问题的, 假设有两个线程 a 和 b 调用 getInstance() 方法
假设 a 先运行, 首先到步骤 4 处。instance = new Singleton() 这句代码首先会申请一段空间,然后将各个属性初始化为零值(0/null),执行构造方法中的属性赋值[1],将这个对象的引用赋值给 instance[2]。在这个过程中,[1] 和 [2] 可能会发生重排序。
此时,线程 b 刚刚进来执行到步骤 1,就有可能会看到 instance 不为 null,然后线程 b 也就不会等待监视器锁,而是直接返回 instance。问题是这个 instance 可能还没执行完构造方法(线程 a 此时还在 4 这一步),所以线程 b 拿到的 instance 是不完整的,它里面的属性值可能是初始化的零值(0/false/null),而不是线程 a 在构造方法中指定的值。
想解决这个问题, 使用 volatile 关键字修饰 instance 即可
另外: 如果对象所有的属性都被 final 关键字修饰, 那么不需要加 volatile 也可以保证不发生重排序。
lock
javaLock 方法
lock():获取锁并阻塞等待
unlock():释放锁
tryLock():尝试非阻塞地获取锁
tryLock(long time, TimeUnit unit):限时等待获取锁
lockInterruptibly():可中断的锁请求
unlock():释放锁
tryLock():尝试非阻塞地获取锁
tryLock(long time, TimeUnit unit):限时等待获取锁
lockInterruptibly():可中断的锁请求
ReentrantLock
ReentrantLock 基本用法
可重入性原理
公平锁与非公平锁的区别和设置
Condition 对象的使用
await() 方法
signal() 和 signalAll() 方法
可重入性原理
公平锁与非公平锁的区别和设置
Condition 对象的使用
await() 方法
signal() 和 signalAll() 方法
读写锁 - ReentrantReadWriteLock
读写锁的基本原理
ReadLock(读锁)和 WriteLock(写锁)
提高并发性能的场景分析
ReadLock(读锁)和 WriteLock(写锁)
提高并发性能的场景分析
Lock 实例与最佳实践
正确锁定和解锁的顺序
避免死锁策略
锁定模式的选择与应用
使用 try-with-resources 结构自动释放锁资源
避免死锁策略
锁定模式的选择与应用
使用 try-with-resources 结构自动释放锁资源
AQS (AbstractQueuedSynchronizer) 框架
原理
状态管理
用 volatile 关键字整型state,表示同步状态。独占模式,值代表当前持有锁线程数( ReentrantLock 重入次数),共享模式下,可同时获取到锁线程数。
线程尝试获取或释放锁,通过CAS修改状态变量。
线程尝试获取或释放锁,通过CAS修改状态变量。
CLH 队列
AQS 维护非阻塞同步双端队列CLH(Craig, Landin, and Hagersten)自旋锁改进来。试图获取锁失败线程,包成Node节点,加入到该队列。
FIFO原则,新节点插到队尾,头节点被授予锁的线程。
FIFO原则,新节点插到队尾,头节点被授予锁的线程。
同步过程
独占模式:ReentrantLock,线程tryAcquire()尝试获取锁时,成功state+1返回true,否则,线程构造成Node,加入队列,进入park()阻塞状态。锁线程释放锁(tryRelease()state-1),后续节点可能成头节点,会唤醒后继节点再次尝试。
共享模式:Semaphore或CountDownLatch,多线程可同时获到“锁”,state值可被多线程累加,递减。
阻塞与唤醒机制
用Unsafe类的park()和unpark()实现线程阻塞和唤醒,提供底层操作系统级别线程挂起和恢复执行能力。
锁降级与条件队列
AQS上构建工具类,如ReentrantLock还支持锁降级,Condition对象,满足特定条件时,才继续执行,有各自等待队列,依赖AQS核心逻辑
AQS 核心数据结构
Node 类:线程节点结构详解
双向链表(CLH 队列)模型
内部状态管理,原子操作类(如 getState()、compareAndSetState())
双向链表(CLH 队列)模型
内部状态管理,原子操作类(如 getState()、compareAndSetState())
基础方法与机制
独占模式(Exclusive Mode)
acquire(int) 方法实现
tryAcquire(int) 抽象方法定义线程阻塞与唤醒逻辑
共享模式(Shared Mode)
acquireShared(int) 与 releaseShared(int)
tryAcquireShared(int) 和 tryReleaseShared(int)
acquire(int) 方法实现
tryAcquire(int) 抽象方法定义线程阻塞与唤醒逻辑
共享模式(Shared Mode)
acquireShared(int) 与 releaseShared(int)
tryAcquireShared(int) 和 tryReleaseShared(int)
AQS 的公平与非公平策略
公平锁的获取与释放流程
非公平锁的特点与性能比较
非公平锁的特点与性能比较
基于 AQS 的并发工具类
ReentrantLock 实现分析
ReentrantReadWriteLock 读写锁原理
Semaphore 信号量机制
CountDownLatch 倒计数器
CyclicBarrier 循环栅栏
ReentrantReadWriteLock 读写锁原理
Semaphore 信号量机制
CountDownLatch 倒计数器
CyclicBarrier 循环栅栏
自定义同步组件
继承 AQS 创建自定义同步器
定义和重写关键方法,实现不同同步逻辑
定义和重写关键方法,实现不同同步逻辑
AQS 的高级特性
自旋锁与适应性自旋
锁的升级与降级(例如 ReentrantLock)
超时与中断支持
锁的升级与降级(例如 ReentrantLock)
超时与中断支持
AQS 性能调优
比 synchronized 关键字
优化同步策略,以少上下文切换和锁竞争
优化同步策略,以少上下文切换和锁竞争
实战应用与案例分析
使用 AQS 解决实际并发问题示例
分布式系统中 AQS 的应用扩展
分布式系统中 AQS 的应用扩展
疑难解答与最佳实践
死锁预防与处理
线程饥饿场景,解决方案
锁定顺序,对并发控制的影响
线程饥饿场景,解决方案
锁定顺序,对并发控制的影响
锁优化技术
自旋锁与自适应自旋
锁粗化与锁消除
轻量级锁与偏向锁的工作机制
锁粗化与锁消除
轻量级锁与偏向锁的工作机制
Lock 错误处理与异常控制
处理 InterruptedException
锁竞争激烈时的监控与诊断
锁竞争激烈时的监控与诊断
实战案例分析
使用 Lock 解决生产者-消费者问题
多线程环境中确保数据一致性
分布式系统中的 Lock 实现及其挑战
多线程环境中确保数据一致性
分布式系统中的 Lock 实现及其挑战
总结与对比
Lock 与 synchronized 关键字的比较
根据应用场景选择合适的锁实现
进一步学习和探索的方向
根据应用场景选择合适的锁实现
进一步学习和探索的方向
valatile(保存内存可见性,指令重排)
内存可见性
上文提到过进入 synchronized 时,使得本地缓存失效,synchronized 块中对共享变量的读取必须从主内存读取;退出 synchronized 时,会将进入 synchronized 块之前和 synchronized块中的写操作刷入到主存中。
volatile 有类似的语义,读一个 volatile 变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个 volatile 属性会立即刷入到主内存。所以,volatile 读和 monitorenter 有相同的语义,volatile 写和 monitorexit 有相同的语义。
上文提到过进入 synchronized 时,使得本地缓存失效,synchronized 块中对共享变量的读取必须从主内存读取;退出 synchronized 时,会将进入 synchronized 块之前和 synchronized块中的写操作刷入到主存中。
volatile 有类似的语义,读一个 volatile 变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个 volatile 属性会立即刷入到主内存。所以,volatile 读和 monitorenter 有相同的语义,volatile 写和 monitorexit 有相同的语义。
禁止重排序
volatile 的禁止重排序并不局限于两个 volatile 的属性操作不能重排序,而且是 volatile 属性操作和它周围的普通属性的操作也不能重排序。
根据 volatile 的内存可见性和禁止重排序,那么我们不难得出一个推论:线程 a 如果写入一个 volatile 变量,此时线程 b 再读取这个变量,那么此时对于线程 a 可见的所有属性对于线程 b
都是可见的(这里想想为何ReentrantLock只用了一个volatile的state属性)
volatile 的禁止重排序并不局限于两个 volatile 的属性操作不能重排序,而且是 volatile 属性操作和它周围的普通属性的操作也不能重排序。
根据 volatile 的内存可见性和禁止重排序,那么我们不难得出一个推论:线程 a 如果写入一个 volatile 变量,此时线程 b 再读取这个变量,那么此时对于线程 a 可见的所有属性对于线程 b
都是可见的(这里想想为何ReentrantLock只用了一个volatile的state属性)
语义实现
内存屏障四大分类:(Load 代表读取指令,Store代表写入指令)
内存屏障类型 使用场景 描述
LoadLoad 屏障 Load1; LoadLoad; Load2 在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore 屏障 Store1; StoreStore; Store2 在Store2写入执行前,保证Store1的写入操作对其它处理器可见
LoadStore 屏障 Load1; LoadStore; Store2 在Store2被写入前,保证Load1要读取的数据被读取完毕。
StoreLoad 屏障 Store1; StoreLoad; Load2 在Load2读取操作执行前,保证Store1的写入对所有处理器可见。
为了实现volatile的内存语义,Java内存模型采取以下的保守策略:
在每个 volatile 写操作的前面插入一个 StoreStore 屏障
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障
在每个 volatile 读操作的后面插入一个 LoadStore 屏障
内存屏障类型 使用场景 描述
LoadLoad 屏障 Load1; LoadLoad; Load2 在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore 屏障 Store1; StoreStore; Store2 在Store2写入执行前,保证Store1的写入操作对其它处理器可见
LoadStore 屏障 Load1; LoadStore; Store2 在Store2被写入前,保证Load1要读取的数据被读取完毕。
StoreLoad 屏障 Store1; StoreLoad; Load2 在Load2读取操作执行前,保证Store1的写入对所有处理器可见。
为了实现volatile的内存语义,Java内存模型采取以下的保守策略:
在每个 volatile 写操作的前面插入一个 StoreStore 屏障
在每个 volatile 写操作的后面插入一个 StoreLoad 屏障
在每个 volatile 读操作的后面插入一个 LoadLoad 屏障
在每个 volatile 读操作的后面插入一个 LoadStore 屏障
使用总结
volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值。
volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
volatile 只能作用于属性,我们用 volatile 修饰属性,这样 compilers 就不会对这个属性做指令重排序。
volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
volatile 提供了 happens-before 保证,对 volatile 变量 v 的写入 happens-before 所有其他线程后续对 v 的读操作。
另 volatile 可以使得 long 和 double 的赋值是原子的
volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
volatile 只能作用于属性,我们用 volatile 修饰属性,这样 compilers 就不会对这个属性做指令重排序。
volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。
volatile 提供了 happens-before 保证,对 volatile 变量 v 的写入 happens-before 所有其他线程后续对 v 的读操作。
另 volatile 可以使得 long 和 double 的赋值是原子的
背景
指令重排序(编译器优化指令)
线程栈,内存可见性
原子性
并发使代码可能会产生各种各样结果, Java 编程语言规范一些基本规则,约束下来实现 JVM,开发者要按照规则写代码,并发代码才能准确预测执行结果
final
线程池
ThreadPoolExecutor构造方法参数
核心参数
corePoolSize(核心线程数)
maximumPoolSize(最大线程数)
maximumPoolSize(最大线程数)
队列和任务处理
workQueue(工作队列类型,如LinkedBlockingQueue、SynchronousQueue等)
线程生命周期管理
keepAliveTime(非核心线程空闲存活时间)
TimeUnit(keepAliveTime的时间单位)
TimeUnit(keepAliveTime的时间单位)
线程工厂
ThreadFactory(用于自定义线程创建逻辑的工厂接口)
拒绝策略
RejectedExecutionHandler(当队列满且无法添加新线程时的拒绝策略)
线程池创建工具类 Executors
newFixedThreadPool 创建固定大小的线程池
newCachedThreadPool 动态调整线程数量的线程池
newSingleThreadExecutor 只有一个工作线程的线程池
newScheduledThreadPool 支持定时及周期性任务执行的线程池
线程池常用方法
execute(Runnable command):提交一个Runnable任务
submit(Callable task):提交一个有返回值的任务
shutdown():关闭线程池,不再接收新任务但继续执行已提交任务
shutdownNow():尝试停止所有正在执行的任务并返回未开始执行的任务列表
awaitTermination(long timeout, TimeUnit unit):等待所有任务完成或超时
submit(Callable task):提交一个有返回值的任务
shutdown():关闭线程池,不再接收新任务但继续执行已提交任务
shutdownNow():尝试停止所有正在执行的任务并返回未开始执行的任务列表
awaitTermination(long timeout, TimeUnit unit):等待所有任务完成或超时
线程池异常处理
任务抛出异常的处理
默认行为:异常会被UncaughtExceptionHandler捕获,若没有设置则默认打印堆栈跟踪信息
可通过自定义RejectedExecutionHandler处理因线程池饱和导致的任务拒绝异常
可通过自定义RejectedExecutionHandler处理因线程池饱和导致的任务拒绝异常
异常传播至Future.get()
异步回调与Future
Future接口及其实现类
get()方法获取任务结果与捕获可能的异常
使用Future实现异步回调
Future.get配合CountDownLatch、CompletableFuture等进行结果合并或异步编程
get()方法获取任务结果与捕获可能的异常
使用Future实现异步回调
Future.get配合CountDownLatch、CompletableFuture等进行结果合并或异步编程
最佳实践
线程池配置优化
异常处理策略设计
结合Future实现优雅的任务完成通知机制
异常处理策略设计
结合Future实现优雅的任务完成通知机制
常见问题与解决方案
ThreadPoolCreationException:线程池初始化失败,检查参数是否合理
ThreadPoolExecutor相关运行时异常处理
回调函数中处理并发异常的技巧
ThreadPoolExecutor相关运行时异常处理
回调函数中处理并发异常的技巧
线程基础
线程的基本概念
线程生命周期
线程状态转换
线程生命周期
线程状态转换
创建和启动线程
2.2.1 继承Thread类创建线程
2.2.2 实现Runnable接口创建线程
2.2.3 使用Executor框架创建线程(Callable与Future)
2.2.1 继承Thread类创建线程
2.2.2 实现Runnable接口创建线程
2.2.3 使用Executor框架创建线程(Callable与Future)
线程同步与通信
2.4.1 Thread.join()
2.4.2 Thread.sleep(), Thread.yield()
2.4.3 Thread.currentThread()与Thread.getName()
2.4.1 Thread.join()
2.4.2 Thread.sleep(), Thread.yield()
2.4.3 Thread.currentThread()与Thread.getName()
线程中断机制
2.5.1 interrupt()方法与isInterrupted()方法
2.5.2 InterruptedException处理
2.5.1 interrupt()方法与isInterrupted()方法
2.5.2 InterruptedException处理
同步机制
synchronized关键字
3.1.1 对象锁
3.1.2 类锁
3.1.3 synchronized代码块与方法
3.1.1 对象锁
3.1.2 类锁
3.1.3 synchronized代码块与方法
锁优化机制
3.2.1 轻量级锁
3.2.2 偏向锁
3.2.3 重量级锁
3.2.1 轻量级锁
3.2.2 偏向锁
3.2.3 重量级锁
内存可见性与volatile关键字
3.3.1 volatile变量的内存语义
3.3.2 happens-before原则
3.3.1 volatile变量的内存语义
3.3.2 happens-before原则
高级同步机制
Lock接口与实现
4.1.1 ReentrantLock
4.1.2 Condition对象
4.1.3 公平锁与非公平锁
4.1.1 ReentrantLock
4.1.2 Condition对象
4.1.3 公平锁与非公平锁
ReadWriteLock接口与ReentrantReadWriteLock
StampedLock
原子操作类(Atomic包)
4.4.1 AtomicInteger, AtomicLong等
4.4.2 AtomicReference及其变种
4.4.3 CAS原理与应用
4.4.1 AtomicInteger, AtomicLong等
4.4.2 AtomicReference及其变种
4.4.3 CAS原理与应用
线程间通信
wait()、notify()与notifyAll()方法
BlockingQueue及其实现
5.2.1 ArrayBlockingQueue
5.2.2 LinkedBlockingQueue
5.2.3 SynchronousQueue
5.2.1 ArrayBlockingQueue
5.2.2 LinkedBlockingQueue
5.2.3 SynchronousQueue
同步辅助类
5.3.1 CountDownLatch
5.3.2 CyclicBarrier
5.3.3 Semaphore
5.3.4 Phaser
5.3.1 CountDownLatch
5.3.2 CyclicBarrier
5.3.3 Semaphore
5.3.4 Phaser
内存模型
内存屏障与指令重排序
JMM的主要特性
volatile与有序性保证
final字段与构造函数初始化安全性
JMM的主要特性
volatile与有序性保证
final字段与构造函数初始化安全性
并发容器与数据结构
8.1 ConcurrentHashMap
8.2 CopyOnWriteArrayList与CopyOnWriteArraySet
8.3 ConcurrentLinkedQueue与其他并发队列
8.4 并发集合的安全迭代器
8.2 CopyOnWriteArrayList与CopyOnWriteArraySet
8.3 ConcurrentLinkedQueue与其他并发队列
8.4 并发集合的安全迭代器
并发设计模式与最佳实践
生产者-消费者模式
9.2 读写锁的应用场景
9.3 双重检查锁定(Double Checked Locking)
9.4 安全发布与不可变对象
9.5 线程局部变量(ThreadLocal)的应用
9.2 读写锁的应用场景
9.3 双重检查锁定(Double Checked Locking)
9.4 安全发布与不可变对象
9.5 线程局部变量(ThreadLocal)的应用
性能调优
线程监控工具(如VisualVM, JConsole)
10.2 死锁检测与预防
10.3 线程栈分析与异常排查
10.2 死锁检测与预防
10.3 线程栈分析与异常排查
实战案例
多线程环境下资源竞争问题解决
11.2 利用线程池提高系统吞吐量
11.3 并发容器的实际运用
11.2 利用线程池提高系统吞吐量
11.3 并发容器的实际运用
设计模式
创建型(5)
抽象工场模式
JDBC 驱动程序加载
建造者模式
例如:java.lang.StringBuilder 或 java.nio.ByteBuffer
工厂方法模式
java.util.Calendar.getInstance() 或 JDBC 中的 DriverManager.getConnection()
原型模式
例如:clone() 方法
单例模式
java.lang.Runtime.getRuntime()
结构型(7)
适配器模式
例如:Java I/O 包中的 InputStreamReader 和 OutputStreamWriter
桥接模式
例如:Java AWT/Swing 中的颜色和颜色UI delegate
组合模式
例如:文件系统目录与文件的关系
装饰模式
例如:Java IO 流的装饰链
外观模式
例如:Java Swing 中的 JFrame 对 GUI 元素的封装
享元模式
例如:数据库连接池中的复用对象
代理模式
例如:Java RMI、动态代理机制 java.lang.reflect.Proxy
行为型(11)
责任链模式
例如:Servlet 过滤器链
命令模式
例如:java.awt.event.ActionListener 在 GUI 编程中的应用
解释器模式
正则表达式API是JDK中与解释器模式理念相契合的一个部分:
Pattern和Matcher类:
java.util.regex.Pattern 类可以视为解释器模式中的“语法分析器”,它根据给定的正则表达式字符串构建一个内部数据结构(类似于抽象语法树),这个数据结构代表了正则表达式的文法规则。
java.util.regex.Matcher 类扮演了“解释器”的角色,它可以接受输入字符序列,并依据Pattern对象定义的规则进行匹配解析,找到符合规则的子串。
Pattern和Matcher类:
java.util.regex.Pattern 类可以视为解释器模式中的“语法分析器”,它根据给定的正则表达式字符串构建一个内部数据结构(类似于抽象语法树),这个数据结构代表了正则表达式的文法规则。
java.util.regex.Matcher 类扮演了“解释器”的角色,它可以接受输入字符序列,并依据Pattern对象定义的规则进行匹配解析,找到符合规则的子串。
迭代器模式
例如:java.util.Iterator 接口及其实现类
中介者模式
比如Swing组件之间的事件调度
备忘录模式
某些API的设计中可以看到类似备忘录模式的思想。例如:
java.util.Date 类虽然不是严格意义上的备忘录模式实现,但在早些版本的Java中(如Java 7及更早),clone() 方法可以用于创建当前日期对象的一个副本,这个副本可以看作是当前时刻的“备忘录”,可以在需要时恢复或比较不同时间点的状态。
在Java Swing或JavaFX等图形用户界面库中,当处理复杂的可编辑组件时,可能会用到备忘录模式的概念,用来撤销/重做操作序列(通过维护一系列状态的快照)。
java.util.Date 类虽然不是严格意义上的备忘录模式实现,但在早些版本的Java中(如Java 7及更早),clone() 方法可以用于创建当前日期对象的一个副本,这个副本可以看作是当前时刻的“备忘录”,可以在需要时恢复或比较不同时间点的状态。
在Java Swing或JavaFX等图形用户界面库中,当处理复杂的可编辑组件时,可能会用到备忘录模式的概念,用来撤销/重做操作序列(通过维护一系列状态的快照)。
观察者模式
java.util.Observable 类和 java.util.Observer 接口
状态模式
根据对象状态改变其行为,如JDBC驱动管理的状态转换
策略模式
java.util.Comparator
模板方法模式
javax.servlet.http.HttpServlet
访问者模式
在Java Compiler API中,javax.lang.model.element.Element 和 javax.lang.model.element.ElementVisitor 接口之间存在一定的访问者模式特点。
Element 是一个接口,代表了编译器处理过程中遇到的各种程序元素,如类、接口、方法等,形成了一个对象结构。
ElementVisitor 是一个访客接口,定义了一系列可以作用于不同类型的 Element 的方法,允许用户根据不同的元素类型执行相应的操作。
Element 是一个接口,代表了编译器处理过程中遇到的各种程序元素,如类、接口、方法等,形成了一个对象结构。
ElementVisitor 是一个访客接口,定义了一系列可以作用于不同类型的 Element 的方法,允许用户根据不同的元素类型执行相应的操作。
jvm
JVM构成图
类加载子系统
根据给定全限定名类名(如:java.lang.Object)装载class文件到运行时数据区的方法区
根据给定全限定名类名(如:java.lang.Object)装载class文件到运行时数据区的方法区
运行时数据区(JMM内存模型)
子主题
执行引擎
执行classes指令
执行classes指令
本地库接口
本地方法库(native libraries)交互,其它编程语言交互接口。
本地方法库(native libraries)交互,其它编程语言交互接口。
类加载机制
类的生命周期?
加载->验证->准备->解析->初始化->使用->卸载
加载->验证->准备->解析->初始化->使用->卸载
类加载器有哪些?
核心类加载器(BootstrapClassLoader)
加载支撑JVM运行于JRE的lib下核心类库
加载支撑JVM运行于JRE的lib下核心类库
扩展类加载器(ExtensionClassLoader)
加载支撑JVM运行于JRE下ext的JAR类包
加载支撑JVM运行于JRE下ext的JAR类包
应用加载器(AppClassLoader)
加载ClassPath路径下类包,加载自己的类
加载ClassPath路径下类包,加载自己的类
自定义加载器(继承ClassLoader)
加载自定义路径类包
加载自定义路径类包
如何创建一个自定义加载器?
1、继承java.lang.ClassLoader
2、重写findClass方法(打破双亲委派机制,重写loadClass();不想打破双亲委派机制findClass())
1、继承java.lang.ClassLoader
2、重写findClass方法(打破双亲委派机制,重写loadClass();不想打破双亲委派机制findClass())
什么是双亲委派机制?
类加载时,向上委派,向下加载。
类加载时,向上委派,向下加载。
为什么要设计双亲委派机制?
- 1,沙箱安全:自己写java.lang.String.class类不会被加载,可防止核心API被篡改
- 2,避免类重复加载:父已加载,子无需加载,加载唯一
什么是全盘负责委托机制?
ClassLoder装载时,类所依赖引用类由这个ClassLoder载入,除非显示使用另外一个ClassLoder
ClassLoder装载时,类所依赖引用类由这个ClassLoder载入,除非显示使用另外一个ClassLoder
JVM内存模型
堆(共享)
年轻代(默认1/3)
Eden(默认8/10)
Survivor0(默认1/10)
Survivor1(默认1/10)
老年代(默认2/3)
虚拟机栈(私有)
局部变量表
操作数栈
动态链接
方法出口
本地方法栈(私有)
程序计数器(私有)
方法区(元空间)(共享)
常量
类信息
Java 堆和栈区别?
1、内存区域不同
2、可见性不同(堆共享、栈私有)
3、存放数据不同(堆存放对象,静态变量,字符串常量池。栈存放常量、变量、类信息)
1、内存区域不同
2、可见性不同(堆共享、栈私有)
3、存放数据不同(堆存放对象,静态变量,字符串常量池。栈存放常量、变量、类信息)
JVM参数设置
-Xmx3072m:JVM最大堆内存3G
-Xms3072m:JVM初始堆3G。与-Xmx相同,避免垃圾回收完成后,JVM重新分配内存
-Xmn2048:设年轻代大小2G
-Xss256K:每个线程栈大小,默认1m,值越小,JVM开启线程越多
-XX:SurvivorRatio=4:设置Eden区与Survivor区比值,默认是8,1个Survivor区占年轻代1/10
-XX:MetaspaceSize: 指定元空间触发Fullgc初始阈值(元空间无固定初始大小), 字节为单位,默认是21M
建议JVM MetaspaceSize和MaxMetaspaceSize同值,比初始值大,
8G物理内存机器,两值设256M。
8G物理内存机器,两值设256M。
让对象在新生代里分配和回收,别让太多对象频繁进老年代,避免频繁老年代垃圾回收,
给系统充足内存,避免新生代频繁进行垃圾回收。
给系统充足内存,避免新生代频繁进行垃圾回收。
-XX:MaxMetaspaceSize: 设元空间最大值, 默认是-1,设计上不限制, 限于本地内存
GC类型
Minor GC 新生代收集
- 堆中新生代垃圾收集动作,Minor GC频繁,速度比较快
- 年轻代Eden区分配满触发
- MinorGC 标记-复制算法。
Major GC 老年代收集
- 堆中老年代垃圾收集动作
- 老年代对象较稳定,MajorGC不会频繁
- 只有CMS收集器才有单独MajorGC
Mixed GC 混合收集
- 整个新生代,部分老年代垃圾收集动作。
- 只有G1收集器会有这种行为
Full GC 整堆收集
- 整个Java堆和方法区垃圾收集
- Full GC 前先Minor GC
对象晋升老年代
大对象直接进入老年代
避免Eden和两个Survivor间大量内存拷贝(新生代用标记-复制算法收集内存)。
大对象是指需要大量连续内存空间的对象,如:大数组和长字符串
避免Eden和两个Survivor间大量内存拷贝(新生代用标记-复制算法收集内存)。
大对象是指需要大量连续内存空间的对象,如:大数组和长字符串
长期存活的对象将进入老年代
虚拟机为对象年龄设计数器,对象经过1次Minor GC,进入Survivor区,
每次GC对象年龄加1,到阀值(默认15)进老年区。
可以通过参数 -XX:MaxTenuringThreshold 来设置
虚拟机为对象年龄设计数器,对象经过1次Minor GC,进入Survivor区,
每次GC对象年龄加1,到阀值(默认15)进老年区。
可以通过参数 -XX:MaxTenuringThreshold 来设置
对象动态年龄判断
Survivor相同年龄对象总和大于Survivor一半,大于等于的对象可进老年代。
对象动态年龄判断机制,一般minor gc后触发
Survivor相同年龄对象总和大于Survivor一半,大于等于的对象可进老年代。
对象动态年龄判断机制,一般minor gc后触发
对象
对象引用类型
强引用
不可达时回收
不可达时回收
程序代码普遍存在引用赋值,强引用关系还存在,不会回收掉被引用的对象
软引用
内存不足时回收
内存不足时回收
有用,非必须对象。被软引用关联的对象,内存溢出前,会把这些对象列进回收范围,进行二次回收,这次回收仍无足够的内存, 才会抛出内存溢出异常。
弱引用
垃圾收集器工作时回收
垃圾收集器工作时回收
非必须对象,比软引用弱,弱引用关联的对象,只能生存到下次垃圾收集。下次垃圾收集器开始,都会回收掉被弱引用关联的对象
虚引用
垃圾收集器工作时回收
垃圾收集器工作时回收
“幽灵引用”或者“幻影引用”,对象是否有虚引用,不会生存时间构成影响,无法通过虚引用取得对象实例。对象被收集器回收时收到一个系统通知
对象内存布局
对象头
对象自身运行时数据(mark word)
哈希码
GC分代年龄
锁状态标志
线程持有锁
偏向线程Id
偏向时间戳
类型指针
时数组,还有记录数组长度数据
实例数据
对象填充
对象访问
句柄访问
句柄访问好处,reference存储稳定句柄地址,对象移动(垃圾收集标记,复制,移动对象的行为)只会改变句柄实例数据指针,reference本身不被修改
直接指针
HotSpot虚拟机主要使用直接指针来进行对象访问
HotSpot虚拟机主要使用直接指针来进行对象访问
直接指针访问速度更快,节省一次指针定位时间开销,对象访问Java中非常频繁,这类开销积少成多,是可观的执行成本。
HotSpot虚拟机用直接指针来进行对象访问
HotSpot虚拟机用直接指针来进行对象访问
逃逸分析
栈上分配
对象不会逃逸线程外,可将对象在栈上分配,对象占用的内存随栈帧出栈而销毁,这样一来,垃圾收集压力降低很多
同步消除
线程同步是耗时过程,逃逸分析能够确定变量不会逃逸出线程,无法被其他线程访问,变量读写不会有竞争, 同步措施可安全地消除掉。
标量替换
一个数据是基本数据类型,不可拆分,称之为标量。把Java对象拆散,将其成员变量恢复为原始类型来访问,过程称为标量替换。假如逃逸分析能够证明对象不会被方法外部访问,且可以被拆散,可不创建对象,直接创建若干成员变量代替,让对象成员变量在栈上分配和读写
垃圾收集理论及算法
对象内存回收算法
引用计数器法
为对象创建引用计数,新增引用+1,释放 -1,计数为 0 就可回收。
缺点不能解决循环引用问题
为对象创建引用计数,新增引用+1,释放 -1,计数为 0 就可回收。
缺点不能解决循环引用问题
可达性分析算法
GC Roots 向下搜索,搜索过路径称为引用链。
对象到 GC Roots 无引用链,对象可回收
GC Roots 向下搜索,搜索过路径称为引用链。
对象到 GC Roots 无引用链,对象可回收
可作为GC Root根的对象
- 虚拟机栈(本地变量)引用对象
- 本地方法栈引用对象
- 静态变量引用对象
分代收集理论
对象存活时间不同,将内存分几块,Java堆分新生代(1/3)和老年代(2/3),由业务场景,选择分代收集器,各年代内存分配,回收触发机制
对象存活时间不同,将内存分几块,Java堆分新生代(1/3)和老年代(2/3),由业务场景,选择分代收集器,各年代内存分配,回收触发机制
标记 - 清除
标记出需要回收对象,标记后,统一回收掉被标记对象。
优点:实现简单,不需要对象进行移动。
缺点:标记、清除效率低,产生大量不连续内存碎片,内存利用率相对不高,加大触发垃圾回收的频率。
适用于老年代,CMS用标记清除
标记出需要回收对象,标记后,统一回收掉被标记对象。
优点:实现简单,不需要对象进行移动。
缺点:标记、清除效率低,产生大量不连续内存碎片,内存利用率相对不高,加大触发垃圾回收的频率。
适用于老年代,CMS用标记清除
标记 - 复制
可用内存,分2等份,每次只用一块。这块用完,将存活对象,复制到另一块,再把用过内存清理掉。
优点:顺序分配内存,实现简单、运行高效,不考虑内存碎片。
缺点:实际可用内存为一半,对象存活率高时,会频繁进行复制。
适用于年轻代
可用内存,分2等份,每次只用一块。这块用完,将存活对象,复制到另一块,再把用过内存清理掉。
优点:顺序分配内存,实现简单、运行高效,不考虑内存碎片。
缺点:实际可用内存为一半,对象存活率高时,会频繁进行复制。
适用于年轻代
标记 - 整理
标记,让存活对象向一端移动,清理边界以外内存
优点:解决标记-清理算法存在大量内存碎片问题。
缺点:需要局部对象移动,降低了效率。
适用于老年代
标记,让存活对象向一端移动,清理边界以外内存
优点:解决标记-清理算法存在大量内存碎片问题。
缺点:需要局部对象移动,降低了效率。
适用于老年代
垃圾收集器
Serial收集器(标记 - 复制)
单线程收集器,收集时,要stop the world,用复制算法。
和CMS搭配
和CMS搭配
Serial Old收集器(标记-整理)
Serial收集器对应老年代版本,单线程收集器,用标记整理算法
ParNew收集器(标记 -复制)
可以搭配Serial Old和CMS
可以搭配Serial Old和CMS
Serial收集器多线程版本,要stop the world,复制算法。
和CMS搭配
和CMS搭配
Parallel收集器(标记 -复制)
JDK8 新生代 默认,侧重吞吐量
JDK8 新生代 默认,侧重吞吐量
分代模型的新生代收集器,并发多线程复制算法收集器,目标达到可控的吞吐量。如虚拟机运行100分钟,垃圾回收1分钟,吞吐量99%。
JDK8默认
JDK8默认
Parallel Old收集器(标记-整理)
JDK8 老年代 默认,侧重吞吐量
JDK8 老年代 默认,侧重吞吐量
Parallel Scavenge收集器老年代版本,多线程,标记-整理算法
CMS收集器(标记-清除)
老年代用 侧重低停顿
老年代用 侧重低停顿
初始标记(STW)
暂停所有其他线程(STW),记录gc roots直接能引用对象,速度很快
并发标记
GC Roots关联对象,遍历引用链过程, 耗时长,无需停顿用户线程, 可与垃圾收集线程并发运行
重新标记(STW)
修正并发标记期间,用户程序运行,标记变动部分,会STW
并发清除
开启用户线程,GC线程,对未标记区域做清扫。新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)
并发重置
重置GC过程中标记数据
G1收集器(标记-复制)
JDK9默认
JDK9默认
初始标记(initialmark,STW)
暂停所有其他线程(STW),记录gc roots直接能引用对象,速度很快
并发标记(ConcurrentMarking)
GC Roots关联对象,遍历引用链过程, 耗时长,无需停顿用户线程, 可与垃圾收集线程并发运行
最终标记(Remark,STW)
修正并发标记期间,用户程序运行,标记变动部分,会STW
筛选回收(Cleanup,STW)
复制算法,将region存活对象复制另一个region中,不会像CMS很多内存碎片,需整理一次,G1用复制算法回收,几乎不会太多内存碎片。
如何选择垃圾收集器?
Serial
回收器内存小(约100m),单核心,停顿时间无要求
回收器内存小(约100m),单核心,停顿时间无要求
Parallel
吞吐量有要求,4G以下可用parallel
吞吐量有要求,4G以下可用parallel
CMS/G1
低停顿,内存4-8G可用ParNew+CMS,8G上可用G1
低停顿,内存4-8G可用ParNew+CMS,8G上可用G1
ZGC
内存较大,几百G用ZGC
内存较大,几百G用ZGC
JVM常用调优命令
jps
JVM Process Status Tool,显示系统内HotSpot虚拟机进程
JVM Process Status Tool,显示系统内HotSpot虚拟机进程
jstat
JVM statistics Monitoring 监视虚拟机运行时状态信息,显示出虚拟机进程中类装载、内存、垃圾收集、JIT编译等数据。
JVM statistics Monitoring 监视虚拟机运行时状态信息,显示出虚拟机进程中类装载、内存、垃圾收集、JIT编译等数据。
jmap
JVM Memory Map 生成heap dump文件
JVM Memory Map 生成heap dump文件
jhat
JVM Heap Analysis Tool 命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置微型HTTP/HTML服务器,生成dump分析结果,可在浏览器中查看
JVM Heap Analysis Tool 命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置微型HTTP/HTML服务器,生成dump分析结果,可在浏览器中查看
jstack
生成java虚拟机当前线程快照。
生成java虚拟机当前线程快照。
jinfo
JVM Configuration info 实时查看和调整虚拟机运行参数
JVM Configuration info 实时查看和调整虚拟机运行参数
常见问题
什么时候会触发FullGC?
System.gc()
不会立即生效,不可控,不建议使用
老年代空间不足
大对象空间分配,长期存活对象,进老年代,老年代放不下,会触发Full GC
永久代空间不足
方法区由永久代实现,永久代空间不足 Full GC
老年代空间担保失败
年轻代GC后,仍有大量对象存活,需要老年代进行分配担保,把Survivor无法容纳对象,送入老年代
1、minor gc前,jvm先检查,老年代连续可用空间,,是否大于新生代对象总大小(包含垃圾对象),大于直接minor gc,不会触发full gc
反之,看是否开启老年代,空间担保机制和是否允许冒险。未开启,直接full gc;开启了,判断老年代连续可用空间,是否大于minor gc后进入老年代对象的总大小,如果大于,则冒险进行minor gc,否则,则进行full gc
1、minor gc前,jvm先检查,老年代连续可用空间,,是否大于新生代对象总大小(包含垃圾对象),大于直接minor gc,不会触发full gc
反之,看是否开启老年代,空间担保机制和是否允许冒险。未开启,直接full gc;开启了,判断老年代连续可用空间,是否大于minor gc后进入老年代对象的总大小,如果大于,则冒险进行minor gc,否则,则进行full gc
Cocurrent mode failure
cms并发标记和并发清理期间,有新垃圾产生,老年代没有足够空间.
JVM中一次完整的GC流程是怎样的,对象如何晋升到老年代,oom前几次垃圾回收?
你知道哪几种垃圾收集器,各自的优缺点,重点讲下cms和G1,包括原理,流程,优缺点。
原理,都是多线程操作,都有存在stw过程,存在并发标记,清理的过程,CMS多了重置操作,G1
流程,初始标记(stw)->并发标记->重新标记->收集垃圾->重置 G1 初始标记stw->并发标记->最终标记->复制移动
优点:回收效率高,cpu利用率,stw时间短,
内存溢出(out of memory)的场景及解决方案?
java堆内存溢出(OutOfMemoryError.heap)
jvm内存太小,所需内存太大,创建对象时,分配空间就会抛出这个异常
1、可适当调整-Xms和-Xmx两个jvm参数
2、提高请求执行速度,垃圾回收越早越好
1、可适当调整-Xms和-Xmx两个jvm参数
2、提高请求执行速度,垃圾回收越早越好
元空间内存溢出(OutOfMemoryError: Metaspace)
加载的类太多或者引用第三方JAR。
设置-XX:MetaspaceSize(初始空间大小)和-XX:MaxMetaspaceSize(最大空间),触发FULL GC
设置-XX:MetaspaceSize(初始空间大小)和-XX:MaxMetaspaceSize(最大空间),触发FULL GC
永久代内存溢出(OutOfMemoryError:PermGenspace)
-XX:MaxPermSize=128m
-XXermSize=128m
-XXermSize=128m
栈内存溢出(StackOverflowError)
1、调高-Xss(线程栈大小)
2、减少递归调用
2、减少递归调用
直接内存内存溢出(OutOfMemoryError: Direct buffer memory)
1、NIO框架用ByteBuffer中的allocateDirect()时,及时clear内存
2、-XX:MaxDirectMemorySize=128m
2、-XX:MaxDirectMemorySize=128m
GC 开销超过限制(OutOfMemoryError:GCoverheadlimitexceeded)
Java 进程98%时间垃圾回收,恢复不到2%堆空间,最后连续5个(编译时常量)垃圾回收如此。
1、使用 -Xmx 增加堆大小
2、使用 -XX:-UseGCOverheadLimit 取消 GC 开销限制
3、修复应用程序中的内存泄漏
1、使用 -Xmx 增加堆大小
2、使用 -XX:-UseGCOverheadLimit 取消 GC 开销限制
3、修复应用程序中的内存泄漏
线程栈满(Stack size too small)
一个线程空间大小是有限制的,线程空间满后,出现异常。
增加线程栈大小。-Xss2m
增加线程栈大小。-Xss2m
系统内存被占满(OutOfMemoryError: unable to create new native thread)
1、减少线程数量。
2、线程数量不能减少情况,设置-Xss减小单个线程大小。以便能生产更多的线程。
2、线程数量不能减少情况,设置-Xss减小单个线程大小。以便能生产更多的线程。
内存溢出如何排查?
1、配置-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\\log,出现溢出时会生成dumpfile.hprof文件。
2、通过jmap命令生成dump文件。jmap -dump:format=b,file=<dumpfile.hprof> <pid>
3、通过MAT来查看具体的问题
1、配置-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\\log,出现溢出时会生成dumpfile.hprof文件。
2、通过jmap命令生成dump文件。jmap -dump:format=b,file=<dumpfile.hprof> <pid>
3、通过MAT来查看具体的问题
线上CPU使用比较高,如何快速定位问题?
1、定位进程,top命令查看CPU占比最高的进程PID1893
2、定位线程,top -Hp 1893命令查看CPU占比最高的线程PID4519
3、转16进制,printf %x 4519命令将线程B转换成16进制11a7
4、定位代码,jstack 1893 |grep -A 200 11a7命令查看栈信息,定位到具体的代码行
1、定位进程,top命令查看CPU占比最高的进程PID1893
2、定位线程,top -Hp 1893命令查看CPU占比最高的线程PID4519
3、转16进制,printf %x 4519命令将线程B转换成16进制11a7
4、定位代码,jstack 1893 |grep -A 200 11a7命令查看栈信息,定位到具体的代码行
OOM会导致JVM直接退出吗?为什么?
不会,线程抛OOM后,线程持有对象,所占资源都会被释放掉(gc),不影响其它线程
工作线程都退出了(只有守护线程),JVM才会退出
注:内存泄露不会马上导致异常,内存泄露堆积后,最终可能会导致内存溢出。
不会,线程抛OOM后,线程持有对象,所占资源都会被释放掉(gc),不影响其它线程
工作线程都退出了(只有守护线程),JVM才会退出
注:内存泄露不会马上导致异常,内存泄露堆积后,最终可能会导致内存溢出。
集合
自由主题
0 条评论
下一页