招商银行外包面试经历
面试录音文字整理(26分05秒)
开场
面试官:方便开一下摄像头吗? 候选人:摄像头方便。 面试官:行,你先做个自我介绍。
自我介绍 & 项目介绍
候选人:好的,此除省略1000+文字
面试官:业务这方面的话,其实这不是核心,因为你的业务跟我们业务完全就是不搭边的。问几个技术问题吧。
过滤器 & 拦截器
面试官:过滤器和拦截器了解吗? 候选人:过滤器拦截器,这个了解。过滤器的话,它就是Servlet的过滤器,相当于实现这个Filter接口,当它这个相当于你可以根据它这个Request的一些参数进行判断,也可以调用这个Response对象写一些东西。一般,如果一般过滤器常用的作用就是对这个,一般做对请求做一些拦截,比如说request的一些URI之类的东西,判断一下,如果不合理,相当于就直接response写一个什么无效之类的,直接return,有效的调用filter点doFilterChain。
拦截器的话,它是SpringMVC里面的一个机制,相当于它这个的话,你要实现SpringMVC的HandlerInterceptor接口,它里面提供了三个方法,一个叫preHandle,一个叫postHandle,一个叫afterCompletion。preHandle就是它最终Spring的目标,controller目标方法之前,执行前它会调这个preHandle,如果这个方法你返回了false,它就不会去调用目标方法。
postHandle就是目标方法执行之后,afterCompletion就相当于目标方法执行之后,这个响应写回之后,它会调这个afterCompletion。HandlerInterceptor的话,它最终Spring去调用的时候,它相当于它是在这个DispatcherServlet里面,根据你这个request对象去获取到它这个handler execution chain,相当于他把这个拦截器的链,还有相当于你最终要具体处理的那个handler method一起获取到。
相当于在调用handler method之前,他就会循环去遍历这个handler interceptor的preHandle方法,如果其中有一个失败,他直接请求就结束了,postHandle也不会执行,直接去逆序执行afterCompletion,相当于把执行,比如说执行了5个的preHandle,他再去执行5个的afterCompletion。preHandle是顺序执行,afterCompletion是逆序执行。
面试官:那这个顺序可以编排吗? 候选人:编排,这个顺序是可以编排的,相当于这个编排的话,直接实现这个Spring的Order接口,相当于直接具体给它返回一个顺序,就可以编排。
面试官:实际使用场景,你用的什么拦截器?你用拦截器干了啥?做了啥?在你项目组,你不要说广泛的,在你项目组。 候选人:在我项目组里面,我用它来防重复提交。相当于HandlerInterceptor里面,它这个的话,它处理的preHandle的时候,相当于可以拿到handler method对象,这handler method对象可以获取到方法上的注解信息。我相当于写了一个通用的注解叫RepeatSubmit,相当于可以定义他这个具体用户,同一个用户在多少秒之内只能提交,这个URI多少次数,相当于我拿到这个他对应的URI和用户ID拼成一个key。
面试官:那你这里为什么不用AOP呢? 候选人:AOP,这个其实,因为用AOP其实可以实现同样作用,用这个handler interceptor主要是当时是为了考虑扩展,因为相当于只根据比如说只根据URI去做是比较简单,后续可能可以直接通过request,那个request body生成MD5值,然后生成key,这样的话就相当于几乎可以做基于请求体的级别的来,相当于基于同一次请求的防重复提交。
然后基于AOP其实也差不多的道理,相当于你针对你的目标方法之前,通过这个去实现一个环绕通知方法,直接写一个@Annotation的表达式,也可以达到同样的效果。当时主要是因为考虑到几种场景,一个是扩展,还有一个是相当于我们内部,比如说定时任务调用是获取不到用户编号,所以说就相当于这样的话就限内部一些定时任务的方法,它是调用这个RepeatSubmit注解是没用的,差不多是这个原因。
线程池
面试官:你有用过线程池吗? 候选人:线程池有用过,线程池的话我们这边相当于线程池一般有两种用法嘛。一种就是相当于定时任务去处理一个比较复杂,比较大的任务,耗时任务,相当于把这个耗时任务做一个拆分,拆分成一个个小的任务的一个list,再把这个list,再创建一个CountDownLatch,就是让它的大小等于这个list的大小,相当于初始化一个CountDownLatch的初始化值,再去循环调用那个execute的方法,在每一个任务处理完的finally里面去调这CountDownLatch countDown,再相当于在...
面试官:execute和submit的区别? 候选人:execute方法相当于你直接去执行这么一个,是直接放一个Runnable进去,submit方法它其实是可以放一个Future进去,Future是可以具体获取到那个返回值。
面试官:那你这里线程池参数怎么配? 候选人:线程池参数,一般的话就比较简单的场景,就比如说直接给它配线程池参数,一般就是核心线程数,最大线程数,阻塞队列大小。还有拒绝策略,还有他什么线程工厂,还有线程最大存活时间。一般比较最简单的配置方式,就是看CPU核心数,CPU核心数加一作为核心线程数,CPU核心线程数乘二作为最大线程数。
阻塞队列大小的话,根据你具体业务的处理能力。比如说你相当于你的处理效率比较高的话,你最大线程数设的也比较多,你这样就可以把队列设的稍微大一点。拒绝策略的话,根据你具体业务来看,判断你这个任务能不能丢弃。如果说能丢弃,像那种记日志的,就设置成直接忽略那种。相当于有一部分,比如说不能丢弃的,你可能就是直接给他抛异常,或者说直接交由调用者,就相当于调用线程池的线程去执行,或者说你自己去实现。
至于核心线程数和最大线程数,拒绝策略,我这边相当于用到的几种拒绝策略的话,就是两种,一种是我前台写了一个日志拦截器,相当于基于一个filter去记日志。他用户操作了之后,我在这个请求处理之前,就调用线程池,发一条消息到消息队列,把他用户的ID,还有他那个信息发进去,这时候就用Discard的拒绝策略。如果说这个线程的处理能力不够,相当于直接把这个拒绝掉,不要影响到用户正常的前台请求过来。
至于任务执行的时候,我是使用相当于交给调用者,进调用者的线程来处理。因为我这种情况下不是不能丢任务,如果丢任务的话,可能会导致后面出脏数据等一系列问题。所以说当线程池处理能力不够的时候,调用者线程来处理,一般线程池就是。
面试官:那他会阻塞吗?他会阻塞后面的任务吗? 候选人:他会阻塞,相当于他执行不完的时候,他就没办法再往里再添加新的任务了,所以这种一般。
面试官:为什么你要用这个策略呢? 候选人:这种因为一般我们处理涉及到线程池的主要都是后台的定时任务,这种的话相当于定时任务多执行一会也没关系,就是线程池主要是提速的。像针对这个场景,就是针对你这个调用方线程,他也对于这个,就是他吞吐量有比较高要求的这种,我觉得可以就是自己实现一个拒绝策略。比如说你把这个任务记到表里面,或者说发到MQ,表里的话就交给定时任务,他去扫描去补偿,MQ的话就交给消费者去处理。
定时任务 & 分布式锁
面试官:那定时任务你用了什么?用了哪个? 候选人:定时任务的话,就是定时分布式任务调度框架嘛。 面试官:对,用的哪个框架? 候选人:分布式任务调度框架,其实这是一个公司自研的分布式调度框架。我们这边只需要配置它调度服务器的服务端的地址,在它像页面上面配置一些,比如说任务执行的Cron表达式,还有它需要方法需要的参数,相当于它就可以具体去执行。
面试官:那你知道它其中一些设计的原理吗?比如说我多实例怎么保证它的幂等性? 候选人:幂等性,这个我有看过它框架的一些源码。它其实幂等性就有点类似于用Redis分布式锁的机制,相当于它任务,它在创建一个任务开始执行的时候,它先尝试,把任务相当于设置到Redis hset进去,最终执行完的时候,相当于把Redis上的数据做删除。中间如果它多次启动,它基于Redis,相当于它加Lua脚本的机制,就把多次启动给拦截掉。
这个过程中当然可能避免不了一些脏数据,比如说他执行到一半挂了,相当于执行到一半挂了,他其实基于心跳,他有ZK心跳监测机制,他是能知道这个任务挂了,但是有少数场景,Redis有脏数据,这个时候可以在他那个前台点一下清除任务流水,相当于把这个删除,也可以再起起来。
微服务 & ZK分布式锁
面试官:那你微服务组件里用过哪些? 候选人:微服务组件的话,微服务组件用的比较多的是公司的微服务组件。公司它有一套自研的RPC框架,它相当于调用方,它框架它是用ZK来当注册中心,相当于服务方启动起来的时候,它把它相关的一些服务信息,还有它的实例的IP端口信息注册到ZK上面,之后调用方。
面试官:你知道ZK为什么能弄分布式锁吗? 候选人:这个我知道,ZK分布式锁其实是基于它的几个机制,相当于ZK它有一个临时顺序节点。相当于你往ZK里面,你单独建一个lock的目录,多个线程去同时去创建临时顺序节点,其中只有一个线程能创建到最小的节点,创建到最小的节点的值,它就拿到锁。同时临时节点又比较依赖会话的心跳监测,如果它挂了,它相当于锁也会自动释放。
面试官:它跟Redis的分布式锁有什么区别? 候选人:区别,它俩的区别其实,Redis首先它吞吐量比ZK大很多。相当于你高并发的场景,一般推荐用Redis,还有第二个特点,Redis的一致性其实不如ZK。Redis它比如说它的数据同步是一个异步同步,相当于你master挂了之后,还没,数据还没有同步到slave的时候,它相当于这个时候,它slave被选举成新的master,你这个上面其实没有锁的数据,有可能导致两个线程同时获取到锁。
ZK的话,它是相当于你客户端请求过来,它一定要保证它所有的最终写操作,事务操作全部交给master,就是相当于master在同步给所有的子节点成功之后才返回成功,它的一致性比较高,但是吞吐量比较低。业务上就是像这种框架它使用的是ZK,但是实际业务使用还是建议用Redis分布式锁来保证这吞吐量,同时在基于这个,比如说数据库层面,可以针对...
CAP原理
面试官:你知道CAP原理吗? 候选人:知道。 面试官:那Redis是基于什么呢?它是CAP的里面哪种模式? 候选人:Redis它其实是能保证AP,它不能保证强一致性,因为它是异步同步,顶多可以通过这个,比如说设置,比如说设置这个Redis,它保证数据同,至少同步到一个从节点。才返回。
面试官:ZooKeeper它是哪种? 候选人:ZooKeeper它其实保证的是CP,它像,相当于保证的是强一致性,但是它并不能保证高可用。比如说Zookeeper,它master挂了,它这段时间要重新选举新的master。
面试官:对,那所以说刚才那回答刚才那个问题,这两个的,它两个的适用场景,你为什么说一定要建议使用Redis呢? 候选人:使用Redis的话,相当于因为业务上,比如说你这个用系统有很多用户去访问,如果你这个Redis它比较,相当于它如果挂了的话,影响的时间比较长,影响你这个系统的使用。所以说这个时候一般建议使用这个Redis作为分布式锁,相当于它。
面试官:那我需要强一致性呢?那我需要强一致性怎么办? 候选人:强一致性,你需要强一致性的话,就是要看具体需要什么程度,如果说你特别需要强一致性,可能就要用ZK分布式锁。如果说你可以兼容一下,可以考虑配Redis的参数,相当于设置它主节点至少同步到一个从节点之后,才返回给客户端成功。这样的话其实一定程度上一致性也提高了,同时也能保证。
微服务之间的调用。微服务之间的RPC调用的话,我们公司用的它其实是它一个自研的RPC调用。它相当于通过它有一个类似于,它有一个客户端注册调用注册中心的一个组件,去获取到对应的服务的列表。在进行具体调用的时候,它可能要获取从这个列表之中负载均衡,或者说基于负载均衡的选一个地址,去具体去调用,调用完之后。
面试官:就是你不是说,那你实际写的时候会考虑哪些啊?用你们那个自研的RPC里面,它有哪些功能呢? 候选人:它的功能主要就是,比如说相当于你需要调用其他中心的一个接口,相当于直接可以通过它这个组件去,去写它提交中心的编码,还有对应的他们服务的实现的类名,还有方法名直接就可以调用。你需要考虑的。
面试官:除了这个调用功能之外,它有额外的哪些功能呢? 候选人:额外的功能,它支持一个服务的熔断和降级。比如说熔断的话是我们在配置里面会配置,它比如说在10秒之内,调用接口,如果80%都异常的话,就会触发它这个熔断保护,就相当于直接降级,走我们这个默认实现。相当于同时的话,他等到基本上就是这种配置,就是一个熔断降级。
线上问题排查
面试官:有排查过现在线上的生产问题吗? 候选人:线上的生产问题,线上生产问题有排查过,比如说我们那边当时,比如说我们这边消息,我们这边要消费订单中心几百万的消息,相当于消费几百万的消息的话,他这个吞吐量如果下降的话,我们消息就会积压,这个时候就要去排查它消息积压的原因。我们我们这边其实设计的时候就有考虑过这种情况,相当于针对每一个工单的每一个明细,记录它处理的开始时间和结束时间,相当于我们具体去排它每一个,相当于排那些比较慢的规则,相当于把这个规则的SQL拉出来,然后再去排这个SQL的。
面试官:为什么会积压呢? 候选人:为什么会积压?因为它比较依赖消息,比较依赖你客户端的ACK机制嘛,相当于你客户端不ACK,它这个消息队列没办法把这个消息给删掉,所以说客户端处理能力比较慢,就不ACK,它这个积累的越来越多就积压,所以说这时候。
面试官:为什么你们那里消费的时候会处理的比较慢呢?它的根本原因是什么? 候选人:处理的比较慢,其实根本原因是业务逻辑比较慢,根本原因可以从几个方面去分析,一是你这个代码里面有没有去调比较慢的接口,二是你这个SQL里面有没有慢SQL,相当于没走索引的SQL。或者说走了临时表或者filesort的这种SQL。三的话就是,比如说你这个代码逻辑本身就有一些问题,可能有一些死锁之类的,导致它比较慢。就是目前是这几种常见的原因。
面试官:那你遇到过OOM的问题吗? 候选人:OOM的问题的话,这个基本上线上实际遇到的,实际也有遇到OOM的问题,就比如说之前用定时任务去处理大批量任务的时候,去处理大批量数据的时候,有出现OOM。后来其实我们也是,相当于也是判断是程序设计的不合理,相当于针对这个OOM,比如说可能我们就是不要去同时加载太多的对象,比如说加载,比如说可能分布去处理,相当于处理完这一批提交事务再处理下一批,相当于不要同时处理这么一批,相当于把业务上去改造,来减少同时在堆内存里面堆积太多的对象,长期使用对象。
一般前台相关的话,其实OOM相对来说少一点,主要是后端处理大批量数据的时候会有OOM。前台除非说代码里写的哪里有问题,比如说你把一些对象,一些长期使用的对象,给它放到static里面,相当于这种垃圾回收器没办法回收,这种才会导致,或者说你代码里面去调用一些,比如说一些IO流的操作,没去释放,导致内存泄漏。
正常情况下,其实前台相关的,因为我们系统QPS没有那么高。相当于平时平时的情况下,基本上就大约两三千左右,到月底峰值相当于营业员赶业务,也只能达到四五千。除非是定时任务这种消息消费者这种,要处理很多订单中心的数据,或者定时任务这种出现问题。
收尾
面试官:嗯,我这边就没什么其他问题了,看你有什么想问的。 候选人:我们公司它使用的是什么技术栈? 面试官:Spring Cloud那一套嘛。 候选人:哦,Spring Cloud Alibaba这一套。 面试官:对。 候选人:嗯,好的,那我这边也没什么问题,然后我们这个QPS大约有多少? 面试官:我们这是主要做数据处理的。 候选人:哦,也是类似于我们这种消息消费,相当于数据处理。 面试官:你单服务的实例能达到十几个。 候选人:嗯,好的,我明白,那我这边没有什么问题了,面试官。 面试官:嗯,好。那今天就先这样,后面有结果他们会通知你的。 候选人:嗯,好的好的,谢谢。
