SpringCloud篇面试题
# Ribbon是如何获取服务器列表的?
当controller通过调用restTemplate.getForObject()方法最终会调用到LoadBalancerInterceptor的intercept方法,
通过截取请求uri获取服务名称
进到execute方法,这里会通过服务名称去获取一个负载均衡器
再进到getLoadBalancer方法

最终进到getInstance方法
通过getInstance方法,最后会调用SpringClientFactory父类的getInstance方法
再进去
重点看这个createContext方法
在这段代码里面,new了一个AnnotationConfigApplicationContext类,这个类就是Spring的上下文对象,然后调用context的register方法,为容器注册了一个RibbonClientConfiguration配置类,最后调用context的refresh方法,会走spring 容器的生命周期流程,简单说就是帮我们注册了ribbon客户端的配置类对象到容器中,然后进入到这个配置类我们可以看到一个关键类ILoadBalance
他返回了一个ZoneAeareLoadBalancer对象
可以看到继承了DynamicServerListLoadBalancer,当ZoneAeareLoadBalancer创建时会调用父类的构造方法
父类构造方法又会调用restOfInit方法

由于这里使用的注册中心是nacos所以是nacos的实现
这里会调用到NacosNamingService里面的selectInstances方法

这里默认subscribe是true,最终会调用 hostReactor.getServiceInfo()方法

重点关注scheduleUpdateIfAbsent方法,方法名有点定时更新的意思
Future相信都不陌生,点进addTask看一下
不难看出来这是一个定时的一个任务提交,每隔1000毫秒执行一次,然后再看看传参
很明显UpdateTask实现了Runnable是一个线程类,回到addTask方法里面,这里会调用一个定时任务去执行UpdateTask的run方法
点进去可以看到调用了serverProxy.queryList,看上方官方注释也能看到这个方法是获取当前的服务列表
再进到这个方法中
这里首先封装http接口需要的请求参数,最终通过http请求调用nacos提供的api接口,到这里整个Ribbon底层如何获取注册中心的服务列表就已经完成了
面试话术
在调用restTemplate.getForObject()后,最终会调用loadBalaner的execute方法,在这个方法中会获取loadBalander实例,实例是通过加载服务配置类从容器中获取的,当缓存中没有我们要调用的微服务上下文对象RibbonClientConfiguration时就会去初始化,创建实例。
在RibbonClientConfiguration中默认是会创建ZoneAvoidanceRule这个负载均衡对象,在创建对象时又会调用父类DynamicServerListLoadBalancer的构造方法,此时就会开启定时拉取服务列表功能,追根源的话就是调用HostReactor这个类的scheduleUpdateIfAbsent方法,开启异步线程,每隔1000ms,也就是一秒拉取一次列表更新缓存。所以容器中负载均衡对象所持有的服务列表是一直在更新的,在获取到服务名称后直接从负载均衡对象中获取即可。
# Feign接口调用超时怎么办?
面试话术
这个我在单机测试的时候经常遇到,电脑不好、网络带宽的原因都有可能会出现这个报错,Feign的远程调用超时本质是ribbon的负载均衡超时,呃,或者说是其中一个原因吧,Feign和ribbon都有Readtimeout和ConnectTimeout这两个参数,就是读取超时时间和连接超时时间,默认都是1秒,而且不设置的话优先是使用ribbon的默认时间。
# Feign接口调用的时候怎么鉴权?
/**
* feign自定义拦截器
* 实现微服务之间参数传递
* 服务端可以通过HttpServletRequest获取到前面传递的参数
*/
@Component
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
requestTemplate.header("userId",request.getHeader("userId"));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
面试话术
可以通过配置Feign拦截器来进行鉴权,利用权限表,在远程调用时通过feign拦截器判断用户id是否一致
# SOA和微服务的区别?
先来回顾一下集群和分布式的特点
- 集群强调的是节点与节点之间,功能是对等的,每个节点都是全量部署,具有高可用性
- 分布式强调的是节点与节点之间交互协调对外提供服务
在分布式的早期是缺少标准的,节点间的交互存在缺少维护,需要人为的管理的问题,不同节点交互的接口协议也不同,造成调用关系复杂混乱,这时SOA的出现就是为了解决这个问题。
** SOA**(全称:Service Oriented Architecture),中文意思为 “面向服务的架构”,你可以将它理解为一个架构模型或者一种设计方法,而并不是服务解决方案。其中包含多个服务, 服务之间通过相互依赖或者通过通信机制,来完成相互通信的,最终提供一系列的功能。一个服务通常以独立的形式存在与操作系统进程中。各个服务之间通过网络调用 。
跟 SOA 相提并论的还有一个 ESB(企业服务总线),简单来说ESB就是一根管道,用来连接各个服务节点。为了集成不同系统,不同协议的服务,ESB作为企业应用中枢的存在,提供了一个中心化的注册中心,最核心的功能在于格式转换,ESB 可以简单理解为:它做了消息的转化解释和路由工作,让不同的服务互联互通;
(opens new window)
SOA的问题
- 各个子系统之间没有统一的通信标准,导致系统间通信与数据交互间变得异常复杂
- ESB的中心化思想,成为整体应用流量的瓶颈
- ESB成本超级高,没有好用的开源,被大厂绑架
- ESB属于重量级产品,部署规划异常笨重
微服务
微服务架构提出了去中心化思想,给出了一套标准:将一个单体应用拆分为多个小型服务,每个服务独立运行独立部署和开发,并且以轻量级机制通信(HTTP REST API),允许使用不同语言以及不同的存储技术。
微服务架构其实和SOA架构类似,微服务是在SOA上做的升华。微服务架构重点强调的一个是"业务需要彻底的组件化和服务化",比如SOA中的ESB企业服务总线,涵盖了消息转化、路由、服务通信,在微服务架构中就将其拆分为多个组件,如注册中心、网关、消息队列等等,各个组件独立运行,应用也是根据服务划分模块,每个模块也是独立运行。
微服务的特征
- 通过服务实现组件化
- 按业务能力来划分服务和开发团队
- 去中心化
- 基础设施自动化(devops、自动化部署)
SOA 和微服务架构的区别
- 思想上的差异,SOA是中心化思想,所用的组件都集中在ESB企业总线中,微服务是去中心化思想,去掉ESB企业总线。微服务不再强调传统SOA架构里面比较重的ESB企业服务总线,同时SOA的思想进入到单个业务系统内部实现真正的组件化
- Docker容器技术的出现,为微服务提供了更便利的条件,比如更小的部署单元,每个服务可以通过类似Node或者Spring Boot等技术跑在自己的进程中。
- SOA注重的是系统集成方面,而微服务关注的是完全分离
面试话术:
SOA也是一款面向服务的框架跟微服务想比我认为他们的相似度还是非常高的,微服务可以说是SOA架构的一种优化,或者说升华。
其中最核心的区别在于思想上的差异,SOA是中心化思想,为了解决早期分布式架构服务间调用关系复杂,服务地址、服务状态难以管理的问题,提出使用一个中枢处理服务发现与服务间通信等问题,其实现就是ESB(企业服务总线),这个实现也伴随着许多问题,由于这个ESB负责了路由、注册中心等功能,导致ESB变得非常笨重,并且开发成本非常高,以及所有服务的通信都需要经过这个ESB,这个ESB就成为了整体应用流量的瓶颈,系统的流量大小取决于ESB的性能。
而微服务是去中心化思想,为优化ESB存在的问题,微服务将这个ESB的功能拆分为多个小组件,比如注册中心组件、网关组件、负载均衡组件等等,并且这些组件都独立运行,就是微服务所强调的彻底组件化和服务化,使一个重量级组件拆分为多个轻量级的小组件,大大减少了开发的成本,减轻了系统的维护难度,降低了各服务间的耦合度。
# SpringCloud有哪些核心组件?
面试话术:(从服务发现开始分析)
- 注册中心组件:Eureka、Nacos
- 远程调用组件:OpenFeign、Dubbo
- 负载均衡组件:Ribbon
- 服务保护组件:Hystrix、Sentinal
- 服务配置管理:SpringCloudConfig、Nacos
- 网关组件:SpringCloudGateway、zuul
# SpringCloud vs SpringCloudAlibaba 什么关系?
面试话术
一种包含关系吧,SpringCloud包含SpringCloudAlibaba,可以从依赖那看得出来,alibaba的依赖都是spring-cloud-starter-alibaba什么什么的,像是spring-cloud-starter-alibaba-nacos啊、sentinel啊、seata啊,然后eureka、gateway、feign、ribbon、hystrix、springcloudconfig、slueth这些就是springcloud的。
# REST和RPC的区别
RPC(远程过程调用),两个分布式系统通过网络进行远程的方法调用,作为RPC最典型的特点就是服务器端存在一个存根,客户端也存在相对应的一个存根,通过存根来生成对应的调用接口,底层使用的是socket通信。
REST就显得更为轻量级,使用的是HTTP通信,发送url请求响应json结果
模式比较
| RPC | REST | |
|---|---|---|
| 消息格式 | 二进制Thrift、Protobuf | 文本xml、JSON |
| 通信协议 | TCP | HTTP、HTTP/2 |
| 性能 | 高 | 一般 |
| 接口契约IDL | Thrift、Protobuf IDL | Swagger |
| 客户端 | 强类型客户端 | HTTP客户端即可,OKHttp、HttpClient、UrlConnection |
| 框架 | Dubbo、GRPC、Thrift | SpringMVC、Struts 2等 |
| 开发者友好 | 一般自动生成存根、客户端、使用友好。二进制消息阅读不友好 | JSON文本人机阅读友好 |
| 应用场景 | 服务间通信推荐RPC(性能要求高)/REST(性能要求低) | 对外暴露接口推荐REST |
面试话术:
首先一句话概括:两者不是一个层面上的东西,REST 是一种架构,大部分的REST的实现中使用了RPC的机制,例如spring的RestTemplate、OpenFeign等
使用方式不同
- RPC是强类型客户端,服务器端提出的要求,在客户端也必须生成相对应的接口
- REST就没有这么定性的要求,只要客户端持有一个HTTP的工具包即可,如HttpClient、OKHttp等
从性能上比较
- RPC使用的是TCP,更加底层,传输效率会更高
- REST使用的是HTTP,功能性会更强,但是传输效率会相对更低一些
- RPC的消息格式是二进制的,REST通常是文本或者JSON字符串,自然RPC消息传输的性能也会更好
应用场景上比较
- 若是对执行效率比较敏感的应用,推荐使用RPC
- 若是对应性能要求不是那么极致的话,又考虑到其维护性,兼容性,那么更多的考虑使用REST
面向对象不同
- RPC,是面向方法的。可自定义协议,优化数据的传输。主要用于分布式系统之间,服务模块之间的通信。
- REST,是面向资源的。更加注重接口的规范。通用性更强。
# eureka的工作原理
1、各个服务将自己注册到eureka注册中心服务,eureka保存各个服务的服务名称与服务列表的映射关系
2、服务调用者定时去eureka注册中心拉取服务信息副本保存到本地
3、在进行服务调用时服务调用者通过服务名称从本地服务信息副本获取被调用服务的服务列表信息
4、通过负载均衡从服务列表中选择一个具体的实例发起实际请求
# 请解释一下SpringCloud中Feign接口调用的过程
面试话术
Feign的接口调用写在消费方嘛,然后消费方在调用feign接口的时候会扫描@FeginClient注解获取服务名,方便根据这个服务名去服务中心拉取服务列表,然后服务降级的fallback实现也是在这个注解配置的,然后就是扫描调用接口上的@GetMapping @RequestParam等然后拼接需要调用的url路径和参数值,接着,在底层利用JDK动态代理产生代理对象,代理对象底层检测本地是否有缓存最新的注册列表,没有则使用RestTemplate去注册中心拉取列表,然后通过负载均衡算法计算出路由地址,拼接完整请求地址,向服务提供方发出请求,获取响应结果,进行解析封装成对象。
# 服务框架的演变
**单体架构:**将业务的所有功能集中在一个项目中开发,打成一个包部署
优点:
- 架构简单
- 部署成本低
缺点:
- 耦合度高
**分布式架构:**根据业务功能对系统进行拆分,每个服务模块作为独立项目开发运行,称为一个服务
优点:
- 降低服务耦合
- 有利于服务升级扩展
缺点:缺少一套标准
- 服务拆分粒度如何?
- 服务集群地址如何维护?
- 服务之间如何实现远程调用?
- 服务健康状态如何感知?防止级联失败
微服务:一种经过良好架构设计的分布式架构方案
特点:
- 服务拆分粒度更小,分布式根据业务功能对系统进行模块拆分,微服务根据服务进行拆分,要求做到单一职责,每一个服务都对应一种业务能力, 比如用户模块拆分为会员积分服务、会员货币服务、会员登录服务等等。
- 微服务对外暴露接口,方便服务之间的相互调用。
- 允许团队独立、技术独立、数据独立、部署独立,允许不同人员,不同语言,不同数据存储技术的使用,只要对外暴露公用的接口即可。
- 由于拆分粒度更小,耦合度更低,所以只要做好服务降级、隔离、容错之类的就可以避免级联问题的出现
# Nacos的服务注册表结构是怎样的?
面试官问这题是希望回答两点
- Nacos的分级存储模型
- Nacos的分级存储模型的源码实现
Nacos的分级存储模型

Nacos的分级存储模型的源码实现
粗略描述
Nacos服务器实现就是一个springboot项目,并且是基于springmvc的一个web项目,Nacos对外暴露的所有接口都是RESTful风格的,所以可以通过controller找到很多服务的入口。
通过官方提供的文档 (opens new window)我们可以找到服务注册的接口是/nacos/v1/ns/instance,这个接口所在于nacos-naming这个模块下的InstanceController中。
我们先来看一下官方文档规定的请求参数
可以看到有环境名、分组名、服务名、ip、端口、健康状态、是否为临时实例,分级存储模型所需要的信息全都在请求参数中了,再查看接口方法
在解析完请求所携带的所有参数后,将解析后的环境id、服务名、实例对象传到ServiceManager的registerInstance方法中进行注册逻辑,在这个ServiceManager类的成员变量声明中就可以找到nacos的分级存储模型实现。
也就是下面这个结构
面试话术:
Nacos的注册表结构关乎于Nacos的一个分级存储模型。也就是namespace--group--service--cluster--instance这么一个结构模型,通过namespace进行一个环境隔离,再通过group进行一个功能分组便于管理,一般使用的就是默认的DEFAULT_GROUP,在服务变的非常多的时候才会进行一个分组,service就是每个服务模块的名字,服务的实例又分了集群进行管理,集群一般是以地区进行划分。
在Nacos底层是使用ConcurrentHashMap来实现这个分级存储模型的,简单说就是一个Map嵌套结构,比如第一层Map的泛型是String,Map,String对应的就是环境名Map就是该环境下各个分组的一个Map,然后分组的Map就是一个String,Service的一个Map,String就是分组名Service就是该分组下的Service对象,然后这个Service对象里又维护了一个Map存放了当前服务名下的集群名称及其对应的集群对象,然后这个Cluster对象里就使用了一个Set集合来存放服务实例了,就是这么一个Map层级嵌套结构。
# Nacos如何应对高并发的注册压力?
服务注册核心方法:
大致描述这个方法做的事情:
这个方法做了三件事,创建服务到注册表,从注册表中获取服务对象,最后添加示例到服务对象中。
首先是创建服务到注册表,注册表就是一个map层级嵌套结构嘛,所以其实就是往map里put一个服务对象,然后进入这个方法后会判断是否为首次注册,首次注册则会创建空服务,而且创建的时候会通过双检锁保护并发下才单例创建问题,若不为空则直接退出方法。
服务是单例的,服务下的实例是多例的
接着就是通过环境id和服务名从注册表中获取这个服务对象,然后做一个非空判断,为空直接就抛异常,不为空就进入添加示例的逻辑方法。
添加示例的时候,会通过环境id、服务名以及是否为临时实例生成一个唯一标识,可以理解成服务id,该服务下的所有示例都共享这个id,然后从注册表中获取服务对象,以服务对象为锁进行操作,避免了一个并发的修改,也是进行了一个并发安全的处理。然后他拷贝注册表中旧的实例列表,结合新注册的实例,得到最终的一个实例列表,最后就是将最终的实例列表更新到本地注册表以及同步给Nacos集群中的其他节点。
然后关键的来了,在更新最终实例列表到本地注册表的时候,他其实是将服务id和更新实例列表的这么一个事件放进了阻塞队列,这就是nacos做的一个性能优化,将服务需要进行的操作存入了阻塞队列,然后异步的进行一个执行阻塞队列中的任务,从而达到提高并发能力。
更新本地列表的这个service类在初始化的时候,会利用线程池开启异步任务,然后来看一下这个任务:
经典的生产者消费者模式,写了个死循环,不断的从阻塞队列中获取任务,而且由于是阻塞队列,只有当队列中存在任务时才会唤醒线程,没有任务时会wait等待,所以即使是死循环也不会造成资源消耗。
所以说更新本地实例列表是一个异步更新,同样的,同步给集群中的其他节点也是一个异步任务,先把任务丢到队列中,利用线程池来执行队列中的任务。
面试话术
首先肯定是要做一个集群,进行负载均衡来减轻单节点的注册压力,然后Nacos对服务注册这块也做了优化。我之前有稍微看过这部分的源码,Nacos对于高并发注册服务的优化写在了addInstance方法里,就是一个添加实例到服务的方法,这个方法我记得主要做了俩件事。
第一件事:从注册列表中拷贝旧的实例列表,然后添加新的实例到列表中,返回一个最终的实例列表
第二件事:将最终的实例列表同步到本地以及集群中的其他节点
其优化就在第二件事中,将最终的实例列表同步到本地以及集群中的其他节点都是异步执行的,在这个方法中只是将服务id和更新列表的一个事件放入了阻塞队列,最终是开启了一个独立线程池来执行队列中的任务。
也就是说Nacos内部接收到注册的请求时,不会立即创建实例,而是将服务注册的任务放入一个阻塞队列就立即响应给客户端。然后利用线程池读取阻塞队列中的任务,异步来完成实例更新,从而提高并发写能力。就是服务注册到时候,Nacos的主线程只管添加任务到队列中就完了,请求业务非常的短,业务越短,执行性能就越强,并发能力也就越强。
# Nacos如何处理并发读写问题
面试话术
我记得nacos在更新实例列表的时候用的是CopyOnWrite技术,先将旧的实例列表拷贝一份,就是拷贝service服务对象中的clusterMap,然后修改都是在备份上修改,读取是直接读取的缓存中的旧列表,当实例的修改与更新操作完毕,就会以当前服务对象为锁去进行一个clusterMap的覆盖,或者说将引用指向更新好的备份列表,由于是以服务对象为锁,所以同一个服务间的操作只能串行执行,不会出现并发写的问题。
# Nacos和Eureka的区别
面试话术
nacos和eureka有相同点也有不同点(从接口方式、实例类型、健康检测、服务发现来答)
相同点:
- 两者支持服务注册和服务拉取
- 两者都支持服务者心跳机制实现健康检测(续约)
- 都对外暴露了Rest风格的API接口
不同点:
1)Nacos可以实现服务注册发现,也可以做配置管理;Eureka只能做服务注册发现。
2)Nacos临时实例心跳不正常会被剔除,非临时实例(永久实例)则不会被剔除;而Eureka只能注册临时实例,实例失效会被剔除(Eureka不支持永久实例)
spring.cloud.nacos.discovery.ephemeral=false 创建永久实例
3)Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式;而Eureka只有心跳模式;
4)Nacos支持服务列表定时拉取和主动通知模式,服务列表更新更及时,减少服务调用失败的机率;而Eureka采用被动定时服务列表拉取更新;
# Sentinel 和 Hystrix 的区别?
面试话术
sentinel是springcloudalibaba的,Hystrix是springcloud的,然后:
- 隔离方式不同,Sentinel采用信号量,Hystrix默认采用线程池,信号号量性能比线程池好,更轻量,线程池的话虽然是支持了异步调用以及主动超时,但是线程的创建也会造成额外的开销,调用的服务越多,需要的线程池数量就越多,开销就会成倍增加。
- 熔断策略不同,Sentinel可以基于超时和异常比例,Hystrix只支持异常比例
- 限流功能不同,Sentinel有丰富限流功能(QPS,链路模式、关联模式等),Hystrix限流功能很弱
- 第三方框架整合,Sentinel可以整合SpringCloud和Dubbo,Hystrix只能整合SpringCloud
# sentinel的限流与gateway的限流有什么区别
**滑动窗口计数器算法:**以时间跨度为窗口限制流量数,计算当前时间-时间跨度后的窗口到当前窗口中的请求总数,判断是否超过阈值。在内存中的实现相当于是每一个时间区间都是一个数组,时间划分的越细对内存的消耗也就越大。
**令牌桶算法:**以固定的速率生成令牌存入桶中,如果桶满后,多余的令牌丢弃,进入的请求必须获取到令牌才可以被处,没有令牌的请求都会被拒绝。具体的实现是基于一种算法,用一个计数器记录上一次请求的时间以及已经取了几个令牌,比如一秒钟内来了两个请求,发现上一次请求还在这一秒内,那就根据速率去推算令牌还够不够。
**漏桶算法:**将请求放入桶中,以固定速率处理请求,可以理解成在桶内派对等待,sentinel在实现漏桶算法时采用了排队等待模式,让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行,并发的多个请求必须等待,基于这么一个算法:预期的等待时间=最近一次请求的预期等待时间+允许的间隔。如果请求的等待时间超出最大时长则会被拒绝,也就是队列已满。比如OPS=5,意味着每200ms处理一个请求,timeout为2000,意味着预等待超过2000ms的请求会被拒绝。
面试话术
限流算法常见的有三种实现:滑动时间窗口、令牌桶算法、漏桶算法。Gateway则采用了基于Redis实现的令牌桶算法,而Sentinel内部比较复杂:
- 默认限流模式是基于滑动时间窗口算法。
- 排队等待的限流模式基于漏桶算法,在内存中是一种等待队列的实现。
- 而热点参数限流则是基于令牌桶算法,是基于一种算法判断两个请求的时间间隔允许的流量数。
为什么没用gateway做限流就是因为gateway的限流还要依赖Redis,会对Redis产生额外的压力,然后呢限流的规则也比较单一,不一定能满足业务的场景。而sentinel的限流就比较强大,而且有图形化界面,也方便运维去做管理,然后sentinel还能跟网关整合,就挺好的。
# 项目中使用了熔断机制吗?
面试话术
有的,我们项目使用sentinel作为熔断机制。sentinel实现服务降级有线程隔离和熔断降级嘛,然后两者最大的区别我觉得是线程隔离是占用线程数达到某个值后降级,熔断是响应超时比例,就是慢调比例或者异常比例达到某个值时降级。
然后具体使用的话就是在需要进行熔断降级的服务那边配置yml,开启feign对sentinel的支持,然后先访问一下那个接口,让sentinel监控到,接着在sentinel那配置一下参数,比如什么慢调比例啊,异常比例啊,熔断时长之类的。然后还可以创建一个feign接口服务降级的实现类,实现那个FallbackFactory接口,编写降级逻辑,响应友好提示什么的。
# 请问熔断和降级的区别?
面试话术
这两个是一个因果关系吧,降级是熔断的结果,熔断是一个Sentinel或Hystrix框架的一个自带的机制,该机制统计超时比例就是慢调比例或异常比例达到某个阈值时,就会发生降级。
降级本质就是一个Fallback实现类,返回给用户友好信息。
线程隔离和熔断都能实现服务降级的效果。
# 能不能大概解释一下熔断器的执行流程?

面试话术
首先,熔断器就是一个超时比例或异常比例的统计程序,该程序放在服务消费方,当超时比例或异常比例没有达到阈值,熔断器处于关闭状态,请求可以正常通过。
但是,当超时比例或异常比例到达阈值,熔断器开启,所有请求会立即被降级,请求降级后会执行我们定制的Fallback接口,返回给用户友好提示信息。
接着,熔断器会等待一段时间(默认5s),然后进入半开状态,半开状态会放行一个请求尝试调用,如果失败,继续保持打开状态,如果成功,则回到关闭状态。
# 你了解过CAP定理和BASE理论吗?
面试话术
有的,C指的是一致性、A指的是可用性、P是指容错性,CAP定理认为,在服务存在容错性的前提下,一致性和可用性只能存在一个,也就是只能存在CP或者AP。
然后BASE就是对CAP的一种补充,BA指的是基本可用,S代表软状态、E代表最终一致性,BASE认为选择了一致性只是损失了部分可用,但系统还处于基本可用,以及选择可用性,也只是存在一种数据不一致的临时状态,这种软状态结束后最终还是会达成一致性,就主张不是那么绝对。
# 项目中有用到分布式事务么?什么技术?Seata有哪些模式?你们用的哪个模式?
面试话术
有的,我们用的是阿里巴巴的Seata来提供分布式事务管理。Seata有四种模式,分别是XA、AT、TCC、SAGA,我们项目中使用了AT和TCC,大部分服务用的是AT,只有个别对性能有需求的才用的TCC。
# 请问Seata的AT模式是AP还是CP?那大概解释一下Seata的AT模式的原理
面试话术
Seata的AT模式是属于强可用的,也就是AP。
AT模式原理的话,我记得大概是这样的,Seata架构中不是有三个组件嘛,TM、RM、CT,然后我们在想要加入分布式事务管理的接口上加个@GlobalTransactional,这样就列入TM全局事务的管理了,然后向全局事务表和分支事务表添加记录的操作,四个模式都有这里就不说了。
AT模式的特别在于他还依赖另外两张表,一张全局锁表lock_table还有一张保存数据快照的表undo_log,在分支事务的SQL执行前后会做一个数据的前后快照到undo_log,而且他的分支事务执行是直接提交的,不需要阻塞,这也是AT模式属于强可用的原因,最后全部分支事务执行完后,TM给TC发送检测请求,检测结束后,若状态正常则向所有RM发出请求删除undo_log中的数据快照,如果出现需要回滚的情况,就会让所有RM根据前快照进行数据还原,覆盖。然后这里就可能会出现脏写的问题,所以seata利用全局事务锁表,在每个分支事务提交之前,判断是否能获取全局事务锁,决定是否提交,这样就能防止脏写。但是比较用了全局锁,还是会对性能有一定的影响。
# 请大概解析一下Seata的TCC模式的原理?
面试话术
TCC模式其实跟AT很像,最大的区别我觉得是其中的那些分布式事务管理的逻辑需要我们自己编写,这个还挺头疼的。
大致的一个流程就是执行分支事务的时候会执行我们编写的try方法,然后CT那边检测分布式事务状态正常的话就会走我们的confirm方法,需要回滚就走我们的cancel方法。因为TCC是依赖于一个资源预留的机制嘛,就相当于AT的undo_log,数据快照那张表,资源预留表的字段是我们可以自定义的,所以try方法里就是写我们进行资源预留的逻辑,confirm就是成功提交的逻辑,成功就删除预留记录,cancel就是回滚的一个逻辑,回滚就根据预留记录进行一个逆向操作嘛,具体逻辑就看具体业务了,然后还得留意一下空回滚和业务悬挂的问题,就是try的时候判断一下有没有cancel过,然后cancel的时候判断一下有没有try过。哦,使用TCC模式还得去声明一个TCC接口,来用于编写一个具体的逻辑,用那个@LocalTCC标记接口为TCC业务接口,然后在其中一个方法上加个@TwoPhaseBusinessAction来告诉Seata哪个方法是try哪个方法是confirm哪个是cancel。最后去实现这个接口来编辑逻辑就行了,总之感觉挺麻烦的这个,但有些时候也确实得用这种模式追求一个较好的性能,因为不需要快照嘛,而且没有全局锁,特别快。
哦,说到这里我想起来一个场景,就是TCC能防止脏写是因为操作的是数值的回滚,就比如我减了,我就加回去,但是像是修改名字这种逆向处理的话又跟AT模式一样了,因为是数据覆盖的操作所以避免不了会有脏写。然后得出结论就是TCC更适合用于比如秒杀商品库存变动这类的场景,库存是数量嘛,修改描述啊、名字这类的还是建议用AT更好,可能也有解决办法吧,当时我们也没什么头绪就直接用AT解决了。
# 怎么保证API的安全性?
面试话术
可以用签名验证的方式来保证,比如用户登录,然后分配给用户一个密钥,然后使用md5加密这个密钥+参数,加密成签名字符串,发送的时候发送这个参数和签名,接收端同样的通过密钥拼接参数使用md5加密,加密后的字符串去合签名配对,如果一样则说明参数没有被修改。JWT就可以做这个功能。
# 怎么保证API的幂等性?
面试话术
幂等性也就是说保证请求不重复嘛,那这可以用Redis来做。利用Redis的全局锁,就是setnx那个指令,不可以重复存入相同的key,所以可以在API的请求端,每次刷新页面的时候都利用密钥生成新的token,然后请求携带上这个token,就相当于携带一个请求的唯一标识,然后接受端在获取到这个请求的时候就去尝试将这个token set进Redis,通过setnx这个指令,也就是redistemplate的setIfAbabsent方法,如果返回true则说明这个请求第一次来,如果为false则说明是相同请求,那就不做处理。这种一般用在增删改业务上。
# token过期了怎么办?怎么处理?
面试话术
浏览器每次携带token访问后台后可以取出其中的过期时间,然后判断一下过期时间是否即将到期,比如30分钟的token,判断一下是否还剩10分钟,如果还剩10分钟就重新生成一个30分钟过期的token给浏览器保存,浏览器覆盖之前的token以此达到续期的效果。
# 请问你们的前后端项目开发流程?
面试话术
1)需求分析。分析业务需求,要实现什么功能
2)定义接口。接口包含请求方式,请求路径,请求参数,响应返回值。后端需要和前端工程师商量
3)开发和测试接口。后端一般使用poastman来测试接口
4)和前端联调。找前端一起调试接口。