个人面试
更新: 6/26/2025 字数: 0 字 时长: 0 分钟
这里的问题并非全都是面经,还有一些是我的提前准备
这里需要注意的是,在面试的时候一定要胆子大,也就是敢说,就算一个问题你不会,也要试图用会的内容尝试解决他,引导面试官去问你会的内容。
如果真不会,或者回答不上来(也就是大脑一片空白了),别磕巴,直接说没了解过或者没遇到过,这样至少能给面试官留下一个较好的性格印象。
还有就是不需要所有的都去编,因为有的问题可能就是学生自己完成不了的(比如你写你自己完成了一套特别特别nb的系统,然后面试官一问实际情况你一点回答不出来),这里你就实际的去说就好。但是你不能应该会的不会(比如一些微服务或者特别底层的可以不会,但是你的项目中遇到的问题或者具体实现的方式如果都不知道的话那可太扯了)
个人介绍
面试官您好,我目前大学在读,正在读大二,是软件工程专业。我平日主要使用Java与Kotlin语言通过SpringBoot框架进行后端方向的开发。熟悉使用MySQL,Redis等技术栈,同时学习过SpringCloud,SpringAI等框架,具有一定的分布式项目与Ai项目的开发能力。平日我会将我自己学习的内容更新到我的Github个人主页上,目前也已经有了五六万行的数据量。然后项目部分的话,这两个项目是我自己设计开发的,已经使用Git同步更新到了Github上,第一个项目主要是一个涉及到分布式的图文交流论坛,第二个则是基于SpringAI框架的本地Agent服务。
项目中遇到过什么问题
在分布式项目中,我曾经遇到过Gateway结合SaToken的时候在网关层通过Id获取Token获取不到的问题,当时这个问题我发现只会在第一次请求这个用户的Token的时候出现,于是我去查询了Satoken的仓库的issue,发现并没有人提过这个issue,于是我只能自己在Ai的帮助下尝试解决这个问题,最后我通过他的异常发现可能是Satoken第一次请求的时候没有初始化导致的,于是我查询了Satoken的Filter的Order,发现他的值是-100,于是我将我自己网关层的Filter的Order降到了-90,进而在Satoken的Filter之后进行,从而保证了Satoken的Filter一定在我之前完成,经过实际测试,这一操作确实也解决了第一次获取Token获取不到的问题
除此之外我还遇到过一个ThreadLocal问题,就是我的系统中有一些并发操作,这些操作中有时会从ThreadLocal中获取数据,但ThreadLocal是线程专有的,因此会出现问题,最开始我没有想到这一点,所以项目中出现了问题,于是我又了解到可以使用InheritableThreadLocal
,但是他只适用于严格的父子线程,而我的项目中使用的线程池,因此我只能再找解决方案,最后我使用了阿里开源的TransmittableThreadLocal
来解决这个问题,解决了在使用线程池或异步执行框架时,InheritableThreadLocal
不能传递父子线程上下文的问题。
你的项目开发流程
因为我的项目只有我一个人进行开发,所以我进可能的尝试去模拟正式的一个开发流程,由于我的分布式项目使用了DDD的开发模式,所以我会先尝试去进行一个小的建模,去找到一个问题域的聚合根,然后争取将业务集中在domain中完成。部署的话我会现在服务器上部署一套CI/CD系统,然后通过Git提交项目后进行一个自动化的部署。
你说你用了CI/CD的流程,能介绍一下是如何实现的吗
好的,早期的话我的CI/CD系统的话主要是使用了Gitea+Jekins进行的部署,Jekins自动的获取Gitea的git信息,当git提交数据到Gitea之后就会自动的拉取Gitea仓库中的数据进行项目的构建与部署。
后来由于我的项目模块变多,一个个的通过上传到Gitea上拉取部署过于不便了,所以我决定使用打包Docker镜像的方式来进行部署,这里也就将项目改成了Gitea+Harbor+Jekins+Docker+Docker File的形式进行部署(引入了Harbor)。然后通过在项目中编写Docker File文件来创建Docker镜像,让Jenkins自动获取仓库中的DockerFile文件在进行自动的使用Docker构建镜像,然后上传到Harbor到,再通过Docker拉取Harbor中的Docker镜像来部署项目
你这是个微服务项目,那你统计过微服务模块的数量以及部署需要的服务器吗
这里我选择诚实说,因为我确实没有进行过微服务的实际部署,这也是可以接受的,毕竟每个人的学校水平不同,我所在的学校环境无法为我提供一个大型的微服务环境,我也没有实力自己去开好几台服务器进行部署。
有的问题如果不会实话实说就好,毕竟有的情况是可以接受的。如果接受不了(不论是你还是面试官)那只能去背,不过这就要保证你得不露馅了
总共应该有十个左右模块,然后的话由于这个项目是我自己在开发,我也没有能力和环境去支付高昂的服务器费用,所以只是在我自己的电脑上启动虚拟机部署了一下,根据测试大概需要10+g的内存才能运行。
我看你提到了DDD,说说DDD是什么
啊,DDD的话就是领域驱动设计嘛(Domain Driven Design),是一种以Domain为核心的设计模式,争取将所有的业务集中在Domain层或者说是领域对象中解决。在我的项目中使用了六边形架构,每一个模块的话都依赖于核心的Domain模块,(然后对外的过程依赖于infrastructure模块)力争让项目结构分工明确
Redis Key重复了怎么办
这里就是一个问题,面试官可能想问的是Redis底层的HashMap如何解决的Hash冲突问题,但是你不会,你可以尝试用业务的方式解答一下,万一面试官不接着往下问了呢
因为系统采用了Leaf框架进行分布式Id生成,因此一般不会产生Key重复的情况,所以我会先检查是不是代码哪里写出了问题导致的Key重复,如果不是代码问题那么我会选择使用Lua脚本来对redis进行一个先查询再存入的过程。如果还是无法避免的话可能就要考虑是不是我的业务整体设计的存在问题。
实际,面试官可能是想表达Hash冲突怎么办
这是一个数据结构结合真实实现的问题,HashMap真实的情况是会根据你传入的Key去计算Key的hash值,也就是说不论什么类型的Key(Redis中都是String,但Java并非如此),都会被计算成为一个Hash值,然后通过这个Hash值去分配地址,这个Hash值大概率是不会冲突的,但也确实可能存在两个不同的Key最后计算的Hash一样的情况,这时就需要解决Hash冲突,Redis中采取的是链地址法方案,也就是说如果oldKey的值和newKey的值冲突了,会将newKey插在oldKey的位置,然后newKey中专门分配一块内存用来指向oldKey,即让oldKey插在newKey的后面(尾插法)
Redis内存不足怎么办
Redis是基于内存的,所以Redis中存放的数据一般都是一些热点的讯息,如果Redis内存不足,那就要查看是不是有的数据应该去主动的给他设计一个过期时间,让他去自动的过期删除掉,如果实在没有问题,那就可能要考虑是不是需要去增加物理内存
Redis的常用数据结构
Redis是键值对数据库,也就是所有的数据都是K-V的形式存在,其中K肯定是String类型,所以这里提及的是V的区别
String,Set,ZSet,List,HashMap,以及一些拓展类型(布隆过滤器,bitmap)
我看你压测了Leaf框架,你是如何完成的?
我使用了Jmeter来对我的服务进行压测,并且取得了较好的效果。
我先是在我的服务里专门启动了一个对外的API,他只提供一个创建Id的作用,然后我在Jmeter里会开启多个线程组,模拟多个并发用户进行车市,并且运行了一段时间模拟高峰期。我查询Jmeter的聚合报告和吞吐量图得到了项目的性能。
Leaf是个怎样的框架
Leaf提供了号段模式和SnowFlake模式,我在项目中使用的是号段模式,他需要在数据库中引入Leaf的特定表并且进行一定的配置,然后他会从数据库中批量的获取ID然后在内存中分配,减少数据库的压力,除此之外Leaf还支持一种snowflake模式,他是基于zookeeper实现的,通过获取机器ID和时间戳来进行生成一个唯一的雪花ID
我看你使用了Nacos?你用Nacos做了什么,为什么要使用Nacos?
首先Nacos是Spring Cloud Alibaba体系中的一个配置中心,用来管理微服务,由于我的微服务中存在一些一样的配置内容,所以我将他们一并放到了Nacos中,然后使用Nacos进行统一的配置,这样就减少了我重复写配置文件的过程,除此之外,Nacos还提供了动态更新配置的功能,这使得我不再需要只为了修改一下配置文件就要重启服务,Nacos还提供了监视心跳的功能,来监控微服务是否健康。而且项目中的其他的Cloud组件也需要通过Nacos中对微服务的命名来寻找微服务。
RBAC具体在你的项目中是如何体现的,为什么选择Satoken,如何在Gateway中实现的鉴权,Gateway中如何实现的Id转发
RBAC就是Role-Based-Access-Control嘛,也就是通过将权限分配给角色,再将角色分配给用户的方式来完成的权限管理,实现了用户与权限的解耦。
使用Satoken主要是因为他相对Spring Security更加轻量化,更加易用。而且他是国内的开源框架,提供一流的中文文档与技术支持,我认为这也是在技术选型上值得思考的一点。
鉴权方面的话Satoken自己就提供了针对请求地址的拦截器,然后Gateway会根据传入的请求头中携带的Token值来进行鉴权认证。
至于Id转发,我在Gateway中使用了GlobalFilter这个全局过滤器,让每个请求在经过网关的时候先通过这个过滤器,然后将userId与其具体的值塞到请求头中,这样就可以将这个值传递给下游了。
而下游我也是通过一个OncePerRequestFilter来进行过滤,将Header中的Id取出,然后放到TransmittableThreadLocal里面,这样就实现了下游一次请求对整体userId的获取
我看你同时引入了Guava和Sentinel进行限流,这是为什么
Sentinel是Spring Cloud Alibaba的限流组件,可以在全局层面进行一个流量的限制,根据流量的QPS,并发线程数等进行限流,这也是项目的第一层限流策略,而Guava则是整个项目的另一层限流策略,通过在本地进行限流,对一些已经有预期会高并发的api进行限流,同时还给Sentinel做了兜底,保证了高可用
有MySQL了为什么还要使用Cassandra?
因为MySQL对长文本的存储并不友好,InnoDB中基于行来存储数据,而行又在页中,一页最大16kb,若出现行溢出页,则会在页中保留一个指针指向溢出的页,这样的机制会造成更多的磁盘I/O,而长文本很可能出现溢出页的情况,因此必须考虑使用一个新的数据库解决这个问题。
Cassandra本身支持分布式,去中心化的操作,而且支持宽列存储,可以将长文本内容作为某个列的值存储,而无需严格遵循预定义的固定Schema,因此适合我们现在的情况。
为什么不使用其他的DB,你实际考察过吗?
是的,我在选型的时候还考虑了MogoDB和Redis,但是Redis基于内存,而内存价格过高,显然不适合;然后就是MogoDB,虽然MogoDB自身对表结构要求相对宽松,但是其不支持事务操作,这就导致了数据一致性难以保证,我认为这在后端中是不被允许的,因此我放弃了MogoDB
我看你提到有使用Caffeine作为二级缓存,如何实现的
首先构建多级缓存是为了应对缓存失效的情况,进而避免对数据库照成过大的压力,其次就是Caffeine是本地缓存,其效率更高,因此适合一些特别热点的数据,能够及时快速的返回。
至于避免缓存雪崩,是因为我在使用缓存的时候会给Redis与Caffeine的过期时间设置加上一个随机数,这样就保证了二者不会同时垮掉,进而就预防了缓存雪崩。
Redis的三种常见问题?
啊,您说的是缓存击穿,缓存雪崩和缓存穿透吧。
缓存击穿的话是热点Key过期导致的大量数据打到服务器,这一点的话是使用二级缓存的随机时间解决。
缓存雪崩的话是通过单个key添加随机时间完成。
缓存穿透是将恶意key设置到黑名单中
我看你还使用了布隆过滤器?怎么用的?为什么要用?
布隆过滤器被用在我的项目中的点赞收藏模块,由于这两个模块大概率会有很高的并发量和内存占用,因此必须使用一个较好的数据结构来解决。
布隆过滤器可以快速的判断一个数据是否在过滤器中,但其本身存在误判可能(存在的可能不存在)与不允许删除的问题,因此还要解决这个问题。
由于我的业务中不允许误判,所以我增加了这样的业务设计。
首先如果返回已经点赞就需要向下查询,下一层中我使用的是ZSET进行缓存,我为ZSET设置了最大内存为100,也就是只缓存用户的最近查看的100个点赞,避免过大的占用内存。
这样如过返回没点赞一定是正确的,因为没点赞只能是用户一次赞都没点过,而点赞取消和误判的情况都被算在已点赞的情况,放到下一层去处理。
ZSET和BITMAP不行吗
ZSET返回速度和内存占用都太高
BITMAP采用的是偏移量进行判断,也即是第n位的1或0表示有与无,而我们存放的是noteId,如果使用雪花算法肯定是放不下的。而号段模式的话由于使用的是偏移量,因此如果两个noteId差距较大也会出现这个问题。
ES如何优化的你的项目?
ES采用的是倒排索引的形式,会构建一个词-id-全文的索引格式,十分适合我们的模糊收缩,而MySQL自带的模糊搜索效率近似全部查询一边,因此不适合。
Cancel是将自己伪装成一个MySQL的从库,进而同步MySQL的数据,然后再将MySQL操作同步到ES当中