面试题
网络
TCP连接过程中如果拔掉网线会发生什么?
针对这个问题,要分场景来讨论:
- 拔掉网线后,有数据传输;
- 拔掉网线后,没有数据传输;
拔掉网线后,有数据传输 在客户端拔掉网线后,服务端向客户端发送的数据报文会得不到任何的响应,在等待一定时长后,服务端就会触发超时重传机制,重传未得到响应的数据报文。
如果在服务端重传报文的过程中,客户端刚好把网线插回去了。由于拔掉网线并不会改变客户端的 TCP 连接状态,并且还是处于 ESTABLISHED 状态,所以这时客户端是可以正常接收服务端发来的数据报文的,然后客户端就会回 ACK 响应报文。此时,客户端和服务端的 TCP 连接依然存在的,就感觉什么事情都没有发生。
但如果如果在服务端重传报文的过程中,客户端一直没有将网线插回去。服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是服务端的 TCP 连接就会断开。等客户端插回网线后,如果客户端向服务端发送了数据,由于服务端已经没有与客户端相同四元祖的 TCP 连接了,因此服务端内核就会回复 RST 报文,客户端收到后就会释放该 TCP 连接。此时,客户端和服务端的 TCP 连接都已经断开了。
拔掉网线后,没有数据传输
如果双方都没有开启 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,那么客户端和服务端的 TCP 连接状态将会一直保持存在。 如果双方都开启了 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,TCP keepalive 机制会探测到对方的 TCP 连接没有存活,于是就会断开 TCP 连接。而如果在 TCP 探测期间,客户端插回了网线,那么双方原本的TCP 连接还是能正常存在。 KeepAlive机制:当连接超过一段时间没有数据传输之后,TCP自动发送一个数据为空的报文给对方,如果对方回应了这个报文,说明对方还在线,连接可以继续保持,如果对方没有报文返回,并且重试了多次之后则认为连接丢失,没有必要保持连接。
数据结构
树的深度优先遍历用什么数据结构,如果不用递归的话?广度优先遍历用什么数据结构?
二叉树的深度优先遍历的非递归的通用做法是采用栈,广度优先遍历的非递归的通用做法是采用队列。
堆排序过程
算法
KMP算法
Java基础
HashMap为什么会有线程安全问题?
我们知道jdk1.7和jdk1.8中HashMap都是线程不安全的,那就具体讲一下为什么会线程不安全(两个方面)。
①调用put方法
假如有两个线程A和B,A希望插入一个key-value到HashMap中,首先会通过A的key得到桶的索引坐标,然后获取该桶的链表头结点,线程A的时间片用完,而此时B线程被调用执行,和线程A一样执行,只不过线程B成功的将数据插入到桶里面。假设线程A插入时候计算的坐标和B线程要插入的索引坐标是一致的,那么当B线程成功插入以后,线程A再次被调用运行的时候,它依然持有原来的链表头,但是它对B线程插入的过程一无所知,那么线程A就会对此坐标上的数据进行覆盖,那么线程B插入的数据就会消失,造成数据不一致的行为。
②扩容
JDK7版本中的HashMap扩容时使用头插法,假设此时有元素一指向元素二的链表,当有两个线程使用HashMap扩容的时,若线程一在迁移元素时阻塞,但是已经将指针指向了对应的元素,线程二正常扩容,因为使用的是头插法,迁移元素后将元素二指向元素一。此时若线程一被唤醒,在现有基础上再次使用头插法,将元素一指向元素二,形成循环链表。若查询到此循环链表时,便形成了死锁。而JDK8版本中的HashMap在扩容时保证元素的顺序不发生改变,就不再形成死锁,但是注意此时HashMap还是线程不安全的
系统设计
缓存一致性从哪些方面考量,读写,每个步骤详细说有哪些问题?
https://juejin.cn/post/7066344853277245470
1 前言
日常开发中,我们接口响应缓慢,往往是因为数据库读写产生的,这时为了优化这些接口,往往我们会将数据库的数据写入缓存中,让接口直接从缓存中获取数据,这样能极大的提高接口的访问速度。但是随之而来的问题就是,在我们更新数据库的数据时,就需要去更新缓存中的数据,或者是删除缓存中的数据,让其再次访问时通过读数据库,再将读到的数据刷入缓存中。但是如果在这期间出现并发,就很容易导致数据库缓存中数据不一致,这也是本篇文章的主题如何应对数据库缓存双写一致性问题。
这里我们说的一致性是指最终一致性,并不是强一致性。
2 推荐方案 Cache Aside Pattern
命中:程序从缓存读取数据,读到数据即命中
失效:程序从缓存读取数据,未读到数据,此时缓存失效,需要先去数据库读取,再刷入缓存
更新:先更新数据库,再删除缓存
3 更新方案
- 更新缓存
- 先更新缓存,再更新数据库
- 先更新数据库,再更新缓存
- 删除缓存
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存(常用)
4 更新方案选择原因
4.1先更新缓存,再更新数据库
如下图,当A线程更新完缓存的数据A,这时A线程出现延迟,B线程将A线程缓存的更新覆盖,并且将数据库中的数据A也更新,A线程恢复去更新数据库,这时又将B线程对数据库的修改覆盖,这样就会出现严重的双写不一致,导致后续每次读取到的缓存中的数据都是有问题的,并且数据库的数据对我们来说更为重要,我们一般持久化都是依赖数据库的,如果先更新缓存的话,后续程序宕机,数据库中的数据就得不到更新,我们一般是不会依赖缓存做持久化保存的,所以这种方案是一定不能选择的。
4.2 先更新数据库,再更新缓存
如下图,当A线程更新完数据库的数据A,这时A线程出现延迟,B线程将A线程数据库的更新覆盖,并且将缓存中的数据A也更新,A线程恢复去更新缓存,这时又将B线程对缓存的修改覆盖,这样就会出现严重的双写不一致,导致后续每次取到缓存中的数据都是有问题的。
与上个方案一样,凡是更新缓存数据的都会出现这种严重的双写不一致,所以一般我们不采取更新缓存的方案,而是从删除缓存中的方案做选择。
4.3 先删除缓存,再更新数据库
如下图,当A线程删除缓存数据A,这时A线程出现延迟,B线程将读取A,发现缓存无数据,将数据库A的旧值查出来,并且将其更新到缓存中,当A线程恢复时,又将A的新值写入数据库,这样也会出现严重的双写不一致,导致后续每次取到缓存中的数据都是有问题的。
所以,先删除缓存的方案也不建议选择。
4.4 先更新数据库,再删除缓存(常用)
先更新数据库再删除缓存这种方案是我们所选择的,当然这种也有几率出现缓存不一致的现象,当缓存失效时,就会出现和先删除缓存,在更新数据库一样的问题。如下图:
但是一般情况下,是不会出现上述情况,出现上述情况的机率是特别低的。出现上述情况也可以采取延迟双删,先删除一次,让线程休眠一会,再删除一次,就会将不小心写入的错误数据清掉。
所以说这种方案只会出现下一种情况,如果想要避免这种情况只能通过加锁来解决,避免读到脏数据。
5 优化方案
基于上述方案我们还可以做哪些优化
- 读数据加锁(分布式锁)防止高并发打垮数据库
- 延迟双删,防止缓存失效时(读写分离架构下,读从库延迟问题),存入旧数据,第二次删除可以异步执行等待删除
- 如果需要做重试机制可以依赖于消息队列的可靠消费
- 可以通过订阅Binlog日志来优化删除逻辑
禁忌:过度设计,一般简单的延迟双删就可以实现需求,无需增加系统复杂度
线上问题
有一天线上的应用响应变得很慢,你应该怎么排查?
最近线上环境某个接口服务响应时间偏长,导致用户体验超差,那平时该怎么快速的排查这类问题呢?
①、为代码添加上详细的打印日志;不建议 ,一是线上环境,没法随便的重新部署更换了详细日志的代码,二是 添加详细的日志输出,那就意味这会生成大量的日志文件,这些日志文件会占据大量服务器磁盘空间。
②、搭建一个模拟了线上环境的测试环境进行复盘排查;额,出现了这种问题哪有那么多的时间让你进行环境复盘排查,所以此方案也是 不建议的 。
③、线上诊断神器 Arthas ,这个工具是阿里开源的,专门用于线上环境问题排查的,这个工具提供了很多的 命令 用来排查问题;当出现上面的响应时间偏长的问题,就可以使用 Arthas 提供的 trace 命令进行排查,使用这个工具的 trace 命令可以统计到方法中整个调用链路上的所有性能开销和追踪调用链路,查找其中耗时比较长的方法再具体排查即可。
Arthas 问题排查:
1、首先需要下载阿里开源的Arthas 的诊断工具 Jar 包,下载地址:https://arthas.aliyun.com/arthas-boot.jar ;然后将 Jar 包放到 部署服务接口项目的服务器中 。
2、然后使用 ps 命令,查询出当前运行服务接口的程序进程号;例如:本文章模拟的服务接口程序 Jar 包名称为 springboot_arthas-1.0.0.jar ,所以命令为:ps -ef | grep springboot_arthas-1.0.0 。
3、然后运行Arthas 诊断工具,命令:java -jar arthas-boot.jar
4、可以使用 trace命令 监控服务接口方法中调用的其它方法的耗时;
trace
命令能主动搜索 class-pattern
/method-pattern
对应的方法调用路径,渲染和统计整个调用链路上的所有性能开销和追踪调用链路。
具体命令格式:trace 全限定类名
例如:监控本服务接口;
com.lyl.controller.TestController : 全限定类名,process:TestController 类中的方法;
具体命令: trace com.lyl.controller.TestController process
通过trace 命令监控统计的调用链路各个方法的执行耗时,可以发现调用的 com.lyl.util.StringUtil 类中的 test2() 方法执行耗时比较大;所以需要特别去查看这个方法的代码是否存在问题;如果这个代码中还存在许多的方法调用链路,则需要再次使用 trace 命令进行监控调用链路的耗时,找出具体可能存在问题的方法。
Arthas 阿里开源的诊断工具还提供了很多的命令供使用,大家可以去查看学习,地址:https://arthas.aliyun.com/doc/commands.html 。
注意: ①、使用Arthas 诊断的程序代码,在打包时 不能混淆 ,否则在使用trace 命令会报 类或方法找不到 ; ②、在使用trace命令监控统计时,需要JMeter测试脚本正在运行调用服务接口,如果没有调用,则统计不到内部调用链路的耗时情况;
概念性质
里氏替换
该原则的核心思想就是在程序当中,如果将一个父类对象替换成它的子类对象后,该程序不会发生异常。这也是该原则希望达到的一种理想状态。
通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。
里氏代换原则是开闭原则的重要方式之一,由于使用父类对象的地方都可以使用子类对象,因此在程序中尽量使用父类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
优点:
1、代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。2、提高代码的重用性; 3、子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,4、“世界上没有两片完全相同的叶子”是指明子与父的不同; 5、提高代码的可扩展性,实现父类的方法就可以“为所欲为”了,君不见很多开源框架的扩展接口他都是通过继承父类来完成的; 6、提高产品或项目的开放性。
缺点:
1、继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法; 2、降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束; 3、增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果————大段的代码需要重构。