java技术路线图(面试)
2022-06-27 14:59:13 26 举报
AI智能生成
总结的面试技术宝典,涵盖面比较全,凭借此宝典拿下不少offer。
作者其他创作
大纲/内容
各服务通过nacos client向注册中心发送注册请求,发送的注册请求数据放在一个阻塞队列中(set集合)。客户端即刻返回注册成功
Nacos server通过一个异步任务去阻塞队列中进行消费,然后去写进注册表,最终完成注册。
1.服务注册
解答:通过内存队列+异步任务的形式
2.注册过程中如何实现高并发?
1.nacos client通过一个心跳任务来保持与注册中心的连接,也就是保活
2.ZK、netty也是通过心跳机制进行保活,但是使用的是轻量级的socket实现;Nacos底层使用的是http接口;
3.心跳机制:通过线程池,延时启动,默认5秒里面有个方法是setBeat();
服务端需要更新本次心跳的时间,也就是最后更新时间。
4.接收到心跳之后,服务端需要做什么?
Helthcheck()方法,进行健康检查任务,runnable 默认延时5秒处理
服务端拿到Instance的set集合后,遍历判断使用(当前时间-最后更新时间>设置的健康超时时间(默认15秒)),如果超过了15秒,则将状态改为 不健康。里面还有一个遍历判断:使用(当前时间-最后更新时间>设置的健康超时时间(默认30秒)),如果超过了30秒,则认为服务挂掉了,则从注册表中将该服务剔除
5.服务端如何进行感知心跳的?
3.心跳机制
如何读写数据避免数据错乱?保持读写的一致性?一种是加锁,但加锁无法实现读写的高并发。一种是写时复制COW。也就是在注册时:复制一份注册表(根据Instance,复制其中的一部分),在副本中进行写,写完进行替换。复制的内容其实是一个set集合,也就是Map结构里面的内层结构,根据Instance名称进行定位,并非复制的整个注册表。服务发现时,读取原数据,拉取到本地,然后经过frign接口去调用其他服务,当然frign调用的时候也要借助ribbon进行负载均衡,所以说ribbon实际上是做的客户端的负载均衡。实际上nacos是一个读写分离的机制。从而实现了高并发。
那么问题来了:假设两个服务同时注册、同时都复制了一份然后进行副本修改,替换的时候会产生并发覆盖问题吗?解答:不会。因为消费的时候是从阻塞队列中进行消费,是一个单线程的消费,所以不会出现并发覆盖的问题。
那么问题又来了:既然是单线程的消费,会不会导致队列任务的积压,因为单线程毕竟消费慢?解答:一、这是一个极小的概率事件,内存队列也比较的快,可以处理的来。二、就算出现了也无所谓,稍微延迟一点没有关系,eureka还延迟一分钟以上呢。除非你同时启动上千台才会出现这种情况。注:nacos的TPS为13000/S.TPS为吞吐量,吞吐量为每秒内的访问数值,QPS为峰值时的每秒钟访问数值。
4.Nacos如何处理注册表的读写高并发冲突?
1.nacos
Eureka作为SpringCloud的服务注册功能服务器,他是服务注册中心,系统中的其他服务使用Eureka的客户端将其连接到Eureka Service中,并且保持心跳,这样工作人员可以通过EurekaService来监控各个微服务是否运行正常。
1.什么是Eureka?
集群吧,注册多台Eureka,然后把SpringCloud服务互相注册,客户端从Eureka获取信息时,按照Eureka的顺序来访问。
2.Eureka怎么实现高可用?
默认情况下,如果Eureka Service在一定时间内没有接收到某个微服务的心跳,Eureka Service会进入自我保护模式,在该模式下Eureka Service会保护服务注册表中的信息,不在删除注册表中的数据,当网络故障恢复后,Eureka Servic 节点会自动退出自我保护模式
3.什么是Eureka的自我保护模式?
可以从注册中心中根据服务别名获取注册的服务器信息。
4.DiscoveryClient的作用?
2.eureka
1.各服务启动的时候去zk注册信息(创建的都是临时节点)
2.客户端获取当前在线的服务列表,并注册监听事件
3.如果服务器节点下线
4.发送服务器节点上下线事件通知
5.客户端重新再去获取服务器列表,并注册监听
1.zookeeper工作机制
1.Zookeeper: 一个领导者(Leader) ,多个跟随者(Follower) 组成的集群。
2.集群中只要有半数以上节点存活,Zookeeper集 群就能正常服务
4.更新请求顺序进行,来自同一个Clent的更新请求按其发送顺序依次执行。
5.数据更新原子性,一次数据更新要么成功,要么失败。
6.实时性,在一定时间范围内,Client 能读到最新数据。
2.zookeeper的特点
3.zookeeper的数据结构
1.统一命名服务
2.统一集群管理、统一配置管理
3.服务动态上下线
在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求
4.软负载均衡
4.zookeeper的应用场景
Zookeeper中的配置文件zoo.cfg中
Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒。
它用于心跳机制,并且设置最小的session超时时间为两倍心跳时间。(session的最小超时时间是2*tickTime)
1.tickTime =2000:通信心跳数,Zookeeper 服务器与客户端心跳时间,单位毫秒
集群中的Follower跟随者服务器与Leader领导者服务器之间初始连接时能容忍的最多心跳数(tickTime的数量),用它来限定集群中的Zookeeper服务器接到Leader的时限。
2.initLimit =10:LF 初始通信时限
集群中Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime,Leader认为Follwer死掉,从服务器列表中删除Follwer。
3.syncLimit =5:LF 同步通信时限
主要用于保存 Zookeeper 中的数据。
4.dataDir:数据文件目录+数据持久化路径
5.clientPort =2181:客户端连接端口
5.配置参数解读
如果你了解过2PC协议的话,理解起来就简单很多了,消息广播的过程实际上是一个简化版本的二阶段提交过程。通俗的理解就比较简单了,我是领导,我要向各位传达指令,不过传达之前我先问一下大家支不支持我,若有一半以上的人支持我,那我就向各位传达指令了
1.Leader将客户端的request转化成一个Proposal(提议),leader首先把proposal发送到FIFO队列里
2.FIFO取出队头proposal给Follower;Follower反馈一个ACK给队列;队列把ACK交给leader
3.leader收到半数以上ACK,就会发送commit指令给FIFO队列;FIFO队列把commit给Follower。
1.消息广播模式
leader就是一个领导,既然领导挂了,整个组织肯定不会散架,毕竟离开谁都能活下去是不是,这时候我们只需要选举一个新的领导即可,而且还要把前leader还未完成的工作做完,也就是说不仅要进行leader服务器选取,而且还要进行崩溃恢复。我们一个一个来解决
1.leader选举
既然要恢复,有些场景是不能恢复的,ZAB协议崩溃恢复要求满足如下2个要求:第一:确保已经被leader 提交的proposal必须最终被所有的follower服务器提交。第二:确保丢弃已经被leader出的但是没有被提交的proposal。
2.崩溃恢复
2.崩溃恢复模式
1.Zab协议
持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除
短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除
说明:创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护
注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序
2.节点类型
1.半数机制:集群中半数以上机器存活,集群可用。所以 Zookeeper 适合安装奇数台服务器
2.Zookeeper 虽然在配置文件中并没有指定 Master 和 Slave。但是,Zookeeper 工作时,是有一个节点为 Leader,其他则为 Follower,Leader 是通过内部的选举机制临时产生的。
场景:假设有五台服务器组成的 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 一样当小弟。
3.选举机制举例
3.选举机制(面试重点)
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]
注:常见的监听
4.监听器原理(面试重点)
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数据写成功了,这时就认为整个写操作成功。
5.写数据流程
6.zk内部原理
ls path [watch] :使用 ls 命令来查看当前 znode 中所包含的内容
ls2 path [watch]:查看当前节点数据并能看到更新次数等数据
普通创建-s 含有序列-e 临时(重启或者超时消失)
create:
get path [watch]:获得节点的值
set:设置节点的具体值
stat:查看节点状态
delete:删除节点
rmr:递归删除节点
7.zk的常用命令
3.zookeeper
5. ZooKeeper保证的是CP,Eureka保证的是AP;CAP: C:一致性>Consistency; 取舍:(强一致性、单调一致性、会话一致性、最终一致性、弱一致性) A:可用性>Availability; P:分区容错性>Partition tolerance;
1.注册中心
Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用。
使用了一个RouteLocatorBuilder的bean去创建路由,除了创建路由RouteLocatorBuilder可以让你添加各种predicates和filters,predicates断言的意思,顾名思义就是根据具体的请求的规则,由具体的route去处理,filters是各种过滤器,用来对请求做各种判断和修改。
1.gateway介绍
统一管理微服务请求,权限控制、负载均衡、路由转发、监控、安全控制黑名单和白名单等
2.网关的作用是什么?
对外暴露,权限校验,服务聚合,日志审计等
3.网关的应用场景有哪些?
网关是对所有服务的请求进行分析过滤,过滤器是对单个服务而言。
1.网关与过滤器有什么区别?
通过path配置拦截请求,通过ServiceId到配置中心获取转发的服务列表,gateway内部使用Ribbon实现本地负载均衡和转发。
2.如何实现动态gateway网关路由转发
1.客户端携带用户密码登录之后,在服务端进行账号密码的验证。
2.如果登录成功,则根据用户密码生成token,同时将token写入redis缓存,设置过期时间。
3.客户端携带token请求业务系统,经过网关服务,从redis缓存中读取并对token进行验证;
4.token验证成功,则路由到业务系统;验证失败则返回权限校验失败的错误码401;
3.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 对象。
可以按照path接口地址限流、可以根据ip来限流或者自定义限流器
配置文件
持续高速访问某个路径,速度过快时,返回 HTTP ERROR 429 。
4.gateway网关如何进行限流?
自定义全局过滤器实现IP访问限制(黑白名单)
黑名单实际可以去数据库或者redis中查询
思路:获取客户端ip,判断是否在⿊名单中,在的话就拒绝访问,不在的话就放⾏ // 从上下⽂中取出request和response对象// 从request对象中获取客户端ip// 拿着clientIp去⿊名单中查询,存在的话就决绝访问
5.gateway网关如何进行黑名单、白名单设置?
4.面试补充
2.gateway网关
简单来说: 先将集群,集群就是把一个的事情交给多个人去做,假如要做1000个产品给一个人做要10天,我叫10个人做就是一天,这就是集群,负载均衡的话就是用来控制集群,他把做的最多的人让他慢慢做休息会,把做的最少的人让他加量让他做多点。
在计算中,负载平衡可以改善跨计算机,计算机集群,网络链接,中央处理单元或磁盘驱动器等多种计算资源的工作负载分布。负载平衡旨在优化资源使用,最大化吞吐量,最小化响应时间并避免任何单一资源的过载。使用多个组件进行负载平衡而不是单个组件可能会通过冗余来提高可靠性和可用性。负载平衡通常涉及专用软件或硬件,例如多层交换机或域名系统服务器进程。
1.负载平衡的意义什么?
Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡算法Ribbon客户端组件提供一系列完善的配置项,如连接超时,重试等。简单的说,就是在配置文件中列出后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随即连接等)去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法。(有点类似Nginx)
2.Ribbon是什么?
答:Nginx是反向代理同时可以实现负载均衡,nginx拦截客户端请求采用负载均衡策略根据upstream配置进行转发,相当于请求通过nginx服务器进行转发。Ribbon是客户端负载均衡,从注册中心读取目标服务器信息,然后客户端采用轮询策略对服务直接访问,全程在客户端操作。
3.Nginx与Ribbon的区别?
答:Ribbon使用discoveryClient从注册中心读取目标服务信息,对同一接口请求进行计数,使用%取余算法获取目标服务集群索引,返回获取到的目标服务信息。
4.Ribbon底层实现原理?
答:开启客户端负载均衡
5.@LoadBalanced注解的作用?
3.Ribbon
当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应当更多的服务请求到这些资源导致更多的请求等待,发生连锁效应(雪崩效应)
断路器有三种状态:打开状态:一段时间内 达到一定的次数无法调用 并且多次监测没有恢复的迹象 断路器完全打开 那么下次请求就不会请求到该服务半开状态:短时间内 有恢复迹象 断路器会将部分请求发给该服务,正常调用时 断路器关闭关闭状态:当服务一直处于正常状态 能正常调用
1.什么是断路器?
在分布式系统,我们一定会依赖各种服务,那么这些个服务一定会出现失败的情况,就会导致雪崩,Hystrix就是这样的一个工具,防雪崩利器,它具有服务降级,服务熔断,服务隔离,监控等一些防止雪崩的技术。
服务降级:接口调用失败就调用本地的方法返回一个空
服务熔断:接口调用失败就会进入调用接口提前定义好的一个熔断的方法,返回错误信息
服务隔离:隔离服务之间相互影响
Hystrix有四种防雪崩方式:
2.什么是 Hystrix?
雪崩效应是在大型互联网项目中,当某个服务发生宕机时,调用这个服务的其他服务也会发生宕机,大型项目的微服务之间的调用是互通的,这样就会将服务的不可用逐步扩大到各个其他服务中,从而使整个项目的服务宕机崩溃.发生雪崩效应的原因有以下几点
1.单个服务的代码存在bug. 2请求访问量激增导致服务发生崩溃(如大型商城的枪红包,秒杀功能). 3.服务器的硬件故障也会导致部分服务不可用.
3.谈谈服务雪崩效应?
4.在微服务中,如何保护服务?
服务降级:当客户端请求服务器端的时候,防止客户端一直等待,不会处理业务逻辑代码,直接返回一个友好的提示给客户端
服务熔断:是在服务降级的基础上更直接的一种保护方式,当在一个统计时间范围内的请求失败数量达到设定值(requestVolumeThreshold)或当前的请求错误率达到设定的错误率阈值(errorThresholdPercentage)时开启断路,之后的请求直接走fallback方法,在设定时间(sleepWindowInMilliseconds)后尝试恢复。
服务隔离:就是Hystrix为隔离的服务开启一个独立的线程池,这样在高并发的情况下不会影响其他服务。服务隔离有线程池和信号量两种实现方式,一般使用线程池方式
5.谈谈服务降级、服务熔断、服务隔离
Hystrix实现服务降级的功能是通过重写HystrixCommand中的getFallback()方法,当Hystrix的run方法或construct执行发生错误时转而执行getFallback()方法。
6.服务降级底层是如何实现的?
4.Hystrix
Feign 是一个声明web服务客户端,这使得编写web服务客户端更容易
他将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建Http请求了,直接调用接口就行了,不过要注意,调用方法要和本地抽象方法的签名完全一致。
1.什么是Feign?
RestTemplate
Feign
2.SpringCloud有几种调用接口方式?
1.调用方式同:Ribbon需要我们自己构建Http请求,模拟Http请求然后通过RestTemplate发给其他服务,步骤相当繁琐
2.而Feign则是在Ribbon的基础上进行了一次改进,采用接口的形式,将我们需要调用的服务方法定义成抽象方法保存在本地就可以了,不需要自己构建Http请求了,直接调用接口就行了,不过要注意,调用方法要和本地抽象方法的签名完全一致。
3.Ribbon和Feign调用服务的区别?
5.Feign
Spring Cloud Config为分布式系统中的外部配置提供服务器和客户端支持,可以方便的对微服务各个环境下的配置进行集中式管理。Spring Cloud Config分为Config Server和Config Client两部分。Config Server负责读取配置文件,并且暴露Http API接口,Config Client通过调用ConfigServer的接口来读取配置文件。
1.什么是Spring Cloud Config?
Apollo、zookeeper、springcloud config、nacos
2.分布式配置中心有那些框架?
动态变更项目配置信息而不必重新部署项目。
3.
springcloud config实时刷新采用SpringCloud Bus消息总线。
4.SpringCloud Config 可以实现实时刷新吗?
6.Config
子主题
7.Sentinel
13.springcloud
1.创建SqlSessionFactory
2.通过SqlSessionFactory创建SqlSession
3.通过sqlsession执行数据库操作
4.调用session.commit()提交事务
5.调用session.close()关闭会话
1.mybatis编程步骤是什么样的
1.读取mybatis配置文件mybatis-config.xml
2.加载映射文件mapper.xml
3.构造会话工厂sqlsessionfactory
4.创建会话对象sqlsession
5.executor执行器
Map、list类型
string、integer等基本类型
POJO类型
输入
输出
6.mappedstatement对象
7.数据库
2.mybatis的mapper底层原理
3.Mybatis都有哪些Executor执行器?它们之间的区别是什么?
4.Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
5.spring二级缓存是什么?
14.mybatis
1.基于mysql
1.zk通过临时节点,解决掉了死锁的问题,⼀旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会⾃动删除掉,其他客户端⾃动获取锁。
2.zk通过节点排队监听的机制,也实现了阻塞的原理,其实就是个递归在那⽆限等待最⼩节点释放的过程。
3.实现锁的可重⼊也很好实现,可以带上线程信息就可以了,或者机器信息这样的唯⼀标识,获取的时候判断⼀下。zk的集群也是⾼可⽤的,只要半数以上的或者,就可以对外提供服务了。
总结:
2.基于zookeeper
3.基于redis
1.分布式锁
1.原⼦性(Atomicity),可以理解为⼀个事务内的所有操作要么都执⾏,要么都不执⾏。
2.⼀致性(Consistency),可以理解为数据是满⾜完整性约束的,也就是不会存在中间状态的数据,⽐如你账上有400,我账上有100,你给我打200块,此时你账上的钱应该是200,我账上的钱应该是300,不会存在我账上钱加了,你账上钱没扣的中间状态。
3.隔离性(Isolation),指的是多个事务并发执⾏的时候不会互相⼲扰,即⼀个事务内部的数据对于其他事务来说是隔离的。
4.持久性(Durability),指的是⼀个事务完成了之后数据就被永远保存下来,之后的其他操作或故障都不会对事务的结果产⽣影响。
1.事务的ACID特性
分布式事务顾名思义就是要在分布式系统中实现事务,它其实是由多个本地事务组合⽽成。对于分布式事务⽽⾔⼏乎满⾜不了 ACID,其实对于单机事务⽽⾔⼤部分情况下也没有满⾜ ACID,不然怎么会有四种隔离级别呢?所以更别说分布在不同数据库或者不同应⽤上的分布式事务了。
2.什么是分布式事务
1.CAP理论
2.BASE理论
3.分布式事务基础理论
2PC即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase),2是指两个阶段,P是指准备阶段,C是指提交阶段。
1. 准备阶段(Prepare phase):事务管理器给每个参与者发送Prepare消息,每个数据库参与者在本地执行事 务,并写本地的Undo/Redo日志,此时事务没有提交。 (Undo日志是记录修改前的数据,用于数据库回滚,Redo日志是记录修改后的数据,用于提交事务后写入数 据文件)
2. 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者 发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操 作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
1.什么是2pc
总结:整个2PC的事务流程涉及到三个角色AP、RM、TM。AP指的是使用2PC分布式事务的应用程序;RM指的是资 源管理器,它控制着分支事务;TM指的是事务管理器,它控制着整个全局事务。
1.在准备阶段RM执行实际的业务操作,但不提交事务,资源锁定;
2.在提交阶段TM会接受RM在准备阶段的执行回复,只要有任一个RM执行失败,TM会通知所有RM执行回滚操 作,否则,TM将会通知所有RM提交该事务。提交阶段结束资源锁释放
1、需要本地数据库支持XA协议。Oracle、MySQL都支持2PC协议,
2、资源锁需要等到两个阶段结束才释放,性能较差。
XA方案的问题:
1.基于数据库的XA 协议来实现2PC又称为XA方案
1.传统2PC的问题在Seata中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作 在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务0侵入的方式解决微服 务场景下面临的分布式事务问题,它目前提供AT模式(即2PC)及TCC模式的分布式事务解决方案。
1.Seata把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务 达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务
1.Transaction Coordinator (TC): 事务协调器,它是独立的中间件,需要独立部署运行,它维护全局事务的运 行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各各分支事务的提交或回滚。
2.Transaction Manager (TM): 事务管理器,TM需要嵌入应用程序中工作,它负责开启一个全局事务,并最终 向TC发起全局提交或全局回滚的指令。
3.Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器TC的指令,驱动分 支(本地)事务的提交和回滚。
2.与 传统2PC 的模型类似,Seata定义了3个组件来协议分布式事务的处理过程
2.Seata的设计思想如下:
1.架构层次方面,传统2PC方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现,而 Seata的 RM 是以jar包的形式作为中间件层部署在应用程序这一侧的。
2.两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成 才释放。而Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2持锁的时间,整体提高效率。
3.Seata实现2PC与传统2PC的差别:
2.Seata方案
2.解决方案
1.2PC
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会进行重试。
1. Try 阶段是做业务检查(一致性)及资源预留(隔离),此阶段仅是一个初步操作,它和后续的Confirm 一起才能 真正构成一个完整的业务逻辑。
2. Confirm 阶段是做确认提交,Try阶段所有分支事务执行成功后开始执行 Confirm。通常情况下,采用TCC则 认为 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需引 入重试机制或人工处理。
3.Cancel 阶段是在业务执行错误需要回滚的状态下执行分支事务的业务取消,预留资源释放。通常情况下,采 用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。
注:TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链条,用来记录事务上下文, 追踪和记录状态,由于Confirm 和cancel失败需进行重试,因此需要实现幂等,幂等性是指同一个操作无论请求多少次,其结果都相同
3.TCC分为三个阶段:
1.tcc-transaction
2.Hmily
3.ByteTCC
注:Seata也支持TCC,但Seata的TCC模式对Spring Cloud并没有提供支持。
4.TCC 解决方案
在没有调用 TCC 资源 Try 方法的情况下,调用了二阶段的 Cancel 方法,Cancel 方法需要识别出这是一个空回 滚,然后直接返回成功。
出现原因:是当一个分支事务所在服务宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶 段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。
解决思路:是关键就是要识别出这个空回滚。思路很简单就是需要知道一阶段是否执行,如果执行了,那就是正常回 滚;如果没执行,那就是空回滚。前面已经说过TM在发起全局事务时生成全局事务记录,全局事务ID贯穿整个分 布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务 ID 和分支事务 ID,第一阶段 Try 方法里会 插入一条记录,表示一阶段执行了。Cancel 接口里读取该记录,如果该记录存在,则正常回滚;如果该记录不存 在,则是空回滚
1.空回滚
为了保证TCC二阶段提交重试机制不会引发数据不一致,要求 TCC 的二阶段 Try、 Confirm 和 Cancel 接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很有可能导致数据 不一致等严重问题
解决思路在上述“分支事务记录”中增加执行状态,每次执行前都查询该状态。
2.幂等
悬挂就是对于一个分布式事务,其二阶段 Cancel 接口比 Try 接口先执行。
出现原因是在 RPC 调用分支事务try时,先注册分支事务,再执行RPC调用,如果此时 RPC 调用的网络发生拥堵, 通常 RPC 调用是有超时时间的,RPC 超时以后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC 请求 才到达参与者真正执行,而一个 Try 方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预 留的业务资源就再也没有人能够处理了,对于这种情况,我们就称为悬挂,即业务资源预留后没法继续处理。
解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,“分支事务记录”表中是否已经有二阶段事务记录,如果有则不执行Try。
3.悬挂
5.TCC需要注意三种异常处理分别是空回滚、幂等、悬挂:
如果拿TCC事务的处理流程与2PC两阶段提交做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处 理,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据操作的粒度,使 得降低锁冲突、提高吞吐量成为可能。
而不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、confirm、cancel三个操作。此 外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。
6.小结:TCC与2PC的比较
2.TCC
1.方案:可靠消息最终一致性方案是指当事务发起方执行完成本地事务后并发出一条消息,事务参与方(消息消费者)一定能 够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
2.此方案是利用消息中间件完成,如下图: 事务发起方(消息生产方)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件 之间,事务参与方(消息消费方)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事 务问题
事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实 现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最 终一致性方案的关键问题
span style=\
先进行数据库操作,再发送消息,这种情况下貌似没有问题,如果发送MQ消息失败,就会抛出异常,导致数据库事务回滚。但如果是超时异常,数 据库回滚,但MQ其实已经正常发送了,同样会导致不一致。
1.本地事务与消息发送的原子性问题
事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息。
2.事务参与方接收消息的可靠性
由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重 复消费。 要解决消息重复消费的问题就要实现事务参与方的方法幂等性
3.消息重复消费的问题
3.可靠消息最终一致性方案要解决以下几个问题:
本地消息表这个方案最初是eBay提出的,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后 通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功再将消息删除。
1、用户注册 用户服务在本地事务新增用户和增加 ”积分消息日志“。(用户表和消息表通过本地事务保证一致) 下边是伪代码 begin transaction; //1.新增用户 //2.存储积分消息日志 commit transation; 这种情况下,本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作与记录消息日志操作具备原 子性。
2、定时任务扫描日志:如何保证将消息发送给消息队列呢? 经过第一步消息已经写到消息日志表中,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息 中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试
3、消费消息 如何保证消费者一定能消费到消息呢? 这里可以使用MQ的ack(即消息确认)机制,消费者监听MQ,如果消费者接收到消息并且业务处理完成后向MQ 发送ack(即消息确认),此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则消费者会不断重 试向消费者来发送消息。 积分服务接收到”增加积分“消息,开始增加积分,积分增加成功后向消息中间件回应ack,否则消息中间件将重复 投递此消息。 由于消息会重复投递,积分服务的”增加积分“功能需要实现幂等性。
举例:
1.本地消息表方案
RocketMQ 事务消息设计则主要是为了解决 Producer 端的消息发送与本地事务执行的原子性问题,RocketMQ 的 设计中 broker 与 producer 端的双向通信能力,使得 broker 天生可以作为一个事务协调者存在;而 RocketMQ 本身提供的存储机制为事务消息提供了持久化能力;RocketMQ 的高可用机制以及可靠消息设计则为事务消息在系 统发生异常时依然能够保证达成事务的最终一致性。
在RocketMQ 4.3后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ 内部,解决 Producer 端的消息发送与本地事务执行的原子性问题。
2.RocketMQ事务消息方案
4.解决方案:
3.可靠消息最终一致性
4.最大努力通知
4.分布式事务解决方案
2PC 和 3PC 是⼀种强⼀致性事务,不过还是有数据不⼀致,阻塞等⻛险,⽽且只能⽤在数据库层⾯。
TCC 是⼀种补偿性事务思想,适⽤的范围更⼴,在业务层⾯实现,因此对业务的侵⼊性较⼤,每⼀个操作都需要实现对应的三个⽅法。
本地消息、事务消息和最⼤努⼒通知其实都是最终⼀致性事务,因此适⽤于⼀些对时间不敏感的业务。
在条件允许的情况下,我们尽可能选择本地事务单数据源,因为它减少了网络交互带来的性能损耗,且避免了数据 弱一致性带来的种种问题。若某系统频繁且不合理的使用分布式事务,应首先从整体设计角度观察服务的拆分是否 合理,是否高内聚低耦合?是否粒度太小?分布式事务一直是业界难题,因为网络的不确定性,而且我们习惯于拿 分布式事务与单机事务ACID做对比。 无论是数据库层的XA、还是应用层TCC、可靠消息、最大努力通知等方案,都没有完美解决分布式事务问题,它们 不过是各自在性能、一致性、可用性等方面做取舍,寻求某些场景偏好下的权衡。
11.总结
2.分布式事务
15.分布式
1.注意:9300 端口为 Elasticsearch 集群间组件的通信端口,9200 端口为浏览器访问的 http协议 RESTful 端口;打开浏览器(推荐使用谷歌浏览器),输入地址:http://localhost:9200,测试结果
4.用 JSON 作为文档序列化的格式,比如一条用户信息:{ \"name\" : \"John\
5.通过 elasticsearch-head 插件查看集群情况
1.基础知识
1.对比关系型数据库,创建索引就等同于创建数据库
2.在 Postman 中,向 ES 服务器发 PUT 请求 :http://127.0.0.1:9200/shopping。shopping为索引名称
3.注意:创建索引库的分片数默认 1 片,在 7.0.0 之前的 Elasticsearch 版本中,默认 5 片如果重复添加索引,会返回错误信息
1.创建索引
1.在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/_cat/indices?v
2.这里请求路径中的_cat 表示查看的意思,indices 表示索引,所以整体含义就是查看当前 ES服务器中的所有索引,就好像 MySQL 中的 show tables 的感觉
2.查看所有索引
1.在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/shopping、shopping为索引名称
2.查看索引向 ES 服务器发送的请求路径和创建索引是一致的。但是 HTTP 方法不一致。创建为put,查看为get
3.查看单个索引
1.在 Postman 中,向 ES 服务器发 DELETE 请求 :http://127.0.0.1:9200/shopping
4.删除索引
2.索引操作
1.这里的文档可以类比为关系型数据库中的表数据,添加的数据格式为 JSON 格式
2.在 Postman 中,向 ES 服务器发 POST 请求 :http://127.0.0.1:9200/shopping/_doc{\"title\":\"小米手机\
3.上面的数据创建后,由于没有指定数据唯一性标识(ID),默认情况下,ES 服务器会随机生成一个。如果想要自定义唯一性标识,需要在创建时指定:http://127.0.0.1:9200/shopping/_doc/1
1.创建文档
1.查看文档时,需要指明文档的唯一性标识,类似于 MySQL 中数据的主键查询在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/shopping/_doc/1
2.查看文档
1.和新增文档一样,输入相同的 URL 地址请求,如果请求体变化,会将原有的数据内容覆盖在 Postman 中,向 ES 服务器发 POST 请求 :http://127.0.0.1:9200/shopping/_doc/1
3.修改文档
1.修改数据时,也可以只修改某一给条数据的局部信息
2.在 Postman 中,向 ES 服务器发 POST 请求 :http://127.0.0.1:9200/shopping/_update/1{ \"doc\": { \"price\":3000.00 } }
4.修改字段
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 } } }
5.删除文档
3.文档操作
1.有了索引库,等于有了数据库中的 database。接下来就需要建索引库(index)中的映射了,类似于数据库(database)中的表结构(table)。创建数据库表需要设置字段名称,类型,长度,约束等;索引库也一样,需要知道这个类型下有哪些字段,每个字段有哪些约束信息,这就叫做映射(mapping)
在 Postman 中,向 ES 服务器发 PUT 请求 :http://127.0.0.1:9200/student/_mapping{ \"properties\": { \"name\":{ \"type\": \"text\
2.创建映射
1.字段名:任意填写,下面指定许多属性,例如:title、subtitle、images、price
String 类型,又分两种:text:可分词、keyword:不可分词,数据会作为完整字段进行匹配
Numerical:数值类型,分两类基本数据类型:long、integer、short、byte、double、float、half_float浮点数的高精度类型:scaled_float
Date:日期类型
Array:数组类型
Object:对象
2.type:类型,Elasticsearch 中支持的数据类型非常丰富,说几个关键的:
3.index:是否索引,默认为 true,也就是说你不进行任何配置,所有字段都会被索引。true:字段会被索引,则可以用来进行搜索false:字段不会被索引,不能用来搜索
4.store:是否将数据进行独立存储,默认为 false。原始的文本会存储在_source 里面,默认情况下其他提取出来的字段都不是独立存储的,是从_source 里面提取出来的。当然你也可以独立的存储某个字段,只要设置\"store\": true 即可,获取独立存储的字段要比从_source 中解析快得多,但是也会占用更多的空间,所以要根据实际业务需求来设置
5.analyzer:分词器,这里的 ik_max_word 即使用 ik 分词器
3.映射数据说明:
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_mapping
4.查看映射
4.映射操作mapping
在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search{ \"query\": { \"match_all\": {} } }# \"query\":这里的 query 代表一个查询对象,里面可以有不同的查询属性# \"match_all\":查询类型,例如:match_all(代表查询所有), match,term , range 等等# {查询条件}:查询条件会根据类型的不同,写法也有差异
1.查询所有文档
match 匹配类型查询,会把查询条件进行分词,然后进行查询,多个词条之间是 or 的关系在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search{ \"query\": { \"match\": { \"name\":\"zhangsan\" } } }
2.匹配查询
multi_match 与 match 类似,不同的是它可以在多个字段中查询。在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search{ \"query\": { \"multi_match\": { \"query\": \"zhangsan\
3.字段匹配查询
term 查询,精确的关键词匹配查询,不对查询条件进行分词。在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search{ \"query\": { \"term\": { \"name\": { \"value\": \"zhangsan\" } } } }
4.关键字精确查询
terms 查询和 term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定值中的任何一个值,那么这个文档满足条件,类似于 mysql 的 in在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search{ \"query\": { \"terms\": { \"name\": [\"zhangsan\
5.多关键字精确查询
默认情况下,Elasticsearch 在搜索的结果中,会把文档中保存在_source 的所有字段都返回。如果我们只想获取其中的部分字段,我们可以添加_source 的过滤在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search{ \"_source\": [\"name\
6.指定字段查询
includes:来指定想要显示的字段excludes:来指定不想要显示的字段在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search{ \"_source\": { \"includes\": [\"name\
7.过滤字段
`bool`把各种其它查询通过`must`(必须 )、`must_not`(必须不)、`should`(应该)的方式进行组合在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search{ \"query\": { \"bool\": { \"must\": [ { \"match\": { \"name\": \"zhangsan\
8.组合查询
range 查询找出那些落在指定区间内的数字或者时间。
9.范围查询
返回包含与搜索字词相似的字词的文档。编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括: 更改字符(box → fox) 删除字符(black → lack) 插入字符(sic → sick) 转置两个相邻字符(act → cat)为了找到相似的术语,fuzzy 查询会在指定的编辑距离内创建一组搜索词的所有可能的变体或扩展。然后查询返回每个扩展的完全匹配。通过 fuzziness 修改编辑距离。一般使用默认值 AUTO,根据术语的长度生成编辑距离。
10.模糊查询
sort 可以让我们按照不同的字段进行排序,并且通过 order 指定排序的方式。desc 降序,asc升序。在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search{ \"query\": { \"match\": { \"name\":\"zhangsan\
11.单字段排序
假定我们想要结合使用 age 和 _score 进行查询,并且匹配的结果首先按照年龄排序,然后按照相关性得分排序在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search{ \"query\": { \"match_all\
12.多字段排序
在进行关键字搜索时,搜索出的内容中的关键字会显示不同的颜色,称之为高亮。
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\
13.高亮查询
from:当前页的起始索引,默认从 0 开始。 from = (pageNum - 1) * sizesize:每页显示多少条在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search{ \"query\": { \"match_all\
14.分页查询
聚合允许使用者对 es 文档进行统计分析,类似与关系型数据库中的 group by,当然还有很多其他的聚合,例如取最大值、平均值等等。 对某个字段取最大值 max在 Postman 中,向 ES 服务器发 GET 请求 :http://127.0.0.1:9200/student/_search
15.聚合查询
5.高级查询
1.配置服务器集群时,集群中节点数量没有限制,大于等于 2 个节点就可以看做是集群了。一般出于高性能及高可用方面来考虑集群中节点数量都是 3 个以上。
2.除了负载能力,单点服务器也存在其他问题:单台机器存储容量有限、单服务器容易出现单点故障,无法实现高可用、单服务的并发处理能力有限
1.单机&集群
一个集群就是由一个或多个服务器节点组织在一起,共同持有整个的数据,并一起提供索引和搜索功能。一个 Elasticsearch 集群有一个唯一的名字标识,这个名字默认就是”elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。
2.集群cluster
集群中包含很多服务器,一个节点就是其中的一个服务器。作为集群的一部分,它存储数据,参与集群的索引和搜索功能
在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何 Elasticsearch 节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”的集群。
3.节点node
6.es集群
系统中的数据,随着业务的发展,时间的推移,将会非常多,而业务中往往采用模糊查询进行数据的搜索,而模糊查询会导致查询引擎放弃索引,导致系统查询数据时都是全表扫描,在百万级别的数据库中,查询效率是非常低下的,而我们使用 ES 做一个全文索引,将经常查询的系统功能的某些字段,比如说电商系统的商品表中商品名,描述、价格还有 id 这些字段我们放入 ES 索引库里,可以提高查询速度
1.为什么要使用 Elasticsearch?
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功能。
2.Elasticsearch 的 master 选举流程?
1.网络问题:集群间的网络延迟导致一些节点访问不到 master,认为 master 挂掉了从而选举出新的master,并对 master 上的分片和副本标红,分配新的主分片
2.节点负载:主节点的角色既为 master 又为 data,访问量较大时可能会导致 ES 停止响应造成大面积延迟,此时其他节点得不到主节点的响应认为主节点挂掉了,会重新选取主节点。
3.内存回收:data 节点上的 ES 进程占用的内存较大,引发 JVM 的大规模内存回收,造成 ES 进程失去响应。
1.脑裂”问题可能的成因:
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
2.脑裂问题解决方案:
3.Elasticsearch 集群脑裂问题?
倒排索引是搜索引擎的核心。搜索引擎的主要目标是在查找发生搜索条件的文档时提供快速搜索。ES中的倒排索引其实就是 lucene 的倒排索引,区别于传统的正向索引,倒排索引会再存储数据时将关键词和数据进行关联,保存到倒排表中,然后查询时,将查询内容进行分词后在倒排表中进行查询,最后匹配数据即可。
4.Elasticsearch 中的倒排索引是什么?
集群是一个或多个节点(服务器)的集合,它们共同保存您的整个数据,并提供跨所有节点的联合索引和搜索功能。群集由唯一名称标识,默认情况下为“elasticsearch”。此名称很重要,因为如果节点设置为按名称加入群集,则该节点只能是群集的一部分。
节点是属于集群一部分的单个服务器。它存储数据并参与群集索引和搜索功能。
索引就像关系数据库中的“数据库”。它有一个定义多种类型的映射。索引是逻辑名称空间,映射到一个或多个主分片,并且可以有零个或多个副本分片。 MySQL =>数据库 Elasticsearch =>索引
文档类似于关系数据库中的一行。不同之处在于索引中的每个文档可以具有不同的结构(字段),但是对于通用字段应该具有相同的数据类型。 MySQL => Databases => Tables => Columns / Rows Elasticsearch => Indices => Types =>具有属性的文档
类型是索引的逻辑类别/分区,其语义完全取决于用户。
5.Elasticsearch 中的集群、节点、索引、文档、类型是什么?
7.es面试题
16.elasticsearch
1.调度中心:负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。
2.执行模块(执行器):负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效;接收“调度中心”的执行请求、终止请求和日志请求等。
注:生产中使用xxl-job2.2.0版本。使用注解:@XxlJob(\"userStatisticsTiming\")
1.框架系统组成
调度密集或者耗时任务可能会导致任务阻塞,集群情况下调度组件小概率情况下会重复触发;针对上述情况,可以通过结合 “单机路由策略(如:第一台、一致性哈希)” + “阻塞策略(如:单机串行、丢弃后续调度)” 来规避,最终避免任务重复执行。
1.如何避免任务重复执行
2.分片任务:一个任务在多台服务器上同时都执行,降低任务处理时间,调度器会调用配置的所有机器
3.执行器用的端口和该执行器本身的端口没有关系,在启动时可以指定执行器端口,xxljob会自己启一个服务用于调度,如果不指定该端口,默认为9999
2.问题总结
4.1 老版本自有bug,句柄数过多导致任务调度失败,修改源码修复老版本GULE(shell)模式,调用远程接口时,打开连接,没有关闭资源,随着任务的执行,未关闭的句柄数越来越多,最高为65535,达到后就无法继续调度任务,重启可以解决,但是每隔一段时间就会出现该问题。4.2 任务重复执行,有可能的原因,1.tigger重复调度(老版本使用quartz,quartz本身bug,单台机器也可能出现重复调度)。新版本摒弃quartz,自己解析cron表达式,计算下次执行时间,单机不会重复调度;集群使用行锁避免重复调度(需要mysql的引擎为InnoDB,myisam不支持行锁)2.glue模式,curl调用定时任务接口,会走域名,走ng,ng默认配置请求服务器发生错误或者超时,会尝试调用别的机器,导致重复调用;解决方式,使用BEAN模式配置定时任务
3.遇到的问题
5.1 自己解析cron表达式,自己计算下次调度时间5.2 调度策略:触发任务(tigger)使用线程池,维护了两个线程池(快、慢),正常调度都会走快的线程池(默认 core:10 max:200 queue:1000 丢弃策略:默认,抛出异常),当有任务调度时间超过500ms,并且出现10次,则会将该任务放入慢的线程池,目的是为了不影响其他任务调度
4.新版本特性
1.丰富告警通道2.加入prometheus埋点,记录不同时刻所有的调度任务数3.调度线程池满,默认会抛出异常丢弃任务,我们捕获异常,发送告警4.勾子机制,发布重启时先让线程池中的任务执行完,再关闭服务7.注册和销毁没有使用zk,而是使用了DB,每30s将注册信息写入到DB,admin每30s读取数据库,获取当前可用的执行器;执行器关闭时会调用destory方法,更新数据库,删除对应的记录
5.我们做的改造
为了避免多个服务器同时调度任务, 通过mysql悲观锁实现分布式锁(for update语句)
1 setAutoCommit(false)关闭隐式自动提交事务,2 启动事务select lock for update(排他锁)3 读db任务信息 -> 拉任务到内存时间轮 -> 更新db任务信息4 commit提交事务,同时会释放for update的排他锁(悲观锁)任务处理完毕后,释放悲观锁,准备等待下一次循环。
6.一致性保证
7.如何触发
17.xxljob
(1) docker从基础镜像运行一个容器(2)执行一条指令并对容器作出修改(3)执行类似docker commit的操作提交一个 新的镜像层(4) docker再基 于刚提交的镜像运行个新容器(5)执行dockerfile中的下一 条指令 直到所有指令都执行完成
1.Dockerfile的构建过程
从应用软件的角度来看,Dockerfile、Docker镜像与Docker容器分别代表软件的三个不同阶段,* Dockerfile是软件的原材料* Docker镜像是软件的交付品* Docker容器则可以认为是软件的运行态。Dockerfile面向开发,Docker镜像成为交付标准,Docker容器则涉及部署与运维,三者缺一不可,合力充当Docker体系的基石。
1.Dockerfile,需要定义一个Dockerfile,Dockerfile定义了进程需要的一切东西。Dockerfile涉及的内容包括执行代码或者是文件、环境变量、依赖包、运行时环境、动态链接库、操作系统的发行版、服务进程和内核进程(当应用进程需要和系统服务和内核进程打交道,这时需要考虑如何设计namespace的权限控制)等等;
2.Docker镜像,在用Dockerfile定义一个文件之后,docker build时会产生一个Docker镜像,当运行 Docker镜像时,会真正开始提供服务;
3.Docker容器,容器是直接提供服务的。
2.docker组成
18.docker
1.问题描述:用户快速点了两次 “提交订单” 按钮,浏览器会向后端发送两条创建订单的请求,最终会创建两条一模一样的订单
解决方案就是采用幂等机制,多次请求和一次请求产生的效果是一样的
1.解决方案就是采用幂等机制,多次请求和一次请求产生的效果是一样的
2.前端通过js脚本控制,无法解决用户刷新提交的请求。另外也无法解决恶意提交。不建议采用该方案,如果想用,也只是作为一个补充方案。
3.前后约定附加参数校验。当用户点击购买按钮时,渲染下单页面,展示商品、收货地址、运费、价格等信息,同时页面会埋上Token 信息,用户提交订单时,后端业务逻辑会校验token,有且匹配才认为是合理请求。注意:同一个 Token 只能用一次,用完后立马失效掉。
2.解决方案:
1.如何避免重复下单
商品信息是可以修改的,当用户下单后,为了更好解决后面可能存在的买卖纠纷,创建订单时会同步保存一份商品详情信息,称之为订单快照。
同一件商品,会有很多用户会购买,如果热销商品,短时间就会有上万的订单。如果每个订单都创建一份快照,存储成本太高。另外商品信息虽然支持修改,但毕竟是一个低频动作。我们可以理解成,大部分订单的商品快照信息都是一样的,除非下单时用户修改过。
如何实时识别修改动作是解决快照成本的关键所在。我们采用摘要比对的方法。创建订单时,先检查商品信息摘要是否已经存在,如果不存在,会创建快照记录。订单明细会关联商品的快照主键
由于订单快照属于非核心操作,即使失败也不应该影响用户正常购买流程,所以通常采用异步流程执行
2.订单快照,减少存储成本
购物车是电商系统的标配功能,暂存用户想要购买的商品。分为添加商品、列表查看、结算下单三个动作。
技术设计并不是特别复杂,存储的信息也相对有限(用户id、商品id、sku_id、数量、添加时间)。这里特别拿出来单讲主要是用户体验层面要注意几个问题:添加购物车时,后端校验用户未登录,常规思路,引导用户跳转登录页,待登录成功后,再添加购物车。多了一步操作,给用户一种强迫的感觉,体验会比较差。有没有更好的方式?如果细心体验京东、淘宝等大平台,你会发现即使未登录态也可以添加购物车,这到底是怎么实现的?
其实原理并不复杂,服务端这边在用户登录态校验时,做了分支路由,当用户未登录时,会创建一个临时Token,作为用户的唯一标识,购物车数据挂载在该Token下,为了避免购物车数据相互影响以及设计的复杂度,这里会有一个临时购物车表。当然,临时购物车表的数据量并不会太大,why?用户不会一直闲着添加购物车玩,当用户登录后,查看自己的购物车,服务端会从请求的cookie里查找购物车Token标识,并查询临时购物车表是否有数据,然后合并到正式购物车表里。
特别说明:临时购物车是不是一定要在服务端存储?未必。有架构师倾向前置存储,将数据存储在浏览器或者APP LocalStorage,这部分数据毕竟不是共享的,但是不太好的增加了设计的复杂度。1.客户端需要借助本地数据索引,远程请求查完整信息2.如果是登录态,还要增加数据合并逻辑考虑到这两部分数据只是用户标识的差异性,所以作者还是建议统一存到服务端,日后即使业务逻辑变更,只需要改一处就可以了,毕竟自运营系统,良好的可维护性也需要我们非常关注的。
3.购物车,混合存储,未登录时临时购物车
1.下单减库存:即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,下单时直接通过数据库的事务机制控制商品库存,这样一定不会出现超卖的情况。但是你要知道,有些人下完单可能并不会付款。
2.付款减库存:即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
3.付款减库存:即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
注:至于采用哪一种减库存方式更多是业务层面的考虑,减库存最核心的是大并发请求时保证数据库中的库存字段值不能为负数。
1.常见的库存扣减方式
1.至于采用哪一种减库存方式更多是业务层面的考虑,减库存最核心的是大并发请求时保证数据库中的库存字段值不能为负数。update ... set amount = amount - 1 where id = $id and amount - 1 >=0
2.设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时 SQL 语句会报错
3.分布式锁
4.库存超卖
举个例子:商家发货,填写运单号,开始填了 123,后来发现填错了,然后又修改为 456。此时,如果就为某种特殊场景埋下错误伏笔,具体我们来看下:
过程:1.开始「请求A」发货,调订单服务接口,更新运单号 1232.但是响应有点慢,超时了3.此时,商家发现运单号填错了,发起了「请求B」,更新运单号为 456 ,订单服务也响应成功了4.这时,「请求A」触发了重试,再次调用订单服务,更新运单号 123,订单服务也响应成功了5.订单服务最后保存的 运单号 是 123
很多人可能会说,不重试不就可以了,要知道重试机制 是高可用服务的重要保障手段,很多重试是框架自动发起的。
数据库表引入一个额外字段 version,每次更新时,判断表中的版本号与请求参数携带的版本号是否一致;一致:才触发更新.不一致:说明这期间执行过数据更新,可能会引发错误,拒绝执行。
解决方案:
5.商家卖货,物流单更新ABA问题
用户支付,我们要从买家账户减掉一定金额,再往卖家增加一定金额,为了保证数据的完整性、可追溯性,变更余额时,我们通常会同时插入一条记录流水。账户流水核心字段:流水ID、金额、交易双方账户、交易时间戳、订单号、注意:账户流水只能新增,不能修改和删除。流水号必须是自增的。后续,系统对账时,我们只需要对交易流水明细数据做累计即可,如果出现和余额不一致情况,一般以交易流水为准来修复余额数据。
更新余额、记录流水 虽属于两个操作,但是要保证要么都成功,要么都失败。要做到事务。常用的隔离级别是 RC 和 RR ,因为这两种隔离级别都可以避免脏读。
当然,如果涉及多个微服务调用,会用到分布式事务分布式事务,细想下也很容易理解,就是将一个大事务拆分为多个本地事务,本地事务依然借助于数据库自身事务来解决,难点在于解决这个分布式一致性问题,借助重试机制,保证最终一致是我们常用的方案。
6.账户余额更新,保证事务
互联网业务大部分都是 读多写少,为了提升数据库集群的吞吐性能,我们通常会采用 主从架构、读写分离
部署一个主库实例,客户端请求所有写操作全部写到主库,然后借助 MySQL 自带的 主从同步 功能,做一些简单配置,可以近乎实时的将主库的数据同步给 多个从库实例,主从延迟非常小,一般不超过 1 毫秒。客户端请求的所有读操作全部打到 从库,借助多实例集群提升读请求的整体处理能力。
这个方案看似天衣无缝,但实际有个 副作用主从同步虽然近乎实时,但还是有个 时间差 ,主库数据刚更新完,但数据还没来得及同步到从库,后续读请求直接访问了从库,看到的还是旧数据,影响用户体验。任何事情都不是完美的,从主同步也是一样,没有完美的解决方案,我们要找到其中的平衡取舍点。
以淘宝为例:从产品策划角度解决问题。我们在支付成功后,并没有立即跳到 订单详情页,而是增加了一个 无关紧要的 中间页(支付成功页),一是告诉你支付的结果是成功的,钱没丢,不要担心;另外也可以增加一些推荐商品,引流提升网站的GMV。最重要的,增加了一个缓冲期,为 订单的主从库数据同步 争取了更多的时间。可谓一举多得,其他互联网业务也是类似道理。
7.mysql读写分离带来的数据不一致问题
根据二八定律,系统绝大部分的性能开销花在20%的业务。数据也不例外,从数据的使用频率来看,经常被业务访问的数据称为热点数据;反之,称之为冷数据。在了解的数据的冷、热特性后,便可以指导我们做一些有针对性的性能优化。这里面有业务层面的优化,也有技术层面的优化。比如:电商网站,一般只能查询3个月内的订单,如果你想看看3个月前的订单,需要访问历史订单页面。
方案一:以“下单时间”为标准,将3 个月前的订单数据当作冷数据,3 个月内的当作热数据。
方案二:根据“订单状态”字段来区分,已完结的订单当作冷数据,未完结的订单当作热数据。
方案三:组合方式,把下单时间 > 3 个月且状态为“已完结”的订单标识为冷数据,其他的当作热数据。
1.冷热数据区分的标准是什么?以电商订单为例:
方案一:直接修改业务代码,每次业务请求触发冷热数据判断,根据结果路由到对应的冷数据表或热数据表。缺点:如果判断标准是 时间维度,数据过期了无法主动感知。
方案二:如果觉得修改业务代码,耦合性高,不易于后期维护。可以通过监听数据库变更日志 binlog 方式来触发
方案三:常用的手段是跑定时任务,一般是选择凌晨系统压力小的时候,通过跑批任务,将满足条件的冷数据迁移到其他存储介质。在途业务表中只留下来少量的热点数据。
2.如何触发冷热数据的分离
判断数据是冷、还是热
将冷数据插入冷数据表中
然后,从原来的热库中删除迁移的数据
3.如何实现冷热数据分离,过程大概分为三步
方案一:界面设计时会有选项区分,如上面举例的电商订单
方案二:直接在业务代码里区分。
4.如何使用冷热数据
实现思路:
8.历史订单,归档
如果电商网站的订单数过多,我们一般会想到 分库分表 解决策略。没问题,这个方向是对的。
1、买家,查询 我的订单 列表,需要根据 buyer_id 来查询
2、查看订单详情,需要根据 order_id 来查询
3、卖家,查询 我的销售 列表,需要根据 seller_id 来查询
但是查询维度很多
一个订单号 19 位,我们会发现同一个用户不同订单的最后 6 位都是一样的,没错,那是用户id的后6位。
这样,上文中 场景1、场景2 的查询可以共性抽取, 采用 buyer_id 或 order_id 的 后六位 作为分表键,对 1 000 000 取模,得到买家维度的订单分表的编号。
至于 场景3 卖家维度的订单查询,我们可以采用数据异构方式,按 seller_id 维度另外存储一份数据,专门供卖家使用。
而订单分表只有一个分表键,如何满足多维度 SQL 操作呢?
9.订单分库分表,多维度查询
10.秒杀场景
1.电商项目技术架构常见问题场景
2.如何从0搭建公司的后端技术栈
1.通过慢查日志等定位那些执行效率较低的SQL语句
需要重点关注type、rows、filtered、extra
2.explain 分析SQL的执行计划
了解SQL执行的线程的状态及消耗的时间。
默认是关闭的,开启语句“set profiling = 1;”SHOW PROFILES ;SHOW PROFILE FOR QUERY #{id};
3.show profile 分析
trace分析优化器如何选择执行计划,通过trace文件能够进一步了解为什么优惠券选择A执行计划而不选择B执行计划。
set optimizer_trace=\"enabled=on\";set optimizer_trace_max_mem_size=1000000;select * from information_schema.optimizer_trace;
4.trace
1.优化索引;2.优化SQL语句:修改SQL、IN 查询分段、时间查询分段、基于上一次数据过滤;3.改用其他实现方式:ES、数仓等;4.数据碎片处理。
5.确定问题并采用相应的措施
1.sql优化步骤
sql语句:select * from _t where orderno=''
1.场景:
2.分析:
1.最左匹配
索引:KEY `idx_mobile` (`mobile`)
sql语句:select * from _user where mobile=12345678901
隐式转换相当于在索引上做运算,会让索引失效。mobile是字符类型,使用了数字,应该使用字符串匹配,否则MySQL会用到隐式替换,导致索引失效。
2.隐式转换
对于大分页的场景,可以优先让产品优化需求,如果没有优化的,有如下两种优化方式:
1.一种是把上一次的最后一条数据,也即上面的c传过来,然后做“c < xxx”处理,但是这种一般需要改接口协议,并不一定可行;‘
3.大分页
in查询在MySQL底层是通过n*m的方式去搜索,类似union,但是效率比union高。
in查询在进行cost代价计算时(代价 = 元组数 * IO平均值),是通过将in包含的数值,一条条去查询获取元组数的,因此这个计算过程会比较的慢,所以MySQL设置了个临界值(eq_range_index_dive_limit),5.6之后超过这个临界值后该列的cost就不参与计算了。因此会导致执行计划选择不准确。默认是200,即in条件超过了200个数据,会导致in的代价计算存在问题,可能会导致Mysql选择的索引不准确。
4.in + order by
sql语句:select * from _order where shop_id = 1 and created_at > '2021-01-01 00:00:00' and order_status = 10
范围查询还有“IN、between”。
5.范围查询阻断,后续字段不能走索引
在索引上,避免使用NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等。
6.不等于、不包含不能用到索引的快速搜索
如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的蛮大一部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据
select * from _order where order_status = 1
查询出所有未支付的订单,一般这种订单是很少的,即使建了索引,也没法使用索引
7.优化器选择不使用索引的情况
如果是统计某些数据,可能改用数仓进行解决;
如果是业务上就有那么复杂的查询,可能就不建议继续走SQL了,而是采用其他的方式进行解决,比如使用ES等进行解决
8.复杂查询
desc 和asc混用时会导致索引失效
9.asc和desc混用
对于推送业务的数据存储,可能数据量会很大,如果在方案的选择上,最终选择存储在MySQL上,并且做7天等有效期的保存
10.大数据
2.场景分析
3.SQL调优的5大步骤+10个案例
1.秒杀前,页面访问压力大(解决方案:页面静态化,cdn+redis+ngnix多级缓存)
2.秒杀时,下单过于集中,作弊软件刷单(解决方案:前端页面增加校验或者答题环节)
3.秒杀时,下单请求系统冲击力大,影响其他正常功能(解决方案:为秒杀单独出一套系统订单系统)
4.秒杀时,要快速精准的扣库存(解决方案:基于缓存如redis实现快速精准的扣减库存)
5.秒杀后,快速过滤掉未抢到的下单请求(解决方案:库存扣减完后,快速的通知ngnix,过滤下单请求)
6.秒杀后,下单模块压力大(解决方案:下单请求写入mq,后端下单模块慢慢下单)
1.要如何选择mq产品
2.如何快速处理未支付的订单
3.如何保证下单操作与消息发送的事务一致性
4.如何保证集群的高吞吐和高可用
5.如何保证mq消息的高吞吐和高可用
6.如何保证高性能文件的读写
7.问题放大镜
1.如何设计一个秒杀系统
2.CPU-100%如何进行解决
3.设计消息发送系统
4.如何设计一个高效的异常处理架构
5.在工作中遇到的印象深刻的难点?怎么解决的?
4.场景设计
19.项目场景梳理
面试官想听到的是:你能随着我们公司的发展一起成长!
其一,表达对职业的认可;其二,表达对应聘公司及所属行业的认可;其三,表达长远的职业规划
1.你有什么职业规划?
能被接受的离职原因有两个:一是求发展,二是不可抗力。
离开上家公司是一个特别痛苦的选择,我和领导、同事都相处得很好,通过自己的努力也赢得了大家的信任,但公司规模太小发展空间不大,我希望找一份更有挑战的工作,没办法只好做出这个选择
应聘更大的公司,你可以说希望找一个大一点的平台历练自己;应聘一家小公司,你可以说希望有更自由的空间施展在自己的手脚。
你所追求的,恰好是新公司可以提供的。每天路上三个小时,起得比鸡早,睡得比狗晚,不想把自己的人生奉献给公交车;之前和男朋友异地恋,现在选择了夫唱妇随,来到了他的城市;公司破产,老板跑路,心有凌云志,奈何平台散……诸如此类的不可抗力,不会加分也不会减分。
2.为什么从上家公司离职?
参考的回答套路:一,我很了解你们二,我很认同你们三,我熟悉应聘的职位并具备胜任的能力四,我能给公司创造价值及实现自我价值
3.你为什么选择我们公司?
其一,不要说没有什么兴趣爱好;其二,不要说容易让人产生负面联想的兴趣爱好;你的爱好最好对求职形成加持,你说喜欢长跑,潜台词是身体好;你说喜欢看书,说明你爱学习;应聘技术类职位,说爱下象棋或围棋,说明你爱动脑子、善于分析、逻辑性强。
4.你有什么兴趣爱好?
开玩笑说:缺钱
最好你回答的确实是缺点,但对未来的工作影响不大,并给出了自己的改进方法,再顺手带出来一个优点。
“我的缺点是不会拒绝别人,同事找我帮忙都一概揽下,结果有时影响了自身的工作进度。”不懂拒绝是缺点,但也带出来了一个优点:热心肠。“我反思了这个问题,应该安排好工作的优先顺序,向求助同事展示手头的工作,并给出自己何时可以帮忙的预计时间,让他自行决定是否求助,这样既不影响同事关系,又不影响自己的工作。”
5.你认为自己最大的缺点是什么?
回答的基本套路是:通过失败说成功。不要强调结果,更多叙述过程,重点落在收获和成长。案件重演时,仔细描述遇到的困难,处理的方法,客观的原因适当提及,承认自己的不足和失误,如果重来一次你会如何解决非常关键,让面试官感觉你勇于担当、勤于思考、乐于改正。
6.说说你印象最深的一次失败经历?
正确的回答是:“我不会在开会时和领导吵起来”,服从意识是基本的职业素养。
无论如何不该在开会时和领导吵起来,你不想干了?实在觉得自己是对的,可以会后去敲领导办公室的门,“王总,我还是不太明白为什么我那招不成,我想和您讨教一下……”跟领导打交道不能挑战他的权威。
7.你开会时和领导吵起来了,你会怎么处理?
其一,不要问很容易找到答案的问题
其二,不要只问薪酬福利
可以问问与工作相关的问题,如关于岗位职责和部门情况的问题:“公司对这个职位的具体期望是?”“部门有多少位同事?”
面试结束了,面试官没说何时给消息,建议你多问一句:“大概多久能有回复?
8.你还有什么要问我的吗?
20.非技术套路问题
21.设计模式
22.大数据部分
1.很自然地,最简单的方式是对所有的QQ号码进行排序,重复的QQ号码必然相邻,保留第一个,去掉后面重复的就行。
2.可是,面试官要问你,去重一定要排序吗?显然,排序的时间复杂度太高了,无法通过腾讯面试。
方法一:排序
1.既然直接排序的时间复杂度太高,那就用hashmap吧,具体思路是把QQ号码记录到hashmap中,由于hashmap的去重性质,自动只保留一个重复的
2.可是,面试官又要问你了:实际要存40亿QQ号码,1G的内存够分配这么多空间吗?显然不行,无法通过腾讯面试。
方法二:hashmap
1.显然,这是海量数据问题,自然想到文件切割的方式,避免内存过大。可是,绞尽脑汁思考,要么使用文件间的归并排序,要么使用桶排序,反正最终是能排序的。既然排序好了,那就能实现去重了,貌似就万事大吉了。
2.接着,面试官又要问你:这么多的文件操作,效率自然不高啊。显然,无法通过腾讯面试。
方法三:文件切割
1.我们可以对hashmap进行优化,采用bitmap这种数据结构,可以顺利地同时解决时间问题和空间问题。
2.显然,可以推导出来:512MB大小足够标识所有QQ号码的存在与否,请注意:QQ号码的理论最大值为2^32 - 1,大概是43亿左右
3.用512MB的unsigned int数组来记录文件中QQ号码的存在与否,形成一个bitmap;然后从小到大遍历所有正整数(4字节),当bitmapFlag值为1时,就表明该数是存在的。而且,从上面的过程可以看到,自动实现了去重。显然,这种方式可以通过腾讯的面试。
方法四:bitmap
方法五:使用布隆过滤器
1.文件中有40亿个QQ号码,请设计算法对QQ号码去重,相同的QQ号码仅保留一个,内存限制1G.
请注意,这里必须限制40亿个QQ号码互不相同。通过bitmap记录,客观上就自动完成了排序功能。
2.文件中有40亿个互不相同的QQ号码,请设计算法对QQ号码进行排序,内存限制1G.
我知道,一些刷题经验丰富的人,最开始想到的肯定是用堆或者文件切割,这明显是犯了本本主义错误。直接用bitmap排序,当场搞定中位数
3.文件中有40亿个互不相同的QQ号码,求这些QQ号码的中位数,内存限制1G.
我知道,很多人背诵过top-K问题,信心满满,想到用小顶堆或者文件切割,这明显又是犯了本本主义错误。直接用bitmap排序,当场搞定top-K问题。
4.文件中有40亿个互不相同的QQ号码,求这些QQ号码的top-K,内存限制1G.
1.我知道,一些吸取了经验教训的人肯定说,直接bitmap啊。然而,又一次错了。根据容斥原理可知:因为QQ号码的个数是43亿左右(理论值2^32 - 1),所以80亿个QQ号码必然存在相同的QQ号码。
2.文件分割+bitmap
5.文件中有80亿个QQ号码,试判断其中是否存在相同的QQ号码,内存限制1G.
1.大文件去重、排序问题?一千万的数据,你是怎么快速查询的?
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外部自增,毫秒级别,理论上会出现重复值,但是概率极小,可以忽略不计
2.如果MySQL的自增 ID 用完了,怎么办?
1.问题;当需要从数据库查询的表有上万条记录的时候,一次性查询所有结果会变得很慢,特别是随着数据量的增加特别明显,这时需要使用分页查询。对于数据库分页查询,也有很多种方法和优化的点
3.测试结果:从查询时间来看,基本可以确定,在查询记录量低于100时,查询时间基本没有差距,随着查询记录量越来越大,所花费的时间也会越来越多;针对查询偏移量的测试:随着查询偏移的增大,尤其查询偏移大于10万以后,查询时间急剧增加。
1.这种方式先定位偏移位置的 id,然后往后查询,这种方式适用于 id 递增的情况。
3.这种方式相较于原始一般的查询方法,将会增快数倍。
优化方案1:使用子查询优化
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字段,这为分页查询带来很多便利
优化方案2:使用id限定优化
对于使用 id 限定优化中的问题,需要 id 是连续递增的,但是在一些场景下,比如使用历史表的时候,或者出现过数据缺失问题时,可以考虑使用临时存储的表来记录分页的id,使用分页的id来进行 in 查询。这样能够极大的提高传统的分页查询速度,尤其是数据量上千万的时候
优化方案3:使用临时表优化
3.数据量很大,分页查询很慢,有什么优化方案?
1.注解@Transactional配置的方法非public权限修饰;
2.注解@Transactional所在类非Spring容器管理的bean;比如@Service 注解注释掉;
这种失效场景是我们日常开发中最常踩坑的地方;在类A里面有方法a 和方法b, 然后方法b上面用 @Transactional加了方法级别的事务,在方法a里面 调用了方法b, 方法b里面的事务不会生效。为什么会失效呢?
解决方案:类内部使用其代理类调用事务方法:以上方法略作改动
3.注解@Transactional所在类中,注解修饰的方法被类内部方法调用;
4.业务代码抛出异常类型非RuntimeException,事务失效;
在事务方法中使用try-catch,导致异常无法抛出,自然会导致事务失效。
解决方案:捕获异常并抛出异常
5.业务代码中存在异常时,使用try…catch…语句块捕获,而catch语句块没有throw new RuntimeExecption异常;(最难被排查到问题且容易忽略)
此种事务传播行为不是特殊自定义设置,基本上不会使用Propagation.NOT_SUPPORTED,不支持事务
6.注解@Transactional中Propagation属性值设置错误即Propagation.NOT_SUPPORTED(一般不会设置此种传播机制)
7.mysql关系型数据库,且存储引擎是MyISAM而非InnoDB,则事务会不起作用
4.Spring事务失效的场景有哪些?如何解决?
1.使用 redis 给订单设置过期时间,最后通过判断 redis 中是否还有该订单来决定订单是否已经完成。这种解决方案相较于消息的延迟推送性能较低,因为我们知道 redis 都是存储于内存中,我们遇到恶意下单或者刷单的将会给内存带来巨大压力。
2.使用传统的数据库轮询来判断数据库表中订单的状态,这无疑增加了IO次数,性能极低。
3.使用 jvm 原生的 DelayQueue ,也是大量占用内存,而且没有持久化策略,系统宕机或者重启都会丢失订单信息。
4.rabbitmq死信队列+TTL过期时间来实现延迟队列。
5.淘宝七天自动确认收货,让你设计,可以怎么实现?
2、虽然我们不会采⽤快速排序的算法来实现TOP-K问题,但我们可以利⽤快速排序的思想,在数组中随机找⼀个元素key,将数组分成两部分Sa和Sb,其中Sa的元素>=key,Sb的元素<key,然后分析两种情况:(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
6.有没有jvm调优实战经验?
7.如何保证接口的幂等性?
8.经典top k问题?
1、业务⽅⾯拆分:所有技术⽅⾯的考虑,包括架构设计和解耦拆分都要考虑业务的需要。在服务拆分时,先从业务⻆度确定拆分的⽅案。拆分的边界要充分考虑业务的独⽴性和专业性,⽐如搜索类服务、⽀付类服务、购物⻋类服务,按服务的业务功能合理地划出拆分边界。
2、减少维护成本:拆分前的维护成本 - 拆分后的维护成本 ≧ 0
3、服务独⽴:确保拆分后的服务由相对独⽴的团队负责维护,尽量不要出现在不同服务之间的交叉调⽤。
4、系统扩展:拆分的⼀个重要理由也是最有价值的结果是提⾼了系统的扩展性。⽤户对不同的服务有不同的并发和性能⽅⾯的要求,因此服务具有不同的扩展性。把具有不同扩展性要求的服务拆分出来分别进⾏部署,可以降低成本,提⾼效率。
9.拆分微服务应该注意哪些地⽅,如何拆分?
优化程序,优化服务配置,优化系统配置
1.尽量使⽤缓存,包括⽤户缓存,信息缓存等,多花点内存来做缓存,可以⼤量减少与数据库的交互,提⾼性能。
2.⽤jprofiler等⼯具找出性能瓶颈,减少额外的开销。
3.优化数据库查询语句,减少直接使⽤hibernate等⼯具的直接⽣成语句(仅耗时较⻓的查询做优化)。
4.优化数据库结构,多做索引,提⾼查询效率。
5.统计的功能尽量做缓存,或按每天⼀统计或定时统计相关报表,避免需要时进⾏统计的功能。
6.能使⽤静态⻚⾯的地⽅尽量使⽤,减少容器的解析(尽量将动态内容⽣成静态html来显示)。
7.解决以上问题后,使⽤服务器集群来解决单台的瓶颈问题。
10.⾼并发系统性能优化?
性能优化根据优化的类别,分为业务优化和技术优化。业务优化产生的效果也是非常大的,但它属于产品和管理的范畴。同为程序员,在平常工作中,我们面对的优化方式,主要是通过一系列的技术手段,来完成对既定的优化目标。
1.在写代码的时候,你会发现有很多重复的代码可以提取出来,做成公共的方法。这样,在下次用的时候,就不用再费劲写一遍了。
2.软件系统中,谈到数据复用,我们首先想到的就是缓冲和缓存。缓冲(Buffer),常见于对数据的暂存,然后批量传输或者写入。多使用顺序方式,用来缓解不同设备之间频繁地缓慢地随机写,缓冲主要针对的是写操作。缓存(Cache),常见于对已读取数据的复用,通过将它们缓存在相对高速的区域,缓存主要针对的是读操作。
3.与之类似的,是对于对象的池化操作,比如数据库连接池、线程池等,在 Java 中使用得非常频繁。由于这些对象的创建和销毁成本都比较大,我们在使用之后,也会将这部分对象暂时存储,下次用的时候,就不用再走一遍耗时的初始化操作了。
1.复用优化
1.第一种模式是多机,采用负载均衡的方式,将流量或者大的计算拆分成多个部分,同时进行处理。比如,Hadoop 通过 MapReduce 的方式,把任务打散,多机同时进行计算。
2.第二种模式是采用多进程。比如 Nginx,采用 NIO 编程模型,Master 统一管理 Worker 进程,然后由 Worker 进程进行真正的请求代理,这也能很好地利用硬件的多个 CPU。
3.第三种模式是使用多线程,这也是 Java 程序员接触最多的。比如 Netty,采用 Reactor 编程模型,同样使用 NIO,但它是基于线程的。Boss 线程用来接收请求,然后调度给相应的 Worker 线程进行真正的业务计算。
1.并行优化
1.同步方式,请求会一直阻塞,直到有成功,或者失败结果的返回。虽然它的编程模型简单,但应对突发的、时间段倾斜的流量,问题就特别大,请求很容易失败。
2.异步操作可以方便地支持横向扩容,也可以缓解瞬时压力,使请求变得平滑。同步请求,就像拳头打在钢板上;异步请求,就像拳头打在海绵上。你可以想象一下这个过程,后者肯定是富有弹性的,体验更加友好。
2.变同步为异步
最后一种,就是使用一些常见的设计模式来优化业务,提高体验,比如单例模式、代理模式等。举个例子,在绘制 Swing 窗口的时候,如果要显示比较多的图片,就可以先加载一个占位符,然后通过后台线程慢慢加载所需要的资源,这就可以避免窗口的僵死
3.惰性加载
2.计算优化
1.你要尽量保持返回数据的精简。一些客户端不需要的字段,那就在代码中,或者直接在 SQL 查询中,就把它去掉
2.对于一些对时效性要求不高,但对处理能力有高要求的业务。我们要吸取缓冲区的经验,尽量减少网络连接的交互,采用批量处理的方式,增加处理速度
3.结果集合很可能会有二次使用,你可能会把它加入缓存中,但依然在速度上有所欠缺。这个时候,就需要对数据集合进行处理优化,采用索引或者 Bitmap 位图等方式,加快数据访问速度
3.结果集优化
1.现实中的性能问题,和锁相关的问题是非常多的。大多数我们会想到数据库的行锁、表锁、Java 中的各种锁等。在更底层,比如 CPU 命令级别的锁、JVM 指令级别的锁、操作系统内部锁等,可以说无处不在
2.只有并发,才能产生资源冲突。也就是在同一时刻,只能有一个处理请求能够获取到共享资源。解决资源冲突的方式,就是加锁。再比如事务,在本质上也是一种锁
3.按照锁级别,锁可分为乐观锁和悲观锁,乐观锁在效率上肯定是更高一些;按照锁类型,锁又分为公平锁和非公平锁,在对任务的调度上,有一些细微的差别
4.对资源的争用,会造成严重的性能问题,所以会有一些针对无锁队列之类的研究,对性能的提升也是巨大的。
4.资源冲突优化
比如,作为 List 的实现,LinkedList 和 ArrayList 在随机访问的性能上,差了好几个数量级;又比如,CopyOnWriteList 采用写时复制的方式,可以显著降低读多写少场景下的锁冲突。而什么时候使用同步,什么时候是线程安全的,也对我们的编码能力有较高的要求。
5.算法优化
目前被广泛使用的垃圾回收器是 G1,通过很少的参数配置,内存即可高效回收。CMS 垃圾回收器已经在 Java 14 中被移除,由于它的 GC 时间不可控,有条件应该尽量避免使用
对 JVM 虚拟机进行优化,也能在一定程度上能够提升 JAVA 程序的性能。如果参数配置不当,甚至会造成 OOM 等比较严重的后果。
6.jvm优化
在平时的编程中,尽量使用一些设计理念良好、性能优越的组件。比如,有了 Netty,就不用再选择比较老的 Mina 组件。而在设计系统时,从性能因素考虑,就不要选 SOAP 这样比较耗时的协议。
7.高效实现
11.java性能优化的7个方向
23.经典面试题补充
1.使用nacos 2.0.0版本作为注册中心时,发现在nacos客户端查询注册成功且非default_group组的服务时会响应500
2.
1.nacos2.0版本作为配置中心的bug
1.方式为gitlab权限设置+重大问题小组会议形式讨论
2.如何进行代码review?
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
3.京东面试复盘
1.内存泄漏:内存泄漏就是内存中的变量没有回收,一直存在与内存中,造成内存的浪费的行为
如HashMap、LinkedList等等。如果这些容器为静态的,那么它们的生命周期与程序一致,则容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。简单而言,长生命周期的对象持有短生命周期对象的引用,尽管短生命周期的对象不再使用,但是因为长生命周期对象持有它的引用而导致不能被回收。
1. 静态集合类
在对数据库进行操作的过程中,首先需要建立与数据库的连接,当不再使用时,需要调用close方法来释放与数据库的连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在访问数据库的过程中,对Connection、Statement或ResultSet不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。
2. 各种连接,如数据库连接、网络连接和IO连接等
一般而言,一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏。另一方面,如果没有及时地把对象设置为null,很有可能导致内存泄漏的发生。
如上面这个伪代码,通过readFromNet方法把接受的消息保存在变量msg中,然后调用saveDB方法把msg的内容保存到数据库中,此时msg已经就没用了,由于msg的生命周期与对象的生命周期相同,此时msg还不能回收,因此造成了内存泄漏。实际上这个msg变量可以放在receiveMsg方法内部,当方法使用完,那么msg的生命周期也就结束,此时就可以回收了。还有一种方法,在使用完msg后,把msg设置为null,这样垃圾回收器也会回收msg的内存空间
public class Us ingRandom {private String msg;public void receiveMsg(){readFromNet();//从网络中接受数据保存到msg中saveDB();//把msg保存到数据库中}
3. 变量不合理的作用域
如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄露。
4. 内部类持有外部类
当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为的参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中单独删除当前对象,造成内存泄露
5. 改变哈希值
2.内存泄漏的情况
1.内存泄漏怎么处理?
在spring中主要用到的设计模式有:工厂模式、单例模式、代理模式、模板模式、观察者模式、适配器模式。
IOC控制反转也叫依赖注入,它就是典型的工厂模式,通过sessionfactory去注入实例
解释:将对象交给容器管理,你只需要在spring配置文件总配置相应的bean,以及设置相关属性,让spring容器来生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件中配置的bean初始化好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你 需要调用这些bean的类(假设这个类名是A),分配的方法就是调用A的setter方法来注入,而不需要你在A类里面new这些bean了。
总结:对象实例化与初始化进行解耦
1.工厂模式
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实例,导致蜜汁错误。
并不是所有的注解默认都是单例模式,@RestController就是多例
2.单例模式
Spring中的AOP就是典型的代理模式,首先我们先聊聊AOP(面向切面编程)的的一个设计原理:AOP可以说是对OOP的补充和完善 OOP引入了封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,oop允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能,日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在OOP设计中,它导致大量代码重复,而不利于各个模块的重用。实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码简单点解释,比方说你想在你的biz层所有类中都加上一个打印‘你好’的功能,这时就可以用AOP思想来做,你先写个类写个类方法,方法经实现打印‘你好’,然后IOC这个类ref=\"biz.* \"让每个类都注入即可实现。
3.代理模式
定义:模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤目的:1.使用模版方法模式的目的是避免编写重复代码,以便开发人员可以专注于核心业务逻辑的实现 2.解决接口与接口实现类之间继承矛盾问题
4.模板模式
定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新例如:报社的业务就是出版报纸。向某家报社订阅报纸,只要他们有新报纸出版,就会给你送来。只要你是他们的订户、你就会一直收到新报纸。当你不想再看报纸的时候,取消订阅,他们就不会再送新报纸来。只要报社还在运营,就会一直有人(或单位)向他们订阅报纸或取消订阅报纸。报社:被观察者订户:观察者一个报社对应多个订户
在Spring中有一个ApplicationListener,采用观察者模式来处理的,ApplicationEventMulticaster作为主题,里面有添加,删除,通知等。spring有一些内置的事件,当完成某种操作时会发出某些事件动作,他的处理方式也就上面的这种模式,当然这里面还有很多,可以了解下spring的启动过程。
在java.util 包下 除了常用的 集合 和map之外还有一个Observable类,他的实现方式其实就是观察者模式。里面也有添加、删除、通知等方法。
5.观察者模式
定义:将一个类的接口转接成用户所期待的。一个适配使得因接口不兼容而不能在一起工作的类能在一起工作,做法是将类自己的接口包裹在一个已存在的类中例子:以手机充电为例,电压220V,手机支持5.5V,充电器相当于适配器
AOP和MVC中,都有用到适配器模式。spring aop框架对BeforeAdvice、AfterAdvice、ThrowsAdvice三种通知类型的支持实际上是借助适配器模式来实现的,这样的好处是使得框架允许用户向框架中加入自己想要支持的任何一种通知类型,上述三种通知类型是spring aop框架定义的,它们是aop联盟定义的Advice的子类型。Spring中的AOP中AdvisorAdapter类,它有三个实现:MethodBeforAdviceAdapter、AfterReturnningAdviceAdapter、ThrowsAdviceAdapter。
6.适配器模式
2.spring中使用了哪些设计模式?
Java中创建一个对象所涉及的知识主要有以下几点:1.类加载机制及双亲委派模型 2.JVM组成结构及内存模型首先new一个对象一定是在一个线程中某个方法执行了new操作,此时程序计数器告诉了执行引擎需要执行new所在行的代码,执行引擎获取到new指令后触发了初始化类的操作。初始化类操作首先要检查该类是否被加载,该检查操作需要到方法区中(该new关键字所在的类的)运行时常量池当中先获取该类的全限定类名,通过全限定类名在方法区中查找是否已经被加载。如果没有被加载,使用经过双亲委派模型选定类加载器(一般是appclassloader或自定义加载器)加载类经理加载验证准备解析阶段后,在方法区中开辟了该类的空间、在堆中创建了Class对象,在方法区中为静态变量分配内存。之后执行初始化操作为静态变量赋值。然后开始在堆区为该对象进行内存分配,内存大小在加载阶段就已经决定了。
3.new一个对象的过程?
1.什么是双亲委派?
2.为什么打破双亲委派?
4.什么是双亲委派机制,为什么要打破双亲委派机制?
5.jvm都做过哪些优化?
分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点。
-XX:+PrintGC 输出简要GC日志-XX:+PrintGCDetails 输出详细GC日志-Xloggc:gc.log 输出GC日志到文件
1.如何生成GC日志:
1.JVM启动时增加两个参数:# 出现OOME时生成堆dump:-XX:+HeapDumpOnOutOfMemoryError# 生成堆文件地址:-XX:HeapDumpPath=/home/hadoop/dump/
第一种方式是一种事后方式,需要等待当前JVM出现问题后才能生成dump文件,实时性不高; 第二种方式在执行时,JVM是暂停服务的,所以对线上的运行会产生影响。所以建议第一种方式。
3.第三方可视化工具生成
2.如何产生dump文件
1.监控分析
如果各项参数设置合理,系统没有超时日志或异常信息出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。 遇到以下情况,就需要考虑进行JVM调优:
系统吞吐量与响应性能不高或下降;Heap内存(老年代)持续上涨达到设置的最大内存值;Full GC 次数频繁;GC 停顿时间过长(超过1秒);应用出现OutOfMemory等内存异常;应用中有使用本地缓存且占用大量内存空间;
2.判断
调优的最终目的都是为了应用程序使用最小的硬件消耗来承载更大的吞吐量或者低延迟。 jvm调优主要是针对垃圾收集器的收集性能优化,减少GC的频率和Full GC的次数,令运行在虚拟机上的应用能够使用更少的内存、高吞吐量、低延迟。
下面列举一些JVM调优的量化目标参考实例,注意:不同应用的JVM调优量化目标是不一样的。堆内存使用率<=70%;老年代内存使用率<=70%;avgpause<=1秒;Full GC次数0或avg pause interval>=24小时 ;
3.确定优化目标
调优一般是从满足程序的内存使用需求开始的,之后是时间延迟的要求,最后才是吞吐量的要求。 要基于这个步骤来不断优化,每一个步骤都是进行下一步的基础,不可逆行之。
4.调整参数
5.对比调优前后的指标差异
6.重复上诉的过程
6.jvm调优的步骤
7.hashmap的扩容过程?
确定是哪个接口存在性能问题确定这个接口的内部逻辑是怎样的,做了哪些事情分析接口存在性能问题的根本原因寻找确立优化方案回归验证方案效果
1.是不是资源层面的瓶颈,硬件、配置环境之类的问题?2.针对查询类接口,是不是没有添加缓存,如果加了,是不是热点数据导致负载不均衡?3.是不是有依赖于第三方接口,导致因第三方请求拖慢了本地请求?4.是不是接口涉及业务太多,导致程序执行跑很久?5.是不是sql层面的问题导致的数据等待加长,进而拖慢接口?6.网络层面的原因?带宽不足?DNS解析慢?7.确实是代码质量差导致的,如出现内存泄漏,重复循环读取之类?
8.接口响应慢从哪些方面拍查?
4.蘑菇车联面试复盘
24.面试复盘
反射:在运行状态中,对任意一个类,都能知道这个类的所有属性和方法,对任意一个对象,都能 调用它的任意一个方法和属性。这种能动态获取信息及动态调用对象方法的功能称为 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\
通过 getFields()可以获取 Class 类的属性,但无法获取私有属性,而 getDeclaredFields()可 以获取到包括私有属性在内的所有属性。带有 Declared 修饰的方法可以反射到私有的方法, 没有 Declared 修饰的只能用来反射公有的方法,其他如 Annotation\\Field\\Constructor 也是如 此。
获取过程:
1.通过new对象实现反射机制
2.通过路径实现反射机制
3.通过类名实现反射机制
Java获取反射的三种方法
1.说说你对 Java 反射的理解
注解是通过@interface 关键字来进行定义的,形式和接口差不多,只是前面多了一个@
注解是通过反射获取的,可以通过 Class 对象的 isAnnotationPresent()方法判断它是否应用了 某个注解,再通过 getAnnotation()方法获取 Annotation 对象
2.说说你对 Java 注解的理解
泛型就是将类型变成参数传入,使得可以使用的类型多样化,从而实现解耦。
与普通的 Object 代替一切类型这样简单粗暴而言,泛型使得数据的类别可以像参数一样由外部传递进来。它提供了一种扩展能力。它更符合面向抽象开发的软件编程宗旨。
当具体的类型确定后,泛型又提供了一种类型检测的机制,只有相匹配的数据才能正常的赋值,否则编译器就不通过。所以说,它是一种类型安全检测机制,一定程度上提高了软件的安全性防止出现低级的失误。
泛型提高了程序代码的可读性,不必要等到运行的时候才去强制转换,在定义或者实例化阶段,因为 Cache< String > 这个类型显化的效果,程序员能够一目了然猜测出代码要操作的数据类型。
3.说一下泛型原理,并举例说明
1.字符串常量池需要 String 不可变。因为 String 设计成不可变,当创建一个 String 对象时, 若此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。 如果字符串变量允许必变,会导致各种逻辑错误,如改变一个对象会影响到另一个独立对象。
2.String 对象可以缓存 hashCode。字符串的不可变性保证了 hash 码的唯一性,因此可以缓 存 String 的 hashCode,这样不用每次去重新计算哈希码。在进行字符串比较时,可以直接比较 hashCode,提高了比较性能;
3.安全性。String 被许多 java 类用来当作参数,如 url 地址,文件 path 路径,反射机制所 需的 String 参数等,若 String 可变,将会引起各种安全隐患。
1.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 对象也是在运行时才做的。
2.String在堆栈中的存储
4.关于String
utf-8 是一种变长编码技术,utf-8 编码中的中文占用的字节不确定,可能 2 个、3 个、4 个, int 型占 4 个字节
5.utf-8 编码中的中文占几个字节;int 型几个字节?
代理是一种常用的设计模式,目的是:为其他对象提供一个代理以控制对某个对象的访问, 将两个类的关系解耦。代理类和委托类都要实现相同的接口,因为代理真正调用的是委托类 的方法。
6.静态代理和动态代理的区别,什么场景使用?
final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用 System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。
7.final finally finalize
1.浅拷贝是指拷贝对象时仅仅拷贝对象本身(包括对象中的基本变量),而不拷贝对象包含的引用指向的对象。
2.深拷贝不仅拷贝对象本身,而且拷贝对象包含的引用指向的所有对象
举例来说更加清楚:对象 A1 中包含对 B1 的引用, B1 中包含对 C1 的引用。浅拷贝 A1 得到 A2 , A2 中依然包含对 B1 的引用, B1 中依然包含对 C1 的引用。深拷贝则是对浅拷贝的递归,深拷贝 A1 得到 A2 , A2 中包含对 B2 ( B1 的 copy )的引用, B2 中包含对 C2 ( C1 的 copy )的引用若不对clone()方法进行改写,则调用此方法得到的对象即为浅拷贝
8.深拷贝与浅拷贝
1.静态初始化:初始化时由程序员显式指定每个数组元素的初始值,由系统决定数组长度String[] names = new String[]{\"多啦A梦\
2.动态初始化:初始化时由程序员显示的指定数组的长度,由系统为数据每个元素分配初始值String[] cars = new String[4];
静态初始化方式,程序员虽然没有指定数组长度,但是系统已经自动帮我们给分配了,而动态初始化方式,程序员虽然没有显示的指定初始化值,但是因为 Java 数组是引用类型的变量,所以系统也为每个元素分配了初始化值 null ,当然不同类型的初始化值也是不一样的,假设是基本类型int类型,那么为系统分配的初始化值也是对应的默认值0。
9.数组在内存中如何分配
1.String:不可变的字符串序列;如果操作少量的数据用String
2.StringBuffer:可变的字符串序列、效率低、线程安全;多线程操作字符串缓冲区下操作大量的数据StringBuffer;
3.StringBulider:可变的字符串序列、效率高、线程不安全;单线程操作字符串缓冲区下操作大量的数据使用StringBuilder;
10.String/StringBuffer/StringBuilder的区别
byte:1字节
short:2字节
int:4字节
long:8字节
1.整型
float:4字节
double:8字节
2.浮点型
char:2字节
3.char类型
boolean:1字节
4.boolean类型
11.java的基础数据类型以及字节数
封装(Encapsulation)是面向对象方法的重要原则,就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节
举个比较通俗的例子,比如我们的USB接口。如果我们需要外设且只需要将设备接入USB接口中,而内部是如何工作的,对于使用者来说并不重要。而USB接口就是对外提供的访问接口
对成员变量实行更准确的控制、良好的封装能够减少代码之间的耦合度、外部成员无法修改已封装好的程序代码
1.封装
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。当然,如果在父类中拥有私有属性(private修饰),则子类是不能被继承的。
只支持单继承,即一个子类只允许有一个父类,但是可以实现多级继承,及子类拥有唯一的父类,而父类还可以再继承。子类可以拥有父类的属性和方法、子类可以拥有自己的属性和方法、子类可以重写覆盖父类的方法。
提高代码复用性、父类的属性方法可以用于子类、可以轻松的定义子类、使设计应用程序变得简单
重载(overload):是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。
2.继承
多态是同一个行为具有多个不同表现形式或形态的能力。
多态的体现形式:继承、父类引用指向子类、重写
向上转型:格式:父类名称 对象名 = new 子类名称(); 含义:右侧创建一个子类对象,把它当作父类来使用。 注意:向上转型一定是安全的。 缺点:一旦向上转型,子类中原本特有的方法就不能再被调用了。
3.多态
12.面向对象语言的特征
1.string.inten()方法:
13.其他面试题
1.java基础知识
在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状态,完成三次握手;
1.tcp的三次握手
tcp采用四次挥手来释放连接
2.tcp的四次握手
3.为什么连接的时候是3次握手,关闭的时候是4次握手?
RFC 793中规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。所以规定中2MSL=4min。在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。
4.为什么TIME_WAIT状态需要经过2MSL(最大报文生存时间)才能返回到CLOSE状态?
1.三次握手完成两个重要的功能,既要双方做好发送数据的准备工作(双方都知道彼此已准备好),也要允许双方就初始序列号进行协商,这个序列号在握手过程中被发送和确认。
5.为什么不能使用2次握手进行连接?
TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去, 白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段, 以后每隔75秒钟发送一次。若连发送10个探测报文仍然没反应, 服务器就认为客户端出了故障,接着就关闭连接。
6.如果已经建立了连接,但是客户端突然故障了怎么办?
1.TCP三次握手和四次握手
tcp的滑动窗口做流量控制与乱序重排
保证了tcp的可靠性以及流控特性
2.TCP的滑动时间窗口
其实HTTPS就是从HTTP加上加密处理(一般是SSL安全通信线路)+认证+完整性保护
区别:1. https需要拿到ca证书,需要钱的2. 端口不一样,http是80,https4433. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。4. http和https使用的是完全不同的连接方式(http的连接很简单,是无状态的;HTTPS 协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。)
3.http与https的区别
在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)中设定这个时间。实现长连接要客户端和服务端都支持长连接
4.http是长连接还是短连接?
1.TCP是面向连接的协议,发送数据前要先建立连接,TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢失,没有重复,并且按顺序到达;UDP是无连接的协议,发送数据前不需要建立连接,是没有可靠性;
2.TCP通信类似于于要打个电话,接通了,确认身份后,才开始进行通行;UDP通信类似于学校广播,靠着广播播报直接进行通信。
3.TCP只支持点对点通信,UDP支持一对一、一对多、多对一、多对多;
4.TCP是面向字节流的,UDP是面向报文的; 面向字节流是指发送数据时以字节为单位,一个数据包可以拆分成若干组进行发送,而UDP一个报文只能一次发完。
5.TCP首部开销(20字节)比UDP首部开销(8字节)要大
6.UDP 的主机不需要维持复杂的连接状态表
5.tcp与udp的区别
1.get重点在从服务器上获取资源,post重点在向服务器发送数据;
2.Get传输的数据量小,因为受URL长度限制,但效率较高; Post可以传输大量数据,所以上传文件时只能用Post方式;
3.get是不安全的,因为get请求发送数据是在URL上,是可见的,可能会泄露私密信息,如密码等; post是放在请求头部的,是安全的
6.GET与POST的区别
1.cookie
2.session
3.单个cookie保存的数据不能超过4K,session无此限制
4.session一定时间内保存在服务器上,当访问增多,占用服务器性能,考虑到服务器性能方面,应当使用cookie。
7.cookie与session的区别
CND 一般包含分发服务系统、负载均衡系统和管理系统
8.cdn原理
1. 建立TCP连接:三次握手
2.Web浏览器向Web服务器发送请求行:一旦建立了TCP连接,Web浏览器就会向Web服务器发送请求命令。例如:GET /sample/hello.jspHTTP/1.1。
3. Web浏览器发送请求头:浏览器发送其请求命令之后,还要以头信息的形式向Web服务器发送一些别的信息,之后浏览器发送了一空白行来通知服务器,它已经结束了该头信息的发送。
4. Web服务器应答:客户机向服务器发出请求后,服务器会客户机回送应答, HTTP/1.1 200 OK ,应答的第一部分是协议的版本号和应答状态码。
5. Web服务器发送应答头:正如客户端会随同请求发送关于自身的信息一样,服务器也会随同应答向用户发送关于它自己的数据及被请求的文档。
6. Web服务器向浏览器发送数据:Web服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据。
7. Web服务器关闭TCP连接:四次握手
9.一次完整的HTTP请求所经历几个步骤?
2.计算机网络基础知识
1.java基础
排列有序,可重复
底层使用的数组结构
查询速度快,增删比较慢,getter()和setter()方法比较快
线程不安全
当容量不够时,ArrayList扩容为当前容量*1.5+1
ArrayList
线程安全,效率低
当容量不够时,Vector默认扩容为一倍的容量
Vector
底层使用的双向循环链表数据结构
查询速度慢,增删快,add()和remove()方法比较快
Linkedlist
List
排列无序,不可重复
底层使用hash表实现
存取速度快
内部是hashmap
HashSet
排列无序,不可重复
底层使用二叉树
排序存储
内部是TreeMap的SortedSet
TreeSet
采用hash表存储,并用双向链表记录插入顺序
内部是LinkedHashMap
LinkedHashSet
Set
Queue
Collection:Collection 是集合 List、Set、Queue 的最基本的接口
Iterator:迭代器,可以通过迭代器遍历集合中的数据
键不可重复、值可以重复
底层是哈希表
允许key值为null,value也可以为null
HashMap
线程安全
key、value都不可以为null
hashtable
底层是二叉树
TreeMap
Map:是映射表的基础接口
集合部分接口的继承关系图
大方向上,hashmap里面是一个数组,然后数组上每个元素是一个单向的链表,每个元素的实体是嵌套类Entry的实例,Entry实例包含四个属性:key、value、hash值、用于单向链表的next
capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍loadFactor:负载因子,默认为 0.75threshold:扩容的阈值,等于 capacity * loadFactor
jdk1.7(数组+链表)
Java8 对 HashMap 进行了一些修改,最大的不同就是利用了红黑树,所以其由 数组+链表+红黑树 组成。
查找的时候,根据 hash 值我们能够快速定位到数组的具体下标,但是之后的话,需要顺着链表一个个比较下去才能找到我们需要的,时间复杂度取决于链表的长度,为 O(n)。为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
当链表长度大于或等于阈值(默认为 8)的时候,如果同时还满足容量大于或等于 MIN_TREEIFY_CAPACITY(默认为 64)的要求,就会把链表转换为红黑树。同样,后续如果由于删除或者其他原因调整了大小,当红黑树的节点小于或等于 6 个以后,又会恢复为链表形态。通常如果 hash 算法正常的话,那么链表的长度也不会很长,那么红黑树也不会带来明显的查询时间上的优势,反而会增加空间负担。所以通常情况下,并没有必要转为红黑树,所以就选择了概率非常小,小于千万分之一概率,也就是长度为 8 的概率(当长度为 8 的时候,概率仅为 0.00000006),把长度 8 作为转化的默认阈值。
jdk1.8(数组+链表+红黑树)
线程不安全,在并发环境下,可能会形成环状链表,导致get操作时CPU空转,所以,在并发环境下使用hashmap是非常危险的。
缺点
hashmap的实现原理
容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的\"分段锁\"思想
简单理解就是,ConcurrentHashMap 是一个 Segment 数组,Segment 通过继承ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
jdk1.7(ReentrantLock+Segment+HashEntry)
采用的与hashmap的底层数据结构一样:数组+链表+红黑树
JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8实现降低锁的粒度就是HashEntry(首节点)
JDK1.8版本的数据结构变得更加简单,去掉了Segment这种数据结构,使用synchronized来进行同步锁粒度降低,所以不需要分段锁的概念,实现的复杂度也增加了
JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
低粒度加锁方式,synchronized并不比ReentrantLock差;粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存
JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock
jdk1.8(synchronized+CAS+HashEntry+红黑树)
currenthashmap的实现原理
1.Collection 和 Collections 有什么区别?
Vector每次扩容增加一倍Arraylist每次扩容增加50%Hashmap通过resize方法,每次扩容2倍,负载因子默认0.75F
2.Hashmap的扩容操作是怎么实现的?
哈希:hash一般翻译为散列,hash就是指使用哈希算法把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值
哈希冲突:当两个不同的输入值,根据同一散列函数计算出相同的散列值的现象,我们把它叫做哈希碰撞
1.链表法就是将相同的hash值的对象组织成一个链表放在hash值对应的槽位
2.开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用的槽位。
解决方案:
3.Hashmap是怎么解决哈希冲突的?
4.能否使用任何类作为 Map 的 key?
2.效率:因为线程安全的问题, HashMap 要比HashTable效率高- -点. 另外,HashTable 基本被淘汰,不要在代码中使用它; (如果 你要保证线程安全的话就使用ConcurrentHashMap ) ;
6.底层数据结构: JDK1.8 以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
7.推荐使用:在Hashtable的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用HashMap替代,如果需要多线程使用则用ConcurrentHashMap替代。
5.HashMap 与 HashTable 有什么区别?
6.如何决定使用 HashMap 还是 TreeMap?
答: ConcurrentHashMap结合了HashMap和HashTable二者的优势。HashMap 没有考虑同步,HashTable考虑了同步的问题使用了synchronized关键字,所以HashTable在每次同步执行时都要锁住整个结构。ConcurrentHashMap 锁的方式是稍微细粒度的。
7.ConcurrentHashMap 和 Hashtable 的区别?
8.HashMap 和 ConcurrentHashMap 的区别?
基于 LinkedHashMap 的访问顺序的特点,可构造一个 LRU(Least Recently Used) 最近最少使用简单缓存。也有一些开源的缓存产品如 ehcache 的淘汰策略( LRU )就是在 LinkedHashMap 上扩展的。
9.LinkedHashMap 的应用
经典面试题
在JDK1.7中,由”数组+链表“组成,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。
在JDK1.8中,有“数组+链表+红黑树”组成。当链表过长,则会严重影响HashMap的性能,红黑树搜索时间复杂度是O(logn),而链表是O(n)。因此,JDK1.8对数据结构做了进一步的优化,引入了红黑树,链表和红黑树在达到一定条件会进行转换:(1)当链表超过8且数组长度(数据总量)超过64才会转为红黑树(2)将链表转换成红黑树前会判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是转换为红黑树,以减少搜索时间。
1. HashMap的底层数据结构是什么?
1.hashmap存取是无序的2.键和值位置都可以是null,但是键位置只能是一个null3.键位置是唯一的,底层的数据结构是控制键的4.jdk1.8前数据结构是:链表+数组jdk1.8之后是:数组+链表+红黑树5.阈值(边界值)>8并且数组长度大于64,才将链表转换成红黑树,变成红黑树的目的是提高搜索速度,高效查询
2.说一下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
开放定址法和再哈希法的区别是
3. 解决hash冲突的办法有哪些?HashMap用的哪种?
在数组比较小时如果出现红黑树结构,反而会降低效率,而红黑树需要进行左旋右旋,变色,这些操作来保持平衡,同时数组长度小于64时,搜索时间相对要快些,总之是为了加快搜索速度,提高性能
JDK1.8以前HashMap的实现是数组+链表,即使哈希函数取得再好,也很难达到元素百分百均匀分布。当HashMap中有大量的元素都存放在同一个桶中时,这个桶下有一条长长的链表,此时HashMap就相当于单链表,假如单链表有n个元素,遍历的时间复杂度就从O(1)退化成O(n),完全失去了它的优势,为了解决此种情况,JDK1.8中引入了红黑树(查找的时间复杂度为O(logn))来优化这种问题
4. 为什么要在数组长度大于64之后,链表才会进化为红黑树
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的次数,避免过多的性能消耗
5. 为什么加载因子设置为0.75,初始化临界值是12?
hashCode方法是Object中的方法,所有的类都可以对其进行使用,首先底层通过调用hashCode方法生成初始hash值h1,然后将h1无符号右移16位得到h2,之后将h1与h2进行按位异或(^)运算得到最终hash值h3,之后将h3与(length-1)进行按位与(&)运算得到hash表索引
平方取中法
取余数
伪随机数法
其他可以计算出hash值的算法有
6. 哈希表底层采用何种算法计算hash值?还有哪些算法可以计算出hash值?
hashCode相等产生hash碰撞,hashCode相等会调用equals方法比较内容是否相等,内容如果相等则会进行覆盖,内容如果不等则会连接到链表后方,链表长度超过8且数组长度超过64,会转变成红黑树节点
7. 当两个对象的hashCode相等时会怎样
只要两个元素的key计算的hash码值相同就会发生hash碰撞,jdk8之前使用链表解决哈希碰撞,jdk8之后使用链表+红黑树解决哈希碰撞
8. 何时发生哈希碰撞和什么是哈希碰撞,如何解决哈希碰撞?
以jdk8为例,简要流程如下:
1.首先根据key的值计算hash值,找到该元素在数组中存储的下标
2.如果数组是空的,则调用resize进行初始化;
3.如果没有哈希冲突直接放在对应的数组下标里
4.如果冲突了,且key已经存在,就覆盖掉value
5.如果冲突后是链表结构,就判断该链表是否大于8,如果大于8并且数组容量小于64,就进行扩容;如果链表节点数量大于8并且数组的容量大于64,则将这个结构转换成红黑树;否则,链表插入键值对,若key存在,就覆盖掉value
6.如果冲突后,发现该节点是红黑树,就将这个节点挂在树上
9. HashMap的put方法流程
HashMap在容量超过负载因子所定义的容量之后,就会扩容。java里的数组是无法自己扩容的,将HashMap的大小扩大为原来数组的两倍
扩容之后原位置的节点只有两种调整1.保持原位置不动(新bit位为0时)2.散列原索引+扩容大小的位置去(新bit位为1时)扩容之后元素的散列设置的非常巧妙,节省了计算hash值的时间
当数组长度从16到32,其实只是多了一个bit位的运算,我们只需要在意那个多出来的bit为是0还是1,是0的话索引不变,是1的话索引变为当前索引值+扩容的长度,比如5变成5+16=21
这样的扩容方式不仅节省了重新计算hash的时间,而且保证了当前桶中的元素总数一定小于等于原来桶中的元素数量,避免了更严重的hash冲突,均匀的把之前冲突的节点分散到新的桶中去
10. HashMap的扩容方式
一般用Integer、String这种不可变类当HashMap当key
因为String是不可变的,当创建字符串时,它的hashcode被缓存下来,不需要再次计算,相对于其他对象更快
因为获取对象的时候要用到equals()和hashCode()方法,那么键对象正确的重写这两个方法是非常重要的,这些类很规范的重写了hashCode()以及equals()方法
11. 一般用什么作为HashMap的key?
树节点占用空间是普通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,两者相差不大,而红黑树节点占用更多的内存空间,所以此时转换最为友好
12. 为什么Map桶中节点个数超过8才转为红黑树?
多线程下扩容死循环。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中都存在
13. HashMap为什么线程不安全?
我们计算索引需要将hashCode值与length-1进行按位与运算,如果数组长度很小,比如16,这样的值和hashCode做异或实际上只有hashCode值的后4位在进行运算,hash值是一个随机值,而如果产生的hashCode值高位变化很大,而低位变化很小,那么有很大概率造成哈希冲突,所以我们为了使元素更好的散列,将hash值的高位也利用起来
14. 计算hash值时为什么要让低16bit和高16bit进行异或处理
第一步:通过hash(key)方法获取key 在node数组下的位置。判断数组是否为空并且数组长度是否大于0并且数组索引下第一个节点是否为空。第二步:查看第一个结点的是否满足返回条件,满足则直接返回第三步:如果不满足 则判断 数组下是链表还是红黑树 都通过equals 方法进行比对key是否相同,相同则返回Node节点第四步:全不满足,返回null
15.9. HashMap的get方法流程
hashmap夺命14问
2.Java集合
1.加载-验证-准备-解析-初始化-使用-卸载
加载器分为引导类加载器、扩展类加载器、应用类加载器。其中引导类加载器为扩展类加载器的父类加载器,扩展类加载器为应用类加载器的父类,向上委托。
1.什么是双亲委派机制?
1.避免类的重复加载,当父加载器已经加载了该类时,就没有必要子classloader再加载一次,保证了加载类的一致性
3.95%以上的类都是由应用类加载器进行加载的。这样丛下往上进行加载,第一次加载时可能比较慢,但是在之后运用的时候就查找比较快;如果换成从上往下进行加载,第一次可能比较快,可之后的大量查找会比较慢。
4.引导类加载器是由C++实现的,也比较快,进而减少了加载时间。
2.为什么要使用双亲委派机制?
2.双亲委派机制
1.jvm的类加载机制
是描述java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息.每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程
栈帧( Frame)是用来存储数据和部分过程结果的数据结构,同时也被用来处理动态链接(Dynamic Linking)、 方法返回值和异常分派( Dispatch Exception)。栈帧随着方法调用而创建,随着方法结束而销毁——无论方法是正常完成还是异常完成(抛出了在方法内未被捕获的异常)都算作方法结束。
栈
本地方法栈
程序计数器
1.线程私有
新生代与老年代的比例为1:2
新生代中分为Eden区和Survivor区(From Survivor和ToSurvivor),比例为8:1:1
是被线程共享的一块内存区域,创建的对象和数组都保存在 Java 堆内存中,也是垃圾收集器进行垃圾收集的最重要的内存区域
堆
用于存储被 JVM 加载的类信息、常量、静态变量、即时编译器编译后的代码等数据.
方法区(元空间)
2.线程共享
2.java内存模型
1.虚拟机错误 VirtualMachineError
1.示例:堆内对象不能进行回收,堆内存持续增大,这样达到了堆内存的最大值,数据满了,所以就溢出了。
2.解决方案:找到问题点,分析哪个地方是否存储了大量类没有进行回收,通过JMAP命令将线上的堆内存导出来之后进行分析;
3.可通过设置jvm参数中的 -Xmx100m,设置最大的堆内存进行错误呈现;
1.堆溢出-java.lang.OutOfMemoryError:Java heap space
1.示例:无限的创建线程,直到线程无法进行创建,则会抛出该异常;
2.可通过设置jvm参数中的 -Xss512k,设置栈帧的大小,默认为1M
2.栈溢出-java.lang.OutOfMemoryError
1.示例:死循环的递归调用
2.程序每次进行递归的时候,会将结果数据压入栈,包括里面的指针等,这个时候就需要帧栈大一些才能承受更多的递归调用;通过设置-Xss进行调整;
3.解决方案:通过jstack将线程数据导到文件进行分析;找到递归的点,如果程序就是需要递归的次数的话,那么这个时候就需要增加帧栈的大小以适应程序。
3.栈溢出-java.lang.StackOverFlowError
1.元数据区存储着类的相关信息、常量池、静态变量、方法描述符、字段描述符,运行时产生大量的类会造成这个区域的溢出;
2.产生情况:通过CBLIG大量的生成类,导致元数据空间满了;jdk1.7的时候运用String.inten()不当,产生了大量的常量数据;加载jsp以及动态生成jsp文件;
3.通过jvm的参数:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize:100M进行设置;如果调大了之后还是出现异常,就需要分析哪里出现的溢出并且fix掉;
4.元信息溢出-java.lang.OutOfMemoryError:Metaspace
1.直接内存也就是堆外内存。一般出现在程序中使用了NIO,比如netty。NIO为了提高性能,避免在java heap和native heap之间切换,所以使用直接内存。默认情况下,堆外内存与堆内存大小一致,堆外内存不受jvm的限制,但是受制于机器整体内存的大小限制。
2.解决思路:这种情况一般是我们使用了直接内存造成的溢出,这个时候我们需要检查一下程序里面是否使用了NIO,比如说netty,检查一下里面的直接内存配置。
5.直接内存溢出-java.lang.OutOfMemoryError:Direct buffer memory
1.如果98%的GC回收不到2%的空间的时候就会报这个错误,也就是最大最小内存出现了问题。
2.比如我们创建了一个线程池,如果线程池执行的时候核心线程数处理不过来的时候会把数据放到LinkedBlockingQueue里面,也就是放在了堆内存中。这个时候就需要检查一下- Xms /-Xmx最大最小堆内存空间设置的是否合理;再一个dump出现当前内存来分析一下是否使用了大量的循环或者是使用了大量内存的代码。
3.GC排查工具:jvisualvm、arthas
6.GC超限-java.lang.OutOfMemoryError:GC overload limit exceeded
2.内存溢出OutOfMemoryError
3.线程死锁 ThreadDeath
1.Error
1.运行时异常,这种异常我们不需要进行处理,完全由虚拟机接管。写程序时不需要进行catch或者throw
2.NullPointerException-空指针引用异常
3.ClassCastException - 类型强制转换异常
4.IllegalArgumentException - 传递非法参数异常
5.ArithmeticException - 算术运算异常
6.ArrayStoreException - 向数组中存放与声明类型不兼容对象异常
7.IndexOutOfBoundsException - 下标越界异常
8.NumberFormatException - 数字格式异常
9.NegativeArraySizeException - 创建一个大小为负数的数组错误异常
10.SecurityException - 安全异常
11.UnsupportedOperationException - 不支持的操作异常
1.运行时异常:RuntimeException
2.IO异常:IOException
3.SQL异常:SQLException
2.Exception
1.java异常结构:Throwable
1.jvisualvm
2.arthas阿里调优工具
3.jconsole
4.MAT(Memory Analyzer)
2.JC的排查工具
-Xms:初始堆大小
-Xmx:最大堆大小
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
1.堆设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParalledlOldGC:设置并行年老代收集器
-XX:+UseConcMarkSweepGC:设置并发收集器
2.垃圾收集器设置
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
3.垃圾回收统计信息
-XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
4.并行收集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。
5.并发收集器设置
3.jvm常见的参数配置
1.问题:线程运行每秒产生60M对象,运行25秒即占满了eden区
2.解决:增加eden区s区的大小,让朝生夕死的对象尽量在年轻代就被干掉,不要被挪进老年代,从而触发Full GC
3.JVM调优案例1:能否对jvm进行调优,让其几乎不发生Full GC?
1.可以使用G1垃圾收集器,设置目标暂停时间,默认为200ms;边找边计算,这块区域需要GC的时间接近200ms,就会进行GC
2.通过设置jvm的参数:-XX:MaxGCPauseMills:目标暂停时间(默认为200ms)
4.JVM调优案例2:对于大内存的如何进行优化?
1.问题:在递归中,不能无限制的调用自己,必须要有边界条件,能够让递归结束,因为每一次递归调用都会在栈内存中开辟新的空间,重新执行方法,如果递归的层次太深,很容易造成栈内存溢出-java.lang.StackOverFlowError
2.解决方案:通过jstack将线程数据导到文件进行分析;找到递归的点,如果程序就是需要递归的次数的话,那么这个时候就需要增加帧栈的大小以适应程序。
5.JVM调优案例3:递归方法造成的栈内存的溢出
1.问题解析:一般CPU占用100%都是死循环或者是大对象的锅
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函数的调用错误
6.CPU百分百如何排查?
1.使用top -c命令找出当前进程的运行列表,按一下P可以按照CPU的使用率进行排序,这样可以找到哪个进程消耗CPU最高,比如PID为8862.使用top -Hp 886 找出这个进程下面的线程,按P进行排序,比如线程 9886消耗最高2. 获取内存dump: jmap -histo:live pid这种方式会先出发fullgc,所有如果不希望触发fullgc 可以使用jmap -histo pid
7.内存爆满如何排查
2. 获取内存dump: jmap -histo:live pid这种方式会先出发fullgc,所有如果不希望触发fullgc 可以使用jmap -histo pid
3.第三种方式:jdk启动加参数:-XX:+HeapDumpBeforeFullGC -XX:HeapDumpPath=/httx/logs/dump这种方式会产生dump日志,再通过jvisualvm.exe 或者Eclipse Memory Analysis Tools 工具进行分析
8.java获取内存dump的几种方式
3.jvm的调优
1.引用计数法---无法解决循环引用的问题
经过一系列成为GCRoots的点作为起点,向下搜索,当一个对象到任何GC Roots都没有引用链相连,说明其已经死亡
JVM栈中的引用
元空间中的静态引用
JNI中的引用(一般说的natice中的方法)
锁的引用
GC Roots
2.可达性分析算法
1.判断哪些对象已经死亡
对象存活比较多的时候适用
适合老年代
适用场景
提前GC
容易产生碎片空间
扫描了2次:标记存活对象、清除没有标记的对象
1.标记清除
存活对象少,比较高效
扫描整个空间(标记存活对象并且复制移动)
适合年轻代
需要空闲空间
需要复制移动对象
2.复制copy
标记后不是去清理对象,而是将存活的对象移动到内存的一端。然后清除端边界外的对象。
3.标记整理
目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的区域,一般情况下将GC堆划分为老年代和年轻代。
老年代的特点是每次垃圾回收时只有少量的对象需要被回收,年轻代的特点是每次垃圾回收时都有大量的垃圾需要被回收,因此可以根据不同的区域选择不同的算法
老年代因为每次回收的对象比较少,一般采用标记整理(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
回收步骤
4.分带收集
2.垃圾回收机制
把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,他处于可达状态,他是不能够被垃圾回收期回收的,即使该对象以后永远都不会被用到 JVM 也不会回收。因此强引用是造成 Java 内存泄漏的主要原因之一。
1.强引用
软引用需要用 SoftReference 类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。
2.软引用
弱引用需要用 WeakReference 类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。
3.弱引用
虚引用需要 PhantomReference 类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
4.虚引用
3.java中的四种引用类型
单线程、复制算法
java虚拟机运行在Client模式下的默认新生代垃圾收集器
Serial
Serial+多线程
是Serial的多线程版本,java虚拟机运行在Server模式下的默认新生代垃圾收集器
Parnew
多线程复制算法、高效
它重点关注的是程序达到一个可控制的吞吐量,高吞吐量可以最高效率地利用 CPU 时间,尽快地完成程序的运算任务,主要适用于在后台运算而不需要太多交互的任务。
自适应调节策略也是 ParallelScavenge 收集器与 ParNew 收集器的一个重要区别
Paraller Scavenge
1.新生代
单线程、标记整理算法
是Serial垃圾收集器的老年代版本
java虚拟机运行在Client模式下的默认老年代垃圾收集器
在 JDK1.5 之前版本中与新生代的 Parallel Scavenge 收集器搭配使用。作为年老代中使用 CMS 收集器的后备垃圾收集方案。
Serial Old
多线程、标记整理算法
Parallel Old 收集器是Parallel Scavenge的年老代版本。
Parallel Old 正是为了在老年代同样提供吞吐量优先的垃圾收集器,如果系统对吞吐量要求比较高,可以优先考虑新生代 Parallel Scavenge和年老代 Parallel Old 收集器的搭配策略。
Parallel Old
多线程、标记清除算法
其最主要目标是获取最短垃圾回收停顿时间,可以为交互比较高的程序提高用户体验
对CPU资源敏感
无法处理浮动的垃圾
基于标记清除算法,产生大量的垃圾碎片
初始标记 :stw 从gc root 开始直接可达的对象
并发标记: gc root 对对象进行可达性分析 找出存活对象 可达性分析算法,不需要stw
最终标记:stw 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
并发清除:清除 GC Roots 不可达对象,和用户线程一起工作,不需要暂停工作线程。由于耗时最长的并发标记和并发清除过程中,垃圾收集线程可以和用户现在一起并发工作,所以总体上来看CMS 收集器的内存回收和用户线程是一起并发地执行
收集步骤
CMS(Concurrent Mark Sweep)
基于标记整理算法,不产生垃圾碎片,分配大对象不会提前GC
可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。充分利用cpu,多核条件下缩短stw
并发标记: gc root 对对象进行可达性分析 找出存活对象 可达性分析算法,不需要stw
最终标记:stw 为了修正在并发标记期间,因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要暂停所有的工作线程。
并发清除:根据用户期待的gc停顿时间指定回收计划
young gc 回收所有的eden s区 复制一些存活对象到old区s区
mixed gc
回收模式
g1分区域 每个区域是有老年代概念的 但是收集器以整个区域为单位收集
g1回收后马上合并空闲内存 cms 在stw的时候做
与cms的区别
XX:G1HeapRegionSize
复制成活对象到一个区域 暂停所有线程stw
设置参数
G1
2.老年代
4.垃圾回收器
4.垃圾回收
利用可达性分析算法,虚拟机会将一些对象定义为 GC Roots,从 GC Roots 出发沿着引用链 向下寻找,如果某个对象不能通过 GC Roots 寻找到,虚拟机就认为该对象可以被回收掉。
1.哪些情况下的对象会被垃圾回收机制处理掉?
虚拟机栈(栈帧中的本地变量表)中引用的对象;
方法区中的类静态属性引用的对象,常量引用的对象;
本地方法栈中 JNI(Native 方法)引用的对象;
2.哪些对象可以被看做是 GC Roots 呢?
即使不可达,对象也不一定会被垃圾收集器回收,1)先判断对象是否有必要执行 finalize() 方法,对象必须重写 finalize()方法且没有被运行过。2)若有必要执行,会把对象放到一个 队列中,JVM 会开一个线程去回收它们,这是对象最后一次可以逃逸清理的机会。
3.对象不可达,一定会被垃圾收集器回收么?
5.面试题补充
3.jvm
1.避免全表扫描,提升查询的效率
2.主键、普通键、唯一键都可以作为索引
1.为什么要使用索引
1.选择唯一性索引:唯一性索引的值是唯一的,可以更快速的通过索引来确定某条记录
2.为经常需要排序、分组和联合操作的字段建立索引
3.经常作为查询条件的字段建立索引
4.限制索引的数目:越多的索引,会耗费大量时间进行索引维护,会似表更新变得很浪费时间
5.尽量使用数据量少的索引,如果索引的值很长,那么查询的速度会受到影响
6.尽量使用前缀来作为索引
7.删除很少使用或者不再使用的索引
8.最左匹配原则
9.尽量选择区分度高的列作为索引,区分度高是指字段不重复的列
10.索引列不参与计算,保持列干净,带函数的查询不参与索引
11.尽量的扩展索引,而不是新建索引
2.索引创建的原则有哪些
1.对半搜索,左孩子右孩子,每个节点最多2个孩子
2.左子树的键值小于根的键值,右子树的键值大于根的键值。
3.二叉排序树的查找性能在0(Log2n)到O(n)之间。因此,为了获得较好的查找性能,就要构造-棵平衡的二叉排序树。
1.二叉树
1.符合二叉树的条件下
2.任何节点的两个子树的高度最大差为1如果在avl 树中进行插入和删除节点操作,可能导致avl树失去平衡,那么可以通过旋转重新达到平衡。因此我们说的二叉树也称自平衡二叉树
2.平衡二叉树
1.红黑树和avl树类似,都是在进行插入和删除操作时通过特定的操作保持二叉树的平衡,从而获得较高的查找性能。
3.红黑树
1.根节点至少包括两个孩子
2.树中每个节点最多包含m个孩子(m>2)
3.除根节点和叶节点外,其他每个节点至少有ceil(m/2)个孩子,ceil是取上限,ceil(3/2)=2
4.所有的叶子节点都位于同一层
4.B-tree
1.B+树是B树的变体,其定义基本与B树相同
2.非叶子节点仅用来索引,数据都保存在叶子节点中
3.所有的叶子节点均有一个链指针指向下一个叶子节点
1.B+tree的磁盘读写代价更低
2.B+树的查询效率更加稳定
3.B+树更有利于对数据库的扫描
4.B+Tree更适合作为存储索引
5.B+-tree
仅仅能满足“=\
6.hash索引
7.bitmap索引(位图索引)
2.索引的数据结构
1.只有一个聚集索引,聚簇索引的叶子节点存储行记录。根据聚簇索引的 key 查找是非常快的
2.如果表定义了主键,则主键就是聚集索引;
3.如果表没有定义PK,则第一个 not NULL unique 列是聚集索引;否则,InnoDB 会创建一个隐藏的 row-id 作为聚集索引。
4.叶子节点保存的不仅仅是键值,还保存了位于同一行信息的其他列的信息,由于密集索引决定了表的物理排列顺序,一个表只能有一个物理排列顺序,所以一个表只能创建一个密集索引
1.密集索引(聚簇索引)
1.叶子节点仅保存了键位信息以及该行数据的地址,有的稀疏索引仅保存了键位信息及其主键,仍然需要地址或者主键信息进行定位。
2.稀疏索引
3.密集索引与稀疏索引的区别
3.第一个索引字段是绝对有序的,第二个、第三个就不一定了。所以如果直接使用第二个索引字段是不会走B+Tree索引查询的,这就是联合索引必须满足最左匹配原则的原因。
以下排列组合都会走索引: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 是一个范围查询,遇到范围查询就停止了。
4.例子:比如有联合索引 [a、b、c],where 过滤条件中哪些排列组合可以用到索引?
5.联合索引的最左匹配原则的成因
7.Mysism与Innodb的区别是什么
1.频繁执行全表count的语句
2.对数据进行增删改的频率不高,查询非常频繁
3.没有事务
1.myisam
1.数据增删改查都相当频繁
2.可靠性要求比较高,要求支持事务
2.innodb
8.myisam与innodb适合的场景
按锁的粒度划分:表级锁、行级锁、页级锁
按锁级别划分:共享锁、排它锁
按加锁方式划分:自动锁、显示锁
按操作划分:DML锁、DDL锁
按使用方式划分:乐观锁、悲观锁
9.数据库锁的分类
1.悲观锁是对数据提前进行加锁,依赖于数据库层面,对其他事务呈保守策略,故在高并发情况下影响效率。for update
1.悲观锁
1.乐观锁是在表中引入version字段或者时间戳,每次更新操作之前先查询出version号,更新时以该version字段作为条件,若不一致则更新失败,说明此session数据已过期,根据逻辑处理要求重新更新。
2.乐观锁
10.悲观锁与乐观锁
1.原子性Atomicity:事务是一个完整的操作。事务的各步操作是不可分的(原子的);要么都执行,要么都不执行。
2.一致性Consistency:当事务完成时,数据必须处于一致状态。
3.隔离性Isolation:对数据进行修改的所有并发事务是彼此隔离的,这表明事务必须是独立的,它不应以任何方式依赖于或影响其他事务。
4.永久性Durability:事务完成后,它对数据库的修改被永久保持,事务日志能够保持事务的永久性。
11.数据库事务的四大特性
1.MVCC(Multiversion concurrency control) 就是同一份数据保留多版本的一种方 式,进而实现并发控制。在查询的时候,通过 read view 和版本链找到对应版本的数据
2.作用:提升并发性能。对于高并发场景,MVCC 比行级锁开销更小。
MVCC 的实现依赖于版本链,版本链是通过表的三个隐藏字段实现。 DB_TRX_ID:当前事务 id,通过事务 id 的大小判断事务的时间顺序。 DB_ROLL_PRT:回滚指针,指向当前行记录的上一个版本,通过这个指针 将数据的多个版本连接在一起构成 undo log 版本链。 DB_ROLL_ID:主键,如果数据表没有主键,InnoDB 会自动生成主键
使用事务更新行记录的时候,就会生成版本链,执行过程如下: 1.用排他锁锁住该行; 2.将该行原本的值拷贝到 undo log,作为旧版本用于回滚;3. 修改当前行的值,生成一个新版本,更新事务 id,使回滚指针指向旧版本的 记录,这样就形成一条版本链。
3.实现原理:
12.MVVC机制
1.快照读:读取的是快照版本。普通的 SELECT 就是快照读。通过 mvcc 来进行 并发控制的,不用加锁。快照读读到的有可能不是数据的最新版本,可能是之前的历史版本
2.当前读:读取的是最新版本。UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE 是当前读。 当前读就是加了锁的增删改查语句
3.快照读情况下,InnoDB 通过 mvcc 机制避免了幻读现象。而 mvcc 机制无法避免当前读 情况下出现的幻读现象。因为当前读每次读取的都是最新数据,这时如果两次查询中间 有其它事务插入数据,就会产生幻读
12.当前读与快照度
Serializable (串行化):可避免脏读、不可重复读、幻读的发生。通过强制事务排序,使之不可能相互冲突,从而 解决幻读问题
Repeatable read (可重复读):可避免脏读、不可重复读的发生;MySQL 的默认事务隔离级别,它确保同一 事务的多个实例在并发读取数据时,会看到同样的数据行,解决了不可重 复读的问题。
Read committed (读已提交):一个事务只能看见已经提交事务所做的改 变。可避免脏读的发生。oracle数据库默认rc级别
Read uncommitted (读未提交):最低级别,任何情况都无法保证;所有事务都可以看到其他未提交事务的执行结果
1.事务的隔离级别
1.更新丢失:mysql所有事务隔离级别在数据库层面上均可避免
2.脏读:是指在一个事务处理过程里读取了另一个未提交的事务中的数据
3.不可重复读:是指在对于数据库中的某行记录,一个事务范围内多次查询却 返回了不同的数据值,这是由于在查询间隔,另一个事务修改了数据并提 交了。
4.幻读:是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围 内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行, 就像产生幻觉一样,这就是发生了幻读
5.不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不 可重复读则是读取了前一事务提交的数据。
6.幻读和不可重复读都是读取了另一条已经提交的事务,不同的是不可重复读的重点是修 改,幻读的重点在于新增或者删除
2.各级别下的并发访问问题
表象:快照读(非阻塞读)–InnoDB实现了伪MVCC来避免幻读。
内在:next-key锁(X锁+gap锁),当前读情况下通过next-key锁避免幻读
快照读情况下,InnoDB 通过 mvcc 机制避免了幻读现象。而 mvcc 机制无法避免当前读 情况下出现的幻读现象。因为当前读每次读取的都是最新数据,这时如果两次查询中间 有其它事务插入数据,就会产生幻读
3.Innodb可重复读隔离级别下如何避免幻读
redo日志
4.RC、RR级别下的Innodb的非阻塞读如何实现
13.事务隔离级别以及各级别下的并发访问问题
对于读远多于写的表可以考虑使用查询缓存.
8.0版本的查询缓存功能被删了
通过整库备份+binlog进行恢复. 前提是要有定期整库备份且保存了binlog日志
2.mysql怎么恢复半个月之前的数据
3.订单量数据表越来越大导致查询缓慢,如何处理?
4.一千万条数据的单表,如何进行分页查询?
1.数据量小的表没必要建立索引,建立索引反而会增加额外的索引开销
2.数据的变更需要维护索引,因此更多的索引意味着要有更多的维护成本
3.更多的索引意味着也需要更多的空间
6.索引是建立的越多越好吗?
查看慢查询状态并设置:Show variables like '%quer%'set global show_query_log='on'set global long_query_time=1慢查询数量:show status like '%slow_queries%'
1.如何打开慢日志
看到这个的时候,查询就需要优化了。MYSQL需要进行额外的步骤来发现如何对返回的行排序。它根据连接类型以及存储排序键值和匹配条件的全部行的行指针来排序全部行
优化方法:1、修改逻辑,不在mysql中使用order by而是在应用中自己进行排序。2、使用mysql索引,将待排序的内容放到索引中,直接利用索引的排序。
Using filesort
列数据是从仅仅使用了索引中的信息而没有读取实际的行动的表返回的,这发生在对表的全部的请求列都是同一个索引的部分的时候
Using index
看到这个的时候,查询需要优化了。这里,MYSQL需要创建一个临时表来存储结果,这通常发生在对不同的列集进行ORDER BY上,而不是GROUP BY上
Using temporary
ALL:这个连接类型对于前面的每一个记录联合进行完全扫描,这一般比较糟糕,应该尽量避免
ALL
extra列:
order by会导致Using filesort,建立索引。数据量占大部分的情况下也会放弃使用索引。
group by会导致Using temporary; Using filesort,将分组字段使用索引即可解决;
由于 Using filesort是使用算法在 内存中进行排序,MySQL对于排序的记录的大小也是有做限制:max_length_for_sort_data,默认为1024。
亲测补充
2.explain调优
1.使用or关键字(但是并不是所有带or的查询都会失效,如果有两个字段,两个字段都有索引就不会失效,会走两个索引)
2.使用like关键字(但是并不是所有like查询都会失效,只有在查询时字段最左侧加%和左右侧都加%才会导致索引失效)
3.组合索引(如果查询的字段在组合索引中不是最左侧的字段,那么该组合索引是不会生效的。即左前缀原则)
4.索引列有运算符!=
5.索引列使用了函数
7.不走索引的原因是因为mysql中innodb搜索引擎的数据结构是B+树,索引查询b+树查询是通过二分法进行查询的,这就要求叶子节点上的数据必须是有序的,不然会走全表查询。
3.索引失效的原因有哪些?
联合索引将高频字段放在最左边
4.做过哪些Mysql索引相关的优化?
7.如何定位并优化慢查询sql?
问题:某个表有近千万数据,查询比较慢,如何优化?
解答:当 MySQL 单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下: 1.限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月 的时间范围内; 2.读写分离:经典的数据库拆分方案,主库负责写,从库负责读;3.通过分库分表的方式进行优化,主要有垂直拆分和水平拆分。
8.大表如何进行优化?
14.经典面试题
1.MySQL 分两层:Server 层和引擎层。区别如下:Server 层:主要做的是 MySQL 功能层面的事情。Server 层也有自己的日志,称为 binlog(归档日志)引擎层:负责存储相关的具体事宜。redo log 是 InnoDB 引擎特有的日志。
2.redo log 是物理日志,记录的是“在某个数据页上做了什么修改”;binlog 是逻辑日志,记录的是这个语句的原始逻辑,比如\"给 ID=2这一行的c字段加1\"
3.redo log 是循环写的,空间固定会用完;
4.binlog 是可以追加写入的。“追加写”是指 binlog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。
15.说一下mysql的redo log和binlog?
1.回表查询:先到普通索引上定位主键值,再到聚集索引上定位行记录,多了一次磁盘IO,它的性能较扫一遍索引树低(一般情况下)
1.一般我们自己建的索引不管是单列索引还是联合索引,都称为普通索引,相对应的另外一种就是聚簇索引。每个普通索引就对应着一颗独立的索引B+树,索引 B+ 树的节点仅仅包含了索引里的几个字段的值以及主键值。
2.根据索引树按照条件找到了需要的数据,仅仅是索引里的几个字段的值和主键值,如果用 select * 则还需要很多其他的字段,就得走一个回表操作,根据主键再到主键的聚簇索引里去找,聚簇索引的叶子节点是数据页,找到数据页里才能把一行数据的所有字段值提取出来。
2.详细说明:
16.mysql的回表?
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;
1.主从复制的流程
主库写入binlog后强制同步日志到从库,所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。
1.全同步复制
和全同步不同的是,半同步复制的逻辑是这样,从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为写操作完成。
主从复制存在数据丢失问题的解决方案:在使用过程中需要开启半同步复制;
2.半同步复制
2.主从复制数据丢失问题
1.MySQL 的高可用由互为主从的MySQL构成,平时只有主库提供服务,备库不提供服务。当主库停止服务时,服务自动切换到备库
2.主库master挂了之后,从库进行选举出新的master,其他从库挂到新的master上;
3.LVS+Keepalived:Keepalived可以进行检查心跳和动态漂移;当Master节点出现异常主服务所在keepalived会发出通知,然后slave节点的keepalived通知从节点切换为master
1.高可用(HA)架构
1.高并发下读写分离会出现数据延迟问题。
2.解决方案如下:分库分表;开启并行复制;在业务逻辑上避免
2.读写分离架构
一主一从、一主多从、多级主从、多主模式
3.主从复制的使用场景
17.mysql主从复制的原理
垂直划分数据库是根据业务进行划分,例如购物场景,可以将库中涉及商品、订单、用 户的表分别划分出成一个库,通过降低单库的大小来提高性能。同样的,分表的情况就 是将一个大表根据业务功能拆分成一个个子表,例如商品基本信息和商品描述,商品基 本信息一般会展示在商品列表,商品描述在商品详情页,可以将商品基本信息和商品描 述拆分成两张表
优点:行记录变小,数据页可以存放更多记录,在查询时减少 I/O 次数。
缺点: 主键出现冗余,需要管理冗余列; 会引起表连接 JOIN 操作,可以通过在业务服务器上进行 join 来减少数据 库压力; 依然存在单表数据量过大的问题。
1.垂直划分
水平划分是根据一定规则,例如时间或 id 序列值等进行数据的拆分。比如根据年份来 拆分不同的数据库。每个数据库结构一致,但是数据得以拆分,从而提升性能。
优点:单库(表)的数据量得以减少,提高性能;切分出的表结构相同,程序改动较少。
缺点: 分片事务一致性难以解决 跨节点 join 性能差,逻辑复杂 数据分片在扩容时需要迁移.
2.水平划分
mycat/sharingjdbc
工具
18.分库分表
4.Mysql
参考:https://mp.weixin.qq.com/s/--obCEm1sHK4yqomLVxPVQ
1.redis单线程模型
主线程负责建立接收连接请求,获取客户端socket连接放入等待队列;主线程处理完读事件之后,通过RR(Round Robin轮询)将这些连接分配给多个IO子线程;主线程阻塞等待IO子线程读取socket完毕;主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行;主线程阻塞等待IO线程将数据回写socket完毕;解除绑定,清空等待队列;
其中IO线程组的特点:IO线程要么同时在读socket,要么同时在写,不会同时既读又写;IO线程组只负责读写socket解析命令,不负责命令的处理,命令的处理仍然是main线程在顺序处理
2.redis6.0的多线程模型
1.value为String字符串
常被用来存储计数器,粉丝数等,简单的分布式锁也会用到该类型
2.使用场景:
1.字符串String
1.key - value 形式的,value 是一个map;String Key和String Value的map容器;每一个hash可以存储4294967295个键值对
2.使用场景
2.哈希 hash
1.列表
在 Redis 中可以把 list 用作栈、队列、阻塞队列
3.列表 list
1.集合,不能有重复元素,可以做点赞,收藏等
4.集合 set
1.有序集合,不能有重复元素,有序集合中的每个元素都需要指定一个分数,根据分数对元素进行升序排序。可以做排行榜
5.有序集合 zset
1.geospatial: Redis 在 3.2 推出 Geo 类型,该功能可以推算出地理位置信息,两地之间的距离
2.hyperloglog:基数:数学上集合的元素个数,是不能重复的。这个数据结构常用于统计网站的 UV。
3.bitmap: bitmap 就是通过最小的单位 bit 来进行0或者1的设置,表示某个元素对应的值或者状态。一个 bit 的值,或者是0,或者是1;也就是说一个 bit 能存储的最多信息是2。bitmap 常用于统计用户信息比如活跃粉丝和不活跃粉丝、登录和未登录、是否打卡等
6.三种特殊类型
3.redis的几种数据结构
1.概念:
2.优点:
2.有可能会产生长时间的数据丢失
3.缺点:
1.RDB方式
1.AOF可以「更好的保护数据不丢失」,一般AOF会以每隔1秒,通过后台的一个线程去执行一次fsync操作,如果redis进程挂掉,最多丢失1秒的数据
3.AOF日志文件的命令通过非常可读的方式进行记录,这个非常「适合做灾难性的误删除紧急恢复」,如果某人不小心用 flushall 命令清空了所有数据,只要这个时候还没有执行 rewrite,那么就可以将日志文件中的 flushall 删除,进行恢复
3.「数据恢复比较慢」,不适合做冷备。
2.AOF方式
1.master禁用了RDB快照时,发生了主从同步(复制初始化)操作,也会生成RDB快照,但是之后如果master发成了重启,就会用RDB快照去恢复数据,这份数据可能已经很久了,中间就会丢失数据
2.在这种一主多从的结构中,master每次和slave同步数据都要进行一次快照,从而在硬盘中生成RDB文件,会影响性能
为了解决这种问题,redis在后续的更新中也加入了无硬盘复制功能,也就是说直接通过网络发送给slave,避免了和硬盘交互,但是也是有io消耗
无硬盘复制是什么
4.redis的持久化方式
1.redis查询慢的定位以及解决
1.redis是key-value的数据库,我们可以设置redis中缓存的key的过期时间。redis的过期策略就是指当redis中缓存的key过期了,redis会如何处理。
每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除;
该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量
2.策略1:定时删除
只有当访问一个key时,才会判断该key是否已过期,过期则删除。
该策略可以最大化的节省cpu资源,但是对内存非常不友好。极端情况下可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
3.策略1:惰性删除
每隔一定的时间,会扫描一定数量的数据库的expries字典中一定数量的key,并清除其中已经过期的key。
该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
4.(expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)Redis中同时使用了惰性过期和定期过期两种过期策略。
4.策略3:定期删除
2.Redis的过期键的删除策略
1.纯内存操作
2.核心是基于非阻塞IO的多路复用机制
3.使用单线程模型来处理客户端的请求,避免了上下文切换带来的性能问题
4.自身采用的是c语言编写,有很多的优化机制,比如动态字符串sds
3.redis单线程快的原因
过期键删除策略
当服务器内存不够时 Redis 的 8 种淘汰策略
Redis 中的两种主要的淘汰算法 LRU 和 LFU
4.京东二面:内存耗尽后Redis会发生什么?
6.redis面试题补充
1.均匀过期/随机过期:缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2.永不过期
3.互斥锁:一般并发量不是特别多的时候,使用最多的解决方案就是加锁排队,加互斥锁。
4.缓存预热:比如启动服务之前先做个接口加载缓存
1.缓存雪崩
1.缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上造成数据库短时间内承受大量造求而崩掉;比如用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题
1.接口层增加校验,如用户鉴权检验,id作基础检验,id<0的直接拦截;
2.返回空对象
比如场景:与别的公司进行对接数据,接口要暴露在外网,结果另外一家公司不靠谱泄露了秘钥之类的,所以拿这个顶上,因为布隆过滤器实际上是防止大量的不存在的key来攻击
3.使用布隆过滤器
2.缓存穿透
1.加互斥锁
2.设置永不过期
3.缓存击穿
7.缓存雪崩、缓存穿透、缓存击穿
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。它具备自动故障转移、集群监控、消息通知等功能
工作原理
集群监控:负责监控redis master和slave进程是否正常工作
消息通知:如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
故障转移:如果master node挂掉了,会自动转移到slave node.上
配置中心:如果故障转移发生了,通知client 客户端新的master 地址。
哨兵组件的功能
1.第一个发现该master挂了的哨兵,向每个哨兵发送命令,让对方选举自己成为领头哨兵
2.其他哨兵如果没有选举过他人,就会将这一票投给第一个发现该master挂了的哨兵
3.第一个发现该master挂了的哨兵如果发现有超过一半哨兵投给自己,并且其数量也超过了设定的quoram参数,那么该哨兵就成了领头哨兵
4.如果多个哨兵同时参与这个选举,那么就会重复该过程,直到选出一个领头哨兵
5.选出领头哨兵后,就开始了故障修复,会从选出一个从数据库作为新的master
哨兵的选举过程
1.哨兵模式
cluster集群模式:在redis3.0版本中支持了cluster集群部署的方式,这种集群部署的方式能自动将数据进行分片,每个master上放一部分数据,提供了内置的高可用服务,即使某个master挂了,服务还可以正常地提供
优点
判断故障的逻辑其实与哨兵模式有点类似,在集群中,每个节点都会定期的向其他节点发送ping命令,通过有没有收到回复来判断其他节点是否已经下线。如果长时间没有回复,那么发起ping命令的节点就会认为目标节点疑似下线,也可以和哨兵一样称作主观下线,当然也需要集群中一定数量的节点都认为该节点下线才可以。
1.当A节点发现目标节点疑似下线,就会向集群中的其他节点散播消息,其他节点就会向目标节点发送命令,判断目标节点是否下线
2.如果集群中半数以上的节点都认为目标节点下线,就会对目标节点标记为下线,从而告诉其他节点,让目标节点在整个集群中都下线
cluster的故障恢复是怎么做的?
2.redis cluster
优点:优势在于非常简单,服务端的Redis实例彼此独立,相互无关联,每个Redis实例像单服务器一样运行,非常容易线性扩展,系统的灵活性很强
缺点:由于sharding处理放到客户端,规模进一步扩 大时给运维带来挑战。客户端sharding不支持动态增删节点。服务端Redis实例群拓扑结构有变化时,每个客户端都需要更新调整。连接不能共享,当应用规模增大时,资源浪费制约优化
3.redis sharding
8.Redis集群方案
1.当一个从数据库启动时,它会向主数据库发送一个SYNC命令,master收到后,在后台保存快照,也就是我们说的RDB持久化,当然保存快照是需要消耗时间的,并且redis是单线程的,在保存快照期间redis受到的命令会缓存起来
2.快照完成后会将缓存的命令以及快照一起打包发给slave节点,从而保证主从数据库的一致性。
3.从数据库接受到快照以及缓存的命令后会将这部分数据写入到硬盘上的临时文件当中,写入完成后会用这份文件去替换掉RDB快照文件,当然,这个操作是不会阻塞的,可以继续接收命令执行,具体原因其实就是fork了一个子进程,用子进程去完成了这些功能
因为不会阻塞,所以,这部分初始化完成后,当主数据库执行了改变数据的命令后,会异步的给slave,这也就是我们说的复制同步阶段,这个阶段会贯穿在整个中从同步的过程中,直到主从同步结束后,复制同步才会终止。
1.Redis主从复制的核心原理
2.全量复制
1.服务器收到slaveof命令,判断是否是第一次复制
2.如果是第一次复制,向主节点发送psync命令,master返回fullresync{runid}{offset},runid表示主节点的运行ID,offset表示当前主节点的复制偏移量,执行全量同步
3.部分复制
9.redis的主从复制
1.分布式锁框架redission:
2.redlock红锁
3.redis与zk在分布式架构上的异同
10.基于redis实现分布式锁
需要解决第二步执行失败引发的不一致(配合MQ)、需要解决并发问题带来的不一致(删除缓存)
想要保证数据库和缓存一致性,推荐采用「先更新数据库,再删除缓存」方案,并配合「消息队列」或「订阅变更日志」的方式来做。
延时双删的策略:写的时候:先更新数据库,再删除缓存,休眠一段时间,再次删除缓存。读的时候:先读缓存,缓存中没有再读数据库,然后回写到redis
11.如何解决redis缓存与数据库的双写不一致问题
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淘汰。
12.redis8种内存淘汰策略
LRU算法
LFU算法
13.Redis的内存淘汰算法
思路:该方案通常是在小型项目中使用,即通过一个线程定时的去扫描数据库,通过订单时间来判断是否有超时的订单,然后进行update或delete等操作
优点:简单易行,支持集群操作
缺点:对服务器内存消耗大;存在延迟,比如你每隔3分钟扫描一次,那最坏的延迟时间就是3分钟;假设你的订单有几千万条,每隔几分钟这样扫描一次,数据库损耗极大;
1.通过定时任务进行数据库轮询
思路:该方案是利用JDK自带的DelayQueue来实现,这是一个无界阻塞队列,该队列只有在延迟期满的时候才能从中获取元素,放入DelayQueue中的对象,是必须实现Delayed接口的。Poll():获取并移除队列的超时元素,没有则返回空;take():获取并移除队列的超时元素,如果没有则wait当前线程,直到有元素满足超时条件,返回结果。
缺点:服务器重启后,数据全部消失,怕宕机;集群扩展相当麻烦;因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常;代码复杂度较高;
2.jdk的延迟队列
如果当前指针指在1上面,我有一个任务需要4秒以后执行,那么这个执行的线程回调或者消息将会被放在5上。那如果需要在20秒之后执行怎么办,由于这个环形结构槽数只到8,如果要20秒,指针需要多转2圈。位置是在2圈之后的5上面(20 % 8 + 1)
我们用Netty的HashedWheelTimer来实现
缺点:服务器重启后,数据全部消失,怕宕机;集群扩展相当麻烦;因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现OOM异常
3.时间轮算法
思路2:该方案使用redis的Keyspace Notifications,中文翻译就是键空间机制,就是利用该机制可以在key失效之后,提供一个回调,实际上是redis会给客户端发送一个消息。是需要redis版本2.8以上。
4.使用redis缓存
我们可以采用rabbitMQ的延时队列。RabbitMQ具有以下两个特性,可以实现延迟队列:RabbitMQ可以针对Queue和Message设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为dead letter;lRabbitMQ的Queue可以配置x-dead-letter-exchange 和x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了deadletter,则按照这两个参数重新路由。
5.使用消息队列
14.redis怎么实现延时任务?
我们在使用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等等问题。
1.基于redis的setnx的操作
其实限流涉及的最主要的就是滑动窗口,上面也提到1-10怎么变成2-11。其实也就是起始值和末端值都各+1即可。其实限流涉及的最主要的就是滑动窗口,上面也提到1-10怎么变成2-11。其实也就是起始值和末端值都各+1即可。
例子:我们可以将请求打造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID生成,而score可以用当前时间戳表示,因为score我们可以用来计算当前时间戳之内有多少的请求数量。而zset数据结构也提供了range方法让我们可以很轻易的获取到2个时间戳内有多少请求
利弊:通过代码可以做到滑动窗口的效果,并且能保证每N秒内至多M个请求,缺点就是zset的数据结构会越来越大。实现方式相对也是比较简单的。
2.基于redis的数据结构zset
令牌桶算法提及到输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了。
也就是说我们每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反。
实现:依靠List的leftPop来获取令牌;再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以我这里还是用UUID进行了生成;针对这些限流方式我们可以在AOP或者filter中加入以上代码,用来做到接口的限流,最终保护你的网站。
3.基于redis的令牌桶算法
15.redis如何进行限流?
所谓的大key问题是某个key的value比较大,本质上是大value问题。key往往是程序可以自行设置的,value往往不受程序控制,因此可能导致value很大。
例子:某个短视频有很多用户收藏,假如有这样的数据结构:歌单和用户之间的映射关系采用redis存储:redis的key是视频ID,长度可控且很小;redis的value是个list,list包含了用户ID,用户可能很多,就导致list长度不可控
value是String类型时,size超过10KB可视为大key;value是ZSET、Hash、List、Set等集合类型时,它的成员数量超过1w个即可看做大key
1.什么是大key?
首先redis的核心工作线程是单线程,处理请求任务是串行的,前面的完成不了,后面的无法进行处理,同时也导致分布式架构中内存数据和CPU的不平衡。
执行大key命令的客户端本身,耗时明显增加,甚至超时执行大key相关读取或者删除操作时,会严重占用带宽和CPU,影响其他客户端大key本身的存储带来分布式系统中分片数据不平衡,CPU使用率也不平衡大key有时候也是热key,读取操作频繁,影响面会很大执行大key删除时,在低版本redis中可能阻塞线程
大key的影响还是很明显的,最典型的就是阻塞线程,并发量下降,导致客户端超时,服务端业务成功率下降。
影响
2.大key的影响
1.增加内存&流量&超时等指标监控:由于大key的value很大,执行读取时可能阻塞线程,这样Redis整体的qps会下降,并且客户端超时会增加,网络带宽会上涨,配置这些报警可以让我们发现大key的存在。
使用bigkeys命令以遍历的方式分析Redis实例中的所有Key,并返回整体统计信息与每个数据类型中Top1的大Key
redis-cli -h localhhost:2181 --bigkeys
2.使用bigkeys命令,在线检索工具
使用redis-rdb-tools离线分析工具来扫描RDB持久化文件,虽然实时性略差,但是完全离线对性能无影响。
3.使redis-rdb-tools,离线文件分析工具
4.可视化页面,依赖redis官方
3.如何找到大key?
当Redis版本小于4.0时,避免使用阻塞式命令KEYS,而是建议通过SCAN命令执行增量迭代扫描key,然后判断进行删除。
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
渐进式删除<4.0版本
使用UNLINK命令安全的删除大key,该命令能够以非阻塞的方式,逐步的清理传入的key
Redis UNLINK 命令类似与 DEL 命令,表示删除指定的 key,如果指定 key 不存在,命令则忽略。UNLINK 命令不同与 DEL 命令在于它是异步执行的,因此它不会阻塞。UNLINK 命令是非阻塞删除,非阻塞删除简言之,就是将删除操作放到另外一个线程去处理。
惰性删除>4.0版本
1.可删除:大key并非热key就可以在DB中查询使用,则可以在redis中删除
当vaule是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。
当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。
2.不可删除:value压缩和拆分
4.大key的治理?
16.如何解决redis的大key问题?
17.redis的缓存命中率大概多少?
5.redis
1.定义 Thread 类的子类,并重写该类的 run 方法,该 run 方法的方法体就代表了线程要 完成的任务。因此把 run()方法称为执行体。
2.创建 Thread 子类的实例,即创建了线程对象。
3.调用线程对象的 start()方法来启动该线程。
1.继承thread类进行创建
1.定义 runnable 接口的实现类,并重写该接口的 run()方法,该 run()方法的方法体同样 是该线程的线程执行体。
2.创建 Runnable 实现类的实例,并依此实例作为 Thread 的 target 来创建 Thread 对象, 该 Thread 对象才是真正的线程对象。
2.通过实现runnable接口来创建线程类
1.创建 Callable 接口的实现类,并实现 call()方法,该 call()方法将作为线程执行体,并且 有返回值
2.创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对 象封装了该 Callable 对象的 call()方法的返回值。
3.使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
4.调用 FutureTask 对象的 get()方法来获得子线程执行结束后的返回值
3.通过 Callable 和 Future 创建线程
优势是: 线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。 在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同 一份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向 对象的思想。
劣势是:编程稍微复杂,如果要访问当前线程,则必须使用 Thread.currentThread()方法。
采用实现 Runnable、Callable 接口的方式创建多线程时
优势是:编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread()方法,直接使用 this 即可获得当前线程。
劣势是:线程类已经继承了 Thread 类,所以不能再继承其他父类。
使用继承 Thread 类的方式创建多线程时
4.三种方式的对比
1.创建线程的方式有哪几种
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度 程序精度和准确性的影响。 让其他线程有机会继续执行,但它并不释放对象锁。也就是如 果有 Synchronized 同步块,其他线程仍然不能访问共享数据。注意该方法要捕获异常比如有 两个线程同时执行(没有 Synchronized),一个线程优先级为 MAX_PRIORITY,另一个为 MIN_PRIORITY,如果没有 Sleep()方法,只有高优先级的线程执行完成后,低优先级的线程 才能执行;但当高优先级的线程 sleep(5000)后,低优先级就有机会执行了
总之,sleep()可以使低优先级的线程得到执行的机会,当然也可以让同优先级、高优先级的 线程有执行的机会。
1.sleep()方法
yield()方法和 sleep()方法类似,也不会释放“锁标志”,区别在于,它没有参数,即 yield() 方法只是使当前线程重新回到可执行状态,所以执行 yield()的线程有可能在进入到可执行状 态后马上又被执行,另外 yield()方法只能使同优先级或者高优先级的线程得到执行机会,这 也和 sleep()方法不同。
2.yield()方法
Thread 的非静态方法 join()让一个线程 B“加入”到另外一个线程 A 的尾部。在 A 执行完毕 之前,B 不能工作。
Thread t = new MyThread(); t.start();t.join();保证当前线程停止执行,直到该线程所加入的线程完成为止。然而,如果它加入的线程没有 存活,则当前线程不需要停止。
3.join()方法
2.sleep() 、join()、yield()有什么区别
当创建 Thread 类的一个实例(对象)时,此线程进入新建状态(未被启动)。 例如:Thread t1=new Thread();
1.新建(new Thread)
线程已经被启动,正在等待被分配给 CPU 时间片,也就是说此时线程正在就绪队列中排队 等候得到 CPU 资源。例如:t1.start();
2.就绪(runnable)
线程获得 CPU 资源正在执行任务(run()方法),此时除非此线程自动放弃 CPU 资源或者有 优先级更高的线程进入,线程将一直运行到结束。
3.运行(running)
当线程执行完毕或被其它线程杀死,线程就进入死亡状态,这时线程不可能再进入就绪状态 等待执行。 自然终止:正常运行 run()方法后终止。 异常终止:调用**stop()**方法让一个线程终止运行。
4.死亡(dead)
由于某种原因导致正在运行的线程让出 CPU 并暂停自己的执行,即进入堵塞状态。 正在睡眠:用 sleep(long t) 方法可使线程进入睡眠方式。一个睡眠着的线程在指定的时间过 去可进入就绪状态。 正在等待:调用 wait()方法。(调用 notify()方法回到就绪状态) 被另一个线程所阻塞:调用 suspend()方法。(调用 resume()方法恢复)
5.阻塞(blocked)
3.线程的生命周期
1.不可使用stop()方法,使用stop方法会让线程戛然而止。无法让我们知道做完了什么,哪些还没有做完,也无法进行一些线程的清理工作
配合volatile关键字,volatile关键字保证了线程正确的读取变量的值。可以保证变量在不同线程之间的可见性。
2.正确的停止方式:设置退出旗标
thread.interrupt()方法初衷并不是用于停止线程,调用interrupt方法是在当前线程中打了一个停止标志,并不是真的停止线程。
实质上这种方式退出线程还是在使用退出旗标的方式
thread.interrupt()方法的作用是唤醒阻塞的线程,并抛出异常。当sleep后,线程阻塞,thread.interrupt()方法执行后,线程又被唤醒并抛出异常。因为线程被唤醒,所以设置的变量值this.isInterrupted()的值为false,while语句继续,线程继续执行。
3.thread.interrupt()方法
4.如何优雅的退出线程
notify()和notifyAll()都是Object对象用于通知处在等待该对象的线程的方法。两者的最大区别在于:
notifyAll使所有原来在该对象上等待被notify的所有线程统统退出wait的状态,变成等待该对象上的锁,一旦该对象被解锁,他们就会去竞争。notify则文明得多,它只是选择一个wait状态线程进行通知,并使它获得该对象上的锁,但不惊动其他同样在等待被该对象notify的线程们,当第一个线程运行完毕以后释放对象上的锁此时如果该对象没有再次使用notify语句,则即便该对象已经空闲,其他wait状态等待的线程由于没有得到该对象的通知,继续处在wait状态,直到这个对象发出一个notify或notifyAll,它们等待的是被notify或notifyAll,而不是锁
5.notify()与notifyAll()
1.线程
1.每次 new Thread() 新建对象,性能差;
2.线程缺乏统一管理,可能无限制的新建线程,相互竞争,有可能占用过多系统资源导致死机或OOM;
3.缺少更多的功能,如更多执行、定期执行、线程中断;
1.为什么使用线程池
1.重用存在的线程,减少对象创建、消亡的开销,性能佳,降低资源消耗;
2.可有效控制最大并发线程数,提高系统资源利用率,同时可以避免过多资源竞争,避免阻塞,提高响应速度;
3.提供定时执行、定期执行、单线程、并发数控制等功能,以达到提高线程的可管理性。
2.线程池的好处
1.创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池的最大数 量,这时线程规模将不再变化,当线程发生未预期的错误而结束时,线程池会补充一个新的 线程
核心线程数和最大线程数大小一样;没有所谓的非空闲时间,即 keepAliveTime 为 0; 阻塞队列为无界队列 LinkedBlockingQueue,可能会导致 OOM(堆积的请求处理队列可能会耗费非常大的内存,甚至OOM)
2.线程池特点:
提交任务;如果线程数少于核心线程,创建核心线程执行任务;如果线程数等于核心线程,把任务添加到 LinkedBlockingQueue 阻塞 队列;如果线程执行完任务,去阻塞队列取任务,继续执行;
3.工作流程:
适用于处理 CPU 密集型的任务,确保 CPU 在长期被工作 线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。
4.使用场景:
1.newFixedThreadPool (固定数目线程的线程池)
1.可缓存的线程池,如果线程池的容量超过了任务数,自动回收空闲线程,任务增加时可以自动添加新线程,线程池的容量不限制
核心线程数为 0 ;最大线程数为 Integer.MAX_VALUE,即无限大,可能会因为无限创建 线程,导致 OOM ;阻塞队列是 SynchronousQueue;非核心线程空闲存活时间为 60 秒;
当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会 创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由 于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不 会占用任何资源。
提交任务;因为没有核心线程,所以任务直接加到 SynchronousQueue 队列;判断是否有空闲线程,如果有,就去取出任务执行。如果没有空闲线程,就新建一个线程执行。 执行完任务的线程,还可以存活 60 秒,如果在这期间,接到任务, 可以继续活下去;否则,被销毁
用于并发执行大量短期的小任务。
2.newCachedThreadPool (可缓存线程的线程池)
1.前三种线程池的构造直接调用 ThreadPoolExecutor 的构造方法。
核心线程数为 1 ;最大线程数也为 1;阻塞队列是无界队列 LinkedBlockingQueue,可能会导致 OOM ;keepAliveTime 为 0;
2.线程池的特点:
提交任务;线程池是否有一条线程在,如果没有,新建线程执行任务 ;如果有,将任务加到阻塞队列 ;当前的唯一线程,从队列取任务,执行完一个,再继续取,一个线程 执行任务。
适用于串行执行任务的场景,一个任务一个任务地执行。
4.适用场景:
3.newSingleThreadExecutor (单线程的线程池)
最大线程数为 Integer.MAX_VALUE,也有 OOM 的风险 ;阻塞队列是 DelayedWorkQueue ;keepAliveTime 为 0 ;scheduleAtFixedRate() :按某种速率周期执行 ;scheduleWithFixedDelay():在某个延迟后执行;
1.线程池的特点:
1.线程从 DelayQueue 中获取已到期的 ScheduledFutureTask (DelayQueue.take())。到期任务是指 ScheduledFutureTask 的 time 大于等于当前时间。
2.线程执行这个 ScheduledFutureTask。
3.线程修改 ScheduledFutureTask 的 time 变量为下次将要被执行的时 间。
4.线程把这个修改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。
2.工作流程:
周期性执行任务的场景,需要限制线程数量的场景
3.适用场景:
4.newScheduledThreadPool (定时及周期执行的线程池)
注:阿里爸爸警告写的很明确“线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的行规则,规避资源耗尽的风险。”
3.创建线程池的几种方式
1.corePoolSize:指定了线程池中的线程数量(即核心线程数)。
2.maximumPoolSize:指定了线程池中的最大线程数量。
3.keepAliveTime:当线程池线程数量超过corePoolSize时,多余的空闲线程会在多长时间内被销毁。
4.unit:keepAliveTime的时间单位。
5.workQueue:任务队列,被提交但尚未被执行的任务。参数workQueue是指提交但未执行的任务队列。若当前线程池中线程数>=corePoolSize时,就会尝试将任务添加到任务队列中
6.threadFactory:线程工厂,用于创建线程,一般用默认的即可。即Executors类的静态方法defaultThreadFactory()。
7.handler:拒绝策略,当任务太多来不及处理,如何拒绝任务。
2.线程池的7大参数
1.线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2.当调用 execute() 方法添加一个任务时,线程池会做如下判断:a) 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;b) 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;c) 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;d) 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
3.当一个线程完成任务时,它会从队列中取下一个任务来执行。
4.当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小
注意:关系2中C是针对的有界队列,无界队列永远都不会满,所以只有前2种关系。
3.线程池执行过程
参数workQueue是指提交但未执行的任务队列。若当前线程池中线程数>=corePoolSize时,就会尝试将任务添加到任务队列中
1.SynchronousQueue:直接提交队列。SynchronousQueue没有容量,所以实际上提交的任务不会被添加到任务队列,总是将新任务提交给线程执行,如果没有空闲的线程,则尝试创建新的线程,如果线程数量已经达到最大值(maximumPoolSize),则执行拒绝策略
2.LinkedBlockingQueue:无界的任务队列。LinkedBlockingQueue有默认的容量大小为:Integer.MAX_VALUE,当然也可以传入指定的容量大小;当有新的任务来到时,若系统的线程数小于corePoolSize,线程池会创建新的线程执行任务;当系统的线程数量等于corePoolSize后,因为是无界的任务队列,总是能成功将任务添加到任务队列中,所以线程数量不再增加。若任务创建的速度远大于任务处理的速度,无界队列会快速增长,直到内存耗尽LinkedBlockingQueue:可以进行设置大小,比如5000,就变为了有界队列
3.ArrayBlockingQueue有界队列:当使用有限的最大线程数时,有界队列(如ArrayBlockingQueue)可以防止资源耗尽,但是难以调整和控制。队列大小和线程池大小可以相互作用:使用大的队列和小的线程数可以减少CPU使用率、系统资源和上下文切换的开销,但是会导致吞吐量变低,如果任务频繁地阻塞(例如被I/O限制),系统就能为更多的线程调度执行时间。使用小的队列通常需要更多的线程数,这样可以最大化CPU使用率,但可能会需要更大的调度开销,从而降低吞吐量。
4.DelayQueue:DelayQueue(延迟队列)是一个任务定时周期的延迟 执行的队列。根据指定的执行时间从小到大排序,否则根据插入到队 列的先后排序。newScheduledThreadPool 线程池使用了这个队列。
5.PriorityBlockingQueue:PriorityBlockingQueue(优先级队列)是具 有优先级的无界阻塞队列
4.线程池的入队策略
线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列也已经排满了,再也塞不下新任务了。这时候我们就需要拒绝策略机制合理的处理这个问题。
直接抛出异常,阻止系统正常运行。默认的拒绝策略。
使用场景:这个就没有特殊的场景了,但是一点要正确处理抛出的异常。但是注意,ExecutorService中的线程池实例队列都是无界的,也就是说把内存撑爆了都不会触发拒绝策略。当自己自定义线程池实例时,使用这个策略一定要处理好触发策略时抛的异常,因为他会打断当前的执行流程。
1.AbortPolicy :
调用主线程执行被拒绝的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
使用场景:一般在不允许失败的、对性能要求不高、并发量较小的场景下使用,因为线程池一般情况下不会关闭,也就是提交的任务一定会被运行,但是由于是调用者线程自己执行的,当多次提交任务时,就会阻塞后续任务执行,性能和效率自然就慢了
2.CallerRunsPolicy:
该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。
使用场景:如果你提交的任务无关紧要,你就可以使用它 。因为它就是个空实现,会悄无声息的吞噬你的的任务。所以这个策略基本上不用了
3.DiscardPolicy :
丢弃最老的(等待时间最长的)一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
使用场景:这个策略还是会丢弃任务,丢弃时也是毫无声息,但是特点是丢弃的是老的未执行的任务,而且是待执行优先级较高的任务。
4.DiscardOldestPolicy :
注:以上内置拒绝策略均实现了 RejectedExecutionHandler 接口,若以上策略仍无法满足实际需要,完全可以自己扩展 RejectedExecutionHandler 接口。通过自定义线程池拒绝策略,将拒绝的线程进行业务补偿处理。
当dubbo的工作线程触发了线程拒绝后,主要做了三个事情,原则就是尽量让使用者清楚触发线程拒绝策略的真实原因。
1)输出了一条警告级别的日志,日志内容为线程池的详细设置参数,以及线程池当前的状态,还有当前拒绝任务的一些详细信息。可以说,这条日志,使用dubbo的有过生产运维经验的或多或少是见过的,这个日志简直就是日志打印的典范,其他的日志打印的典范还有spring。得益于这么详细的日志,可以很容易定位到问题所在2)输出当前线程堆栈详情,这个太有用了,当你通过上面的日志信息还不能定位问题时,案发现场的dump线程上下文信息就是你发现问题的救命稻草。3)继续抛出拒绝执行异常,使本次任务失败,这个继承了JDK默认拒绝策略的特性
dubbo中的线程拒绝策略
Netty中的实现很像JDK中的CallerRunsPolicy,舍不得丢弃任务。不同的是,CallerRunsPolicy是直接在调用主线程执行的任务。而 Netty是新建了一个线程来处理的。所以,Netty的实现相较于调用者执行策略的使用面就可以扩展到支持高效率高性能的场景了。但是也要注意一点,Netty的实现里,在创建线程时未做任何的判断约束,也就是说只要系统还有资源就会创建新的线程来处理,直到new不出新的线程了,才会抛创建线程失败的异常。
Netty中的线程池拒绝策略
activeMq中的策略属于最大努力执行任务型,当触发拒绝策略时,在尝试一分钟的时间重新将任务塞进任务队列,当一分钟超时还没成功时,就抛出异常
activeMq中的线程池拒绝策略
pinpoint的拒绝策略实现很有特点,和其他的实现都不同。他定义了一个拒绝策略链,包装了一个拒绝策略列表,当触发拒绝策略时,会将策略链中的rejectedExecution依次执行一遍。
pinpoint中的线程池拒绝策略
5.线程池的拒绝策略
直接实现RejectedExecutionHandler的rejectedExecution方法即可
注:最佳自定义创建线程池,队列有界,maximumPoolSize有限,使用任务拒绝策略。如果队列无界,服务不了的任务总是会排队,消耗内存,甚至引发内存不足异常。如果队列有界但maximumPoolSize无线,可能会创建过多线程,占内存和CPU
4.自定义线程池
System.out.println(Runtime.getRuntime().availableProcessors()) //查看CPU核数
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:一般公式:CPU核数+1个线程的线程池
1.CPU密集型
由于IO密集型任务线程并不是一直执行任务,则应配置尽可能多的线程,如:CPU核数*2
IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:参考公式: CPU核数 / 1 -阻塞系数 阻塞系数在0.8~0.9之间
2.IO密集型
1.动态调参:支持线程池参数动态调整、界面化操作;包括修改线程池核心大小、最大核心大小、队列长度等;参数修改后及时生效
2.任务监控:支持应用粒度、线程池粒度、任务粒度的Transaction监控;可以看到线程池的任务执行情况、最大任务执行时间、平均任务执行时间、95/99线等。
3.负载告警:线程池队列任务积压到一定值的时候会通过大象(美团内部通讯工具)告知应用开发负责人;当线程池负载数达到一定阈值的时候会通过大象告知应用开发负责人。
4.操作监控:创建/修改和删除线程池都会通知到应用的开发负责人。
5.操作日志:可以查看线程池参数的修改记录,谁在什么时候修改了线程池参数、修改前的参数值是什么。
6.权限校验:只有应用开发负责人才能够修改应用的线程池参数。
3.动态监控调参(美团方案)
5.如何设置线程池的参数
核心线程数默认是不会被回收的;
如果需要回收核心线程数,需要调用下面的方法:allowCoreThreadTimeOut 该值默认为 false
1.核心线程数会被回收吗?需要什么设置?
线程池被创建后如果没有任务过来,里面是不会有线程的。如果需要预热的话可以调用下面的两个方法:全部启动:仅启动一个:
2.线程池被创建后里面有线程吗?如果没有的话,你知道有什么方法对线程池进行预热吗?
答案是:一个都不用,我们生产只使用自定义的
3.你在工作中的单一的/固定的/可变的,你这三种创建线程池的方法,你用哪个多?超级大坑
submit()方法用于提交需要返回值的任务。线程池会返回一个 future 类 型的对象,通过这个 future 对象可以判断任务是否执行成功,并且可 以通过 future 的 get()方法来获取返回值
4.线程池提交 execute 和 submit 有什么区别?
可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们 的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来 中断线程,所以无法响应中断的任务可能永远无法终止
shutdownNow()能立即停止线程池,正在跑的和正在等待的任务都停 下了。这样做立即生效,但是风险也比较大。
shutdown()只是关闭了提交通道,用 submit()是无效的;而内部的任 务该怎么跑还是怎么跑,跑完再彻底停止线程池。
5.线程池怎么关闭知道吗?
6.补充面试题
2.线程池
场景 1:协调子线程结束动作:等待所有子线程运行结束
CountDownLatch 允许一个或多个线程等待其他线程完成操作。 例如,我们很多人喜欢玩的王者荣耀,开黑的时候,得等所有人都上线之 后,才能开打。
CountDownLatch是同步工具类之一,可以指定一个计数值,在并发环境下由线程进行减1操作,当计数值变为0之后,被await方法阻塞的线程将会唤醒,实现线程间的同步。
1.CountDownLatch(倒计数器)
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要 做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直 到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会 继续运行。
CountDownLatch 类似,都可以协调多线程的结束动作,在它们结束后 都可以执行特定动作
2.CyclicBarrier(同步屏障)
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协 调各个线程,以保证合理的使用公共资源
Semaphore 其实和锁有点类似,它一般用于控制对某组资源的访问权限。
3.Semaphore(信号量)
CountDownLatch 是一次性的,而 CyclicBarrier 则可以多次设置屏障, 实现重复利用;
CountDownLatch 中的各个子线程不可以等待其他线程,只能完成自 己的任务;而 CyclicBarrier 中的各个线程可以等待其他线程
在 CountDownLatch 中,如果某个线程出现 问题,其他线程不受影响;在 CyclicBarrier 中,如果某个线程遇到了中断、超时等问 题时,则处于 await 的线程都会出现问题
CyclicBarrier 和 CountDownLatch 有什么区别?
3.并发工具类
ArrayBlockingQueue :由数组结构组成的有界阻塞队列。
1.ArrayBlockingQueue
LinkedBlockingQueue :由链表结构组成的有界阻塞队列。
2.LinkedBlockingQueue
PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
3.PriorityBlockingQueue
DelayQueue:使用优先级队列实现的无界阻塞队列。
4.DelayQueue
SynchronousQueue:不存储元素的阻塞队列。
5.SynchronousQueue
LinkedTransferQueue:由链表结构组成的无界阻塞队列。
6.LinkedTransferQueue
LinkedBlockingDeque:由链表结构组成的双向阻塞队列
7.LinkedBlockingDeque
4.阻塞队列
synchronized 经常用的,用来保证代码的原子性。
修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当 前对象实例的锁
修饰静态方法:也就是给当前类加锁,会作⽤于类的所有对象实例 , 进⼊同步代码前要获得当前 class 的锁
修饰代码块 :指定加锁对象,对给定对象/类加锁。 synchronized(this|object) 表示进⼊同步代码库前要获得给定对象的 锁
1.synchronized如何使用
synchronized 修饰代码块时,JVM 采用 monitorenter、monitorexit 两 个指令来实现同步,monitorenter 指令指向同步代码块的开始位 置, monitorexit 指令则指向同步代码块的结束位置。
synchronized 修饰同步方法时,JVM 采用 ACC_SYNCHRONIZED 标记符来实 现同步,这个标识指明了该方法是一个同步方法。
synchronized底层实际上是CAS自旋锁。
2.synchronized实现原理
1.synchronized
1.Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现
2.synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3.Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4.通过Lock可以知道有没有成功获取锁,而synchronized却无法办到
5.Lock可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择
2.synchronized和lock区别
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 不用手动释放锁。
3.说说 synchronized 和 ReentrantLock 的区别?
6.使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域。
4.volatile和synchronized区别
ThreaLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间的一些公共变量的传递的复杂度。用于解决数据库连接、session管理等。
5.ThreadLocal
new ReentrantLock()构造函数默认创建的是非公平锁 NonfairSync。
ReentantLock 继承接口 Lock 并实现了接口中定义的方法,他是一种可重入锁,除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁请求、定时锁等避免多线程死锁的方法。
6.ReentrantLock()
公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列 中排队,队列中的第一个线程才能获得锁 .
公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非 公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞, CPU 唤醒阻塞线程的开销比非公平锁大
公平锁
非公平锁是多个线程加锁时直接尝试获取锁,获取不到才会到等待队列 的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接 获取到锁 。
非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为 线程有几率不阻塞直接获得锁,CPU 不必唤醒所有线程。缺点是处于等 待队列中的线程可能会饿死,或者等很久才会获得锁
默认创建的对象 lock()的时候: 如果锁当前没有被其它线程占用,并且当前线程之前没有获取过该锁, 则当前线程会获取到该锁,然后设置当前锁的拥有者为当前线程,并设置 AQS 的状态值为 1 ,然后直接返回。如果当前线程之前己经获取过 该锁,则这次只是简单地把 AQS 的状态值加 1 后返回。
如果该锁己经被其他线程持有,非公平锁会尝试去获取锁,获取失败的 话,则调用该方法线程会被放入 AQS 队列阻塞挂起
非公平锁
非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果 这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方 法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否 有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程 长期处于饥饿状态。
区别
7.公平锁与非公平锁
1.CAS 叫做 CompareAndSwap,⽐较并交换,主要是通过处理器的指令来保 证操作的原⼦性的。
2.CAS 指令包含 3 个参数:共享变量的内存地址 A、预期的值 B 和共享变 量的新值 C。 只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更 新为新值 C。作为一条 CPU 指令,CAS 指令本身是能够保证原子性的
ABA问题:并发环境下,假设初始条件是 A,去修改数据时,发现是 A 就会执行修改。 但是看到的虽然是 A,中间可能发生了 A 变 B,B 又变回 A 的情况。此时 A 已经非彼 A,数据即使成功修改,也可能有问题。
解决方案:加版本号、时间戳等
ABA问题
问题:自旋 CAS,如果一直循环执行,一直不成功,会给 CPU 带来非常大的执行 开销。
解决方案:在 Java 中,很多使用自旋 CAS 的地方,会有一个自旋次数的限制,超过一 定次数,就停止自旋
循环性能开销
问题:CAS 保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无法直接保证操作的原子性的。
解决方案:可以考虑改用锁来保证操作的原子性;可以考虑合并多个变量,将多个变量封装成一个对象,通过 AtomicReference 来保证原子性。
只能保证一个变量的原子操作
3.CAS产生的三大问题
8.CAS
Java 对象头里,有一块结构,叫 Mark Word 标记字段,这块结构会随着锁的 状态变化而变化。
Mark Word 存储对象自身的运行数据,如哈希码、GC 分代年龄、锁状态标 志、偏向时间戳(Epoch) 等。
1.锁的状态
在 JDK1.6 之前,synchronized 的实现直接调用 ObjectMonitor 的 enter 和 exit,这种锁被称之为重量级锁。从 JDK6 开始,HotSpot 虚拟机开发团队 对 Java 中的锁进行优化,如增加了适应性自旋、锁消除、锁粗化、轻量级 锁和偏向锁等优化策略,提升了 synchronized 的性能
偏向锁:在无竞争的情况下,只是在 Mark Word 里存储当前线程指针, CAS 操作都不做。 轻量级锁:在没有多线程竞争时,相对重量级锁,减少操作系统互斥量 带来的性能消耗。但是,如果存在锁竞争,除了互斥量本身开销,还额 外有 CAS 操作的开销。 自旋锁:减少不必要的 CPU 上下文切换。在轻量级锁升级为重量级锁时, 就使用了自旋加锁的方式 锁粗化:将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更 大的锁。 锁消除:虚拟机即时编译器在运行时,对一些代码上要求同步,但是被 检测到不可能存在共享数据竞争的锁进行消除。
2.synchronized 优化了解吗?
1.锁升级方向:无锁-->偏向锁---> 轻量级锁---->重量级锁,这个方向基本上 是不可逆的。
3.锁升级的过程是什么样的?
9.锁升级
独占锁模式下,每次只能有一个线程能持有锁,ReentrantLock 就是以独占方式实现的互斥锁。独占锁是一种悲观保守的加锁策略,它避免了读/读冲突,如果某个只读线程获取锁,则其他读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。
1.独占锁
共享锁则允许多个线程同时获取锁,并发访问 共享资源,如:ReadWriteLock。共享锁则是一种乐观锁,它放宽了加锁策略,允许多个执行读操作的线程同时访问共享资源。
如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁
如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁!
ReadWriteLock 读写锁
2.共享锁
10.共享锁+独占锁
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作
java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败
11.乐观锁+悲观锁
1.使用循环原子类,例如 AtomicInteger,实现 i++原子操作
2.使用 juc 包下的锁,如 ReentrantLock ,对 i++操作加锁 lock.lock() 来实现原子性
3.使用 synchronized,对 i++操作加锁
1.Java 有哪些保证原子性的方法?如何保证多线程 下 i++ 结果正确?
1.sleep 来自 Thread 类,和 wait 来自 Object 类。
2.最主要是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3.wait,notify和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用(使用范围)
4.sleep 必须捕获异常,而 wait , notify 和 notifyAll 不需要捕获异常
2.wait与sleep的区别
12.面试题补充
5.锁
6.JUC框架图
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。AQS的全称为(AbstractQueuedSynchronizer),这个类在java.util.concurrent.locks包
1.AQS是什么?
用大白话来说,AQS就是基于CLH队列(CLH同步队列是一个FIFO双向队列),用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。
注意:AQS是自旋锁:**在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,直到被其他线程获取成功
实现了AQS的锁有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物
AQS 定义了两种资源共享方式:1.Exclusive:独占,只有一个线程能执行,如ReentrantLock2.Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
2.AQS的原理
AQS管理一个关于状态信息的单一整数,该整数可以表现任何状态。比如, Semaphore 用它来表现剩余的许可数,ReentrantLock 用它来表现拥有它的线程已经请求了多少次锁;FutureTask 用它来表现任务的状态(尚未开始、运行、完成和取消)
3.AQS用法
7.AQS
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。t.join(); //调用join方法,等待线程t执行完毕t.join(1000); //等待 t 线程,等待时间是1000毫秒。
1.使用join
CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行。它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0。另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒。这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条件之后再执行
2.使用CountDownLatch(倒计数器)
FutureTask一个可取消的异步计算,FutureTask 实现了Future的基本方法,提空 start cancel 操作,可以查询计算是否已经完成,并且可以获取计算的结果。结果只可以在计算完成之后获取,get方法会阻塞当计算没有完成的时候,一旦计算已经完成,那么计算就不能再次启动或是取消。一个FutureTask 可以用来包装一个 Callable 或是一个runnable对象。因为FurtureTask实现了Runnable方法,所以一个 FutureTask可以提交(submit)给一个Excutor执行(excution).
3.使用CachedThreadPool
阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。
4.使用blockingQueue
5.使用单个线程池
1.如何保证多线程,每个线程顺序执行?
所谓死锁:是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进
1.什么是死锁?
互斥条件:⼀个资源每次只能被⼀个线程使⽤请求和保持条件:⼀个线程在阻塞等待某个资源时,不释放已占有资源不剥夺条件:⼀个线程已经获得的资源,在未使⽤完之前,不能被强⾏剥夺环路等待条件:若⼲线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满⾜其中某⼀个条件即可。⽽其中前3个 条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
2.产生死锁的必要条件:
1. 要注意加锁顺序,保证每个线程按同样的顺序进⾏加锁2. 要注意加锁时限,可以针对所设置⼀个超时时间3. 要注意死锁检查,这是⼀种预防机制,确保在第⼀时间发现死锁并进⾏解决
3.在开发过程中如何解决
2.Java死锁如何避免?
2.使用synchronized配合wait()和notifyAll()
3.Java多个线程顺序打印数字
8.面试补充
6.多线程与高并发
1.API 使用简单,开发门槛低;
2.功能强大,预置了多种编解码功能,支持多种主流协议;
3.性能高,通过与其它业界主流的 NIO 框架对比,Netty 的综合性能最优;
4.经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络游戏、企业应 用、电信软件等众多行业得到成功商用;
1.为什么选择netty
它会导致 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的轮询结果为空,也没有wakeup或新消息处理,则会一直空轮询,占用CPU,导致CPU使用率100%,
Selector BUG出现的原因:
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。
Netty的解决办法:
2.原生的 NIO 在 JDK 1.7 版本存在 epoll bug
1.客户端像服务端发送消息,服务端不知道客户端每次发送消息的数据大小,服务端可能出现把一个数据包拆成两个数据包进行读取这种被称为拆包, 也有可能把两个数据包当成一个数据包读取这种被称为粘包
1.一次性的读取到dataA+dataB,这就是粘包
3.服务端读到两个数据包。分别为dataA+ dataB_ 1和dataB_ 2。dataA发送的时候页带把dataB也发送了一部分,dataB剩下一部分单独发送,和2-样也是拆包
4.服务端读到两个数据包,分别为dataA和dataB 正常情况
2.例子:客户端像服务端发送了两个数据包dataA和dataB,但是服务端收到的包可能有4种情况
1.应用程序写入的数据大于套接字缓冲区大小,这将会发生拆包
2.应用程序写入数据小于套接字缓冲区大小,网卡应用多次写入的数据发送到网络上,这将会发生粘包
4.接收方法不及时读取套接字缓冲区数据,这将发生粘包。
3.为什么会发生粘包拆包?
1.发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每-个数据包的实际长度了。
2.发送端将每个数据包封装为固定长度不够的可以通过补0填充),这样接收端每次从接收缓中区中读取固定长度的数据就自然而然的把每 个数据包拆分开来。
4.粘包拆包的解决方案?
3.什么是tcp的粘包拆包
1. 作为 NIO 服务器,接受客户端 TCP 连接,作为 NIO 客户端,向服务端发起 TCP 连接
2. 服务端读请求数据并响应,客户端写请求并读取响应场景:对应小业务则适合,编码简单,对于高负载,高并发不合适。一个 NIO 线程处理太多请求,负载很高,并且响应变慢,导致大量请求超时,万一线程挂了,则不可用
1.Reactor 单线程模型
一个 Acceptor线程,一组 NIO 线程,一般是使用自带线程池,包含一个任务队列和多个可用线程场景:可满足大多数场景,当Acceptor需要做负责操作的时候,比如认证等耗时操作 ,在高并发情况下也会有性能问题
2.Reactor 多线程模型
Acceptor不在是一个线程,而是一组 NIO 线程,IO 线程也是一组 NIO 线程,这样就是 2 个线程池去处理接入和处理 IO场景:满足目前大部分场景,也是 Netty推荐使用的线程模型BossGroup 处理连接的WorkGroup 处理业务的
3.主从 Reactor 多线程模型
4.netty的线程模型
Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端 Channel,由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 IO 阻塞导致的线程挂起。
1.IO多路复用通讯方式
由于 Netty 采用了异步通信模式,一个 IO 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 IO 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
2.异步通讯NIO
Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝
Netty 提供了组合 Buffer 对象,可以聚合多个 ByteBuffer 对象,用户可以像操作一个 Buffer 那样方便的对组合 Buffer 进行操作,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的Buffer。
Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环 write 方式导致的内存拷贝问题
3.零拷贝机制(DIRECT BUFFERS 使用堆外直接内存)
随着 JVM 虚拟机和 JIT 即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区 Buffer,情况却稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制
4.内存池(基于内存池的缓冲区重用机制)
5.高效的reactor线程模型
Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎 CPU 利用率不高,并发程度不够。但是,通过调整 NIO 线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优
6.无锁设计、线程绑定
Netty 默认提供了对 Google Protobuf 的支持,通过扩展 Netty 的编解码接口,用户可以实现其它的高性能序列化框架,例如 Thrift 的压缩二进制编解码框架
7.高性能的序列化框架
5.netty的高性能设计
1.时间复杂度为O(n),有IO事件发生了,却不知道用哪个流,只能进行无差别的轮询所有的流,找出读或者写的流,再对他们进行操作
2.同时每次调用select ,都需要任内核态遍历所有传递过来的fd,这个开销在fd很多时也很大。
3.select 支持的文件描述得太小,默认:1024/ 32位机器,单个进程能打开的最大连接数由FD_SetSize来定义。
4.select模型是水平触发,应用程序如果没有完成对一个已经就绪的文中描述符进行IO操作,那么之后每次进行Select调用还是会将这些文件描述符通知进程
2.缺点:
1.创建文件描述符集合fd_set,可以关注上面的读、写、异常事件,要创建3个fd_set,分别对这三种事件进行监听收集
2.调用select等待事件的发生
3.轮询select所有fd_set中的每一个fd,检查是否有相应的事件发生,如果有,则进行处理
3.流程步骤:
4.select函数
1.select模型
1.事件复杂度为O(n).poll的本质与select没有区别,他将用户传入的数据拷贝到内核空间,然后查询每个fd对应的设备状态,但是没有最大连接数的限制,原因是他是基于链表来存储。注意:windows平台不支持pool。
1.大量的fd复制于用户态与内核态之间,而不管这样的复制有没有意义;
2.水平触发,如果报告了fd,没有被处理,那么下次poll时还会继续触发报告;
1.创建描述符集合,设置关注的事件
2.调用poll()函数,等待事件的发生,类似select,poll也可以设置等待时间;
3.轮询描述符事件,检查事件,处理事件;
3.流程模型:
4.poll函数:
1.select需要为读、写、异常事件分别建立一个文件描述符集合,最后轮询的时候需要分别轮询这三个集合;而poll只需要创建一个文件描述符集合,在每个文件描述符对应的结构上分别设置读、写、异常事件,最后轮询的时候,同时检查这三个事件
2.poll没有最大连接数的限制,原因是基于链表进行存储;
5.select与pool的区别:
2.pool模型
1.时间复杂度为O(1);
1.可以说没有最大并发数的限制,能打开的上限远远大于1024(1G内存上能监听的端口约为10万个端口);
2.效率提升:不是轮询的方式,不会随着fd数目的增加而效率下降,只有活跃的用户的FD才会调用callback函数,即epool最大的优点在于只管活跃的连接数,而跟连接总数无关;
3.内存拷贝:使用了零拷贝技术。epoo通过用户态与内核态共享一块内存,来实现消息的传递;利用mmap()文件映射内存加速内核空间的消息传递,即epool使用mmap减少复制开销;
4.epool保证了每个FD在整个拷贝过程中只拷贝一次,select、pool每次调用都要把FD集合从用户态拷贝到内核态一次。
2.特点:
3.通过调用epool_wait来等待内核通知事件发生,进而进行事件的处理;epool_wait()
3.epool模型
6.IO多路复用
1.BIO(阻塞IO)
2.NIO(非阻塞IO)
4.
5.
7.IO模式
8.netty原码分析
在 linux系统上,AIO 的底层实现仍然使用 epoll,与 NIO 相同,因此在性能上没有明显的优势Netty 整体架构是 reactor 模型,采用 epoll机制,IO 多路复用,同步非阻塞模型Netty是基于 Java NIO 类库实现的异步通讯框架特点: 异步非阻塞,基于事件驱动,性能高,高可靠性,高可定制性。
1.Netty 使用 NIO 而不是 AIO
9.netty面试题补充
7.netty4.X
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.并发度最好的是:消费者组里的个数与主题数一样的时候
注:
1.kafka架构图
通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。
考虑一下你负责的系统中是否有类似的场景,就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦,也是可以的
比如很多的消息需要进行埋点,同步给其他的部门
1.解耦
场景:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。
如果使用 MQ,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了
2.异步
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
3.削峰填谷
即使一部分挂掉,不影响其他的使用
4.可恢复性
kafka能够保证一个patition内的消息的有序性
5.顺序保证
2.消息队列的好处
1.点对点模式(一对一)
Kafka是基于发布-订阅模式
2.发布-订阅模式(一对多)
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 阻塞直到新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可 以批量发送)。
3.consumer是推还是拉?
3.消息队列的传递模式
1.Kafka中消息是以 topic 进行分类的,生产者生产消息,消费者消费消息,都是面向 topic 的
2.topic 是逻辑上的概念,而 partition 是物理上的概念,每个 partition 对应于一个 log 文件,该 log 文件中存储的就是 producer 生产的数据。Producer生产的数据会被不断追加到该 log 文件末端,且每条数据都有自己的 offset。消费者组中的每个消费者,都会实时记录自己 消费到了哪个 offset,以便出错恢复时,从上次的位置继续消费
4.kafka的工作流程
1.一个topic分为多个partition
2.一个partition分为多个segment(顺序读写、分段命令、二分查找)
3.一个segment对应两个文件:.log文件、index文件(分段索引、稀疏存储)
1.文件存储机制
2.由于生产者生产的消息会不断追加到log文件末尾,为防止log文件过大导致数据定位效率低下,Kafka采取了分片和索引机制,将每个partition 分为多个segment。每个segment对应两个文件一-“index\
3.index和log文件以当前segment的第一条消息的 offset 命名。
4.index文件存储大量的索引信息,“ .log”文件存储大量的数据,索引文件中的元数据指向对应数据文件中message的物理偏移地址。
5.kafka的文件存储机制
我们需要将producer发送的数据封装成一- 个ProducerRecord对象。
1.指明partition 的情况下,直接将指明的值直接作为partiton 值;
由于消息 topic 由多个 partition 组成,且 partition 会均衡分布到不同 broker 上,因此,为了有效利用 broker 集群的性能,提高消息的吞吐量,producer 可以通过随机或者 hash 等方式,将消息平均发送到多个 partition 上,以实现负载均衡。
4.负载均衡(partition 会均衡分布到不同 broker 上)
是提高消息吞吐量重要的方式,Producer 端可以在内存中合并多条消息后,以一次请求的方式发送了批量的消息给 broker,从而大大减少 broker 存储消息的 IO 操作次数。但也一定程度上影响了消息的实时性,相当于以时延代价,换取更好的吞吐量。
5.批量发送
Producer 端可以通过 GZIP 或 Snappy 格式对消息集合进行压缩。Producer 端进行压缩之后,在Consumer 端需进行解压。压缩的好处就是减少传输的数据量,减轻对网络传输的压力,在对大数据处理上,瓶颈往往体现在网络上而不是 CPU(压缩和解压会耗掉部分 CPU 资源)。
6.压缩(GZIP或Snappy)
1.kafka生产者分区策略
2.何时发送ack?
优点:延迟低
缺点:选举新的leader时,容忍n台节点的故障,需要2n+1个副本
1.半数以上的follower同步完成,即可发送ack
优点:选举新的leader时,容忍n台节点的故障,需要n+1个副本
缺点:延迟高
2.全部的follower同步完成,才可以发送ack
1.同样为了容忍n台节点的故障,第- -种方案需要2n+1个副本,而第二种方案只需要n+1个副本,而Kafka的每个分区都有大量的数据,第-种方案会造成大量数据的冗余。
2.虽然第二种方案的网络延迟会比较高,但网络延迟对Kafka的影响较小。
3.kafka选择的是第二种方案(全部同步)
3.多少个follower同步完成之后发送ack?
3.Replica.lag.time.max.ms、Replica.lag.time.max.messages低版本的kafka使用时间跟条数2个条件0.9之后高版本的kafka只使用时间条件:在延迟时间内就加入进来,在延迟时间外就剔出。默认延时时间为10秒(如果不能及时的发送抓取请求或者不能及时的消费到最后一个消息,这个follower会被leader移出ISR)
4.ISR
对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等ISR中的follower全部接收成功。所以Kafka为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,选择以下的配置。request.required.asks=0
1.acks=0:
2.acks=1:
3.acks=-1(All):
5.ack应答机制
2.kafka生产者数据可靠性如何保证
1.LEO(Log End Offset):指的是每个副本最大的offset,也就是最后一个offset
2.HW(High Watermark):指的是消费者能见到的最大的offset,ISR队列中最小的LEO
3.follower故障
注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
4.leader故障
3.故障处理
At Least once语义可以保证数据不丢失,但是不能保证数据不重复
1.At Least Once语义
At Most Once语义可以保证数据不重复但是不能保证不丢失,但是对于一些非常重要的信息,比如说交易数据,下游数据消费者要求数据即不重复也不丢失,即Exactly Once语义
2.At Most Once语义
4.但是PID重启就会变化,同时不同的Partition也具有不同主键,所以幂等性无法保证跨分区跨会话的Exactly Once。
总结:1.需要设置acks为-1,保证了生产者到server之间数据不丢失,不能保证数据不重复;2.启用幂等性,这样在上游进行了去重,进而保证了数据不重复
3.Exactly Once语义
4.Exactly Once语义(精准一次性)
kafka生产者数据不均匀,不能均匀分配数据到每个分区,这样容易造成消费积压,造成这个的原因是因为生产者使用了固定的key导致
5.kafka生产数据不能均匀到每个分区怎么处理?
6.kafka生产者product
1.consumer采用pull(拉)模式从broker中读取数据
1.消费方式
RoundRobinAssignor的分配策略是将消费组内订阅的所有Topic的分区及所有消费者进行排序后尽量均衡的分配(RangeAssignor是针对单个Topic的分区进行排序分配的)。如果消费组内,消费者订阅的Topic列表是相同的(每个消费者都订阅了相同的Topic),那么分配结果是尽量均衡的(消费者之间分配到的分区数的差值不会超过1)。如果订阅的Topic列表是不同的,那么分配结果是不保证“尽量均衡”的,因为某些消费者不参与一些Topic的分配
1.RoundRobin(轮询)
RangeAssignor对每个Topic进行独立的分区分配。对于每一个Topic,首先对分区按照分区ID进行排序,然后订阅这个Topic的消费组的消费者再进行排序,之后尽量均衡的将分区分配给消费者。这里只能是尽量均衡,因为分区数可能无法被消费者数量整除,那么有一些消费者就会多分配到一些分区
注:这种分配方式明显的一个问题是随着消费者订阅的Topic的数量的增加,不均衡的问题会越来越严重,比如上图中4个分区3个消费者的场景,C0会多分配一个分区。如果此时再订阅一个分区数为4的Topic,那么C0又会比C1、C2多分配一个分区,这样C0总共就比C1、C2多分配两个分区了,而且随着Topic的增加,这个情况会越来越严重。分配结果:订阅2个Topic,每个Topic4个分区,共3个ConsumerC0:[T0P0,T0P1,T1P0,T1P1]C1:[T0P2,T1P2]C2:[T0P3,T1P3]
2.Range(默认的分配策略)
StickyAssignor分区分配算法,目的是在执行一次新的分配时,能在上一次分配的结果的基础上,尽量少的调整分区分配的变动,节省因分区分配变化带来的开销。Sticky是“粘性的”,可以理解为分配结果是带“粘性的”——每一次分配变更相对上一次分配做最少的变动。其目标有两点:分区的分配尽量的均衡。每一次重分配的结果尽量与上一次分配结果保持一致。当这两个目标发生冲突时,优先保证第一个目标。第一个目标是每个分配算法都尽量尝试去完成的,而第二个目标才真正体现出StickyAssignor特性的
3.StickyAssignor分区分配算法
2.分区分配策略
2.消息丢失: 手动确认 ack 而不是自动提交 消息重复: 消费端幂等处理
由于Consumer在消费过程中可能会出现断电宕机等故障,Consumer恢复后,需要从故障前的位置继续消费,所以Consumer需要实时记录自己消费到哪个位置,以便故障恢复后继续消费。Kafka0.9版本之前,Consumer默认将offset保存在zookeeper中,从0.9版本开始,Consumer默认将offset保存在Kafka一个内置的名字叫_consumeroffsets的topic中。默认是无法读取的,可以通过设置consumer.properties中的exclude.internal.topics=false来读取。
3.消费者offset的维护
3.消息丢失
4.Consumer Group
5.Rebalance (重平衡)
7.kafka的消费者consumer
Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到600M/s,而随机写只有100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
1.顺序写磁盘
零拷贝并不是一次拷贝没有,只是减少了内核态到用户态之间的拷贝,减少不必要的拷贝次数
文件--内核空间--用户空间--socket缓冲区--网络
2.零拷贝技术
8.kafka高效读写数据
1.如果是Kafka消费能力不足,则可以考虑增加topic 的partition 的个数,同时提升消费者组的消费者数量,消费数=分区数(二者缺一不可)
2.若是下游数据处理不及时,则提高每批次拉取的数量。批次拉取数量过少(拉取数据/处理时间 <生产速度) ,使处理 的数据小于生产的数据, 也会造成数据积压。
1.consumer导致kafka积压了大量消息
1、消费kafka消息时,应该尽量减少每次消费时间,可通过减少调用三方接口、读库等操作,从而减少消息堆积的可能性。2、如果消息来不及消费,可以先存在数据库中,然后逐条消费(还可以保存消费记录,方便定位问题)3、每次接受kafka消息时,先打印出日志,包括消息产生的时间戳。4、kafka消 息保留时间( 修改kafka配置文件,默认一周)5、任务启动从上次提交offset处开始消费处理
2.消息过期失效
9.kafka 消息堆积如何处理
wget https://archive.apache.org/dist/kafka/2.6.2/kafka_2.12-2.6.2.tgztar zxf kafka_2.12-2.6.2.tgzcd kafka_2.12-2.6.2
1.安装kakfa
bin/kafka-topics.sh --list --zookeeper z-1.uat-im-kafka.ntscpq.c21.kafka.us-east-1.amazonaws.com:2181
2.查看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
3.查看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
4.扩容topic分区数
10.kafka运维命令
1.kafka
1.Broker:接收和分发消息的应用,简单来说就是消息队列服务器实体
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的唯一的线路。
1.rabbitmq的架构
1.主备模式
1.远程模式我们会在不同的地域部署多个MQ节点或者集群,假设我们在北京和成都部署了2台MQ节点然后用户下单
2.用户在北京浏览我们的商城进行下单
3.商城使用MQ做异步处理但是发现北京的MQ压力非常的大这个时候他就可以把消息复制给成都的节点然后由成都的MQ去处理
4.使用了远程模式以后,消息就变成了近端同步确认远端异步确认,这种方式大大提高了订单的确认速度,同时也是一种可靠的实现
2.远程模式
镜像队列也被称为Mirror队列,主要是用来保证mq消息可靠性的,他通过消息复制的方式能够保证我们的消息100%不丢失同时该集群模式也是企业中使用最多的模式。
1.用户发送一条消息
2.使用haproxy做负载均衡,同时因为当这个节点也可能挂掉所以受用keeplive漂移到另外一个haproxy节点提供服务
3.镜像队列模式
1.当用户发送条mq消息过来以后使用LBS做负载假设路由到了北京集群
2.北京集群接受到了该条消息以后就会使用federation插件把该条消息复制给成都的集群
3.我们做集群一般都是使用镜像队列的方式, 所以没有必要在集群与集群之间复制只需要北京的某一个节 点把数据复制到成都的某一个节点上然后成都的节点会使用镜像队列的方式自己去同步到所有的节点上
4.多活模式
5.集群模式总结
2.集群架构模式
2.拥有持久化的机制,进程消息,队列中的信息也可以保存下来。
3.实现消费者和生产者之间的解耦。
4.对于高并发场景下,利用消息队列可以使得同步访问变为串行访问达到一定量的限流,利于数据库的操作
5.可以使用消息队列达到异步下单的效果,排队中,后台进行逻辑下单。
3.为什么要使用rabbitmq
1.simple模式(即简单的收发模式)
2.work queues工作队列模式
3.pub/sub发布订阅模式
4.routing路由模式
5.topic主题模式
4.rabbitmq的工作模式有哪些
1.过期时间
2.死信队列
延迟队列存储的对象是对应的延迟消息;所谓\
3.延迟队列
1.发布确认:有两种方式:消息发送成功确认和消息发送失败回调。
场景:业务处理伴随消息的发送,业务处理失败(事物回滚)后要求消息不发送。rabbitmq 使用调用者的外部事物,通常是首选,因为它是非侵入性的(低耦合)。
2.事务支持
4.消息确认机制
5.消息追踪
5.rabbitmq的高级部分
若该队列至少有一个消费者订阅,消息将以循环(round-robin)的方式发送给消 费者。每条消息只会分发给一个订阅的消费者(前提是消费者能够正常处理消息并进行确认)。 通过路由可实现多消费的功能
6.消息如何分发
消息提供方->路由->一至多个队列 消息发布到交换器时,消息将拥有一个路由键(routing key),在消息创建时设定。 通过队列路由键,可以把队列绑定到交换器上。 消息到达交换器后,RabbitMQ 会将消息的路由键与队列的路由键进行匹配(针对不同的交换器有不同的路由规则)
fanout:如果交换器收到消息,将会广播到所有绑定的队列上
direct:如果路由键完全匹配,消息就被投递到相应的队列
topic:可以使来自不同源头的消息能够到达同一个队列。 使用 topic 交换器时, 可以使用通配符
常用的交换器
7.消息如何路由
消息持久化,当然前提是队列必须持久化 RabbitMQ 确保持久性消息能从服务器重启中恢复的方式是,将它们写入磁盘上 的一个持久化日志文件,当发布一条持久性消息到持久交换器上时,Rabbit 会在消息提交到日志文件后才发送响应。 一旦消费者从持久队列中消费了一条持久化消息,RabbitMQ 会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前 RabbitMQ 重启, 那么 Rabbit 会自动重建交换器和队列(以及绑定),并重新发布持久化日志文件中的消息到合适的队列
8.如何确保消息不丢失
先说为什么会重复消费:正常情况下,消费者在消费消息的时候,消费完毕后,会发送一个确认消息给消息队列,消息队列就知道该消息被消费了,就会将该消息从消息队列中删除;
但是因为网络传输等等故障,确认信息没有传送到消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他的消费者。
针对以上问题,一个解决思路是:保证消息的唯一性,就算是多次传输,不要让消息的多次消费带来影响;保证消息等幂性;1.比如:在写入消息队列的数据做唯一标示,消费消息时,根据唯一标识判断是否消费过;2.假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。
在消息生产时,MQ 内部针对每条生产者发送的消息生成一个 inner-msg-id,作为去重的依据(消息投递失败并重传),避免重复的消息进入队列;在消息消费时,要求消息体中必须要有一个 bizId(对于同一业务全局唯一,如支付 ID、订单 ID、帖子 ID 等)作为去重的依据,避免同一条消息被重复消费。
9.如何避免消息重复投递或者重复消费
将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID。 一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。 如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(not acknowledged,未确认)消息。
1.发送方确认模式
消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ 才能安全地把消息从队列中删除。 这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否 需要重新发送消息。也就是说,只要连接不中断,RabbitMQ 给了 Consumer 足够长的时间来处理消息。保证数据的最终一致性;下面罗列几种特殊情况 如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ 会认为 消息没有被分发,然后重新分发给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要去重)如果消费者接收到消息却没有确认消息,连接也未断开,则 RabbitMQ 认为该消费者繁忙,将不会给该消费者分发更多的消息。
2.接收方确认机制
10.如何确保消息正确地发送至 RabbitMQ? 如何确保消息接收方消费了消息?
问题:当消息生产的速度长时间,远远大于消费的速度时。就会造成消息堆积。
消息积压处理办法:临时紧急扩容:
先修复 consumer 的问题,确保其恢复消费速度,然后将现有 cnosumer 都停掉。新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。
1.消息队列堆积,想办法把消息转移到一个新的队列,增加服务器慢慢来消费这个消息
2.解决消费者的性能瓶颈:改短休眠时间
3.增加消费线程,增加多台服务器部署消费者。快速消费。
解决:
11.消息堆积如何解决
MQ中消息失效:假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。
12.mq中消息失效怎么办
mq消息队列块满了:如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,消费一个丢弃一个,都不要了,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。
13.mq中消息队列快满了怎么办
解决方案:生产者报据商品的id算出-个hash值然后在对我们的队列的个数取余就可以让相同id的所有的操作压到同一个队列,且每一个队列都只有一个消费者,此时就不会出现乱序的情况
场景1:
场景描述:当RabbitMQ采用简单队列模式的时候,如果消费者采用多线程的方式来加速消息的处理,此时也会出现消息乱序的问题。
生产者依次发送123三个消息;消费者从mq中拉取消息进行消费,此时因为只有一个消费者,消费能力非常有限,为了提交消息的处理能力,消费者使用多线程进行处理;消费者拉取消息1以后交给线程1处理消息2交给线程2处理消息3交给线程3处理但是每个线程执行的时间是不可控的.可能线程3先执行线程1再执行最后线程2执行此时就出现了消浪乱序的问题
解决方案:生产者按照自己希望的顺序依次发送消息到queue;消费者拉取消息然后根据d算出一个hash值然后把同d的商品压倒向-个内存队列公司一个线程去处理此时就保证了有序性
场景2:
14.如何进行有序的消息消费
比如说这个消息队列系统,我们从以下几个角度来考虑一下:首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。能不能支持数据 0 丢失啊?可以呀,有点复杂的。
15.设计MQ的思路
场景介绍:消息生产者发送消息成功,但是MQ没有收到该消息,消息在从生产者传输到MQ的过程中丢失,一般是由于网络不稳定的原因。
说明:异步监听模式,可以实现边发送消息边进行确认,不影响主线程任务执行。
1.消息在生产者丢失
2.消息在mq丢失
3.消息在消费者丢失
16.消息丢失
17.重复消费
2.rabbitmq
3.rocketmq
异步:A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 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 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。
就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其 实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦。
应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。
削峰:减少高峰时期对服务器压力。
流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求
日志处理 - 解决大量日志传输。
消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。
1.mq的优点
1.系统可用性降低本来系统运行好好的,现在你非要加入个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性会降低;
2. 系统复杂度提高加入了消息队列,要多考虑很多方面的问题,比如:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。因此,需要考虑的东西更多,复杂性增大。
3. 一致性问题A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。
2.mq的缺点
一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;
后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高;
不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。
所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。
如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范
3.你们公司生产环境用的是什么消息中间件?
1.问题:消息有序指的是可以按照消息的发送顺序来消费。假如生产者产生了 2 条消息:M1、M2,假定 M1 发送到 S1,M2 发送到 S2,如果要保证 M1 先 于 M2 被消费,怎么做?
2.解决方案:保证生产者 - MQServer - 消费者是一对一对一的关系
3.缺陷:并行度就会成为消息系统的瓶颈(吞吐量不够)更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花费更多的精力来解决阻塞的问题。 (2)通过合理的设计或者将问题分解来规避。不关注乱序的应用实际大量存在队列无序并不意味着消息无序 所以从业务层面来保证消息的顺序而不仅仅是依赖于消息系统,是一种更合理的方式
1.消息的顺序问题
造成消息重复的根本原因是:网络不可达。所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理?消费端处理消息的业务逻辑保持幂等性。只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。
2.消息的重复问题
4.MQ 有哪些常见问题?如何解决这些问题?
4.mq面试问题补充
8.mq部分
9.linux
Hbase 适合存储 PB 级别的海量数据,在 PB 级别的数据以及采用廉价 PC 存储的情况下,能在几十到百毫秒内返回数据。这与 Hbase 的极易扩展性息息相关。正式因为 Hbase 良好的扩展性,才为海量数据的存储提供了便利。
1.海量存储
这里的列式存储其实说的是列族存储,Hbase 是根据列族来存储数据的。列族下面可以有非常多的列,列族在创建表的时候就必须指定。
2.列式存储
Hbase 的扩展性主要体现在两个方面,一个是基于上层处理能力(RegionServer)的扩展,一个是基于存储的扩展(HDFS)
通过横向添加 RegionSever 的机器,进行水平扩展,提升 Hbase 上层的处理能力,提升 Hbsae服务更多 Region 的能力
3.极易扩展
由于目前大部分使用 Hbase 的架构,都是采用的廉价 PC,因此单个 IO 的延迟其实并不小,一般在几十到上百 ms 之间。这里说的高并发,主要是在并发的情况下,Hbase 的单个IO 延迟下降并不多。能获得高并发、低延迟的服务。
4.高并发
稀疏主要是针对 Hbase 列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的
5.稀疏
1.hbase特点
Hbase 是由 Client、Zookeeper、Master、HRegionServer、HDFS 等几个组件组成,下面来介绍一下几个组件的相关功能:
Client 包含了访问 Hbase 的接口,另外 Client 还维护了对应的 cache 来加速 Hbase 的访问,比如 cache 的.META.元数据的信息
1.client
HBase 通过 Zookeeper 来做 master 的高可用、RegionServer 的监控、元数据的入口以及集群配置的维护等工作。具体工作如下:
1.通过 Zoopkeeper 来保证集群中只有 1 个 master 在运行,如果 master 异常,会通过竞争机制产生新的 master 提供服务
2.通过 Zoopkeeper 来监控 RegionServer 的状态,当 RegionSevrer 有异常的时候,通过回调的形式通知 Master RegionServer 上下线的信息
3.通过 Zoopkeeper 存储元数据的统一入口地址
2.Zookeeper
master 节点的主要职责如下:1.为 RegionServer 分配 Region2.维护整个集群的负载均衡3.维护集群的元数据信息4.发现失效的 Region,并将失效的 Region 分配到正常的 RegionServer 上 5.当 RegionSever 失效的时候,协调对应 Hlog 的拆分
3.Hmaster
HregionServer 直接对接用户的读写请求,是真正的“干活”的节点。它的功能概括如下:1.管理 master 为其分配的 Region2.处理来自客户端的读写请求3.负责和底层 HDFS 的交互,存储数据到 HDFS4.负责 Region 变大以后的拆分5.负责 Storefile 的合并工作
4.HregionServer
HDFS 为 Hbase 提供最终的底层数据存储服务,同时为 HBase 提供高可用(Hlog 存储在HDFS)的支持,具体功能概括如下:提供元数据和表数据的底层分布式存储服务;数据多副本,保证的高可靠和高可用性
5.HDFS
2.hbase架构
1.监控 RegionServer2.处理 RegionServer 故障转移3.处理元数据的变更4.处理 region 的分配或转移 5.在空闲时间进行数据的负载均衡6.通过 Zookeeper 发布自己的位置给客户端
1.HMaster
1.负责存储 HBase 的实际数据2.处理分配给它的 Region3.刷新缓存到 HDFS4.维护 Hlog5.执行压缩6.负责处理 Region 分片
2.RegionServer
1.Write-Ahead logs
Hbase 表的分片,HBase 表会根据 RowKey值被切分成不同的 region 存储在 RegionServer中,在一个 RegionServer 中可以有多个不同的 region。
2.Region
HFile 存储在 Store 中,一个 Store 对应 HBase 表中的一个列族
3.Store
顾名思义,就是内存存储,位于内存中,用来保存当前的数据操作,所以当数据保存在WAL 中之后,RegsionServer 会在内存中存储键值对
4.MemStore
这是在磁盘上保存原始数据的实际的物理文件,是实际的存储文件。StoreFile 是以 Hfile的形式存储在 HDFS 的。
5.HFile
3.其他组件
3.HBase 中的角色
1.进入 HBase 客户端命令行:bin/hbase shell
2.查看当前数据库中有哪些表:list
6.查看表结构:describe ‘student’
8.统计表数据行数:count 'student'
9.删除数据:
10.清空表数据:truncate 'student'提示:清空表的操作顺序为先 disable,然后再 truncate。
首先需要先让该表为 disable 状态:hbase(main):019:0> disable 'student'然后才能 drop 这个表:hbase(main):020:0> drop 'student'提示:如果直接 drop 表,会报错:ERROR: Table student is enabled. Disable it first.
11.删除表
12.变更表信息
4.HBase Shell 操作
1.通过单个 RowKey 访问
2.通过 RowKey 的 range(正则)
3.全表扫描
2.访问 HBASE table 中的行,只有三种方式:
3.RowKey 行键 (RowKey)可以是任意字符串(最大长度是 64KB,实际应用中长度一般为10-100bytes),在 HBASE 内部,RowKey 保存为字节数组。存储时,数据按照 RowKey 的字典序(byte order)排序存储。设计 RowKey 时,要充分排序存储这个特性,将经常一起读取的行存储放到一起。(位置相关性)
1.RowKey
列族:HBASE 表中的每个列,都归属于某个列族。列族是表的 schema 的一部 分(而列不是),必须在使用表之前定义。列名都以列族作为前缀。例如 courses:history,courses:math都属于 courses 这个列族。
2.Column Family:列族
关键字:无类型、字节码
3.Cell
HBASE 中通过 rowkey和 columns 确定的为一个存贮单元称为cell。每个 cell都保存 着同一份数据的多个版本。版本通过时间戳来索引。时间戳的类型是 64 位整型。时间戳可以由 HBASE(在数据写入时自动 )赋值,此时时间戳是精确到毫秒 的当前系统时间。时间戳也可以由客户显式赋值。如果应用程序要避免数据版 本冲突,就必须自己生成具有唯一性的时间戳。每个 cell 中,不同版本的数据按照时间倒序排序,即最新的数据排在最前面。
版本回收:为了避免数据存在过多版本造成的的管理 (包括存贮和索引)负担,HBASE 提供 了两种数据版本回收方式。一是保存数据的最后 n 个版本,二是保存最近一段 时间内的版本(比如最近七天)。用户可以针对每个列族进行设置。
4.Time Stamp
1.Table:表,所有的表都是命名空间的成员,即表必属于某个命名空间,如果没有指定,则在 default 默认的命名空间中。
2.RegionServer group:一个命名空间包含了默认的 RegionServer Group
3.Permission:权限,命名空间能够让我们来定义访问控制列表 ACL(Access Control List)。例如,创建表,读取表,删除,更新等等操作。
4.Quota:限额,可以强制一个命名空间可包含的 region 的数量。
5.命名空间(HBase NameSpaces)
5.HBase 数据结构
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,再返回给客户端
1.读流程
1.Client 向 HregionServer 发送写请求;
2.HregionServer 将数据写到 HLog(write ahead log)。为了数据的持久化和恢复;
3.HregionServer 将数据写到内存(MemStore);
4.反馈 Client 写成功。
2.写流程
1.当 MemStore 数据达到阈值(默认是 128M,老版本是 64M),将数据刷到硬盘,将内存中的数据删除,同时删除 HLog 中的历史数据;
2.并将数据存储到 HDFS 中;
3.在 HLog 中做标记点。
3.数据 flush 过程
1.当数据块达到 4 块,Hmaster 触发合并操作,Region 将数据块加载到本地,进行合并;
2.当合并的数据超过 256M,进行拆分,将拆分后的 Region 分配给不同的 HregionServer管理
3.当HregionServer宕机后,将HregionServer上的hlog拆分,然后分配给不同的HregionServer加载,修改.META.;
4.注意:HLog 会同步到 HDFS。
4.数据合并过程
6.Hbase原理
在 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/
5.打开页面测试查看: http://hadooo102:16010
1.高可用
每一个 region 维护着 startRow 与 endRowKey,如果加入的数据符合某个 region 维护的rowKey 范围,则该数据交给这个 region 维护。那么依照这个原则,我们可以将数据所要投放的分区提前大致的规划好,以提高 HBase 性能。
2.预分区
一条数据的唯一标识就是 rowkey,那么这条数据存储于哪个分区,取决于 rowkey 处于哪个一个预分区的区间内,设计 rowkey的主要目的 ,就是让数据均匀的分布于所有的 region中,在一定程度上防止数据倾斜。接下来我们就谈一谈 rowkey 常用的设计方案
比如:原 本 rowKey 为 1001 的 , SHA1 后变成:dd01903921ea24941c26a48f2cec24e0bb0e8cc7原 本 rowKey 为 3001 的 , SHA1 后变成:49042c54de64a1e9bf0b33e00245660ef92dc7bd原 本 rowKey 为 5001 的 , SHA1 后变成:7b61dec07e02c188790670af43e717f0f46e8913在做此操作之前,一般我们会选择从数据集中抽取样本,来决定什么样的 rowKey 来 Hash后作为每个分区的临界值
1.生成随机数、hash、散列值
20170524000001 转成 1000004250710220170524000002 转成 20000042507102这样也可以在一定程度上散列逐步 put 进来的数据。
2.字符串反转
20170524000001_a12e20170524000001_93i7
3.字符串拼接
如果RowKey是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将RowKey的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer实现负载均衡的几率,如果没有散列字段,首字段直接是时间信息,将产生所有数据都在一个RegionServer上堆积的热点现象,这样在做数据检索的时候负载将会集中在个别RegionServer,降低查询效率。
RowKey散列原则
RowKey是按照字典排序存储的,因此,设计RowKey时候,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。
举个例子:如果最近写入HBase表中的数据是最可能被访问的,可以考虑将时间戳作为RowKey的一部分,由于是字段排序,所以可以使用Long.MAX_VALUE-timeStamp作为RowKey,这样能保证新写入的数据在读取时可以别快速命中。
RowKey唯一原则
3.RowKey 设计
HBase 操作过程中需要大量的内存开销,毕竟 Table 是可以缓存在内存中的,一般会分配整个可用内存的 70%给 HBase 的 Java 堆。但是不建议分配非常大的堆内存,因为 GC 过程持续太久会导致 RegionServer 处于长期不可用状态,一般 16~48G 内存就可以了,如果因为框架占用内存过高导致系统内存不足,框架一样会被系统服务拖死。
4.内存优化
1.允许在 HDFS 的文件中追加内容
2.优化 DataNode 允许的最大文件打开数
3.优化延迟高的数据操作的等待时间
4.优化数据的写入效率
5.设置 RPC 监听数量
6.优化 HStore 文件大小
7.优化 hbase 客户端缓存
8.指定 scan.next 扫描 HBase 所获取的行数
9.flush、compact、split 机制
5.基础优化
7.HBase优化
10.hbase
3.时间复杂度:O(N^2)
1.冒泡排序
2.交换第一个索|处和最小值所在的索引|处的值
2.选择排序
4.时间复杂度:O(N^2)
3.插入排序
2.对分好组的每一组数据完成插入排序;
4.希尔排序
4.时间复杂度:O(nlogn)
5.归并排序
6.快速排序
1.java算法
2.雪花算法
3.Leaf算法(美团)
11.数据结构与算法
@SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。@EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项。@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })@ComponentScan:Spring组件扫描
1.SpringBoot启动类注解?它是由哪些注解组成?
1. 直接执行 main 方法运行2. 命令行 java -jar 的方式 打包用命令或者放到容器中运行3.用 Maven/ Gradle 插件运行
2.SpringBoot启动方式?
不需要,内置了 Tomcat/Jetty。
3.SpringBoot需要独立的容器运行?
@EnableAutoConfiguration (开启自动配置) 该注解引入了AutoConfigurationImportSelector,该类中的方法会扫描所有存在META-INF/spring.factories的jar包。
4.SpringBoot自动配置原理?
在启动类加: @ImportResource(locations = {\"classpath:spring.xml\"})
5.SpringBoot如何兼容Spring项目?
@PatchMapping@PostMapping@GetMapping@PutMapping@DeleteMapping
6.针对请求访问的几个组合注解?
@SpringBootTest
7.编写测试用例的注解?
@ControllerAdvice@ExceptionHandler
8.SpringBoot异常处理相关注解?
@PropertySource@Value@Environment@ConfigurationProperties
9.SpringBoot读取配置相关注解有?
10.SpringBoot Starter的工作原理
跨域可以在前端通过 JSONP 来解决,但是 JSONP 只可以发送 GET 请求,无法发送其他类型的请求,在 RESTful 风格的应用中,就显得非常鸡肋,因此我们推荐在后端通过 (CORS,Cross\u0002origin resource sharing) 来解决跨域问题。这种解决方案并非 Spring Boot 特有的,在传统的SSM 框架中,就可以通过 CORS 来解决跨域问题,只不过之前我们是在 XML 文件中配置 CORS ,现在可以通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。
11.Spring Boot 中如何解决跨域问题 ?
12.springboot
AOP:将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情。
1.谈谈你对AOP的理解
1.ioc容器
2.控制反转,也就做依赖注入
2.谈谈你对IOC的理解
基于 Java 的配置,允许你在少量的 Java 注解的帮助下,进行你的大部分 Spring 配置而非通过 XML 文件。
以@Configuration 注解为例,它用来标记类可以当做一个 bean 的定义,被 Spring IOC 容器使用。
是@Bean 注解,它表示此方法将要返回一个对象,作为 一个 bean 注册进 Spring 应用上下文。
3.什么是基于 Java 的 Spring 注解配置? 给一些注解的例子
这个注解表明 bean 的属性必须在配置的时候设置,通过一个 bean 定义的显式的属 性 值 或 通 过 自 动 装 配 , 若 @Required 注 解 的 bean 属 性 未 被 设 置 , 容 器 将 抛 出 BeanInitializationException。
1.@Required 注解
@Autowired 注解提供了更细粒度的控制,包括在何处以及如何完成自动装配。它 的用法和@Required 一样,修饰 setter 方法、构造器、属性或者具有任意名称和/ 或多个参数的 PN 方法。
2.@Autowired 注解
该注解表明该类扮演控制器的角色,Spring 不需要你继承任何其他控制器基类或引 用 Servlet API。
3.@Controller 注解
该注解是用来映射一个 URL 到一个类或一个特定的方处理法上。
4.@RequestMapping 注解
@Resource的作用相当于@Autowired,只不过@Autowired按byType自动注入,而@Resource默认按 byName自动注入罢了
@Resource有两个属性是比较重要的,分是name和type,Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。
2.@Autowired默认按类型byType装配(这个注解是属业spring的),默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,如:@Autowired(required=false) ,如果我们想使用名称装配可以结合@Qualifier注解进行使用,如下:@Autowired () @Qualifier ( \"baseDao\" )private BaseDao baseDao;
3.@Resource(这个注解属于J2EE的),默认按照名称进行装配,名称可以通过name属性进行指定,如果没有指定name属性,当注解写在字段上时,默认取字段名进行安装名称查找,如果注解写在setter方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name属性一旦指定,就只会按照名称进行装配。@Resource (name= \"baseDao\" )private BaseDao baseDao;
5.spring的注解@Autowired 与java中注解@Resource的区别
4.spring中的常用注解
解析类得到BeanDefinition--实例化得到对象---属性填充(对对象中加了@Autowired)--回调Aware方法--调用BeanPostProcessor初始前方法--调用初始化方法--调用BeanPostProcessor初始化后方法(AOP也在这里/如果创建的bean是单例模式,还是将bean扔入线程池)--使用Bean--Spring关闭时调用DisposableBean的destory()方法。
3.描述一下spring bean的生命周期
4.spring支持的几种bean的作用域
5.BeanFactory和ApplicationContext有什么区别?
1.spring 配置文件中 <bean> 节点的 autowire 参数可以控制 bean 自动装配的方式 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>
6.spring 自动装配 bean 有哪些方式?
12.spring
JAVA技术路线
0 条评论
下一页