java技术路线图(面试)
2022-06-27 14:59:13 26 举报
AI智能生成
总结的面试技术宝典,涵盖面比较全,凭借此宝典拿下不少offer。
作者其他创作
大纲/内容
13.springcloud
1.注册中心
1.nacos
1.服务注册
各服务通过nacos client向注册中心发送注册请求,发送的注册请求数据放在一个阻塞队列中(set集合)。客户端即刻返回注册成功
Nacos server通过一个异步任务去阻塞队列中进行消费,然后去写进注册表,最终完成注册。
2.注册过程中如何实现高并发?
解答:通过内存队列+异步任务的形式
3.心跳机制
1.nacos client通过一个心跳任务来保持与注册中心的连接,也就是保活
2.ZK、netty也是通过心跳机制进行保活,但是使用的是轻量级的socket实现;Nacos底层使用的是http接口;
3.心跳机制:通过线程池,延时启动,默认5秒里面有个方法是setBeat();
4.接收到心跳之后,服务端需要做什么?
服务端需要更新本次心跳的时间,也就是最后更新时间。
5.服务端如何进行感知心跳的?
Helthcheck()方法,进行健康检查任务,runnable 默认延时5秒处理
服务端拿到Instance的set集合后,遍历判断
使用(当前时间-最后更新时间>设置的健康超时时间(默认15秒)),如果超过了15秒,则将状态改为 不健康。
里面还有一个遍历判断:
使用(当前时间-最后更新时间>设置的健康超时时间(默认30秒)),如果超过了30秒,则认为服务挂掉了,则从注册表中将该服务剔除
使用(当前时间-最后更新时间>设置的健康超时时间(默认15秒)),如果超过了15秒,则将状态改为 不健康。
里面还有一个遍历判断:
使用(当前时间-最后更新时间>设置的健康超时时间(默认30秒)),如果超过了30秒,则认为服务挂掉了,则从注册表中将该服务剔除
4.Nacos如何处理注册表的读写高并发冲突?
使用COW机制,也就是写实复制技术Copy On Write
首先一个概念:注册表的结构是一个双层Map的结构
Map<String,Map<String,Service>>
Space--Group--Service--Cluster--Instance
例子:develop(开发版本)--组(订单服务组、积分服务组)--服务(订单服务)--地区(BJ、NJ)--实例(机器9001/9002)
首先一个概念:注册表的结构是一个双层Map的结构
Map<String,Map<String,Service>>
Space--Group--Service--Cluster--Instance
例子:develop(开发版本)--组(订单服务组、积分服务组)--服务(订单服务)--地区(BJ、NJ)--实例(机器9001/9002)
如何读写数据避免数据错乱?保持读写的一致性?
一种是加锁,但加锁无法实现读写的高并发。
一种是写时复制COW。也就是在注册时:复制一份注册表(根据Instance,复制其中的一部分),在副本中进行写,写完进行替换。复制的内容其实是一个set集合,也就是Map结构里面的内层结构,根据Instance名称进行定位,并非复制的整个注册表。
服务发现时,读取原数据,拉取到本地,然后经过frign接口去调用其他服务,当然frign调用的时候也要借助ribbon进行负载均衡,所以说ribbon实际上是做的客户端的负载均衡。实际上nacos是一个读写分离的机制。从而实现了高并发。
一种是加锁,但加锁无法实现读写的高并发。
一种是写时复制COW。也就是在注册时:复制一份注册表(根据Instance,复制其中的一部分),在副本中进行写,写完进行替换。复制的内容其实是一个set集合,也就是Map结构里面的内层结构,根据Instance名称进行定位,并非复制的整个注册表。
服务发现时,读取原数据,拉取到本地,然后经过frign接口去调用其他服务,当然frign调用的时候也要借助ribbon进行负载均衡,所以说ribbon实际上是做的客户端的负载均衡。实际上nacos是一个读写分离的机制。从而实现了高并发。
那么问题来了:假设两个服务同时注册、同时都复制了一份然后进行副本修改,替换的时候会产生并发覆盖问题吗?
解答:不会。因为消费的时候是从阻塞队列中进行消费,是一个单线程的消费,所以不会出现并发覆盖的问题。
解答:不会。因为消费的时候是从阻塞队列中进行消费,是一个单线程的消费,所以不会出现并发覆盖的问题。
那么问题又来了:既然是单线程的消费,会不会导致队列任务的积压,因为单线程毕竟消费慢?
解答:一、这是一个极小的概率事件,内存队列也比较的快,可以处理的来。二、就算出现了也无所谓,稍微延迟一点没有关系,eureka还延迟一分钟以上呢。除非你同时启动上千台才会出现这种情况。
注:nacos的TPS为13000/S.
TPS为吞吐量,吞吐量为每秒内的访问数值,
QPS为峰值时的每秒钟访问数值。
解答:一、这是一个极小的概率事件,内存队列也比较的快,可以处理的来。二、就算出现了也无所谓,稍微延迟一点没有关系,eureka还延迟一分钟以上呢。除非你同时启动上千台才会出现这种情况。
注:nacos的TPS为13000/S.
TPS为吞吐量,吞吐量为每秒内的访问数值,
QPS为峰值时的每秒钟访问数值。
2.eureka
1.什么是Eureka?
Eureka作为SpringCloud的服务注册功能服务器,他是服务注册中心,系统中的其他服务使用Eureka的客户端将其连接到Eureka Service中,并且保持心跳,这样工作人员可以通过EurekaService来监控各个微服务是否运行正常。
2.Eureka怎么实现高可用?
集群吧,注册多台Eureka,然后把SpringCloud服务互相注册,客户端从Eureka获取信息时,按照Eureka的顺序来访问。
3.什么是Eureka的自我保护模式?
默认情况下,如果Eureka Service在一定时间内没有接收到某个微服务的心跳,Eureka Service会进入自我保护模式,在该模式下Eureka Service会保护服务注册表中的信息,不在删除注册表中的数据,当网络故障恢复后,Eureka Servic 节点会自动退出自我保护模式
4.DiscoveryClient的作用?
可以从注册中心中根据服务别名获取注册的服务器信息。
3.zookeeper
1.zookeeper工作机制
Zookeeper从设计模式角度来理解:是-一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper.上注册的那些观察者做出相应的反应。
1.各服务启动的时候去zk注册信息(创建的都是临时节点)
2.客户端获取当前在线的服务列表,并注册监听事件
3.如果服务器节点下线
4.发送服务器节点上下线事件通知
5.客户端重新再去获取服务器列表,并注册监听
2.zookeeper的特点
1.Zookeeper: 一个领导者(Leader) ,多个跟随者(Follower) 组成的集群。
2.集群中只要有半数以上节点存活,Zookeeper集 群就能正常服务
3.全局数据一致:每个Server保存一份相同的数据副本,Client 无论连接到哪个Server,数据都是一致的。
4.更新请求顺序进行,来自同一个Clent的更新请求按其发送顺序依次执行。
5.数据更新原子性,一次数据更新要么成功,要么失败。
6.实时性,在一定时间范围内,Client 能读到最新数据。
3.zookeeper的数据结构
ZooKeeper数据模型的结构与Unix文件系统很类似,整体上可以看作是一棵树,每个节点称做一
个ZNode。每一个ZNode默认能够存储1MB的数据 ,每个ZNode都可以通过其路径唯一标识。
个ZNode。每一个ZNode默认能够存储1MB的数据 ,每个ZNode都可以通过其路径唯一标识。
4.zookeeper的应用场景
1.统一命名服务
在分布式环境下,经常需要对应用/服务进行统一命名 ,便于识别。
例如: IP不容易记住,而域名容易记住。
例如: IP不容易记住,而域名容易记住。
2.统一集群管理、统一配置管理
1.分布式环境下,配置文件同步非常常见。
( 1 )-般要求- -个集群中,所有节点的配置信息是一致的, 比如Kafka集群。
( 2)对配置文件修改后,希望能够快速同步到各个节点上。
( 1 )-般要求- -个集群中,所有节点的配置信息是一致的, 比如Kafka集群。
( 2)对配置文件修改后,希望能够快速同步到各个节点上。
2.配置管理可交由ZooKeeper实现。
( 1 )可将配置信息写入ZooKeeper上的一-个Znode.
(2)各个客户端服务器监听这个Znode.
( 3 ) - -旦Znode中的数据被修改, ZooKeeper将通知
各个客户端服务器。
( 1 )可将配置信息写入ZooKeeper上的一-个Znode.
(2)各个客户端服务器监听这个Znode.
( 3 ) - -旦Znode中的数据被修改, ZooKeeper将通知
各个客户端服务器。
ZooKeeper可以实现实时监控节点状态变化
( 1 )可将节点信息写入ZooKeeper,上的一一个ZNode.
(2)监听这个ZNode可获取它的实时状态变化。
( 1 )可将节点信息写入ZooKeeper,上的一一个ZNode.
(2)监听这个ZNode可获取它的实时状态变化。
3.服务动态上下线
4.软负载均衡
在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求
5.配置参数解读
Zookeeper中的配置文件zoo.cfg中
1.tickTime =2000:通信心跳数,Zookeeper 服务器与客户端心跳时间,单位毫秒
Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒。
它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间。(session的最小超时时间是2*tickTime)
2.initLimit =10:LF 初始通信时限
集群中的Follower跟随者服务器与Leader领导者服务器之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的Zookeeper服务器接到Leader的时限。
3.syncLimit =5:LF 同步通信时限
集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime,Leader认为Follwer死掉,从服务器列表中删除Follwer。
4.dataDir:数据文件目录+数据持久化路径
主要用于保存 Zookeeper 中的数据。
5.clientPort =2181:客户端连接端口
6.zk内部原理
1.Zab协议
1.消息广播模式
如果你了解过2PC协议的话,理解起来就简单很多了,消息广播的过程实际上是一个简化版本的二阶段提交过程。
通俗的理解就比较简单了,我是领导,我要向各位传达指令,不过传达之前我先问一下大家支不支持我,若有一半以上的人支持我,那我就向各位传达指令了
通俗的理解就比较简单了,我是领导,我要向各位传达指令,不过传达之前我先问一下大家支不支持我,若有一半以上的人支持我,那我就向各位传达指令了
1.Leader将客户端的request转化成一个Proposal(提议),leader首先把proposal发送到FIFO队列里
2.FIFO取出队头proposal给Follower;Follower反馈一个ACK给队列;队列把ACK交给leader
3.leader收到半数以上ACK,就会发送commit指令给FIFO队列;FIFO队列把commit给Follower。
2.崩溃恢复模式
leader就是一个领导,既然领导挂了,整个组织肯定不会散架,毕竟离开谁都能活下去是不是,这时候我们只需要选举一个新的领导即可,而且还要把前leader还未完成的工作做完,也就是说不仅要进行leader服务器选取,而且还要进行崩溃恢复。我们一个一个来解决
1.leader选举
looking状态:也就是观望状态,这时候是由于组织出现内部问题,那就停下来,做一些其他的事。
following状态:自身是一个组织成员,做自己的事。
leading状态:自身是-个组织老大,做自己的事。
following状态:自身是一个组织成员,做自己的事。
leading状态:自身是-个组织老大,做自己的事。
这就是整个选举的过程。并且每个人的选举,都代表了一个事件,为了保证分布式系统的时间有序性,因此给每一个事件都分配了一个Zxid。相当于编了-个号。32位是按照数字递增,即每次客户端发起一个proposal, 低32位的数字简单加1。高32位是leader周期的epoch编号。
每当选举出一个新的eader时, 新的leader就从本地事物日志中取出ZXID然后解析出高32位的epoch编号,进行加1,再将32位的全部设置为0。这样就保证了每次新选举的leader后,保证了ZXID的唯一性而且是保证递增的。
2.崩溃恢复
既然要恢复,有些场景是不能恢复的,ZAB协议崩溃恢复要求满足如下2个要求:
第一:确保已经被leader 提交的proposal必须最终被所有的follower服务器提交。
第二:确保丢弃已经被leader出的但是没有被提交的proposal。
第一:确保已经被leader 提交的proposal必须最终被所有的follower服务器提交。
第二:确保丢弃已经被leader出的但是没有被提交的proposal。
第一步:选取当前取出最大的ZXID,代表当前的事件是最新的。
第二步:新leader把这个事件proposal提交给其他的follower节点
第三步: follower节点会根据leader的消息进行回退或者是数据同步操作。最终目的要保证集群中所有节点的数据副本保持一致。
这就是整个恢复的过程,其实就是相当于有个日志一样的东西, 记录每一次操作, 然后把出事前的最新操作恢复,然后进行同步即可。
第二步:新leader把这个事件proposal提交给其他的follower节点
第三步: follower节点会根据leader的消息进行回退或者是数据同步操作。最终目的要保证集群中所有节点的数据副本保持一致。
这就是整个恢复的过程,其实就是相当于有个日志一样的东西, 记录每一次操作, 然后把出事前的最新操作恢复,然后进行同步即可。
2.节点类型
持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除
短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除
说明:创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护
注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
3.选举机制(面试重点)
1.半数机制:集群中半数以上机器存活,集群可用。所以 Zookeeper 适合安装奇数台服务器
2.Zookeeper 虽然在配置文件中并没有指定 Master 和 Slave。但是,Zookeeper 工作时,是有一个节点为 Leader,其他则为 Follower,Leader 是通过内部的选举机制临时产生的。
3.选举机制举例
场景:假设有五台服务器组成的 Zookeeper 集群,它们的 id 从 1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么
1.服务器 1 启动,发起一次选举。服务器 1 投自己一票。此时服务器 1 票数一票,不够半数以上(3 票),选举无法完成,服务器 1 状态保持为 LOOKING
2.服务器 2 启动,再发起一次选举。服务器 1 和 2 分别投自己一票并交换选票信息:此时服务器 1 发现服务器 2 的 ID 比自己目前投票推举的(服务器 1)大,更改选票为推举服务器 2。此时服务器 1 票数 0 票,服务器 2 票数 2 票,没有半数以上结果,选举无法完成,服务器 1,2 状态保持 LOOKING
3.服务器 3 启动,发起一次选举。此时服务器 1 和 2 都会更改选票为服务器 3。此次投票结果:服务器 1 为 0 票,服务器 2 为 0 票,服务器 3 为 3 票。此时服务器 3 的票数已经超过半数,服务器 3 当选 Leader。服务器 1,2 更改状态为 FOLLOWING,服务器 3 更改状态为 LEADING;
4.服务器 4 启动,发起一次选举。此时服务器 1,2,3 已经不是 LOOKING 状态,不会更改选票信息。交换选票信息结果:服务器 3 为 3 票,服务器 4 为 1 票。此时服务器 4服从多数,更改选票信息为服务器 3,并更改状态为 FOLLOWING;
5.服务器 5 启动,同 4 一样当小弟。
4.监听器原理(面试重点)
1.首先要有一个main()线程
2.在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)
3.通过connect线程将注册的监听事件发送给Zookeeper。
4.在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
5.Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
6.listener线程内部调用了process()方法。
注:常见的监听
1.监听节点数据的变化 get path[watch]
2.监听子节点增减的变化 ls path[watch]
5.写数据流程
1.Client向ZooKeeper的Server1上写数据,发送一个写请求。
2.如果Server1不是Leader,那么Server1会把接受到的请求进一步转发给Leader,因为每个ZooKeeper的Server里面有一个是Leader。这个Leader会将写请求广播给各个Server,比如Server1和Server2,各个Server会将该写请求加入待写队列,并向Leader发送成功信息。
3.当Leader收到半数以上Server的成功信息,说明该写操作可以执行。Leader会向各个Server发送提交信息,各个Server收到信息后会落实队列里的写请求,此时写成功。
4.Server1会进一步通知Client数据写成功了,这时就认为整个写操作成功。
7.zk的常用命令
ls path [watch] :使用 ls 命令来查看当前 znode 中所包含的内容
ls2 path [watch]:查看当前节点数据并能看到更新次数等数据
create:
普通创建
-s 含有序列
-e 临时(重启或者超时消失)
-s 含有序列
-e 临时(重启或者超时消失)
get path [watch]:获得节点的值
set:设置节点的具体值
stat:查看节点状态
delete:删除节点
rmr:递归删除节点
4.Eureka和ZooKeeper都可以提供服务注册
与发现的功能,请说说两个的区别?
与发现的功能,请说说两个的区别?
1. ZooKeeper中的节点服务挂了就要选举 在选举期间注册服务瘫痪,虽然服务最终会恢复,但是选举期间不可用的, 选举就是改微服务做了集群,必须有一台主其他的都是从
2. Eureka各个节点是平等关系,服务器挂了没关系,只要有一台Eureka就可以保证服务可用,数据都是最新的。 如果查询到的数据并不是最新的,就是因为Eureka的自我保护模式导致的
3. Eureka本质上是一个工程,而ZooKeeper只是一个进程
4. Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像ZooKeeper 一样使得整个注册系统瘫痪
5. ZooKeeper保证的是CP,Eureka保证的是AP;CAP: C:一致性>Consistency; 取舍:(强一致性、单调一致性、会话一致性、最终一致性、弱一致性) A:可用性>Availability; P:分区容错性>Partition tolerance;
2.gateway网关
1.gateway介绍
Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。
使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。
2.网关的作用是什么?
统一管理微服务请求,权限控制、负载均衡、路由转发、监控、安全控制黑名单和白名单等
3.网关的应用场景有哪些?
对外暴露,权限校验,服务聚合,日志审计等
4.面试补充
1.网关与过滤器有什么区别?
网关是对所有服务的请求进行分析过滤,过滤器是对单个服务而言。
2.如何实现动态gateway网关路由转发
通过path配置拦截请求,通过ServiceId到配置中心获取转发的服务列表,gateway内部使用Ribbon实
现本地负载均衡和转发。
现本地负载均衡和转发。
3.gateway如何实现鉴权检验?
1.客户端携带用户密码登录之后,在服务端进行账号密码的验证。
2.如果登录成功,则根据用户密码生成token,同时将token写入redis缓存,设置过期时间。
3.客户端携带token请求业务系统,经过网关服务,从redis缓存中读取并对token进行验证;
4.token验证成功,则路由到业务系统;验证失败则返回权限校验失败的错误码401;
4.gateway网关如何进行限流?
在 Spring Cloud Gateway 上实现限流是个不错的选择,只需要编写一个过滤器就可以了。Spring Cloud Gateway 已经内置了一个RequestRateLimiterGatewayFilterFactory,我们可以直接使用。目前RequestRateLimiterGatewayFilterFactory的实现依赖于 Redis,所以我们还要引入spring-boot-starter-data-redis-reactive。
在上面的配置文件,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
burstCapacity:令牌桶总容量。
replenishRate:令牌桶每秒填充平均速率。
key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
burstCapacity:令牌桶总容量。
replenishRate:令牌桶每秒填充平均速率。
key-resolver:用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。
可以按照path接口地址限流、可以根据ip来限流或者自定义限流器
配置文件
spring:
cloud:
gateway:
discovery:
locator:
#是否与服务发现组件进行结合,通过serviceId转发到具体实例
#是否开启基于服务发现的路由规则
enabled: true
##表示将请求路径的服务名配置改成小写 ,因为服务注册的时候,向注册中心注册时将服务名转成大写的了
lowerCaseServiceId: true
routes:
- id: after_route
uri: lb://bit-msa-pasm-api
predicates:
- Path=/service/**
filters:
- name: RequestRateLimiter
args:
# 令牌桶每秒填充平均速率
redis-rate-limiter.replenishRate: 1
# 令牌桶的上限
redis-rate-limiter.burstCapacity: 2
# 使用SpEL表达式从Spring容器中获取Bean对象
key-resolver: "#{@pathKeyResolver}"
cloud:
gateway:
discovery:
locator:
#是否与服务发现组件进行结合,通过serviceId转发到具体实例
#是否开启基于服务发现的路由规则
enabled: true
##表示将请求路径的服务名配置改成小写 ,因为服务注册的时候,向注册中心注册时将服务名转成大写的了
lowerCaseServiceId: true
routes:
- id: after_route
uri: lb://bit-msa-pasm-api
predicates:
- Path=/service/**
filters:
- name: RequestRateLimiter
args:
# 令牌桶每秒填充平均速率
redis-rate-limiter.replenishRate: 1
# 令牌桶的上限
redis-rate-limiter.burstCapacity: 2
# 使用SpEL表达式从Spring容器中获取Bean对象
key-resolver: "#{@pathKeyResolver}"
持续高速访问某个路径,速度过快时,返回 HTTP ERROR 429 。
5.gateway网关如何进行黑名单、白名单设置?
自定义全局过滤器实现IP访问限制(黑白名单)
黑名单实际可以去数据库或者redis中查询
思路:获取客户端ip,判断是否在⿊名单中,在的话就拒绝访问,不在的话就放⾏
// 从上下⽂中取出request和response对象
// 从request对象中获取客户端ip
// 拿着clientIp去⿊名单中查询,存在的话就决绝访问
// 从上下⽂中取出request和response对象
// 从request对象中获取客户端ip
// 拿着clientIp去⿊名单中查询,存在的话就决绝访问
3.Ribbon
1.负载平衡的意义什么?
简单来说: 先将集群,集群就是把一个的事情交给多个人去做,假如要做1000个产品给一个人做要10天,我叫10个人做就是一天,这就是集群,负载均衡的话就是用来控制集群,他把做的最多的人让他慢慢做休息会,把做的最少的人让他加量让他做多点。
在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。
2.Ribbon是什么?
Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法
Ribbon客户端组件提供一系列完善的配置项,如连接超时,重试等。简单的说,就是在配置文件
中列出后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随即连接等)去连
接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。(有点类似Nginx)
Ribbon客户端组件提供一系列完善的配置项,如连接超时,重试等。简单的说,就是在配置文件
中列出后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随即连接等)去连
接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。(有点类似Nginx)
3.Nginx与Ribbon的区别?
答:Nginx是反向代理同时可以实现负载均衡,nginx拦截客户端请求采用负载均衡策略根据upstream配置进行转发,相当于请求通过nginx服务器进行转发。Ribbon是客户端负载均衡,从注册中心读取目标服务器信息,然后客户端采用轮询策略对服务直接访问,全程在客户端操作。
4.Ribbon底层实现原理?
答:Ribbon使用discoveryClient从注册中心读取目标服务信息,对同一接口请求进行计数,使用%取余算法获取目标服务集群索引,返回获取到的目标服务信息。
5.@LoadBalanced注解的作用?
答:开启客户端负载均衡
4.Hystrix
1.什么是断路器?
当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应当更多的服务请求到这些资源导致更多的请求等待,发生连锁效应(雪崩效应)
断路器有三种状态:
打开状态:一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象 断路器完全打开 那么下次请求就不会请求到该服务
半开状态:短时间内 有恢复迹象 断路器会将部分请求发给该服务,正常调用时 断路器关闭
关闭状态:当服务一直处于正常状态 能正常调用
打开状态:一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象 断路器完全打开 那么下次请求就不会请求到该服务
半开状态:短时间内 有恢复迹象 断路器会将部分请求发给该服务,正常调用时 断路器关闭
关闭状态:当服务一直处于正常状态 能正常调用
2.什么是 Hystrix?
在分布式系统,我们一定会依赖各种服务,那么这些个服务一定会出现失败的情况,就会导致雪崩,Hystrix就是这样的一个工具,防雪崩利器,它具有服务降级,服务熔断,服务隔离,监控等一些防止雪崩的技术。
Hystrix有四种防雪崩方式:
服务降级:接口调用失败就调用本地的方法返回一个空
服务熔断:接口调用失败就会进入调用接口提前定义好的一个熔断的方法,返回错误信息
服务隔离:隔离服务之间相互影响
服务监控:在服务发生调用时,会将每秒请求数、成功请求数等运行指标记录下来。
3.谈谈服务雪崩效应?
雪崩效应是在大型互联网项目中,当某个服务发生宕机时,调用这个服务的其他服务也会发生宕机,大型项目的微服务之间的调用是互通的,这样就会将服务的不可用逐步扩大到各个其他服务中,从而使整个项目的服务宕机崩溃.发生雪崩效应的原因有以下几点
1.单个服务的代码存在bug. 2请求访问量激增导致服务发生崩溃(如大型商城的枪红包,秒杀功能). 3.服务器的硬件故障也会导致部分服务不可用.
4.在微服务中,如何保护服务?
一般使用使用Hystrix框架,实现服务隔离来避免出现服务的雪崩效应,从而达到保护服务的效果。当微服务中,高并发的数据库访问量导致服务线程阻塞,使单个服务宕机,服务的不可用会蔓延到其他服务,引起整体服务灾难性后果,使用服务降级能有效为不同的服务分配资源,一旦服务不可用则返回友好提示,不占用其他服务资源,从而避免单个服务崩溃引发整体服务的不可用.
5.谈谈服务降级、服务熔断、服务隔离
服务降级:当客户端请求服务器端的时候,防止客户端一直等待,不会处理业务逻辑代码,直接返回一个友好的提示给客户端
服务熔断:是在服务降级的基础上更直接的一种保护方式,当在一个统计时间范围内的请求失败数量达到设定值(requestVolumeThreshold)或当前的请求错误率达到设定的错误率阈值(errorThresholdPercentage)时开启断路,之后的请求直接走fallback方法,在设定时间(sleepWindowInMilliseconds)后尝试恢复。
服务隔离:就是Hystrix为隔离的服务开启一个独立的线程池,这样在高并发的情况下不会影响其他服务。服务隔离有线程池和信号量两种实现方式,一般使用线程池方式
6.服务降级底层是如何实现的?
Hystrix实现服务降级的功能是通过重写HystrixCommand中的getFallback()方法,当Hystrix的run方法或construct执行发生错误时转而执行getFallback()方法。
5.Feign
1.什么是Feign?
Feign 是一个声明web服务客户端,这使得编写web服务客户端更容易
他将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建Http请求了,直接调用接口就行了,不过要注意,调用方法要和本地抽象方法的签名完全一致。
2.SpringCloud有几种调用接口方式?
RestTemplate
Feign
3.Ribbon和Feign调用服务的区别?
1.调用方式同:Ribbon需要我们自己构建Http请求,模拟Http请求然后通过RestTemplate发给其他服务,步骤相当繁琐
2.而Feign则是在Ribbon的基础上进行了一次改进,采用接口的形式,将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建Http请求了,直接调用接口就行了,不过要注意,调用方法要和本地抽象方法的签名完全一致。
6.Config
1.什么是Spring Cloud Config?
Spring Cloud Config为分布式系统中的外部配置提供服务器和客户端支持,可以方便的对微服务各个环境下的配置进行集中式管理。Spring Cloud Config分为Config Server和Config Client两部分。Config Server负责读取配置文件,并且暴露Http API接口,Config Client通过调用ConfigServer的接口来读取配置文件。
2.分布式配置中心有那些框架?
Apollo、zookeeper、springcloud config、nacos
3.
动态变更项目配置信息而不必重新部署项目。
4.SpringCloud Config 可以实现实时刷新吗?
springcloud config实时刷新采用SpringCloud Bus消息总线。
7.Sentinel
子主题
子主题
子主题
14.mybatis
1.mybatis编程步骤是什么样的
1.创建SqlSessionFactory
2.通过SqlSessionFactory创建SqlSession
3.通过sqlsession执行数据库操作
4.调用session.commit()提交事务
5.调用session.close()关闭会话
2.mybatis的mapper底层原理
1.读取mybatis配置文件mybatis-config.xml
2.加载映射文件mapper.xml
3.构造会话工厂sqlsessionfactory
4.创建会话对象sqlsession
5.executor执行器
6.mappedstatement对象
输入
Map、list类型
string、integer等基本类型
POJO类型
输出
Map、list类型
string、integer等基本类型
POJO类型
7.数据库
3.Mybatis都有哪些Executor执行器?它们之间的区别是什么?
●Mybatis有3三种基本的Executor执行器,SimpleExecutor、 ReuseExecutor、 BatchExecutor.
●SimpleExecutor: 每执行一次update或select, 就开启一个Statement对象,用完立刻关闭Statement对象。
●ReuseExecutor: 执行update或select, 以sq|作为key查找Statement对象, 存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。
●BatchExecutor: 执行update (没有select, JDBC批处理不支持select) ,将所有sq|都添加到批处理中(addBatch()) ,等待统-执行(executeBatch()) ,它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐-执行executeBatch()批处理。 与DBC批处理相同。
●SimpleExecutor: 每执行一次update或select, 就开启一个Statement对象,用完立刻关闭Statement对象。
●ReuseExecutor: 执行update或select, 以sq|作为key查找Statement对象, 存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。
●BatchExecutor: 执行update (没有select, JDBC批处理不支持select) ,将所有sq|都添加到批处理中(addBatch()) ,等待统-执行(executeBatch()) ,它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐-执行executeBatch()批处理。 与DBC批处理相同。
4.Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
●Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一, collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true |false.
●它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值, 那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
●当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一 样的。
●它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值, 那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
●当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一 样的。
5.spring二级缓存是什么?
15.分布式
1.分布式锁
1.基于mysql
2.基于zookeeper
子主题
子主题
子主题
子主题
总结:
1.zk通过临时节点,解决掉了死锁的问题,⼀旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会⾃动删除掉,其他客户端⾃动获取锁。
2.zk通过节点排队监听的机制,也实现了阻塞的原理,其实就是个递归在那⽆限等待最⼩节点释放的过程。
3.实现锁的可重⼊也很好实现,可以带上线程信息就可以了,或者机器信息这样的唯⼀标识,获取的时候判断⼀下。zk的集群也是⾼可⽤的,只要半数以上的或者,就可以对外提供服务了。
3.基于redis
2.分布式事务
1.事务的ACID特性
1.原⼦性(Atomicity),可以理解为⼀个事务内的所有操作要么都执⾏,要么都不执⾏。
2.⼀致性(Consistency),可以理解为数据是满⾜完整性约束的,也就是不会存在中间状态的数据,⽐如你账上有400,我账上有100,你给我打200块,此时你账上的钱应该是200,我账上的钱应该是300,不会存在我账上钱加了,你账上钱没扣的中间状态。
3.隔离性(Isolation),指的是多个事务并发执⾏的时候不会互相⼲扰,即⼀个事务内部的数据对于其他事务来说是隔离的。
4.持久性(Durability),指的是⼀个事务完成了之后数据就被永远保存下来,之后的其他操作或故障都不会对事务的结果产⽣影响。
2.什么是分布式事务
分布式事务顾名思义就是要在分布式系统中实现事务,它其实是由多个本地事务组合⽽成。
对于分布式事务⽽⾔⼏乎满⾜不了 ACID,其实对于单机事务⽽⾔⼤部分情况下也没有满⾜ ACID,不然
怎么会有四种隔离级别呢?所以更别说分布在不同数据库或者不同应⽤上的分布式事务了。
对于分布式事务⽽⾔⼏乎满⾜不了 ACID,其实对于单机事务⽽⾔⼤部分情况下也没有满⾜ ACID,不然
怎么会有四种隔离级别呢?所以更别说分布在不同数据库或者不同应⽤上的分布式事务了。
3.分布式事务基础理论
1.CAP理论
CAP是Consistency、Availability、Partition tolerance三个词语的缩写,分别表示-致性、可用性、分区容忍性。
CAP是一个已经被证实的理论: -个分布式系统最多只能同时满足一致性 ( Consistency)、可用性( Availbility )和分区容忍性( Partition tolerance )这三项中的两项。它可以作为我们进行架构设计、技术选型的考量标准。对于多数大型互联网应用的场景,结点众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,而且要保证服务可用性达到N个9 ( 9.99..%) , 并要达到良好的响应性能来提高用户体验,因此-般都会做出如下选择:保证P和A,舍弃C强一致,保证最终-致性。
2.BASE理论
●基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。如,电商网站交易付款出现问题了,商品依然可以正常浏览。
●软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态) ,这个状态不影响系统可用性,如订单的"支付中"、“数据同步中"等状态,待数据最终-致后状态改为"成功”状态。
●最终一致:最终-致是指经过- -段时间后,所有节点数据都将会达到一致。如订单的"支付中"状态,最终会变为”支付成功”或者“支付失败" ,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、 等待。
●软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(也叫软状态) ,这个状态不影响系统可用性,如订单的"支付中"、“数据同步中"等状态,待数据最终-致后状态改为"成功”状态。
●最终一致:最终-致是指经过- -段时间后,所有节点数据都将会达到一致。如订单的"支付中"状态,最终会变为”支付成功”或者“支付失败" ,使订单状态与实际交易结果达成一致,但需要一定时间的延迟、 等待。
4.分布式事务解决方案
1.2PC
1.什么是2pc
2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。
1. 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事 务,并写本地的Undo/Redo日志,此时事务没有提交。 (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数 据文件)
2. 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者 发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操 作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
2.解决方案
1.基于数据库的XA 协议来实现2PC又称为XA方案
总结:整个2PC的事务流程涉及到三个角色AP、RM、TM。AP指的是使用2PC分布式事务的应用程序;RM指的是资 源管理器,它控制着分支事务;TM指的是事务管理器,它控制着整个全局事务。
1.在准备阶段RM执行实际的业务操作,但不提交事务,资源锁定;
2.在提交阶段TM会接受RM在准备阶段的执行回复,只要有任一个RM执行失败,TM会通知所有RM执行回滚操 作,否则,TM将会通知所有RM提交该事务。提交阶段结束资源锁释放
XA方案的问题:
1、需要本地数据库支持XA协议。Oracle、MySQL都支持2PC协议,
2、资源锁需要等到两个阶段结束才释放,性能较差。
2.Seata方案
1.传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作 在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务0侵入的方式解决微服 务场景下面临的分布式事务问题,它目前提供AT模式(即2PC)及TCC模式的分布式事务解决方案。
2.Seata的设计思想如下:
1.Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务 达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务
2.与 传统2PC 的模型类似,Seata定义了3个组件来协议分布式事务的处理过程
1.Transaction Coordinator (TC): 事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运 行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各各分支事务的提交或回滚。
2.Transaction Manager (TM): 事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终 向TC发起全局提交或全局回滚的指令。
3.Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分 支(本地)事务的提交和回滚。
3.Seata实现2PC与传统2PC的差别:
1.架构层次方面,传统2PC方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现,而 Seata的 RM 是以jar包的形式作为中间件层部署在应用程序这一侧的。
2.两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成 才释放。而Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2持锁的时间,整体提高效率。
2.TCC
1.TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作:预处理Try、确认 Confirm、撤销Cancel。Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的 操作即回滚操作。
2.TM首先发起所有的分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所 有分支事务的Cancel操作,若try操作全部成功,TM将会发起所有分支事务的Confirm操作,其中Confirm/Cancel 操作若执行失败,TM会进行重试。
3.TCC分为三个阶段:
1. Try 阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm 一起才能 真正构成一个完整的业务逻辑。
2. Confirm 阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则 认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引 入重试机制或人工处理。
3.Cancel 阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采 用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。
注:TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文, 追踪和记录状态,由于Confirm 和cancel失败需进行重试,因此需要实现幂等,幂等性是指同一个操作无论请求多少次,其结果都相同
4.TCC 解决方案
1.tcc-transaction
2.Hmily
3.ByteTCC
注:Seata也支持TCC,但Seata的TCC模式对Spring Cloud并没有提供支持。
5.TCC需要注意三种异常处理分别是空回滚、幂等、悬挂:
1.空回滚
在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回 滚,然后直接返回成功。
出现原因:是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶 段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。
解决思路:是关键就是要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,如果执行了,那就是正常回 滚;如果没执行,那就是空回滚。前面已经说过TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分 布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 Try 方法里会 插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存 在,则是空回滚
2.幂等
为了保证TCC二阶段提交重试机制不会引发数据不一致,要求 TCC 的二阶段 Try、 Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据 不一致等严重问题
解决思路在上述“分支事务记录”中增加执行状态,每次执行前都查询该状态。
3.悬挂
悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。
出现原因是在 RPC 调用分支事务try时,先注册分支事务,再执行RPC调用,如果此时 RPC 调用的网络发生拥堵, 通常 RPC 调用是有超时时间的,RPC 超时以后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC 请求 才到达参与者真正执行,而一个 Try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预 留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。
解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,“分支事务记录”表中是否已经有二阶段事务记录,如果有则不执行Try。
6.小结:TCC与2PC的比较
如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处 理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使 得降低锁冲突、提高吞吐量成为可能。
而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此 外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。
3.可靠消息最终一致性
1.方案:可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能 够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
2.此方案是利用消息中间件完成,如下图: 事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件 之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事 务问题
3.可靠消息最终一致性方案要解决以下几个问题:
1.本地事务与消息发送的原子性问题
事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实 现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最 终一致性方案的关键问题
先发送消息,再操作数据库,这种情况下无法保证数据库操作与发送消息的一致性,因为可能发送消息成功,数据库操作失败。
先进行数据库操作,再发送消息,这种情况下貌似没有问题,如果发送MQ消息失败,就会抛出异常,导致数据库事务回滚。但如果是超时异常,数 据库回滚,但MQ其实已经正常发送了,同样会导致不一致。
2.事务参与方接收消息的可靠性
事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。
3.消息重复消费的问题
由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重 复消费。 要解决消息重复消费的问题就要实现事务参与方的方法幂等性
4.解决方案:
1.本地消息表方案
本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后 通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
举例:
1、用户注册 用户服务在本地事务新增用户和增加 ”积分消息日志“。(用户表和消息表通过本地事务保证一致)
下边是伪代码
begin transaction;
//1.新增用户
//2.存储积分消息日志
commit transation;
这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原 子性。
下边是伪代码
begin transaction;
//1.新增用户
//2.存储积分消息日志
commit transation;
这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原 子性。
2、定时任务扫描日志:如何保证将消息发送给消息队列呢? 经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息 中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试
3、消费消息 如何保证消费者一定能消费到消息呢? 这里可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ 发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重 试向消费者来发送消息。 积分服务接收到”增加积分“消息,开始增加积分,积分增加成功后向消息中间件回应ack,否则消息中间件将重复 投递此消息。 由于消息会重复投递,积分服务的”增加积分“功能需要实现幂等性。
2.RocketMQ事务消息方案
RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题,RocketMQ 的 设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ 本身提供的存储机制为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系 统发生异常时依然能够保证达成事务的最终一致性。
在RocketMQ 4.3后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ 内部,解决 Producer 端的消息发送与本地事务执行的原子性问题。
4.最大努力通知
最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理业 务失败,在接收通知方收到通知后积极进行失败处理,无论发起通知方如何处理结果都会不影响到接收通知方的后 续处理;发起通知方需提供查询执行情况接口,用于接收通知方校对结果。典型的使用场景:银行通知、支付结果 通知等。
11.总结
2PC 和 3PC 是⼀种强⼀致性事务,不过还是有数据不⼀致,阻塞等⻛险,⽽且只能⽤在数据库层⾯。
TCC 是⼀种补偿性事务思想,适⽤的范围更⼴,在业务层⾯实现,因此对业务的侵⼊性较⼤,每⼀个操作都需要实现对应的三个⽅法。
本地消息、事务消息和最⼤努⼒通知其实都是最终⼀致性事务,因此适⽤于⼀些对时间不敏感的业务。
在条件允许的情况下,我们尽可能选择本地事务单数据源,因为它减少了网络交互带来的性能损耗,且避免了数据 弱一致性带来的种种问题。若某系统频繁且不合理的使用分布式事务,应首先从整体设计角度观察服务的拆分是否 合理,是否高内聚低耦合?是否粒度太小?分布式事务一直是业界难题,因为网络的不确定性,而且我们习惯于拿 分布式事务与单机事务ACID做对比。 无论是数据库层的XA、还是应用层TCC、可靠消息、最大努力通知等方案,都没有完美解决分布式事务问题,它们 不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。
16.elasticsearch
1.基础知识
1.注意:9300 端口为 Elasticsearch 集群间组件的通信端口,9200 端口为浏览器访问的 http协议 RESTful 端口;打开浏览器(推荐使用谷歌浏览器),输入地址:http://localhost:9200,测试结果
2.最新版本为8.1.2,生产使用7.8
3.ES 里的 Index 可以看做一个库,而 Types 相当于表,Documents 则相当于表的行,mapping就是表的数据结构。这里 Types 的概念已经被逐渐弱化,Elasticsearch 6.X 中,一个 index 下已经只能包含一个type,Elasticsearch 7.X 中, Type 的概念已经被删除了。
4.用 JSON 作为文档序列化的格式,比如一条用户信息:
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"birthDate": "1990/05/01",
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
{
"name" : "John",
"sex" : "Male",
"age" : 25,
"birthDate": "1990/05/01",
"about" : "I love to go rock climbing",
"interests": [ "sports", "music" ]
}
5.通过 elasticsearch-head 插件查看集群情况
2.索引操作
1.创建索引
1.对比关系型数据库,创建索引就等同于创建数据库
2.在 Postman 中,向 ES 服务器发 PUT 请求 :http://127.0.0.1:9200/shopping。shopping为索引名称
3.注意:创建索引库的分片数默认 1 片,在 7.0.0 之前的 Elasticsearch 版本中,默认 5 片如果重复添加索引,会返回错误信息
2.查看所有索引
1.在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/_cat/indices?v
2.这里请求路径中的_cat 表示查看的意思,indices 表示索引,所以整体含义就是查看当前 ES服务器中的所有索引,就好像 MySQL 中的 show tables 的感觉
3.查看单个索引
1.在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/shopping、shopping为索引名称
2.查看索引向 ES 服务器发送的请求路径和创建索引是一致的。但是 HTTP 方法不一致。创建为put,查看为get
4.删除索引
1.在 Postman 中,向 ES 服务器发 DELETE 请求 :http://127.0.0.1:9200/shopping
3.文档操作
1.创建文档
1.这里的文档可以类比为关系型数据库中的表数据,添加的数据格式为 JSON 格式
2.在 Postman 中,向 ES 服务器发 POST 请求 :http://127.0.0.1:9200/shopping/_doc
{"title":"小米手机",
"category":"小米",
"images":"http://www.gulixueyuan.com/xm.jpg",
"price":3999.00
}此处发送请求的方式必须为 POST,不能是 PUT,否则会发生错误
{"title":"小米手机",
"category":"小米",
"images":"http://www.gulixueyuan.com/xm.jpg",
"price":3999.00
}此处发送请求的方式必须为 POST,不能是 PUT,否则会发生错误
3.上面的数据创建后,由于没有指定数据唯一性标识(ID),默认情况下,ES 服务器会随机生成一个。如果想要自定义唯一性标识,需要在创建时指定:http://127.0.0.1:9200/shopping/_doc/1
2.查看文档
1.查看文档时,需要指明文档的唯一性标识,类似于 MySQL 中数据的主键查询
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/shopping/_doc/1
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/shopping/_doc/1
3.修改文档
1.和新增文档一样,输入相同的 URL 地址请求,如果请求体变化,会将原有的数据内容覆盖
在 Postman 中,向 ES 服务器发 POST 请求 :http://127.0.0.1:9200/shopping/_doc/1
在 Postman 中,向 ES 服务器发 POST 请求 :http://127.0.0.1:9200/shopping/_doc/1
4.修改字段
1.修改数据时,也可以只修改某一给条数据的局部信息
2.在 Postman 中,向 ES 服务器发 POST 请求 :http://127.0.0.1:9200/shopping/_update/1
{
"doc": {
"price":3000.00
}
}
{
"doc": {
"price":3000.00
}
}
5.删除文档
1.删除一个文档不会立即从磁盘上移除,它只是被标记成已删除(逻辑删除)。
在 Postman 中,向 ES 服务器发 DELETE 请求 :http://127.0.0.1:9200/shopping/_doc/1
在 Postman 中,向 ES 服务器发 DELETE 请求 :http://127.0.0.1:9200/shopping/_doc/1
2.一般删除数据都是根据文档的唯一性标识进行删除,实际操作时,也可以根据条件对多条数据进行删除
3.向 ES 服务器发 POST 请求 :http://127.0.0.1:9200/shopping/_delete_by_query
{
"query":{
"match":{
"price":4000.00
}
} }
{
"query":{
"match":{
"price":4000.00
}
} }
4.映射操作mapping
1.有了索引库,等于有了数据库中的 database。接下来就需要建索引库(index)中的映射了,类似于数据库(database)中的表结构(table)。
创建数据库表需要设置字段名称,类型,长度,约束等;索引库也一样,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做映射(mapping)
创建数据库表需要设置字段名称,类型,长度,约束等;索引库也一样,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做映射(mapping)
2.创建映射
在 Postman 中,向 ES 服务器发 PUT 请求 :http://127.0.0.1:9200/student/_mapping
{
"properties": {
"name":{
"type": "text",
"index": true
},
"sex":{
"type": "text",
"index": false
},
"age":{
"type": "long",
"index": false
}
} }
{
"properties": {
"name":{
"type": "text",
"index": true
},
"sex":{
"type": "text",
"index": false
},
"age":{
"type": "long",
"index": false
}
} }
3.映射数据说明:
1.字段名:任意填写,下面指定许多属性,例如:title、subtitle、images、price
2.type:类型,Elasticsearch 中支持的数据类型非常丰富,说几个关键的:
String 类型,又分两种:text:可分词、keyword:不可分词,数据会作为完整字段进行匹配
Numerical:数值类型,分两类
基本数据类型:long、integer、short、byte、double、float、half_float
浮点数的高精度类型:scaled_float
基本数据类型:long、integer、short、byte、double、float、half_float
浮点数的高精度类型:scaled_float
Date:日期类型
Array:数组类型
Object:对象
3.index:是否索引,默认为 true,也就是说你不进行任何配置,所有字段都会被索引。
true:字段会被索引,则可以用来进行搜索
false:字段不会被索引,不能用来搜索
true:字段会被索引,则可以用来进行搜索
false:字段不会被索引,不能用来搜索
4.store:是否将数据进行独立存储,默认为 false。原始的文本会存储在_source 里面,默认情况下其他提取出来的字段都不是独立存储
的,是从_source 里面提取出来的。当然你也可以独立的存储某个字段,只要设置"store": true 即可,获取独立存储的字段要比从_source 中解析快得多,但是也会占用更多的空间,所以要根据实际业务需求来设置
的,是从_source 里面提取出来的。当然你也可以独立的存储某个字段,只要设置"store": true 即可,获取独立存储的字段要比从_source 中解析快得多,但是也会占用更多的空间,所以要根据实际业务需求来设置
5.analyzer:分词器,这里的 ik_max_word 即使用 ik 分词器
4.查看映射
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_mapping
5.高级查询
1.查询所有文档
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match_all": {}
} }
# "query":这里的 query 代表一个查询对象,里面可以有不同的查询属性
# "match_all":查询类型,例如:match_all(代表查询所有), match,term , range 等等
# {查询条件}:查询条件会根据类型的不同,写法也有差异
{
"query": {
"match_all": {}
} }
# "query":这里的 query 代表一个查询对象,里面可以有不同的查询属性
# "match_all":查询类型,例如:match_all(代表查询所有), match,term , range 等等
# {查询条件}:查询条件会根据类型的不同,写法也有差异
2.匹配查询
match 匹配类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是 or 的关系
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name":"zhangsan"
}
} }
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name":"zhangsan"
}
} }
3.字段匹配查询
multi_match 与 match 类似,不同的是它可以在多个字段中查询。
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"multi_match": {
"query": "zhangsan",
"fields": ["name","nickname"]
}
} }
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"multi_match": {
"query": "zhangsan",
"fields": ["name","nickname"]
}
} }
4.关键字精确查询
term 查询,精确的关键词匹配查询,不对查询条件进行分词。
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"term": {
"name": {
"value": "zhangsan"
}
}
} }
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"term": {
"name": {
"value": "zhangsan"
}
}
} }
5.多关键字精确查询
terms 查询和 term 查询一样,但它允许你指定多值进行匹配。
如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件,类似于 mysql 的 in
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"terms": {
"name": ["zhangsan","lisi"]
}
} }
如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件,类似于 mysql 的 in
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"terms": {
"name": ["zhangsan","lisi"]
}
} }
6.指定字段查询
默认情况下,Elasticsearch 在搜索的结果中,会把文档中保存在_source 的所有字段都返回。
如果我们只想获取其中的部分字段,我们可以添加_source 的过滤
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"_source": ["name","nickname"],
"query": {
"terms": {
"nickname": ["zhangsan"]
}
} }
如果我们只想获取其中的部分字段,我们可以添加_source 的过滤
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"_source": ["name","nickname"],
"query": {
"terms": {
"nickname": ["zhangsan"]
}
} }
7.过滤字段
includes:来指定想要显示的字段
excludes:来指定不想要显示的字段
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"_source": {
"includes": ["name","nickname"]
},
"query": {
"terms": {
"nickname": ["zhangsan"]
}
} }
excludes:来指定不想要显示的字段
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"_source": {
"includes": ["name","nickname"]
},
"query": {
"terms": {
"nickname": ["zhangsan"]
}
} }
8.组合查询
`bool`把各种其它查询通过`must`(必须 )、`must_not`(必须不)、`should`(应该)的方
式进行组合
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "zhangsan"
}
}
],
"must_not": [
{
"match": {
"age": "40"
}
}
],
"should": [
{
"match": {
"sex": "男"
}
}
]
}
} }
式进行组合
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "zhangsan"
}
}
],
"must_not": [
{
"match": {
"age": "40"
}
}
],
"should": [
{
"match": {
"sex": "男"
}
}
]
}
} }
9.范围查询
range 查询找出那些落在指定区间内的数字或者时间。
10.模糊查询
返回包含与搜索字词相似的字词的文档。
编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括:
更改字符(box → fox) 删除字符(black → lack)
插入字符(sic → sick) 转置两个相邻字符(act → cat)
为了找到相似的术语,fuzzy 查询会在指定的编辑距离内创建一组搜索词的所有可能的变体
或扩展。然后查询返回每个扩展的完全匹配。
通过 fuzziness 修改编辑距离。一般使用默认值 AUTO,根据术语的长度生成编辑距离。
编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括:
更改字符(box → fox) 删除字符(black → lack)
插入字符(sic → sick) 转置两个相邻字符(act → cat)
为了找到相似的术语,fuzzy 查询会在指定的编辑距离内创建一组搜索词的所有可能的变体
或扩展。然后查询返回每个扩展的完全匹配。
通过 fuzziness 修改编辑距离。一般使用默认值 AUTO,根据术语的长度生成编辑距离。
11.单字段排序
sort 可以让我们按照不同的字段进行排序,并且通过 order 指定排序的方式。desc 降序,asc
升序。
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name":"zhangsan"
}
},
"sort": [{
"age": {
"order":"desc"
}
}]
}
升序。
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name":"zhangsan"
}
},
"sort": [{
"age": {
"order":"desc"
}
}]
}
12.多字段排序
假定我们想要结合使用 age 和 _score 进行查询,并且匹配的结果首先按照年龄排序,然后
按照相关性得分排序
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
},
{
"_score":{
"order": "desc"
}
}
] }
按照相关性得分排序
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
},
{
"_score":{
"order": "desc"
}
}
] }
13.高亮查询
在进行关键字搜索时,搜索出的内容中的关键字会显示不同的颜色,称之为高亮。
Elasticsearch 可以对查询内容中的关键字部分,进行标签和样式(高亮)的设置。
在使用 match 查询的同时,加上一个 highlight 属性:
pre_tags:前置标签
post_tags:后置标签
fields:需要高亮的字段
title:这里声明 title 字段需要高亮,后面可以为这个字段设置特有配置,也可以空
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name": "zhangsan"
}
},
"highlight": {
"pre_tags": "<font color='red'>",
"post_tags": "</font>",
"fields": {
"name": {}
}
} }
在使用 match 查询的同时,加上一个 highlight 属性:
pre_tags:前置标签
post_tags:后置标签
fields:需要高亮的字段
title:这里声明 title 字段需要高亮,后面可以为这个字段设置特有配置,也可以空
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match": {
"name": "zhangsan"
}
},
"highlight": {
"pre_tags": "<font color='red'>",
"post_tags": "</font>",
"fields": {
"name": {}
}
} }
14.分页查询
from:当前页的起始索引,默认从 0 开始。 from = (pageNum - 1) * size
size:每页显示多少条
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
}
],
"from": 0,
"size": 2
}
size:每页显示多少条
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"age": {
"order": "desc"
}
}
],
"from": 0,
"size": 2
}
15.聚合查询
聚合允许使用者对 es 文档进行统计分析,类似与关系型数据库中的 group by,当然还有很
多其他的聚合,例如取最大值、平均值等等。
对某个字段取最大值 max
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
多其他的聚合,例如取最大值、平均值等等。
对某个字段取最大值 max
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
6.es集群
1.单机&集群
1.配置服务器集群时,集群中节点数量没有限制,大于等于 2 个节点就可以看做是集群了。一般出于高性能及高可用方面来考虑集群中节点数量都是 3 个以上。
2.除了负载能力,单点服务器也存在其他问题:单台机器存储容量有限、单服务器容易出现单点故障,无法实现高可用、单服务的并发处理能力有限
2.集群cluster
一个集群就是由一个或多个服务器节点组织在一起,共同持有整个的数据,并一起提供索引和搜索功能。一个 Elasticsearch 集群有一个唯一的名字标识,这个名字默认就是”elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。
3.节点node
集群中包含很多服务器,一个节点就是其中的一个服务器。作为集群的一部分,它存储数据,参与集群的索引和搜索功能
在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何 Elasticsearch 节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”的集群。
7.es面试题
1.为什么要使用 Elasticsearch?
系统中的数据,随着业务的发展,时间的推移,将会非常多,而业务中往往采用模糊查询进行数据的搜索,而模糊查询会导致查询引擎放弃索引,导致系统查询数据时都是全表扫描,在百万级别的数据库中,查询效率是非常低下的,而我们使用 ES 做一个全文索引,将经常查询的系统功能的某些字段,比如说电商系统的商品表中商品名,描述、价格还有 id 这些字段我们放入 ES 索引库里,可以提高查询速度
2.Elasticsearch 的 master 选举流程?
1.Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个 RPC 来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要 ping 通)这两部分
2.对所有可以成为 master 的节点(node.master: true)根据 nodeId 字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第 0 位)节点,暂且认为它是 master 节点。
3.如果对某个节点的投票数达到一定的值(可以成为 master 节点数 n/2+1)并且该节点自己也选举自己,那这个节点就是 master。否则重新选举一直到满足上述条件。
4.master 节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data 节点可以关闭 http功能。
3.Elasticsearch 集群脑裂问题?
1.脑裂”问题可能的成因:
1.网络问题:集群间的网络延迟导致一些节点访问不到 master,认为 master 挂掉了从而选举出新的master,并对 master 上的分片和副本标红,分配新的主分片
2.节点负载:主节点的角色既为 master 又为 data,访问量较大时可能会导致 ES 停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
3.内存回收:data 节点上的 ES 进程占用的内存较大,引发 JVM 的大规模内存回收,造成 ES 进程失去响应。
2.脑裂问题解决方案:
1.减少误判:discovery.zen.ping_timeout 节点状态的响应时间,默认为 3s,可以适当调大,如果 master在该响应时间的范围内没有做出响应应答,判断该节点已经挂掉了。调大参数(如 6s,discovery.zen.ping_timeout:6),可适当减少误判。
2.选举触发: discovery.zen.minimum_master_nodes:1。该参数是用于控制选举行为发生的最小集群主节点数量。当备选主节点的个数大于等于该参数的值,且备选主节点中有该参数个节点认为主节点挂了,进行选举。官方建议为(n/2)+1,n 为主节点个数(即有资格成为主节点的节点个数)
3.角色分离:即 master 节点与 data 节点分离,限制角色,主节点配置为:node.master: true node.data: false从节点配置为:node.master: false node.data: true
4.Elasticsearch 中的倒排索引是什么?
倒排索引是搜索引擎的核心。搜索引擎的主要目标是在查找发生搜索条件的文档时提供快速搜索。ES中的倒排索引其实就是 lucene 的倒排索引,区别于传统的正向索引,倒排索引会再存储数据时将关键词和数据进行关联,保存到倒排表中,然后查询时,将查询内容进行分词后在倒排表中进行查询,最后匹配数据即可。
5.Elasticsearch 中的集群、节点、索引、文档、类型是什么?
集群是一个或多个节点(服务器)的集合,它们共同保存您的整个数据,并提供跨所有节点的联合索
引和搜索功能。群集由唯一名称标识,默认情况下为“elasticsearch”。此名称很重要,因为如果节点设
置为按名称加入群集,则该节点只能是群集的一部分。
引和搜索功能。群集由唯一名称标识,默认情况下为“elasticsearch”。此名称很重要,因为如果节点设
置为按名称加入群集,则该节点只能是群集的一部分。
节点是属于集群一部分的单个服务器。它存储数据并参与群集索引和搜索功能。
索引就像关系数据库中的“数据库”。它有一个定义多种类型的映射。索引是逻辑名称空间,映射到一
个或多个主分片,并且可以有零个或多个副本分片。 MySQL =>数据库 Elasticsearch =>索引
个或多个主分片,并且可以有零个或多个副本分片。 MySQL =>数据库 Elasticsearch =>索引
文档类似于关系数据库中的一行。不同之处在于索引中的每个文档可以具有不同的结构(字段),但
是对于通用字段应该具有相同的数据类型。 MySQL => Databases => Tables => Columns / Rows
Elasticsearch => Indices => Types =>具有属性的文档
是对于通用字段应该具有相同的数据类型。 MySQL => Databases => Tables => Columns / Rows
Elasticsearch => Indices => Types =>具有属性的文档
类型是索引的逻辑类别/分区,其语义完全取决于用户。
17.xxljob
1.框架系统组成
1.调度中心:负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。
2.执行模块(执行器):负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;接收“调度中心”的执行请求、终止请求和日志请求等。
注:生产中使用xxl-job2.2.0版本。使用注解:@XxlJob("userStatisticsTiming")
2.问题总结
1.如何避免任务重复执行
调度密集或者耗时任务可能会导致任务阻塞,集群情况下调度组件小概率情况下会重复触发;
针对上述情况,可以通过结合 “单机路由策略(如:第一台、一致性哈希)” + “阻塞策略(如:单机串行、丢弃后续调度)” 来规避,最终避免任务重复执行。
针对上述情况,可以通过结合 “单机路由策略(如:第一台、一致性哈希)” + “阻塞策略(如:单机串行、丢弃后续调度)” 来规避,最终避免任务重复执行。
2.分片任务:一个任务在多台服务器上同时都执行,降低任务处理时间,调度器会调用配置的所有机器
3.执行器用的端口和该执行器本身的端口没有关系,在启动时可以指定执行器端口,xxljob会自己启一个服务用于调度,如果不指定该端口,默认为9999
3.遇到的问题
4.1 老版本自有bug,句柄数过多导致任务调度失败,修改源码修复
老版本GULE(shell)模式,调用远程接口时,打开连接,没有关闭资源,随着任务的执行,未关闭的句柄数越来越多,最高为65535,达到后就无法继续调度任务,重启可以解决,但是每隔一段时间就会出现该问题。
4.2 任务重复执行,有可能的原因,
1.tigger重复调度(老版本使用quartz,quartz本身bug,单台机器也可能出现重复调度)。新版本摒弃quartz,自己解析cron表达式,计算下次执行时间,单机不会重复调度;集群使用行锁避免重复调度(需要mysql的引擎为InnoDB,myisam不支持行锁)
2.glue模式,curl调用定时任务接口,会走域名,走ng,ng默认配置请求服务器发生错误或者超时,会尝试调用别的机器,导致重复调用;解决方式,使用BEAN模式配置定时任务
老版本GULE(shell)模式,调用远程接口时,打开连接,没有关闭资源,随着任务的执行,未关闭的句柄数越来越多,最高为65535,达到后就无法继续调度任务,重启可以解决,但是每隔一段时间就会出现该问题。
4.2 任务重复执行,有可能的原因,
1.tigger重复调度(老版本使用quartz,quartz本身bug,单台机器也可能出现重复调度)。新版本摒弃quartz,自己解析cron表达式,计算下次执行时间,单机不会重复调度;集群使用行锁避免重复调度(需要mysql的引擎为InnoDB,myisam不支持行锁)
2.glue模式,curl调用定时任务接口,会走域名,走ng,ng默认配置请求服务器发生错误或者超时,会尝试调用别的机器,导致重复调用;解决方式,使用BEAN模式配置定时任务
4.新版本特性
5.1 自己解析cron表达式,自己计算下次调度时间
5.2 调度策略:
触发任务(tigger)使用线程池,维护了两个线程池(快、慢),正常调度都会走快的线程池(默认 core:10 max:200 queue:1000 丢弃策略:默认,抛出异常),当有任务调度时间超过500ms,并且出现10次,则会将该任务放入慢的线程池,目的是为了不影响其他任务调度
5.2 调度策略:
触发任务(tigger)使用线程池,维护了两个线程池(快、慢),正常调度都会走快的线程池(默认 core:10 max:200 queue:1000 丢弃策略:默认,抛出异常),当有任务调度时间超过500ms,并且出现10次,则会将该任务放入慢的线程池,目的是为了不影响其他任务调度
5.我们做的改造
1.丰富告警通道
2.加入prometheus埋点,记录不同时刻所有的调度任务数
3.调度线程池满,默认会抛出异常丢弃任务,我们捕获异常,发送告警
4.勾子机制,发布重启时先让线程池中的任务执行完,再关闭服务
7.注册和销毁
没有使用zk,而是使用了DB,每30s将注册信息写入到DB,admin每30s读取数据库,获取当前可用的执行器;执行器关闭时会调用destory方法,更新数据库,删除对应的记录
2.加入prometheus埋点,记录不同时刻所有的调度任务数
3.调度线程池满,默认会抛出异常丢弃任务,我们捕获异常,发送告警
4.勾子机制,发布重启时先让线程池中的任务执行完,再关闭服务
7.注册和销毁
没有使用zk,而是使用了DB,每30s将注册信息写入到DB,admin每30s读取数据库,获取当前可用的执行器;执行器关闭时会调用destory方法,更新数据库,删除对应的记录
6.一致性保证
为了避免多个服务器同时调度任务, 通过mysql悲观锁实现分布式锁(for update语句)
1 setAutoCommit(false)关闭隐式自动提交事务,
2 启动事务select lock for update(排他锁)
3 读db任务信息 -> 拉任务到内存时间轮 -> 更新db任务信息
4 commit提交事务,同时会释放for update的排他锁(悲观锁)
任务处理完毕后,释放悲观锁,准备等待下一次循环。
2 启动事务select lock for update(排他锁)
3 读db任务信息 -> 拉任务到内存时间轮 -> 更新db任务信息
4 commit提交事务,同时会释放for update的排他锁(悲观锁)
任务处理完毕后,释放悲观锁,准备等待下一次循环。
7.如何触发
1.早期:基于quartz.现在:timewheel时间轮,这个时间轮本质就是一个Map<Integer, List>
2.触发算法:
拿到了距now 5秒内的任务列表数据:scheduleList,分三种情况处理:for循环遍历scheduleList集合
(1)对到达now时间后的任务:(任务下一次触发时间+5s<now):直接跳过不执行; 重置trigger_next_time;
(2)对到达now时间后的任务:(任务下一次触发时间<now<任务下一次触发时间+5s):线程执行触发逻辑; 若任务下一次触发时间是在5秒内, 则放到时间轮内(Map<Integer, List> 秒数(1-60) => 任务id列表);再 重置trigger_next_time
(3)对未到达now时间的任务(任务下一次触发时间>now):直接放到时间轮内;重置trigger_next_time 。
拿到了距now 5秒内的任务列表数据:scheduleList,分三种情况处理:for循环遍历scheduleList集合
(1)对到达now时间后的任务:(任务下一次触发时间+5s<now):直接跳过不执行; 重置trigger_next_time;
(2)对到达now时间后的任务:(任务下一次触发时间<now<任务下一次触发时间+5s):线程执行触发逻辑; 若任务下一次触发时间是在5秒内, 则放到时间轮内(Map<Integer, List> 秒数(1-60) => 任务id列表);再 重置trigger_next_time
(3)对未到达now时间的任务(任务下一次触发时间>now):直接放到时间轮内;重置trigger_next_time 。
18.docker
1.Dockerfile的构建过程
(1) docker从基础镜像运行一个容器
(2)执行一条指令并对容器作出修改
(3)执行类似docker commit的操作提交一个 新的镜像层
(4) docker再基 于刚提交的镜像运行个新容器
(5)执行dockerfile中的下一 条指令 直到所有指令都执行完成
(2)执行一条指令并对容器作出修改
(3)执行类似docker commit的操作提交一个 新的镜像层
(4) docker再基 于刚提交的镜像运行个新容器
(5)执行dockerfile中的下一 条指令 直到所有指令都执行完成
2.docker组成
从应用软件的角度来看,Dockerfile、Docker镜像与Docker容器分别代表软件的三个不同阶段,
* Dockerfile是软件的原材料
* Docker镜像是软件的交付品
* Docker容器则可以认为是软件的运行态。
Dockerfile面向开发,Docker镜像成为交付标准,Docker容器则涉及部署与运维,三者缺一不可,合力充当Docker体系的基石。
* Dockerfile是软件的原材料
* Docker镜像是软件的交付品
* Docker容器则可以认为是软件的运行态。
Dockerfile面向开发,Docker镜像成为交付标准,Docker容器则涉及部署与运维,三者缺一不可,合力充当Docker体系的基石。
1.Dockerfile,需要定义一个Dockerfile,Dockerfile定义了进程需要的一切东西。Dockerfile涉及的内容包括执行代码或者是文件、环境变量、依赖包、运行时环境、动态链接库、操作系统的发行版、服务进程和内核进程(当应用进程需要和系统服务和内核进程打交道,这时需要考虑如何设计namespace的权限控制)等等;
2.Docker镜像,在用Dockerfile定义一个文件之后,docker build时会产生一个Docker镜像,当运行 Docker镜像时,会真正开始提供服务;
3.Docker容器,容器是直接提供服务的。
19.项目场景梳理
1.电商项目技术架构常见问题场景
1.如何避免重复下单
1.问题描述:用户快速点了两次 “提交订单” 按钮,浏览器会向后端发送两条创建订单的请求,最终会创建两条一模一样的订单
2.解决方案:
解决方案就是采用幂等机制,多次请求和一次请求产生的效果是一样的
1.解决方案就是采用幂等机制,多次请求和一次请求产生的效果是一样的
2.前端通过js脚本控制,无法解决用户刷新提交的请求。另外也无法解决恶意提交。
不建议采用该方案,如果想用,也只是作为一个补充方案。
不建议采用该方案,如果想用,也只是作为一个补充方案。
3.前后约定附加参数校验。
当用户点击购买按钮时,渲染下单页面,展示商品、收货地址、运费、价格等信息,同时页面会埋上Token 信息,用户提交订单时,后端业务逻辑会校验token,有且匹配才认为是合理请求。
注意:同一个 Token 只能用一次,用完后立马失效掉。
当用户点击购买按钮时,渲染下单页面,展示商品、收货地址、运费、价格等信息,同时页面会埋上Token 信息,用户提交订单时,后端业务逻辑会校验token,有且匹配才认为是合理请求。
注意:同一个 Token 只能用一次,用完后立马失效掉。
2.订单快照,减少存储成本
商品信息是可以修改的,当用户下单后,为了更好解决后面可能存在的买卖纠纷,创建订单时会同步保存一份商品详情信息,称之为订单快照。
同一件商品,会有很多用户会购买,如果热销商品,短时间就会有上万的订单。如果每个订单都创建一份快照,存储成本太高。另外商品信息虽然支持修改,但毕竟是一个低频动作。我们可以理解成,大部分订单的商品快照信息都是一样的,除非下单时用户修改过。
如何实时识别修改动作是解决快照成本的关键所在。我们采用摘要比对的方法。创建订单时,先检查商品信息摘要是否已经存在,如果不存在,会创建快照记录。订单明细会关联商品的快照主键
由于订单快照属于非核心操作,即使失败也不应该影响用户正常购买流程,所以通常采用异步流程执行
3.购物车,混合存储,未登录时临时购物车
购物车是电商系统的标配功能,暂存用户想要购买的商品。分为添加商品、列表查看、结算下单三个动作。
技术设计并不是特别复杂,存储的信息也相对有限(用户id、商品id、sku_id、数量、添加时间)。这里特别拿出来单讲主要是用户体验层面要注意几个问题:添加购物车时,后端校验用户未登录,常规思路,引导用户跳转登录页,待登录成功后,再添加购物车。多了一步操作,给用户一种强迫的感觉,体验会比较差。有没有更好的方式?如果细心体验京东、淘宝等大平台,你会发现即使未登录态也可以添加购物车,这到底是怎么实现的?
其实原理并不复杂,服务端这边在用户登录态校验时,做了分支路由,当用户未登录时,会创建一个临时Token,作为用户的唯一标识,购物车数据挂载在该Token下,为了避免购物车数据相互影响以及设计的复杂度,这里会有一个临时购物车表。当然,临时购物车表的数据量并不会太大,why?用户不会一直闲着添加购物车玩,当用户登录后,查看自己的购物车,服务端会从请求的cookie里查找购物车Token标识,并查询临时购物车表是否有数据,然后合并到正式购物车表里。
特别说明:
临时购物车是不是一定要在服务端存储?未必。
有架构师倾向前置存储,将数据存储在浏览器或者APP LocalStorage,这部分数据毕竟不是共享的,但是不太好的增加了设计的复杂度。
1.客户端需要借助本地数据索引,远程请求查完整信息
2.如果是登录态,还要增加数据合并逻辑
考虑到这两部分数据只是用户标识的差异性,所以作者还是建议统一存到服务端,日后即使业务逻辑变更,只需要改一处就可以了,毕竟自运营系统,良好的可维护性也需要我们非常关注的。
临时购物车是不是一定要在服务端存储?未必。
有架构师倾向前置存储,将数据存储在浏览器或者APP LocalStorage,这部分数据毕竟不是共享的,但是不太好的增加了设计的复杂度。
1.客户端需要借助本地数据索引,远程请求查完整信息
2.如果是登录态,还要增加数据合并逻辑
考虑到这两部分数据只是用户标识的差异性,所以作者还是建议统一存到服务端,日后即使业务逻辑变更,只需要改一处就可以了,毕竟自运营系统,良好的可维护性也需要我们非常关注的。
4.库存超卖
1.常见的库存扣减方式
1.下单减库存:即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。
2.付款减库存:即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
3.付款减库存:即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
注:至于采用哪一种减库存方式更多是业务层面的考虑,减库存最核心的是大并发请求时保证数据库中的库存字段值不能为负数。
2.解决方案:
1.至于采用哪一种减库存方式更多是业务层面的考虑,减库存最核心的是大并发请求时保证数据库中的库存字段值不能为负数。update ... set amount = amount - 1 where id = $id and amount - 1 >=0
2.设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时 SQL 语句会报错
3.分布式锁
5.商家卖货,物流单更新ABA问题
举个例子:商家发货,填写运单号,开始填了 123,后来发现填错了,然后又修改为 456。此时,如果就为某种特殊场景埋下错误伏笔,具体我们来看下:
过程:
1.开始「请求A」发货,调订单服务接口,更新运单号 123
2.但是响应有点慢,超时了
3.此时,商家发现运单号填错了,发起了「请求B」,更新运单号为 456 ,订单服务也响应成功了
4.这时,「请求A」触发了重试,再次调用订单服务,更新运单号 123,订单服务也响应成功了
5.订单服务最后保存的 运单号 是 123
1.开始「请求A」发货,调订单服务接口,更新运单号 123
2.但是响应有点慢,超时了
3.此时,商家发现运单号填错了,发起了「请求B」,更新运单号为 456 ,订单服务也响应成功了
4.这时,「请求A」触发了重试,再次调用订单服务,更新运单号 123,订单服务也响应成功了
5.订单服务最后保存的 运单号 是 123
解决方案:
很多人可能会说,不重试不就可以了,要知道重试机制 是高可用服务的重要保障手段,很多重试是框架自动发起的。
数据库表引入一个额外字段 version,每次更新时,判断表中的版本号与请求参数携带的版本号是否一致;
一致:才触发更新.
不一致:说明这期间执行过数据更新,可能会引发错误,拒绝执行。
一致:才触发更新.
不一致:说明这期间执行过数据更新,可能会引发错误,拒绝执行。
6.账户余额更新,保证事务
用户支付,我们要从买家账户减掉一定金额,再往卖家增加一定金额,为了保证数据的完整性、可追溯性,变更余额时,我们通常会同时插入一条记录流水。
账户流水核心字段:流水ID、金额、交易双方账户、交易时间戳、订单号、
注意:账户流水只能新增,不能修改和删除。流水号必须是自增的。
后续,系统对账时,我们只需要对交易流水明细数据做累计即可,如果出现和余额不一致情况,一般以交易流水为准来修复余额数据。
账户流水核心字段:流水ID、金额、交易双方账户、交易时间戳、订单号、
注意:账户流水只能新增,不能修改和删除。流水号必须是自增的。
后续,系统对账时,我们只需要对交易流水明细数据做累计即可,如果出现和余额不一致情况,一般以交易流水为准来修复余额数据。
更新余额、记录流水 虽属于两个操作,但是要保证要么都成功,要么都失败。要做到事务。常用的隔离级别是 RC 和 RR ,因为这两种隔离级别都可以避免脏读。
当然,如果涉及多个微服务调用,会用到分布式事务
分布式事务,细想下也很容易理解,就是将一个大事务拆分为多个本地事务,本地事务依然借助于数据库自身事务来解决,难点在于解决这个分布式一致性问题,借助重试机制,保证最终一致是我们常用的方案。
分布式事务,细想下也很容易理解,就是将一个大事务拆分为多个本地事务,本地事务依然借助于数据库自身事务来解决,难点在于解决这个分布式一致性问题,借助重试机制,保证最终一致是我们常用的方案。
7.mysql读写分离带来的数据不一致问题
互联网业务大部分都是 读多写少,为了提升数据库集群的吞吐性能,我们通常会采用 主从架构、读写分离
部署一个主库实例,客户端请求所有写操作全部写到主库,然后借助 MySQL 自带的 主从同步 功能,做一些简单配置,可以近乎实时的将主库的数据同步给 多个从库实例,主从延迟非常小,一般不超过 1 毫秒。
客户端请求的所有读操作全部打到 从库,借助多实例集群提升读请求的整体处理能力。
客户端请求的所有读操作全部打到 从库,借助多实例集群提升读请求的整体处理能力。
这个方案看似天衣无缝,但实际有个 副作用
主从同步虽然近乎实时,但还是有个 时间差 ,主库数据刚更新完,但数据还没来得及同步到从库,后续读请求直接访问了从库,看到的还是旧数据,影响用户体验。任何事情都不是完美的,从主同步也是一样,没有完美的解决方案,我们要找到其中的平衡取舍点。
主从同步虽然近乎实时,但还是有个 时间差 ,主库数据刚更新完,但数据还没来得及同步到从库,后续读请求直接访问了从库,看到的还是旧数据,影响用户体验。任何事情都不是完美的,从主同步也是一样,没有完美的解决方案,我们要找到其中的平衡取舍点。
以淘宝为例:从产品策划角度解决问题。我们在支付成功后,并没有立即跳到 订单详情页,而是增加了一个 无关紧要的 中间页(支付成功页),一是告诉你支付的结果是成功的,钱没丢,不要担心;另外也可以增加一些推荐商品,引流提升网站的GMV。最重要的,增加了一个缓冲期,为 订单的主从库数据同步 争取了更多的时间。
可谓一举多得,其他互联网业务也是类似道理。
可谓一举多得,其他互联网业务也是类似道理。
8.历史订单,归档
根据二八定律,系统绝大部分的性能开销花在20%的业务。数据也不例外,从数据的使用频率来看,经常被业务访问的数据称为热点数据;反之,称之为冷数据。
在了解的数据的冷、热特性后,便可以指导我们做一些有针对性的性能优化。这里面有业务层面的优化,也有技术层面的优化。比如:电商网站,一般只能查询3个月内的订单,如果你想看看3个月前的订单,需要访问历史订单页面。
在了解的数据的冷、热特性后,便可以指导我们做一些有针对性的性能优化。这里面有业务层面的优化,也有技术层面的优化。比如:电商网站,一般只能查询3个月内的订单,如果你想看看3个月前的订单,需要访问历史订单页面。
实现思路:
1.冷热数据区分的标准是什么?以电商订单为例:
方案一:以“下单时间”为标准,将3 个月前的订单数据当作冷数据,3 个月内的当作热数据。
方案二:根据“订单状态”字段来区分,已完结的订单当作冷数据,未完结的订单当作热数据。
方案三:组合方式,把下单时间 > 3 个月且状态为“已完结”的订单标识为冷数据,其他的当作热数据。
2.如何触发冷热数据的分离
方案一:直接修改业务代码,每次业务请求触发冷热数据判断,根据结果路由到对应的冷数据表或热数据表。缺点:如果判断标准是 时间维度,数据过期了无法主动感知。
方案二:如果觉得修改业务代码,耦合性高,不易于后期维护。可以通过监听数据库变更日志 binlog 方式来触发
方案三:常用的手段是跑定时任务,一般是选择凌晨系统压力小的时候,通过跑批任务,将满足条件的冷数据迁移到其他存储介质。在途业务表中只留下来少量的热点数据。
3.如何实现冷热数据分离,过程大概分为三步
判断数据是冷、还是热
将冷数据插入冷数据表中
然后,从原来的热库中删除迁移的数据
4.如何使用冷热数据
方案一:界面设计时会有选项区分,如上面举例的电商订单
方案二:直接在业务代码里区分。
9.订单分库分表,多维度查询
如果电商网站的订单数过多,我们一般会想到 分库分表 解决策略。没问题,这个方向是对的。
但是查询维度很多
1、买家,查询 我的订单 列表,需要根据 buyer_id 来查询
2、查看订单详情,需要根据 order_id 来查询
3、卖家,查询 我的销售 列表,需要根据 seller_id 来查询
而订单分表只有一个分表键,如何满足多维度 SQL 操作呢?
一个订单号 19 位,我们会发现同一个用户不同订单的最后 6 位都是一样的,没错,那是用户id的后6位。
这样,上文中 场景1、场景2 的查询可以共性抽取, 采用 buyer_id 或 order_id 的 后六位 作为分表键,对 1 000 000 取模,得到买家维度的订单分表的编号。
至于 场景3 卖家维度的订单查询,我们可以采用数据异构方式,按 seller_id 维度另外存储一份数据,专门供卖家使用。
10.秒杀场景
2.如何从0搭建公司的后端技术栈
3.SQL调优的5大步骤+10个案例
1.sql优化步骤
1.通过慢查日志等定位那些执行效率较低的SQL语句
2.explain 分析SQL的执行计划
需要重点关注type、rows、filtered、extra
type由上至下,效率越来越高:
ALL 全表扫描;
index 索引全扫描;
range 索引范围扫描,常用语<,<=,>=,between,in等操作;
ref 使用非唯一索引扫描或唯一索引前缀扫描,返回单条记录,常出现在关联查询中;
eq_ref 类似ref,区别在于使用的是唯一索引,使用主键的关联查询;
const/system 单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询;
null MySQL不访问任何表或索引,直接返回结果;
虽然上至下,效率越来越高,但是根据cost模型,假设有两个索引idx1(a, b, c),idx2(a, c),SQL为"select * from t where a = 1 and b in (1, 2) order by c";如果走idx1,那么是type为range,如果走idx2,那么type是ref;当需要扫描的行数,使用idx2大约是idx1的5倍以上时,会用idx1,否则会用idx2。
ALL 全表扫描;
index 索引全扫描;
range 索引范围扫描,常用语<,<=,>=,between,in等操作;
ref 使用非唯一索引扫描或唯一索引前缀扫描,返回单条记录,常出现在关联查询中;
eq_ref 类似ref,区别在于使用的是唯一索引,使用主键的关联查询;
const/system 单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询;
null MySQL不访问任何表或索引,直接返回结果;
虽然上至下,效率越来越高,但是根据cost模型,假设有两个索引idx1(a, b, c),idx2(a, c),SQL为"select * from t where a = 1 and b in (1, 2) order by c";如果走idx1,那么是type为range,如果走idx2,那么type是ref;当需要扫描的行数,使用idx2大约是idx1的5倍以上时,会用idx1,否则会用idx2。
Extra:
1.Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行;
2.Using temporary:使用了临时表保存中间结果,性能特别差,需要重点优化;
3.Using index:表示相应的 select 操作中使用了覆盖索引(Coveing Index),避免访问了表的数据行,效率不错!如果同时出现 using where,意味着无法直接通过索引查找来查询到符合条件的数据;
4.Using index condition:MySQL5.6之后新增的ICP,using index condtion就是使用了ICP(索引下推),在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。
1.Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行;
2.Using temporary:使用了临时表保存中间结果,性能特别差,需要重点优化;
3.Using index:表示相应的 select 操作中使用了覆盖索引(Coveing Index),避免访问了表的数据行,效率不错!如果同时出现 using where,意味着无法直接通过索引查找来查询到符合条件的数据;
4.Using index condition:MySQL5.6之后新增的ICP,using index condtion就是使用了ICP(索引下推),在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。
3.show profile 分析
了解SQL执行的线程的状态及消耗的时间。
默认是关闭的,开启语句“set profiling = 1;”
SHOW PROFILES ;
SHOW PROFILE FOR QUERY #{id};
SHOW PROFILES ;
SHOW PROFILE FOR QUERY #{id};
4.trace
trace分析优化器如何选择执行计划,通过trace文件能够进一步了解为什么优惠券选择A执行计划而不选择B执行计划。
set optimizer_trace="enabled=on";
set optimizer_trace_max_mem_size=1000000;
select * from information_schema.optimizer_trace;
set optimizer_trace_max_mem_size=1000000;
select * from information_schema.optimizer_trace;
5.确定问题并采用相应的措施
1.优化索引;
2.优化SQL语句:修改SQL、IN 查询分段、时间查询分段、基于上一次数据过滤;
3.改用其他实现方式:ES、数仓等;
4.数据碎片处理。
2.优化SQL语句:修改SQL、IN 查询分段、时间查询分段、基于上一次数据过滤;
3.改用其他实现方式:ES、数仓等;
4.数据碎片处理。
2.场景分析
1.最左匹配
1.场景:
索引:KEY `idx_shopid_orderno` (`shop_id`,`order_no`)
sql语句:select * from _t where orderno=''
2.分析:
查询匹配从左往右匹配,要使用order_no走索引,必须查询条件携带shop_id或者索引(shop_id,order_no)调换前后顺序。
2.隐式转换
1.场景:
索引:KEY `idx_mobile` (`mobile`)
sql语句:select * from _user where mobile=12345678901
2.分析:
隐式转换相当于在索引上做运算,会让索引失效。mobile是字符类型,使用了数字,应该使用字符串匹配,否则MySQL会用到隐式替换,导致索引失效。
3.大分页
1.场景:
索引:KEY `idx_a_b_c` (`a`, `b`, `c`)
sql语句:select * from _t where a = 1 and b = 2 order by c desc limit 10000, 10;
2.分析:
对于大分页的场景,可以优先让产品优化需求,如果没有优化的,有如下两种优化方式:
1.一种是把上一次的最后一条数据,也即上面的c传过来,然后做“c < xxx”处理,但是这种一般需要改接口协议,并不一定可行;‘
2.另一种是采用延迟关联的方式进行处理,减少SQL回表,但是要记得索引需要完全覆盖才有效果,SQL改动如下:
select t1.* from _t t1, (select id from _t where a = 1 and b = 2 order by c desc limit 10000, 10) t2 where t1.id = t2.id;
select t1.* from _t t1, (select id from _t where a = 1 and b = 2 order by c desc limit 10000, 10) t2 where t1.id = t2.id;
4.in + order by
1.场景:
索引:KEY `idx_shopid_status_created` (`shop_id`, `order_status`, `created_at`)
sql语句:select * from _order where shop_id = 1 and order_status in (1, 2, 3) order by created_at desc limit 10
2.分析:
in查询在MySQL底层是通过n*m的方式去搜索,类似union,但是效率比union高。
in查询在进行cost代价计算时(代价 = 元组数 * IO平均值),是通过将in包含的数值,一条条去查询获取元组数的,因此这个计算过程会比较的慢,所以MySQL设置了个临界值(eq_range_index_dive_limit),5.6之后超过这个临界值后该列的cost就不参与计算了。因此会导致执行计划选择不准确。默认是200,即in条件超过了200个数据,会导致in的代价计算存在问题,可能会导致Mysql选择的索引不准确。
解决:可以(order_status, created_at)互换前后顺序,并且调整SQL为延迟关联。
5.范围查询阻断,后续字段不能走索引
1.场景:
索引:KEY `idx_shopid_created_status` (`shop_id`, `created_at`, `order_status`)
sql语句:select * from _order where shop_id = 1 and created_at > '2021-01-01 00:00:00' and order_status = 10
2.分析:
范围查询还有“IN、between”。
6.不等于、不包含不能用到索引的快速搜索
select * from _order where shop_id=1 and order_status not in (1,2)
select * from _order where shop_id=1 and order_status != 1
select * from _order where shop_id=1 and order_status != 1
在索引上,避免使用NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等。
7.优化器选择不使用索引的情况
如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的蛮大一部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据
select * from _order where order_status = 1
查询出所有未支付的订单,一般这种订单是很少的,即使建了索引,也没法使用索引
8.复杂查询
select sum(amt) from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01';
select * from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01' limit 10;
select * from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01' limit 10;
如果是统计某些数据,可能改用数仓进行解决;
如果是业务上就有那么复杂的查询,可能就不建议继续走SQL了,而是采用其他的方式进行解决,比如使用ES等进行解决
9.asc和desc混用
select * from _t where a=1 order by b desc, c asc
desc 和asc混用时会导致索引失效
10.大数据
对于推送业务的数据存储,可能数据量会很大,如果在方案的选择上,最终选择存储在MySQL上,并且做7天等有效期的保存
4.场景设计
1.如何设计一个秒杀系统
1.秒杀前,页面访问压力大(解决方案:页面静态化,cdn+redis+ngnix多级缓存)
2.秒杀时,下单过于集中,作弊软件刷单(解决方案:前端页面增加校验或者答题环节)
3.秒杀时,下单请求系统冲击力大,影响其他正常功能(解决方案:为秒杀单独出一套系统订单系统)
4.秒杀时,要快速精准的扣库存(解决方案:基于缓存如redis实现快速精准的扣减库存)
5.秒杀后,快速过滤掉未抢到的下单请求(解决方案:库存扣减完后,快速的通知ngnix,过滤下单请求)
6.秒杀后,下单模块压力大(解决方案:下单请求写入mq,后端下单模块慢慢下单)
7.问题放大镜
1.要如何选择mq产品
2.如何快速处理未支付的订单
3.如何保证下单操作与消息发送的事务一致性
4.如何保证集群的高吞吐和高可用
5.如何保证mq消息的高吞吐和高可用
6.如何保证高性能文件的读写
2.CPU-100%如何进行解决
3.设计消息发送系统
4.如何设计一个高效的异常处理架构
5.在工作中遇到的印象深刻的难点?怎么解决的?
20.非技术套路问题
1.你有什么职业规划?
面试官想听到的是:你能随着我们公司的发展一起成长!
其一,表达对职业的认可;其二,表达对应聘公司及所属行业的认可;其三,表达长远的职业规划
2.为什么从上家公司离职?
能被接受的离职原因有两个:一是求发展,二是不可抗力。
离开上家公司是一个特别痛苦的选择,我和领导、同事都相处得很好,通过自己的努力也赢得了大家的信任,但公司规模太小发展空间不大,我希望找一份更有挑战的工作,没办法只好做出这个选择
应聘更大的公司,你可以说希望找一个大一点的平台历练自己;应聘一家小公司,你可以说希望有更自由的空间施展在自己的手脚。
你所追求的,恰好是新公司可以提供的。每天路上三个小时,起得比鸡早,睡得比狗晚,不想把自己的人生奉献给公交车;之前和男朋友异地恋,现在选择了夫唱妇随,来到了他的城市;公司破产,老板跑路,心有凌云志,奈何平台散……诸如此类的不可抗力,不会加分也不会减分。
3.你为什么选择我们公司?
参考的回答套路:
一,我很了解你们
二,我很认同你们
三,我熟悉应聘的职位并具备胜任的能力
四,我能给公司创造价值及实现自我价值
一,我很了解你们
二,我很认同你们
三,我熟悉应聘的职位并具备胜任的能力
四,我能给公司创造价值及实现自我价值
4.你有什么兴趣爱好?
其一,不要说没有什么兴趣爱好;其二,不要说容易让人产生负面联想的兴趣爱好;你的爱好最好对求职形成加持,你说喜欢长跑,潜台词是身体好;你说喜欢看书,说明你爱学习;应聘技术类职位,说爱下象棋或围棋,说明你爱动脑子、善于分析、逻辑性强。
5.你认为自己最大的缺点是什么?
开玩笑说:缺钱
最好你回答的确实是缺点,但对未来的工作影响不大,并给出了自己的改进方法,再顺手带出来一个优点。
“我的缺点是不会拒绝别人,同事找我帮忙都一概揽下,结果有时影响了自身的工作进度。”不懂拒绝是缺点,但也带出来了一个优点:热心肠。“我反思了这个问题,应该安排好工作的优先顺序,向求助同事展示手头的工作,并给出自己何时可以帮忙的预计时间,让他自行决定是否求助,这样既不影响同事关系,又不影响自己的工作。”
6.说说你印象最深的一次失败经历?
回答的基本套路是:通过失败说成功。
不要强调结果,更多叙述过程,重点落在收获和成长。案件重演时,仔细描述遇到的困难,处理的方法,客观的原因适当提及,承认自己的不足和失误,如果重来一次你会如何解决非常关键,让面试官感觉你勇于担当、勤于思考、乐于改正。
不要强调结果,更多叙述过程,重点落在收获和成长。案件重演时,仔细描述遇到的困难,处理的方法,客观的原因适当提及,承认自己的不足和失误,如果重来一次你会如何解决非常关键,让面试官感觉你勇于担当、勤于思考、乐于改正。
7.你开会时和领导吵起来了,你会怎么处理?
正确的回答是:“我不会在开会时和领导吵起来”,服从意识是基本的职业素养。
无论如何不该在开会时和领导吵起来,你不想干了?实在觉得自己是对的,可以会后去敲领导办公室的门,“王总,我还是不太明白为什么我那招不成,我想和您讨教一下……”跟领导打交道不能挑战他的权威。
8.你还有什么要问我的吗?
其一,不要问很容易找到答案的问题
其二,不要只问薪酬福利
可以问问与工作相关的问题,如关于岗位职责和部门情况的问题:
“公司对这个职位的具体期望是?”
“部门有多少位同事?”
“公司对这个职位的具体期望是?”
“部门有多少位同事?”
面试结束了,面试官没说何时给消息,建议你多问一句:“大概多久能有回复?
21.设计模式
22.大数据部分
23.经典面试题补充
1.大文件去重、排序问题?一千万的数据,你是怎么快速查询的?
1.文件中有40亿个QQ号码,请设计算法对QQ号码去重,相同的QQ号码仅保留一个,内存限制1G.
方法一:排序
1.很自然地,最简单的方式是对所有的QQ号码进行排序,重复的QQ号码必然相邻,保留第一个,去掉后面重复的就行。
2.可是,面试官要问你,去重一定要排序吗?显然,排序的时间复杂度太高了,无法通过腾讯面试。
方法二:hashmap
1.既然直接排序的时间复杂度太高,那就用hashmap吧,具体思路是把QQ号码记录到hashmap中,由于hashmap的去重性质,自动只保留一个重复的
2.可是,面试官又要问你了:实际要存40亿QQ号码,1G的内存够分配这么多空间吗?显然不行,无法通过腾讯面试。
方法三:文件切割
1.显然,这是海量数据问题,自然想到文件切割的方式,避免内存过大。可是,绞尽脑汁思考,要么使用文件间的归并排序,要么使用桶排序,反正最终是能排序的。既然排序好了,那就能实现去重了,貌似就万事大吉了。
2.接着,面试官又要问你:这么多的文件操作,效率自然不高啊。显然,无法通过腾讯面试。
方法四:bitmap
1.我们可以对hashmap进行优化,采用bitmap这种数据结构,可以顺利地同时解决时间问题和空间问题。
2.显然,可以推导出来:512MB大小足够标识所有QQ号码的存在与否,请注意:QQ号码的理论最大值为2^32 - 1,大概是43亿左右
3.用512MB的unsigned int数组来记录文件中QQ号码的存在与否,形成一个bitmap;然后从小到大遍历所有正整数(4字节),当bitmapFlag值为1时,就表明该数是存在的。而且,从上面的过程可以看到,自动实现了去重。显然,这种方式可以通过腾讯的面试。
方法五:使用布隆过滤器
2.文件中有40亿个互不相同的QQ号码,请设计算法对QQ号码进行排序,内存限制1G.
很显然,直接用bitmap, 标记这40亿个QQ号码的存在性,然后从小到大遍历正整数,当bitmapFlag的值为1时,就输出该值,输出后的正整数序列就是排序后的结果。
请注意,这里必须限制40亿个QQ号码互不相同。通过bitmap记录,客观上就自动完成了排序功能。
3.文件中有40亿个互不相同的QQ号码,求这些QQ号码的中位数,内存限制1G.
我知道,一些刷题经验丰富的人,最开始想到的肯定是用堆或者文件切割,这明显是犯了本本主义错误。直接用bitmap排序,当场搞定中位数
4.文件中有40亿个互不相同的QQ号码,求这些QQ号码的top-K,内存限制1G.
我知道,很多人背诵过top-K问题,信心满满,想到用小顶堆或者文件切割,这明显又是犯了本本主义错误。直接用bitmap排序,当场搞定top-K问题。
5.文件中有80亿个QQ号码,试判断其中是否存在相同的QQ号码,内存限制1G.
1.我知道,一些吸取了经验教训的人肯定说,直接bitmap啊。然而,又一次错了。根据容斥原理可知:因为QQ号码的个数是43亿左右(理论值2^32 - 1),所以80亿个QQ号码必然存在相同的QQ号码。
2.文件分割+bitmap
2.如果MySQL的自增 ID 用完了,怎么办?
1.表的自增 id 达到上限后,再申请时它的值就不会改变,进而导致继续插入数据时报主键冲突的错误
2.如果你创建的 InnoDB 表没有指定主键,那么 InnoDB 会给你创建一个不可见的,长度为 6 个字节的 row_id;row_id 达到上限后,则会归 0 再重新递增,如果出现相同的 row_id,后写的数据会覆盖之前的数据
3.MySQL中redo log 和 binlog 相配合的时候,它们有一个共同的字段叫作 Xid。它在 MySQL 中是用来对应事务的;Xid 只需要不在同一个 binlog 文件中出现重复值即可。虽然理论上会出现重复值,但是概率极小,可以忽略不计
4.InnoDB 内部维护了一个 max_trx_id 全局变量;InnoDB 的 max_trx_id 递增值每次 MySQL 重启都会被保存起来,所以我们文章中提到的脏读的例子就是一个必现的 bug,好在留给我们的时间还很充裕
5.thread_id 是我们使用中最常见的,而且也是处理得最好的一个自增 id 逻辑了
6.redis外部自增,毫秒级别,理论上会出现重复值,但是概率极小,可以忽略不计
3.数据量很大,分页查询很慢,有什么优化方案?
1.问题;当需要从数据库查询的表有上万条记录的时候,一次性查询所有结果会变得很慢,特别是随着数据量的增加特别明显,这时需要使用分页查询。对于数据库分页查询,也有很多种方法和优化的点
2.举例:select * from orders_history where type=8 limit 1000,10;该条语句将会从表 orders_history 中查询offset: 1000开始之后的10条数据,也就是第1001条到第1010条数据(1001 <= id <= 1010)。
3.测试结果:从查询时间来看,基本可以确定,在查询记录量低于100时,查询时间基本没有差距,随着查询记录量越来越大,所花费的时间也会越来越多;针对查询偏移量的测试:随着查询偏移的增大,尤其查询偏移大于10万以后,查询时间急剧增加。
优化方案1:使用子查询优化
1.这种方式先定位偏移位置的 id,然后往后查询,这种方式适用于 id 递增的情况。
2.select * from orders_history where type=8 and id>=(select id from orders_history where type=8 limit 100000,1)limit 100;
3.这种方式相较于原始一般的查询方法,将会增快数倍。
优化方案2:使用id限定优化
1.这种方式假设数据表的id是连续递增 的,则我们根据查询的页数和查询的记录数可以算出查询的id的范围,可以使用 id between and 来查询:
2.select * from orders_history where type=2 and id between 1000000 and 1000100 limit 100;
3.这种查询方式能够极大地优化查询速度,基本能够在几十毫秒之内完成。限制是只能使用于明确知道id的情况,不过一般建立表的时候,都会添加基本的id字段,这为分页查询带来很多便利
优化方案3:使用临时表优化
对于使用 id 限定优化中的问题,需要 id 是连续递增的,但是在一些场景下,比如使用历史表的时候,或者出现过数据缺失问题时,可以考虑使用临时存储的表来记录分页的id,使用分页的id来进行 in 查询。这样能够极大的提高传统的分页查询速度,尤其是数据量上千万的时候
4.Spring事务失效的场景有哪些?如何解决?
1.注解@Transactional配置的方法非public权限修饰;
2.注解@Transactional所在类非Spring容器管理的bean;比如@Service 注解注释掉;
3.注解@Transactional所在类中,注解修饰的方法被类内部方法调用;
这种失效场景是我们日常开发中最常踩坑的地方;在类A里面有方法a 和方法b, 然后方法b上面用 @Transactional加了方法级别的事务,在方法a里面 调用了方法b, 方法b里面的事务不会生效。为什么会失效呢?
其实原因很简单,Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。
解决方案:类内部使用其代理类调用事务方法:以上方法略作改动
4.业务代码抛出异常类型非RuntimeException,事务失效;
解决方案:@Transactional注解修饰的方法,加上rollbackfor属性值,指定回滚异常类型:@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
5.业务代码中存在异常时,使用try…catch…语句块捕获,而catch语句块没有throw new RuntimeExecption异常;(最难被排查到问题且容易忽略)
在事务方法中使用try-catch,导致异常无法抛出,自然会导致事务失效。
解决方案:捕获异常并抛出异常
6.注解@Transactional中Propagation属性值设置错误即Propagation.NOT_SUPPORTED(一般不会设置此种传播机制)
此种事务传播行为不是特殊自定义设置,基本上不会使用Propagation.NOT_SUPPORTED,不支持事务
7.mysql关系型数据库,且存储引擎是MyISAM而非InnoDB,则事务会不起作用
5.淘宝七天自动确认收货,让你设计,可以怎么实现?
1.使用 redis 给订单设置过期时间,最后通过判断 redis 中是否还有该订单来决定订单是否已经完成。这种解决方案相较于消息的延迟推送性能较低,因为我们知道 redis 都是存储于内存中,我们遇到恶意下单或者刷单的将会给内存带来巨大压力。
2.使用传统的数据库轮询来判断数据库表中订单的状态,这无疑增加了IO次数,性能极低。
3.使用 jvm 原生的 DelayQueue ,也是大量占用内存,而且没有持久化策略,系统宕机或者重启都会丢失订单信息。
4.rabbitmq死信队列+TTL过期时间来实现延迟队列。
6.有没有jvm调优实战经验?
1、最简单且最容易想到的算法是对数组进⾏排序(快速排序),然后取最⼤或最⼩的K个元素。总的时间复杂度为O(N*logN)+O(K)=O(N*logN)。该算法存在以下问题:
(1). 快速排序的平均复杂度为O(N*logN),但最坏时间复杂度为O(n2),不能始终保证较好的复杂度
(2). 只需要前k⼤或k⼩的数,,实际对其余不需要的数也进⾏了排序,浪费了⼤量排序时间
总结:通常不会采取该⽅案。
(1). 快速排序的平均复杂度为O(N*logN),但最坏时间复杂度为O(n2),不能始终保证较好的复杂度
(2). 只需要前k⼤或k⼩的数,,实际对其余不需要的数也进⾏了排序,浪费了⼤量排序时间
总结:通常不会采取该⽅案。
2、虽然我们不会采⽤快速排序的算法来实现TOP-K问题,但我们可以利⽤快速排序的思想,在数组中随机找⼀个元素key,将数组分成两部分Sa和Sb,其中Sa的元素>=key,Sb的元素<key,然后分析两种情况:
(1)若Sa中元素的个数⼤于或等于k,则在Sa中查找最⼤的k个数
(2)若Sa中元素的个数⼩于k,其个数为len,则在Sb中查找k-len个数字
如此递归下去,不断把问题分解为更⼩的问题,直到求出结果
(1)若Sa中元素的个数⼤于或等于k,则在Sa中查找最⼤的k个数
(2)若Sa中元素的个数⼩于k,其个数为len,则在Sb中查找k-len个数字
如此递归下去,不断把问题分解为更⼩的问题,直到求出结果
3.寻找N个数中的第K⼤的数,可以将问题转化寻找N个数中第K⼤的问题。对于⼀个给定的数p, 可以在O(N)的时间复杂度内找出所有不⼩于P的数。
4.上⾯⼏种解法都会对数据访问多次,那么就有⼀个问题,当数组中元素个数⾮常⼤时,如:100亿,这时候数据不能全部加载到
内存,就要求我们尽可能少的遍历所有数据。针对这种情况,下⾯我们介绍⼀种针对海量数据的解决⽅案。使用堆排序
内存,就要求我们尽可能少的遍历所有数据。针对这种情况,下⾯我们介绍⼀种针对海量数据的解决⽅案。使用堆排序
5.bitmap
7.如何保证接口的幂等性?
8.经典top k问题?
子主题
子主题
子主题
子主题
9.拆分微服务应该注意哪些地⽅,如何拆分?
1、业务⽅⾯拆分:所有技术⽅⾯的考虑,包括架构设计和解耦拆分都要考虑业务的需要。在服务拆分时,先从业务⻆度确定拆分的⽅案。拆分的边界要充分考虑业务的独⽴性和专业性,⽐如搜索类服务、⽀付类服务、购物⻋类服务,按服务的业务功能合理地划出拆分边界。
2、减少维护成本:拆分前的维护成本 - 拆分后的维护成本 ≧ 0
3、服务独⽴:确保拆分后的服务由相对独⽴的团队负责维护,尽量不要出现在不同服务之间的交叉调⽤。
4、系统扩展:拆分的⼀个重要理由也是最有价值的结果是提⾼了系统的扩展性。⽤户对不同的服务有不同的并发和性能⽅⾯的要求,因此服务具有不同的扩展性。把具有不同扩展性要求的服务拆分出来分别进⾏部署,可以降低成本,提⾼效率。
10.⾼并发系统性能优化?
优化程序,优化服务配置,优化系统配置
1.尽量使⽤缓存,包括⽤户缓存,信息缓存等,多花点内存来做缓存,可以⼤量减少与数据库的交互,提⾼性能。
2.⽤jprofiler等⼯具找出性能瓶颈,减少额外的开销。
3.优化数据库查询语句,减少直接使⽤hibernate等⼯具的直接⽣成语句(仅耗时较⻓的查询做优化)。
4.优化数据库结构,多做索引,提⾼查询效率。
5.统计的功能尽量做缓存,或按每天⼀统计或定时统计相关报表,避免需要时进⾏统计的功能。
6.能使⽤静态⻚⾯的地⽅尽量使⽤,减少容器的解析(尽量将动态内容⽣成静态html来显示)。
7.解决以上问题后,使⽤服务器集群来解决单台的瓶颈问题。
11.java性能优化的7个方向
性能优化根据优化的类别,分为业务优化和技术优化。业务优化产生的效果也是非常大的,但它属于产品和管理的范畴。同为程序员,在平常工作中,我们面对的优化方式,主要是通过一系列的技术手段,来完成对既定的优化目标。
1.复用优化
1.在写代码的时候,你会发现有很多重复的代码可以提取出来,做成公共的方法。这样,在下次用的时候,就不用再费劲写一遍了。
2.软件系统中,谈到数据复用,我们首先想到的就是缓冲和缓存。
缓冲(Buffer),常见于对数据的暂存,然后批量传输或者写入。多使用顺序方式,用来缓解不同设备之间频繁地缓慢地随机写,缓冲主要针对的是写操作。
缓存(Cache),常见于对已读取数据的复用,通过将它们缓存在相对高速的区域,缓存主要针对的是读操作。
缓冲(Buffer),常见于对数据的暂存,然后批量传输或者写入。多使用顺序方式,用来缓解不同设备之间频繁地缓慢地随机写,缓冲主要针对的是写操作。
缓存(Cache),常见于对已读取数据的复用,通过将它们缓存在相对高速的区域,缓存主要针对的是读操作。
3.与之类似的,是对于对象的池化操作,比如数据库连接池、线程池等,在 Java 中使用得非常频繁。由于这些对象的创建和销毁成本都比较大,我们在使用之后,也会将这部分对象暂时存储,下次用的时候,就不用再走一遍耗时的初始化操作了。
2.计算优化
1.并行优化
1.第一种模式是多机,采用负载均衡的方式,将流量或者大的计算拆分成多个部分,同时进行处理。比如,Hadoop 通过 MapReduce 的方式,把任务打散,多机同时进行计算。
2.第二种模式是采用多进程。比如 Nginx,采用 NIO 编程模型,Master 统一管理 Worker 进程,然后由 Worker 进程进行真正的请求代理,这也能很好地利用硬件的多个 CPU。
3.第三种模式是使用多线程,这也是 Java 程序员接触最多的。比如 Netty,采用 Reactor 编程模型,同样使用 NIO,但它是基于线程的。Boss 线程用来接收请求,然后调度给相应的 Worker 线程进行真正的业务计算。
2.变同步为异步
1.同步方式,请求会一直阻塞,直到有成功,或者失败结果的返回。虽然它的编程模型简单,但应对突发的、时间段倾斜的流量,问题就特别大,请求很容易失败。
2.异步操作可以方便地支持横向扩容,也可以缓解瞬时压力,使请求变得平滑。同步请求,就像拳头打在钢板上;异步请求,就像拳头打在海绵上。你可以想象一下这个过程,后者肯定是富有弹性的,体验更加友好。
3.惰性加载
最后一种,就是使用一些常见的设计模式来优化业务,提高体验,比如单例模式、代理模式等。举个例子,在绘制 Swing 窗口的时候,如果要显示比较多的图片,就可以先加载一个占位符,然后通过后台线程慢慢加载所需要的资源,这就可以避免窗口的僵死
3.结果集优化
1.你要尽量保持返回数据的精简。一些客户端不需要的字段,那就在代码中,或者直接在 SQL 查询中,就把它去掉
2.对于一些对时效性要求不高,但对处理能力有高要求的业务。我们要吸取缓冲区的经验,尽量减少网络连接的交互,采用批量处理的方式,增加处理速度
3.结果集合很可能会有二次使用,你可能会把它加入缓存中,但依然在速度上有所欠缺。这个时候,就需要对数据集合进行处理优化,采用索引或者 Bitmap 位图等方式,加快数据访问速度
4.资源冲突优化
1.现实中的性能问题,和锁相关的问题是非常多的。大多数我们会想到数据库的行锁、表锁、Java 中的各种锁等。在更底层,比如 CPU 命令级别的锁、JVM 指令级别的锁、操作系统内部锁等,可以说无处不在
2.只有并发,才能产生资源冲突。也就是在同一时刻,只能有一个处理请求能够获取到共享资源。解决资源冲突的方式,就是加锁。再比如事务,在本质上也是一种锁
3.按照锁级别,锁可分为乐观锁和悲观锁,乐观锁在效率上肯定是更高一些;按照锁类型,锁又分为公平锁和非公平锁,在对任务的调度上,有一些细微的差别
4.对资源的争用,会造成严重的性能问题,所以会有一些针对无锁队列之类的研究,对性能的提升也是巨大的。
5.算法优化
比如,作为 List 的实现,LinkedList 和 ArrayList 在随机访问的性能上,差了好几个数量级;又比如,CopyOnWriteList 采用写时复制的方式,可以显著降低读多写少场景下的锁冲突。而什么时候使用同步,什么时候是线程安全的,也对我们的编码能力有较高的要求。
6.jvm优化
目前被广泛使用的垃圾回收器是 G1,通过很少的参数配置,内存即可高效回收。CMS 垃圾回收器已经在 Java 14 中被移除,由于它的 GC 时间不可控,有条件应该尽量避免使用
对 JVM 虚拟机进行优化,也能在一定程度上能够提升 JAVA 程序的性能。如果参数配置不当,甚至会造成 OOM 等比较严重的后果。
7.高效实现
在平时的编程中,尽量使用一些设计理念良好、性能优越的组件。比如,有了 Netty,就不用再选择比较老的 Mina 组件。而在设计系统时,从性能因素考虑,就不要选 SOAP 这样比较耗时的协议。
24.面试复盘
1.nacos2.0版本作为配置中心的bug
1.使用nacos 2.0.0版本作为注册中心时,发现在nacos客户端查询注册成功且非default_group组的服务时会响应500
2.
2.如何进行代码review?
1.方式为gitlab权限设置+重大问题小组会议形式讨论
3.京东面试复盘
1.spring中都有哪些设计模式?
2.代理模式是什么?
3.tcp的keepalivetaime
4.java中utf-8是什么字节
5.tcp的大端与小端
6.mybatis的二级缓存
7.tcp的拆包粘包如何处理的
8.netty为什么高效?
9.epool模型
10.多个线程顺序打印1/2/3/4/5/6
11.utf-8 编码中的中文占几个字节?
12.并发锁
13.gc root
14.oom的情况举例
15.mysql索引有哪些?b-+tree与b-tere的区别
16.sql优化做了哪些
17.使用redis过程中遇到的问题
缓存雪崩、缓存穿透、缓存击穿
子主题
子主题
18.redis治理大key问题
19.mysql与redis双写不一致问题
20.canel如何使用
21.redis如何保证高可用
22.redis哨兵机制+redis cluster
23.k8s
4.蘑菇车联面试复盘
1.内存泄漏怎么处理?
1.内存泄漏:内存泄漏就是内存中的变量没有回收,一直存在与内存中,造成内存的浪费的行为
2.内存泄漏的情况
1. 静态集合类
如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
2. 各种连接,如数据库连接、网络连接和IO连接等
在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
3. 变量不合理的作用域
一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。
实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间
实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间
public class Us ingRandom {
private String msg;
public void receiveMsg(){
readFromNet();//从网络中接受数据保存到msg中
saveDB();//把msg保存到数据库中
}
private String msg;
public void receiveMsg(){
readFromNet();//从网络中接受数据保存到msg中
saveDB();//把msg保存到数据库中
}
4. 内部类持有外部类
如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
5. 改变哈希值
当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露
2.spring中使用了哪些设计模式?
在spring中主要用到的设计模式有:工厂模式、单例模式、代理模式、模板模式、观察者模式、适配器模式。
1.工厂模式
IOC控制反转也叫依赖注入,它就是典型的工厂模式,通过sessionfactory去注入实例
解释:将对象交给容器管理,你只需要在spring配置文件总配置相应的bean,以及设置相关属性,让spring容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件中配置的bean初始化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你 需要调用这些bean的类(假设这个类名是A),分配的方法就是调用A的setter方法来注入,而不需要你在A类里面new这些bean了。
总结:对象实例化与初始化进行解耦
2.单例模式
Spring中JavaBean默认为单例,因为spring上下文中会有很多个dao\service\action对象,如果用多例模式的话,就每次要用到的时候,都会重新创建一个新的对象,内存中就会有很多重复的对象,所以单例模式的特点就是能减少我们的内存空间,节约性能。
还有常用Spring中 @Repository、@Component、@Configuration @Service注解作用下的类默认都是单例模式的,所以,我目前认为在Spring下使用单例最优的方式是将类@Component注册为组件。使用场景主要有:数据库配置、Redis配置、权限配置、Filter过滤、webMvcConfig、swagger及自定义的时间转换器、类型转换器、对接第三方硬件时,调用硬件的dll、so文件等。
单独使用@Component注解,只能控制到类上,使用@Configuration+@Bean可以控制到方法级别粒度,但是尽量避免@Component+@Bean组合使用,因为@Component+@Bean并不是单例,在调用过程中可能会出现多个Bean实例,导致蜜汁错误。
单独使用@Component注解,只能控制到类上,使用@Configuration+@Bean可以控制到方法级别粒度,但是尽量避免@Component+@Bean组合使用,因为@Component+@Bean并不是单例,在调用过程中可能会出现多个Bean实例,导致蜜汁错误。
并不是所有的注解默认都是单例模式,@RestController就是多例
3.代理模式
Spring中的AOP就是典型的代理模式,首先我们先聊聊AOP(面向切面编程)的的一个设计原理:
AOP可以说是对OOP的补充和完善 OOP引入了封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,oop允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能,日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在OOP设计中,它导致大量代码重复,而不利于各个模块的重用。
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码
简单点解释,比方说你想在你的biz层所有类中都加上一个打印‘你好’的功能,这时就可以用AOP思想来做,你先写个类写个类方法,方法经实现打印‘你好’,然后IOC这个类ref="biz.* "让每个类都注入即可实现。
AOP可以说是对OOP的补充和完善 OOP引入了封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,oop允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能,日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在OOP设计中,它导致大量代码重复,而不利于各个模块的重用。
实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码
简单点解释,比方说你想在你的biz层所有类中都加上一个打印‘你好’的功能,这时就可以用AOP思想来做,你先写个类写个类方法,方法经实现打印‘你好’,然后IOC这个类ref="biz.* "让每个类都注入即可实现。
4.模板模式
定义:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤
目的:1.使用模版方法模式的目的是避免编写重复代码,以便开发人员可以专注于核心业务逻辑的实现
2.解决接口与接口实现类之间继承矛盾问题
目的:1.使用模版方法模式的目的是避免编写重复代码,以便开发人员可以专注于核心业务逻辑的实现
2.解决接口与接口实现类之间继承矛盾问题
5.观察者模式
定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新
例如:
报社的业务就是出版报纸。
向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户、你就会一直收到新报纸。
当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。
只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸。
报社:被观察者
订户:观察者
一个报社对应多个订户
例如:
报社的业务就是出版报纸。
向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户、你就会一直收到新报纸。
当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。
只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸。
报社:被观察者
订户:观察者
一个报社对应多个订户
在Spring中有一个ApplicationListener,采用观察者模式来处理的,ApplicationEventMulticaster作为主题,里面有添加,删除,通知等。
spring有一些内置的事件,当完成某种操作时会发出某些事件动作,他的处理方式也就上面的这种模式,当然这里面还有很多,可以了解下spring的启动过程。
spring有一些内置的事件,当完成某种操作时会发出某些事件动作,他的处理方式也就上面的这种模式,当然这里面还有很多,可以了解下spring的启动过程。
在java.util 包下 除了常用的 集合 和map之外还有一个Observable类,他的实现方式其实就是观察者模式。里面也有添加、删除、通知等方法。
6.适配器模式
定义:将一个类的接口转接成用户所期待的。一个适配使得因接口不兼容而不能在一起工作的类能在一起工作,做法是将类自己的接口包裹在一个已存在的类中
例子:以手机充电为例,电压220V,手机支持5.5V,充电器相当于适配器
例子:以手机充电为例,电压220V,手机支持5.5V,充电器相当于适配器
AOP和MVC中,都有用到适配器模式。
spring aop框架对BeforeAdvice、AfterAdvice、ThrowsAdvice三种通知类型的支持实际上是借助适配器模式来实现的,这样的好处是使得框架允许用户向框架中加入自己想要支持的任何一种通知类型,上述三种通知类型是spring aop框架定义的,它们是aop联盟定义的Advice的子类型。
Spring中的AOP中AdvisorAdapter类,它有三个实现:MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。
spring aop框架对BeforeAdvice、AfterAdvice、ThrowsAdvice三种通知类型的支持实际上是借助适配器模式来实现的,这样的好处是使得框架允许用户向框架中加入自己想要支持的任何一种通知类型,上述三种通知类型是spring aop框架定义的,它们是aop联盟定义的Advice的子类型。
Spring中的AOP中AdvisorAdapter类,它有三个实现:MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。
3.new一个对象的过程?
Java中创建一个对象所涉及的知识主要有以下几点:
1.类加载机制及双亲委派模型 2.JVM组成结构及内存模型
首先new一个对象一定是在一个线程中某个方法执行了new操作,此时程序计数器告诉了执行引擎需要执行new所在行的代码,执行引擎获取到new指令后触发了初始化类的操作。初始化类操作首先要检查该类是否被加载,该检查操作需要到方法区中(该new关键字所在的类的)运行时常量池当中先获取该类的全限定类名,通过全限定类名在方法区中查找是否已经被加载。如果没有被加载,使用经过双亲委派模型选定类加载器(一般是appclassloader或自定义加载器)加载类经理加载验证准备解析阶段后,在方法区中开辟了该类的空间、在堆中创建了Class对象,在方法区中为静态变量分配内存。之后执行初始化操作为静态变量赋值。
然后开始在堆区为该对象进行内存分配,内存大小在加载阶段就已经决定了。
1.类加载机制及双亲委派模型 2.JVM组成结构及内存模型
首先new一个对象一定是在一个线程中某个方法执行了new操作,此时程序计数器告诉了执行引擎需要执行new所在行的代码,执行引擎获取到new指令后触发了初始化类的操作。初始化类操作首先要检查该类是否被加载,该检查操作需要到方法区中(该new关键字所在的类的)运行时常量池当中先获取该类的全限定类名,通过全限定类名在方法区中查找是否已经被加载。如果没有被加载,使用经过双亲委派模型选定类加载器(一般是appclassloader或自定义加载器)加载类经理加载验证准备解析阶段后,在方法区中开辟了该类的空间、在堆中创建了Class对象,在方法区中为静态变量分配内存。之后执行初始化操作为静态变量赋值。
然后开始在堆区为该对象进行内存分配,内存大小在加载阶段就已经决定了。
3. 内存分配完后,虚拟机需要将分配到的内存空间中的数据类型都 初始化为零值(不包括对象头);
虚拟机要 对对象头进行必要的设置 ,例如这个对象是哪个类的实例(即所属类)、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都存放在对象的对象头中。
至此,从虚拟机视角来看,一个新的对象已经产生了。但是在Java程序视角来看,执行new操作后会接着执行如下步骤:
调用对象的init()方法 ,根据传入的属性值给对象属性赋值。
在线程 栈中新建对象引用 ,并指向堆中刚刚新建的对象实例。https://www.cnblogs.com/gjmhome/p/11401397.html
虚拟机要 对对象头进行必要的设置 ,例如这个对象是哪个类的实例(即所属类)、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息都存放在对象的对象头中。
至此,从虚拟机视角来看,一个新的对象已经产生了。但是在Java程序视角来看,执行new操作后会接着执行如下步骤:
调用对象的init()方法 ,根据传入的属性值给对象属性赋值。
在线程 栈中新建对象引用 ,并指向堆中刚刚新建的对象实例。https://www.cnblogs.com/gjmhome/p/11401397.html
4.什么是双亲委派机制,为什么要打破双亲委派机制?
1.什么是双亲委派?
2.为什么打破双亲委派?
子主题
5.jvm都做过哪些优化?
子主题
子主题
6.jvm调优的步骤
1.监控分析
分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点。
1.如何生成GC日志:
-XX:+PrintGC 输出简要GC日志
-XX:+PrintGCDetails 输出详细GC日志
-Xloggc:gc.log 输出GC日志到文件
-XX:+PrintGCDetails 输出详细GC日志
-Xloggc:gc.log 输出GC日志到文件
2.如何产生dump文件
1.JVM启动时增加两个参数:
# 出现OOME时生成堆dump:
-XX:+HeapDumpOnOutOfMemoryError
# 生成堆文件地址:
-XX:HeapDumpPath=/home/hadoop/dump/
# 出现OOME时生成堆dump:
-XX:+HeapDumpOnOutOfMemoryError
# 生成堆文件地址:
-XX:HeapDumpPath=/home/hadoop/dump/
2.jmap生成:发现程序异常前通过执行指令,直接生成当前JVM的dump文件
jmap -dump:file=文件名.dump [pid]
# 9257是指JVM的进程号
jmap -dump:format=b,file=testmap.dump 9257
jmap -dump:file=文件名.dump [pid]
# 9257是指JVM的进程号
jmap -dump:format=b,file=testmap.dump 9257
第一种方式是一种事后方式,需要等待当前JVM出现问题后才能生成dump文件,实时性不高; 第二种方式在执行时,JVM是暂停服务的,所以对线上的运行会产生影响。所以建议第一种方式。
3.第三方可视化工具生成
2.判断
如果各项参数设置合理,系统没有超时日志或异常信息出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。 遇到以下情况,就需要考虑进行JVM调优:
系统吞吐量与响应性能不高或下降;
Heap内存(老年代)持续上涨达到设置的最大内存值;
Full GC 次数频繁;
GC 停顿时间过长(超过1秒);
应用出现OutOfMemory等内存异常;
应用中有使用本地缓存且占用大量内存空间;
Heap内存(老年代)持续上涨达到设置的最大内存值;
Full GC 次数频繁;
GC 停顿时间过长(超过1秒);
应用出现OutOfMemory等内存异常;
应用中有使用本地缓存且占用大量内存空间;
3.确定优化目标
调优的最终目的都是为了应用程序使用最小的硬件消耗来承载更大的吞吐量或者低延迟。 jvm调优主要是针对垃圾收集器的收集性能优化,减少GC的频率和Full GC的次数,令运行在虚拟机上的应用能够使用更少的内存、高吞吐量、低延迟。
下面列举一些JVM调优的量化目标参考实例,注意:不同应用的JVM调优量化目标是不一样的。
堆内存使用率<=70%;
老年代内存使用率<=70%;
avgpause<=1秒;
Full GC次数0或avg pause interval>=24小时 ;
堆内存使用率<=70%;
老年代内存使用率<=70%;
avgpause<=1秒;
Full GC次数0或avg pause interval>=24小时 ;
4.调整参数
调优一般是从满足程序的内存使用需求开始的,之后是时间延迟的要求,最后才是吞吐量的要求。 要基于这个步骤来不断优化,每一个步骤都是进行下一步的基础,不可逆行之。
5.对比调优前后的指标差异
6.重复上诉的过程
7.hashmap的扩容过程?
子主题
子主题
8.接口响应慢从哪些方面拍查?
确定是哪个接口存在性能问题
确定这个接口的内部逻辑是怎样的,做了哪些事情
分析接口存在性能问题的根本原因
寻找确立优化方案
回归验证方案效果
确定这个接口的内部逻辑是怎样的,做了哪些事情
分析接口存在性能问题的根本原因
寻找确立优化方案
回归验证方案效果
1.是不是资源层面的瓶颈,硬件、配置环境之类的问题?
2.针对查询类接口,是不是没有添加缓存,如果加了,是不是热点数据导致负载不均衡?
3.是不是有依赖于第三方接口,导致因第三方请求拖慢了本地请求?
4.是不是接口涉及业务太多,导致程序执行跑很久?5.是不是sql层面的问题导致的数据等待加长,进而拖慢接口?
6.网络层面的原因?带宽不足?DNS解析慢?
7.确实是代码质量差导致的,如出现内存泄漏,重复循环读取之类?
2.针对查询类接口,是不是没有添加缓存,如果加了,是不是热点数据导致负载不均衡?
3.是不是有依赖于第三方接口,导致因第三方请求拖慢了本地请求?
4.是不是接口涉及业务太多,导致程序执行跑很久?5.是不是sql层面的问题导致的数据等待加长,进而拖慢接口?
6.网络层面的原因?带宽不足?DNS解析慢?
7.确实是代码质量差导致的,如出现内存泄漏,重复循环读取之类?
1.java基础
1.java基础知识
1.说说你对 Java 反射的理解
反射:在运行状态中,对任意一个类,都能知道这个类的所有属性和方法,对任意一个对象,都能 调用它的任意一个方法和属性。这种能动态获取信息及动态调用对象方法的功能称为 java 语言的反射机制。
反射的作用:开发过程中,经常会遇到某个类的某个成员变量、方法或属性是私有的,或只 对系统应用开放,这里就可以利用 java 的反射机制通过反射来获取所需的私有成员或是方 法
获取过程:
获取类的 Class 对象实例 Class clz = Class.forName("com.zhenai.api.Apple");
根 据 Class 对 象 实 例 获 取 Constructor 对 象 Constructor appConstructor = clz.getConstructor();
使 用 Constructor 对 象 的 newInstance 方 法 获 取 反 射 类 对 象 Object appleObj = appConstructor.newInstance();
获取方法的 Method 对象 Method setPriceMethod = clz.getMethod("setPrice", int.class);
利用 invoke 方法调用方法 setPriceMethod.invoke(appleObj, 14);
通过 getFields()可以获取 Class 类的属性,但无法获取私有属性,而 getDeclaredFields()可 以获取到包括私有属性在内的所有属性。带有 Declared 修饰的方法可以反射到私有的方法, 没有 Declared 修饰的只能用来反射公有的方法,其他如 Annotation\Field\Constructor 也是如 此。
Java获取反射的三种方法
1.通过new对象实现反射机制
2.通过路径实现反射机制
3.通过类名实现反射机制
2.说说你对 Java 注解的理解
注解是通过@interface 关键字来进行定义的,形式和接口差不多,只是前面多了一个@
注解是通过反射获取的,可以通过 Class 对象的 isAnnotationPresent()方法判断它是否应用了 某个注解,再通过 getAnnotation()方法获取 Annotation 对象
3.说一下泛型原理,并举例说明
泛型就是将类型变成参数传入,使得可以使用的类型多样化,从而实现解耦。
与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Cache< String > 这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。
4.关于String
1.String 为什么要设计成不可变的?
1.字符串常量池需要 String 不可变。因为 String 设计成不可变,当创建一个 String 对象时, 若此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。 如果字符串变量允许必变,会导致各种逻辑错误,如改变一个对象会影响到另一个独立对象。
2.String 对象可以缓存 hashCode。字符串的不可变性保证了 hash 码的唯一性,因此可以缓 存 String 的 hashCode,这样不用每次去重新计算哈希码。在进行字符串比较时,可以直接比较 hashCode,提高了比较性能;
3.安全性。String 被许多 java 类用来当作参数,如 url 地址,文件 path 路径,反射机制所 需的 String 参数等,若 String 可变,将会引起各种安全隐患。
2.String在堆栈中的存储
String str1 = "abc"; //栈中开辟一块空间存放引用 str1,str1 指向池中 String 常量"abc"
String str2 = "def"; //栈中开辟一块空间存放引用 str2,str2 指向池中 String 常量"def"
String str3 = str1 + str2;//栈中开辟一块空间存放引用 str3
//str1+str2 通过 StringBuilder 的最后一步 toString()方法返回一个新的 String 对象"abcdef"
//会在堆中开辟一块空间存放此对象,引用str3指向堆中的(str1+str2)所返回的新String对象。
System.out.println(str3 == "abcdef");
//返回 false 因为 str3 指向堆中的"abcdef"对象,而"abcdef"是字符池中的对象,所以结果为 false。JVM 对 String str="abc"对象放在常量池是在编译时做的,而 String str3=str1+str2 是在运行时才知 道的,new 对象也是在运行时才做的。
String str2 = "def"; //栈中开辟一块空间存放引用 str2,str2 指向池中 String 常量"def"
String str3 = str1 + str2;//栈中开辟一块空间存放引用 str3
//str1+str2 通过 StringBuilder 的最后一步 toString()方法返回一个新的 String 对象"abcdef"
//会在堆中开辟一块空间存放此对象,引用str3指向堆中的(str1+str2)所返回的新String对象。
System.out.println(str3 == "abcdef");
//返回 false 因为 str3 指向堆中的"abcdef"对象,而"abcdef"是字符池中的对象,所以结果为 false。JVM 对 String str="abc"对象放在常量池是在编译时做的,而 String str3=str1+str2 是在运行时才知 道的,new 对象也是在运行时才做的。
5.utf-8 编码中的中文占几个字节;int 型几个字节?
utf-8 是一种变长编码技术,utf-8 编码中的中文占用的字节不确定,可能 2 个、3 个、4 个, int 型占 4 个字节
6.静态代理和动态代理的区别,什么场景使用?
代理是一种常用的设计模式,目的是:为其他对象提供一个代理以控制对某个对象的访问, 将两个类的关系解耦。代理类和委托类都要实现相同的接口,因为代理真正调用的是委托类 的方法。
7.final finally finalize
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用 System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用 System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
8.深拷贝与浅拷贝
1.浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。
2.深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象
举例来说更加清楚:对象 A1 中包含对 B1 的引用, B1 中包含对 C1 的引用。
浅拷贝 A1 得到 A2 , A2 中依然包含对 B1 的引用, B1 中依然包含对 C1 的引用。
深拷贝则是对浅拷贝的递归,深拷贝 A1 得到 A2 , A2 中包含对 B2 ( B1 的 copy )的引用, B2 中包含对 C2 ( C1 的 copy )的引用
若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝
浅拷贝 A1 得到 A2 , A2 中依然包含对 B1 的引用, B1 中依然包含对 C1 的引用。
深拷贝则是对浅拷贝的递归,深拷贝 A1 得到 A2 , A2 中包含对 B2 ( B1 的 copy )的引用, B2 中包含对 C2 ( C1 的 copy )的引用
若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝
9.数组在内存中如何分配
1.静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度String[] names = new String[]{"多啦A梦", "大雄", "静香"};
2.动态初始化:初始化时由程序员显示的指定数组的长度,由系统为数据每个元素分配初始值String[] cars = new String[4];
静态初始化方式,程序员虽然没有指定数组长度,但是系统已经自动帮我们给分配了,而动态初始化方式,程序员虽然没有显示的指定初始化值,但是因为 Java 数组是引用类型的变量,所以系统也为每个元素分配了初始化值 null ,当然不同类型的初始化值也是不一样的,假设是基本类型int类型,那么为系统分配的初始化值也是对应的默认值0。
10.String/StringBuffer/StringBuilder的区别
1.String:不可变的字符串序列;如果操作少量的数据用String
2.StringBuffer:可变的字符串序列、效率低、线程安全;多线程操作字符串缓冲区下操作大量的数据StringBuffer;
3.StringBulider:可变的字符串序列、效率高、线程不安全;单线程操作字符串缓冲区下操作大量的数据使用StringBuilder;
11.java的基础数据类型以及字节数
1.整型
byte:1字节
short:2字节
int:4字节
long:8字节
2.浮点型
float:4字节
double:8字节
3.char类型
char:2字节
4.boolean类型
boolean:1字节
12.面向对象语言的特征
1.封装
封装(Encapsulation)是面向对象方法的重要原则,就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节
举个比较通俗的例子,比如我们的USB接口。如果我们需要外设且只需要将设备接入USB接口中,而内部是如何工作的,对于使用者来说并不重要。而USB接口就是对外提供的访问接口
对成员变量实行更准确的控制、良好的封装能够减少代码之间的耦合度、外部成员无法修改已封装好的程序代码
2.继承
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。当然,如果在父类中拥有私有属性(private修饰),则子类是不能被继承的。
只支持单继承,即一个子类只允许有一个父类,但是可以实现多级继承,及子类拥有唯一的父类,而父类还可以再继承。
子类可以拥有父类的属性和方法、子类可以拥有自己的属性和方法、子类可以重写覆盖父类的方法。
子类可以拥有父类的属性和方法、子类可以拥有自己的属性和方法、子类可以重写覆盖父类的方法。
提高代码复用性、父类的属性方法可以用于子类、可以轻松的定义子类、使设计应用程序变得简单
重写(override):是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!
重载(overload):是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。
3.多态
多态是同一个行为具有多个不同表现形式或形态的能力。
多态的体现形式:继承、父类引用指向子类、重写
向上转型:格式:父类名称 对象名 = new 子类名称();
含义:右侧创建一个子类对象,把它当作父类来使用。
注意:向上转型一定是安全的。
缺点:一旦向上转型,子类中原本特有的方法就不能再被调用了。
含义:右侧创建一个子类对象,把它当作父类来使用。
注意:向上转型一定是安全的。
缺点:一旦向上转型,子类中原本特有的方法就不能再被调用了。
13.其他面试题
1.string.inten()方法:
s.intern()方法的时候,会将共享池中的字符串与外部的字符串(s)进行比较,如果共享池中有与之相等的字符串,则不会将外部的字符串放到共享池中的,返回的只是共享池中的字符串,如果不同则将外部字符串放入共享池中,并返回其字符串的句柄(引用)-- 这样做的好处就是能够节约空间
子主题
2.计算机网络基础知识
1.TCP三次握手和四次握手
1.tcp的三次握手
在tcp/ip协议中,tcp协议提供可靠的连接服务,采用三次握手建立一个连接
1.第一次握手:建立连接时,客户端发送syn包(syn=j)到服务器,并进入syn_send状态,等待服务器的确认;
2.第二次握手:服务器收到syn包,必须确认客户的syn(ack=j+1),同时自己也发送一个syn包(syn=k),即syn+ack,此时服务器进入syn_recv状态;
3.第三次握手:客户端收到服务器的syn+ack包,向服务器发送确认包ack(ack=k+1),此包发送完毕,客户端和服务器进入established状态,完成三次握手;
2.tcp的四次握手
tcp采用四次挥手来释放连接
1.第一次挥手: Client发送一一个FIN,用来关闭Client到Server的数据传送,Client 进入FIN_WAIT_1状态;
2.第二次挥手: Server 收到FIN后,发送一个ACK给Client,确认序号为收到序号+1 (与SYN相同,一个FIN占用一个序号),Server 进入CLOSE _WAIT状态;
3.第三次挥手:Server发送一一个FIN,用来关闭Server到Client的数据传送,Server 进入LAST_ ACK状态;
4.第四次挥手: Client 收到FIN后,Client 进入TIME _WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
3.为什么连接的时候是3次握手,关闭的时候是4次握手?
答:因为当Server端收 到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来
同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文, 告诉Client
端,“你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。 故需要四步握
手。
同步的。但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文, 告诉Client
端,“你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。 故需要四步握
手。
关闭时时4次是:因为全双工,发送方和接收方都需要FIN报文和ACK报文
4.为什么TIME_WAIT状态需要经过2MSL(最大报文生存时间)才能返回到CLOSE状态?
答:虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可能最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。在Client发送出最后的ACK回复, 但该ACK可能丢失。Server如果没有收到ACK,将不断重复发送FIN片段。所以Client不能立即关闭, 它必须确认Server接收到了该ACK. Client会在发送出ACK之后进入到TIME_WAIT状态。Client会设置一 个计时器,等待2MSL的时间。如果在该时间内再次收到FIN,那么Client会重发ACK并再次等待2MSL。所谓的2MSL是 两倍的MSL(Maximum Segment Lifetime)。MSL指一 个片段在网络中最大的存活时间, 2MSL就是一个发送和一个回复所需的最大时间。如果直到2MSL,Client都没有再次收到FIN,那么Client推断ACK已经被成功接收,则结束TCP连接。
RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。所以规定中2MSL=4min。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。
5.为什么不能使用2次握手进行连接?
1.三次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
2.现在把三次握手改成仅需要两次握手,死锁是可能发生的。作为例子,考虑计算机S和C之间的通信,假定C给S发送一个连接请求分组,S收到了这个分组,并发送了确认应答分组。按照两次握手的协定,S认为连接已经成功地建立了,可以开始发送数据分组。可是, C在S的应答分组在传输中被云失的情况下,将不知道S是否已准备好,不知道S建立什么样的序列号,C甚至怀疑S是否收到自己的连接请求分组。在这种情况下,C认为连接还未建立成功,将忽略S发来的任何数据分组,只等待连接确认应答分组。而S在发出的分组超时后,重复发送同样的分组。这样就形成了死锁。
3.通过三次握手,A知道B能收能发,B知道A能收能发,通信连接至此建立。三次连接是保证可靠的最小握手次数,再多次握手也不能提高通信成功的概率,反而浪费资源。
6.如果已经建立了连接,但是客户端突然故障了怎么办?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去, 白白浪费资源。服务器每收到一次客户端的请求后都
会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段, 以后每隔
75秒钟发送一次。若连发送10个探测报文仍然没反应, 服务器就认为客户端出了故障,接着就关闭连接。
会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段, 以后每隔
75秒钟发送一次。若连发送10个探测报文仍然没反应, 服务器就认为客户端出了故障,接着就关闭连接。
2.TCP的滑动时间窗口
tcp的滑动窗口做流量控制与乱序重排
保证了tcp的可靠性以及流控特性
3.http与https的区别
其实HTTPS就是从HTTP加上加密处理(一般是SSL安全通信线路)+认证+完整性保护
区别:
1. https需要拿到ca证书,需要钱的
2. 端口不一样,http是80,https443
3. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
4. http和https使用的是完全不同的连接方式(http的连接很简单,是无状态的;HTTPS 协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。)
1. https需要拿到ca证书,需要钱的
2. 端口不一样,http是80,https443
3. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
4. http和https使用的是完全不同的连接方式(http的连接很简单,是无状态的;HTTPS 协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。)
4.http是长连接还是短连接?
在HTTP/1.0中,默认使用的是短连接。也就是说,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。如果客户端浏览器访问的某个HTML或其他类型的 Web页中包含有其他的Web资源,如JavaScript文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会建立一个HTTP会话
从 HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头有加入这行代码:Connection:keep-alive。比如:Keep-Alive: timeout=20,表示这个TCP通道可以保持20秒。另外还可能有max=XXX,表示这个长连接最多接收XXX次请求就断开。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接
5.tcp与udp的区别
1.TCP是面向连接的协议,发送数据前要先建立连接,TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢失,没有重复,并且按顺序到达;UDP是无连接的协议,发送数据前不需要建立连接,是没有可靠性;
2.TCP通信类似于于要打个电话,接通了,确认身份后,才开始进行通行;UDP通信类似于学校广播,靠着广播播报直接进行通信。
3.TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多;
4.TCP是面向字节流的,UDP是面向报文的; 面向字节流是指发送数据时以字节为单位,一个数据包可以拆分成若干组进行发送,而UDP一个报文只能一次发完。
5.TCP首部开销(20字节)比UDP首部开销(8字节)要大
6.UDP 的主机不需要维持复杂的连接状态表
6.GET与POST的区别
1.get重点在从服务器上获取资源,post重点在向服务器发送数据;
2.Get传输的数据量小,因为受URL长度限制,但效率较高; Post可以传输大量数据,所以上传文件时只能用Post方式;
3.get是不安全的,因为get请求发送数据是在URL上,是可见的,可能会泄露私密信息,如密码等; post是放在请求头部的,是安全的
7.cookie与session的区别
1.cookie
是由服务器发给客户端的特殊信息,以文本的形式存放在客户端;客户端再次请求的时候,会把Cookie回发;服务器接收到后,会解析Cookie生成与客户端相对应的内容;
2.session
服务器端的机制,在服务器上保存的信息;解析客户端请求并操作session id ,按需保存状态信息;
3.单个cookie保存的数据不能超过4K,session无此限制
4.session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。
8.cdn原理
CND 一般包含分发服务系统、负载均衡系统和管理系统
9.一次完整的HTTP请求所经历几个步骤?
1. 建立TCP连接:三次握手
2.Web浏览器向Web服务器发送请求行:一旦建立了TCP连接,Web浏览器就会向Web服务器发送请求命令。例如:GET /sample/hello.jsp
HTTP/1.1。
HTTP/1.1。
3. Web浏览器发送请求头:浏览器发送其请求命令之后,还要以头信息的形式向Web服务器发送一些别的信息,之后浏览器发送了一空白行来通知服务器,它已经结束了该头信息的发送。
4. Web服务器应答:客户机向服务器发出请求后,服务器会客户机回送应答, HTTP/1.1 200 OK ,应答的第一部分是协议的版本号和应答状态码。
5. Web服务器发送应答头:正如客户端会随同请求发送关于自身的信息一样,服务器也会随同应答向用户发送关于它自己的数据及被请求的文档。
6. Web服务器向浏览器发送数据:Web服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据。
7. Web服务器关闭TCP连接:四次握手
2.Java集合
集合部分接口的继承关系图
Collection:Collection 是集合 List、Set、Queue 的最基本的接口
List
ArrayList
排列有序,可重复
底层使用的数组结构
查询速度快,增删比较慢,getter()和setter()方法比较快
线程不安全
当容量不够时,ArrayList扩容为当前容量*1.5+1
Vector
排列有序,可重复
底层使用的数组结构
查询速度快,增删比较慢,getter()和setter()方法比较快
线程安全,效率低
当容量不够时,Vector默认扩容为一倍的容量
Linkedlist
排列有序,可重复
底层使用的双向循环链表数据结构
查询速度慢,增删快,add()和remove()方法比较快
线程不安全
Set
HashSet
排列无序,不可重复
底层使用hash表实现
存取速度快
内部是hashmap
TreeSet
排列无序,不可重复
底层使用二叉树
排序存储
内部是TreeMap的SortedSet
LinkedHashSet
采用hash表存储,并用双向链表记录插入顺序
内部是LinkedHashMap
Queue
Iterator:迭代器,可以通过迭代器遍历集合中的数据
Map:是映射表的基础接口
HashMap
键不可重复、值可以重复
底层是哈希表
线程不安全
允许key值为null,value也可以为null
hashtable
键不可重复、值可以重复
底层是哈希表
线程安全
key、value都不可以为null
TreeMap
键不可重复、值可以重复
底层是二叉树
hashmap的实现原理
jdk1.7(数组+链表)
大方向上,hashmap里面是一个数组,然后数组上每个元素是一个单向的链表,每个元素的实体是嵌套类Entry的实例,Entry实例包含四个属性:key、value、hash值、用于单向链表的next
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍
loadFactor:负载因子,默认为 0.75
threshold:扩容的阈值,等于 capacity * loadFactor
loadFactor:负载因子,默认为 0.75
threshold:扩容的阈值,等于 capacity * loadFactor
jdk1.8(数组+链表+红黑树)
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。
查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
当链表长度大于或等于阈值(默认为 8)的时候,如果同时还满足容量大于或等于 MIN_TREEIFY_CAPACITY(默认为 64)的要求,就会把链表转换为红黑树。同样,后续如果由于删除或者其他原因调整了大小,当红黑树的节点小于或等于 6 个以后,又会恢复为链表形态。
通常如果 hash 算法正常的话,那么链表的长度也不会很长,那么红黑树也不会带来明显的查询时间上的优势,反而会增加空间负担。所以通常情况下,并没有必要转为红黑树,所以就选择了概率非常小,小于千万分之一概率,也就是长度为 8 的概率(当长度为 8 的时候,概率仅为 0.00000006),把长度 8 作为转化的默认阈值。
通常如果 hash 算法正常的话,那么链表的长度也不会很长,那么红黑树也不会带来明显的查询时间上的优势,反而会增加空间负担。所以通常情况下,并没有必要转为红黑树,所以就选择了概率非常小,小于千万分之一概率,也就是长度为 8 的概率(当长度为 8 的时候,概率仅为 0.00000006),把长度 8 作为转化的默认阈值。
缺点
线程不安全,在并发环境下,可能会形成环状链表,导致get操作时CPU空转,所以,在并发环境下使用hashmap是非常危险的。
currenthashmap的实现原理
jdk1.7(ReentrantLock+Segment+HashEntry)
容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
jdk1.8(synchronized+CAS+HashEntry+红黑树)
采用的与hashmap的底层数据结构一样:数组+链表+红黑树
JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8实现降低锁的粒度就是HashEntry(首节点)
JDK1.8版本的数据结构变得更加简单,去掉了Segment这种数据结构,使用synchronized来进行同步锁粒度降低,所以不需要分段锁的概念,实现的复杂度也增加了
JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock
低粒度加锁方式,synchronized并不比ReentrantLock差;粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存
经典面试题
1.Collection 和 Collections 有什么区别?
java.util.Collection是一 个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统-操作方式, 其直接继承接口有List与Set。
Collections则是集合类的一 个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
Collections则是集合类的一 个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。
2.Hashmap的扩容操作是怎么实现的?
Vector每次扩容增加一倍
Arraylist每次扩容增加50%
Hashmap通过resize方法,每次扩容2倍,负载因子默认0.75F
Arraylist每次扩容增加50%
Hashmap通过resize方法,每次扩容2倍,负载因子默认0.75F
3.Hashmap是怎么解决哈希冲突的?
哈希:hash一般翻译为散列,hash就是指使用哈希算法把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值
哈希冲突:当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们把它叫做哈希碰撞
解决方案:
1.链表法就是将相同的hash值的对象组织成一个链表放在hash值对应的槽位
2.开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
4.能否使用任何类作为 Map 的 key?
可以使用任何类作为Map的key,然而在使用之前,需要考虑以下几点:
●如果类重写了equals()方法,也应该重写hashCode()方法。
●类的所有实例需要遵循与equals0和hashCode()相关的规则。
●如果一个类没有使用equals(), 不应该在hashCode()中使用它。
●用户自定义Key类最佳实践是使之为不可变的,这样hashCode()值可以被缓存起来,拥有更好的性能。不可变的类也可以确保hashCode()和equals)在未来不会改变,这样就会解决与可变相关的问题了。
●如果类重写了equals()方法,也应该重写hashCode()方法。
●类的所有实例需要遵循与equals0和hashCode()相关的规则。
●如果一个类没有使用equals(), 不应该在hashCode()中使用它。
●用户自定义Key类最佳实践是使之为不可变的,这样hashCode()值可以被缓存起来,拥有更好的性能。不可变的类也可以确保hashCode()和equals)在未来不会改变,这样就会解决与可变相关的问题了。
5.HashMap 与 HashTable 有什么区别?
1.线程安全: HashMap 是非线程安全的, HashTable 是线程安全的; HashTable 内部的方法基本都经过synchronized 修饰。(如果 你要保证线程安全的话就使用ConcurrentHashMap ) ;
2.效率:因为线程安全的问题, HashMap 要比HashTable效率高- -点. 另外,HashTable 基本被淘汰,不要在代码中使用它; (如果 你要保证线程安全的话就使用ConcurrentHashMap ) ;
3.对Null key和Null value的支持: HashMap 中, null 可以作为键,这样的键只有-一个,可以有一个或多个键所对应的值为null。但是在HashTable中put进的键值只要有一个null,直接抛NullPointerException.
4.初始容量大小和每次扩充容量大小的不同: 创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
5.创建时如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充为2的幕次方大小。也就是说HashMap总是使用2的幕作为哈希表的大小,后面会介绍到为什么是2的幂次方。
6.底层数据结构: JDK1.8 以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
7.推荐使用:在Hashtable的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用HashMap替代,如果需要多线程使用则用ConcurrentHashMap替代。
6.如何决定使用 HashMap 还是 TreeMap?
1.TreeMap是一个有序的key-value集合,它是通过红黑树实现的。TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法。TreeMap是线程非同步的。
2.对于在Map中插入、删除和定位元素这类操作, HashMap是最好的选择。 然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。 基于你的Collection的大小, 也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。
7.ConcurrentHashMap 和 Hashtable 的区别?
答: ConcurrentHashMap结合了HashMap和HashTable二者的优势。HashMap 没有考虑同步,HashTable考虑了同步的问题使用了synchronized关键字,所以HashTable在每次同步执行时都要锁住整个结构。ConcurrentHashMap 锁的方式是稍微细粒度的。
ConcurrentHashMap和Hashtable的区别主要体现在实现线程安全的方式上不同。
底层数据结构: JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,JDK1.8采用的数据结构跟HashMap1.8的结构-样,数组+链表/红黑二叉树。Hashtable 和JDK1.8之前的HashMap的底层数据结构类似都是采用数组+链表的形式,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式:
1.在JDK1.7的时候,ConcurrentHashMap (分段锁)对整个桶数组进行了分割分段(Segment),每-把锁只锁容器其中-部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment, 比Hashtable效率提高16倍。)到了JDK1.8的时候已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。JDK1.6以后 对synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本;
2.②Hashtable(同-把锁) :使用synchronized来保证线程安全,效率非常低下。当-个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。
底层数据结构: JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,JDK1.8采用的数据结构跟HashMap1.8的结构-样,数组+链表/红黑二叉树。Hashtable 和JDK1.8之前的HashMap的底层数据结构类似都是采用数组+链表的形式,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的;
实现线程安全的方式:
1.在JDK1.7的时候,ConcurrentHashMap (分段锁)对整个桶数组进行了分割分段(Segment),每-把锁只锁容器其中-部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment, 比Hashtable效率提高16倍。)到了JDK1.8的时候已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。JDK1.6以后 对synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本;
2.②Hashtable(同-把锁) :使用synchronized来保证线程安全,效率非常低下。当-个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。
8.HashMap 和 ConcurrentHashMap 的区别?
1. ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段 上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一 些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(DK1.8之 后ConcurrentHashMap启用了- -种全新的方式实现利用CAS算法。)
2. HashMap的键值对允许有null,但是ConCurrentHashMap都不允许.
9.LinkedHashMap 的应用
基于 LinkedHashMap 的访问顺序的特点,可构造一个 LRU(Least Recently Used) 最近最少使用简单缓存。也有一些开源的缓存产品如 ehcache 的淘汰策略( LRU )就是在 LinkedHashMap 上扩展的。
hashmap夺命14问
1. HashMap的底层数据结构是什么?
在JDK1.7中,由”数组+链表“组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
在JDK1.8中,有“数组+链表+红黑树”组成。当链表过长,则会严重影响HashMap的性能,红黑树搜索时间复杂度是O(logn),而链表是O(n)。因此,JDK1.8对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:
(1)当链表超过8且数组长度(数据总量)超过64才会转为红黑树
(2)将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。
(1)当链表超过8且数组长度(数据总量)超过64才会转为红黑树
(2)将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。
2.说一下HashMap的特点
1.hashmap存取是无序的
2.键和值位置都可以是null,但是键位置只能是一个null
3.键位置是唯一的,底层的数据结构是控制键的
4.jdk1.8前数据结构是:链表+数组jdk1.8之后是:数组+链表+红黑树
5.阈值(边界值)>8并且数组长度大于64,才将链表转换成红黑树,变成红黑树的目的是提高搜索速度,高效查询
2.键和值位置都可以是null,但是键位置只能是一个null
3.键位置是唯一的,底层的数据结构是控制键的
4.jdk1.8前数据结构是:链表+数组jdk1.8之后是:数组+链表+红黑树
5.阈值(边界值)>8并且数组长度大于64,才将链表转换成红黑树,变成红黑树的目的是提高搜索速度,高效查询
3. 解决hash冲突的办法有哪些?HashMap用的哪种?
解决Hash冲突方法有:开放定址法、再哈希法、链地址法(HashMap中常见的拉链法)、建立公共溢出区。HashMap中采用的是链地址法。
开放定址法:
开放定址法也称为再散列法,基本思想就是,如果p=H(key)出现冲突时,则以p为基础,再次hash,p1=H(p),如果p1再次出现冲突,则以p1为基础,以此类推,直到找到一个不冲突的哈希地址pi。因此开放定址法所需要的hash表的长度要大于等于所需要存放的元素,而且因为存在再次hash,所以只能在删除的节点上做标记,而不能真正删除节点
再哈希法
再哈希法(双重散列,多重散列),提供多个不同的hash函数,R1=H1(key1)发生冲突时,再计算R2=H2(key1),直到没有冲突为止。这样做虽然不易产生堆集,但增加了计算的时间。
链地址法(拉链法)
链地址法(拉链法),将哈希值相同的元素构成一个同义词的单链表,并将单链表的头指针存放在哈希表的第i个单元中,查找、插入和删除主要在同义词链表中进行,链表法适用于经常进行插入和删除的情况
建立公共溢出区
建立公共溢出区,将哈希表分为公共表和溢出表,当溢出发生时,将所有溢出数据统一放到溢出区
开放定址法和再哈希法的区别是
开放定址法只能使用同一种hash函数进行再次hash,再哈希法可以调用多种不同的hash函数进行再次hash
4. 为什么要在数组长度大于64之后,链表才会进化为红黑树
在数组比较小时如果出现红黑树结构,反而会降低效率,而红黑树需要进行左旋右旋,变色,这些操作来保持平衡,同时数组长度小于64时,搜索时间相对要快些,总之是为了加快搜索速度,提高性能
JDK1.8以前HashMap的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当HashMap中有大量的元素都存放在同一个桶中时,这个桶下有一条长长的链表,此时HashMap就相当于单链表,假如单链表有n个元素,遍历的时间复杂度就从O(1)退化成O(n),完全失去了它的优势,为了解决此种情况,JDK1.8中引入了红黑树(查找的时间复杂度为O(logn))来优化这种问题
5. 为什么加载因子设置为0.75,初始化临界值是12?
HashMap中的threshold是HashMap所能容纳键值对的最大值。计算公式为length*LoadFactory。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数也越大
loadFactory越趋近于1,那么数组中存放的数据(entry也就越来越多),数据也就越密集,也就会有更多的链表长度处于更长的数值,我们的查询效率就会越低,当我们添加数据,产生hash冲突的概率也会更高
默认的loadFactory是0.75,loadFactory越小,越趋近于0,数组中个存放的数据(entry)也就越少,表现得更加稀疏
0.75是对空间和时间效率的一种平衡选择
如果负载因子小一些比如是0.4,那么初始长度16*0.4=6,数组占满6个空间就进行扩容,很多空间可能元素很少甚至没有元素,会造成大量的空间被浪费
如果负载因子大一些比如是0.9,这样会导致扩容之前查找元素的效率非常低
loadfactory设置为0.75是经过多重计算检验得到的可靠值,可以最大程度的减少rehash的次数,避免过多的性能消耗
如果负载因子大一些比如是0.9,这样会导致扩容之前查找元素的效率非常低
loadfactory设置为0.75是经过多重计算检验得到的可靠值,可以最大程度的减少rehash的次数,避免过多的性能消耗
6. 哈希表底层采用何种算法计算hash值?还有哪些算法可以计算出hash值?
hashCode方法是Object中的方法,所有的类都可以对其进行使用,首先底层通过调用hashCode方法生成初始hash值h1,然后将h1无符号右移16位得到h2,之后将h1与h2进行按位异或(^)运算得到最终hash值h3,之后将h3与(length-1)进行按位与(&)运算得到hash表索引
其他可以计算出hash值的算法有
平方取中法
取余数
伪随机数法
7. 当两个对象的hashCode相等时会怎样
hashCode相等产生hash碰撞,hashCode相等会调用equals方法比较内容是否相等,内容如果相等则会进行覆盖,内容如果不等则会连接到链表后方,链表长度超过8且数组长度超过64,会转变成红黑树节点
8. 何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞?
只要两个元素的key计算的hash码值相同就会发生hash碰撞,jdk8之前使用链表解决哈希碰撞,jdk8之后使用链表+红黑树解决哈希碰撞
9. HashMap的put方法流程
以jdk8为例,简要流程如下:
1.首先根据key的值计算hash值,找到该元素在数组中存储的下标
2.如果数组是空的,则调用resize进行初始化;
3.如果没有哈希冲突直接放在对应的数组下标里
4.如果冲突了,且key已经存在,就覆盖掉value
5.如果冲突后是链表结构,就判断该链表是否大于8,如果大于8并且数组容量小于64,就进行扩容;如果链表节点数量大于8并且数组的容量大于64,则将这个结构转换成红黑树;否则,链表插入键值对,若key存在,就覆盖掉value
6.如果冲突后,发现该节点是红黑树,就将这个节点挂在树上
10. HashMap的扩容方式
HashMap在容量超过负载因子所定义的容量之后,就会扩容。java里的数组是无法自己扩容的,将HashMap的大小扩大为原来数组的两倍
扩容之后原位置的节点只有两种调整
1.保持原位置不动(新bit位为0时)
2.散列原索引+扩容大小的位置去(新bit位为1时)
扩容之后元素的散列设置的非常巧妙,节省了计算hash值的时间
1.保持原位置不动(新bit位为0时)
2.散列原索引+扩容大小的位置去(新bit位为1时)
扩容之后元素的散列设置的非常巧妙,节省了计算hash值的时间
当数组长度从16到32,其实只是多了一个bit位的运算,我们只需要在意那个多出来的bit为是0还是1,是0的话索引不变,是1的话索引变为当前索引值+扩容的长度,比如5变成5+16=21
这样的扩容方式不仅节省了重新计算hash的时间,而且保证了当前桶中的元素总数一定小于等于原来桶中的元素数量,避免了更严重的hash冲突,均匀的把之前冲突的节点分散到新的桶中去
11. 一般用什么作为HashMap的key?
一般用Integer、String这种不可变类当HashMap当key
因为String是不可变的,当创建字符串时,它的hashcode被缓存下来,不需要再次计算,相对于其他对象更快
因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的,这些类很规范的重写了hashCode()以及equals()方法
12. 为什么Map桶中节点个数超过8才转为红黑树?
树节点占用空间是普通Node的两倍,如果链表节点不够多却转换成红黑树,无疑会耗费大量的空间资源,并且在随机hash算法下的所有bin节点分布频率遵从泊松分布,链表长度达到8的概率只有0.00000006,几乎是不可能事件,所以8的计算是经过重重科学考量的
从平均查找长度来看,红黑树的平均查找长度是logn,如果长度为8,则logn=3,而链表的平均查找长度为n/4,长度为8时,n/2=4,所以阈值8能大大提高搜索速度
当长度为6时红黑树退化为链表是因为logn=log6约等于2.6,而n/2=6/2=3,两者相差不大,而红黑树节点占用更多的内存空间,所以此时转换最为友好
13. HashMap为什么线程不安全?
多线程下扩容死循环。JDK1.7中的HashMap使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题
多线程的put可能导致元素的丢失。多线程同时执行put操作,如果计算出来的索引位置是相同的,那会造成前一个key被后一个key覆盖,从而导致元素的丢失。此问题在JDK1.7和JDK1.8中都存在
put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,有可能导致这个问题,此问题在JDK1.7和JDK1.8中都存在
14. 计算hash值时为什么要让低16bit和高16bit进行异或处理
我们计算索引需要将hashCode值与length-1进行按位与运算,如果数组长度很小,比如16,这样的值和hashCode做异或实际上只有hashCode值的后4位在进行运算,hash值是一个随机值,而如果产生的hashCode值高位变化很大,而低位变化很小,那么有很大概率造成哈希冲突,所以我们为了使元素更好的散列,将hash值的高位也利用起来
15.9. HashMap的get方法流程
第一步:通过hash(key)方法获取key 在node数组下的位置。判断数组是否为空并且数组长度是否大于0并且数组索引下第一个节点是否为空。
第二步:查看第一个结点的是否满足返回条件,满足则直接返回
第三步:如果不满足 则判断 数组下是链表还是红黑树 都通过equals 方法进行比对key是否相同,相同则返回Node节点
第四步:全不满足,返回null
第一步:通过hash(key)方法获取key 在node数组下的位置。判断数组是否为空并且数组长度是否大于0并且数组索引下第一个节点是否为空。
第二步:查看第一个结点的是否满足返回条件,满足则直接返回
第三步:如果不满足 则判断 数组下是链表还是红黑树 都通过equals 方法进行比对key是否相同,相同则返回Node节点
第四步:全不满足,返回null
3.jvm
1.jvm的类加载机制
1.加载-验证-准备-解析-初始化-使用-卸载
2.双亲委派机制
1.什么是双亲委派机制?
加载器分为引导类加载器、扩展类加载器、应用类加载器。其中引导类加载器为扩展类加载器的父类加载器,扩展类加载器为应用类加载器的父类,向上委托。
2.为什么要使用双亲委派机制?
1.避免类的重复加载,当父加载器已经加载了该类时,就没有必要子classloader再加载一次,保证了加载类的一致性
2.沙箱安全机制,自己编写的java.lang.String.class类不会被加载,这样便可以防止核心API类库被篡改。
3.95%以上的类都是由应用类加载器进行加载的。这样丛下往上进行加载,第一次加载时可能比较慢,但是在之后运用的时候就查找比较快;如果换成从上往下进行加载,第一次可能比较快,可之后的大量查找会比较慢。
4.引导类加载器是由C++实现的,也比较快,进而减少了加载时间。
2.java内存模型
1.线程私有
栈
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息.每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
本地方法栈
本地方法区和 Java Stack 作用类似, 区别是虚拟机栈为执行 Java 方法服务, 而本地方法栈则为Native 方法服务, 如果一个 JVM 实现使用 C-linkage 模型来支持 Native 调用, 那么该栈将会是一个C 栈,但 HotSpot VM 直接就把本地方法栈和虚拟机栈合二为一。
程序计数器
一块较小的内存空间, 是当前线程所执行的字节码的行号指示器,每条线程都要有一个独立的程序计数器,这类内存也称为“线程私有”的内存。
2.线程共享
堆
新生代与老年代的比例为1:2
新生代中分为Eden区和Survivor区(From Survivor和ToSurvivor),比例为8:1:1
是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域
现代 VM 采用分代收集算法, 因此 Java 堆从 GC 的角度还可以细分为: 新生代(Eden 区、From Survivor 区和 To Survivor 区)和老年代
方法区(元空间)
用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据.
3.jvm的调优
1.java异常结构:Throwable
1.Error
1.虚拟机错误 VirtualMachineError
2.内存溢出OutOfMemoryError
1.堆溢出-java.lang.OutOfMemoryError:Java heap space
1.示例:堆内对象不能进行回收,堆内存持续增大,这样达到了堆内存的最大值,数据满了,所以就溢出了。
2.解决方案:找到问题点,分析哪个地方是否存储了大量类没有进行回收,通过JMAP命令将线上的堆内存导出来之后进行分析;
3.可通过设置jvm参数中的 -Xmx100m,设置最大的堆内存进行错误呈现;
2.栈溢出-java.lang.OutOfMemoryError
1.示例:无限的创建线程,直到线程无法进行创建,则会抛出该异常;
2.可通过设置jvm参数中的 -Xss512k,设置栈帧的大小,默认为1M
3.栈溢出-java.lang.StackOverFlowError
1.示例:死循环的递归调用
2.程序每次进行递归的时候,会将结果数据压入栈,包括里面的指针等,这个时候就需要帧栈大一些才能承受更多的递归调用;通过设置-Xss进行调整;
3.解决方案:通过jstack将线程数据导到文件进行分析;找到递归的点,如果程序就是需要递归的次数的话,那么这个时候就需要增加帧栈的大小以适应程序。
4.元信息溢出-java.lang.OutOfMemoryError:Metaspace
1.元数据区存储着类的相关信息、常量池、静态变量、方法描述符、字段描述符,运行时产生大量的类会造成这个区域的溢出;
2.产生情况:通过CBLIG大量的生成类,导致元数据空间满了;jdk1.7的时候运用String.inten()不当,产生了大量的常量数据;加载jsp以及动态生成jsp文件;
3.通过jvm的参数:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize:100M进行设置;如果调大了之后还是出现异常,就需要分析哪里出现的溢出并且fix掉;
5.直接内存溢出-java.lang.OutOfMemoryError:Direct buffer memory
1.直接内存也就是堆外内存。一般出现在程序中使用了NIO,比如netty。NIO为了提高性能,避免在java heap和native heap之间切换,所以使用直接内存。默认情况下,堆外内存与堆内存大小一致,堆外内存不受jvm的限制,但是受制于机器整体内存的大小限制。
2.解决思路:这种情况一般是我们使用了直接内存造成的溢出,这个时候我们需要检查一下程序里面是否使用了NIO,比如说netty,检查一下里面的直接内存配置。
6.GC超限-java.lang.OutOfMemoryError:GC overload limit exceeded
1.如果98%的GC回收不到2%的空间的时候就会报这个错误,也就是最大最小内存出现了问题。
2.比如我们创建了一个线程池,如果线程池执行的时候核心线程数处理不过来的时候会把数据放到LinkedBlockingQueue里面,也就是放在了堆内存中。这个时候就需要检查一下- Xms /-Xmx最大最小堆内存空间设置的是否合理;再一个dump出现当前内存来分析一下是否使用了大量的循环或者是使用了大量内存的代码。
3.GC排查工具:jvisualvm、arthas
3.线程死锁 ThreadDeath
2.Exception
1.运行时异常:RuntimeException
1.运行时异常,这种异常我们不需要进行处理,完全由虚拟机接管。写程序时不需要进行catch或者throw
2.NullPointerException-空指针引用异常
3.ClassCastException - 类型强制转换异常
4.IllegalArgumentException - 传递非法参数异常
5.ArithmeticException - 算术运算异常
6.ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
7.IndexOutOfBoundsException - 下标越界异常
8.NumberFormatException - 数字格式异常
9.NegativeArraySizeException - 创建一个大小为负数的数组错误异常
10.SecurityException - 安全异常
11.UnsupportedOperationException - 不支持的操作异常
2.IO异常:IOException
3.SQL异常:SQLException
2.JC的排查工具
1.jvisualvm
2.arthas阿里调优工具
3.jconsole
4.MAT(Memory Analyzer)
3.jvm常见的参数配置
1.堆设置
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
2.垃圾收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
3.垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
4.并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
5.并发收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
3.JVM调优案例1:能否对jvm进行调优,让其几乎不发生Full GC?
1.问题:线程运行每秒产生60M对象,运行25秒即占满了eden区
2.解决:增加eden区s区的大小,让朝生夕死的对象尽量在年轻代就被干掉,不要被挪进老年代,从而触发Full GC
4.JVM调优案例2:对于大内存的如何进行优化?
1.可以使用G1垃圾收集器,设置目标暂停时间,默认为200ms;边找边计算,这块区域需要GC的时间接近200ms,就会进行GC
2.通过设置jvm的参数:-XX:MaxGCPauseMills:目标暂停时间(默认为200ms)
5.JVM调优案例3:递归方法造成的栈内存的溢出
1.问题:在递归中,不能无限制的调用自己,必须要有边界条件,能够让递归结束,因为每一次递归调用都会在栈内存中开辟新的空间,重新执行方法,如果递归的层次太深,很容易造成栈内存溢出-java.lang.StackOverFlowError
2.解决方案:通过jstack将线程数据导到文件进行分析;找到递归的点,如果程序就是需要递归的次数的话,那么这个时候就需要增加帧栈的大小以适应程序。
6.CPU百分百如何排查?
1.问题解析:一般CPU占用100%都是死循环或者是大对象的锅
2.解决方案:
1.使用top -c命令找出当前进程的运行列表,按一下P可以按照CPU的使用率进行排序,这样可以找到哪个进程消耗CPU最高,比如PID为886
2.使用top -Hp 886 找出这个进程下面的线程,按P进行排序,比如线程 9886消耗最高
3.9886是十进制的,我们需要转换为16进制:269e;接下来导出我们的进程快照:jstack -l 886>./886.stack
4.使用grep查看一下线程在文件中做了什么:cat 886.stack | grep '269e' -C 8
5.这样我们就能定位到死循环的那个类,还可以看到一个epoll函数的调用错误
7.内存爆满如何排查
1.使用top -c命令找出当前进程的运行列表,按一下P可以按照CPU的使用率进行排序,这样可以找到哪个进程消耗CPU最高,比如PID为886
2.使用top -Hp 886 找出这个进程下面的线程,按P进行排序,比如线程 9886消耗最高
2. 获取内存dump: jmap -histo:live pid
这种方式会先出发fullgc,所有如果不希望触发fullgc 可以使用jmap -histo pid
2.使用top -Hp 886 找出这个进程下面的线程,按P进行排序,比如线程 9886消耗最高
2. 获取内存dump: jmap -histo:live pid
这种方式会先出发fullgc,所有如果不希望触发fullgc 可以使用jmap -histo pid
8.java获取内存dump的几种方式
1、获取内存详情:jmap -dump:format=b,file=e.bin pid
这种方式可以用 jvisualvm.exe 进行内存分析,或者采用 Eclipse Memory Analysis Tools (MAT)这个工具
这种方式可以用 jvisualvm.exe 进行内存分析,或者采用 Eclipse Memory Analysis Tools (MAT)这个工具
2. 获取内存dump: jmap -histo:live pid
这种方式会先出发fullgc,所有如果不希望触发fullgc 可以使用jmap -histo pid
这种方式会先出发fullgc,所有如果不希望触发fullgc 可以使用jmap -histo pid
3.第三种方式:jdk启动加参数:
-XX:+HeapDumpBeforeFullGC
-XX:HeapDumpPath=/httx/logs/dump
这种方式会产生dump日志,再通过jvisualvm.exe 或者Eclipse Memory Analysis Tools 工具进行分析
-XX:+HeapDumpBeforeFullGC
-XX:HeapDumpPath=/httx/logs/dump
这种方式会产生dump日志,再通过jvisualvm.exe 或者Eclipse Memory Analysis Tools 工具进行分析
4.垃圾回收
1.判断哪些对象已经死亡
1.引用计数法---无法解决循环引用的问题
2.可达性分析算法
经过一系列成为GCRoots的点作为起点,向下搜索,当一个对象到任何GC Roots都没有引用链相连,说明其已经死亡
GC Roots
JVM栈中的引用
元空间中的静态引用
JNI中的引用(一般说的natice中的方法)
锁的引用
2.垃圾回收机制
1.标记清除
适用场景
对象存活比较多的时候适用
适合老年代
缺点
提前GC
容易产生碎片空间
扫描了2次:标记存活对象、清除没有标记的对象
2.复制copy
适用场景
存活对象少,比较高效
扫描整个空间(标记存活对象并且复制移动)
适合年轻代
缺点
需要空闲空间
需要复制移动对象
3.标记整理
标记后不是去清理对象,而是将存活的对象移动到内存的一端。然后清除端边界外的对象。
4.分带收集
目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的区域,一般情况下将GC堆划分为老年代和年轻代。
老年代的特点是每次垃圾回收时只有少量的对象需要被回收,年轻代的特点是每次垃圾回收时都有大量的垃圾需要被回收,因此可以根据不同的区域选择不同的算法
年轻代一般采用复制算法,因为年轻代每次垃圾回收都要回收大量的对象,即需要进行复制的对象比较少,一般将年轻代划分为一块较大的eden区和两块Survivor(From Space、To Space),每次使用Eden区和其中的一块Survivor区,当进行回收时,将两块空间中还存活的对象复制到另外的一块Survicor中
老年代因为每次回收的对象比较少,一般采用标记整理(mark-compact)算法。
回收步骤
1.JAVA 虚拟机提到过的处于方法区的元空间,它用来存储 class 类,常量,方法描述等。对元空间的回收主要包括废弃常量和无用的类。
2.对象的内存分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放对象的那一块),少数情况会直接分配到老生代。
3.当新生代的 Eden Space 和 From Space 空间不足时就会发生一次 GC,进行 GC 后,Eden Space 和 From Space 区的存活对象会被挪到 To Space,然后将 Eden Space 和 From Space 进行清理。
4.如果 To Space 无法足够存储某个对象,则将这个对象存储到老生代。
5.在进行 GC 后,使用的便是 Eden Space 和 To Space 了,如此反复循环。
6.当对象在 Survivor 区躲过一次 GC 后,其年龄就会+1。默认情况下年龄到达 15 的对象会被移到老年代中。之所以最大设置为15,是因为在对象头markword中表示的对象的分带年龄大小为4bit,最大值为15
3.java中的四种引用类型
1.强引用
把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,他处于可达状态,他是不能够被垃圾回收期回收的,即
使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
2.软引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
3.弱引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
4.虚引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
4.垃圾回收器
1.新生代
Serial
单线程、复制算法
java虚拟机运行在Client模式下的默认新生代垃圾收集器
Parnew
Serial+多线程
是Serial的多线程版本,java虚拟机运行在Server模式下的默认新生代垃圾收集器
Paraller Scavenge
多线程复制算法、高效
它重点关注的是程序达到一个可控制的吞吐量,高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而
不需要太多交互的任务。
不需要太多交互的任务。
自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别
2.老年代
Serial Old
单线程、标记整理算法
是Serial垃圾收集器的老年代版本
java虚拟机运行在Client模式下的默认老年代垃圾收集器
在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。作为年老代中使用 CMS 收集器的后备垃圾收集方案。
Parallel Old
多线程、标记整理算法
Parallel Old 收集器是Parallel Scavenge的年老代版本。
Parallel Old 正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
CMS(Concurrent Mark Sweep)
多线程、标记清除算法
其最主要目标是获取最短垃圾回收停顿时间,可以为交互比较高的程序提高用户体验
缺点
对CPU资源敏感
无法处理浮动的垃圾
基于标记清除算法,产生大量的垃圾碎片
收集步骤
初始标记 :stw 从gc root 开始直接可达的对象
并发标记: gc root 对对象进行可达性分析 找出存活对象 可达性分析算法,不需要stw
最终标记:stw 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并
发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看
CMS 收集器的内存回收和用户线程是一起并发地执行
发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看
CMS 收集器的内存回收和用户线程是一起并发地执行
G1
基于标记整理算法,不产生垃圾碎片,分配大对象不会提前GC
可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。充分利用cpu,多核条件下缩短stw
收集步骤
初始标记 :stw 从gc root 开始直接可达的对象
并发标记: gc root 对对象进行可达性分析 找出存活对象 可达性分析算法,不需要stw
最终标记:stw 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
并发清除:根据用户期待的gc停顿时间指定回收计划
回收模式
young gc 回收所有的eden s区 复制一些存活对象到old区s区
mixed gc
与cms的区别
g1分区域 每个区域是有老年代概念的 但是收集器以整个区域为单位收集
g1回收后马上合并空闲内存 cms 在stw的时候做
设置参数
XX:G1HeapRegionSize
复制成活对象到一个区域 暂停所有线程stw
5.面试题补充
1.哪些情况下的对象会被垃圾回收机制处理掉?
利用可达性分析算法,虚拟机会将一些对象定义为 GC Roots,从 GC Roots 出发沿着引用链 向下寻找,如果某个对象不能通过 GC Roots 寻找到,虚拟机就认为该对象可以被回收掉。
2.哪些对象可以被看做是 GC Roots 呢?
虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中的类静态属性引用的对象,常量引用的对象;
本地方法栈中 JNI(Native 方法)引用的对象;
3.对象不可达,一定会被垃圾收集器回收么?
即使不可达,对象也不一定会被垃圾收集器回收,1)先判断对象是否有必要执行 finalize() 方法,对象必须重写 finalize()方法且没有被运行过。2)若有必要执行,会把对象放到一个 队列中,JVM 会开一个线程去回收它们,这是对象最后一次可以逃逸清理的机会。
4.Mysql
1.为什么要使用索引
1.避免全表扫描,提升查询的效率
2.主键、普通键、唯一键都可以作为索引
2.索引创建的原则有哪些
1.选择唯一性索引:唯一性索引的值是唯一的,可以更快速的通过索引来确定某条记录
2.为经常需要排序、分组和联合操作的字段建立索引
3.经常作为查询条件的字段建立索引
4.限制索引的数目:越多的索引,会耗费大量时间进行索引维护,会似表更新变得很浪费时间
5.尽量使用数据量少的索引,如果索引的值很长,那么查询的速度会受到影响
6.尽量使用前缀来作为索引
7.删除很少使用或者不再使用的索引
8.最左匹配原则
9.尽量选择区分度高的列作为索引,区分度高是指字段不重复的列
10.索引列不参与计算,保持列干净,带函数的查询不参与索引
11.尽量的扩展索引,而不是新建索引
2.索引的数据结构
1.二叉树
1.对半搜索,左孩子右孩子,每个节点最多2个孩子
2.左子树的键值小于根的键值,右子树的键值大于根的键值。
3.二叉排序树的查找性能在0(Log2n)到O(n)之间。因此,为了获得较好的查找性能,就要构造-棵平衡的二叉排序树。
2.平衡二叉树
1.符合二叉树的条件下
2.任何节点的两个子树的高度最大差为1
如果在avl 树中进行插入和删除节点操作,可能导致avl树失去平衡,那么可以通过旋转重新达到平衡。因此我们说的二叉树也称自平衡二叉树
如果在avl 树中进行插入和删除节点操作,可能导致avl树失去平衡,那么可以通过旋转重新达到平衡。因此我们说的二叉树也称自平衡二叉树
3.红黑树
1.红黑树和avl树类似,都是在进行插入和删除操作时通过特定的操作保持二叉树的平衡,从而获得较高的查找性能。
2.特点:1.节点是红色或黑色2.根节点是黑色3.叶子节点(nil,空节点)是黑色4.每个红色节点的两个子节点都是黑色
4.B-tree
1.根节点至少包括两个孩子
2.树中每个节点最多包含m个孩子(m>2)
3.除根节点和叶节点外,其他每个节点至少有ceil(m/2)个孩子,ceil是取上限,ceil(3/2)=2
4.所有的叶子节点都位于同一层
5.B+-tree
1.B+树是B树的变体,其定义基本与B树相同
2.非叶子节点仅用来索引,数据都保存在叶子节点中
3.所有的叶子节点均有一个链指针指向下一个叶子节点
4.B+Tree更适合作为存储索引
1.B+tree的磁盘读写代价更低
2.B+树的查询效率更加稳定
3.B+树更有利于对数据库的扫描
6.hash索引
仅仅能满足“=",“IN”,不能使用范围查询
无法被用来避免数据的排序操作
不能利用部分索引|键查询
不能避免表扫描
遇到大量Hash值相等的情况后性能并不一-定就会比B -Trea索引高
无法被用来避免数据的排序操作
不能利用部分索引|键查询
不能避免表扫描
遇到大量Hash值相等的情况后性能并不一-定就会比B -Trea索引高
7.bitmap索引(位图索引)
3.密集索引与稀疏索引的区别
1.密集索引(聚簇索引)
1.只有一个聚集索引,聚簇索引的叶子节点存储行记录。根据聚簇索引的 key 查找是非常快的
2.如果表定义了主键,则主键就是聚集索引;
3.如果表没有定义PK,则第一个 not NULL unique 列是聚集索引;否则,InnoDB 会创建一个隐藏的 row-id 作为聚集索引。
4.叶子节点保存的不仅仅是键值,还保存了位于同一行信息的其他列的信息,由于密集索引决定了表的物理排列顺序,一个表只能有一个物理排列顺序,所以一个表只能创建一个密集索引
2.稀疏索引
1.叶子节点仅保存了键位信息以及该行数据的地址,有的稀疏索引仅保存了键位信息及其主键,仍然需要地址或者主键信息进行定位。
5.联合索引的最左匹配原则的成因
1.最左前缀匹配原则,非常重要的原则,mysq|会一直向右匹配直到遇到范围查询(>、 <、between, like)就停止匹配,比如a = 3 andb = 4 andc> 5 andd = 6如果建立(a,b,c,d)顺序的索引, d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到, a,b,d的顺序可以任意调整。
2.=和in可以乱序,比如a= 1 andb= 2andc= 3建立(a,b,c)索引可以任意顺序,mysq|的查询优化器会帮你优化成索引可以识别的形式
3.第一个索引字段是绝对有序的,第二个、第三个就不一定了。所以如果直接使用第二个索引字段是不会走B+Tree索引查询的,这就是联合索引必须满足最左匹配原则的原因。
4.例子:比如有联合索引 [a、b、c],where 过滤条件中哪些排列组合可以用到索引?
以下排列组合都会走索引:a、ab、ac、ba、ca、abc、acb、bac、bca、cab、cba。必须有一个 a,排列组合中的顺序会被优化器优化,所以不用关心顺序。
以下排列组合不会走索引:b、c、bc、cb。因为没有 a。
关于范围查询:a=xxx and b<10 and b > 5 and c =xxx,c 字段用不到索引,因为 b 是一个范围查询,遇到范围查询就停止了。
7.Mysism与Innodb的区别是什么
1.InnoDB支持事务, MyISAM不支持.
2.InnoDB支持行级锁, MyISAM支持表级锁.
3.InnoDB支持多版本并发控制(MVVC), MyISAM不支持.
4.InnoDB支持外键, MyISAM不支持.
5.MyISAM支持全文索引, InnoDB部分版本不支持(但可以使用Sphinx插件).
8.myisam与innodb适合的场景
1.myisam
1.频繁执行全表count的语句
2.对数据进行增删改的频率不高,查询非常频繁
3.没有事务
2.innodb
1.数据增删改查都相当频繁
2.可靠性要求比较高,要求支持事务
9.数据库锁的分类
按锁的粒度划分:表级锁、行级锁、页级锁
按锁级别划分:共享锁、排它锁
按加锁方式划分:自动锁、显示锁
按操作划分:DML锁、DDL锁
按使用方式划分:乐观锁、悲观锁
10.悲观锁与乐观锁
1.悲观锁
1.悲观锁是对数据提前进行加锁,依赖于数据库层面,对其他事务呈保守策略,故在高并发情况下影响效率。for update
2.乐观锁
1.乐观锁是在表中引入version字段或者时间戳,每次更新操作之前先查询出version号,更新时以该version字段作为条件,若不一致则更新失败,说明此session数据已过期,根据逻辑处理要求重新更新。
11.数据库事务的四大特性
1.原子性Atomicity:事务是一个完整的操作。事务的各步操作是不可分的(原子的);要么都执行,要么都不执行。
2.一致性Consistency:当事务完成时,数据必须处于一致状态。
3.隔离性Isolation:对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务。
4.永久性Durability:事务完成后,它对数据库的修改被永久保持,事务日志能够保持事务的永久性。
12.MVVC机制
1.MVCC(Multiversion concurrency control) 就是同一份数据保留多版本的一种方 式,进而实现并发控制。在查询的时候,通过 read view 和版本链找到对应版本的数据
2.作用:提升并发性能。对于高并发场景,MVCC 比行级锁开销更小。
3.实现原理:
MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实现。 DB_TRX_ID:当前事务 id,通过事务 id 的大小判断事务的时间顺序。 DB_ROLL_PRT:回滚指针,指向当前行记录的上一个版本,通过这个指针 将数据的多个版本连接在一起构成 undo log 版本链。 DB_ROLL_ID:主键,如果数据表没有主键,InnoDB 会自动生成主键
使用事务更新行记录的时候,就会生成版本链,执行过程如下: 1.用排他锁锁住该行; 2.将该行原本的值拷贝到 undo log,作为旧版本用于回滚;3. 修改当前行的值,生成一个新版本,更新事务 id,使回滚指针指向旧版本的 记录,这样就形成一条版本链。
12.当前读与快照度
1.快照读:读取的是快照版本。普通的 SELECT 就是快照读。通过 mvcc 来进行 并发控制的,不用加锁。快照读读到的有可能不是数据的最新版本,可能是之前的历史版本
2.当前读:读取的是最新版本。UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE 是当前读。 当前读就是加了锁的增删改查语句
3.快照读情况下,InnoDB 通过 mvcc 机制避免了幻读现象。而 mvcc 机制无法避免当前读 情况下出现的幻读现象。因为当前读每次读取的都是最新数据,这时如果两次查询中间 有其它事务插入数据,就会产生幻读
13.事务隔离级别以及各级别下的并发访问问题
1.事务的隔离级别
Serializable (串行化):可避免脏读、不可重复读、幻读的发生。通过强制事务排序,使之不可能相互冲突,从而 解决幻读问题
Repeatable read (可重复读):可避免脏读、不可重复读的发生;MySQL 的默认事务隔离级别,它确保同一 事务的多个实例在并发读取数据时,会看到同样的数据行,解决了不可重 复读的问题。
Read committed (读已提交):一个事务只能看见已经提交事务所做的改 变。可避免脏读的发生。oracle数据库默认rc级别
Read uncommitted (读未提交):最低级别,任何情况都无法保证;所有事务都可以看到其他未提交事务的执行结果
2.各级别下的并发访问问题
1.更新丢失:mysql所有事务隔离级别在数据库层面上均可避免
2.脏读:是指在一个事务处理过程里读取了另一个未提交的事务中的数据
3.不可重复读:是指在对于数据库中的某行记录,一个事务范围内多次查询却 返回了不同的数据值,这是由于在查询间隔,另一个事务修改了数据并提 交了。
4.幻读:是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围 内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行, 就像产生幻觉一样,这就是发生了幻读
5.不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不 可重复读则是读取了前一事务提交的数据。
6.幻读和不可重复读都是读取了另一条已经提交的事务,不同的是不可重复读的重点是修 改,幻读的重点在于新增或者删除
3.Innodb可重复读隔离级别下如何避免幻读
表象:快照读(非阻塞读)–InnoDB实现了伪MVCC来避免幻读。
内在:next-key锁(X锁+gap锁),当前读情况下通过next-key锁避免幻读
快照读情况下,InnoDB 通过 mvcc 机制避免了幻读现象。而 mvcc 机制无法避免当前读 情况下出现的幻读现象。因为当前读每次读取的都是最新数据,这时如果两次查询中间 有其它事务插入数据,就会产生幻读
4.RC、RR级别下的Innodb的非阻塞读如何实现
redo日志
14.经典面试题
1.MySQL查询缓存有什么弊端, 应该什么情况下使用, 8.0
版本对查询缓存有什么变更
版本对查询缓存有什么变更
查询缓存可能会失效非常频繁, 对于一个表, 只要有更新, 该表的全部查询缓存都会被清空. 因此对于频繁更新的表来说, 查询缓存不一定能起到正面效果.
对于读远多于写的表可以考虑使用查询缓存.
8.0版本的查询缓存功能被删了
2.mysql怎么恢复半个月之前的数据
通过整库备份+binlog进行恢复. 前提是要有定期整库备份且保存了binlog日志
3.订单量数据表越来越大导致查询缓慢,如何处理?
分库分表. 由于历史订单使用率并不高, 高频的可能只是近期订单, 因此, 将订单表按照时间进行拆分, 根据数据量的大小考虑按月分表或按年分表. 订单ID最好包含时间(如根据雪花算法生成), 此时既能根据订单ID直接获取到订单记录, 也能按照时间进行查询.
4.一千万条数据的单表,如何进行分页查询?
数据量过大的情况下, limit offset 分页会由于扫描数据太多而越往后查询越慢. 可以配合当前页最后一条ID进行查询, SELECT * FROM T WHERE id > #{ID} LIMIT #{LIMIT} . 当然, 这种情况下ID必须是有序的, 这也是有序ID的好处之一
5.唯一索引比普通索引快吗, 为什么?
唯一索引不一定比普通索引快, 还可能慢
1. 查询时, 在未使用 limit 1 的情况下, 在匹配到一条数据后, 唯一索引即返回, 普通索引会继续匹配下一条数据, 发现不匹配后返回. 如此看来唯一索引少了一次匹配, 但实际上这个消耗微乎其微.
2. 更新时, 这个情况就比较复杂了. 普通索引将记录放到 change buffer 中语句就执行完毕了. 而对唯一索引而言, 它必须要校验唯一性, 因此, 必须将数据页读入内存确定没有冲突, 然后才能继续操作. 对于写多读少的情况, 普通索引利用 change buffer 有效减少了对磁盘的访问次数, 因此普通索引性能要高于唯一索引.
6.索引是建立的越多越好吗?
1.数据量小的表没必要建立索引,建立索引反而会增加额外的索引开销
2.数据的变更需要维护索引,因此更多的索引意味着要有更多的维护成本
3.更多的索引意味着也需要更多的空间
7.如何定位并优化慢查询sql?
1.如何打开慢日志
查看慢查询状态并设置:Show variables like '%quer%'
set global show_query_log='on'
set global long_query_time=1
慢查询数量:show status like '%slow_queries%'
set global show_query_log='on'
set global long_query_time=1
慢查询数量:show status like '%slow_queries%'
2.explain调优
type列:显示了连接使用了何种类型,从最好到最坏:const、eq_reg、ref、range、index和ALL。all代表的是全表扫描,index代表索引全扫描;range代表索引范围扫描,常用于<,<=,>=,between等操作;ref使用唯一索引扫描或者唯一索引前缀扫描,返回单条记录,常出现在关联查询中,即哪些列或常量被用于查找索引列上的值。一般来说,得保证查询至少达到range级别,最好能达到ref级别;
extra列:
Using filesort
看到这个的时候,查询就需要优化了。MYSQL需要进行额外的步骤来发现如何对返回的行排序。它根据连接类型以及存储排序键值和匹配条件的全部行的行指针来排序全部行
优化方法:
1、修改逻辑,不在mysql中使用order by而是在应用中自己进行排序。
2、使用mysql索引,将待排序的内容放到索引中,直接利用索引的排序。
1、修改逻辑,不在mysql中使用order by而是在应用中自己进行排序。
2、使用mysql索引,将待排序的内容放到索引中,直接利用索引的排序。
Using index
列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候
Using temporary
看到这个的时候,查询需要优化了。这里,MYSQL需要创建一个临时表来存储结果,这通常发生在对不同的列集进行ORDER BY上,而不是GROUP BY上
ALL
ALL:这个连接类型对于前面的每一个记录联合进行完全扫描,这一般比较糟糕,应该尽量避免
亲测补充
order by会导致Using filesort,建立索引。数据量占大部分的情况下也会放弃使用索引。
group by会导致Using temporary; Using filesort,将分组字段使用索引即可解决;
由于 Using filesort是使用算法在 内存中进行排序,MySQL对于排序的记录的大小也是有做限制:max_length_for_sort_data,默认为1024。
3.索引失效的原因有哪些?
1.使用or关键字(但是并不是所有带or的查询都会失效,如果有两个字段,两个字段都有索引就不会失效,会走两个索引)
2.使用like关键字(但是并不是所有like查询都会失效,只有在查询时字段最左侧加%和左右侧都加%才会导致索引失效)
3.组合索引(如果查询的字段在组合索引中不是最左侧的字段,那么该组合索引是不会生效的。即左前缀原则)
4.索引列有运算符!=
5.索引列使用了函数
6.B-tree索引 is null不会走,is not null会走,位图索引 is null,is not null 都会走
7.不走索引的原因是因为mysql中innodb搜索引擎的数据结构是B+树,索引查询b+树查询是通过二分法进行查询的,这就要求叶子节点上的数据必须是有序的,不然会走全表查询。
8.有时都考虑到了 但就是不走索引,drop了从建试试在
4.做过哪些Mysql索引相关的优化?
尽量使用主键查询: 聚簇索引上存储了全部数据, 相比普通索引查询, 减少了回表的消耗.
MySQL5.6之后引入了索引下推优化, 通过适当的使用联合索引, 减少回表判断的消耗.
若频繁查询某一列数据, 可以考虑利用覆盖索引避免回表.
联合索引将高频字段放在最左边
8.大表如何进行优化?
问题:某个表有近千万数据,查询比较慢,如何优化?
解答:当 MySQL 单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下: 1.限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月 的时间范围内; 2.读写分离:经典的数据库拆分方案,主库负责写,从库负责读;3.通过分库分表的方式进行优化,主要有垂直拆分和水平拆分。
15.说一下mysql的redo log和binlog?
1.MySQL 分两层:Server 层和引擎层。区别如下:
Server 层:主要做的是 MySQL 功能层面的事情。Server 层也有自己的日志,称为 binlog(归档日志)
引擎层:负责存储相关的具体事宜。redo log 是 InnoDB 引擎特有的日志。
Server 层:主要做的是 MySQL 功能层面的事情。Server 层也有自己的日志,称为 binlog(归档日志)
引擎层:负责存储相关的具体事宜。redo log 是 InnoDB 引擎特有的日志。
2.redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如"给 ID=2这一行的c字段加1"
3.redo log 是循环写的,空间固定会用完;
4.binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
16.mysql的回表?
1.回表查询:先到普通索引上定位主键值,再到聚集索引上定位行记录,多了一次磁盘IO,它的性能较扫一遍索引树低(一般情况下)
2.详细说明:
1.一般我们自己建的索引不管是单列索引还是联合索引,都称为普通索引,相对应的另外一种就是聚簇索引。每个普通索引就对应着一颗独立的索引B+树,索引 B+ 树的节点仅仅包含了索引里的几个字段的值以及主键值。
2.根据索引树按照条件找到了需要的数据,仅仅是索引里的几个字段的值和主键值,如果用 select * 则还需要很多其他的字段,就得走一个回表操作,根据主键再到主键的聚簇索引里去找,聚簇索引的叶子节点是数据页,找到数据页里才能把一行数据的所有字段值提取出来。
3.假设 select * from table order by a,b,c 的语句,(table 有 abcdef 6 个字段),首先得从联合索引的索引树里按照顺序 a、b、c 取出来所有数据,接着对每一条数据都根据主键到聚簇索引的查找,其实性能不高。有时候 MySQL 引擎会觉得用了既用了联合索引和聚簇索引来查找指定的字段,太慢了,那不不如直接全表扫描得了,只用聚集索引就行
17.mysql主从复制的原理
1.主从复制的流程
1.master服务器将数据的改变记录到binlog中;
2.slave连接到master,获取binlog;
3.master创建dump线程,推送binglog到slave;
4.slave启动一个IO线程读取同步过来的master的binlog,记录到relay log中继日志中;
5.slave再开启一个sql线程读取relay log事件并在slave执行,完成同步;
6.slave记录自己的binglog;
大白话:主库会生成一个dump线程,用来给从库I/O线程传binlog;I/O线程会去请求主库的binlog,并将得到的binlog写到本地的relay-log(中继日志)文件中;SQL线程,会读取relay log文件中的日志,并解析成sql语句逐一执行;
2.主从复制数据丢失问题
1.全同步复制
主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。
2.半同步复制
和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。
主从复制存在数据丢失问题的解决方案:在使用过程中需要开启半同步复制;
3.主从复制的使用场景
1.高可用(HA)架构
1.MySQL 的高可用由互为主从的MySQL构成,平时只有主库提供服务,备库不提供服务。当主库停止服务时,服务自动切换到备库
2.主库master挂了之后,从库进行选举出新的master,其他从库挂到新的master上;
3.LVS+Keepalived:Keepalived可以进行检查心跳和动态漂移;当Master节点出现异常主服务所在keepalived会发出通知,然后slave节点的keepalived通知从节点切换为master
2.读写分离架构
1.高并发下读写分离会出现数据延迟问题。
2.解决方案如下:
分库分表;开启并行复制;在业务逻辑上避免
分库分表;开启并行复制;在业务逻辑上避免
一主一从、一主多从、多级主从、多主模式
18.分库分表
1.垂直划分
垂直划分数据库是根据业务进行划分,例如购物场景,可以将库中涉及商品、订单、用 户的表分别划分出成一个库,通过降低单库的大小来提高性能。同样的,分表的情况就 是将一个大表根据业务功能拆分成一个个子表,例如商品基本信息和商品描述,商品基 本信息一般会展示在商品列表,商品描述在商品详情页,可以将商品基本信息和商品描 述拆分成两张表
优点:行记录变小,数据页可以存放更多记录,在查询时减少 I/O 次数。
缺点: 主键出现冗余,需要管理冗余列; 会引起表连接 JOIN 操作,可以通过在业务服务器上进行 join 来减少数据 库压力; 依然存在单表数据量过大的问题。
2.水平划分
水平划分是根据一定规则,例如时间或 id 序列值等进行数据的拆分。比如根据年份来 拆分不同的数据库。每个数据库结构一致,但是数据得以拆分,从而提升性能。
优点:单库(表)的数据量得以减少,提高性能;切分出的表结构相同,程序改动较少。
缺点: 分片事务一致性难以解决 跨节点 join 性能差,逻辑复杂 数据分片在扩容时需要迁移.
工具
mycat/sharingjdbc
5.redis
1.redis单线程模型
参考:https://mp.weixin.qq.com/s/--obCEm1sHK4yqomLVxPVQ
2.redis6.0的多线程模型
主线程负责建立接收连接请求,获取客户端socket连接放入等待队列;
主线程处理完读事件之后,通过RR(Round Robin轮询)将这些连接分配给多个IO子线程;
主线程阻塞等待IO子线程读取socket完毕;
主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行;
主线程阻塞等待IO线程将数据回写socket完毕;
解除绑定,清空等待队列;
主线程处理完读事件之后,通过RR(Round Robin轮询)将这些连接分配给多个IO子线程;
主线程阻塞等待IO子线程读取socket完毕;
主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行;
主线程阻塞等待IO线程将数据回写socket完毕;
解除绑定,清空等待队列;
其中IO线程组的特点:
IO线程要么同时在读socket,要么同时在写,不会同时既读又写;
IO线程组只负责读写socket解析命令,不负责命令的处理,命令的处理仍然是main线程在顺序处理
IO线程要么同时在读socket,要么同时在写,不会同时既读又写;
IO线程组只负责读写socket解析命令,不负责命令的处理,命令的处理仍然是main线程在顺序处理
3.redis的几种数据结构
1.字符串String
1.value为String字符串
2.使用场景:
常被用来存储计数器,粉丝数等,简单的分布式锁也会用到该类型
2.哈希 hash
1.key - value 形式的,value 是一个map;String Key和String Value的map容器;每一个hash可以存储4294967295个键值对
2.使用场景
子主题
子主题
3.列表 list
1.列表
2.使用场景
在 Redis 中可以把 list 用作栈、队列、阻塞队列
子主题
4.集合 set
1.集合,不能有重复元素,可以做点赞,收藏等
2.使用场景
子主题
5.有序集合 zset
1.有序集合,不能有重复元素,有序集合中的每个元素都需要指定一个分数,根据分数对元素进行升序排序。可以做排行榜
2.使用场景
子主题
子主题
6.三种特殊类型
1.geospatial: Redis 在 3.2 推出 Geo 类型,该功能可以推算出地理位置信息,两地之间的距离
2.hyperloglog:基数:数学上集合的元素个数,是不能重复的。这个数据结构常用于统计网站的 UV。
3.bitmap: bitmap 就是通过最小的单位 bit 来进行0或者1的设置,表示某个元素对应的值或者状态。一个 bit 的值,或者是0,或者是1;也就是说一个 bit 能存储的最多信息是2。bitmap 常用于统计用户信息比如活跃粉丝和不活跃粉丝、登录和未登录、是否打卡等
4.redis的持久化方式
1.RDB方式
1.概念:
把某个时间点 redis 内存中的数据以二进制的形式存储的一个.rdb为后缀的文件当中,也就是「周期性的备份redis中的整个数据」,这是redis默认的持久化方式,也就是我们说的快照(snapshot),是采用 fork 子进程的方式来写时同步的。
2.优点:
1.它是将某一时间点redis内的所有数据保存下来,所以当我们做「大型的数据恢复时,RDB的恢复速度会很快」
2.由于RDB的FROK子进程这种机制,队友给客户端提供读写服务的影响会非常小
3.缺点:
1.举例:举个例子假设我们定时5分钟备份一次,在10:00的时候 redis 备份了数据,但是如果在10:04的时候服务挂了,那么我们就会丢失在10:00到10:04的整个数据
2.有可能会产生长时间的数据丢失
3.可能会有长时间停顿:我们前面讲了,fork 子进程这个过程是和 redis 的数据量有很大关系的,如果「数据量很大,那么很有可能会使redis暂停几秒」
2.AOF方式
1.概念:
redis 每次执行一个命令时,都会把这个「命令原本的语句记录到一个.aod的文件当中,然后通过fsync策略,将命令执行后的数据持久化到磁盘中」(不包括读命令)
2.优点:
1.AOF可以「更好的保护数据不丢失」,一般AOF会以每隔1秒,通过后台的一个线程去执行一次fsync操作,如果redis进程挂掉,最多丢失1秒的数据
2.AOF是将命令直接追加在文件末尾的,「写入性能非常高」
3.AOF日志文件的命令通过非常可读的方式进行记录,这个非常「适合做灾难性的误删除紧急恢复」,如果某人不小心用 flushall 命令清空了所有数据,只要这个时候还没有执行 rewrite,那么就可以将日志文件中的 flushall 删除,进行恢复
3.缺点:
1.对于同一份数据源来说,一般情况下AOF 文件比 RDB 数据快照要大
2.由于 .aof 的每次命令都会写入,那么相对于 RDB 来说「需要消耗的性能也就更多」,当然也会有 aof 重写将 aof 文件优化。
3.「数据恢复比较慢」,不适合做冷备。
无硬盘复制是什么
1.master禁用了RDB快照时,发生了主从同步(复制初始化)操作,也会生成RDB快照,但是之后如果master发成了重启,就会用RDB快照去恢复数据,这份数据可能已经很久了,中间就会丢失数据
2.在这种一主多从的结构中,master每次和slave同步数据都要进行一次快照,从而在硬盘中生成RDB文件,会影响性能
为了解决这种问题,redis在后续的更新中也加入了无硬盘复制功能,也就是说直接通过网络发送给slave,避免了和硬盘交互,但是也是有io消耗
6.redis面试题补充
1.redis查询慢的定位以及解决
2.Redis的过期键的删除策略
1.redis是key-value的数据库,我们可以设置redis中缓存的key的过期时间。redis的过期策略就是指当redis中缓存的key过期了,redis会如何处理。
2.策略1:定时删除
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除;
该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量
3.策略1:惰性删除
只有当访问一个key时,才会判断该key是否已过期,过期则删除。
该策略可以最大化的节省cpu资源,但是对内存非常不友好。极端情况下可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
4.策略3:定期删除
每隔一定的时间,会扫描一定数量的数据库的expries字典中一定数量的key,并清除其中已经过期的key。
该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
4.(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,
value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了惰性过期和定期过期两种过期策略。
value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)
Redis中同时使用了惰性过期和定期过期两种过期策略。
3.redis单线程快的原因
1.纯内存操作
2.核心是基于非阻塞IO的多路复用机制
3.使用单线程模型来处理客户端的请求,避免了上下文切换带来的性能问题
4.自身采用的是c语言编写,有很多的优化机制,比如动态字符串sds
4.京东二面:内存耗尽后Redis会发生什么?
过期键删除策略
当服务器内存不够时 Redis 的 8 种淘汰策略
Redis 中的两种主要的淘汰算法 LRU 和 LFU
7.缓存雪崩、缓存穿透、缓存击穿
1.缓存雪崩
1.缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉
2.解决方案:
1.均匀过期/随机过期:缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2.永不过期
3.互斥锁:一般并发量不是特别多的时候,使用最多的解决方案就是加锁排队,加互斥锁。
4.缓存预热:比如启动服务之前先做个接口加载缓存
5.双重缓存:给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,如果缓存标记失效,则更新数据缓存。
2.缓存穿透
1.缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上造成数据库短时间内承受大量造求而崩掉;比如用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题
2.解决方案:
1.接口层增加校验,如用户鉴权检验,id作基础检验,id<0的直接拦截;
2.返回空对象
3.使用布隆过滤器
将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
比如场景:与别的公司进行对接数据,接口要暴露在外网,结果另外一家公司不靠谱泄露了秘钥之类的,所以拿这个顶上,因为布隆过滤器实际上是防止大量的不存在的key来攻击
3.缓存击穿
1.缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期) , 这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬问增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据, 缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
2.解决方案:
1.加互斥锁
2.设置永不过期
8.Redis集群方案
1.哨兵模式
工作原理
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。它具备自动故障转移、集群监控、消息通知等功能
哨兵用于实现redis集群的高可用,本身也是分布式的,作为-个哨兵集群去运行,互相协同工作。
●故障转移时,判断一个master node是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举
●即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的
●哨兵通常需要3个实例,来保证自己的健壮性。
●哨兵+ redis主从的部署架构,是不保证数据零丢失的,只能保证redis集群的高可用性。
●对于哨兵+ redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
●故障转移时,判断一个master node是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举
●即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的
●哨兵通常需要3个实例,来保证自己的健壮性。
●哨兵+ redis主从的部署架构,是不保证数据零丢失的,只能保证redis集群的高可用性。
●对于哨兵+ redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
哨兵组件的功能
集群监控:负责监控redis master和slave进程是否正常工作
消息通知:如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
故障转移:如果master node挂掉了,会自动转移到slave node.上
配置中心:如果故障转移发生了,通知client 客户端新的master 地址。
哨兵的选举过程
1.第一个发现该master挂了的哨兵,向每个哨兵发送命令,让对方选举自己成为领头哨兵
2.其他哨兵如果没有选举过他人,就会将这一票投给第一个发现该master挂了的哨兵
3.第一个发现该master挂了的哨兵如果发现有超过一半哨兵投给自己,并且其数量也超过了设定的quoram参数,那么该哨兵就成了领头哨兵
4.如果多个哨兵同时参与这个选举,那么就会重复该过程,直到选出一个领头哨兵
5.选出领头哨兵后,就开始了故障修复,会从选出一个从数据库作为新的master
2.redis cluster
工作原理
cluster集群模式:在redis3.0版本中支持了cluster集群部署的方式,这种集群部署的方式能自动将数据进行分片,每个master上放一部分数据,提供了内置的高可用服务,即使某个master挂了,服务还可以正常地提供
Redis Cluster是- 种服务端Sharding技术, 3.0版本 开始正式提供。采用slot(槽)的概念, -共分成16384个槽。 将
请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行
方案说明
●通过哈希的方式,将数据分片,每个节点均分存储-定哈希槽(哈希值)区间的数据,默认分配了16384个槽位
●每份数据分片会存储在多个互为主从的多节点上
●数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
●同一分片多个节点间的数据不保持强一 致性
●读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
●扩容时需要需要把旧8节点的数据迁移-部分到新节点
在redis cluster架构下,每个redis要放开两个端口号,比如一个是6379,另外一个就是加1w的端口号,比如
16379.
16379端口号是用来进行节点间通信的,也就是cluster bus的通信,用来进行故障检测、配置更新、故障转移授
权。cluster bus用了另外-种二进制的协议, gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带
宽和外理时间.
请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行
方案说明
●通过哈希的方式,将数据分片,每个节点均分存储-定哈希槽(哈希值)区间的数据,默认分配了16384个槽位
●每份数据分片会存储在多个互为主从的多节点上
●数据写入先写主节点,再同步到从节点(支持配置为阻塞同步)
●同一分片多个节点间的数据不保持强一 致性
●读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
●扩容时需要需要把旧8节点的数据迁移-部分到新节点
在redis cluster架构下,每个redis要放开两个端口号,比如一个是6379,另外一个就是加1w的端口号,比如
16379.
16379端口号是用来进行节点间通信的,也就是cluster bus的通信,用来进行故障检测、配置更新、故障转移授
权。cluster bus用了另外-种二进制的协议, gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带
宽和外理时间.
优点
●无中心架构,支持动态扩容,对业务透明
●具备Sentinel的监控和自动F ailover(故障转移)能力
●客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
●高性能,客户端直连redis服务,免去了proxy代理的损耗
●具备Sentinel的监控和自动F ailover(故障转移)能力
●客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
●高性能,客户端直连redis服务,免去了proxy代理的损耗
缺点
●运维也很复杂,数据迁移需要人工干预
●只能使用0号数据库
●不支持批量操作(pipeline管道操作)
●分布式逻辑和存储模块耦合等
●只能使用0号数据库
●不支持批量操作(pipeline管道操作)
●分布式逻辑和存储模块耦合等
cluster的故障恢复是怎么做的?
判断故障的逻辑其实与哨兵模式有点类似,在集群中,每个节点都会定期的向其他节点发送ping命令,通过有没有收到回复来判断其他节点是否已经下线。如果长时间没有回复,那么发起ping命令的节点就会认为目标节点疑似下线,也可以和哨兵一样称作主观下线,当然也需要集群中一定数量的节点都认为该节点下线才可以。
1.当A节点发现目标节点疑似下线,就会向集群中的其他节点散播消息,其他节点就会向目标节点发送命令,判断目标节点是否下线
2.如果集群中半数以上的节点都认为目标节点下线,就会对目标节点标记为下线,从而告诉其他节点,让目标节点在整个集群中都下线
3.redis sharding
优点:优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强
缺点:由于sharding处理放到客户端,规模进一步扩 大时给运维带来挑战。客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化
9.redis的主从复制
1.Redis主从复制的核心原理
1.当一个从数据库启动时,它会向主数据库发送一个SYNC命令,master收到后,在后台保存快照,也就是我们说的RDB持久化,当然保存快照是需要消耗时间的,并且redis是单线程的,在保存快照期间redis受到的命令会缓存起来
2.快照完成后会将缓存的命令以及快照一起打包发给slave节点,从而保证主从数据库的一致性。
3.从数据库接受到快照以及缓存的命令后会将这部分数据写入到硬盘上的临时文件当中,写入完成后会用这份文件去替换掉RDB快照文件,当然,这个操作是不会阻塞的,可以继续接收命令执行,具体原因其实就是fork了一个子进程,用子进程去完成了这些功能
因为不会阻塞,所以,这部分初始化完成后,当主数据库执行了改变数据的命令后,会异步的给slave,这也就是我们说的复制同步阶段,这个阶段会贯穿在整个中从同步的过程中,直到主从同步结束后,复制同步才会终止。
2.全量复制
1.主节点通过bgsave命令fork子进程进行RDB持久化, 该过程是非常消耗CPU.内存(页表复制)、硬盘I0的
2.主节点通过网络将RDB文件发送给从节点,对主从节点的带宽都会带来很大的消耗
3.从节点清空老数据、载入新RDB文件的过程是阻塞的,无法响应客户端的命令;如果从节点执行
bgrewriteaof,也会带来额外的消耗
bgrewriteaof,也会带来额外的消耗
3.部分复制
1.服务器收到slaveof命令,判断是否是第一次复制
2.如果是第一次复制,向主节点发送psync命令,master返回fullresync{runid}{offset},runid表示主节点的运行ID,offset表示当前主节点的复制偏移量,执行全量同步
3.如果不是第一次复制, 则向master发送psync(runid+offset),这里master会对runid进行进行判断。如果是上次的runid,则执行增量同步,如果原本的matser节点出现了问题,有了新的master节点。此时判断的runid是不相同,说明master节点已经改变,则执行全量同步。
10.基于redis实现分布式锁
1.分布式锁框架redission:
2.redlock红锁
3.redis与zk在分布式架构上的异同
11.如何解决redis缓存与数据库的双写不一致问题
需要解决第二步执行失败引发的不一致(配合MQ)、需要解决并发问题带来的不一致(删除缓存)
想要保证数据库和缓存一致性,推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做。
延时双删的策略:写的时候:先更新数据库,再删除缓存,休眠一段时间,再次删除缓存。读的时候:先读缓存,缓存中没有再读数据库,然后回写到redis
12.redis8种内存淘汰策略
volatile-lru:从已设置过期时间的KV集中优先对最近最少使用(less recently used)的数据淘汰
volitile-ttl:从已设置过期时间的KV集中优先对剩余时间短(time to live)的数据淘汰
volitile-random:从已设置过期时间的KV集中随机选择数据淘汰
allkeys-lru:从所有KV集中优先对最近最少使用(less recently used)的数据淘汰
allKeys-random:从所有KV集中随机选择数据淘汰
noeviction:不淘汰策略,若超过最大内存,返回错误信息
volatile-lfu:从已设置过期时间的KV集中,通过统计访问频率,将访问频率最少,即最不经常使用的KV淘汰。
allkeys-lfu:从所有KV集中,通过统计访问频率,将访问频率最少,即最不经常使用的KV淘汰。
13.Redis的内存淘汰算法
LRU算法
LFU算法
14.redis怎么实现延时任务?
1.通过定时任务进行数据库轮询
思路:该方案通常是在小型项目中使用,即通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行update或delete等操作
优点:简单易行,支持集群操作
缺点:对服务器内存消耗大;
存在延迟,比如你每隔3分钟扫描一次,那最坏的延迟时间就是3分钟;
假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大;
存在延迟,比如你每隔3分钟扫描一次,那最坏的延迟时间就是3分钟;
假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大;
2.jdk的延迟队列
思路:该方案是利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的。Poll():获取并移除队列的超时元素,没有则返回空;take():获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件,返回结果。
优点:效率高,任务触发时间延迟低。
缺点:服务器重启后,数据全部消失,怕宕机;
集群扩展相当麻烦;
因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常;
代码复杂度较高;
集群扩展相当麻烦;
因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常;
代码复杂度较高;
3.时间轮算法
如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。位置是在2圈之后的5上面(20 % 8 + 1)
我们用Netty的HashedWheelTimer来实现
优点:效率高,任务触发时间延迟时间比delayQueue低,代码复杂度比delayQueue低。
缺点:服务器重启后,数据全部消失,怕宕机;集群扩展相当麻烦;因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常
4.使用redis缓存
思路1:利用redis的zset,zset是一个有序集合,每一个元素(member)都关联了一个score,通过score排序来取集合中的值
实现:我们将订单超时时间戳与订单号分别设置为score和member,系统扫描第一个元素判断是否超时;但是在高并发情况下存在消费同一订单情况,采用分布式锁会降低效率;
思路2:该方案使用redis的Keyspace Notifications,中文翻译就是键空间机制,就是利用该机制可以在key失效之后,提供一个回调,实际上是redis会给客户端发送一个消息。是需要redis版本2.8以上。
5.使用消息队列
我们可以采用rabbitMQ的延时队列。RabbitMQ具有以下两个特性,可以实现延迟队列:
RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter;
lRabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了deadletter,则按照这两个参数重新路由。
RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter;
lRabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了deadletter,则按照这两个参数重新路由。
优点:
高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。
缺点:
本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高
高效,可以利用rabbitmq的分布式特性轻易的进行横向扩展,消息支持持久化增加了可靠性。
缺点:
本身的易用度要依赖于rabbitMq的运维.因为要引用rabbitMq,所以复杂度和成本变高
15.redis如何进行限流?
1.基于redis的setnx的操作
我们在使用Redis的分布式锁的时候,大家都知道是依靠了setnx的指令,在CAS(Compare and swap)的操作的时候,同时给指定的key设置了过期时间(expire),我们在限流的主要目的就是为了在单位时间内,有且仅有N数量的请求能够访问我的代码程序。依靠setnx可以很轻松的做到这方面的功能。
例子:比如我们需要在10秒内限定20个请求,那么我们在setnx的时候可以设置过期时间10,当请求的setnx数量达到20时候即达到了限流效果。
利弊:当然这种做法的弊端是很多的,比如当统计1-10秒的时候,无法统计2-11秒之内,如果需要统计N秒内的M个请求,那么我们的Redis中需要保持N个key等等问题。
2.基于redis的数据结构zset
其实限流涉及的最主要的就是滑动窗口,上面也提到1-10怎么变成2-11。其实也就是起始值和末端值都各+1即可。其实限流涉及的最主要的就是滑动窗口,上面也提到1-10怎么变成2-11。其实也就是起始值和末端值都各+1即可。
例子:我们可以将请求打造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID生成,而score可以用当前时间戳表示,因为score我们可以用来计算当前时间戳之内有多少的请求数量。而zset数据结构也提供了range方法让我们可以很轻易的获取到2个时间戳内有多少请求
利弊:通过代码可以做到滑动窗口的效果,并且能保证每N秒内至多M个请求,缺点就是zset的数据结构会越来越大。实现方式相对也是比较简单的。
3.基于redis的令牌桶算法
令牌桶算法提及到输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了。
也就是说我们每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反。
实现:依靠List的leftPop来获取令牌;再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以我这里还是用UUID进行了生成;针对这些限流方式我们可以在AOP或者filter中加入以上代码,用来做到接口的限流,最终保护你的网站。
16.如何解决redis的大key问题?
1.什么是大key?
所谓的大key问题是某个key的value比较大,本质上是大value问题。key往往是程序可以自行设置的,value往往不受程序控制,因此可能导致value很大。
例子:某个短视频有很多用户收藏,假如有这样的数据结构:
歌单和用户之间的映射关系采用redis存储:redis的key是视频ID,长度可控且很小;redis的value是个list,list包含了用户ID,用户可能很多,就导致list长度不可控
歌单和用户之间的映射关系采用redis存储:redis的key是视频ID,长度可控且很小;redis的value是个list,list包含了用户ID,用户可能很多,就导致list长度不可控
value是String类型时,size超过10KB可视为大key;
value是ZSET、Hash、List、Set等集合类型时,它的成员数量超过1w个即可看做大key
value是ZSET、Hash、List、Set等集合类型时,它的成员数量超过1w个即可看做大key
2.大key的影响
首先redis的核心工作线程是单线程,处理请求任务是串行的,前面的完成不了,后面的无法进行处理,同时也导致分布式架构中内存数据和CPU的不平衡。
影响
执行大key命令的客户端本身,耗时明显增加,甚至超时
执行大key相关读取或者删除操作时,会严重占用带宽和CPU,影响其他客户端
大key本身的存储带来分布式系统中分片数据不平衡,CPU使用率也不平衡
大key有时候也是热key,读取操作频繁,影响面会很大
执行大key删除时,在低版本redis中可能阻塞线程
执行大key相关读取或者删除操作时,会严重占用带宽和CPU,影响其他客户端
大key本身的存储带来分布式系统中分片数据不平衡,CPU使用率也不平衡
大key有时候也是热key,读取操作频繁,影响面会很大
执行大key删除时,在低版本redis中可能阻塞线程
大key的影响还是很明显的,最典型的就是阻塞线程,并发量下降,导致客户端超时,服务端业务成功率下降。
3.如何找到大key?
1.增加内存&流量&超时等指标监控:由于大key的value很大,执行读取时可能阻塞线程,这样Redis整体的qps会下降,并且客户端超时会增加,网络带宽会上涨,配置这些报警可以让我们发现大key的存在。
2.使用bigkeys命令,在线检索工具
使用bigkeys命令以遍历的方式分析Redis实例中的所有Key,并返回整体统计信息与每个数据类型中Top1的大Key
redis-cli -h localhhost:2181 --bigkeys
3.使redis-rdb-tools,离线文件分析工具
使用redis-rdb-tools离线分析工具来扫描RDB持久化文件,虽然实时性略差,但是完全离线对性能无影响。
4.可视化页面,依赖redis官方
4.大key的治理?
1.可删除:大key并非热key就可以在DB中查询使用,则可以在redis中删除
渐进式删除<4.0版本
当Redis版本小于4.0时,避免使用阻塞式命令KEYS,而是建议通过SCAN命令执行增量迭代扫描key,然后判断进行删除。
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
惰性删除>4.0版本
使用UNLINK命令安全的删除大key,该命令能够以非阻塞的方式,逐步的清理传入的key
Redis UNLINK 命令类似与 DEL 命令,表示删除指定的 key,如果指定 key 不存在,命令则忽略。
UNLINK 命令不同与 DEL 命令在于它是异步执行的,因此它不会阻塞。
UNLINK 命令是非阻塞删除,非阻塞删除简言之,就是将删除操作放到另外一个线程去处理。
UNLINK 命令不同与 DEL 命令在于它是异步执行的,因此它不会阻塞。
UNLINK 命令是非阻塞删除,非阻塞删除简言之,就是将删除操作放到另外一个线程去处理。
2.不可删除:value压缩和拆分
当vaule是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。
当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。
17.redis的缓存命中率大概多少?
6.多线程与高并发
1.线程
1.创建线程的方式有哪几种
1.继承thread类进行创建
1.定义 Thread 类的子类,并重写该类的 run 方法,该 run 方法的方法体就代表了线程要 完成的任务。因此把 run()方法称为执行体。
2.创建 Thread 子类的实例,即创建了线程对象。
3.调用线程对象的 start()方法来启动该线程。
2.通过实现runnable接口来创建线程类
1.定义 runnable 接口的实现类,并重写该接口的 run()方法,该 run()方法的方法体同样 是该线程的线程执行体。
2.创建 Runnable 实现类的实例,并依此实例作为 Thread 的 target 来创建 Thread 对象, 该 Thread 对象才是真正的线程对象。
3.调用线程对象的 start()方法来启动该线程。
3.通过 Callable 和 Future 创建线程
1.创建 Callable 接口的实现类,并实现 call()方法,该 call()方法将作为线程执行体,并且 有返回值
2.创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对 象封装了该 Callable 对象的 call()方法的返回值。
3.使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
4.调用 FutureTask 对象的 get()方法来获得子线程执行结束后的返回值
4.三种方式的对比
采用实现 Runnable、Callable 接口的方式创建多线程时
优势是: 线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。 在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同 一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向 对象的思想。
劣势是:编程稍微复杂,如果要访问当前线程,则必须使用 Thread.currentThread()方法。
使用继承 Thread 类的方式创建多线程时
优势是:编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread()方法,直接使用 this 即可获得当前线程。
劣势是:线程类已经继承了 Thread 类,所以不能再继承其他父类。
2.sleep() 、join()、yield()有什么区别
1.sleep()方法
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度 程序精度和准确性的影响。 让其他线程有机会继续执行,但它并不释放对象锁。也就是如 果有 Synchronized 同步块,其他线程仍然不能访问共享数据。注意该方法要捕获异常比如有 两个线程同时执行(没有 Synchronized),一个线程优先级为 MAX_PRIORITY,另一个为 MIN_PRIORITY,如果没有 Sleep()方法,只有高优先级的线程执行完成后,低优先级的线程 才能执行;但当高优先级的线程 sleep(5000)后,低优先级就有机会执行了
总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的 线程有执行的机会。
2.yield()方法
yield()方法和 sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即 yield() 方法只是使当前线程重新回到可执行状态,所以执行 yield()的线程有可能在进入到可执行状 态后马上又被执行,另外 yield()方法只能使同优先级或者高优先级的线程得到执行机会,这 也和 sleep()方法不同。
3.join()方法
Thread 的非静态方法 join()让一个线程 B“加入”到另外一个线程 A 的尾部。在 A 执行完毕 之前,B 不能工作。
Thread t = new MyThread();
t.start();
t.join();
保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有 存活,则当前线程不需要停止。
t.start();
t.join();
保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有 存活,则当前线程不需要停止。
3.线程的生命周期
1.新建(new Thread)
当创建 Thread 类的一个实例(对象)时,此线程进入新建状态(未被启动)。 例如:Thread t1=new Thread();
2.就绪(runnable)
线程已经被启动,正在等待被分配给 CPU 时间片,也就是说此时线程正在就绪队列中排队 等候得到 CPU 资源。例如:t1.start();
3.运行(running)
线程获得 CPU 资源正在执行任务(run()方法),此时除非此线程自动放弃 CPU 资源或者有 优先级更高的线程进入,线程将一直运行到结束。
4.死亡(dead)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态 等待执行。 自然终止:正常运行 run()方法后终止。 异常终止:调用**stop()**方法让一个线程终止运行。
5.阻塞(blocked)
由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行,即进入堵塞状态。 正在睡眠:用 sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过 去可进入就绪状态。 正在等待:调用 wait()方法。(调用 notify()方法回到就绪状态) 被另一个线程所阻塞:调用 suspend()方法。(调用 resume()方法恢复)
4.如何优雅的退出线程
1.不可使用stop()方法,使用stop方法会让线程戛然而止。无法让我们知道做完了什么,哪些还没有做完,也无法进行一些线程的清理工作
2.正确的停止方式:设置退出旗标
配合volatile关键字,volatile关键字保证了线程正确的读取变量的值。可以保证变量在不同线程之间的可见性。
3.thread.interrupt()方法
thread.interrupt()方法初衷并不是用于停止线程,调用interrupt方法是在当前线程中打了一个停止标志,并不是真的停止线程。
实质上这种方式退出线程还是在使用退出旗标的方式
thread.interrupt()方法的作用是唤醒阻塞的线程,并抛出异常。当sleep后,线程阻塞,thread.interrupt()方法执行后,线程又被唤醒并抛出异常。因为线程被唤醒,所以设置的变量值this.isInterrupted()的值为false,while语句继续,线程继续执行。
5.notify()与notifyAll()
notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。两者的最大区别在于:
notifyAll使所有原来在该对象上等待被notify的所有线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。
notify则文明得多,它只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁
notify则文明得多,它只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁
2.线程池
1.为什么使用线程池
1.每次 new Thread() 新建对象,性能差;
2.线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM;
3.缺少更多的功能,如更多执行、定期执行、线程中断;
2.线程池的好处
1.重用存在的线程,减少对象创建、消亡的开销,性能佳,降低资源消耗;
2.可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞,提高响应速度;
3.提供定时执行、定期执行、单线程、并发数控制等功能,以达到提高线程的可管理性。
3.创建线程池的几种方式
1.newFixedThreadPool (固定数目线程的线程池)
1.创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数 量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的 线程
2.线程池特点:
核心线程数和最大线程数大小一样;
没有所谓的非空闲时间,即 keepAliveTime 为 0;
阻塞队列为无界队列 LinkedBlockingQueue,可能会导致 OOM(堆积的请求处理队列可能会耗费非常大的内存,甚至OOM)
没有所谓的非空闲时间,即 keepAliveTime 为 0;
阻塞队列为无界队列 LinkedBlockingQueue,可能会导致 OOM(堆积的请求处理队列可能会耗费非常大的内存,甚至OOM)
3.工作流程:
提交任务;
如果线程数少于核心线程,创建核心线程执行任务;
如果线程数等于核心线程,把任务添加到 LinkedBlockingQueue 阻塞 队列;
如果线程执行完任务,去阻塞队列取任务,继续执行;
如果线程数少于核心线程,创建核心线程执行任务;
如果线程数等于核心线程,把任务添加到 LinkedBlockingQueue 阻塞 队列;
如果线程执行完任务,去阻塞队列取任务,继续执行;
4.使用场景:
适用于处理 CPU 密集型的任务,确保 CPU 在长期被工作 线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。
2.newCachedThreadPool (可缓存线程的线程池)
1.可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制
2.线程池特点:
核心线程数为 0 ;
最大线程数为 Integer.MAX_VALUE,即无限大,可能会因为无限创建 线程,导致 OOM ;
阻塞队列是 SynchronousQueue;
非核心线程空闲存活时间为 60 秒;
最大线程数为 Integer.MAX_VALUE,即无限大,可能会因为无限创建 线程,导致 OOM ;
阻塞队列是 SynchronousQueue;
非核心线程空闲存活时间为 60 秒;
当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会 创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由 于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不 会占用任何资源。
3.工作流程:
提交任务;
因为没有核心线程,所以任务直接加到 SynchronousQueue 队列;
判断是否有空闲线程,如果有,就去取出任务执行。
如果没有空闲线程,就新建一个线程执行。
执行完任务的线程,还可以存活 60 秒,如果在这期间,接到任务, 可以继续活下去;否则,被销毁
因为没有核心线程,所以任务直接加到 SynchronousQueue 队列;
判断是否有空闲线程,如果有,就去取出任务执行。
如果没有空闲线程,就新建一个线程执行。
执行完任务的线程,还可以存活 60 秒,如果在这期间,接到任务, 可以继续活下去;否则,被销毁
4.使用场景:
用于并发执行大量短期的小任务。
3.newSingleThreadExecutor (单线程的线程池)
1.前三种线程池的构造直接调用 ThreadPoolExecutor 的构造方法。
2.线程池的特点:
核心线程数为 1 ;
最大线程数也为 1;
阻塞队列是无界队列 LinkedBlockingQueue,可能会导致 OOM ;
keepAliveTime 为 0;
最大线程数也为 1;
阻塞队列是无界队列 LinkedBlockingQueue,可能会导致 OOM ;
keepAliveTime 为 0;
3.工作流程:
提交任务;
线程池是否有一条线程在,如果没有,新建线程执行任务 ;
如果有,将任务加到阻塞队列 ;
当前的唯一线程,从队列取任务,执行完一个,再继续取,一个线程 执行任务。
线程池是否有一条线程在,如果没有,新建线程执行任务 ;
如果有,将任务加到阻塞队列 ;
当前的唯一线程,从队列取任务,执行完一个,再继续取,一个线程 执行任务。
4.适用场景:
适用于串行执行任务的场景,一个任务一个任务地执行。
4.newScheduledThreadPool (定时及周期执行的线程池)
1.线程池的特点:
最大线程数为 Integer.MAX_VALUE,也有 OOM 的风险 ;
阻塞队列是 DelayedWorkQueue ;
keepAliveTime 为 0 ;
scheduleAtFixedRate() :按某种速率周期执行 ;
scheduleWithFixedDelay():在某个延迟后执行;
阻塞队列是 DelayedWorkQueue ;
keepAliveTime 为 0 ;
scheduleAtFixedRate() :按某种速率周期执行 ;
scheduleWithFixedDelay():在某个延迟后执行;
2.工作流程:
1.线程从 DelayQueue 中获取已到期的 ScheduledFutureTask (DelayQueue.take())。到期任务是指 ScheduledFutureTask 的 time 大于等于当前时间。
2.线程执行这个 ScheduledFutureTask。
3.线程修改 ScheduledFutureTask 的 time 变量为下次将要被执行的时 间。
4.线程把这个修改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。
3.适用场景:
周期性执行任务的场景,需要限制线程数量的场景
注:阿里爸爸警告写的很明确“线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的行规则,规避资源耗尽的风险。”
2.线程池的7大参数
1.corePoolSize:指定了线程池中的线程数量(即核心线程数)。
2.maximumPoolSize:指定了线程池中的最大线程数量。
3.keepAliveTime:当线程池线程数量超过corePoolSize时,多余的空闲线程会在多长时间内被销毁。
4.unit:keepAliveTime的时间单位。
5.workQueue:任务队列,被提交但尚未被执行的任务。参数workQueue是指提交但未执行的任务队列。若当前线程池中线程数>=corePoolSize时,就会尝试将任务添加到任务队列中
6.threadFactory:线程工厂,用于创建线程,一般用默认的即可。即Executors类的静态方法defaultThreadFactory()。
7.handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
3.线程池执行过程
1.线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2.当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小
注意:关系2中C是针对的有界队列,无界队列永远都不会满,所以只有前2种关系。
4.线程池的入队策略
参数workQueue是指提交但未执行的任务队列。若当前线程池中线程数>=corePoolSize时,就会尝试将任务添加到任务队列中
1.SynchronousQueue:直接提交队列。SynchronousQueue没有容量,所以实际上提交的任务不会被添加到任务队列,总是将新任务提交给线程执行,如果没有空闲的线程,则尝试创建新的线程,如果线程数量已经达到最大值(maximumPoolSize),则执行拒绝策略
2.LinkedBlockingQueue:无界的任务队列。LinkedBlockingQueue有默认的容量大小为:Integer.MAX_VALUE,当然也可以传入指定的容量大小;当有新的任务来到时,若系统的线程数小于corePoolSize,线程池会创建新的线程执行任务;当系统的线程数量等于corePoolSize后,因为是无界的任务队列,总是能成功将任务添加到任务队列中,所以线程数量不再增加。若任务创建的速度远大于任务处理的速度,无界队列会快速增长,直到内存耗尽
LinkedBlockingQueue:可以进行设置大小,比如5000,就变为了有界队列
LinkedBlockingQueue:可以进行设置大小,比如5000,就变为了有界队列
3.ArrayBlockingQueue有界队列:当使用有限的最大线程数时,有界队列(如ArrayBlockingQueue)可以防止资源耗尽,但是难以调整和控制。队列大小和线程池大小可以相互作用:使用大的队列和小的线程数可以减少CPU使用率、系统资源和上下文切换的开销,但是会导致吞吐量变低,如果任务频繁地阻塞(例如被I/O限制),系统就能为更多的线程调度执行时间。使用小的队列通常需要更多的线程数,这样可以最大化CPU使用率,但可能会需要更大的调度开销,从而降低吞吐量。
4.DelayQueue:DelayQueue(延迟队列)是一个任务定时周期的延迟 执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队 列的先后排序。newScheduledThreadPool 线程池使用了这个队列。
5.PriorityBlockingQueue:PriorityBlockingQueue(优先级队列)是具 有优先级的无界阻塞队列
5.线程池的拒绝策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
1.AbortPolicy :
直接抛出异常,阻止系统正常运行。默认的拒绝策略。
使用场景:这个就没有特殊的场景了,但是一点要正确处理抛出的异常。但是注意,ExecutorService中的线程池实例队列都是无界的,也就是说把内存撑爆了都不会触发拒绝策略。当自己自定义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前的执行流程。
2.CallerRunsPolicy:
调用主线程执行被拒绝的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
使用场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了
3.DiscardPolicy :
该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
使用场景:如果你提交的任务无关紧要,你就可以使用它 。因为它就是个空实现,会悄无声息的吞噬你的的任务。所以这个策略基本上不用了
4.DiscardOldestPolicy :
丢弃最老的(等待时间最长的)一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。
注:以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。
通过自定义线程池拒绝策略,将拒绝的线程进行业务补偿处理。
通过自定义线程池拒绝策略,将拒绝的线程进行业务补偿处理。
dubbo中的线程拒绝策略
当dubbo的工作线程触发了线程拒绝后,主要做了三个事情,原则就是尽量让使用者清楚触发线程拒绝策略的真实原因。
1)输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。可以说,这条日志,使用dubbo的有过生产运维经验的或多或少是见过的,这个日志简直就是日志打印的典范,其他的日志打印的典范还有spring。得益于这么详细的日志,可以很容易定位到问题所在
2)输出当前线程堆栈详情,这个太有用了,当你通过上面的日志信息还不能定位问题时,案发现场的dump线程上下文信息就是你发现问题的救命稻草。
3)继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性
2)输出当前线程堆栈详情,这个太有用了,当你通过上面的日志信息还不能定位问题时,案发现场的dump线程上下文信息就是你发现问题的救命稻草。
3)继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性
Netty中的线程池拒绝策略
Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用主线程执行的任务。而 Netty是新建了一个线程来处理的。
所以,Netty的实现相较于调用者执行策略的使用面就可以扩展到支持高效率高性能的场景了。但是也要注意一点,Netty的实现里,在创建线程时未做任何的判断约束,也就是说只要系统还有资源就会创建新的线程来处理,直到new不出新的线程了,才会抛创建线程失败的异常。
所以,Netty的实现相较于调用者执行策略的使用面就可以扩展到支持高效率高性能的场景了。但是也要注意一点,Netty的实现里,在创建线程时未做任何的判断约束,也就是说只要系统还有资源就会创建新的线程来处理,直到new不出新的线程了,才会抛创建线程失败的异常。
activeMq中的线程池拒绝策略
activeMq中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常
pinpoint中的线程池拒绝策略
pinpoint的拒绝策略实现很有特点,和其他的实现都不同。他定义了一个拒绝策略链,包装了一个拒绝策略列表,当触发拒绝策略时,会将策略链中的rejectedExecution依次执行一遍。
4.自定义线程池
直接实现RejectedExecutionHandler的rejectedExecution方法即可
注:最佳自定义创建线程池,队列有界,maximumPoolSize有限,使用任务拒绝策略。如果队列无界,服务不了的任务总是会排队,消耗内存,甚至引发内存不足异常。如果队列有界但maximumPoolSize无线,可能会创建过多线程,占内存和CPU
5.如何设置线程池的参数
1.CPU密集型
System.out.println(Runtime.getRuntime().availableProcessors()) //查看CPU核数
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集任务只有在真正的多核CPU上才能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:一般公式:CPU核数+1个线程的线程池
2.IO密集型
由于IO密集型任务线程并不是一直执行任务,则应配置尽可能多的线程,如:CPU核数*2
IO密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费的阻塞时间。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式: CPU核数 / 1 -阻塞系数 阻塞系数在0.8~0.9之间
参考公式: CPU核数 / 1 -阻塞系数 阻塞系数在0.8~0.9之间
3.动态监控调参(美团方案)
1.动态调参:支持线程池参数动态调整、界面化操作;包括修改线程池核心大小、最大核心大小、队列长度等;参数修改后及时生效
2.任务监控:支持应用粒度、线程池粒度、任务粒度的Transaction监控;可以看到线程池的任务执行情况、最大任务执行时间、平均任务执行时间、95/99线等。
3.负载告警:线程池队列任务积压到一定值的时候会通过大象(美团内部通讯工具)告知应用开发负责人;当线程池负载数达到一定阈值的时候会通过大象告知应用开发负责人。
4.操作监控:创建/修改和删除线程池都会通知到应用的开发负责人。
5.操作日志:可以查看线程池参数的修改记录,谁在什么时候修改了线程池参数、修改前的参数值是什么。
6.权限校验:只有应用开发负责人才能够修改应用的线程池参数。
6.补充面试题
1.核心线程数会被回收吗?需要什么设置?
核心线程数默认是不会被回收的;
如果需要回收核心线程数,需要调用下面的方法:allowCoreThreadTimeOut 该值默认为 false
2.线程池被创建后里面有线程吗?如果没有的话,你知道有什么方法对线程池进行预热吗?
线程池被创建后如果没有任务过来,里面是不会有线程的。如果需要预热的话可以调用下面的两个方法:
全部启动:
仅启动一个:
全部启动:
仅启动一个:
3.你在工作中的单一的/固定的/可变的,你这三种创建线程池的方法,你用哪个多?超级大坑
答案是:一个都不用,我们生产只使用自定义的
4.线程池提交 execute 和 submit 有什么区别?
submit()方法用于提交需要返回值的任务。线程池会返回一个 future 类 型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可 以通过 future 的 get()方法来获取返回值
5.线程池怎么关闭知道吗?
可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们 的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来 中断线程,所以无法响应中断的任务可能永远无法终止
shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停 下了。这样做立即生效,但是风险也比较大。
shutdown()只是关闭了提交通道,用 submit()是无效的;而内部的任 务该怎么跑还是怎么跑,跑完再彻底停止线程池。
3.并发工具类
1.CountDownLatch(倒计数器)
场景 1:协调子线程结束动作:等待所有子线程运行结束
CountDownLatch 允许一个或多个线程等待其他线程完成操作。 例如,我们很多人喜欢玩的王者荣耀,开黑的时候,得等所有人都上线之 后,才能开打。
CountDownLatch是同步工具类之一,可以指定一个计数值,在并发环境下由线程进行减1操作,当计数值变为0之后,被await方法阻塞的线程将会唤醒,实现线程间的同步。
2.CyclicBarrier(同步屏障)
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要 做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直 到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会 继续运行。
CountDownLatch 类似,都可以协调多线程的结束动作,在它们结束后 都可以执行特定动作
3.Semaphore(信号量)
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协 调各个线程,以保证合理的使用公共资源
Semaphore 其实和锁有点类似,它一般用于控制对某组资源的访问权限。
CyclicBarrier 和 CountDownLatch 有什么区别?
CountDownLatch 是一次性的,而 CyclicBarrier 则可以多次设置屏障, 实现重复利用;
CountDownLatch 中的各个子线程不可以等待其他线程,只能完成自 己的任务;而 CyclicBarrier 中的各个线程可以等待其他线程
CountDownLatch 面向的是任务数,CyclicBarrier 面向的是线程数
在 CountDownLatch 中,如果某个线程出现 问题,其他线程不受影响;在 CyclicBarrier 中,如果某个线程遇到了中断、超时等问 题时,则处于 await 的线程都会出现问题
4.阻塞队列
1.ArrayBlockingQueue
ArrayBlockingQueue :由数组结构组成的有界阻塞队列。
子主题
2.LinkedBlockingQueue
LinkedBlockingQueue :由链表结构组成的有界阻塞队列。
3.PriorityBlockingQueue
PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
4.DelayQueue
DelayQueue:使用优先级队列实现的无界阻塞队列。
5.SynchronousQueue
SynchronousQueue:不存储元素的阻塞队列。
6.LinkedTransferQueue
LinkedTransferQueue:由链表结构组成的无界阻塞队列。
7.LinkedBlockingDeque
LinkedBlockingDeque:由链表结构组成的双向阻塞队列
5.锁
1.synchronized
1.synchronized如何使用
synchronized 经常用的,用来保证代码的原子性。
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当 前对象实例的锁
修饰静态方法:也就是给当前类加锁,会作⽤于类的所有对象实例 , 进⼊同步代码前要获得当前 class 的锁
修饰代码块 :指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的 锁
2.synchronized实现原理
synchronized 修饰代码块时,JVM 采用 monitorenter、monitorexit 两 个指令来实现同步,monitorenter 指令指向同步代码块的开始位 置, monitorexit 指令则指向同步代码块的结束位置。
synchronized 修饰同步方法时,JVM 采用 ACC_SYNCHRONIZED 标记符来实 现同步,这个标识指明了该方法是一个同步方法。
synchronized底层实际上是CAS自旋锁。
2.synchronized和lock区别
1.Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
2.synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3.Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4.通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
5.Lock可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择
3.说说 synchronized 和 ReentrantLock 的区别?
1.锁的实现: synchronized 是 Java 语言的关键字,基于 JVM 实现。而 ReentrantLock 是基于 JDK 的 API 层面实现的(一般是 lock()和 unlock() 方法配合 try/finally 语句块来完成。
2.性能: 在 JDK1.6 锁优化以前,synchronized 的性能比 ReenTrantLock 差很多。但是 JDK6 开始,增加了适应性自旋、锁消除等,两者性能就 差不多了
3.功能特点: ReentrantLock 比 synchronized 增加了一些高级功能,如 等待可中断、可实现公平锁、可实现选择性通知。
4.ReentrantLock 提供了一种能够中断等待锁的线程的机制,通过 lock.lockInterruptibly()来实现这个机制
5.ReentrantLock 可以指定是公平锁还是非公平锁。而 synchronized 只能 是非公平锁。所谓的公平锁就是先等待的线程先获得锁。
6.synchronized 与 wait()和 notify()/notifyAll()方法结合实现等待/通知机 制,ReentrantLock 类借助 Condition 接口与 newCondition()方法实现。
7.ReentrantLock 需要手工声明来加锁和释放锁,一般跟 finally 配合释放 锁。而 synchronized 不用手动释放锁。
4.volatile和synchronized区别
1.volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
2.volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
3.volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.《Java编程思想》上说,定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作)原子性。
4.volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
5.当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。
6.使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。
5.ThreadLocal
ThreaLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间的一些公共变量的传递的复杂度。用于解决数据库连接、session管理等。
6.ReentrantLock()
new ReentrantLock()构造函数默认创建的是非公平锁 NonfairSync。
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
7.公平锁与非公平锁
公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列 中排队,队列中的第一个线程才能获得锁 .
公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非 公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞, CPU 唤醒阻塞线程的开销比非公平锁大
非公平锁
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列 的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接 获取到锁 。
非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为 线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等 待队列中的线程可能会饿死,或者等很久才会获得锁
默认创建的对象 lock()的时候: 如果锁当前没有被其它线程占用,并且当前线程之前没有获取过该锁, 则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为 1 ,然后直接返回。如果当前线程之前己经获取过 该锁,则这次只是简单地把 AQS 的状态值加 1 后返回。
如果该锁己经被其他线程持有,非公平锁会尝试去获取锁,获取失败的 话,则调用该方法线程会被放入 AQS 队列阻塞挂起
区别
非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果 这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方 法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否 有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程 长期处于饥饿状态。
8.CAS
1.CAS 叫做 CompareAndSwap,⽐较并交换,主要是通过处理器的指令来保 证操作的原⼦性的。
2.CAS 指令包含 3 个参数:共享变量的内存地址 A、预期的值 B 和共享变 量的新值 C。 只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更 新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的
3.CAS产生的三大问题
ABA问题
ABA问题:并发环境下,假设初始条件是 A,去修改数据时,发现是 A 就会执行修改。 但是看到的虽然是 A,中间可能发生了 A 变 B,B 又变回 A 的情况。此时 A 已经非彼 A,数据即使成功修改,也可能有问题。
解决方案:加版本号、时间戳等
循环性能开销
问题:自旋 CAS,如果一直循环执行,一直不成功,会给 CPU 带来非常大的执行 开销。
解决方案:在 Java 中,很多使用自旋 CAS 的地方,会有一个自旋次数的限制,超过一 定次数,就停止自旋
只能保证一个变量的原子操作
问题:CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。
解决方案:可以考虑改用锁来保证操作的原子性;可以考虑合并多个变量,将多个变量封装成一个对象,通过 AtomicReference 来保证原子性。
9.锁升级
1.锁的状态
Java 对象头里,有一块结构,叫 Mark Word 标记字段,这块结构会随着锁的 状态变化而变化。
Mark Word 存储对象自身的运行数据,如哈希码、GC 分代年龄、锁状态标 志、偏向时间戳(Epoch) 等。
2.synchronized 优化了解吗?
在 JDK1.6 之前,synchronized 的实现直接调用 ObjectMonitor 的 enter 和 exit,这种锁被称之为重量级锁。从 JDK6 开始,HotSpot 虚拟机开发团队 对 Java 中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级 锁和偏向锁等优化策略,提升了 synchronized 的性能
偏向锁:在无竞争的情况下,只是在 Mark Word 里存储当前线程指针, CAS 操作都不做。
轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量 带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额 外有 CAS 操作的开销。
自旋锁:减少不必要的 CPU 上下文切换。在轻量级锁升级为重量级锁时, 就使用了自旋加锁的方式
锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更 大的锁。
锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被 检测到不可能存在共享数据竞争的锁进行消除。
轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量 带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额 外有 CAS 操作的开销。
自旋锁:减少不必要的 CPU 上下文切换。在轻量级锁升级为重量级锁时, 就使用了自旋加锁的方式
锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更 大的锁。
锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被 检测到不可能存在共享数据竞争的锁进行消除。
3.锁升级的过程是什么样的?
1.锁升级方向:无锁-->偏向锁---> 轻量级锁---->重量级锁,这个方向基本上 是不可逆的。
子主题
子主题
10.共享锁+独占锁
1.独占锁
独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
2.共享锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种
乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
ReadWriteLock 读写锁
如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁
如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
11.乐观锁+悲观锁
1.悲观锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
2.乐观锁
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为
别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),
如果失败则要重复读-比较-写的操作
别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数
据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),
如果失败则要重复读-比较-写的操作
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败
12.面试题补充
1.Java 有哪些保证原子性的方法?如何保证多线程 下 i++ 结果正确?
1.使用循环原子类,例如 AtomicInteger,实现 i++原子操作
2.使用 juc 包下的锁,如 ReentrantLock ,对 i++操作加锁 lock.lock() 来实现原子性
3.使用 synchronized,对 i++操作加锁
2.wait与sleep的区别
1.sleep 来自 Thread 类,和 wait 来自 Object 类。
2.最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3.wait,notify和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用(使用范围)
4.sleep 必须捕获异常,而 wait , notify 和 notifyAll 不需要捕获异常
6.JUC框架图
子主题
子主题
子主题
7.AQS
1.AQS是什么?
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包
AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包
2.AQS的原理
用大白话来说,AQS就是基于CLH队列(CLH同步队列是一个FIFO双向队列),用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
注意:AQS是自旋锁:**在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物
AQS 定义了两种资源共享方式:
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
1.Exclusive:独占,只有一个线程能执行,如ReentrantLock
2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
3.AQS用法
AQS管理一个关于状态信息的单一整数,该整数可以表现任何状态。比如, Semaphore 用它来表现剩余的许可数,ReentrantLock 用它来表现拥有它的线程已经请求了多少次锁;FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消)
使用AQS来实现一个同步器需要覆盖实现如下几个方法,并且使用getState,setState,compareAndSetState这几个方法来设置获取状态
8.面试补充
1.如何保证多线程,每个线程顺序执行?
1.使用join
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
t.join(); //调用join方法,等待线程t执行完毕
t.join(1000); //等待 t 线程,等待时间是1000毫秒。
t.join(); //调用join方法,等待线程t执行完毕
t.join(1000); //等待 t 线程,等待时间是1000毫秒。
2.使用CountDownLatch(倒计数器)
CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行。它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0。另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒。这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条件之后再执行
3.使用CachedThreadPool
FutureTask一个可取消的异步计算,FutureTask 实现了Future的基本方法,提空 start cancel 操作,可以查询计算是否已经完成,并且可以获取计算的结果。结果只可以在计算完成之后获取,get方法会阻塞当计算没有完成的时候,一旦计算已经完成,那么计算就不能再次启动或是取消。
一个FutureTask 可以用来包装一个 Callable 或是一个runnable对象。因为FurtureTask实现了Runnable方法,所以一个 FutureTask可以提交(submit)给一个Excutor执行(excution).
一个FutureTask 可以用来包装一个 Callable 或是一个runnable对象。因为FurtureTask实现了Runnable方法,所以一个 FutureTask可以提交(submit)给一个Excutor执行(excution).
4.使用blockingQueue
阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。
5.使用单个线程池
newSingleThreadExecutor返回以个包含单线程的Executor,将多个任务交给此Exector时,这个线程处理完一个任务后接着处理下一个任务,若该线程出现异常,将会有一个新的线程来替代。
2.Java死锁如何避免?
1.什么是死锁?
所谓死锁:是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进
2.产生死锁的必要条件:
互斥条件:⼀个资源每次只能被⼀个线程使⽤
请求和保持条件:⼀个线程在阻塞等待某个资源时,不释放已占有资源
不剥夺条件:⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
环路等待条件:若⼲线程形成头尾相接的循环等待资源关系
请求和保持条件:⼀个线程在阻塞等待某个资源时,不释放已占有资源
不剥夺条件:⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺
环路等待条件:若⼲线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3个 条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
3.在开发过程中如何解决
1. 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁
2. 要注意加锁时限,可以针对所设置⼀个超时时间
3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
2. 要注意加锁时限,可以针对所设置⼀个超时时间
3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
3.Java多个线程顺序打印数字
1.使用synchronized:三个线程无序竞争同步锁, 如果遇上的是自己的数字, 就打印. 这种方式会浪费大量的循环
2.使用synchronized配合wait()和notifyAll()
3.使用可重入锁:用Lock做, 非公平锁, 三个线程竞争, 如果遇上的是自己的数字, 就打印. 这种方式会浪费大量的循环
4.使用可重入锁, 启用公平锁:和3一样, 但是使用公平锁, 这种情况下基本上可以做到顺序执行, 偶尔会产生多一次循环
5.使用condition条件:给每个线程不同的condition. 可以用condition.signal()精确地通知对应的线程继续执行(在对应的condition上await的线程, 可能是多个).
7.netty4.X
1.为什么选择netty
1.API 使用简单,开发门槛低;
2.功能强大,预置了多种编解码功能,支持多种主流协议;
3.性能高,通过与其它业界主流的 NIO 框架对比,Netty 的综合性能最优;
4.经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络游戏、企业应 用、电信软件等众多行业得到成功商用;
2.原生的 NIO 在 JDK 1.7 版本存在 epoll bug
它会导致 Selector 空轮询,最终导致 CPU 100%。官方声称在 JDK1.6 版本的 update18 修复了 该问题,但是直到 JDK1.7 版本该问题仍旧存在,只不过该 BUG 发生概率降低了一些而已, 它并没有被根本解决。该 BUG 以及与该 BUG 相关的问题单可以参见以下链接内容。 http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6403933 http://bugs.java.com/bugdatabase/view_bug.do?bug_id=2147719
Selector BUG出现的原因:
若Selector的轮询结果为空,也没有wakeup或新消息处理,则会一直空轮询,占用CPU,导致CPU使用率100%,
Netty的解决办法:
1.对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数
2.在某个周期(如100ms)内如果连续发生N次轮询,说明触发了JDK NIO的epoll()死循环bug。n=1024.使用selector模型,最多1024个连接
3.将问题Selector上注册的Channel转移到新建的Selector上;
4.老的问题Selector关闭,使用新建的Selector替换。
这里,netty通过线程不断循环检测select是否返回0,若发生了1024次(次数不重要,若发生了epoll bug,肯定次数飙升),则开始重建selector。
3.什么是tcp的粘包拆包
1.客户端像服务端发送消息,服务端不知道客户端每次发送消息的数据大小,服务端可能出现把一个数据包拆成两个数据包进行读取这种被称为拆包, 也有可能把两个数据包当成一个数据包读取这种被称为粘包
2.例子:客户端像服务端发送了两个数据包dataA和dataB,但是服务端收到的包可能有4种情况
1.一次性的读取到dataA+dataB,这就是粘包
2.服务端读取到2个数据包,分别上位dataA_1和dataA_2+dataB,dataA先发送一部分,剩下的一部分跟随dataB一起发送,该情况成为拆包
3.服务端读到两个数据包。分别为dataA+ dataB_ 1和dataB_ 2。dataA发送的时候页带把dataB也发送了一部分,dataB剩下一部分单独发送,和2-样也是拆包
4.服务端读到两个数据包,分别为dataA和dataB 正常情况
3.为什么会发生粘包拆包?
1.应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包
2.应用程序写入数据小于套接字缓冲区大小,网卡应用多次写入的数据发送到网络上,这将会发生粘包
3.进行MSS (最大报文长度)大小的TCP分段,当TCP报文长度-TCP头部长度> MSS的时候将发生拆包。
4.接收方法不及时读取套接字缓冲区数据,这将发生粘包。
4.粘包拆包的解决方案?
1.发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每-个数据包的实际长度了。
2.发送端将每个数据包封装为固定长度不够的可以通过补0填充),这样接收端每次从接收缓中区中读取固定长度的数据就自然而然的把每 个数据包拆分开来。
3.可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
4.netty的线程模型
1.Reactor 单线程模型
1. 作为 NIO 服务器,接受客户端 TCP 连接,作为 NIO 客户端,向服务端发起 TCP 连接
2. 服务端读请求数据并响应,客户端写请求并读取响应
场景:对应小业务则适合,编码简单,对于高负载,高并发不合适。一个 NIO 线程处理太多请求,负载很高,并且响应变慢,导致大量请求超时,万一线程挂了,则不可用
场景:对应小业务则适合,编码简单,对于高负载,高并发不合适。一个 NIO 线程处理太多请求,负载很高,并且响应变慢,导致大量请求超时,万一线程挂了,则不可用
2.Reactor 多线程模型
一个 Acceptor线程,一组 NIO 线程,一般是使用自带线程池,包含一个任务队列和多个可用线程场景:可满足大多数场景,当Acceptor需要做负责操作的时候,比如认证等耗时操作 ,在高并发情况下也会有性能问题
3.主从 Reactor 多线程模型
Acceptor不在是一个线程,而是一组 NIO 线程,IO 线程也是一组 NIO 线程,这样就是 2 个线程池去处理接入和处理 IO场景:满足目前大部分场景,也是 Netty推荐使用的线程模型BossGroup 处理连接的WorkGroup 处理业务的
5.netty的高性能设计
1.IO多路复用通讯方式
Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端 Channel,由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 IO 阻塞导致的线程挂起。
2.异步通讯NIO
由于 Netty 采用了异步通信模式,一个 IO 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 IO 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
3.零拷贝机制(DIRECT BUFFERS 使用堆外直接内存)
Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝
Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的Buffer。
Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环 write 方式导致的内存拷贝问题
4.内存池(基于内存池的缓冲区重用机制)
随着 JVM 虚拟机和 JIT 即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区 Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制
5.高效的reactor线程模型
1.Reactor 单线程模型
2.Reactor 多线程模型
3.主从 Reactor 多线程模型
6.无锁设计、线程绑定
Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优
7.高性能的序列化框架
Netty 默认提供了对 Google Protobuf 的支持,通过扩展 Netty 的编解码接口,用户可以实现其它的
高性能序列化框架,例如 Thrift 的压缩二进制编解码框架
高性能序列化框架,例如 Thrift 的压缩二进制编解码框架
6.IO多路复用
1.select模型
1.时间复杂度为O(n),有IO事件发生了,却不知道用哪个流,只能进行无差别的轮询所有的流,找出读或者写的流,再对他们进行操作
2.缺点:
1.每次调用slect,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大,呈现线性增长。
2.同时每次调用select ,都需要任内核态遍历所有传递过来的fd,这个开销在fd很多时也很大。
3.select 支持的文件描述得太小,默认:1024/ 32位机器,单个进程能打开的最大连接数由FD_SetSize来定义。
4.select模型是水平触发,应用程序如果没有完成对一个已经就绪的文中描述符进行IO操作,那么之后每次进行Select调用还是会将这些文件描述符通知进程
3.流程步骤:
1.创建文件描述符集合fd_set,可以关注上面的读、写、异常事件,要创建3个fd_set,分别对这三种事件进行监听收集
2.调用select等待事件的发生
3.轮询select所有fd_set中的每一个fd,检查是否有相应的事件发生,如果有,则进行处理
4.select函数
int select(nfds, readfds, writefds, exceptfds, timeout);
nfds:select监视的文件句柄数,视进程中打开的文件数而定,一般设为你要监视各文件中的最大文件号加一。(注:nfds并非一定表示监视的文件句柄数。官方文档仅指出nfds is the highest-numbered file descriptor in any of the three sets, plus 1. (可在linux环境中通过man select命令查得))
readfds:select监视的可读文件句柄集合。
writefds: select监视的可写文件句柄集合。
exceptfds:select监视的异常文件句柄集合。
timeout:本次select()的超时结束时间。非阻塞模式下最大的等待事件。
nfds:select监视的文件句柄数,视进程中打开的文件数而定,一般设为你要监视各文件中的最大文件号加一。(注:nfds并非一定表示监视的文件句柄数。官方文档仅指出nfds is the highest-numbered file descriptor in any of the three sets, plus 1. (可在linux环境中通过man select命令查得))
readfds:select监视的可读文件句柄集合。
writefds: select监视的可写文件句柄集合。
exceptfds:select监视的异常文件句柄集合。
timeout:本次select()的超时结束时间。非阻塞模式下最大的等待事件。
2.pool模型
1.事件复杂度为O(n).poll的本质与select没有区别,他将用户传入的数据拷贝到内核空间,然后查询每个fd对应的设备状态,但是没有最大连接数的限制,原因是他是基于链表来存储。注意:windows平台不支持pool。
2.缺点:
1.大量的fd复制于用户态与内核态之间,而不管这样的复制有没有意义;
2.水平触发,如果报告了fd,没有被处理,那么下次poll时还会继续触发报告;
3.流程模型:
1.创建描述符集合,设置关注的事件
2.调用poll()函数,等待事件的发生,类似select,poll也可以设置等待时间;
3.轮询描述符事件,检查事件,处理事件;
4.poll函数:
poll函数:int poll(Struct poolfd* fds,unsigned int nfds, int timeout)
poolfd结构体定义如下:
Struct Pollfd{ int fd; // 文件描述符
Short events;// 等待的事件
Short revents;// 实际发生的事件}
poolfd结构体定义如下:
Struct Pollfd{ int fd; // 文件描述符
Short events;// 等待的事件
Short revents;// 实际发生的事件}
5.select与pool的区别:
1.select需要为读、写、异常事件分别建立一个文件描述符集合,最后轮询的时候需要分别轮询这三个集合;而poll只需要创建一个文件描述符集合,在每个文件描述符对应的结构上分别设置读、写、异常事件,最后轮询的时候,同时检查这三个事件
2.poll没有最大连接数的限制,原因是基于链表进行存储;
3.epool模型
1.时间复杂度为O(1);
2.特点:
1.可以说没有最大并发数的限制,能打开的上限远远大于1024(1G内存上能监听的端口约为10万个端口);
2.效率提升:不是轮询的方式,不会随着fd数目的增加而效率下降,只有活跃的用户的FD才会调用callback函数,即epool最大的优点在于只管活跃的连接数,而跟连接总数无关;
3.内存拷贝:使用了零拷贝技术。epoo通过用户态与内核态共享一块内存,来实现消息的传递;利用mmap()文件映射内存加速内核空间的消息传递,即epool使用mmap减少复制开销;
4.epool保证了每个FD在整个拷贝过程中只拷贝一次,select、pool每次调用都要把FD集合从用户态拷贝到内核态一次。
3.流程步骤:
1.通过调epolll-crete来创建一个epool文件描述符(句柄)、epol-create中有一个整型的参数Size,用来设置描述符事件列表的大小。
epool_create (int size);
epool_create (int size);
2.通过调用epool_ctrl来给描述符设置关注的事件,并把他添加到内核的事件列表中;
epool_ctrl(int epfd,int fd,struct epool_event * event);
epool_ctrl(int epfd,int fd,struct epool_event * event);
3.通过调用epool_wait来等待内核通知事件发生,进而进行事件的处理;
epool_wait()
epool_wait()
7.IO模式
1.BIO(阻塞IO)
2.NIO(非阻塞IO)
3.
4.
5.
8.netty原码分析
子主题
子主题
子主题
子主题
9.netty面试题补充
1.Netty 使用 NIO 而不是 AIO
在 linux系统上,AIO 的底层实现仍然使用 epoll,与 NIO 相同,因此在性能上没有明显的优势Netty 整体架构是 reactor 模型,采用 epoll机制,IO 多路复用,同步非阻塞模型Netty是基于 Java NIO 类库实现的异步通讯框架特点: 异步非阻塞,基于事件驱动,性能高,高可靠性,高可定制性。
子主题
子主题
8.mq部分
1.kafka
1.kafka架构图
1. broker:Kafka 服务器,负责消息存储和转发
2. topic:消息类别,Kafka 按照 topic 来分类消息
3. partition:topic 的分区,一个 topic 可以包含多个 partition,topic 消息保存在各个partition 上
4. offset:消息在日志中的位置,可以理解是消息在 partition 上的偏移量,也是代表该消息的唯一序号
5. Producer:消息生产者
6. Consumer:消息消费者
7. Consumer Group:消费者分组,每个 Consumer 必须属于一个 group
8. Zookeeper:保存着集群 broker、topic、partition 等 meta 数据;另外,还负责 broker 故障发现,partition leader 选举,负载均衡等功能
注:
1.offset在0.9版本之前是存储在zk,在0.9版本之后存储在本地;之所以换是因为,高并发情况下,消费者需要大量的与zookeeper的交互,太过于频繁,效率不高,造成zookeeper的压力。
2.Kafka存消息存在磁盘,默认保留7天,配置文件中默认为168h。
3.并发度最好的是:消费者组里的个数与主题数一样的时候
2.消息队列的好处
1.解耦
通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。
考虑一下你负责的系统中是否有类似的场景,就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦,也是可以的
比如很多的消息需要进行埋点,同步给其他的部门
2.异步
场景:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。
如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了
3.削峰填谷
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准
来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请
求而完全崩溃。
来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请
求而完全崩溃。
4.可恢复性
即使一部分挂掉,不影响其他的使用
5.顺序保证
kafka能够保证一个patition内的消息的有序性
3.消息队列的传递模式
1.点对点模式(一对一)
在点对点消息系统中,消息持久化到一个队列中。此时,将有一个或多 个消费者消费队列中的数据。但是一条消息只能被消
费一次。当一个消费者消费了队列中的某条数据之后,该条数据则从消息队列中删除。该模式即使有多个消费者同时消费数
据,也能保证数据处理的顺序。
费一次。当一个消费者消费了队列中的某条数据之后,该条数据则从消息队列中删除。该模式即使有多个消费者同时消费数
据,也能保证数据处理的顺序。
2.发布-订阅模式(一对多)
Kafka是基于发布-订阅模式
在发布-订阅消息系统中,消息被持久化到一个topic中。 与点对点消息系统不同的是,消费者可以订阅一个或多个topic,消
费者可以消费该topic中所有的数据,同-条数据可以被多个消费者消费,数据被消费后不会立马删除。在发布-订阅消息系
统中,消息的生产者称为发布者,消费者称为订阅者。
费者可以消费该topic中所有的数据,同-条数据可以被多个消费者消费,数据被消费后不会立马删除。在发布-订阅消息系
统中,消息的生产者称为发布者,消费者称为订阅者。
3.consumer是推还是拉?
Kafka 最初考虑的问题是,customer 应该从 brokes 拉取消息还是 brokers 将消 息推送到 consumer,也就是 pull 还 push。在这方面,Kafka 遵循了一种大部分消息系统共同的传统的设计:producer 将消息推送到 broker,consumer 从broker 拉取消息。
一些消息系统比如 Scribe 和 Apache Flume 采用了 push 模式,将消息推送到下游的 consumer。这样做有好处也有坏处:由 broker 决定消息推送的速率,对于不同消费速率consumer 就不太好处理了。消息系统都致力于让 consumer 以最大的速率最快速的消费消息,但不幸的是,push 模式下,当 broker 推送的速率远大于 consumer 消费的速率时,consumer 恐怕就要崩溃了。最终 Kafka 还 是选取了传统的 pull 模式。
Pull 模式的另外一个好处是 consumer 可以自主决定是否批量的从 broker 拉取数据。Push 模式必须在不知道下游 consumer 消费能力和消费策略的情况下决定是立即推送每条消息还是缓存之后批量推送。如果为了避免 consumer 崩溃而采用较低的推送速率,将可能导致一次只推送较少的消息而造成浪费。Pull 模式下,consumer 就可以根据自己的消费能力去决定这些策略。 Pull 有个缺点是,如果 broker 没有可供消费的消息,将导致 consumer 不断在循 环中轮询,直到新消息到达。为了避免这点,Kafka 有个参数可以让 consumer 阻塞直到新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可 以批量发送)。
4.kafka的工作流程
1.Kafka中消息是以 topic 进行分类的,生产者生产消息,消费者消费消息,都是面向 topic 的
2.topic 是逻辑上的概念,而 partition 是物理上的概念,每个 partition 对应于一个 log 文件,该 log 文件中存储的就是 producer 生产的数据。Producer生产的数据会被不断追加到该 log 文件末端,且每条数据都有自己的 offset。消费者组中的每个消费者,都会实时记录自己 消费到了哪个 offset,以便出错恢复时,从上次的位置继续消费
5.kafka的文件存储机制
1.文件存储机制
1.一个topic分为多个partition
2.一个partition分为多个segment(顺序读写、分段命令、二分查找)
3.一个segment对应两个文件:.log文件、index文件(分段索引、稀疏存储)
2.由于生产者生产的消息会不断追加到log文件末尾,为防止log文件过大导致数据定位效率低下,Kafka采取了分片和索引机制,将每个partition 分为多个segment。每个segment对应两个文件一-“index"文件和“.1og”文件。这些文件位于-一个文件夹下,该文件夹的命名规则为: topic 名称+分区序号。例如,first 这个topic有三个分区,则其对应的文件夹为first-0,first-1 ,first-2。
3.index和log文件以当前segment的第一条消息的 offset 命名。
4.index文件存储大量的索引信息,“ .log”文件存储大量的数据,索引文件中的元数据指向对应数据文件中message的物理偏移地址。
6.kafka生产者product
1.kafka生产者分区策略
我们需要将producer发送的数据封装成一- 个ProducerRecord对象。
1.指明partition 的情况下,直接将指明的值直接作为partiton 值;
2.没有指明partition 值但有key的情况下,将key的hash 值与topic 的partition数进行取余得到partition 值;
3.既没有partition 值又没有key值的情况下,第一次调用时随机生成-一个整数(后面每次调用在这个整数上自增),将这个值与topic 可用的partition 总数取余得到partition值,也就是常说的round-robin 算法。(轮训调度算法)
4.负载均衡(partition 会均衡分布到不同 broker 上)
由于消息 topic 由多个 partition 组成,且 partition 会均衡分布到不同 broker 上,因此,为了有
效利用 broker 集群的性能,提高消息的吞吐量,producer 可以通过随机或者 hash 等方式,将消
息平均发送到多个 partition 上,以实现负载均衡。
效利用 broker 集群的性能,提高消息的吞吐量,producer 可以通过随机或者 hash 等方式,将消
息平均发送到多个 partition 上,以实现负载均衡。
5.批量发送
是提高消息吞吐量重要的方式,Producer 端可以在内存中合并多条消息后,以一次请求的方式发送了批量的消息给 broker,从而大大减少 broker 存储消息的 IO 操作次数。但也一定程度上影响了消息的实时性,相当于以时延代价,换取更好的吞吐量。
6.压缩(GZIP或Snappy)
Producer 端可以通过 GZIP 或 Snappy 格式对消息集合进行压缩。Producer 端进行压缩之后,在Consumer 端需进行解压。压缩的好处就是减少传输的数据量,减轻对网络传输的压力,在对大数据处理上,瓶颈往往体现在网络上而不是 CPU(压缩和解压会耗掉部分 CPU 资源)。
2.kafka生产者数据可靠性如何保证
1.为保证producer发送的数据,能可靠的发送到指定的topic, topic 的每个partition 收到producer发送的数据后,都需要向producer 发送ack ( acknowledgement确认收到),如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。
2.何时发送ack?
答:确保所有的follower与leader同步完成,leader再发送ack ,这样才能保证leader挂掉之后,能在follower中选举出新的leader
3.多少个follower同步完成之后发送ack?
1.半数以上的follower同步完成,即可发送ack
优点:延迟低
缺点:选举新的leader时,容忍n台节点的故障,需要2n+1个副本
2.全部的follower同步完成,才可以发送ack
优点:选举新的leader时,容忍n台节点的故障,需要n+1个副本
缺点:延迟高
3.kafka选择的是第二种方案(全部同步)
1.同样为了容忍n台节点的故障,第- -种方案需要2n+1个副本,而第二种方案只需要n+1
个副本,而Kafka的每个分区都有大量的数据,第-种方案会造成大量数据的冗余。
个副本,而Kafka的每个分区都有大量的数据,第-种方案会造成大量数据的冗余。
2.虽然第二种方案的网络延迟会比较高,但网络延迟对Kafka的影响较小。
4.ISR
1.场景:采用第二种方案之后,设想以下情景: leader收到数据,所有follower 都开始同步数据,但有一个follower,因为某种故障,迟迟不能与leader进行同步,那leader就要- -直等 下去,直到它完成同步,才能发送ack。这个问题怎么解决呢?
2.解决方案:Leader维护了- -个动态的in-sync replica set(ISR),意为和leader保持同步的follower 集合。当ISR中的follower完成数据的同步之后,leader就会给follower发送ack。如果follower长时间未向leader同步数据,则该follower将被踢出ISR,该时间阈值由replica.lag. time.max.ms参数设定。Leader 发生故障之后,就会从ISR中选举新的leader.
3.Replica.lag.time.max.ms、Replica.lag.time.max.messages
低版本的kafka使用时间跟条数2个条件
0.9之后高版本的kafka只使用时间条件:在延迟时间内就加入进来,在延迟时间外就剔出。
默认延时时间为10秒(如果不能及时的发送抓取请求或者不能及时的消费到最后一个消息,这个follower会被leader移出ISR)
低版本的kafka使用时间跟条数2个条件
0.9之后高版本的kafka只使用时间条件:在延迟时间内就加入进来,在延迟时间外就剔出。
默认延时时间为10秒(如果不能及时的发送抓取请求或者不能及时的消费到最后一个消息,这个follower会被leader移出ISR)
5.ack应答机制
对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等ISR中的follower全部接收成功。
所以Kafka为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,选择以下的配置。request.required.asks=0
所以Kafka为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,选择以下的配置。request.required.asks=0
1.acks=0:
0: producer 不等待broker的ack,这一操作提供了一个最低的延迟,broker 一接收到还
没有写入磁盘就已经返回,当broker故障时有可能丢失数据:
没有写入磁盘就已经返回,当broker故障时有可能丢失数据:
2.acks=1:
1: producer 等待broker的ack, partition 的leader落盘成功后返回ack,如果在follower
同步成功之前leader故障,那么将会丢失数据;
同步成功之前leader故障,那么将会丢失数据;
3.acks=-1(All):
-1 (all) : producer 等待broker的ack, partition 的leader和follower全部落盘成功后才
返回ack.但是如果在fllower同步完成后,broker 发送ack之前,leader 发生故障,那么会
造成数据重复。
返回ack.但是如果在fllower同步完成后,broker 发送ack之前,leader 发生故障,那么会
造成数据重复。
3.故障处理
1.LEO(Log End Offset):指的是每个副本最大的offset,也就是最后一个offset
2.HW(High Watermark):指的是消费者能见到的最大的offset,ISR队列中最小的LEO
3.follower故障
follower发生故障后会被临时踢出ISR,待该follower恢复后,follower 会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等该follower的LEO大于等于该Partition的HW,即follower追上leader之后,就可以重新加入ISR了。
4.leader故障
leader发生故障之后,会从ISR中选出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据。.
注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
4.Exactly Once语义(精准一次性)
1.At Least Once语义
将服务器的ACK级别设置为-1,可以保证Producer到Server之间不会丢失数据,即At Least Once语义。
At Least once语义可以保证数据不丢失,但是不能保证数据不重复
2.At Most Once语义
将服务器ACK级别设置为0,可以保证生产者每条消息只会被发送一次,即At Most Once语义。
At Most Once语义可以保证数据不重复但是不能保证不丢失,但是对于一些非常重要的信息,比如说交易数据,下游数据消费者要求数据即不重复也不丢失,即Exactly Once语义
3.Exactly Once语义
1.在0.11版本以前的Kafka,对此是无能为力的,只能保证数据不丢失,再在下游消费者对数据做全局去重。对于多个下游应用的情况,每个都需要单独做全局去重,这就对性能造成了很大影响。
2.在0.11版本的Kafka,引入了一项重大特性:幂等性。所谓的幂等性就是指Producer不论向Server发送多少次重复数据,Server 端都只会持久化一条。幂等性结合At Least Once语义,就构成了Kafka的Exactly Once语义。即:At Least Once+幂等性= Exactly Once
3.要启用幂等性,只需要将Producer的参数中enable idompotence设置为true即可。Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的Producer在初始化的时候会被分配一个PID, 发往同一Partition 的消息会附带Sequence Number。而Broker端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时,Broker 只会持久化一条。
4.但是PID重启就会变化,同时不同的Partition也具有不同主键,所以幂等性无法保证跨分区跨会话的Exactly Once。
总结:1.需要设置acks为-1,保证了生产者到server之间数据不丢失,不能保证数据不重复;2.启用幂等性,这样在上游进行了去重,进而保证了数据不重复
5.kafka生产数据不能均匀到每个分区怎么处理?
kafka生产者数据不均匀,不能均匀分配数据到每个分区,这样容易造成消费积压,造成这个的原因是因为生产者使用了固定的key导致
解决方案:如果设定key,key不要用几个固定的常量,没有key,会随机分发到分区;会均匀;建议有业务含义的值,例如:订单号。正常情况下,应该是数据均匀在每个分区上的
7.kafka的消费者consumer
1.消费方式
1.consumer采用pull(拉)模式从broker中读取数据
2.push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。
它的目标是尽可能以最快速度传递消息,但是这样很容易造成consumner来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率消费消息。。
它的目标是尽可能以最快速度传递消息,但是这样很容易造成consumner来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率消费消息。。
3.pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环中,一直返回空数据。
针对这一点,Kafka的消费者在消费数据时会传入-一个时长参数timeout,如果当前没有数据可供消费,consumer会等待一段时 间之后再返回,这段时长即为timeout.。
针对这一点,Kafka的消费者在消费数据时会传入-一个时长参数timeout,如果当前没有数据可供消费,consumer会等待一段时 间之后再返回,这段时长即为timeout.。
2.分区分配策略
一个consumer group中有多个consumer, 一个topic 有多个partition,所以必然会涉及到partition的分配问题,即确定那个partition由哪个consumer来消费。Kafka有两种分配策略,一是RoundRobin, 二是Range.三是StickyAssignor
1.RoundRobin(轮询)
RoundRobinAssignor的分配策略是将消费组内订阅的所有Topic的分区及所有消费者进行排序后尽量均衡的分配(RangeAssignor是针对单个Topic的分区进行排序分配的)。如果消费组内,消费者订阅的Topic列表是相同的(每个消费者都订阅了相同的Topic),那么分配结果是尽量均衡的(消费者之间分配到的分区数的差值不会超过1)。如果订阅的Topic列表是不同的,那么分配结果是不保证“尽量均衡”的,因为某些消费者不参与一些Topic的分配
举例1:4个分区3个消费者的场景,分配结果如下:
C0: [P0, P3]、C1: [P1]、C2: [P2]
无法完全均衡分配的场景,排序更靠前的消费者分配到更多的分区
C0: [P0, P3]、C1: [P1]、C2: [P2]
无法完全均衡分配的场景,排序更靠前的消费者分配到更多的分区
举例2:2个topic,每个topic4个分区, 3个消费者的场景,分配结果如下:
CO: [TOPO,TOP3, T1P2]、C1: [T0P1, T1P0,T1P3]、C2: [T0P2,T1P1]
无法完全均衡分配的场景,排序更靠前的消费者分配到更多的分区
CO: [TOPO,TOP3, T1P2]、C1: [T0P1, T1P0,T1P3]、C2: [T0P2,T1P1]
无法完全均衡分配的场景,排序更靠前的消费者分配到更多的分区
2.Range(默认的分配策略)
RangeAssignor对每个Topic进行独立的分区分配。对于每一个Topic,首先对分区按照分区ID进行排序,然后订阅这个Topic的消费组的消费者再进行排序,之后尽量均衡的将分区分配给消费者。这里只能是尽量均衡,因为分区数可能无法被消费者数量整除,那么有一些消费者就会多分配到一些分区
1.四个分区p0、p1、p2、p3,两个消费c0、c1,分配情况为:c0(p0、p1)、c1(p2、p3)
2.四个分区p0、p1、p2、p3,三个消费c0、c1、c2,分配情况为:c0(p0、p1)、c1(p2)、c2(p3),即无法完成均衡分配的场景。排序更靠前的消费者分配到更多的分区
3.四个分区p0、p1、p2、p3,五个消费c0、c1、c2、c3、c4,分配情况为:c0(p0)、c1(p1)、c2(p2)、c3(p3),即消费者大于分区数量,排名靠前的消费者分配到分区,排名靠后的分配不到分区
注:这种分配方式明显的一个问题是随着消费者订阅的Topic的数量的增加,不均衡的问题会越来越严重,比如上图中4个分区3个消费者的场景,C0会多分配一个分区。如果此时再订阅一个分区数为4的Topic,那么C0又会比C1、C2多分配一个分区,这样C0总共就比C1、C2多分配两个分区了,而且随着Topic的增加,这个情况会越来越严重。
分配结果:订阅2个Topic,每个Topic4个分区,共3个Consumer
C0:[T0P0,T0P1,T1P0,T1P1]
C1:[T0P2,T1P2]
C2:[T0P3,T1P3]
分配结果:订阅2个Topic,每个Topic4个分区,共3个Consumer
C0:[T0P0,T0P1,T1P0,T1P1]
C1:[T0P2,T1P2]
C2:[T0P3,T1P3]
3.StickyAssignor分区分配算法
StickyAssignor分区分配算法,目的是在执行一次新的分配时,能在上一次分配的结果的基础上,尽量少的调整分区分配的变动,节省因分区分配变化带来的开销。Sticky是“粘性的”,可以理解为分配结果是带“粘性的”——每一次分配变更相对上一次分配做最少的变动。其目标有两点:
分区的分配尽量的均衡。
每一次重分配的结果尽量与上一次分配结果保持一致。
当这两个目标发生冲突时,优先保证第一个目标。第一个目标是每个分配算法都尽量尝试去完成的,而第二个目标才真正体现出StickyAssignor特性的
分区的分配尽量的均衡。
每一次重分配的结果尽量与上一次分配结果保持一致。
当这两个目标发生冲突时,优先保证第一个目标。第一个目标是每个分配算法都尽量尝试去完成的,而第二个目标才真正体现出StickyAssignor特性的
3.消息丢失
1.Consumer接受 Broker 使用 pull (拉)模式, 默认 100ms 拉一次. Consumer 消费的是Partition 的数据
2.消息丢失: 手动确认 ack 而不是自动提交 消息重复: 消费端幂等处理
3.消费者offset的维护
由于Consumer在消费过程中可能会出现断电宕机等故障,Consumer恢复后,需要从故障前的位置继续消费,所以Consumer需要实时记录自己消费到哪个位置,以便故障恢复后继续消费。Kafka0.9版本之前,Consumer默认将offset保存在zookeeper中,从0.9版本开始,Consumer默认将offset保存在Kafka一个内置的名字叫_consumeroffsets的topic中。默认是无法读取的,可以通过设置consumer.properties中的exclude.internal.topics=false来读取。
4.Consumer Group
在 Kafka 中, 一个 Topic 是可以被一个消费组消费, 一个Topic 分发给 Consumer Group 中的Consumer 进行消费, 保证同一条 Message 不会被不同的 Consumer 消费
注意: 当Consumer Group的 Consumer 数量大于 Partition 的数量时, 超过 Partition 的数量将会拿不到消息
5.Rebalance (重平衡)
Rebalance 本质上是一种协议, 规定了一个 Consumer Group 下的所有 consumer 如何达成一致,来分配订阅 Topic 的每个分区
Rebalance 发生时, 所有的 Consumer Group 都停止工作, 直到 Rebalance 完成
8.kafka高效读写数据
1.顺序写磁盘
Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到600M/s,而随机写只有100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
2.零拷贝技术
零拷贝并不是一次拷贝没有,只是减少了内核态到用户态之间的拷贝,减少不必要的拷贝次数
文件--内核空间--用户空间--socket缓冲区--网络
9.kafka 消息堆积如何处理
1.consumer导致kafka积压了大量消息
1.如果是Kafka消费能力不足,则可以考虑增加topic 的partition 的个数,同时提升消费者组的消费者数量,消费数=分区数(二者缺一不可)
2.若是下游数据处理不及时,则提高每批次拉取的数量。批次拉取数量过少(拉取数据/处理时间 <生产速度) ,使处理 的数据小于生产的数据, 也会造成数据积压。
2.消息过期失效
1、消费kafka消息时,应该尽量减少每次消费时间,可通过减少调用三方接口、读库等操作,从而减少消息堆积的可能性。
2、如果消息来不及消费,可以先存在数据库中,然后逐条消费(还可以保存消费记录,方便定位问题)
3、每次接受kafka消息时,先打印出日志,包括消息产生的时间戳。
4、kafka消 息保留时间( 修改kafka配置文件,默认一周)
5、任务启动从上次提交offset处开始消费处理
2、如果消息来不及消费,可以先存在数据库中,然后逐条消费(还可以保存消费记录,方便定位问题)
3、每次接受kafka消息时,先打印出日志,包括消息产生的时间戳。
4、kafka消 息保留时间( 修改kafka配置文件,默认一周)
5、任务启动从上次提交offset处开始消费处理
10.kafka运维命令
1.安装kakfa
wget https://archive.apache.org/dist/kafka/2.6.2/kafka_2.12-2.6.2.tgz
tar zxf kafka_2.12-2.6.2.tgz
cd kafka_2.12-2.6.2
tar zxf kafka_2.12-2.6.2.tgz
cd kafka_2.12-2.6.2
2.查看topic
bin/kafka-topics.sh --list --zookeeper z-1.uat-im-kafka.ntscpq.c21.kafka.us-east-1.amazonaws.com:2181
3.查看topic每个分区的数量
bin/kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list b-1.uat-im-kafka.ntscpq.c21.kafka.us-east-1.amazonaws.com:9092 --topic video_thumbsup_message_topic --time -1
4.扩容topic分区数
bin/kafka-topics.sh --alter --topic video_thumbsup_message_topic --zookeeper z-1.uat-im-kafka.ntscpq.c21.kafka.us-east-1.amazonaws.com:2181 --partitions 3
2.rabbitmq
1.rabbitmq的架构
1.Broker:接收和分发消息的应用,简单来说就是消息队列服务器实体
2.Virtual host:类似于网络中的namespace概念,当多个不同的用户使用同一一个RabbitMQ server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange / queue等
3.Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列
4.Queue: 消息队列载体,每个消息都会被投入到一个或多个队列
5.Binding: 绑定,它的作用就是把exchange和queue按照路由规则绑定起来
6.Routing Key: 路由关键字,exchange根据这个关键字进行消息投递
7.Producer: 消息生产者,就是投递消息的程序
8.Consumer: 消息消费者,就是接受消息的程序
9.Channel: 消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务
注:由Exchange、Queue、RoutingKey三个才能决定一个从Exchange到Queue的唯一的线路。
2.集群架构模式
1.主备模式
用来实现RabbitMQ的高可用集群,一般是在并发和数据不是特别多的时候使用,当主节点挂掉以后会从备
份节点中选择一个 节点出来作为主节点对外提供服务。
份节点中选择一个 节点出来作为主节点对外提供服务。
2.远程模式
主要用来实现双活,简称为Shovel模式,所谓的Shovel模式就是让我们可以把消息复制到不同的数据中心,让
两个跨地域的集群互联。
两个跨地域的集群互联。
1.远程模式我们会在不同的地域部署多个MQ节点或者集群,假设我们在北京和成都部署了2台MQ节点然后用户下单
2.用户在北京浏览我们的商城进行下单
3.商城使用MQ做异步处理但是发现北京的MQ压力非常的大这个时候他就可以把消息复制给成都的节点然后由成都的MQ去处理
4.使用了远程模式以后,消息就变成了近端同步确认远端异步确认,这种方式大大提高了订单的确认速度,同时也是一种可靠的实现
3.镜像队列模式
镜像队列也被称为Mirror队列,主要是用来保证mq消息可靠性的,他通过消息复制的方式能够保证我们的消息100%不丢失同时该集群模式也是企业中使用最多的模式。
1.用户发送一条消息
2.使用haproxy做负载均衡,同时因为当这个节点也可能挂掉所以受用keeplive漂移到另外一个haproxy节点提供服务
3.假设是当前节点收到了用户发送来的消息这个时候他会把该消息复制给节点2和3,当节点1挂掉的时候该消息在节点2和3中都是存在的,该条消息并没有因为节点1挂掉而丢失
4.多活模式
多活模式主要是用来实现异地数据复制,Shovel模式其实也可以实现,但是他的配置及其繁琐同时还要受到版本的限制,所以如果做异地多活我们更加推荐使用多活模式,使用多活模式我们需要借助federation插件来实现集群与集群之间或者节点与节点之前的消息复制,该模式被广泛应用于饿了么、美团滴滴等企业。
1.当用户发送条mq消息过来以后使用LBS做负载假设路由到了北京集群
2.北京集群接受到了该条消息以后就会使用federation插件把该条消息复制给成都的集群
3.我们做集群一般都是使用镜像队列的方式, 所以没有必要在集群与集群之间复制只需要北京的某一个节 点把数据复制到成都的某一个节点上然后成都的节点会使用镜像队列的方式自己去同步到所有的节点上
5.集群模式总结
1.备模式下主节点提供读写,从节点不提供读写服务,只是负责提供备份服务,备份节点的主要功能是在主节点宕机时,完成自动切换从-->主,同时因为主备模式下始终只有一个对外提供服务那么对于高并发的情况下该模式并不合适.
2.远程模式可以让我们实现异地多活的mq,但是现在已经有了更好的异地多活解决方案,所以在实际的项目中已经不推荐使用了
3.镜像队列模式可以让我们的消息100%不失同时可以结合HAProxy来实现高并发的业务场景所以在项目中使用得最多
2.远程模式可以让我们实现异地多活的mq,但是现在已经有了更好的异地多活解决方案,所以在实际的项目中已经不推荐使用了
3.镜像队列模式可以让我们的消息100%不失同时可以结合HAProxy来实现高并发的业务场景所以在项目中使用得最多
3.为什么要使用rabbitmq
1.在分布式系统下具备异步,削峰,负载均衡等一系列高级功能;
2.拥有持久化的机制,进程消息,队列中的信息也可以保存下来。
3.实现消费者和生产者之间的解耦。
4.对于高并发场景下,利用消息队列可以使得同步访问变为串行访问达到一定量的限流,利于数据库的操作
5.可以使用消息队列达到异步下单的效果,排队中,后台进行逻辑下单。
4.rabbitmq的工作模式有哪些
1.simple模式(即简单的收发模式)
2.work queues工作队列模式
3.pub/sub发布订阅模式
4.routing路由模式
5.topic主题模式
5.rabbitmq的高级部分
1.过期时间
过期时间TTL表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。RabbitMQ可以对消息和队列设置TTL.目前有两种方法可以设置。
●第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。
●第二种方法是对消息进行单独设置,每条消息TL可以不同。
如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就称为dead messagd被投递到死信队列,消费者将无法再收到该消息。
●第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。
●第二种方法是对消息进行单独设置,每条消息TL可以不同。
如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就称为dead messagd被投递到死信队列,消费者将无法再收到该消息。
2.死信队列
DLX,全称为Dead-Letter-Exchange ,可以称之为死信交换机,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另-个交换机中,这个交换机就是DLX,绑定DLX的队列就称之为死信队列。
消息变成死信,可能是由于以下的原因:
●消息被拒绝
●消息过期
●队列达到最大长度
DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。要想使用死信队列,只需要在定义队列的时候设置队列参数x-dead-letter-exchange指定交换机即可。
消息变成死信,可能是由于以下的原因:
●消息被拒绝
●消息过期
●队列达到最大长度
DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。要想使用死信队列,只需要在定义队列的时候设置队列参数x-dead-letter-exchange指定交换机即可。
3.延迟队列
延迟队列存储的对象是对应的延迟消息;所谓"延迟消息”是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
在RabbitMQ中延迟队列可以通过过期时间+死信队列来实现;
在RabbitMQ中延迟队列可以通过过期时间+死信队列来实现;
延迟队列的应用场景;如:
●在电商项目中的支付场景;如果在用户下单之后的几十分钟内没有支付成功;那么这个支付的订单算是支付失败,要进行支付失败的异常处理(将库存加回去),这时候可以通过使用延迟队列来处理
●在系统中如有需要在指定的某个时间之后执行的任务都可以通过延迟队列处理
●在电商项目中的支付场景;如果在用户下单之后的几十分钟内没有支付成功;那么这个支付的订单算是支付失败,要进行支付失败的异常处理(将库存加回去),这时候可以通过使用延迟队列来处理
●在系统中如有需要在指定的某个时间之后执行的任务都可以通过延迟队列处理
4.消息确认机制
确认并且保证消息被送达,提供了两种方式:发布确认和事务。(两者不可同时使用)在channel为事务时,不可引入确认模式;同样channel为确认模式下,不可使用事务。
1.发布确认:有两种方式:消息发送成功确认和消息发送失败回调。
2.事务支持
场景:业务处理伴随消息的发送,业务处理失败(事物回滚)后要求消息不发送。rabbitmq 使用调用者的外部事物,通常是首选,因为它是非侵入性的(低耦合)。
5.消息追踪
消息中心的消息追踪需要使用Trace实现,Trace 是Rabbitmq用于记录每一次发送的消息, 方便使用Rabbitmq的开发者调试、排错。可通过插件形式提供可视化界面。Trace启动后 会自动创建系统Exchange:amq.rabbitmq.trace ,每个队列会自动绑定该Exchange,绑定后发送到队列的消息都会记录到Trace日志。
6.消息如何分发
若该队列至少有一个消费者订阅,消息将以循环(round-robin)的方式发送给消 费者。每条消息只会分发给一个订阅的消费者(前提是消费者能够正常处理消息并进行确认)。 通过路由可实现多消费的功能
7.消息如何路由
消息提供方->路由->一至多个队列
消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。
通过队列路由键,可以把队列绑定到交换器上。
消息到达交换器后,RabbitMQ 会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)
消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。
通过队列路由键,可以把队列绑定到交换器上。
消息到达交换器后,RabbitMQ 会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)
常用的交换器
fanout:如果交换器收到消息,将会广播到所有绑定的队列上
direct:如果路由键完全匹配,消息就被投递到相应的队列
topic:可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时, 可以使用通配符
8.如何确保消息不丢失
消息持久化,当然前提是队列必须持久化
RabbitMQ 确保持久性消息能从服务器重启中恢复的方式是,将它们写入磁盘上 的一个持久化日志文件,当发布一条持久性消息到持久交换器上时,Rabbit 会在消息提交到日志文件后才发送响应。
一旦消费者从持久队列中消费了一条持久化消息,RabbitMQ 会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前 RabbitMQ 重启, 那么 Rabbit 会自动重建交换器和队列(以及绑定),并重新发布持久化日志文件中的消息到合适的队列
RabbitMQ 确保持久性消息能从服务器重启中恢复的方式是,将它们写入磁盘上 的一个持久化日志文件,当发布一条持久性消息到持久交换器上时,Rabbit 会在消息提交到日志文件后才发送响应。
一旦消费者从持久队列中消费了一条持久化消息,RabbitMQ 会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前 RabbitMQ 重启, 那么 Rabbit 会自动重建交换器和队列(以及绑定),并重新发布持久化日志文件中的消息到合适的队列
9.如何避免消息重复投递或者重复消费
先说为什么会重复消费:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;
但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
针对以上问题,一个解决思路是:保证消息的唯一性,就算是多次传输,不要让消息的多次消费带
来影响;保证消息等幂性;
1.比如:在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;
2.假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你
不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已
经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。
来影响;保证消息等幂性;
1.比如:在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;
2.假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你
不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已
经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。
在消息生产时,MQ 内部针对每条生产者发送的消息生成一个 inner-msg-id,作为去重的依据(消息投递失败并重传),避免重复的消息进入队列;在消息消费时,要求消息体中必须要有一个 bizId(对于同一业务全局唯一,如支付 ID、订单 ID、帖子 ID 等)作为去重的依据,避免同一条消息被重复消费。
10.如何确保消息正确地发送至 RabbitMQ? 如何确保消息接收方消费了消息?
1.发送方确认模式
将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID。
一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。 如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(not acknowledged,未确认)消息。
一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。 如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(not acknowledged,未确认)消息。
2.接收方确认机制
消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。 这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否
需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了 Consumer 足够长的时间来处理消息。保证数据的最终一致性;下面罗列几种特殊情况
如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ 会认为 消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息。
需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了 Consumer 足够长的时间来处理消息。保证数据的最终一致性;下面罗列几种特殊情况
如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ 会认为 消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息。
11.消息堆积如何解决
问题:当消息生产的速度长时间,远远大于消费的速度时。就会造成消息堆积。
解决:
1.消息队列堆积,想办法把消息转移到一个新的队列,增加服务器慢慢来消费这个消息
消息积压处理办法:临时紧急扩容:
先修复 consumer 的问题,确保其恢复消费速度,然后将现有 cnosumer 都停掉。
新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不
做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。
这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费
数据。
等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消
息。
新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。
然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不
做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。
接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。
这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费
数据。
等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消
息。
2.解决消费者的性能瓶颈:改短休眠时间
3.增加消费线程,增加多台服务器部署消费者。快速消费。
12.mq中消息失效怎么办
MQ中消息失效:假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。
13.mq中消息队列快满了怎么办
mq消息队列块满了:如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。
14.如何进行有序的消息消费
场景1:
场景介绍:当RabbitMQ采用work Queue模式,此时只会有一个Queue但是 会有多个Conpumer ,同时多个Consumer直接是竞争关系,此时就会出现MQ消息乱序的问题。
生产者依次发送条3条消息到MQ同时希望他们也能按照顺序进行消费;此时有三个消费者监听着当前的队列同时他们处于竞争关系现在每个消费者都拿到自己对于的消息但是每个消费者执行的效率和时机都是不确定的,可能得到的结果是消费者3先执行.消费者1再执行消费者2最后则行此时就出现了乱序问题
解决方案:生产者报据商品的id算出-个hash值然后在对我们的队列的个数取余就可以让相同id的所有的操作压到同一个队列,且每一个队列都只有一个消费者,此时就不会出现乱序的情况
场景2:
场景描述:当RabbitMQ采用简单队列模式的时候,如果消费者采用多线程的方式来加速消息的处理,此时也会出现消息乱序的问题。
生产者依次发送123三个消息;消费者从mq中拉取消息进行消费,此时因为只有一个消费者,消费能力非常有限,为了提交消息的处理能力,消费者使用多线程进行处理;消费者拉取消息1以后交给线程1处理消息2交给线程2处理消息3交给线程3处理但是每个线程执行的时间是不可控的.可能线程3先执行线程1再执行最后线程2执行此时就出现了消浪乱序的问题
解决方案:生产者按照自己希望的顺序依次发送消息到queue;消费者拉取消息然后根据d算出一个hash值然后把同d的商品压倒向-个内存队列公司一个线程去处理此时就保证了有序性
15.设计MQ的思路
比如说这个消息队列系统,我们从以下几个角度来考虑一下:
首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?
其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。
其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。能不能支持数据 0 丢失啊?可以呀,有点复杂的。
首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?
其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。
其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。能不能支持数据 0 丢失啊?可以呀,有点复杂的。
16.消息丢失
1.消息在生产者丢失
场景介绍:消息生产者发送消息成功,但是MQ没有收到该消息,消息在从生产者传输到MQ的过程中丢失,一般是由于网络不稳定的原因。
解决方案:采用RabbitMQ发送方消息确认机制,当消息成功被MQ接收到时,会给生产者发送一个确认消息, 表示接收成功。RabbitMQ 发送方消息确认模式有以下三种:普通确认模式,批量确认模式,异步监听确认模式。spring整合RabbitMQ后只使用了异步监听确认模式。
说明:异步监听模式,可以实现边发送消息边进行确认,不影响主线程任务执行。
2.消息在mq丢失
场景介绍:消息成功发送到MQ,消息还没被消费却在MQ中失,比如MQ服务器宕机或者重启会出现这种情况
解决方案:持久化交换机,队列,消息,确保MQ服务器重启时依然能从磁盘恢复对应的交换机,队列和消息。spring整合后默认开启了交换机,队列,消息的持久化,所以不修改任何设置就可以保证消息不在RabbitMQ丢失。但是为了以防万一,还是可以申明下。
3.消息在消费者丢失
场景介绍:消息费者消费消息时,如果设置为自动回复MQ,消息者端收到消息后会自动回复MQ服务器,MQ则会删除该条消息,如果消息已经在MQ被删除但是消费者的业务处理出现异常或者消费者服务宕机,那么就会导致该消息没有处理成功从而导致该条消息丢失。
解决方案:设置为手动回复MQ服务器,当消费者出现异常或者服务宕机时,MQ服务器不会删除该消息,而是会把消息重发给绑定该队列的消费者,如果该队列只绑定了一个消费者,那么该消息会一直保存在MQ服务器, 直到消息者能正常消费为止。本解决方案以一个队列绑定多个消费者为例来说明,-般在生产环境上地会让一个队列绑定多个消费者也就是工作队列模式来减轻压力,提高消息处理效率
MQ重发消息场景:
1.消费者未响应ACK,主动关闭频道或者连接
2.消费者未响应ACK,消费者服务挂掉
MQ重发消息场景:
1.消费者未响应ACK,主动关闭频道或者连接
2.消费者未响应ACK,消费者服务挂掉
17.重复消费
场景介绍:为了防止消息在消费者端丢失,会采用手动回复MQ的方式来解决,同时也引出了一个问题,消费者处理消息成功,手动回复MQ时由于网络不稳定,连接断开,导致MQ没有收到消费者回复的消息,那么该条消息还会保存在MQ的消息队列,由于MQ的消息重发机制,会重新把该条消息发给和该队列绑定的消息者处理,这样就会导致消息重复消费。而有些操作是不允许重复消费的,比如下单,减库存,扣款等操作。
MQ重发消息场景:
1.消费者未响应ACK,主动关闭频道或者连接
2消费者未响应ACK,消费者服务挂掉
MQ重发消息场景:
1.消费者未响应ACK,主动关闭频道或者连接
2消费者未响应ACK,消费者服务挂掉
解决方案:如果消费消息的业务是幂等性操作(同一个操作执行多次,结果不变)就算重复消费也没问题,可以不做处理,如果不支持幕等性操作,如:下单,减库存,扣款等,那么可以在消费者端每次消费成功后将该条消息id保存到数据库,每次消费前查询该消息id,如果该条消息id已经存在那么表示已经消费过就不再消费否则就消费。本方案采用redis存储消息id,因为redis是 单线程的,并且性能也非常好,提供了很多原子性的命令,本方案使用setnx命令存储消息id。
setnx(key,value):如果key不存在则插入成功且返回1 ,如果key存在则不进行任何操作,返回0
setnx(key,value):如果key不存在则插入成功且返回1 ,如果key存在则不进行任何操作,返回0
3.rocketmq
4.mq面试问题补充
1.mq的优点
异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。
异步:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库
要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450
+ 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求。
如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个
请求到返回响应给用户,总时长是 3 + 5 = 8ms。
要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450
+ 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求。
如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个
请求到返回响应给用户,总时长是 3 + 5 = 8ms。
应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。
A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如
果 C 系统现在不需要了呢?A 系统负责人几乎崩溃…A 系统跟其它各种乱七八糟的系统严重耦合,
A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。如果使用 MQ,A
系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需
要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即
可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑
人家是否调用成功、失败超时等情况。
果 C 系统现在不需要了呢?A 系统负责人几乎崩溃…A 系统跟其它各种乱七八糟的系统严重耦合,
A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。如果使用 MQ,A
系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需
要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即
可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑
人家是否调用成功、失败超时等情况。
就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其 实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦。
流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求
削峰:减少高峰时期对服务器压力。
日志处理 - 解决大量日志传输。
消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。
2.mq的缺点
1.系统可用性降低
本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性会降低;
本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性会降低;
2. 系统复杂度提高
加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。
加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。
3. 一致性问题
A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
3.你们公司生产环境用的是什么消息中间件?
一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;
后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高;
不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。
所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。
如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范
4.MQ 有哪些常见问题?如何解决这些问题?
1.消息的顺序问题
1.问题:消息有序指的是可以按照消息的发送顺序来消费。
假如生产者产生了 2 条消息:M1、M2,假定 M1 发送到 S1,M2 发送到 S2,如果要保证 M1 先 于 M2 被消费,怎么做?
假如生产者产生了 2 条消息:M1、M2,假定 M1 发送到 S1,M2 发送到 S2,如果要保证 M1 先 于 M2 被消费,怎么做?
2.解决方案:保证生产者 - MQServer - 消费者是一对一对一的关系
3.缺陷:
并行度就会成为消息系统的瓶颈(吞吐量不够)
更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花
费更多的精力来解决阻塞的问题。 (2)通过合理的设计或者将问题分解来规避。
不关注乱序的应用实际大量存在
队列无序并不意味着消息无序 所以从业务层面来保证消息的顺序而不仅仅是依赖于消息系
统,是一种更合理的方式
并行度就会成为消息系统的瓶颈(吞吐量不够)
更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花
费更多的精力来解决阻塞的问题。 (2)通过合理的设计或者将问题分解来规避。
不关注乱序的应用实际大量存在
队列无序并不意味着消息无序 所以从业务层面来保证消息的顺序而不仅仅是依赖于消息系
统,是一种更合理的方式
2.消息的重复问题
造成消息重复的根本原因是:网络不可达。
所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消
息,应该怎样处理?
消费端处理消息的业务逻辑保持幂等性。只要保持幂等性,不管来多少条重复消息,最后处理的结
果都一样。保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。利用一张日
志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条
消息。
所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消
息,应该怎样处理?
消费端处理消息的业务逻辑保持幂等性。只要保持幂等性,不管来多少条重复消息,最后处理的结
果都一样。保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。利用一张日
志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条
消息。
9.linux
子主题
子主题
10.hbase
1.hbase特点
1.海量存储
Hbase 适合存储 PB 级别的海量数据,在 PB 级别的数据以及采用廉价 PC 存储的情况下,能在几十到百毫秒内返回数据。这与 Hbase 的极易扩展性息息相关。正式因为 Hbase 良好的扩展性,才为海量数据的存储提供了便利。
2.列式存储
这里的列式存储其实说的是列族存储,Hbase 是根据列族来存储数据的。列族下面可以有非常多的列,列族在创建表的时候就必须指定。
3.极易扩展
Hbase 的扩展性主要体现在两个方面,一个是基于上层处理能力(RegionServer)的扩展,一个是基于存储的扩展(HDFS)
通过横向添加 RegionSever 的机器,进行水平扩展,提升 Hbase 上层的处理能力,提升 Hbsae服务更多 Region 的能力
4.高并发
由于目前大部分使用 Hbase 的架构,都是采用的廉价 PC,因此单个 IO 的延迟其实并不小,一般在几十到上百 ms 之间。这里说的高并发,主要是在并发的情况下,Hbase 的单个IO 延迟下降并不多。能获得高并发、低延迟的服务。
5.稀疏
稀疏主要是针对 Hbase 列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的
2.hbase架构
Hbase 是由 Client、Zookeeper、Master、HRegionServer、HDFS 等几个组件组成,下面来介绍一下几个组件的相关功能:
1.client
Client 包含了访问 Hbase 的接口,另外 Client 还维护了对应的 cache 来加速 Hbase 的访
问,比如 cache 的.META.元数据的信息
问,比如 cache 的.META.元数据的信息
2.Zookeeper
HBase 通过 Zookeeper 来做 master 的高可用、RegionServer 的监控、元数据的入口以及集群配置的维护等工作。具体工作如下:
1.通过 Zoopkeeper 来保证集群中只有 1 个 master 在运行,如果 master 异常,会通过竞争机制产生新的 master 提供服务
2.通过 Zoopkeeper 来监控 RegionServer 的状态,当 RegionSevrer 有异常的时候,通过回调的形式通知 Master RegionServer 上下线的信息
3.通过 Zoopkeeper 存储元数据的统一入口地址
3.Hmaster
master 节点的主要职责如下:
1.为 RegionServer 分配 Region
2.维护整个集群的负载均衡
3.维护集群的元数据信息
4.发现失效的 Region,并将失效的 Region 分配到正常的 RegionServer 上
5.当 RegionSever 失效的时候,协调对应 Hlog 的拆分
1.为 RegionServer 分配 Region
2.维护整个集群的负载均衡
3.维护集群的元数据信息
4.发现失效的 Region,并将失效的 Region 分配到正常的 RegionServer 上
5.当 RegionSever 失效的时候,协调对应 Hlog 的拆分
4.HregionServer
HregionServer 直接对接用户的读写请求,是真正的“干活”的节点。它的功能概括如下:
1.管理 master 为其分配的 Region
2.处理来自客户端的读写请求
3.负责和底层 HDFS 的交互,存储数据到 HDFS
4.负责 Region 变大以后的拆分
5.负责 Storefile 的合并工作
1.管理 master 为其分配的 Region
2.处理来自客户端的读写请求
3.负责和底层 HDFS 的交互,存储数据到 HDFS
4.负责 Region 变大以后的拆分
5.负责 Storefile 的合并工作
5.HDFS
HDFS 为 Hbase 提供最终的底层数据存储服务,同时为 HBase 提供高可用(Hlog 存储在HDFS)的支持,具体功能概括如下:
提供元数据和表数据的底层分布式存储服务;数据多副本,保证的高可靠和高可用性
提供元数据和表数据的底层分布式存储服务;数据多副本,保证的高可靠和高可用性
3.HBase 中的角色
1.HMaster
1.监控 RegionServer
2.处理 RegionServer 故障转移
3.处理元数据的变更
4.处理 region 的分配或转移
5.在空闲时间进行数据的负载均衡
6.通过 Zookeeper 发布自己的位置给客户端
2.处理 RegionServer 故障转移
3.处理元数据的变更
4.处理 region 的分配或转移
5.在空闲时间进行数据的负载均衡
6.通过 Zookeeper 发布自己的位置给客户端
2.RegionServer
1.负责存储 HBase 的实际数据
2.处理分配给它的 Region
3.刷新缓存到 HDFS
4.维护 Hlog
5.执行压缩
6.负责处理 Region 分片
2.处理分配给它的 Region
3.刷新缓存到 HDFS
4.维护 Hlog
5.执行压缩
6.负责处理 Region 分片
3.其他组件
1.Write-Ahead logs
2.Region
Hbase 表的分片,HBase 表会根据 RowKey值被切分成不同的 region 存储在 RegionServer中,在一个 RegionServer 中可以有多个不同的 region。
3.Store
HFile 存储在 Store 中,一个 Store 对应 HBase 表中的一个列族
4.MemStore
顾名思义,就是内存存储,位于内存中,用来保存当前的数据操作,所以当数据保存在WAL 中之后,RegsionServer 会在内存中存储键值对
5.HFile
这是在磁盘上保存原始数据的实际的物理文件,是实际的存储文件。StoreFile 是以 Hfile的形式存储在 HDFS 的。
4.HBase Shell 操作
1.进入 HBase 客户端命令行:bin/hbase shell
2.查看当前数据库中有哪些表:list
3.创建表:create 'student','info'
4.插入数据到表:hbase(main):003:0> put 'student','1001','info:sex','male'
hbase(main):004:0> put 'student','1001','info:age','18'
hbase(main):005:0> put 'student','1002','info:name','Janna'
hbase(main):004:0> put 'student','1001','info:age','18'
hbase(main):005:0> put 'student','1002','info:name','Janna'
5.扫描查看表数据:hbase(main):008:0> scan 'student'
hbase(main):009:0> scan 'student',{STARTROW => '1001', STOPROW => '1001'}
hbase(main):010:0> scan 'student',{STARTROW => '1001'}
hbase(main):009:0> scan 'student',{STARTROW => '1001', STOPROW => '1001'}
hbase(main):010:0> scan 'student',{STARTROW => '1001'}
6.查看表结构:describe ‘student’
7.更新指定字段的数据:hbase(main):012:0> put 'student','1001','info:name','Nick'
hbase(main):013:0> put 'student','1001','info:age','100'
hbase(main):013:0> put 'student','1001','info:age','100'
8.统计表数据行数:count 'student'
9.删除数据:
删除某 rowkey 的全部数据:deleteall 'student','1001'
删除某 rowkey 的某一列数据:delete 'student','1002','info:sex'
10.清空表数据:truncate 'student'提示:清空表的操作顺序为先 disable,然后再 truncate。
11.删除表
首先需要先让该表为 disable 状态:
hbase(main):019:0> disable 'student'
然后才能 drop 这个表:
hbase(main):020:0> drop 'student'
提示:如果直接 drop 表,会报错:ERROR: Table student is enabled. Disable it first.
hbase(main):019:0> disable 'student'
然后才能 drop 这个表:
hbase(main):020:0> drop 'student'
提示:如果直接 drop 表,会报错:ERROR: Table student is enabled. Disable it first.
12.变更表信息
将 info 列族中的数据存放 3 个版本:
hbase(main):022:0> alter 'student',{NAME=>'info',VERSIONS=>3}
hbase(main):022:0> get
'student','1001',{COLUMN=>'info:name',VERSIONS=>3}
hbase(main):022:0> alter 'student',{NAME=>'info',VERSIONS=>3}
hbase(main):022:0> get
'student','1001',{COLUMN=>'info:name',VERSIONS=>3}
5.HBase 数据结构
1.RowKey
1.与 nosql 数据库们一样,RowKey 是用来检索记录的主键
2.访问 HBASE table 中的行,只有三种方式:
1.通过单个 RowKey 访问
2.通过 RowKey 的 range(正则)
3.全表扫描
3.RowKey 行键 (RowKey)可以是任意字符串(最大长度是 64KB,实际应用中长度一般为10-100bytes),在 HBASE 内部,RowKey 保存为字节数组。存储时,数据按照 RowKey 的字典序(byte order)排序存储。设计 RowKey 时,要充分排序存储这个特性,将经常一起读取的行存储放到一起。(位置相关性)
2.Column Family:列族
列族:HBASE 表中的每个列,都归属于某个列族。列族是表的 schema 的一部 分(而列不是),必须在使用表之前定义。列名都以列族作为前缀。例如 courses:history,courses:math都属于 courses 这个列族。
3.Cell
由{rowkey, column Family:columu, version} 唯一确定的单元。cell 中的数据是没有类的,全部是字节码形式存贮。
关键字:无类型、字节码
4.Time Stamp
HBASE 中通过 rowkey和 columns 确定的为一个存贮单元称为cell。每个 cell都保存 着同一份数据的多个版本。版本通过时间戳来索引。时间戳的类型是 64 位整型。时间戳可以由 HBASE(在数据写入时自动 )赋值,此时时间戳是精确到毫秒 的当前系统时间。时间戳也可以由客户显式赋值。如果应用程序要避免数据版 本冲突,就必须自己生成具有唯一性的时间戳。每个 cell 中,不同版本的数据按照时间倒序排序,即最新的数据排在最前面。
版本回收:为了避免数据存在过多版本造成的的管理 (包括存贮和索引)负担,HBASE 提供 了两种数据版本回收方式。一是保存数据的最后 n 个版本,二是保存最近一段 时间内的版本(比如最近七天)。用户可以针对每个列族进行设置。
5.命名空间(HBase NameSpaces)
1.Table:表,所有的表都是命名空间的成员,即表必属于某个命名空间,如果没有指定,则在 default 默认的命名空间中。
2.RegionServer group:一个命名空间包含了默认的 RegionServer Group
3.Permission:权限,命名空间能够让我们来定义访问控制列表 ACL(Access Control List)。例如,创建表,读取表,删除,更新等等操作。
4.Quota:限额,可以强制一个命名空间可包含的 region 的数量。
6.Hbase原理
1.读流程
1.Client 先访问 zookeeper,从 meta 表读取 region 的位置,然后读取 meta 表中的数据。meta中又存储了用户表的 region 信息
2.根据 namespace、表名和 rowkey 在 meta 表中找到对应的 region 信息;
3.找到这个 region 对应的 regionserver;
4.查找对应的 region;
5.先从 MemStore 找数据,如果没有,再到 BlockCache 里面读;
6.BlockCache 还没有,再到 StoreFile 上读(为了读取的效率);
7.如果是从 StoreFile 里面读取的数据,不是直接返回给客户端,而是先写入 BlockCache,再返回给客户端
2.写流程
1.Client 向 HregionServer 发送写请求;
2.HregionServer 将数据写到 HLog(write ahead log)。为了数据的持久化和恢复;
3.HregionServer 将数据写到内存(MemStore);
4.反馈 Client 写成功。
3.数据 flush 过程
1.当 MemStore 数据达到阈值(默认是 128M,老版本是 64M),将数据刷到硬盘,将内存中的数据删除,同时删除 HLog 中的历史数据;
2.并将数据存储到 HDFS 中;
3.在 HLog 中做标记点。
4.数据合并过程
1.当数据块达到 4 块,Hmaster 触发合并操作,Region 将数据块加载到本地,进行合并;
2.当合并的数据超过 256M,进行拆分,将拆分后的 Region 分配给不同的 HregionServer管理
3.当HregionServer宕机后,将HregionServer上的hlog拆分,然后分配给不同的HregionServer加载,修改.META.;
4.注意:HLog 会同步到 HDFS。
7.HBase优化
1.高可用
在 HBase 中 Hmaster 负责监控 RegionServer 的生命周期,均衡 RegionServer 的负载,如果 Hmaster 挂掉了,那么整个 HBase 集群将陷入不健康的状态,并且此时的工作状态并不会维持太久。所以 HBase 支持对 Hmaster 的高可用配置
1.关闭 HBase 集群(如果没有开启则跳过此步)bin/stop-hbase.sh
2.在 conf 目录下创建 backup-masters 文件 touch conf/backup-masters
3.在 backup-masters 文件中配置高可用 HMaster 节点:echo hadoop103 > conf/backup-masters
4.将整个 conf 目录 scp 到其他节点:[atguigu@hadoop102 hbase]$ scp -r conf/ hadoop103:/opt/module/hbase/
[atguigu@hadoop102 hbase]$ scp -r conf/ hadoop104:/opt/module/hbase/
[atguigu@hadoop102 hbase]$ scp -r conf/ hadoop104:/opt/module/hbase/
5.打开页面测试查看: http://hadooo102:16010
2.预分区
每一个 region 维护着 startRow 与 endRowKey,如果加入的数据符合某个 region 维护的rowKey 范围,则该数据交给这个 region 维护。那么依照这个原则,我们可以将数据所要投放的分区提前大致的规划好,以提高 HBase 性能。
3.RowKey 设计
一条数据的唯一标识就是 rowkey,那么这条数据存储于哪个分区,取决于 rowkey 处于哪个一个预分区的区间内,设计 rowkey的主要目的 ,就是让数据均匀的分布于所有的 region中,在一定程度上防止数据倾斜。接下来我们就谈一谈 rowkey 常用的设计方案
1.生成随机数、hash、散列值
比如:原 本 rowKey 为 1001 的 , SHA1 后变成:dd01903921ea24941c26a48f2cec24e0bb0e8cc7
原 本 rowKey 为 3001 的 , SHA1 后变成:49042c54de64a1e9bf0b33e00245660ef92dc7bd
原 本 rowKey 为 5001 的 , SHA1 后变成:7b61dec07e02c188790670af43e717f0f46e8913
在做此操作之前,一般我们会选择从数据集中抽取样本,来决定什么样的 rowKey 来 Hash后作为每个分区的临界值
原 本 rowKey 为 3001 的 , SHA1 后变成:49042c54de64a1e9bf0b33e00245660ef92dc7bd
原 本 rowKey 为 5001 的 , SHA1 后变成:7b61dec07e02c188790670af43e717f0f46e8913
在做此操作之前,一般我们会选择从数据集中抽取样本,来决定什么样的 rowKey 来 Hash后作为每个分区的临界值
2.字符串反转
20170524000001 转成 10000042507102
20170524000002 转成 20000042507102
这样也可以在一定程度上散列逐步 put 进来的数据。
20170524000002 转成 20000042507102
这样也可以在一定程度上散列逐步 put 进来的数据。
3.字符串拼接
20170524000001_a12e
20170524000001_93i7
20170524000001_93i7
RowKey散列原则
如果RowKey是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将RowKey的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer实现负载均衡的几率,如果没有散列字段,首字段直接是时间信息,将产生所有数据都在一个RegionServer上堆积的热点现象,这样在做数据检索的时候负载将会集中在个别RegionServer,降低查询效率。
RowKey唯一原则
RowKey是按照字典排序存储的,因此,设计RowKey时候,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。
举个例子:如果最近写入HBase表中的数据是最可能被访问的,可以考虑将时间戳作为RowKey的一部分,由于是字段排序,所以可以使用Long.MAX_VALUE-timeStamp作为RowKey,这样能保证新写入的数据在读取时可以别快速命中。
案例分析:
用户订单列表查询RowKey设计。
##需求场景
某用户根据查询条件查询历史订单列表
##查询条件
开始结束时间(orderTime)-----必选,
订单号(seriaNum),
状态(status),游戏号(gameID)
##结果显示要求
结果按照时间倒叙排列。
#解答
RowKey可以设计为:userNumo r d e r T i m e orderTimeorderTimeseriaNum
注:这样设计已经可以唯一标识一条记录了,订单详情都是可以根据订单号seriaNum来确定。在模糊匹配查询的时候startRow和endRow只需要设置到userNum$orderTime即可,如下:
startRow=userNum$maxvalue-stopTime
endRow=userNum$maxvalue-startTime
其他字段用filter实现。
用户订单列表查询RowKey设计。
##需求场景
某用户根据查询条件查询历史订单列表
##查询条件
开始结束时间(orderTime)-----必选,
订单号(seriaNum),
状态(status),游戏号(gameID)
##结果显示要求
结果按照时间倒叙排列。
#解答
RowKey可以设计为:userNumo r d e r T i m e orderTimeorderTimeseriaNum
注:这样设计已经可以唯一标识一条记录了,订单详情都是可以根据订单号seriaNum来确定。在模糊匹配查询的时候startRow和endRow只需要设置到userNum$orderTime即可,如下:
startRow=userNum$maxvalue-stopTime
endRow=userNum$maxvalue-startTime
其他字段用filter实现。
4.内存优化
HBase 操作过程中需要大量的内存开销,毕竟 Table 是可以缓存在内存中的,一般会分配整个可用内存的 70%给 HBase 的 Java 堆。但是不建议分配非常大的堆内存,因为 GC 过程持续太久会导致 RegionServer 处于长期不可用状态,一般 16~48G 内存就可以了,如果因为框架占用内存过高导致系统内存不足,框架一样会被系统服务拖死。
5.基础优化
1.允许在 HDFS 的文件中追加内容
2.优化 DataNode 允许的最大文件打开数
3.优化延迟高的数据操作的等待时间
4.优化数据的写入效率
5.设置 RPC 监听数量
6.优化 HStore 文件大小
7.优化 hbase 客户端缓存
8.指定 scan.next 扫描 HBase 所获取的行数
9.flush、compact、split 机制
11.数据结构与算法
1.java算法
1.冒泡排序
1.比较相邻的元素。如果前一个元素比后一个元素大 ,就交换这两个元素的位置。
2.对每一对相邻元素做同样的工作 ,从开始第一对元素到结尾的最后一对元素。最终最后位置的元素就是最大值。
3.时间复杂度:O(N^2)
2.选择排序
1.每一次遍历的过程中,都假定第一个索引处的元素是 最小值,和其他索引处的值依次进行比较,如果当前索引处的值大于其他某个索引处的值,则假定其他某个索弓出的值为最小值,最后可以找到最小值所在的索引
2.交换第一个索|处和最小值所在的索引|处的值
3.时间复杂度:O(N^2)
3.插入排序
1.把所有的元素分为两组,已经排序的和未排序的;
2.找到未排序的组中的第一个元素,向已经排序的组中进行插入;
3.倒叙遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素,那么就把待插入元素放到这个位置,其他的元素向后移动-位;
4.时间复杂度:O(N^2)
4.希尔排序
1.选定一个增长量h ,按照增长量h作为数据分组的依据,对数据进行分组;
2.对分好组的每一组数据完成插入排序;
3.减小增长量,最小减为1 ,重复第二步操作。
5.归并排序
1.尽可能的一组数据拆分成两个元索相等的子组,并对每一个子组继续拆分 ,直到拆分后的每个子组的元素个数是1为止。
2.将相邻的两个子组进行合并成一个有序的大组;
3.不断的重复步骤2 ,直到最终只有一个组为止。
2.将相邻的两个子组进行合并成一个有序的大组;
3.不断的重复步骤2 ,直到最终只有一个组为止。
4.时间复杂度:O(nlogn)
归并排序的缺点:需要申请额外的数组空间,导致空间复杂度提升,是典型的以空间换时间的造作。
6.快速排序
1.首先设定一个分界值 ,通过该分界值将数组分成左右两部分;
2.将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值;
3.然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
4.重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。
2.将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值;
3.然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
4.重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。
2.雪花算法
3.Leaf算法(美团)
12.springboot
1.SpringBoot启动类注解?它是由哪些注解组成?
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项。
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@ComponentScan:Spring组件扫描
@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项。
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@ComponentScan:Spring组件扫描
2.SpringBoot启动方式?
1. 直接执行 main 方法运行
2. 命令行 java -jar 的方式 打包用命令或者放到容器中运行
3.用 Maven/ Gradle 插件运行
2. 命令行 java -jar 的方式 打包用命令或者放到容器中运行
3.用 Maven/ Gradle 插件运行
3.SpringBoot需要独立的容器运行?
不需要,内置了 Tomcat/Jetty。
4.SpringBoot自动配置原理?
@EnableAutoConfiguration (开启自动配置) 该注解引入了AutoConfigurationImportSelector,该类中
的方法会扫描所有存在META-INF/spring.factories的jar包。
的方法会扫描所有存在META-INF/spring.factories的jar包。
5.SpringBoot如何兼容Spring项目?
在启动类加:
@ImportResource(locations = {"classpath:spring.xml"})
@ImportResource(locations = {"classpath:spring.xml"})
6.针对请求访问的几个组合注解?
@PatchMapping
@PostMapping
@GetMapping
@PutMapping
@DeleteMapping
@PostMapping
@GetMapping
@PutMapping
@DeleteMapping
7.编写测试用例的注解?
@SpringBootTest
8.SpringBoot异常处理相关注解?
@ControllerAdvice
@ExceptionHandler
@ExceptionHandler
9.SpringBoot读取配置相关注解有?
@PropertySource
@Value
@Environment
@ConfigurationProperties
@Value
@Environment
@ConfigurationProperties
10.SpringBoot Starter的工作原理
我个人理解SpringBoot就是由各种Starter组合起来的,我们自己也可以开发Starter
在sprinBoot启动时由@SpringBootApplication注解会自动去maven中读取每个starter中的spring.factories文件,该文件里配置了所有需要被创建spring容器中的bean,并且进行自动配置把bean注入SpringContext中 //(SpringContext是Spring的配置文件)
在sprinBoot启动时由@SpringBootApplication注解会自动去maven中读取每个starter中的spring.factories文件,该文件里配置了所有需要被创建spring容器中的bean,并且进行自动配置把bean注入SpringContext中 //(SpringContext是Spring的配置文件)
11.Spring Boot 中如何解决跨域问题 ?
跨域可以在前端通过 JSONP 来解决,但是 JSONP 只可以发送 GET 请求,无法发送其他类型的请求,在 RESTful 风格的应用中,就显得非常鸡肋,因此我们推荐在后端通过 (CORS,Crossorigin resource sharing) 来解决跨域问题。这种解决方案并非 Spring Boot 特有的,在传统的SSM 框架中,就可以通过 CORS 来解决跨域问题,只不过之前我们是在 XML 文件中配置 CORS ,现在可以通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。
12.spring
1.谈谈你对AOP的理解
AOP:将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情。
2.谈谈你对IOC的理解
1.ioc容器
ioc容器:实际上就是个map (key, value) ,里面存的是各种对象(在xml里配置的bean节点、@repository.@service. @controller. @component)
在项目启动的时候 会读取配置文件里面的bean节点,根据全限定类名使用反射创建对象放到map里、扫描到打上上述注解的类还是通过反射创建对象放到map里。这个时候map里就有各种对象了,接下来我们在代码里需要用到里面的对象时,再通过DI注入(autowired.resource等注解,xml里bean节点内的ref属性,项目启动的时候会读取xm|节点ref属性根据id注入,也会扫描这些注解,根据类型或id注入; id就是对象名)。
2.控制反转,也就做依赖注入
没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候, 自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手.上。
引OC容器之后,对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
引OC容器之后,对象A与对象B之间失去了直接联系,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
通过前后的对比,不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是"控制反转”这个名称的由来。
全部对象的控制权全部上缴给"第三方"IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了-种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。
全部对象的控制权全部上缴给"第三方"IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了-种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。
3.什么是基于 Java 的 Spring 注解配置? 给一些注解的例子
基于 Java 的配置,允许你在少量的 Java 注解的帮助下,进行你的大部分 Spring 配置而非通过 XML 文件。
以@Configuration 注解为例,它用来标记类可以当做一个 bean 的定义,被 Spring IOC 容器使用。
是@Bean 注解,它表示此方法将要返回一个对象,作为 一个 bean 注册进 Spring 应用上下文。
4.spring中的常用注解
1.@Required 注解
这个注解表明 bean 的属性必须在配置的时候设置,通过一个 bean 定义的显式的属 性 值 或 通 过 自 动 装 配 , 若 @Required 注 解 的 bean 属 性 未 被 设 置 , 容 器 将 抛 出 BeanInitializationException。
2.@Autowired 注解
@Autowired 注解提供了更细粒度的控制,包括在何处以及如何完成自动装配。它 的用法和@Required 一样,修饰 setter 方法、构造器、属性或者具有任意名称和/ 或多个参数的 PN 方法。
3.@Controller 注解
该注解表明该类扮演控制器的角色,Spring 不需要你继承任何其他控制器基类或引 用 Servlet API。
4.@RequestMapping 注解
该注解是用来映射一个 URL 到一个类或一个特定的方处理法上。
5.spring的注解@Autowired 与java中注解@Resource的区别
@Resource的作用相当于@Autowired,只不过@Autowired按byType自动注入,而@Resource默认按 byName自动注入罢了
@Resource有两个属性是比较重要的,分是name和type,Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。
1.@Autowired与@Resource都可以用来装配bean. 都可以写在字段上,或写在setter方法上
2.@Autowired默认按类型byType装配(这个注解是属业spring的),默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用,如下:
@Autowired () @Qualifier ( "baseDao" )
private BaseDao baseDao;
@Autowired () @Qualifier ( "baseDao" )
private BaseDao baseDao;
3.@Resource(这个注解属于J2EE的),默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行安装名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。
@Resource (name= "baseDao" )
private BaseDao baseDao;
@Resource (name= "baseDao" )
private BaseDao baseDao;
3.描述一下spring bean的生命周期
解析类得到BeanDefinition--实例化得到对象---属性填充(对对象中加了@Autowired)--回调Aware方法--调用BeanPostProcessor初始前方法--调用初始化方法--调用BeanPostProcessor初始化后方法(AOP也在这里/如果创建的bean是单例模式,还是将bean扔入线程池)--使用Bean--Spring关闭时调用DisposableBean的destory()方法。
4.spring支持的几种bean的作用域
●singleton: 默认,每个容器中只有一个bean的实例, 单例的模式由BeanFactory自身来维护。该对象的生命周期是与Spring I0C容器-致的(但在第一次被注入时才 会创建)。
●prototype: 为每一 个bean请求提供一 个实例。在每次注入时都会创建一 个新的对象
●request: bean被定义为在每个HTTP请求中创建一个单例对象, 也就是说在单个请求中都会复用这一个单例对象。
●session: 与request范围类似,确保每个session中有一个bean的实例, 在session过期后,bean会随之失
效。
●application: bean被定义为在ServletContext的生命周期中复用一个单例对象。
●websocket: bean被定义为在websocket的生命周期中复用一个单例对象。
●prototype: 为每一 个bean请求提供一 个实例。在每次注入时都会创建一 个新的对象
●request: bean被定义为在每个HTTP请求中创建一个单例对象, 也就是说在单个请求中都会复用这一个单例对象。
●session: 与request范围类似,确保每个session中有一个bean的实例, 在session过期后,bean会随之失
效。
●application: bean被定义为在ServletContext的生命周期中复用一个单例对象。
●websocket: bean被定义为在websocket的生命周期中复用一个单例对象。
5.BeanFactory和ApplicationContext有什么区别?
1.BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean0), 才对该Bean进行加载实例化。这样,我们就不能发现一些存 在的Spring的配置问题。如果Bean的某-个属性没有入,BeanFacotry加载后, 直至第- -次使用调用getBean方法才 会抛出异常。
2.ApplicationContext,它是在容器启动时,-次性创建了所有的Bean.这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。ApplicationContext启动后预载 入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候不用等待,因为它们已经创建好了。
3.胡对于基本的BeanFactory, ApplicationContext 唯-的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。
BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建, 如使用ContextLoader.
BeanFactory和ApplicationContext都支持BeanPostProcessor. BeanFactoryPostProcessor的使用, 但两者之间的区别是: BeanFactory需要手 动注册,而ApplicationContext则是自动注册。
BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建, 如使用ContextLoader.
BeanFactory和ApplicationContext都支持BeanPostProcessor. BeanFactoryPostProcessor的使用, 但两者之间的区别是: BeanFactory需要手 动注册,而ApplicationContext则是自动注册。
4.ApplicationContext是BeanFactory的子接口,ApplicationContext提供了更完整的功能:
①继承MessageSource,因此支持国际化。②统-的资源文件访问方式。③提供在监听器中注册bean的事件。④同时加载多个配置文件。
①继承MessageSource,因此支持国际化。②统-的资源文件访问方式。③提供在监听器中注册bean的事件。④同时加载多个配置文件。
6.spring 自动装配 bean 有哪些方式?
1.spring 配置文件中 <bean> 节点的 autowire 参数可以控制 bean 自动装配的方式
default - 默认的方式和 "no" 方式一样
no - 不自动装配,需要使用 <ref />节点或参数
byName - 根据名称进行装配
byType - 根据类型进行装配
constructor - 根据构造函数进行装配
default - 默认的方式和 "no" 方式一样
no - 不自动装配,需要使用 <ref />节点或参数
byName - 根据名称进行装配
byType - 根据类型进行装配
constructor - 根据构造函数进行装配
2.<bean id="person" class="constxiong.interview.assemble.Person" autowire="no">
<property name="fr" ref="fr"></property>
</bean>
<property name="fr" ref="fr"></property>
</bean>
0 条评论
下一页