一些高频面试题

简介: 这篇文章整理了一些高频面试题

商品管理模块

商家通过APP的后台管理系统,将需要上架的商品进行上架,商家上架的商品,用户可以在用户端通过浏览商品列表或搜索来查看商品,如果用户对商品感兴趣的话,可以点击进入商品的详情页,通过查看商品的详情以及相关的一些其他用户的评论和评价,来了解商品的质量和性能,如果用户决定购买商品,就可以将商品添加到购物车中,如果用户确认购买,就会进入到购买流程,在购买流程当中,用户需要选择支付方式,填写收货地址等信息,在用户点击支付的时候,系统会自动生成相关的订单信息,在支付界面当中,如果用户在15分钟内没有完成支付的话,系统就会自动取消订单,如果用户完成支付,系统就会将订单信息保存到数据库当中。这就是商品管理模块的一个业务流程。

我的收藏模块的业务流程

用户可以在APP上浏览商品列表,通过搜索或者浏览分类来找自己感兴趣的商品。当用户看到自己喜欢的商品的时候,只需要点击收藏按钮,就可以将该商品加入到自己的收藏夹中。用户可以通过个人中心或者收藏夹页面来查看自己的收藏夹,在收藏夹页面,用户可以看到自己收藏的商品列表,如果用户在收藏列表中点击某个商品,就会跳转到该商品的详情页,展示更详细的商品信息。如果用户对某个商品不感兴趣了,就可以在收藏夹页面或者商品详情页点击取消收藏按钮,将该商品从收藏夹中移除。

考虑到收藏可能是一个频繁的操作,就是在短时间内,可能用户点击收藏商品,然后又取消了收藏,这时候实际有效的操作是取消收藏,如果我们直接操作数据 库的话,可能 IO 的开销比较大。所以使用 Redis 的 Hash 数据结构来存储用户的收藏信息。其中,大 key 是用户 ID,小 key 是商品 ID,value 可以用 1 表示已收藏,0 表示未收藏,最后就是我们就是使 用 xxl-job 设置了每 3 分钟就将收藏记录同步到数据库,同时删除 redis 数据。

点赞业务模块

面试官:看你项目中介绍,你负责点赞功能的设计和开发,那你能不能讲讲你们的点赞系统是如何设计的?

答:首先在设计之初我们分析了一下点赞业务可能需要的一些要求。

例如,在我们项目中需要用到点赞的业务不止一个,因此点赞系统必须具备通用性,独立性,不能跟具体业务耦合。

再比如,点赞业务可能会有较高的并发,我们要考虑到高并发写库的压力问题。

所以呢,我们在设计的时候,就将点赞功能抽离出来作为独立服务。当然这个服务中除了点赞功能以外,还有与之关联的评价功能,不过这部分我就没有参与了。在数据层面也会用业务类型对不同点赞数据做隔离。

从具体实现上来说,为了减少数据库压力,我们会利用Redis来保存点赞记录、点赞数量信息。然后利用定时任务定期的将点赞数量同步给业务方,持久化到数据库中。

( 注意事项:回答时要先说自己的思考过程,再说具体设计,彰显你的逻辑清晰。设计的时候先不说细节,只说大概,停顿一下,吸引面试官去追问细节。如果面试官不追问,停顿一下后,自己接着说下面的 )

面试官追问:那你们Redis中具体使用了哪种数据结构呢?

答:我们使用了两种数据结构,set和zset

首先保存点赞记录,使用了set结构,key是业务类型+业务id,值是点赞过的用户id。当用户点赞时就SADD用户id进去,当用户取消点赞时就SREM删除用户id。当判断是否点赞时使用SIsMember即可。当要统计点赞数量时,只需要SCARD就行,而Redis的SET结构会在头信息中保存元素数量,因此SCARD直接读取该值的时间复杂度为O(1),性能非常好。

不过这里存在一个问题,就是页面需要判断当前用户有没有对某些业务点赞。这个时候会传来多个业务id的集合,而SISMEMBER只能一次判断一个业务的点赞状态,要判断多个业务的点赞状态,就必须多次调用SISMEMBER命令,与Redis多次交互,这显然是不合适的。(此处略停顿,等待面试官追问,面试官可能会问“那你们怎么解决的”。如果没追问,自己接着说),所以呢我们就采用了Pipeline管道方式,这样就可以一次请求实现多个业务点赞状态的判断了,减少了网络开销

面试官追问:那你ZSET干什么用的?

答:严格来说ZSET并不是用来实现点赞业务的,因为点赞只靠SET就能实现了。但是这里有一个问题,我们要定期将业务方的点赞总数通过MQ同步给业务方,并持久化到数据库。但是如果只有SET,我没办法知道哪些业务的点赞数发生了变化,需要同步到业务方。

因此,我们又添加了一个ZSET结构,用来记录点赞数变化的业务类型和业务id及对应的点赞总数。可以理解为一个待持久化的点赞任务队列。

每当业务被点赞,除了要缓存点赞记录,还要把业务类型(key),业务id(member)及点赞总数(score)写入ZSET。这样定时任务开启时,只需要从ZSET中获取并移除数据,然后发送MQ给业务方,并持久化到数据库即可。

面试官追问(可能会,没追问就自己说):那为什么一定要用ZSET结构,把更新过的业务扔到一个List中不行吗?

答:扔到List结构中虽然也能实现,但是存在一些问题:

首先,假设定时任务每隔2分钟执行一次,一个业务如果在2分钟内多次被点赞,那就会多次向List中添加同一个业务及对应的点赞总数,数据库也要持久化多次。这显然是多余的,因为只有最后一次才是有效的。而使用ZSET则因为member的唯一性,多次添加会覆盖旧的点赞数量,最终也只会持久化一次。

面试官可能说:“那就改为SET结构,SET中只放业务id,业务方收到MQ通知后再次查询不就行了。”如果没问就自己往下说)

当然要解决这个问题,也可以用SET结构代替List,然后当业务被点赞时,只存业务id到SET并通知业务方。业务方接收到MQ通知后,根据id再次查询点赞总数从而避免多次更新的问题。但是这种做法会导致多次网络通信,增加系统网络负担。而ZSET则可以同时保存业务id及最新点赞数量,避免多次网络查询。

不过,并不是说ZSET方案就是完全没问题的,**毕竟ZSET底层是哈希结构+跳表**,对内存会有额外的占用。但是考虑到我们的定时任务每次会查询并删除ZSET数据,ZSET中的数据量始终会维持在一个较低级别,内存占用也是可以接受的。

积分(签到)系统模块

面试官追问:能不能具体说说使用的场景?

答:比如很多的缓存,我们就使用了String结构来存储。还有点赞功能,我们用了Set结构和SortedSet结构。签到功能,我们用了BitMap结构。

就拿签到来说吧。因为签到数据量非常大嘛,而BitMap则是用bit位来表示签到数据,31bit位就能表示1个月的签到记录,非常节省空间,而且查询效率也比较高。

面试官追问:你使用Redis保存签到记录,那如果Redis宕机怎么办?

答:对于Redis的高可用数据安全问题,有很多种方案。

比如:我们可以给Redis添加数据持久化机制,比如使用AOF持久化。这样宕机后也丢失的数据量不多,可以接受。

或者呢,我们可以搭建Redis主从集群,再结合Redis哨兵。主节点会把数据持续的同步给从节点,宕机后也会有哨兵重新选主,基本不用担心数据丢失问题。

当然,如果对于数据的安全性要求非常高。肯定还是要用传统数据库来实现的。但是为了解决签到数据量较大的问题,我们可能就需要对数据做分表处理了。或者及时将历史数据存档。

总的来说,签到数据使用Redis的BitMap无论是安全性还是数据内存占用情况,都是可以接受的。但是具体是选择Redis还是数据库方案,最终还是要看公司的要求来选择。

优惠券管理面试题

面试官:你们优惠券支持兑换码的方式是吧,哪兑换码是如何生成的呢?

答:

首先要考虑兑换码的验证的高效性,最佳的方案肯定是用自增序列号。因为自增序列号可以借助于BitMap验证兑换状态,完全不用查询数据库,效率非常高。

要满足20亿的兑换码需求,只需要31个bit位就够了,也就是在Integer的取值范围内,非常节省空间。我们就按32位来算,支持42亿数据规模。

不过,仅仅使用自增序列还不够,因为容易被人爆刷。所以还需要设计一个加密验签算法。算法有很多,比如可以使用按位加权方案。32位的自增序列,可以每4位一组,转为10进制,这样就有8个数字。提前准备一个长度为8的加权数组,作为秘钥。对自增序列的8个数字按位加权求和,得到的结果作为签名。

当然,考虑到秘钥的安全性,我们也可以准备多组加权数组,比如准备16组。然后生成兑换码时随机生成一个4位的新鲜值,取值范围刚好是0~15,新鲜值是几,我们就取第几组加权数组作为秘钥。然后把新鲜值、自增序列拼接后按位加权求和,得到签名。

最后把签名值的后14位、新鲜值(4位)、自增序列(32位)拼接,得到一个50位二进制数,然后与一个较大的质数做异或运算加以混淆,再基于Base32或Base64转码,即可的对兑换码。

如果是基于Base32转码,得到的兑换码恰好10位,符合要求。

需要注意的是,用来做异或的大质数、加权数组都属于秘钥,千万不能泄露。如有必要,也可以定期更换。

当我们要验签的时候,首先将结果 利用Base32转码为数字。然后与大质数异或得到原始数值。

接着取高14位,得到签名;取后36位得到新鲜值与自增序列的拼接结果。取中4位得到新鲜值。

根据新鲜值找到对应的秘钥(加权数组),然后再次对后36位加权求和,得到签名。与高14位的签名比较是否一致,如果不一致证明兑换码被篡改过,属于无效兑换码。如果一致,证明是有效兑换码。

接着,取出低32位,得到兑换码的自增序列号。利用BitMap验证兑换状态,是否兑换过即可。

整个验证过程完全不用访问数据库,效率非常高。

面试官:你在项目中哪些地方用到过线程池?

答:很多地方,比如我在实现优惠券的兑换码生成的时候。

当我们在发放优惠券的时候,会判断优惠券的领取方式,我们有基于页面手动领取,基于兑换码兑换领取等多种方式。

如果发现是兑换码领取,则会在发放的同时,生成兑换码。但由于兑换码数量比较多,如果在发放优惠券的同时生成兑换码,业务耗时会比较久。

因此,我们会采用线程池异步生成兑换码的方式。

面试官可能会追问:那你的线程池参数是怎么设置的?

答:线程池的常见参数包括:核心线程、最大线程、任务队列、线程工厂、拒绝策略,线程存活时间,时间单位等。

这里核心线程数我们配置的是2,最大线程数是CPU核数。之所以这么配置是因为发放优惠券并不是高频业务,这里基于线程池做异步处理仅仅是为了减少业务耗时,提高用户体验。所以线程数无需特别高。

队列的大小设置的是200,而拒绝策略采用的是交给调用线程处理的方式。

由于业务访问频率较低,所以基本不会出现线程耗尽的情况,如果真的出现了,就交给调用线程处理,让客户稍微等待一下也行。

优惠券领取面试题(超买,超卖)

面试官:如何解决优惠券的超发问题?

答:超发、超卖问题往往是由于多线程的并发访问导致的。所以解决这个问题的手段就是加锁。可以采用悲观锁,也可以采用乐观锁。

如果并发量不是特别高,就使用悲观锁就可以了。不过性能会受到一定的影响。

如果并发相对较高,对性能有要求,那就可以选择使用乐观锁。

当然,乐观锁也有自己的问题,就是多线程竞争时,失败率比较高的问题。并行访问的N个线程只会有一个线程成功,其它都会失败。

所以,针对这个问题,再结合库存问题的特殊性,我们不一定要是有版本号或者CAS机制实现乐观锁。而是改进为在where条件中加上一个对库存的判断即可。

比如,在where条件中除了优惠券id以外,加上库存必须大于购买数量的条件。这样如果库存不足,where条件不成立,自然也会失败。

这样做借鉴了乐观锁的思想,在线程安全的情况下,保证了并发性能,同时也解决了乐观锁失败率较高的问题,一举多得。

面试官:Spring事务失效的情况碰到过吗?或者知不知道哪些情况会导致事务失效?

在我们项目中确实有碰到过,

我记得是在优惠券业务中,一开始我们的优惠券只有一种领取方式,就是发放后展示在页面,让用户手动领取。领取的过程中有各种校验。那时候没碰到什么问题,项目也都正常运行。

后来产品提出了新的需求,要加一个兑换码兑换优惠券的功能。这个功能开发完以后就发现有时候会出现优惠券发放数量跟实际数量对不上的情况,就是实际发放的券总是比设定的要少。一开始一直找不到原因。

后来发现是某些情况下,在领取失败的时候,扣减的优惠券库存没有回滚导致的,也就是事务没有生效。自习排查后发现,原来是在实现兑换码兑换优惠券的时候,由于很多业务逻辑跟手动领取优惠券很像,所以就把其中的一些数据库操作抽取为一个公共方法,然后在两个业务中都调用。因为所有数据库操作都在这个共享的方法中嘛,所以就把事务注解放到了抽取的方法上。当时没有注意,这恰好就是在非事务方法中调用了事务方法,导致了事务失效。

面试官:在开发中碰到过什么疑难问题,最后是怎么解决的?

答:问题肯定是碰到过的。

比如在开发优惠券功能的时候,优惠券有一个发放数量的限制,也就是库存。还有一个用户限量数量的限制,这个是设置优惠券的时候管理员配置的。

因此我们在用户领取优惠券的时候必须做库存校验、限领数量的校验。由于库存和领取数量都需要先查询统计,再做判断。因此在多线程时可能会发生并发安全问题。

其中库存校验其实是更新数据库中的已经发放的数量,因此可以直接基于乐观锁来解决安全问题。但领取数量不行,因为要临时统计当前用户已经领取了多少券,然后才能做判断。只能是采用悲观锁的方案。但是这样会影响性能。

所以为了提高性能,我们必须减少锁的范围。我们就把统计已经领取数量、判断、新增用户领券记录的这部分代码加锁,而且锁的对象是用户id。这样锁的范围就非常小了,业务的并发能力就有一定的提升。

想法是很好的,但是在实际测试的时候,我们发现尽管加了锁,但是还会出现用户超领的现象。比如限领2张,用户可能会领取3张、4张,甚至更多。也就是说并发安全问题并没有解决。

锁本身经过测试,肯定是没有问题的,所以一开始这个问题确实觉得挺诡异的。后来调试的时候发现,偶然发现,有的时候,当一个线程完成了领取记录的保存,另一个线程在统计领券数量时,依然统计不到这条记录。

这个时候猜测应该是数据库的事务隔离导致的,因为我们领取的整个业务外面加了事务,而加锁的是其中的限领数量校验的部分。因此业务结束时,会先释放锁,然后等整个业务结束,才会提交事务。这就导致在某些情况下,一个线程新增了领券记录,释放了锁;而另一个线程获取锁时,前一个线程事务尚未提交,因此读取不到未提交的领券记录。

为了解决这个问题,我们将事务的范围缩小,保证了事务先提交,再释放锁,最终线程安全问题不再发生了。

1-超发问题

面试官:你做的优惠券功能如何解决券超发的问题?

答:券超发问题常见的有两种场景:

  • 券库存不足导致超发
  • 发券时超过了每个用户限领数量

这两种问题产生的原因都是高并发下的线程安全问题。往往需要通过加锁来保证线程安全。不过在处理细节上,会有一些差别。

首先,针对库存不足导致的超发问题,也就是典型的库存超卖问题,我们可以通过乐观锁来解决。也就是在库存扣减的SQL语句中添加对于库存余量的判断。当然这里不必要求必须与查询到的库存一致,因为这样可能导致库存扣减失败率太高。而是判断库存是否大于0即可,这样既保证了安全,也提高了库存扣减的成功率。

其次,对于用户限领数量超出的问题,我们无法采用乐观锁。因为要判断是否超发,需要先查询用户已领取数量,然后判断有没有超过限领数量,没有超过才会新增一条领取记录。这就导致后续的新增操作会影响超发的判断,只能利用悲观锁将查询已领数量、判断超发、新增领取记录几个操作封装为原子操作。这样才能保证线程的安全。

2-锁实现的问题

面试官:那你这里聊到悲观锁,是用什么来实现的呢?

由于在我们项目中,优惠券服务是多实例部署形成的负载均衡集群。因此考虑到分布式下JVM锁失效问题,我们采用了基于Redisson的分布式锁。

(此处面试官可能会追问怎么实现的呢?如果没有追问就自己往下说,不要停)

不过Redisson分布式锁的加锁和释放锁逻辑对业务侵入比较多,因此就对其做了二次封装(强调是自己做的),利用自定义注解AOP,以及SPEL表达式实现了基于注解的分布式锁。(面试官可能会问SPEL用来做什么,没问的话就自己说)

我在封装的时候用了工厂模式来选择不同的锁类型,利用了策略模式来选择锁失败重试策略,利用SPEL表达式来实现动态锁名称。

(面试官可能追问锁失败重试的具体策略,没有就自己往下说)

因为获取锁可能会失败嘛,失败后可以重试,也可以不重试。如果重试结束可以直接报错,也可以快速结束。综合来说可能包含5种不同失败重试策略。例如:失败后直接结束、失败后直接抛异常、失败后重试一段时间然后结束、失败后重试一段时间然后抛异常、失败后一直重试。

(面试官如果追问Redisson原理,可以参考黑马的Redis视频中对于Redisson的讲解)

注意,这个回答也可以用作这个面试题:你在项目中用过什么设计模式啊?要学会举一反三。

3-性能问题

面试官:加锁以后性能会比较差,有什么好的办法吗?

答:解决性能问题的办法有很多,针对领券问题,我们可以采用MQ来做异步领券,起到一个流量削峰和整型的作用,降低数据库压力。

具体来说,我们可以将优惠券的关键信息缓存到Redis中,用户请求进入后先读取Redis缓存,做好优惠券库存、领取数量的校验,如果校验不通过直接返回失败结果。如果校验通过则通过MQ发送消息,异步去写数据库,然后告诉用户领取成功即可。

当然,前面说的这种办法也存在一个问题,就是可能需要多次与Redis交互。因此还有一种思路就是利用Redis的LUA脚本来编写校验逻辑来代替java编写的校验逻辑。这样就只需要向Redis发一次请求即可完成校验。

项目最难的点,技术(TODO)


你们团队用的什么开发模式

我们用的是敏捷开发,适用于需求不稳定、开发周期较短的项目。目前用这种的很多。

需求一直变更,持续集成,持续部署,持续交付,然后使用Jenkins一键部署吧

薪资构成:

面试官我之前公司是:基本工资+绩效(岗位工作+津贴)绩效一般都是拿满的

热点八股文

JVM篇

JVM的主要组成部分?(重点)

1.类加载子系统

2.运行时数据区(包含:程序计数器,方法区,java堆,虚拟机栈,本地方法栈)

3.执行引擎:负责执行字节码指令(包含:解释器,即时编译器,垃圾回收器

4.本地方法接口:允许 Java 调用本地方法库,提供 Java 与其他语言(如 C、C++)交互的接口

介绍一下java堆?

  1. java堆是线程共享的区域,主要用来保存对象实例,数组等信息,内存不够会抛出OOM异常
  2. java堆分为年轻代和老年代,年轻代有三部分组成,Eden区和两个大小相同的幸存者区(Survivor),老年代主要保存生命周期长的对象,一般是一些老的对象。
  3. jdk1.7以前的java堆中有一个永久代,存储的是类信息,静态变量,常量,编译后的代码。1.8移除了永久代,把这些数据 存储到了本地内存的元空间中,防止内存溢出。

(这整个图就是JVM运行时数据区)

什么是虚拟机栈?

每个线程运行时需要的内存称为虚拟机栈(线程私有的),每个栈由多个栈帧组成,对应着每次方法调用时所占用的内存。栈内存一般存的是局部变量和方法调用。

栈内存溢出 -->(递归调用)

什么是类加载器?

JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来。

类加载器有哪些?

启动类加载器 (BootStrap ClassLoader):加载JAVA_HOME/jre/lib目录下的库

扩展类加载器 (ExtClassLoader):主要加载JAVA_HOME/jre/lib/ext目录中的类

应用类加载器 (AppClassLoader):用于加载classPath下的类

自定义类加载器 (CustomizeClassLoader):自定义类继承ClassLoader,实现自定义类加载规则。

什么是双亲委派模型?

当一个类收到了类加载请求时,不会自己先去加载这个类,而是将其委派给父类,由父类去加载,如果此时父类不能加载,反馈给子类,由子类去完成类的加载

JVM为什么采用双亲委派机制?

避免某一个类被重复加载,保证唯一性。

保证类库API不会被修改

说一下类装载的执行过程?(重点)

加载: 查找和导入class文件

链接:链接分为三步骤,验证: 保证加载类的准确性,准备: 为类变量分配内存并设置初始值,解析: 把类中的符号引用转换为直接引用

初始化: 对类的静态变量,静态代码块执行初始化操作

使用: JVM开始从入口方法开始执行用户的程序代码

卸载: 当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象。

对象什么时候可以被垃圾器回收

如果一个或多个对象没有任何的引用指向它了,那么这个对象现现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。

定位垃圾的方式有两种:

引用计数法

可达性分析算法(现在都是这种)(通过GCRoots来探索所有存活的对象)

JVM垃圾回收器有哪些?

Serial GC(串行垃圾回收器)

Parallel GC(并行垃圾回收器)

G1 GC(Garbage-First)

ZGC(Z Garbage Collector)

JVM垃圾回收算法有哪些?

标记清除算法: 垃圾回收分为2个阶段,分别是标记和清除,效率高,有磁盘碎片,内存不连续

标记整理算法: 标记清除算法一样,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低(老年代用这个)

复制算法: 将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收,无碎片,但内存使用率低。(年轻代用这个)

redis篇

你们项目中哪里用到了Redis ?

在我们的项目中很多地方都用到了Redis , Redis在我们的项目中主要有三个作用 :

  1. 使用Redis做热点数据缓存/接口数据缓存
  2. 使用Redis存储一些业务数据 , 例如 : 验证码 , 用户信息 , 用户行为数据 , 数据计算结果 , 排行榜数据等
  3. 使用Redis实现分布式锁 , 解决并发环境下的资源竞争问题

redis的数据类型

Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:

  • 字符串 string,万能存储方案,比如存储商品库存
  • 哈希 hash,一般存储对象结构,比如购物车这种
  • 列表 list,有序的双向链表,一般存储列表数据,比如用户浏览历史,朋友圈这种
  • 无序集合 set,一般存储去重数据,还有比如对于一些高并发的点赞、收藏
  • 有序集合 zset,一般存储需排行的数据,比如排行榜(因为他的底层是压缩列表、跳表和哈希表)

选答:除此之外还有几种高级的数据结构

  • 用来签到的bitmap,做网站点击、访问量统计的hyperloglog,用来做地理坐标检索的geo
  • 面试官,上面提到的跳表、压缩列表、或者哪种数据类型您看是否要给您深入讲解一下

Redis高级数据类型,在哪里用过

1.比如用在签到统计的 Bitmap(位图)本质是 String 类型的扩展,用二进制位表示值,可以节省空间,

2. 适合海量数据统计的 HyperLogLog(基数统计),比如记录当天访问用户

3. 存储地理位置信息的 Geospatial(G O S背血儿)(地理空间),计算距离,查看附近的人等

缓存穿透

候选人:嗯,缓存穿透是指查询一个一定不存在的数据,由于存储层查不到数据因此不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。解决方案的话,

可以缓存空值或特殊值,就是当查询数据库得到的数据不存在,此时我们仍然去缓存数据,缓存一个空值或一个特殊值的数据,避免每次都会查询数据库,避免缓存穿透。

但是我们通常都会用布隆过滤器来解决它。

布隆过滤器

:嗯,是这样的。布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是Redisson实现的布隆过滤器。它的底层原理是,先初始化一个比较大的数组,里面存放的是二进制0或1。一开始都是0,当一个key来了之后,经过3次hash计算,模数组长度找到数据的下标,然后把数组中原来的0改为1。这样,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。当然,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%。其实这个误判是必然存在的,要不就得增加数组的长度。5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。

缓存击穿

嗯!缓存击穿的意思是,对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这个时间点对这个Key有大量的并发请求过来。这些请求发现缓存过期,一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。解决方案有两种方式:第一,可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 SETNX 去设置一个互斥锁。当操作成功返回时,再进行 load db的操作并回设缓存,否则重试get缓存的方法。第二种方案是设置当前key逻辑过期,大概思路如下:1) 在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间;2) 当查询的时候,从redis取出数据后判断时间是否过期;3) 如果过期,则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据可能不是最新的。当然,两种方案各有利弊:如果选择数据的强一致性,建议使用分布式锁的方案,但性能上可能没那么高,且有可能产生死锁的问题。如果选择key的逻辑删除,则优先考虑高可用性,性能比较高,但数据同步这块做不到强一致。

缓存雪崩

嗯!缓存雪崩意思是,设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重而雪崩。与缓存击穿的区别是:雪崩是很多key,而击穿是某一个key缓存。解决方案主要是,可以将缓存失效时间分散开。比如,可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机。这样,每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

Redis持久化策略

Redis 提供了两种方式,实现数据的持久化到硬盘。

  1. RDB 持久化(Redis Database),它是全量的,是指在指定的时间间隔内将内存中的数据集快照写入磁盘。(二进制存储当redis实例宕机恢复数据的时候,可以从RDB的快照文件中恢复数据。
  2. AOF持久化(Append Only File),它是的增量的,它会把服务器所处理的每一个写操作都记录到日志里。(文本命令存储当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。
  3. 在Redis4.0版本支持混合持久化方式,也就是RDB和AOF一起使用,  ( 设置 aof-use-rdb-preamble yes )

Redis数据过期策略

Redis 的过期键处理机制主要依赖惰性删除定期删除两种核心策略,同时配合内存淘汰机制应对内存不足的场景。

惰性删除数据到达过期时间,不做处理。只会在取出 key 的时候才对数据进行过期检查,判断这个数据是否过期,如果未过期,返回数据,如果已过期,就删除,返回nil。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。

定期删除Redis 每隔一段时间主动扫描部分过期键,对已过期的键进行删除

  1. Redis 默认每隔 100ms 触发一次定期删除任务;
  2. 每次任务并非扫描所有过期键(否则会严重阻塞服务),而是采用 “随机抽样” 的方式:
  • 从设置了过期时间的键集合中,随机抽取 20个键(默认 N=20);
  • 检查这 20 个键是否过期,删除已过期的键;
  • 若本次删除的过期键比例超过 25%,则继续随机抽样删除,直到比例低于 25% 或达到本次任务的时间上限,这个时间上限默认是25ms,为了避免阻塞主线程。

惰性删除对 CPU 更加友好,定期删除对内存更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性删除策略

Redis数据淘汰策略(内存淘汰机制)(说出2-3种即可)

Redis 提供 8 种数据淘汰策略:

淘汰易失数据(具有过期时间的数据)

  1. volatile-lru(least recently used):从设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-lfu(least frequently used):从设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  3. volatile-ttl:从设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  4. volatile-random:从设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

淘汰全库数据

  1. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
  3. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰

 4.  no-eviction:不淘汰,就是说当内存不足以容纳新写入数据时,新写入操作会报错

Redis分布式锁如何实现?

  • 基于MySQL:如基于version的乐观锁、悲观锁(select * from table for update)
  • 基于Redis:set nx px 加锁,lua脚本解锁,watchdog解决续约问题,集群下用RedLock
  • 基于Zookeeper:[一致性算法]ZK同一个目录下只能有一个唯一的文件名,借助ZK的临时节点实现
  • 基于ETCD:基于分布式一致性算法——Raft,Revision 机制

我们常用的还是redis分布式锁

首先redis的原生命令set nx ex就可以实现分布式锁,由于redis是单线程的,用了这个命令之后,只能有一个客户端对某一个key设置值。在没有过期或删除key的时候,其他客户端是不能设置这个key的。但是这种方法可能出现锁超时,锁误删,锁失效的一些问题,就比如加锁的业务还没执行完,锁就被释放了,那在高并发下其他线程就可能拿到这把锁,那当业务完成释放锁时,它会把后来线程的锁给释放,从而达不到一个锁的效果。还有如果redis宕机了,还没来得及释放锁,那就会出现锁失效的情况。

所以大多数场景下我们都是使用是redis的一个框架Redisson实现的。在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间。当锁住的一个业务还没有执行完成的时候,Redisson会引入一个看门狗机制。就是说,每隔一段时间就检查当前业务是否还持有锁。如果持有,就增加加锁的持有时间。当业务执行完成之后,释放锁就可以了。还有一个好处就是,在高并发下,一个业务有可能会执行很快。客户1持有锁的时候,客户2来了以后并不会马上被拒绝。它会自旋不断尝试获取锁。如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。

Redis和Mysql如何保证数据⼀致 (有强一致、最终一致)[重点]

面试官,分业务情况吧,有保证强一致的,还有保证最终一致的

如果业务要求时效性比较高,我们需要保证数据的强一致性,可以采用Redisson实现的读写锁。在读的时候添加共享锁,可以保证读读不互斥、读写互斥。当我们更新数据的时候,添加排他锁(底层用的是set nx)。它是读写、读读都互斥,这样就能保证在写数据的同时,是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是,读方法和写方法上需要使用同一把锁才行。

如果业务可以允许数据同步有一定的延时,那可以用延迟双删来保证数据的最终一致性。如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据。其中,这个延时多久不太好确定。在延时的过程中,可能会出现脏数据。

mysql篇

MYSQL存储引擎

mysql存储引擎有很多, 常用的就二种 : InnoDBMyISAM

他们的区别就是

  • myisam支持256TB的数据存储 , InnoDB只支持64TB的数据存储
  • myisam 不支持事务也不支持外键 , InnoDB支持事务也支持外键
  • myisam使用的是表级锁定,适合读取密集的应用,
  • innoDB是行级锁定,适合高并发的事物处理和高可用的场景。

其实还有memory(heap),archive(阿凯乌),csv 这些不怎么用所以不太了解

Mysql索引 (B+树)

MYSQL索引主要有 : 单列索引 , 组合索引空间索引 , 用的比较多的就是单列索引和组合索引 , 空间索引我这边没有用到过

单列索引 : 在MYSQL数据库表的某一列上面创建的索引叫单列索引 , 单列索引又分为

  • 普通索引:它没有什么限制,纯粹为了查询数据更快一点。
  • 唯一索引:索引列中的值必须是唯一的,但是允许为空值
  • 主键索引:是一种特殊的唯一索引,不允许有空值
  • 全文索引: 只有在myisam引擎、InnoDB(5.6版本以后)才能使用,而且只能在char,varchar,text类型字段上使用全文索引。

组合索引 : 在MYSQL数据库表的多个字段组合上创建的索引 , 称为组合索引也叫联合索引,它需要遵循最左前缀原则,一般情况下,建议使用组合索引代替单列索引(当然主键索引除外)

聚簇索和非聚簇索引

这个还是比较清楚的,因为这个是我们在项目中进行SQL语句优化的理论基础。

聚簇索引,他的特点呢就是数据与索引存放在一块儿,B+tree的叶子节点保存了整行数据,而且在一张表中聚簇索引有且仅有一个,默认主键索引就是聚簇索引。

非聚簇索引,也叫二级索引,指的是数据和索引分开存储,B+tree的叶子节点保存对应的主键,非聚簇索引在一张表中可以有多个。

回表查询

所谓回表查询,就指的是,在执行这条SQL语句的时候,先根据二级索引去检索出对应的主键值;然后再根据主键值,到聚簇索引中查询出对应的数据,这个过程就叫回表查询。 所以回表查询,是需要扫描两次索引的,性能相对来说会差一些。

所以,在项目开发中,我们进行SQL优化的时候,如果需求允许的情况下,尽量避免回表查询,主要从以下几个方面来做:

1). 业务允许的情况下,尽可能根据主键查询,使用聚集索引-避免回表查询。

2). 为表中的字段,根据业务需求创建合适的联合索引,查询时使用索引覆盖-避免回表查询。

3). 使用索引下推,减少回表查询的次数。【索引下推,是mysql5.6之后提供的功能】

  • 可能继续发问的问题:

你刚才提到索引下推,简单聊聊什么是索引下推?

索引下推(Index Condition Pushdown),是MySQL5.6后提供的功能,指的是在多条件查询SQL执行时,提前判断对应的搜索条件是否满足,满足了再去回表(就是将本应该在 server 层进行筛选的条件,下推到存储引擎层来进行筛选判断,这样能有效减少回表),通过减少回表次数进而提高查询效率。

覆盖索引

覆盖索引是指只需要在一棵索引树上就能获取SQL所需的所有列数据 , 因为无需回表查询效率更高。

实现覆盖索引的常见方法是:将被查询的字段,建立到联合索引里去。

就比如一个sql语句 select name,age from user where name='张三',我们可以给name和age建立组合索引,当我们通过name=张三找到name时,可以把age的数据一并返回,就不需要回表了,这就是覆盖索引。

什么是最左前缀原则

在mysql建立组合索引时会遵循最左前缀匹配的原则,在检索数据时从组合索引的最左边开始匹配,组合索引的第一个字段必须出现在查询组句中,这个索引才会被用到 ;

比如我在字段ABC列上建立了一个组合索引,实际上底层会建立三个索引,分别是A,AB,ABC,如果我的查询条件是AB会走AB索引,AC会走A索引,只有当BC,也就是查询条件没有A时就不会走索引,这就是最左前缀原则

索引失效的场景

MySQL 索引通常就是用于提高where条件的匹配速度嘛,编写合理化的SQL能够提高执行效率

  1. 在索引列上进行了运算或者使用了函数
  2. 当查询条件左右两侧类型不匹配的时候会发生隐式转换,隐式转换就相当于对索引列使用了函数,导致索引失效
  3. 在使用like模糊匹配进行查询时,%写在前面索引会失效,写在后面不会失效
  4. 查询条件不符合最左前缀原则(复合索引)
  5. 使用 != 或 not in或 <> 等否定操作符也会导致索引失效,因为数据库可能认为全表扫描比走索引更高效(会去扫描全表)
  6. 当用 or 连接的条件中,存在至少一个列没有索引时,整个查询可能无法使用索引
  7. 查询中的某个列有范围查询,则其右边所有列都无法使用索引优化查找

如何定位慢查询 [重点]

嗯,我们当时在做压力测试时发现有些接口响应时间非常慢,超过了2秒。因为我们的系统部署了运维监控系统Skywalking,在它的报表展示中可以看到哪个接口慢,并且能分析出接口中哪部分耗时较多,包括具体的SQL执行时间,这样就能定位到出现问题的SQL。

如果没有这种监控系统,MySQL本身也提供了慢查询日志功能。可以在MySQL的系统配置文件中开启慢查询日志,并设置SQL执行时间超过多少就记录到日志文件,比如我们之前项目设置的是1秒,超过这个时间的SQL就会记录在日志文件中,我们就可以在那里找到执行慢的SQL。

一个SQL语句执行很慢, 如何分析 [重点]

这个我们可以借助于MySQL中提供的 explain 关键字,在查询的SQL语句之前,加上explain来查询SQL语句的执行计划。

当然explain查看到的执行计划信息比较多,我们主要关注几个核心指标就可以了。比如:

  • 通过 key 、key_len 就能够知道是否命中索引。
  • 通过 type 指标,就能知道该SQL的性能怎么样,有没有进一步优化的可能。一定要规避all全表扫描的情况。 type指标性能由好到坏,依次是:NULL > system > const > eq_ref > ref > range > index > all
  • 还需要关注一个指标,就是extra额外的信息。 通过这一项,我们就能够知道,有没有回表查询。

然后,我们就可以针对 explain 查看到的执行计划,针对于SQL进行优化了。

mysql的性能优化经验 [重点]

嗯,这个话题就比较大了。 那我们在项目中,优化SQL的查询执行效率,会从多个维度来考虑的。

  • 第一个呢,就是表的设计优化

我们会参考阿里的开发手册,比如数据类型的选择,选tinyint、int还是bigint,要根据实际需要选择。字符串类型,到底选择char还是varchar,也需要根据具体业务确定。(char定长字符串,效率高;varchar变长字符串,效率略低)

  • 第二个呢,就是索引的创建。
  • 针对于数据量较大,且查询比较繁琐的表创建索引。(单表超过10w记录)
  • 针对于经常作为查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引。
  • 对于内容较长的字段,考虑使用前缀索引
  • 尽量建立组合索引,来达到一个索引覆盖的效果,减少回表查询。
  • 当然索引不要太多。(索引过多,会增加维护索引的成本,影响增删改的效率)
  • 第三个呢,就是sql语句的编写。
  • 尽量避免使用select *
  • 要尽量避免索引失效的情况。
  • 尽量使用索引覆盖,避免回表查询,提高性能。
  • 那这些情况呢,都可以通过 explain 关键字来查看SQL语句的执行计划。
  • 进阶回答:
  • 那如果从数据库层面来讲,也可以用主从复制,读写分离的模式,来降低单台服务库的访问压力,从而提高效率。 (就是一台mysql专门用来写操作,多台专门用来读操作,然后写的要同步给读的)
  • 当然,如果数据量过大,也可以考虑对目前项目中的数据库进行分库分表处理

事务的特性是什么?

事务的特性是ACID,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。例如,A向B转账500元,这个操作要么都成功,要么都失败,体现了原子性。转账过程中数据要保持一致,A扣除了500元,B必须增加500元,体现了一致性。隔离性体现在A向B转账时,不受其他事务干扰。持久性体现在事务提交后,数据要被持久化存储。

并发事务带来哪些问题?

并发事务可能导致脏读不可重复读幻读。脏读是指一个事务读到了另一个事务未提交的“脏数据”。不可重复读是指在一个事务内多次读取同一数据,由于其他事务的修改导致数据不一致。幻读是指一个事务读取到了其他事务插入的“幻行”。

MySQL的默认隔离级别是?

解决这些问题的方法是使用事务隔离。MySQL支持四种隔离级别:

  1. 读未提交(READ UNCOMMITTED):解决不了所有问题。
  2. 读已提交(READ COMMITTED):能解决脏读,但不能解决不可重复读和幻读。
  3. 可重复读(REPEATABLE READ):能解决脏读和不可重复读,但不能解决幻读,这也是MySQL的默认隔离级别。
  4. 串行化(SERIALIZABLE):可以解决所有问题,但性能较低。

MYSQL 中的 undo log 和 redo log的区别是什么?

undo log记录的是逻辑日志,用于事务回滚时恢复原始数据,保证事务的原子性和一致性。(事务提交前记录修改前的状态)原理:事务执行时,会记录数据被修改前的状态(反向操作)。如果事务需要回滚,数据库可以通过 undo log 撤销已执行的修改

redo log记录的是数据页的物理变化,用于服务宕机后的恢复,保证事务的持久性。(事务提交后记录修改内容到redo log,再更新到数据库当事务执行写操作时,数据库先将修改内容记录到redo log中,再更新内存中的数据页(Buffer Pool)。

事务中的隔离性是如何保证的呢?(你解释一下MVCC)

事务的隔离性通过锁和多版本并发控制(MVCC)来保证。MVCC通过维护数据的多个版本来避免读写冲突。底层实现包括隐藏字段、undo logread view。隐藏字段包括trx_idroll_pointerundo log记录了不同版本的数据,通过roll_pointer形成版本链。read view定义了不同隔离级别下的快照读,决定了事务访问哪个版本的数据。

集合篇

HashMap

底层数据结构:数组+链表+红黑树

扩容因子:0.75   每次扩容是之前的2倍

  • 初始容量 16,当放入第 13 个元素时(超过 3/4)时会进行扩容,每次扩容,容量翻倍
  • 扩容后,会重新计算 key 对应的桶下标(即数组索引)这样,一部分 key 会移动到其它桶中

put和get

  • 数组:存取元素时,利用 key 的 hashCode 来计算它在数组中的索引,这样在没有冲突的情况下,能让存取时间复杂度达到 O(1)
  • 冲突:数组大小毕竟有限,就算元素的 hashCode 唯一,数组大小是 n 的情况下要放入 n+1 个元素,根据鸽巢原理,肯定会发生冲突
  • 解决冲突:一种办法就是利用链表,将这些冲突的元素链起来,当然在在此链表中存取元素,时间复杂度会提高为 O(n)
  • 树化目的是避免链表过长引起的整个 HashMap 性能下降,红黑树的时间复杂度是 O(log{n})
  • 时机:在数组容量达到 >= 64 链表长度 >= 8 时,链表会转换成红黑树
  • 如果树中节点做了删除,节点少到已经没必要维护树,那么红黑树也会退化为链表

线程不安全,线程安全的有ConcurrentHashMap,你看要细讲一下吗?

HashMap 和 ConcurrentHashMap有什么区别?

1. 线程安全性

HashMap:非线程安全,不支持多线程并发访问。无锁,所有操作不保证线程安全。如果多个线程同时修改 HashMap,可能会导致数据不一致或抛出异常。

ConcurrentHashMapJava 7 及以前:使用分段锁(Segment),将 map 分为多个段,不同段可被不同线程同时访问,提高并发度。Java 8 及以后:采用 CAS + synchronized 实现。锁粒度更小,只锁定链表头节点或红黑树的根节点,减少锁竞争。

2. 适用场景

HashMap:单线程环境。

ConcurrentHashMap:多线程环境,如缓存、分布式系统中的共享数据结构等,需要高效并发读写的场景。

ArrayList 和 LinkedList有什么区别?

  1. 数据结构实现:ArrayList 基于动态数组实现,而 LinkedList 基于双向链表实现
  2. 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
  3. 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
  4. 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
  5. 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;

linux和docker篇

01.linux命令熟悉吗?

面试官,这个还是比较熟悉的,linux常见命令有cd 切换目录,ls ll显示当前目录下的内容,mkdir创建文件夹,rm -f 强制删除,mv移动,cp拷贝,查找命令grep,一般都和 | 一起使用 比如查找错误日志信息 cat error.log | grep "错误信息",部署命令nohup java -jar -Dserver.port=8080 order.jar,然后还有一些文本编译器vim 的命令,比如 i 是插入,:q!不保存强制退出,:wq! 强制保存退出,:set nu显示行号,:/搜索内容等等。

02.docker熟悉吗?dockerfile呢?(扩展出docker-compose)

面试官,docker还是比较熟悉的,在我们用linux部署项目的时候,用docker部署是比较方便的,我现在来简单讲一下docker的命令吧,最常用的有 docker ps 查看所有运行中的容器,docker ps -a是查看所有容器包括未启动的容器。docker pull 拉取镜像; docker push 推送镜像;docker images查看镜像;docker rmi 删除镜像;创建容器并启动有docker run -d(-d是后台运行)--name(设置容器的名称)-p(指定端口) -e(配置环境变量)-v(数据卷挂载)--network(配置网络);进入容器内部的命令 docker exec -it 容器名 bash 。然后还有创建网络的命令:docker network create。创建数据卷的命令 docker volume create;查看数据卷的命令 docker volume inspect。查看容器日志的命令:docker logs -f;。dockerfile呢就是用dockerfile来自定义镜像,它里面有from指令 指定基础镜像,env指令设置环境变量命令,expose指令暴露端口,run是执行Linux的shell命令,一般是安装过程的命令, entrypoint指令是镜像中应用的启动命令 有java -jar之类的,执行dockerfile文件是用docker build -t 来构建镜像。我还了解过用docker-compose来部署项目,您看要讲一下吗?

docker-compose是一个.yml文件,(可以一次性创建并运行多个容器)在里面可以编写要创建的容器和要创建的网络等等,首先可以指定版本号version,然后编写servers,比如MySQL啊 nginx,后端项目,然后还可以创建网络networks 并把MySQL,nginx加入到这个网络,最后执行时先 cd 到存放docker-compose文件的目录执行 docker-compose up -d 就可以一键部署了。

你们公司怎么部署代码的?

你们怎么排查错误

  • 原生Linux:用一些原生命令,比如tail、cat、grep
  • Docker系统:docker logs,docker exec进入容器后查看
  • ELK:界面化直接查看就行
  • SkyWalking:https://www.yuque.com/yzxb/index/pgvl8uwpo50gooef,参考这个后半部分

SSM专题篇

Spring IOC 和 DI

IOC 就是控制反转,控制反转是一种设计原则,它将对象的创建和依赖关系的管理从代码中转移到外部容器

DI就是依赖注入,依赖注入是 IOC 的具体实现方式,通过外部注入来提供依赖对象,而不是对象内部创建。

AOP

AOP一般称为面向切面编程,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑抽取出来并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),为了减少重复代码,降低耦合。

公共日志保存,事务处理,权限校验,自定义注解(防重复提交)使用的就是AOP

原理: AOP 的基础实现方式就是动态代理,它是在运行时动态生成代理对象,无需手动编写代理类,常见的动态代理技术有:jdk动态代理和CGlib动态代理,jdk动态代理是基于接口实现, CGlib动态代理是基于继承实现,通过字节码技术为目标类生成子类代理

@Before

@After

@AfterReturning

@AfterThrowing

@Around

springboot自动装配的原理(重点)

springboot自动装配就是自动去把第三方组件的bean装载到 IOC 容器里面

Spring Boot的自动装配原理基于 @SpringBootApplication注解,它是一个复合注解,它封装了@EnableAutoConfiguration,@SpringBootConfiguration、和@ComponentScan。@EnableAutoConfiguration是核心,它通过@Import注解导入配置选择器,读取 META-INF(没ta info)下的 spring.factories 文件中的类名,根据条件注解决定是否将配置类中的Bean导入到Spring容器中。

Spring的 bean 的作用域(Scope)

主要是有五个作用域:

首先是singleton:单例,意味着在整个spring容器中只会存在一个bean实例。

prototype:原型,意味着每次从IOC容器去获取指定bean的时候,都会返回一个新的实例对象。

但是在基于spring框架下的web应用里面,增加了一个会话维度来控制bean的生命周期,它主要有三个选项:第一个是 request,它是针对每次http请求都会创建一个新的bean。

      第二个是session(会话),在http Session中,同一个session共享同一个bean实例,不同session产生不同的bean实例。

第三个是globalSession(全局会话),它是针对全局session维度共享同一个bean实例。

以上就是我对这个问题的理解。

Spring 事务失效场景

1.因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时, 那么这个注解才会生效 , 如果使用的不是被代理对象调用, 那么@Transactional就会失效

最典型的就是非事务方法调用事务方法

(可以通过AopContext获取代理对象 或者 用当前对象的自注入来实现,这样也能获取当前对象的代理对象)

2.如果某个方法是非public的,那么@Transactional也会失效,因为Spring AOP 基于代理模式,默认只对 public 方法生效。非 public 方法的事务注解会被忽略。(因为底层cglib是基于父子类来实现的,子类是不能重载父类的private方法的,所以无法很好的利用代理,也会导致@Transactianal失效)

3.还有就是异常类型不匹配@Transactional 默认只对 RuntimeExceptionError 回滚,如果出现其他异常,那么事务就会失效。(这个时候我们可以在@Transactional 注解的rollbackFor属性来指定这个异常,这样即使出现这个异常,事务也不会失效)

@Transactional(rollbackFor = RuntimeException.class)

4.出现异常吞没:如果在业务中对异常进行了捕获处理 , 出现异常后Spring框架无法感知到异常, @Transactional也会失效

5.事务传播行为不对:当默认事务调用required_new事务,默认事务抛异常回滚时required_new事务不会一起回滚,因为required_new事务会单独开一个事物,所以也会导致事物失效

事务传播行为

事务传播行为是指在嵌套事务中,当一个事务方法被另一个事务方法调用时,如何管理事务的边界和行为。事务的传播行为呢它有很多种,首先默认就是required,当一个事务外部没有事务,那它就会开启一个新的事物,如果外部存在事物,那么它就会融入到外部事务中(适用于增删改,确保操作在同一个事务中执行),supports呢它是外部没事务的时候不会开启新事务,当外部存在事务的时候,它就会融入外部事物(适用于查询操作,它不需要事务但可以兼容事务环境),还有一个就是requires_new,它是不管外部有没有事务,都会开启一个新的事物(适用于内部事务与外部事务不存在业务关系情况,比如日志记录)

Spring的三级缓存,两级可以吗?

在 Spring 框架中,三级缓存是解决循环依赖问题的核心机制。

  1. 一级缓存(singletonObjects):存储完全初始化完成的单例 Bean,这些 Bean 可以直接被使用。
  2. 二级缓存(earlySingletonObjects):存储提前曝光的 Bean 实例(尚未完全初始化,但已实例化的对象),用于解决循环依赖时的引用问题。
  3. 三级缓存(singletonFactories):存储 Bean 的工厂对象(ObjectFactory),用于在需要时创建 Bean 的早期引用,支持 AOP 代理对象的创建。

三级缓存的工作流程

当出现循环依赖时:(如 A 依赖 B,B 依赖 A)

  1. A 实例化后,将自己的工厂对象放入三级缓存。
  2. A 需要注入 B,开始初始化 B。
  3. B 实例化后,也将自己的工厂对象放入三级缓存。
  4. B 需要注入 A,从三级缓存中获取 A 的工厂对象,创建 A 的早期引用并放入二级缓存,同时移除三级缓存中的 A 工厂。
  5. B 完成初始化,放入一级缓存,A 可以正常注入 B。
  6. A 完成初始化,放入一级缓存。

两级缓存是否可行?

理论上可以,但会丧失一些灵活性

  • 可行的情况:如果系统中没有使用 AOP(即不需要创建代理对象),仅使用一级缓存(存储完整 Bean)和二级缓存(存储早期实例)就能解决循环依赖。
  • 不可行的情况:当存在 AOP 时,Bean 需要被代理。三级缓存的工厂对象(ObjectFactory)能够在获取 Bean 时动态创建代理对象,而二级缓存只能存储固定的早期实例。如果直接使用二级缓存,可能导致注入的是原始对象而非代理对象,引发错误。

第二种回答:

spring的三级缓存是为了解决bean的循环依赖问题的,这三个缓存都是一个Map集合,首先一级缓存singletonObjects,它存放的是完全初始化好了的单例bean,二级缓存呢是earlySingletonObjects,它存放的是半成品的bean,就是未依赖注入的bean,三级缓存呢是singletonFactories,它缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的。

(这里面还会用到一个集合叫creating set,它是用来标记这个bean是正在被创建)

举个例子:如果BeanA依赖BeanB,BeanB依赖BeanA,那我们在创建A的时候,会先在creatingSet中标记A正在被创建,然后将 A 的工厂放入三级缓存,然后开始依赖注入B,在creatingSet中标记B正在被创建,那B也在三级缓存中存下B的工厂,然后开始依赖注入A,发现A在creatingSet中,说明存在循环依赖,然后在三级缓存中可以找到A的工厂,调用getObject方法得到A的半成品注入到B当中,并存到二级缓存,此时B的初始化已经完成,把B存到一级缓存,这个时候可以直接把B注入到A里面了,那么A的初始化也就完成了,再把A存到一级缓存,这样就解决了循环依赖。

两级不可以

当 Bean 需要被 AOP 代理时,如果直接将代理对象放入 两级缓存,可能导致后续替换时的混乱。

Spring 通过 三级缓存 动态生成代理对象,避免提前生成不必要的代理实例。

通过 三级缓存 的工厂对象,可以在需要时按需生成代理对象,确保最终 Bean 的一致性。

Bean的生命周期 ?

Bean 的生命周期指的是从 Bean 被创建、初始化到销毁的完整过程

它分为五大块bean的实例化,bean的依赖注入 ,bean的初始化,bean的使用及销毁。面试官你看我要详细的说一下每个阶段吗?

首先第一步实例化bean,spring容器通过反射机制创建bean的实例,调用无参构造函数

第二步依赖注入,spring容器将bean依赖的其他bean通过属性注入,构造器注入或者setter方法注入到当前bean中

第三步执行Aware接口回调,它的作用是让bean感知spring容器的状态,常见接口有BeanNameAware获取bean的名称,BeanFactoryAware获取BeanFactory

第四步是执行初始化前的后置处理BeanPostProcessor,通过实现BeanPostProcessor接口的postProcessBeforeInitialization方法,在Bean初始化前对其进行修改

第五步执行初始化方法,如果bean实现了初始化接口,那就会调用其中的初始化方法,如果在配置中指定了初始化方法,那就调用这个方法

第六步执行初始化后的后置处理BeanPostProcessor,通过postProcessAfterInitialization方法,在bean初始化后对其进行增强,spring AOP的实现就是在这个阶段为bean创建代理对象

第七步bean准备就绪,可被使用,此时bean已经完成所有初始化步骤,可以直接拿来使用。

第八步bean的销毁,当spring容器关闭时,bean就会进入销毁阶段,如果bean实现了销毁接口,那就调用其中的destroy方法,如果通过配置指定了销毁方法,那就调用该方法

这就是bean的完整的生命周期。

springmvc执行流程

spring mvc 执行流程目前的话是分两种执行流程,一种是以前的老的执行流程方式,第二种是现在流行的前后端分离开发的这种执行流程,接下里我分别讲述一下这两种执行流程,第一种老方式的执行流程是

①首先用户从浏览器发起请求到前端控制器DispatcherServlet ,

②然后DispatcherServlet会调用处理器映射器HandlerMapping查找handler(处理器)

③HandlerMapping找到具体的处理器,生成处理器对象以及处理器拦截器(有的话)将其封装成HandlerExecutionChain传给DispatcherServlet

④DispatcherServlet会调用处理器适配器HandlerAdaptor

⑤HandlerAdaptor会找到对应的处理器(Handler/Controller)

⑥Controller执行完会将对应的结果ModelViewAnd返回给HandlerAdaptor

⑦HandlerAdaptor会将ModelAndView返回给DispatcherServlet

⑧DispatcherServlet会将ModelAndView传给视图解析器ViewResolver

⑨ViewResolver将ModelAndView进行解析成View(视图)返回给DispatcherServlet

⑩DispatcherServlet会根据得到的View进行视图渲染然后返回给前端

而前后端分离开发的执行流程则是这样

①用户同样从浏览器发送请求到DispatcherServlet

②DispatcherServlet会调用HandlerMapping

③HandlerMapping找到具体的处理器,生成处理器对象以及处理器拦截器(有的话)将其封装后传给DispatcherServlet

④DispatcherServlet会调用HandlerAdaptor

⑤HandlerAdaptor经过适配找到对应的处理器(Handler/Controller)

⑥方法上添加了@ResponseBody

⑦通过HttpMessageConverter将返回结果转化为JSON格式并响应

过滤器和拦截器的区别

过滤器(Filter)
是 Servlet 规范的一部分,属于 Servlet 容器的功能。Servlet 容器层面的拦截,在请求进入 Servlet 之前 和响应返回客户端之前执行。
作用:对请求和响应进行统一处理(如编码转换、权限校验、日志记录等通常用于非业务逻辑的通用处理。

拦截器(Interceptor)
是 Spring 框架提供的组件,属于 AOP(面向切面编程)的应用。Spring MVC 框架层面的拦截,在        DispatcherServlet 处理请求的过程中执行。会在Controller请求之前和处理完毕之后进行处理。
作用:在请求处理的前后添加自定义逻辑(如登录校验、参数验证、性能监控等),更贴近业务需求。

用户输入一个网址访问到后端返回结果,经过哪些环节?

当用户输入一个网址时,浏览器会解析网址的信息,比如网址协议,路径信息,域名等,然后通过DNS解析成后端的IP地址,然后通过TCP的三次握手与后端建立连接并发起请求,请求包括请求行,请求头,请求体,后端接收到请求后会通过nginx的负载均衡算法请求对应的应用服务器,应用服务器处理完成后发送一个响应数据给前端,响应数据包括响应码,响应头,响应体,如果是短连接,那就会通过TCP的四次挥手断开连接,如果是长连接那么连接就会持续一段时间,前端收到响应后把数据渲染展示给用户。面试官我上面提到的TCP三次握手和四次挥手你看要详细说一下吗?

TCP的三次握手就是前端对后端发起一个请求连接告诉后端我想和你连接,后端返回一个确定连接告诉前端你可以和我连接,前端收到后就发起一个正式连接,这就是TCP的三次握手。

四次挥手就是前端发送一个请求断开给后端告诉后端我想断开连接,后端收到后发送一个收到,但是还要准备一下,准备好了之后再发送一个我已断开给前端,前端收到后再发送一个确认断开给后端,这就是四次挥手。

Mybatis 动态SQL

Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。

其执行原理为:使用OGNL从sql参数对象中计算表达式的值,根据表达式的值动态拼接sql,以此来完成动态sql的功能。

标签:if,choose,when,otherwise,trim,where,set,foreach,bind,sql,include

Mybatisplus和mybatis有什么区别?

MyBatisPlus是建立在MyBatis之上的一个增强工具,它在保留MyBatis所有功能的基础上,提供了丰富的 API 来简化数据层的开发。我说一下mybatisPlus对于mybatis的一些优势吧

1.首先MybatisPlus提供了一些常用的CRUD操作,比如在mapper层有selectById,deleteById,update,inster等

MybatisPlus提供了丰富的插件支持,比如内置的分页插件,还有一些其他插件可以方便地进行二级缓存、性能分析等等

MybatisPlus还提供了一些高级功能,比如逻辑删除、乐观锁

MybatisPlus还可以通过配置文件或注解的方式进行配置,

MybatisPlus还有代码生成器,可以直接根据表结构生成三层架构的基础代码,Controller,Service、Mapper

但是mybatis-plus还是以单表 CRUD 为主、追求快速开发。

而mybatis更擅长于复杂 SQL、多表联查等。我们可以在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。

总的来说呢就是mybatisPlus更适用于单表的增删改查,这种情况基本上不需要写什么代码,适用于快速开发,而mybatis更适用于多表联查的复杂业务和复杂的sql,。

你常用的API有哪些?(TODO)

mybatis的一二级缓存

一级缓存: 基于SqlSession级别的缓存 , 默认开启,它的作用范围同一个 SqlSession 内有效,,首次查询时,结果存入当前 SqlSession 的内存缓存。后续相同查询直接从缓存读取,无需访问数据库。比如在同一个事物下,前面查询了某一个用户的信息,后面又查询了同一个用户的信息,这个时候就会拿前一次查询的缓存结果直接返回,不用再查数据库了

二级缓存 : 基于SqlSessionFactory的NameSpace级别缓存 , 默认没有开启, 需要手动开启 。它的作用范围SqlSession的,基于 Mapper 的 namespace 共享,查询结果存入全局缓存,多个 SqlSession 可共享。然后在增删改操作触发缓存刷新。

#和$区别?

${ }是在MyBatis处理的时候就会直接替换,  会有SQL注入的风险

#{ }是预编译处理,在MyBatis处理的时候会替换为问号(?),然后调用PreparedStatement(婆累怕得)的set方法来对问号?赋值,它能够避免sql注入

并发编程篇

线程和进程的区别?

  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
  • 线程更轻量,线程上下文切换成本一般要比进程低

并行和并发有什么区别?

现在都是多核CPU,在多核CPU下

并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU

并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程

创建线程的方式有哪些?(这里面还有俩个问题要一起问)

1.继承Thread类

2.实现Runnable接口

3.实现callable接口

4.利用线程池来创建线程

线程池的七大参数有什么?

核心线程数最大线程数,线程空闲时间,时间单位,任务队列,线程工厂,拒绝策略

  1. 核心线程数:线程池保持活跃的最小线程数量。当提交的任务数超过核心线程数时,任务会被放入工作队列。
  2. 最大线程数:线程池允许创建的最大线程数量。当工作队列已满且线程数小于最大线程数时,会创建新线程执行任务。
  3. 线程空闲时间:非核心线程(超过核心线程数的线程)在空闲时的存活时间。超时后线程会被回收,以节省资源。
  4. 时间单位:keepAliveTime 的时间单位(如 TimeUnit.SECONDS)。
  5. 任务队列:

workQueue用于存储等待执行的任务的阻塞队列。

常见类型:ArrayBlockingQueue(有界队列)

  • LinkedBlockingQueue(无界队列或有界队列)
  • SynchronousQueue(直接移交队列)
  • PriorityBlockingQueue(优先级队列)
  1. 线程工厂:threadFactory
  • 用于创建线程的工厂类,可自定义线程的名称、优先级等属性。
  1. handler(拒绝策略)
  • 当工作队列已满且线程数达到最大线程数时,新任务的处理策略。默认提供四种:
  • AbortPolicy(默认):抛出 RejectedExecutionException
  • CallerRunsPolicy:由提交任务的线程直接执行。
  • DiscardPolicy:直接丢弃任务。
  • DiscardOldestPolicy:丢弃队列中最老的任务,尝试重新提交当前任务。

核心线程数怎么设置?

核心线程数的设置需要根据任务特性和系统资源来调整

如果是CPU 密集型任务,核心线程数就 ≈ CPU 核心数 + 1

如果是IO 密集型任务,核心线程数 ≈ CPU 核心数 × 2 ,如果有等待时间很长,可以相对应的提高核心线程数

比如 核心线程数=CPU核心数×2×(平均等待时间/平均处理时间)+1

那Runnable接口和callable接口有什么区别呢?

1.首先runnable接口的run()方法没有返回值,callable接口的call()方法有返回值,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果

2.runable的run()方法不能抛出异常只能自己消化,而callable的call方法可以抛异常

线程的 run()和 start()有什么区别?

1.start方法是开启一个线程,通过该线程调用run方法执行run方法中的代码。start方法只能被调用一次。

2.run方法封装了要被线程执行的代码,可以被调用多次。

线程包括哪些状态,状态之间是如何变化的?

在JDK中的Thread类中的枚举State里面定义了6中线程的状态分别是new,runable,terminated,blocked,waiting,time_waiting

新建状态,可运行状态,终结状态,阻塞状态,等待状态,有时限等待状态

关于线程的状态切换情况比较多。我分别介绍一下

当一个线程对象被创建,但还未调用 start 方法时处于新建状态,调用了 start 方法,就会由新建进入可运行状态。如果线程内代码已经执行完毕,由可运行进入终结状态。当然这些是一个线程正常执行情况。

如果线程获取锁失败后,由可运行进入 Monitor 的阻塞队列阻塞,只有当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的阻塞线程,唤醒后的线程进入可运行状态

如果线程获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从可运行状态释放锁等待状态,当其它持锁线程调用 notify() 或 notifyAll() 方法,会恢复为可运行状态

还有一种情况是调用 sleep(long) 方法也会从可运行状态进入有时限等待状态,不需要主动唤醒,超时时间到自然恢复为可运行状态

notify()和 notifyAll()有什么区别?

notifyAll:唤醒所有wait的线程

notify:只随机唤醒一个 wait 线程

新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?

嗯~~,我思考一下 (适当的思考或想一下属于正常情况,脱口而出反而太假[背诵痕迹])

可以这么做,在多线程中有多种方法让线程按特定顺序执行,可以用线程类的join()方法在一个线程中调用另一个线程的join()方法,另外一个线程完成后该线程才会继续执行。

比如说:

使用join方法,T3调用T2的join方法,T2调用T1符join方法,这样就能确保T1会先完成而T3最后完成

在 java 中 wait 和 sleep 方法的不同?

不同点

  • 方法归属不同
  • sleep(long) 是 Thread 的静态方法
  • 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有
  • 醒来时机不同
  • 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
  • wait(long) 和 wait() 还可以被 notify或natifyAll 唤醒,wait() 如果不唤醒就一直等下去
  • 锁特性不同(重点)
  • wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
  • wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用)
  • 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了)

如何停止一个正在运行的线程?

有三种方式可以停止线程

  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止

就是在run方法外面先定义一个boolean类型的变量flag,然后在run方法中写一个死循环,条件就是flag,如果想让线程停止,只需要改变flag的值让它跳出循环,那么这个线程就会结束。

  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程

如果打断一个阻塞中的线程,会报InterruptedException

打断正常线程原理和退出标记相似

synchronized关键字的底层原理?

synchronized 属于悲观锁。底层使用的JVM级别中的Monitor 来决定当前线程是否获得了锁,如果某一个线程获得了锁,在没有释放锁之前,其他线程是不能获得到锁的。它会根据对象头找到创建此对象对应的 Monitor 对象,然后检查 Monitor 对象的 owner 属性,用 CAS 操作去设置 owner 为当前线程,CAS 是原子操作,只能有一个线程能成功,假设该线程CAS成功了,那就加锁成功,可以继续执行 synchronized 代码块内的部分;,如果有其他线程也来抢锁,则进入EntryList 进行阻塞成为Blocked线程,当获得锁的线程执行完了,释放了锁,就会唤醒EntryList 中等待的线程竞争锁,竞争的时候是非公平的,面试官我还了解synchronized锁升级,你看要说一下吗?

synchronized 锁有三个级别:偏向锁、轻量级锁、重量级锁,性能是依次降低的

  • 首先如果就一个线程对同一对象加锁,此时就用偏向锁
  • 那如果又来一个线程,与前一个线程交替为对象加锁,但只是交替,没有竞争,此时要升级为轻量级锁
  • 那如果多个线程加锁时发生了竞争,必须升级为重量级锁,一旦锁发生了竞争,都会升级为重量级锁。

面试官追问:对象头的结构是什么?

对象头主要是由Mark Word 和 Klass Pointer组成,Mark Word存储对象的哈希码、分代年龄、锁状态等运行时数据,Klass Pointer指向对象类的元数据指针。

reetranlock的实现原理

ReentrantLock 是 Java 中的一种可重入锁,其实现原理基于抽象队列同步器(AQS)。AQS 维护了一个同步状态变量 state 和一个等待队列,通过对 state 的原子操作和对队列中线程的管理来实现锁的功能。AQS 使用一个 volatile int 类型的变量 state 表示锁的状态。当 state 为 0 时,代表锁未被任何线程持有;当一个线程首次获取锁成功,JVM 会记录该锁的持有线程,并将 state 设置为 1。若同一个线程再次请求锁,state 会递增,这体现了锁的可重入性。线程释放锁时,state 会递减,当 state 递减为 0,意味着锁已完全释放,其他等待线程有机会获取。

非公平获取锁:就是如果获取锁失败,会调用acquire(1)方法,再次尝试获取锁,如果还失败,就将当前线程加入到 AQS 的同步队列中等待。

公平锁加锁流程:就是按照先来后到的顺序唤醒队列中的线程。

synchronized和ReentrantLock的区别

synchronized:关键字 锁方法或者代码块
由 JVM 自动获取和释放锁,发生异常时会自动释放。

ReentrantLock:一个类
必须手动调用 lock()unlock() 方法,且释放操作必须放在 finally 块中,否则可能导致死锁

高竞争场景
ReentrantLock 通常表现更好,因为 JVM 对 synchronized 的优化(如偏向锁、轻量级锁)在极端竞争下可能失效。

Lock 功能更多,比如可以选择是公平锁还是非公平锁、可以设置加锁超时时间、可中断等

说一下乐观锁和悲观锁的区别(todo)

常见的悲观锁有哪些

Java的悲观锁说两个

reetranlock 的实现原理

阻塞竞争锁和非阻塞竞争锁

守护线程和非守护进程的区别

实现线程池的方式

线程池如何回收线程

什么是双亲委派机制

微服务篇

Spring Cloud用过哪些组件

注册中心和配置中心:nacos

网关:gateway

远程调用:openfeign

负载均衡:loadbalance

服务熔断与降级:sentinel

分布式锁seata的AT模式

当执行分布式事务时TM(全局事务)会去报告给TC事务开始,然后各个RM(分支事务)去TC中注册事务,然后去执行sql并生成undo log 然后立刻提交,当各子事务执行完成后TM会报告给TC,TC会去检查各个子事务是否全部成功,如果全部成功,就删除undo log,只要有一个失败,那就利用undo log对各个事务进行回滚,这个时间段内可能会出现短暂的数据不一致的情况,不过一般的业务都是可以允许的。

那如果要求强一致性的话,可以使用seata的XA模式,它和AT模式不同的点在于,RM执行完sql后不会立刻提交,而是等其他RM执行完之后,TM报告给TC事务执行完成,TC检查如果都成功,才会提交,如果有一个失败,那么就不会提交。这就保证了强一致性,不过性能相对来说会更低。

说一下nacos心跳机制

当一个服务注册到nacos,默认情况下,服务实例每 5 秒 向 Nacos 服务器发送一次心跳包来报告自己的健康状态,如果nacos服务15秒内未收到心跳,nacos会将它标记为不健康,但仍在列表,如果30秒内仍未恢复,那就会彻底删除这个实例。

如果服务实例正常下线,会主动向 Nacos 发送注销请求,服务器会立即移除该实例。

如果服务实例异常崩溃(如进程 killed),无法发送心跳,Nacos 会通过上面说的超时机制自动标记并清理实例,保证服务列表的准确性。

Nacos 的心跳检测机制维护了服务注册与发现的准确性

讲一下OpenFeign的原理和优点

原理:OpenFeign 通过「声明式接口 + jdk动态代理」的方式,将服务间的 HTTP 通信抽象为简单的接口调用,大幅简化了微服务架构中的服务协作开发。

@FeignClient(value = "cart-service",fallbackFactory = CartClientFallbackFactory.class)

优点:

1.与 Spring 生态无缝融合,它支持 Spring MVC 注解(如 @GetMapping@PostMapping)。

2.集成负载均衡,可以使用loadbalance自动实现服务实例的负载均衡,不需要额外配置就可以适配集群环境。

3.支持服务熔断与降级,可与 Sentinel 组件集成,当服务调用失败时自动触发熔断或返回降级结果,提高系统容错能力。

4.减少重复代码,避免在多个地方重复编写相同的 HTTP 调用代码,便于维护。

spring Security

用过鉴权框架吗?实现的原理大概说一下?

  • 面试官,常见的一些鉴权框架比如Shiro、Security、还有自定义RBAC模型都用过一些,相对来说前两者还是比较复杂的,我给您说一下Security的原理吧/说一下RBAC底层/领域模型(就是数据库表)吧
  • Security:他就是在用户请求之后,经过SpringMVC的DispatcherServerlet,找到对应的HandlerMapping之后的Handler(controller)之后,我们会在方法上追加一个注解@PreAuthorize("@ss.hasPermi('elder:bed:list')" ,此时Security会自动进入自己的过滤器
  • 首先判断访问的路径是不是在白名单印象,在里面就直接方向
  • 不在里面就根据用户的id,去底层RBAC模型查找这个用户对应的角色、菜单,返回一个菜单列表,有则放行并生成JWT信息存储在session里,没有则返回HTTP 401权限不足
  • RBAC:参照下图说出自己的理解即可
  • 面试官,RBAC模型就是经典的用户、角色、资源三张主表,同时增加用户-角色、角色-资源两张中间表,用来记录哪个用户有哪些角色,每个角色有哪些资源;当然在此基础之上,也可以增加部门的,也都是一样的原理,我就不重复赘述
目录
相关文章
|
Shell Android开发
Android系统 adb shell push/pull 禁止特定文件
Android系统 adb shell push/pull 禁止特定文件
1100 1
|
关系型数据库 MySQL 数据库
什么是内连接、外连接、交叉连接、笛卡尔积呢?
什么是内连接、外连接、交叉连接、笛卡尔积呢?
|
16天前
|
算法 关系型数据库 Python
配电网中考虑需求响应(Python代码实现)【硕士论文复现】
基于需求侧响应的配电网供电能力综合评估
|
16天前
|
云安全 人工智能 搜索推荐
客户案例|皇家宠物食品:以“懂我”的温暖服务,延续每一份人宠羁绊
皇家宠物食品携手阿里云与Salesforce,打造高性能本地化客户关怀平台,实现多渠道服务整合,为宠主提供个性化、温暖的服务体验,助力科学养宠新时代。
|
16天前
|
存储 安全 API
某网盘不好用?有没有类似某网盘的存储软件?阿里云国际站 OSS:云存储的全能助手,你 get 了吗?
在数据爆炸时代,阿里云国际站OSS提供海量、安全、低成本的云存储服务,支持多种数据类型存储与灵活访问,助力企业与个人高效管理数据,降低存储成本。开通简便,操作友好,是理想的云端数据解决方案。
|
28天前
|
云栖大会
送票!2025云栖大会9月24-26日杭州见
2025杭州·云栖大会,来了!
|
28天前
|
存储 数据库
RAG分块技术全景图:5大策略解剖与千万级生产环境验证
本文深入解析RAG系统中的五大文本分块策略,包括固定尺寸、语义、递归、结构和LLM分块,探讨其工程实现与优化方案,帮助提升知识检索精度与LLM生成效果。
146 0
|
28天前
|
人工智能 JavaScript 前端开发
dify工作流+deepseek开启联网搜索
本内容介绍了如何创建一个包含网络搜索和大模型处理的工作流应用,通过编排开始节点、Web搜索API、LLM模型及结束节点,实现根据用户提问自动检索并返回答案的功能。示例展示了查询“今天日期是多少”的完整流程及各节点数据处理情况。
|
2月前
|
NoSQL 算法 数据库
旅游项目
本项目涵盖酒店预订、支付系统、短信平台、风控审核、权限管理、推荐系统等多个模块。采用微服务架构,结合Redis、RabbitMQ、ES、MongoDB等技术,实现高并发场景下的稳定服务。支持短信防刷、订单超时取消、优惠券发放、分布式锁控制、Drools规则引擎、拼音搜索、数据分片、权限控制、红包抢夺算法及个性化推荐等功能,保障系统安全性与用户体验。
54 0
remote: HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2
remote: HTTP Basic: Access denied. The provided password or token is incorrect or your account has 2
5007 0