面试题目
记录下社招面试拼多多的总结与心得,以及失败的原因吧。
1、服务注册是如何发现的,eureka的基本原理, 容器ip是动态的还是静态的
答:eureka, 基本原理大概说个一些:包括服务注册,服务发现,心跳机制,服务下线, 自我保护机制等等。
2、转账,支付如何保证数据一致性的, 说一下分布式事务的实现, 消息的生产和消费机制。
这个基本上说出个一二三来。其实就是分布式事务,保证这个数据的一致性就是要保证事务的原子性。即,事务要么全部成功,要么全部失败。我就提了下XA协议和TCC模式,具体如何实现的我也不太清楚。
3、mysql 索引优化,子查询优化
这里基本上都讲出来了。之前做过很多压测,包括让sql走上索引,参数表添加缓存等等。
4、线上有排查过什么问题.
5、MQ的实现原理
5、图算法题
还有些问题已经忘了。
总结与回顾
其实关于这次面试,我还是没有做好完全的准备,而且是近三年以来的第一次面试,心里难免还是有点紧张。导致我有些东西知道的知识可能一时半会想不起来。后面把这些问到的知识点再复习一下。基本上只是浅浅的了解了一下,细说一下底层原理我就懵了。大概知道我们有这么个流程,知道哪里出了问题该找谁来看。因为现在吧,大公司基本上就是这么个情况, 包括中间件团队,数据库团队,DTF团队,DCF团队等等。基本上我们只用知道这些东西有,然后找相应团队的负责人帮忙看下问题就能解决。我们都是在脚手架上做着CURD。
但是还是要把面试问到的东西基本原理做一个小小的总结和记录:
Eureka
Eureka是Netflix开源的一款提供服务注册和发现的产品, 开源地址为 Eureka, 注册中心是分布式开发的核心组件之一
而eureka是spring cloud推荐的注册中心实现, Eureka是一个REST (Representational State Transfer)服务 它主要用于AWS云,用于定位服务,以实现中间层服务器的负载平衡和故障转移,我们称此服务为Eureka服务器
Eureka也有一个基于java的客户端组件,Eureka客户端,这使得与服务的交互更加容易,同时客户端也有一个内置的负载平衡器,它执行基本的循环负载均衡。
自我保护机制
自我保护机制主要在Eureka Client和Eureka Server之间存在网络分区的情况下发挥保护作用,在服务器端和客户端都有对应实现.
假设在某种特定的情况下(如网络故障), Eureka Client和Eureka Server无法进行通信,此时Eureka Client无法向Eureka Server发起注册和续约请求,Eureka Server中就可能因注册表中的服务实例租约出现大量过期而面临被剔除的危险,然而此时的Eureka Client可能是处于健康状态的(可接受服务访问),如果直接将注册表中大量过期的服务实例租约剔除显然是不合理的,自我保护机制提高了eureka的服务可用性。
当自我保护机制触发时,Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务,仍能查询服务信息并且接受新服务注册请求,也就是其他功能是正常的。
这里思考下,如果eureka节点A触发自我保护机制过程中,有新服务注册了然后网络回复后,其他peer节点能收到A节点的新服务信息,数据同步到peer过程中是有网络异常重试的,也就是说,是能保证最终一致性的。
服务发现原理
eureka server可以集群部署,多个节点之间会进行(异步方式)数据同步,保证数据最终一致性,Eureka Server作为一个开箱即用的服务注册中心,提供的功能包括:服务注册、接收服务心跳、服务剔除、服务下线等。
需要注意的是,Eureka Server同时也是一个Eureka Client,在不禁止Eureka Server的客户端行为时,它会向它配置文件中的其他Eureka Server进行拉取注册表、服务注册和发送心跳等操作。
eureka server端通过appName和instanceInfoId来唯一区分一个服务实例,服务实例信息是保存在哪里呢?其实就是一个Map中:
|
|
服务注册
Service Provider启动时会将服务信息(InstanceInfo)发送给eureka server,eureka server接收到之后会写入registry中,服务注册默认过期时间DEFAULT_DURATION_IN_SECS = 90秒。InstanceInfo写入到本地registry之后,然后同步给其他peer节点,对应方法com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers。
写入本地redistry
服务信息(InstanceInfo)保存在Lease中,写入本地registry对应方法com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#register,Lease统一保存在内存的ConcurrentHashMap中,在服务注册过程中,首先加个读锁,然后从registry中判断该Lease是否已存在,如果已存在则比较lastDirtyTimestamp时间戳,取二者最大的服务信息,避免发生数据覆盖。使用InstanceInfo创建一个新的InstanceInfo:
|
|
同步给其他peer
InstanceInfo写入到本地registry之后,然后同步给其他peer节点,对应方法com.netflix.eureka.registry.PeerAwareInstanceRegistryImpl#replicateToPeers。如果当前节点接收到的InstanceInfo本身就是另一个节点同步来的,则不会继续同步给其他节点,避免形成“广播效应”;InstanceInfo同步时会排除当前节点。
InstanceInfo的状态有依以下几种:Heartbeat, Register, Cancel, StatusUpdate, DeleteStatusOverride,默认情况下同步操作时批量异步执行的,同步请求首先缓存到Map中,key为requestType+appName+id,然后由发送线程将请求发送到peer节点。
Peer之间的状态是采用异步的方式同步的,所以不保证节点间的状态一定是一致的,不过基本能保证最终状态是一致的。结合服务发现的场景,实际上也并不需要节点间的状态强一致。在一段时间内(比如30秒),节点A比节点B多一个服务实例或少一个服务实例,在业务上也是完全可以接受的(Service Consumer侧一般也会实现错误重试和负载均衡机制)。所以按照CAP理论,Eureka的选择就是放弃C,选择AP。 如果同步过程中,出现了异常怎么办呢,这时会根据异常信息做对应的处理,如果是读取超时或者网络连接异常,则稍后重试;如果其他异常则打印错误日志不再后续处理。
服务续约
Renew(服务续约)操作由Service Provider定期调用,类似于heartbeat。主要是用来告诉Eureka Server Service Provider还活着,避免服务被剔除掉。renew接口实现方式和register基本一致:首先更新自身状态,再同步到其它Peer,服务续约也就是把过期时间设置为当前时间加上duration的值。
注意:服务注册如果InstanceInfo不存在则加入,存在则更新;而服务预约只是进行更新,如果InstanceInfo不存在直接返回false。
服务失效剔除
Eureka Server中有一个EvictionTask,用于检查服务是否失效。Eviction(失效服务剔除)用来定期(默认为每60秒)在Eureka Server检测失效的服务,检测标准就是超过一定时间没有Renew的服务。默认失效时间为90秒,也就是如果有服务超过90秒没有向Eureka Server发起Renew请求的话,就会被当做失效服务剔除掉。失效时间可以通过eureka.instance.leaseExpirationDurationInSeconds进行配置,定期扫描时间可以通过eureka.server.evictionIntervalTimerInMs进行配置。
服务剔除#evict方法中有很多限制,都是为了保证Eureka Server的可用性:比如自我保护时期不能进行服务剔除操作、过期操作是分批进行、服务剔除是随机逐个剔除,剔除均匀分布在所有应用中,防止在同一时间内同一服务集群中的服务全部过期被剔除,以致大量剔除发生时,在未进行自我保护前促使了程序的崩溃。
服务信息拉取
Eureka consumer服务信息的拉取分为全量式拉取和增量式拉取,eureka consumer启动时进行全量拉取,运行过程中由定时任务进行增量式拉取,如果网络出现异常,可能导致先拉取的数据被旧数据覆盖(比如上一次拉取线程获取结果较慢,数据已更新情况下使用返回结果再次更新,导致数据版本落后),产生脏数据。对此,eureka通过类型AtomicLong的fetchRegistryGeneration对数据版本进行跟踪,版本不一致则表示此次拉取到的数据已过期。
fetchRegistryGeneration过程是在拉取数据之前,执行fetchRegistryGeneration.get获取当前版本号,获取到数据之后,通过fetchRegistryGeneration.compareAndSet来判断当前版本号是否已更新。 注意:如果增量式更新出现意外,会再次进行一次全量拉取更新。
Eureka server的伸缩容
Eureka Server是怎么知道有多少Peer的呢?Eureka Server在启动后会调用EurekaClientConfig.getEurekaServerServiceUrls来获取所有的Peer节点,并且会定期更新。定期更新频率可以通过eureka.server.peerEurekaNodesUpdateIntervalMs配置。
这个方法的默认实现是从配置文件读取,所以如果Eureka Server节点相对固定的话,可以通过在配置文件中配置来实现。如果希望能更灵活的控制Eureka Server节点,比如动态扩容/缩容,那么可以override getEurekaServerServiceUrls方法,提供自己的实现,比如我们的项目中会通过数据库读取Eureka Server列表。
eureka server启动时把自己当做是Service Consumer从其它Peer Eureka获取所有服务的注册信息。然后对每个服务信息,在自己这里执行Register,isReplication=true,从而完成初始化。
Service Provider
Service Provider启动时首先时注册到Eureka Service上,这样其他消费者才能进行服务调用,除了在启动时之外,只要实例状态信息有变化,也会注册到Eureka Service。需要注意的是,需要确保配置eureka.client.registerWithEureka=true。register逻辑在方法AbstractJerseyEurekaHttpClient.register中,Service Provider会依次注册到配置的Eureka Server Url上,如果注册出现异常,则会继续注册其他的url。
Renew操作会在Service Provider端定期发起,用来通知Eureka Server自己还活着。 这里instance.leaseRenewalIntervalInSeconds属性表示Renew频率。默认是30秒,也就是每30秒会向Eureka Server发起Renew操作。这部分逻辑在HeartbeatThread类中。在Service Provider服务shutdown的时候,需要及时通知Eureka Server把自己剔除,从而避免客户端调用已经下线的服务,逻辑本身比较简单,通过对方法标记@PreDestroy,从而在服务shutdown的时候会被触发。
Service Consumer
Service Consumer这块的实现相对就简单一些,因为它只涉及到从Eureka Server获取服务列表和更新服务列表。Service Consumer在启动时会从Eureka Server获取所有服务列表,并在本地缓存。需要注意的是,需要确保配置eureka.client.shouldFetchRegistry=true。由于在本地有一份Service Registries缓存,所以需要定期更新,定期更新频率可以通过eureka.client.registryFetchIntervalSeconds配置。
总结
我们为什么要使用Eureka呢,在分布式开发架构中, 任何单点的服务都不能保证不会中断,因此需要服务发现机制,某个节点中断后,服务消费者能及时感知到保证服务高可用。注册中心除了Eureka之外,还有Zookeeper、consul、nacos等解决方案,实现原理不同, 各自适用于不同业务场景。
数据一致性问题
事务
严格意义上的事务实现应该是具备原子性、一致性、隔离性和持久性,简称ACID。
而通俗意义上事务就是为了使得一些更新操作要么都成功,要么都失败。
分布式事务
分布式事务顾名思义就是要在分布式系统中实现事务,它其实是由多个本地事务组合而成。
对于分布式事务而言几乎满足不了ACID,其实对于单机事务而言大部分情况下也没有满足ACID,不然怎么会有四种隔离级别呢?所以更不用说分布在不用数据库或者不同应用上的分布式事务了。
2PC
2PC(Two-phase commit protocol),中文叫二阶段提交。 二阶段提交是一种强一致性设计,2PC 引入一个事务协调者的角色来协调管理各参与者(也可称之为各本地资源)的提交和回滚,二阶段分别指的是准备(投票)和提交两个阶段。
注意这只是协议或者说是理论指导,只阐述了大方向,具体落地还是有会有差异的。
让我们来看下两个阶段的具体流程。
准备阶段协调者会给各参与者发送准备命令,你可以把准备命令理解成除了提交事务之外啥事都做完了。
同步等待所有资源的响应之后就进入第二阶段即提交阶段(注意提交阶段不一定是提交事务,也可能是回滚事务)。
假如在第一阶段所有参与者都返回准备成功,那么协调者则向所有参与者发送提交事务命令,然后等待所有事务都提交成功之后,返回事务执行成功。
假如在第一阶段有一个参与者返回失败,那么协调者就会向所有参与者发送回滚事务的请求,即分布式事务执行失败。
那第二阶段提交失败的话呢?
这里有两种情况。
第一种是第二阶段执行的是回滚事务操作,那么答案是不断重试,直到所有参与者都回滚了,不然那些在第一阶段准备成功的参与者会一直阻塞着。
第二种是第二阶段执行的是提交事务操作,那么答案也是不断重试,因为有可能一些参与者的事务已经提交成功了,这个时候只有一条路,就是头铁往前冲,不断的重试,直到提交成功,到最后真的不行只能人工介入处理。
大体上二阶段提交的流程就是这样,我们再来看看细节。
首先 2PC 是一个同步阻塞协议,像第一阶段协调者会等待所有参与者响应才会进行下一步操作,当然第一阶段的协调者有超时机制,假设因为网络原因没有收到某参与者的响应或某参与者挂了,那么超时后就会判断事务失败,向所有参与者发送回滚命令。
在第二阶段协调者的没法超时,因为按照我们上面分析只能不断重试!
协调者故障分析
协调者是一个单点,存在单点故障问题
假设协调者在发送准备命令之前挂了, 还行,等于事务没开始。
假设协调者在发送准备命令之后挂了,这就不太行了,有些参与者等于都执行了处于事务资源锁定的状态。不仅事务执行不下去,还会因为锁定了一些公共资源而阻塞系统其他操作。
假设协调者在发送事务回滚命令之前挂了,那么事务也是执行不下去,且在第一阶段那些准备成功参与者都阻塞着。
假设协调者在发送回滚事务命令之后挂了,这个还行,至少命令发出去了,很大概率都会回滚成功,资源都会释放。但是如果出现网络分区问题,某些参与者将因为收不到命令而阻塞着。
假设协调者在发送提交事务命令之前挂了,这个不行,这下所有资源都阻塞着。
假设协调者在发送提交事务命令之后挂了,很大概率都会提交成功,然后释放资源。但是如果出现网络分区问题某些参与者因为收不到命令而阻塞着。
协调者故障,通过选举得到新的协调者
因为协调者单点问题,因此我们可以通过选举等操作选出一个新协调者来顶替。
如果处于第一阶段,其实影响不大都回滚好了,在第一阶段事务肯定还没提交。
如果处于第二阶段,假设参与者都没挂,此时新协调者可以向所有参与者确认它们自身情况来推断下一步的操作。
假设有个别参与者挂了!这就有点僵硬了,比如协调者发送了回滚命令,此时第一个参与者收到了并执行,然后协调者和第一个参与者都挂了。
此时其他参与者都没收到请求,然后新协调者来了,它询问其他参与者都说OK,但它不知道挂了的那个参与者到底O不OK,所以它傻了。
问题其实就出在每个参与者自身的状态只有自己和协调者知道,因此新协调者无法通过在场的参与者的状态推断出挂了的参与者是什么情况。
虽然协议上没说,不过在实现的时候我们可以灵活的让协调者将自己发过的请求在哪个地方记一下,也就是日志记录,这样新协调者来的时候不就知道此时该不该发了?
但是就算协调者知道自己该发提交请求,那么在参与者也一起挂了的情况下没用,因为你不知道参与者在挂之前有没有提交事务。
如果参与者在挂之前事务提交成功,新协调者确定存活着的参与者都没问题,那肯定得向其他参与者发送提交事务命令才能保证数据一致。
如果参与者在挂之前事务还未提交成功,参与者恢复了之后数据是回滚的,此时协调者必须是向其他参与者发送回滚事务命令才能保持事务的一致。
所以说极端情况下还是无法避免数据不一致问题。
talk is cheap 让我们再来看下代码,可能更加的清晰。以下代码取自 Distributed System: Principles and Paradigms。
这个代码就是实现了 2PC,但是相比于2PC增加了写日志的动作、参与者之间还会互相通知、参与者也实现了超时。这里要注意,一般所说的2PC,不含上述功能,这都是实现的时候添加的。
|
|
至此已经详细分析了2PC的各种细节,总结如下:
2PC是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。
当然具体的实现可以变形,比如Tree 2PC、Dynamic 2PC
2PC适用于数据库层面的分布式事务场景,而我们业务需求有时候不仅仅关乎数据库,也有可能是上传一张图片或者发送一条短信。
而且像Java中的JTA, 它是基于XA规范实现的事务接口,这里的XA可以简单理解为基于数据库的XA规范来实现的2PC。
解决方案
XA方案
2PC的传统方案是在数据库层面实现的,如 Oracle、MySQL 都支持 2PC 协议,为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型及接口标准,国际开放标准组织 Open Group 定义了分布式事务处理模型DTP(Distributed Transaction Processing Reference Model)。
整个 2PC 的事务流程涉及到三个角色 AP、RM、TM。AP 指的是使用 2PC 分布式事务的应用程序;RM 指的是资源管理器,它控制着分支事务;TM 指的是事务管理器,它控制着整个全局事务。
(1)在准备阶段 RM 执行实际的业务操作,但不提交事务,资源锁定
(2)在提交阶段 TM 会接受 RM 在准备阶段的执行回复,只要有任一个RM执行失败,TM 会通知所有 RM 执行回滚操作,否则,TM 将会通知所有 RM 提交该事务。提交阶段结束资源锁释放。
XA方案的问题
需要本地数据库支持XA协议。 资源锁需要等到两个阶段结束才释放,性能较差。
Seata方案
Seata 是由阿里中间件团队发起的开源项目 Fescar,后更名为 Seata,它是一个是开源的分布式事务框架。
传统 2PC 的问题在 Seata 中得到了解决,它通过对本地关系数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并且对业务 0 侵入的方式解决微服务场景下面临的分布式事务问题,它目前提供 AT 模式(即 2PC)及 TCC 模式的分布式事务解决方案。
Seata 的设计思想如下: Seata 的设计目标其一是对业务无侵入,因此从业务无侵入的 2PC 方案着手,在传统 2PC的基础上演进,并解决 2PC 方案面临的问题。
Seata 把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务。
Seata实现2PC与传统2PC的差别
架构层次方面:传统 2PC 方案的 RM 实际上是在数据库层,RM 本质上就是数据库自身,通过 XA 协议实现,而 Seata 的 RM 是以 jar 包的形式作为中间件层部署在应用程序这一侧的。
两阶段提交方面:传统 2PC无论第二阶段的决议是 commit 还是 rollback ,事务性资源的锁都要保持到 Phase2 完成才释放。而 Seata 的做法是在 Phase1 就将本地事务提交,这样就可以省去 Phase2 持锁的时间,整体提高效率。