九、中间件和分布式 【54道】
- Nginx的应用场景有哪些?
负载均衡, 反向代理, 静态资源服务器 - Nginx负载均衡的策略有哪些?
轮询(默认), 指定权重, IP hash - Nginx的进程模型 ?
nginx模型有两种进程,master进程(相当于管理进程)和worker进程(实际工作进程)。
master进程主要用来管理worker进程,管理包含:接收来自外界的信号,向各worker进程发送信号,
监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。
而基本的网络事件,则是放在worker进程中来处理了。多个worker进程之间是对等的,他们同等竞争来
自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker
进程,不可能处理其它进程的请求。worker进程的个数是可以设置的,一般我们会设置与机器cpu核数
一致,这里面的原因与nginx的进程模型以及事件处理模型是分不开的。 - Nginx常用命令!
启动 ./nginx
关闭 ./nginx -s stop
查看nginx进程 ps -ef | grep nginx
重新加载nginx ./nginx -s reload - Nginx的优化方案!
nginx 进程数,建议按照cpu 数目来指定,一般为它的倍数 (如,2个四核的cpu计为8)。
为每个进程分配cpu,上例中将8 个进程分配到8 个cpu,当然可以写多个,或者将一个进程分配到多个
cpu。
配置每个进程允许的最多连接数, 理论上每台nginx 服务器的最大连接数为 : worker进程数 * worker连
接数。 - Redis的应用场景!
热点数据的缓存, 限时业务的运用, 计数器相关问题, 排行榜相关问题, 分布式锁, 点赞、好友等相互关系的
存储等 - Redis存储数据的结构!
Redis的Key-Value格式中Key只能是String类型
Redis的Value类型有5种:
String, List, Set(无序Set), ZSet(有序Set), Hash类型(Map) - Redis持久化策略!
redis的持久化策策略有2种:默认是RDB
RDB(数据快照模式),定期存储,保存的是数据本身,操作磁盘频率低, 速度快, 服务器突然断电数据
可能丢失一部分.
AOF(追加模式),每次修改数据时,同步到硬盘(写操作日志),保存的是数据的变更记录, 频繁操作磁
盘, 速度慢, 但是数据可靠不容易丢失. - Redis集群架构!
哨兵模式:
需要三台服务器, 一台哨兵服务器, 一台主机, 一台备机
哨兵使用心跳检测技术每隔一段时间向主机发送ping命令, 主机返回pong命令, 认为主机存活, 如果主机
不返回pong命令, 认为主机宕机
哨兵服务器就会通知备机进行切换, 备机充当主机, 备机会最后ping一次主机, 如果主机返回pong命令不
进行切换, 如果主机没有返回pong命令则认为主机确实宕机, 备机切换成主机替代主机工作 - Redis的淘汰策略
volatile-lru:从设置过期时间的数据集中挑选出最近最少使用的数据淘汰。
volatile-ttl:淘汰机制采用LRU,策略基本上与volatile-lru相似,从设置过期时间的数据集中挑选将要过
期的数据淘汰,ttl值越大越优先被淘汰。
volatile-random:随机淘汰, 从已设置过期时间的数据集中任意选择数据淘汰。
allkeys-lru:从所有数据集中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集
合,而非过期的key集合。
allkeys-random:从所有数据集中选择任意数据淘汰。 - Redis缓存的击穿,穿透,雪崩,倾斜问题!
缓存击穿:
就是某一个热点数据,缓存中某一时刻失效了,因而大量并发请求打到数据库上,就像被击穿了一
样。说白了,就是某个数据,数据库有,但是缓存中没有。那么,缓存击穿,就会使得因为这一个
热点数据,将大量并发请求打击到数据库上,从而导致数据库被打垮。
缓存击穿解决方案:
去掉热点数据的生存时间
热点数据访问到数据库的时候加个锁, 这样同一时间只能有一个访问热点数据的请求访问数据库, 防
止数据库宕机.
缓存穿透 :
缓存穿透与击穿的区别就是,
击穿:redis中没有数据, 数据库里“有”数据;
穿透:redis中没有数据, 数据库里也“没”数据。
缓存穿透解决方案:
用户查询会有查询参数, 使用查询参数作为key, value值设置为null, 存入redis并设置生存时间, 如果
这样的请求多了, redis抗压,但是不会造成数据库宕机.
使用布隆过滤器, 直接过滤这样的查询请求
缓存雪崩 : 指的是大面积的 key 同时过期,导致大量并发打到我们的数据库。
雪崩解决方案 : 将key的过期时间设置为随机, 这样不会在同一时间大量过期, 不会在同一时间造成
大量数据库访问, 防止数据库宕机 - Redis的缓存和数据库的双写一致性!
数据库和redis同步数据流程 : 写数据库, 删除redis对应缓存数据, 再将数据库最新数据写入redis中
将不一致分为三种情况:
数据库有数据,缓存没有数据;
数据库有数据,缓存也有数据,数据不相等;
数据库没有数据,缓存有数据。
解决方案大概有以下几种:
对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。
定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载。
给所有的缓存一个失效期 - Redis如何实现的分布式锁!
获取锁:
使用setnx命令向redis中存入一个键值对并设置过期时间
setnx命令在存入之前会进行判断, 如果redis中存在这个键值对, 则无法存入并返回状态0, 相当于锁
被占用, 无法获取锁可以进入等待或再一次尝试, 如果redis中不存在这个键值对, 则可以存入并返回
状态1
释放锁:
使用setnx命令存入数据后, 相当于获取到了锁, 执行完业务后一定要使用delete命令删除这个键值
对, 就相当于释放锁,其他任务就可以继续以上面方式获取锁, 因为设置了键值对的超时时间,在规定
时间内没有释放锁会redis会自动让这个键值对失效,防止死锁 - Redis的线程模型
从redis的性能上进行考虑,单线程避免了上下文频繁切换问题,效率高;
从redis的内部结构设计原理进行考虑,redis是基于Reactor模式开发了自己的网络事件处理器: 这个处
理器被称为文件事件处理器(file event handler)。而这个文件事件处理器是单线程的,所以才叫redis
的单线程模型,这也决定了redis是单线程的 - Elasticsearch的应用场景
互联网全文检: 像百度, 谷歌等
站内全文检索: 贴吧内帖子搜索, 京东, 淘宝商品搜索等
总之就是大数据量, 海量数据的搜索功能都可以用ElasticSearch - 什么是倒排索引
搜索前, 根据被搜索的内容创建文档对象, 文档对象有唯一id, 对被搜索内容进行切分词, 也就是将被搜索
的内容中的一句句话切分成一个个词, 组成索引, 索引记录了文档id, 也就是索引和文档有关联关系, 查询
的时候先查询索引, 根据索引找到文档id, 根据文档id找到文档内容, 这个过程叫做倒排索引算法 - Elasticsearch在5.x,6.x,7.x版本的区别
5.x Lucene 6.x 的支持,磁盘空间少一半;索引时间少一半;查询性能提升25%;支持IPV6。
6.x 开始不支持一个 index 里面存在多个 type
7.x TransportClient被废弃以至于es7的java代码,只能使用restclient。对于java编程,建议采用 Highlevel-rest-client 的方式操作ES集群 - Elasticsearch中分片的概念
单个节点由于物理机硬件限制,存储的文档是有限的,如果一个索引包含海量文档,则不能在单个节点
存储。ES提供分 片机制,同一个索引可以存储在不同分片(数据容器)中,这些分片又可以存储在集群
中不同节点上
分片分为 主分片(primary shard) 以及 从分片(replica shard)
主分片与从分片关系:从分片只是主分片的一个副本,它用于提供数据的冗余副本
从分片应用:在硬件故障时提供数据保护,同时服务于搜索和检索这种只读请求
是否可变:索引中的主分片的数量在索引创建后就固定下来了,但是从分片的数量可以随时改变
索引默认创建的分片:默认设置5个主分片和一组从分片(即每个主分片有一个从分片对应),但是从分
片没有被启用(主从分片在同一个节点上没有意义),因此集群健康值显示为黄色(yellow) - Elasticsearch中refresh和flush是什么
Refresh:
当我们向ES发送请求的时候,我们发现es貌似可以在我们发请求的同时进行搜索。而这个实时建索
引并可以被搜索的过程实际上是一次es 索引提交(commit)的过程,如果这个提交的过程直接将
数据写入磁盘(fsync)必然会影响性能,所以es中设计了一种机制,即:先将index-buffer中文档
(document)解析完成的segment写到filesystem cache之中,这样避免了比较损耗性能的io操
作,又可以使document可以被搜索。以上从index-buffer中取数据到filesystem cache中的过程叫
做refresh。
es默认的refresh间隔时间是1s,这也是为什么ES可以进行近乎实时的搜索
Flush :
我们可能已经意识到如果数据在filesystem cache之中是很有可能在意外的故障中丢失。这个时候
就需要一种机制,可以将对es的操作记录下来,来确保当出现故障的时候,保留在filesystem的数
据不会丢失,并在重启的时候可以从这个记录中将数据恢复过来。elasticsearch提供了translog来
记录这些操作。当向elasticsearch发送创建document索引请求的时候,document数据会先进入
到index buffer之后,与此同时会将操作记录在translog之中,当发生refresh时(数据从index
buffer中进入filesystem cache的过程)translog中的操作记录并不会被清除,而是当数据从
filesystem cache中被写入磁盘之后才会将translog中清空。而从filesystem cache写入磁盘的过程
就是flush。 - 如何提升Elasticsearch的查询效率
调优Filesystem Cache性能的方法:
要让 ES 性能好,最佳的情况下,就是机器的内存,至少可以容纳总数据量的一半。索引数据占用
的内存最好控制在留给Filesystem Cache的内存容量中。尽量将所有索引数据加载到内存中, 这样
搜索的时候可以从内存中搜索索引速度极快.
冷热分离方法:
将热门数据写一个索引,冷门数据单独写一个索引,这样在热门数据被预热(即写入Filesystem
Cache)后,不会被冷数据给冲刷掉。
数据预热方法:
对于热门的搜索词,后台系统可以定时自己去搜索一下,这样,热门数据就会刷到Filesystem
Cache中,后面用户实际来搜索热词时就是直接从内存中搜索了 - 如何提高Elasticsearch的查询命中率
加入扩展词词典ext.dic
主要有两项:生词扩展和同义词扩展
生词扩展:把网络用语、公司名称等词语放入词典中
同义词扩展:如我们平常用的iphone,当输入“苹果”的时候,我们也是期待能查出手机的。
加入停止词词典stop.dic
把 了、啊、哦、的、之类的语气词放入停止词词典;把数据筛选过滤 - RabbitMQ的应用场景
异步处理
很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息
放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它
们。
应用解耦
在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。消息系统在处理过程中间插入了
一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改
两边的处理过程,只要确保它们遵守同样的接口约束
流量消峰
主要可以用来抗高并发的写入, 防止数据库因为高并发写入宕机 - RabbitMQ的底层架构
Broker:它提供一种传输服务,它的角色就是维护一条从生产者到消费者的路线,保证数据能按照指定的
方式进行传输,
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
Queue:消息的载体,每个消息都会被投到一个或多个队列。
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来.
Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
vhost:虚拟主机,一个broker里可以有多个vhost,用作不同用户的权限分离。
Producer:消息生产者,就是投递消息的程序.
Consumer:消息消费者,就是接受消息的程序.
Channel:消息通道,在客户端的每个连接里,可建立多个channel. - RabbitMQ如何保证消息的可靠性
可靠性是保证消息不丢失,分为发送消息不丢失和消费消息不丢失两种 :
发送不丢失有confirm和return机制
消费不丢失可以用ack手动确认机制 - RabbitMQ如何保证避免消息重复消费
让每个消息携带一个全局的唯一ID,即可保证消息的幂等性,具体消费过程为:
消费者获取到消息后先根据id去查询redis是否存在该消息。
如果不存在,则正常消费,消费完毕后写入redis。
如果存在,则证明消息被消费过,直接丢弃。 - RabbitMQ的死信队列
死信:是RabbitMQ中的一种消息机制,当消息在队列的存活时间超过设置的TTL时间或者消息队列的消
息数量已经超过最大队列长度。这样的消息被认为是死亡的信息也就是死信.
死信消息,会被RabbitMQ自动重新投递到另一个交换机上(Exchange),这个交换机往往被称为
DLX(dead-letter-exchange)“死信交换机”,然后交换机根据绑定规则转发到对应的队列上,监听该队列
就可以被重新消费。 - RabbitMQ如何基于死信队列实现延迟队列以及存在的问题
RabbitMQ中没有延迟队列的功能, 但是可以借助延时消息, 死信, 死信队列来实现延迟队列的功能
第一. 给队列发送消息给消息设置一个超时时间, 超过这个时间没有被消费掉这个消息就会被认为是死信,
这个队列不设置消费方.
第二. 队列中的所有消息, 到达超时时间都会成为死信, 会被RabbitMQ发送到死信交换器
第三. 死信交换器中的数据会被RabbitMQ发送到死信队列
第四. 接收方监听器监听死信队列. 这样从里面消费的消息都是超时后的消息, 也就是先了延迟队列的功
能. - Zookeeper的应用场景
可以做为dubbo的注册中心
可以做分布式锁
可以为dubbo提供负载均衡功能
分布式协调/通知 - Zookeeper的存储数据的结构
zookeeper的数据存储结构是一个DataTree数据结构。
其内部是一个Map<String, DataNode> nodes的数据结构,其中key是path,DataNode是真正保存数
据的核心数据结构。 - Zookeeper的节点的类型
永久节点 : 无序但是客户端和zookeeper服务器断开连接后不会被自动删除
永久有序节点 : 有序, 并且客户端和zookeeper服务器断开连接后不会被自动删除
临时节点 : 无序, 客户端和zookeeper服务器断开连接后会被自动删除
临时有序节点 : 有序, 客户端和zookeeper服务器断开连接后会被自动删除 - Zookeeper的集群架构
Zookeeper集群采用选举机制, 也就是多台zookeeper之间会互相进行投票, 从而选出主机leader, 其他的
都是备机flower.
主备之间使用心跳检测技术, 备机每隔一段时间会ping主机, 主机返回pong给备机, 认为主机存活. 如果主
机不返回pong,则认为主机宕机, 这个时候其他备机会再次执行选举机制, 选举出新的主机leader来替代老
主机工作.
Zookeeper集群一旦超过半数机器宕机, 则整个集群不提供服务, 将失去作用, 所以建议zookeeper集群服
务器数量为奇数台. - Zookeeper如何实现分布式锁
使用zookeeper创建临时序列节点来实现分布式锁,适用于顺序执行的程序,大体思路就是创建临时序
列节点,找出最小的序列节点,获取分布式锁,程序执行完成之后此序列节点消失,通过watch来监控
节点的变化,从剩下的节点的找到最小的序列节点,获取分布式锁,执行相应处理,依次类推…… - 阐述一下你理解的微服务架构
微服务架构是一种架构模式,它提倡将单一应用程序划分成一组小的服务,服务之间相互协调、互相配
合,为用户提供最终价值。每个服务运行在其独立的进程中,服务和服务之间采用轻量级的通信机制相
互沟通(通常是基于HTTP的Restful API).每个服务都围绕着具体的业务进行构建,并且能够被独立的部
署到生产环境、类生产环境等。另外,应尽量避免统一的、集中的服务管理机制.这样实现了易扩展,易维
护, 松耦合的特点. - 阐述一下SpringCloud中常用的组件
注册中心Eureka : 所有微服务启动后都要注册到Eureka中
远程调用Feign : 底层使用http协议, 发送请求给被调用方, 执行后返回结果
接口负载均衡Ribbon : 如果被调用的微服务是集群, Ribbion起到了负载均衡的效果
服务网关Gateway : 所有外部访问微服务的请求都要经过网关, 网关根据访问的url路径来转发请求到具体
服务.
配置中心Config : 统一管理所有微服务的配置文件.
熔断器Hystrix : 并发量超过微服务可以承受的极限, 可以进行熔断或者降级. - Eureka的工作机制
服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当服务消费者
要调用服务提供者,则向服务注册中心获取服务提供者地址,然后会将服务提供者地址缓存在本地,下
次再调用时,则直接从本地缓存中取,完成一次调用。
当服务注册中心Eureka Server检测到服务提供者因为宕机、网络原因不可用时,则在服务注册中心将服
务置为DOWN状态,并把当前服务提供者状态向订阅者发布,订阅过的服务消费者更新本地缓存。
服务提供者在启动后,周期性(默认30秒)向Eureka Server发送心跳,以证明当前服务是可用状态。
Eureka Server在一定的时间(默认90秒)未收到客户端的心跳,则认为服务宕机,注销该实例。 - Ribbon如何实现负载均衡的
调用方微服务到Eureka中根据被调用的服务名获取被调用服务器地址ip和端口号列表
在调用方根据返回的地址列表Rbbion默认使用轮询的策略, 分配请求进行逐个调用 - Hystrix的断路器以及实现原理
Hystrix的断路器实现原理采用, 马丁福勒断路器原理如下:
一段时间内,失败率达到一定阈值(比如50%失败,或者失败了50次),断路器打开,此时不再请求服
务提供者,而是只是快速失败的方法(断路方法)
断路器打开一段时间,自动进入“半开”状态,此时,断路器可允许一个请求方法服务提供者,如果请求
调用成功,则关闭断路器,否则继续保持断路器打开状态。
断路器hystrix是保证了局部发生的错误,不会扩展到整个系统,从而保证系统的即使出现局部问题也不
会造成系统雪崩。 - Eureka和Zookeeper实现注册中心的区别
Zookeeper保证的是CP(一致性和分区容错性)
zookeeper对一致性的要求要高于可用性。
Eureka保证的是AP(可用性和分区容错性)
Eureka看明白了这一点, 因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉
不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个
Eureka注册时,如果发现连接失败,则会自动切换至其他节点,只要有一台Eureka还在, 就能保住注册
服务的可用性,只不过查到的信息可能不是最新的 - 分布式项目中如何解决分布式事务的问题
首先尽量不要使用分布式事务, 因为会降低代码执行效率, 尽量保证业务的原子性和幂等性, 不使用分布式
事务最好.
某些业务必须用, 则可以使用分布式事务框架LCN或者阿里的Seata解决 - 阐述一下SpringCloud Alibaba中常用的组件
Nacos(配置中心 + 注册中心)
Nacos实现了服务的配置中心与服务注册发现的功能,Nacos可以通过可视化的配置降低相关的学
习与维护成本,实现动态的配置管理与分环境的配置中心控制。 同时Nacos提供了基于http/RCP的
服务注册与发现功能
Sentinel (分布式流控)
Sentinel是面向分布式微服务架构的轻量级高可用的流控组件,以流量作为切入点,从流量控制,
熔断降级,系统负载保护等维度帮助用户保证服务的稳定性。常用与实现限流、熔断降级等策略
Dubbo (远程过程调用)
Dubbo已经在圈内很火了,SpringCloud Alibaba基于上面提到的Nacos服务注册中心也同样整合了
Dubbo。
RocketMQ (消息队列)
RocketMQ基于Java的高性能、高吞吐量的消息队列,在SpringCloud Alibaba生态用于实现消息驱
动的业务开发,常见的消息队列有Kafka、RocketMQ、RabbitMQ等
Seata (分布式事物)
既然是微服务的产品,那么肯定会用到分布式事物。Seata就是阿里巴巴开源的一个高性能分布式
事物的解决方案。 - zuul和gateway区别?
目前zuul已经被gateway取代
Gateway比zuul功能更加强大, gateway内部实现了限流, 负载均衡等,但是也限制了仅适用于
SpringCould套件, zuul在其他微服务架构下也可以使用, 但是内部没有实现限流, 负载均衡等功能
Zuul仅支持同步, gateway支持异步, 所以gateway性能更好 - springCloud和Dubbo区别?
没有可比性, dubbo只是底层使用了RPC远程过程调用规范的一个远程调用技术. 而SpringCloud框架集
里面有很多子项目, SpringCloud中有注册中心, 配置中心, 远程调用, 接口间负载均衡, 熔断器, 网关等, 可
以对服务进行监控, 治理, 配置等有着全套解决方案. - 分布式事务和分布式任务如何实现?
分布式事务遵循CAP定理, 可以使用2pc两阶段提交, 或者TCC基于补偿机制实现, 又或者可以使用基于消
息最终一致性解决方案等.
具体使用起来也可以使用LCN或者阿里巴巴的Seata框架实现
分布式任务可以使用当当网的ElasticJob分布式任务框架实现 - redis与mysql怎么保证数据的一致?
采用延时双删策略:
先删除缓存;
再写数据库;
休眠500毫秒(根据具体的业务时间来定);
再次删除缓存。
那么,这个500毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以
删除读请求造成的缓存脏数据。
当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据
业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。
双删失败如何处理?
设置缓存数据的过期时间 - 延迟删除是怎么解决数据一致性
延时双删的方案思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数
据库之后,在sleep一段时间,然后再删除缓存。
sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于读写缓存的时间即可。 - 什么是跨域问题? SpringBoot如何解决跨域问题?
跨域问题:
浏览器厂商在生产浏览器的时候, 浏览器内部已经内置了同源策略, 也就是要求当前页面所在的url地
址和发送请求目标的url地址中, 协议, 域名, 端口号不可以有变化, 任意一项发生改变, 服务器虽然可
以接收请求返回响应, 但是浏览器认为不安全, 不接受响应的数据. 这就是跨域问题.
解决方案:
SpringMvc已经内置了跨域解决方案, 在controller类上加入@CrossOrigin注解就可以解决
底层原理就是在响应头中加入Access-Control-Allow-Origin: * 设置 - Zookeeper 集群节点为什么要部署成奇数
Zookeeper集群中只要有超过半数机器不工作,那么整个集群都是不可用的. 当宕掉几个zookeeper节点
服务器之后,剩下的个数必须大于宕掉的个数,也就是剩下的节点服务数必须大于n/2,这样zookeeper
集群才可以继续使用,无论奇偶数都可以选举leader。例如 : 5台zookeeper节点机器最多宕掉2台,还
可以继续使用,因为剩下3台大于5/2。至于为什么最好为奇数个节点?这样是为了以最大容错服务器个
数的条件下,能节省资源。比如,最大容错为2的情况下,对应的zookeeper服务数,奇数为5,而偶数
为6,也就是6个zookeeper服务的情况下最多能宕掉2个服务,所以从节约资源的角度看,没必要部署
6(偶数)个zookeeper服务节点 - Zookeeper脑裂问题和解决方案?
什么是脑裂:
脑裂(Split-Brain) 就是比如当你的集群里面有3个节点,它们都知道在这个 cluster 里需要选举出一
个 master。那么当它们3个之间的通信完全没有问题的时候,就会达成共识,选出其中一个作为
master。但是如果它们之间的通信出了问题,其中一个节点网络抖动断开好长时间, 那么剩下的两
个结点都会觉得现在没有 master,所以又会选举出一个 master,当网络恢复后集群里面就会有两
个 master。两个master都会争抢资源, 集群就会混乱出现问题.
解决Split-Brain脑裂的问题方案:
法定人数方式 : 比如3个节点的集群,集群可以容忍1个节点失效,这时候还能选举出1个
lead,集群还可用。这是zookeeper防止"脑裂"默认采用的方法。
采用Redundant communications (冗余通信)方式:集群中采用多种通信方式,防止一种通
信方式失效导致集群中的节点无法通信。
Fencing (共享资源) 方式:比如能看到共享资源就表示在集群中,能够获得共享资源的锁的就
是Leader,看不到共享资源的,就不在集群中。 - 什么是幂等性(Idempotence)及用在那里?
幂等性是能够以同样的方式做两次,而最终结果将保持不变,就好像它只做了一次的特性。
用法:在远程服务或数据源中使用幂等性,以便当它多次接收指令时,只处理一次。 - 什么是熔断?什么是服务降级?
熔断
服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整
个系统出现雪崩,暂时停止对该服务的调用。
降级
服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能
(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请
求,而直接返回一个提前准备好的fallback(退路)错误处理信息。这样,虽然提供的是一个有损
的服务,但却保证了整个系统的稳定性和可用性。 - dubbo支持的协议有哪些
支持 : dubbo协议, rmi协议, hessian协议, http协议, webservice协议, thrift协议, memcached协议,
redis协议
dubbo协议(默认)
连接个数:单连接
连接方式:长连接
传输协议:TCP
传输方式:NIO 异步传输
序列化:Hessian 二进制序列化
适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费
者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串。
适用场景:常规远程服务方法调用
rmi协议
连接个数:多连接
连接方式:短连接
传输协议:TCP
传输方式:同步传输
序列化:Java 标准二进制序列化
适用范围:传入传出参数数据包大小混合,消费者与提供者个数差不多,可传文件。
适用场景:常规远程服务方法调用,与原生RMI服务互操作
hessian协议
连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:Hessian二进制序列化
适用范围:传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。
适用场景:页面传输,文件传输,或与原生hessian服务互操作
http协议
连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:表单序列化
适用范围:传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表
单或URL传入参数,暂不支持传文件。
适用场景:需同时给应用程序和浏览器 JS 使用的服务。
WebService协议
连接个数:多连接
连接方式:短连接
传输协议:HTTP
传输方式:同步传输
序列化:SOAP 文本序列化
适用场景:系统集成,跨语言调用 - dubbo的连接方式有几种
无注册中心, 直接连接
在开发阶段, 服务提供方没有必要部署集群, 所以采用服务调用方直接连接服务提供方更方便测试与
开发
采用Zookeeper作为注册中心连接
在线上部署阶段使用, 对于某些并发访问压力大的服务器节点可以部署集群, 这时dubbo的服务提
供方服务器集群可以使用zookeeper来管理.
分组连接
当一个接口有多种实现时,可以用 group 区分。 - dubbo的负载均衡策略有哪些
Random LoadBalance 随机, 按权重设置随机概率(默认)
RoundRobin LoadBalance 轮询, 按权重设置轮询比率
LeastActive LoadBalance 最少活跃调用数, 相同活跃数的随机
ConsistentHash LoadBalance 一致性Hash, 相同参数的请求总是发到同一提供者 - dubbo的序列化有哪些, 推荐哪种
Hessian 序列化:是修改过的 hessian lite,默认启用, 推荐使用.
json 序列化:使用 FastJson 库
java 序列化:JDK 提供的序列化,性能不理想
dubbo 序列化:未成熟的高效 java 序列化实现,不建议在生产环境使用
十、设计模式面试 【2道】
- 谈一谈你了解的设计模式有哪些?
大致按照模式的应用目标分类,设计模式可以分为创建型模式、结构型模式和行为型模式。
创建型模式 : 是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式(Factory、Abstract
Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)。
结构型模式 : 是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。常见的结构型
模式,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式
(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等。
行为型模式 : 是从类或对象之间交互、职责划分等角度总结的模式。比较常见的行为型模式有策略模式
(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭
代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor) - 设计模式开发中的应用!
单例模式: 例如: 配置类config的对象只能有一个
工厂模式: 例如: 使用BeanFactory工厂创建对象, 不用自己去new对象
代理模式: 例如: spring中的aop底层实现就是动态代理
适配器模式: 例如: SpringMvc中的HandlerInterceptorAdapter就是适配器模式
建造者模式: 例如: mybatis中的SqlsessionFactoryBuilder就是建造者模式, 遵循了单一职责原则, 只做一
件事.
十一、 数据结构 【19道】
- 什么是空间和时间复杂度?
时间复杂度 :
(1)时间频度 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。
但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时
间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数
多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
(2)时间复杂度在刚才提到的时间频度中,n称为问题的规模,当n不断变化时,时间频度T(n)也会不断
变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度概念。 一般情况下,算法
中基本操作重复执行的次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近
于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),
称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。
空间复杂度 :
一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运
行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常
数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助
空间。程序执行时所需存储空间包括以下两部分。
(1)固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即
代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
(2)可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小
与算法有关。
一个算法所需的存储空间用f(n)表示。S(n)=O(f(n)) 其中n为问题的规模,S(n)表示空间复杂度。 - 常见的数据结构有哪些?
数组, 链表, 队列, 堆, 栈, 树, 散列表, 图 - 链表的数据结构的特点
链表使用不连续内存空间, 空间利用率高
链表插入删除效率高, 查询修改效率慢
链表占用空间大小不固定, 可以扩展 - 栈数据结构的特点
栈是一种操作受限的线性表,只允许从一端插入和删除数据。拥有后进先出的特点
栈的插入和删除只能在一个位置上进行的表,栈只有进栈、出栈两种操作。前者相当于插入,后者相当
于删除最后的元素。
从底层看,栈就是CPU寄存器里的某个指针所指向的一片内存区域 - 队列数据结构的特点
队列也是一种操作受限的数据结构,只能从队尾的一端进行插入,队头的一端进行删除。先进先出
FIFO。双端队列不受这种限制,两端都能进行插入和删除。 - 说一说什么是跳表?Redis为什么用跳表实现有序集合?
Redis中的有序集合是通过跳表来实现的,还用到了散列表,它支持的核心操作有:插入一个数据、删除
一个数据、查找一个数据、按照区间查找数据、迭代输出有序序列。
按照区间来查找数据,这个操作红黑树的效率没有跳表高,其他几个操作红黑树都可以完成,时间复杂
度都一样。按照区间来查找,跳表可以做到O(logn)的时间复杂度定位区间的起点,然后在原始链表中的
顺序往后遍历即可,非常高效。
跳表使用空间换时间,通过构建多级索引来提高查询的效率,实现基于链表的二分查找,跳表是一个动
态数据结构,支持快速的插入、删除、查找操作,时间复杂度都是O(logn)。
跳表的空间复杂度是O(n),通过改变索引构建策略 - 散列表的数据结构特点
哈希表的查找效率主要取决于构造哈希表时选取的哈希函数和处理冲突的方法。
在各种查找方法中,平均査找长度与结点个数n无关的查找方法是哈希表查找法。
哈希函数取值是否均匀是评价哈希函数好坏的标准。
哈希存储方法只能存储数据元素的值,不能存储数据元素之间的关系。 - 二叉树数据数据结构特点
每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两棵子树,而是最多有。
没有子树或者有一棵子树都是可以的。
左子树和右子树是有顺序的,次序不能任意颠倒
即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。 - 图数据结构特点
无向图 : 每个节点都没有方向,边上可能有权重
有向图 : 每个节点是有方向的的
有向无环图 : 可以描述任务之间的关系 - 堆数据结构特点
将根节点最大的堆叫大顶堆或者大根堆,根节点最小的堆叫小顶堆或小根堆。
常见的堆有二叉堆,斐波拉契堆。
如果是大顶堆,常见操作及时间复杂度:
查找最大值:o(1)
删除最大值:o(logn)
添加值:o(1)或o(logn) - 大顶堆和小顶堆的区别?
大顶堆 : 根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最大者,称为大顶堆。大根堆要求根
节点的关键字既大于或等于左子树的关键字值,又大于或等于右子树的关键字值。
小顶堆 : 根结点(亦称为堆顶)的关键字是堆里所有结点关键字中最小者,称为小顶堆。小根堆要求根
节点的关键字既小于或等于左子树的关键字值,又小于或等于右子树的关键字值。 - 说一说常见的排序算法和对应的时间复杂度
- 用Java代码实现冒泡排序和快速排序
package 冒泡排序; import java.util.Arrays; /** * 冒泡排序 * @author mmz */ public class BubbleSort { public static void BubbleSort(int[] arr) { int temp;//定义一个临时变量 for(int i=0;i<arr.length-1;i++){//冒泡趟数 for(int j=0;j<arr.length-i-1;j++){ if(arr[j+1]<arr[j]){ temp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = temp; } } } } public static void main(String[] args) { int arr[] = new int[]{1,6,2,2,5}; BubbleSort.BubbleSort(arr); System.out.println(Arrays.toString(arr)); } } // 快速排序 public class QuickSort{ public static void quickSort(int[] arr,int low,int high){ int i,j,temp,t; if(low>high){ return; } i=low; j=high; //temp就是基准位 temp = arr[low]; while (i<j) { //先看右边,依次往左递减 while (temp<=arr[j]&&i<j) { j--; } //再看左边,依次往右递增 while (temp>=arr[i]&&i<j) { i++; } //如果满足条件则交换 if (i<j) { t = arr[j]; arr[j] = arr[i]; arr[i] = t; } } //最后将基准为与i和j相等位置的数字交换 arr[low] = arr[i]; arr[i] = temp; //递归调用左半数组 quickSort(arr, low, j-1); //递归调用右半数组 quickSort(arr, j+1, high); } public static void main(String[] args){ int[] arr = {10,7,2,4,7,62,3,4,2,1,8,9,19}; quickSort(arr, 0, arr.length-1); for (int i = 0; i < arr.length; i++) { System.out.println(arr[i]); } } } • 1 • 2 • 3 • 4 • 5 • 6 • 7 • 8 • 9 • 10 • 11 • 12 • 13 • 14 • 15 • 16 • 17 • 18 • 19 • 20 • 21 • 22 • 23 • 24 • 25 • 26 • 27 • 28 • 29 • 30 • 31 • 32 • 33 • 34 • 35 • 36 • 37 • 38 • 39 • 40 • 41 • 42 • 43 • 44 • 45 • 46 • 47 • 48 • 49 • 50 • 51 • 52 • 53 • 54 • 55 • 56 • 57 • 58 • 59 • 60 • 61 • 62 • 63 • 64 • 65 • 66 • 67 • 68
- 100万用户如何根据年龄排序?
可以通过桶排序,基数排序,计数排序
桶排序:桶排序要把数据进行划分到m个桶内,希望的是桶内数据是均匀的,并且桶与桶之间有着天然
的大小顺序。极端情况下时间复杂度会退化为O(nlog n); 比较适合外部排序。在进行划分桶数据的时
候,可能存在桶数据不均匀的情况,可以选择在多的数据桶进行继续划分桶,直到桶数据可以加载到内
存中为止。
计数排序:把一系列的数字统计个数放在数组内A。 依次累加,统计结果还是在同一个数组中存放A。
临时数组C存放排序后的结果。 遍历最初时的数据B,从A中找到该值。 A数组中的值-1就是数据B在临时
数组C存放的位置。 把A数组中的值减1. 循环这个过程。该计数规则只适合数据范围不大的情景 - 深度优先和广度优先搜索算法?
广度优先搜索(Breadth-First-Search),一般简称为 BFS。直观地讲,它其实就是一种地毯式层层推进
的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。
visited,布尔数组,记录顶点是否已经被访问过,访问过则为真,没有访问过则为假,这里用 0 和 1 表
示。
vertex,记录上一层的顶点,也即已经被访问但其相连的顶点还没有被访问的顶点。当一层的顶点搜索
完成后,我们还需要通过这一层的顶点来遍历与其相连的下一层顶点,这里我们用队列来记录上一层的
顶点。
prev,记录搜索路径,保存的是当前顶点是从哪个顶点遍历过来的,比如 prev[4] = 1,说明顶点 4 是通
过顶点 1 而被访问到的。
深度优先搜索(Depth-First-Search),简称 DFS,最直观的例子就是走迷宫。
假设你站在迷宫的某个分岔路口,你想找到出口。你随意选择一个岔路口来走,走着走着发现走不通的
时候就原路返回到上一个分岔路口,再选择另一条路继续走,直到找到出口,这种走法就是深度优先搜
索的策略。
深度优先搜索用的是一种比较著名的思想——回溯思想,这种思想非常适合用递归来实现。深度优先搜
索的代码里面有几个和广度优先搜索一样的部分 visited、prev 和 Print() 函数,它们的作用也都是一样
的。此外,还有一个特殊的 found 变量,标记是否找到终止顶点,找到之后我们就可以停止递归不用再
继续查找了。 - 如何快速获取Top10热门搜索关键词?
使用优先级队列实现 : 优先级队列,顾名思义,它首先应该是一个队列。队列最大的特性就是先进先
出。不过,在优先级队列中,数据的出队顺序不是先进先出,而是按照优先级来,优先级最高的,最先
出队 - 单向链表反转如何实现!
①.迭代反转链表:从当前链表的首元节点开始,一直遍历至链表的最后一个节点,这期间会逐个改变所遍历到
的节点的指针域,另其指向前一个节点。
public static Node reverse2(Node head) { if (head == null || head.getNext() == null) { return head; } // 上一结点等于头节点 Node pre = head; // 当前结点 Node cur = head.getNext(); // 临时结点,用于保存当前结点的指针域(即下一结点) Node tmp; // 当前结点为null,说明位于尾结点 while (cur != null) { //临时节点, 保存当前节点下一个节点 tmp = cur.getNext(); // 反转指针域的指向 cur.setNext(pre); // 指针往下移动 pre = cur; cur = tmp; } // 最后将原链表的头节点的指针域置为null,还回新链表的头结点,即原链表的尾结点 head.setNext(null); return pre; } • 1 • 2 • 3 • 4 • 5 • 6 • 7 • 8 • 9 • 10 • 11 • 12 • 13 • 14 • 15 • 16 • 17 • 18 • 19 • 20 • 21 • 22 • 23 • 24
public static Node Reverse1(Node head) { // head看作是前一结点,head.getNext()是当前结点,reHead是反转后新链表的头结点 if (head == null || head.getNext() == null) { // 若为空链或者当前结点在尾结点,则直接还回 return head; } // 先反转后续节点head.getNext() Node reHead = Reverse1(head.getNext()); // 将当前结点的指针域指向前一结点 head.getNext().setNext(head); // 前一结点的指针域令为null; head.setNext(null); // 反转后新链表的头结点 return reHead; } ③.头插法反转链表:在原有链表的基础上,依次将位于链表头部的节点摘下,然后采用从头部插入的方式生成 一个新链表,则此链表即为原链表的反转。 ```java public ListNode reverseList(ListNode head) { ListNode dum = new ListNode(); ListNode pre = head; while (pre!=null){ ListNode pNext = pre.next; pre.next = dum.next; dum.next = pre; pre = pNext; } head = dum.next; return head; } • 1 • 2 • 3 • 4 • 5 • 6 • 7 • 8 • 9 • 10 • 11 • 12 • 13 • 14 • 15 • 16 • 17 • 18 • 19 • 20 • 21 • 22 • 23 • 24 • 25 • 26 • 27 • 28 • 29 • 30 • 31
- 如何判断链表是否有环?
使用追赶的方法,设定两个指针slow、fast
从头指针开始,每次fast指针前进1步, slow指针前进2步。
如存在环,则两者相遇;如不存在环,fast遇到NULL退出 - 如何找到单向链表的中间元素
设定两个指针slow、fast
从头指针开始,每次fast指针前进1步, slow指针前进2步
当fast指针走到链表结尾位置, slow指针所处位置的元素就是中间元素.
十二、 工具使用 【3道】
- 在Linux中如何查看tomcat日志, 命令是什么?
tail -f tomcat日志所在路径 - maven推送自己写的工具包项目到公司maven私服供其他组员使用的命令?
mvn deploy 可以推送到公司内部的私服服务器,
其他组员maven的settings.xml中可以配置公司私服的地址,
然后项目的pom文件中写上这个jar包的坐标, 就会自动从公司私服下载这个依赖包, 项目中就可以直接使
用. - 使用maven项目如何打包部署
使用mvn package命令进行打包
War包项目打包后将打包好的文件上传到服务器tomcat的webapps目录, 重启tomcat
Springboot项目打包后是jar包, 将打包好的文件上传到服务器, 执行java -jar xxx.jar启动运行项目.