趁着今天有时间,将去年整理的一些八股,稍微做一下记录,时间有限,后续再调整格式了~
-
动态代理会在运行时生成动态类的字节码并加载到方法区,同时使用对应的类定义开实例化对象。cglib 使用继承的方式动态生成代理类,此时被代理的方法调用自身其他方法时,由于继承的原因其他方法也是被代理增强的子类的方法,jdk 使用实现接口结合 handler 组合原对象的方式动态生成代理类,此时被代理的方法调用其他自身的方法,由于组合的原因其他方法并没有被代理增强的。一道很经典的八股题,Spring 三级缓存就是是为了解决循环依赖情况下,Bean 被动态代理的场景,动态代理默认是在 set 注入阶段之后,也即一般情况下是在解循环依赖之后再处理生成增强对象的,但是这样的话会导致在 set 注入阶段,注入的 Bean 是原对象而非动态代理的增强对象,通过引入三级缓存存放触发动态代理的 ObjectFactory 可以将动态代理的生成时间段提前到 set 阶段时,也即在完成了动态代理之后,增强的对象会被放置到二级缓存用于单例化,具体可以考虑如下场景: A 依赖 B, C 依赖 A,B 依赖 C和 A,同时 A 被打了 @Transactional 需要被动态代理
-
B+ 树是个 N 叉树,同时所有的值都在叶子节点,键都在中间节点,它用于数据库索引的优点在于:1. B+ 树的高度和二叉树之类的比起来更低,树的高度代表了查询的耗时,所以查询性能更好。2. B+ 树的叶子节点都被串联起来了,适合范围查询。3. B+ 树的非叶子节点没有存放数据,所以适合放入内存中。
索引最左匹配指的是联合索引的多个键会按照从左到右的方向进行排序,也即先排 a ,a 相等再排 b,这也就导致了如果查询中是 a=x and b=x 会命中索引,但是 a=x or b=x 则会全表查询,因为 b 并不排序 -
数据库索引总结:从数据结构上来说,在 MySQL 里面索引主要是 B+ 树索引。它的查询性能更好,适合范围查询,也适合放在内存里。MySQL 的索引又可以从不同的角度进一步划分。比如说根据叶子节点是否包含数据分成聚簇索引和非聚簇索引,还有包含某个查询的所有列的覆盖索引等等。数据库使用索引遵循最左匹配原则。但是最终数据库会不会用索引,也是一个比较难说的事情,跟查询有关,也跟数据量有关。在实践中,是否使用索引以及使用什么索引,都要以 EXPLAIN 为准。
-
数据库锁总结:在获取行级锁之前需要先放置一个表意向锁,从而避免表锁遍历,总结一下:IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录,也就是说其实IS锁和IX锁是兼容的,IX锁和IX锁是兼容的。Innodb 会为对应的记录行身上有的每个锁创建具体的锁结构,所有的锁都会有一个载体(某一个记录行),然后锁上面会有具体的类型,比如:gap 锁,对于一个值是否被gap 锁覆盖,只要简单的往上找离它最近的记录行,然后判断该记录行上是否持有 gap 锁,对于一个很大的区间,Innodb会为其中的每个记录行都上一个 next-key 锁(记录锁➕间隙锁)。delete 一个存在的值时会产生一个记录锁,而对于不存在的值的时候,应该会在第一个比它大的记录行上面生成一个 gap 锁,但是由于最大值上面不管是加 gap 锁,记录锁还是 Next-key锁的效果都一样,所以为了减少锁的种类,就用了记录锁。然后 gap 锁只对 insert 操作生效,gap 锁本身不会与其他锁包括gap锁本身阻塞。另外对于插入意向锁,它只是insert 操作被阻塞后的一个产物,没啥太大用处(插入意向锁之间也不会阻塞),insert 本身不会加锁,它通过 transactionid 进行隐式加锁,除了两种情况:1. 在插入前先会检查第一个比他大的记录行身上有没有 gap 锁(对于最大值这个虚标记而言,记录锁也能达到相同效果),如果有的话就阻塞并在第一个比他大的记录行上创建插入意向锁(虽然是锁但是啥都不会阻塞)。2. 对于 unique key 遇到相同值时,如果在插入新记录时,发现页面中已有的记录的主键或者唯一二级索引列与待插入记录的主键或者唯一二级索引列值相同,此时插入新记录的事务会获取页面中已存在的键值相同的记录的锁(RC级别时记录锁,RR级别时next-key锁)。对于 update 好像会更加复杂点。Mysql 隐式地使用锁来实现事务控制。
可以使用 explain SQL,查看 SQL 的运行计划,其中最重要的是 type 类型, system > const > eq_ref > ref > range > index > ALL,这里需要注意 eq_ref 代表索引查询某个具体值,例如: id=1,index 代表索引全表查询,例如:order by id -
事务总结:事务隔离机制分为:读未提交,读已提交,可重复读和串行化,分别解决了脏读,不可重复读和幻读的问题,理论上来说可重复读是没有解决幻读的。但是 MySQL 因为使用了临键锁,因此它的可重复读隔离级别已经解决了幻读问题。使用锁完全可以实现各种事务隔离机制,读写数据的时候加锁(根据隔离级别不同,加不同的锁),如果加不了锁就卡住,在事务提交之后再统一释放事务期间加的锁。但是这种悲观锁的性能太差了,为了提升性能,innodb 提出了基于乐观锁的 MVCC,不过 MVCC 在写写时也会加锁。
MVCC:为了实现 MVCC,InnoDB 引擎给每一行都加了两个额外的字段 trx_id 和 roll_ptr,每一个事务在开始的时候就会获得一个 ID,然后这个事务内操作的行的 trx_id都会被修改为这个事务的 ID,而roll_ptr 则把每一行的历史版本(Undo Log)串联在一起构成“版本链”。除了这两个列外, MVCC 还有一个重要的概念就是 Read View,当事务内部要读取数据的时候,Read View 就被用来控制这个事务应该读取哪个版本的数据,Read View 最关键的字段叫做 m_ids,它代表的是当前已经开始,但是还没有结束的事务的 ID,也叫做活跃事务 ID,除此之外还min_trx_id表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,以及max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。Read View 只用于已提交读和可重复读两个隔离级别,它用于这两个隔离级别的不同点就在于什么时候生成 Read View,前者在每次读的时候都会创建一个 ReadView,后者在每次事务开始的时候创建一个 ReadView 。Read View 的具体匹配规则如下:
如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。 -
K8S 中 KubeDNS 负责将 Service 名称转换为 ClusterIP,而 KubeProxy 则通过 EndPoint 资源创建 iptablees/ipvs 规则,其中应用 netfilter 层面的负载均衡算法,从而将 ClusterIP 转化为 PodIP 并路由到对应的 PodIP,iptables 和 ipvs 的核心区别在于前者使用链表存储规则需要线性匹配和更新规则,而后者使用 iptables 的扩展 ipset(可以简单理解为一个 HashMap,键是 ip 段,值是对应的规则),引入了带索引的数据结构从而加快了规则匹配和更新速度
-
注册中心主要考虑服务端挂了和注册中心挂了两种情况,对于服务端挂了需要注册中心通过心跳机制去发现,同时通过重试的方式避免网络波动问题,但是在服务端挂了到注册中心发现再到注册中心把信息同步给客户端会有一个时间延迟,因此需要客户端自身做好 failover,在调用失败情况下切换目标节点,针对注册中心挂了的情况,由于数据不一致的问题始终存在,且客户端自身有 failover,因此我们一般会选择 AP 模型而非 CP 模型
-
Nginx 对于请求是接收完了请求体之后再转发给后端,因为它要对于请求体进行分析并且决定是否转发,但是对于响应而言,Nginx 默认是流式地转发给客户端的,也就是说它会立即转发后端给的响应体给客户端,而不会等待接收完毕
-
如果你的 Spring Boot 应用部署在 Kubernetes 上,确保 Pod 的 terminationGracePeriodSeconds 设置得足够长,以便 Tomcat 有时间处理当前的请求。例如,如果你认为 Tomcat 最多需要60秒来完成所有请求,你可以设置它为60。当 Kubernetes 试图终止一个 Pod 时,它首先发送 SIGTERM 信号。Spring Boot 通常能够正确地处理此信号,因为它的嵌入式 Tomcat 默认会在接收到 SIGTERM 信号时进行优雅地关闭。这意味着它会等待所有活动线程(即当前正在处理的请求)完成,但不接受新的请求。由于 Tomcat 此时不再接收新的请求,因此对于新的请求它会返回 503,此时客户端需要做重试处理。
-
线程池处理任务的流程:
- 如果当前运行的线程数小于核心线程数,那么就会新建一个核心线程来执行任务。核心线程是常驻线程,它会轮询任务队列,并执行对应的任务
- 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。
- 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就直接新建一个普通线程来执行任务。
- 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝
-
超时时间和重试次数的配置中,其实这两个数是有互相配合的,举个例子,比如我调用下游的成功率要达到五个9,调用下游的耗时98分位是20ms,那如果我设置超时时间20ms就意味着有2%的概率失败,那加一次重试可以降低到0.04%,可用性达到99.96%,不满足需求,那就再加一次重试,可用性达到99.9992%,但如果超时次数太多对上下游压力也会比较大,所以最好不要超过3次,如果重试次数确定了,那就可以通过提高超时时间来保证成功率,比如设置成99分位的耗时或者更高,但最理想的方案还是依据SLA,来确定超时时间和重试次数。