字节面经
上次面试字节的时候,面试官问了我的项目
:特指面试官
:特指我
你先介绍一下这个项目吧?
Playing-Life呢,是专门用于运动组队的一个项目,然后里面的核心板块儿分别是用户和组队,其中用户模块儿里提供了基本的增删改查,可以方便用户进行个人信息的修改以及查询与自己标签相匹配的用户。而组队板块,则为用户提供了创建队伍,更新队伍,查看自己加入或者创建队伍,退出队伍等功能。
上面就是我当时的回答,现在来回过头想想,确实有点业余了哈,那不如修改一下或者精进一下,显得更Professional一些。
Playing-Life呢,是为了进行用户运动组队而诞生的一个网页。其中,使用SpringBoot完成了后端的开发,为用户提供了基于Cookie的单点登录,用户更新个人标签,根据标签进行查询用户的接口,另外,对于首页推荐用户提供了使用Spring Scheduler进行缓存预热的方案,避免用户第一次访问花费过多时间。为了提升用户的访问体验,这里采用了旁路缓存的策略来实现数据更新。
自恋的总结一波,怎么说呢?就感觉第一个回答很客观,没有任何技术点,我要是面试官,就不知道要问啥问题。好的,来看第二个,这样就很不错,抛出来了三个点来方便面试官进行提问。
首先为啥要使用Cookie,有没有了解过其他的单点登录实现方式?
用户在登录成功之后会生成一个Session记录在服务端,然后返回的时候会返回一个SessionID,而这个SessionID就是之后要用的Cookie,这里使用Cookie是结合着共享Session来实现的,因为在分布式系统中,会用到负载均衡,所以有可能会有多台服务器来被分发,而这里用上共享Session是每台服务器都从Redis中去取SessionID用来判断是否登录。这样就可以保证不需要在登录之后重新进行登录。
其他的单点登录方式有了解过JWT,JWT是存储在客户端,在发送请求的时候会携带,它是由头部,载荷,签名三部分组成,其大小肯定会比一个sessionID的字符串大,会在一定程度上浪费网络资源,然后JWT还有一个服务端的验证过程,会验证签名是否正确,即在服务端重新计算一遍签名,确保有效。
追问: 你刚才提到了网络资源浪费,为什么会浪费资源呢?
看,这不就来了嘛!这不就将HTTP呼之欲出了。因为在网络传输的过程中可以将传输过程分为TCP、IP四层协议栈,而最为核心的是网络层,传输层与应用层,其中传输层的TCP协议与网络层的IP协议都会对发送的数据进行分段发送,就会发生多次请求。那原来一次就可以发送完成,现在多了好几次,那不就是浪费资源吗?
别急,这里还能继续追问。 好的,了解,刚刚你提到了TCP协议与IP协议的分段发送,那你讲一下TCP协议以及IP协议为什么要进行分段发送吧?
IP协议的分段其实叫分片,引入这个分片机制最主要的是为了防止传输内容过大,无法在网络中传输的问题。而引入分片机制后,在超过IP数据包超过MTU就可以进行分片,添加标识与偏移量来获取相对位置,并在目标主机进行重组来获取完整的数据,但是一旦其中一个分片丢失,就会导致目标主机重组IP报文失败,而导致无法得到完整的TCP数据,从而使得一整个IP数据报都需要重传(重传所有的IP分片),非常的浪费资源。
而TCP引入的分段就是为了解决无法重组得到TCP数据。在一个TCP数据超过MSS时,就会对其进行分段,这样就能保证每一个IP数据报里都有一个完完整整的TCP分段数据,这样即便其中一个IP数据报丢失,也只是相当于丢失了一段TCP信息,只会重传丢失的那一个报文,并不会全部重传。由此来看,TCP层传输大大提升了传输效率。
你觉得这就完了吗?不不不不,还可以继续追问
好的,了解,刚才聊到了TCP的分段是为了防止丢包,那可以讲一下TCP是怎样进行传输的吗?
好的,TCP首先是通过三次握手建立了连接,然后之后会进行数据传输。然后TCP的可靠性主要是通过序列号与确认应答来实现的,即接收方在接收到发送方的数据之后会返回一个确认应答消息,在发送方未接收到相应的确认消息就会触发超时重传。但是这样有一个坏处,就是比如说有一个包在网络中丢失了,然后触发超时重传,就会导致后面的数据没有办法被发送,引起阻塞,比较影响性能,然后呢,(太啰嗦了,然后中间有一个点没想起来的话就会很影响面试官的体验,不如埋个坑,让面试官来引出来。)发送方发送数据,接收方接收后会返回一个确认消息,来进行正常的数据传输,但是可能在这个过程中会出现丢包或者网络延迟导致发送的数据没有被接收方接收,这样就会触发TCP的超时重传或者快速重传机制,在长时间内无法接收到确认信息的话有可能会导致TCP连接断开。然后其中一方执行主动断开连接来进行四次挥手关闭TCP连接。(这里真的真的好多坑啊)
是的,到了这个篇幅,似乎还没有结束,如果面试官比较执着的话,还是可以继续问下去的。
你刚才说到的丢包引起的超时重传,可以讲一下超时重传和快速重传的区别吗
好的,其中超时重传是在发送方并未接收到接收方发送的确认信息,在超过超时重传时间之后会进行数据重传,如果下一个超时重传的时间间隔内依旧没有收到确认信息,就会将等待时间变成两倍,然后达到一定的次数,其中linux内核里设置的tcp_retries2默认是15,也就是在依次增加两倍的时间持续15次之后,依旧没有得到确认消息,这个时候就会断开TCP连接。
而快速重传则是通过数据来进行判断是否需要重传,而不是超时重传的通过时间判断。在发送方发送了连续5个数据之后,假设第二次数据在网络中发生了延迟,没有到达接受方,这个时候后三个确认信息的ack全部都是2,就会触发快速重传机制,而为了保证不重传后面的所有数据,引入SACK(选择性确认),他就是在成功的接收数据之后呢,只返回当前成功接收的那些数据。比如还是发送5个数据,其中第二个数据丢失,然后其他均成功接收,这个时候后面的3个确认信息返回的就有SACK是3、34,35。
好的,了解,那我还想问一下,刚刚你说到TCP断开连接,那有哪些情况下TCP会发生四次挥手断开连接呢?
四次挥手发生的一般都是其中一方主动去断开连接,比如说客户端在发送完数据之后主动关闭TCP连接,就会发生四次挥手来关闭连接。也有一些异常情况,比如说其中一段的进程崩溃,会引起内核回收TCP连接资源,率先发送Fin请求。
而且断开TCP连接也有其他的一些方式,比如说其中一方直接断开连接,比较常见的场景就是客户端或者服务端掉电的情况下(断电导致主机崩溃),这种情况下就不会发生四次挥手了。如果有数据传输的话,可能在达到最大的超时时间之后就会断开连接,而如果没有数据传输的话,而且也没有开启tcp-active的话,正常的那一端的tcp连接会一直存在,并且一直保持在Established的状态。而开启了tcp-active的话,正常的那一段会每隔一个保护时间,就发送一些比较小的探测报文,如果连续几个都没有接收到的话,就会判定该连接已死,然后断开连接。
不敢相信,到了这里竟然只是有一个问题衍生出来的。这就是压迫感,其实这里还能扩散的问一个问题。(算了,再来一个吧)
你刚才有提到快速重传,里面实现传输的方式似乎是滑动窗口?可以讲一下TCP的滑动窗口是怎样实现的吗?
好的,TCP的报文头里有一个字段叫window,是代表滑动窗口的大小,也就是接收方告诉发送方可接收的数据大小。发送方就会被分成四部分,第一部分是已经接收并且确认的数据,第二部分是已经接收但是未确认的数据,第三部分是未接收但是在接收方处理能力之内的数据,第四部分是未接收但是不在接收方处理能力之内的数据。因此也就组成了可用窗口、发送窗口。而接收方就只有已发送并确认的数据,已发送但未收到确认的数据,未发送数据。而且滑动窗口并不是一成不变的,而是在每次发送数据的时候发生变化。滑动窗口的过程为接收方确认信息中的发送窗口提供给发送方,发送方可以直接不用等待ACK信息将发送窗口的数据全部发送,这时可用窗口为0,然后在发送方接收到接收方发来的ACK后,发送窗口会向右滑动,此时可用窗口增加,后续便可以发送可用窗口的内容。直至最后完成数据传输。
好了,今天的面试之旅就到这里,明天继续回忆被拷打的痛苦。
昨天聊到TCP的滑动窗口,但是有一点我是没搞太清楚,所以就匆匆结束了追问,其实有一个点是TCP的滑动窗口到底是将发送窗口全部发送,还是只发送可用窗口的内容。这个就要考虑到滑动窗口的接收机制,正常情况下就是发送窗口全部发送之后接收方全部返回ack确认信息,发送方收到后,滑动窗口向后移动。可是网络中可能产生丢包,比如发送方的发送信息丢失,或者接收方的确认信息丢失。
发送但未接收
第一种情况:发送方发送之后未被接收方接收,需要进行重传,接收方与发送方的窗口均为发生变化。其中包括超时重传与快速重传
第二种情况:发送方发送之后被接收方接收,但是在发送确认信息时丢失。如果不是最后一个消息丢失,发送方会根据累计确认原则得知丢失的确认消息,就不需要再次重传数据;如果是最后一个消息丢失,发送方会触发超时重传,接收方并不会重复发送确认信息,而是直接丢弃,等后面的数据接收之后通过ACK序列号进行累计确认。
:好的,了解,那你讲一下TCP的三次握手和四次挥手吧。
:好的,(这里还是得注意一下,虽然听了很多的内容说要引导面试官,但是这里还是要把面试官问的问题说完再去做扩展,要不然就会给面试官一种这小子啥也不知道啊的错觉)。
TCP的三次握手,首先服务端与客户端均处于close状态,然后服务端主动监听某个端口,处于Listen状态,接着便是客户端随机化一个序列号,然后将这个序列号放入TCP报文的头部,并将其中的SYN置为1,将其发送给接收方,然后客户端处于SYN-SENT状态,接收方接收数据之后,同样会生成一个随机序列号放入发送序列号,然后将接收到的序列号+1放入确认应答号,并同时将SYN与ACK置为1,并将该报文发送给客户端,然后处于SYN-Recv状态,最后客户端接收到报文之后,同样会回复一个确认报文,其中确认应答号为服务端发送的序列号加1,并将ACK置为1,且可以携带数据进行发送,之后客户端进入Established状态,服务端在接收到确认报文之后也进入Established状态。
然后TCP的三次握手是为了保证不会有历史连接重置TCP连接导致资源浪费
TCP的四次挥手则是发送方向接收方发送FIN报文,然后发送方进入FIN_WAIT_1状态,接着接收方接收到FIN保温之后,回复一个ACK确认请求,进入CLOSE_wait状态,接着发送方接收到确认信息后进入FIN_WAIT_2状态,接着接收方继续发送数据,发送完成之后发送一个FIN报文,并进入LAST_ACK状态,发送方接收之后回复一个ACK确认信息,并进入TIME_WAIT状态,接收方接收到ACK之后关闭连接,而发送方在等待两个MSL之后自动关闭连接。
:那为什么TCP连接建立需要三次握手,不是二次握手或者四次握手呢?
:因为三次握手是为了保证不会发生历史连接重置连接,保证发送与接收方的序列号同步,保证资源部被浪费。
其中历史连接是考虑到在客户机发送一个SYN之后重启,然后SYN发生延迟,此时客户机右发送一个新的SYN,而此时新的SYN也发生了阻塞,这时旧的SYN终于被接收端接收,然后接收方进入SYN_RCVD状态,发送一个ACK确认信息,但是发送方需要接受的是新的SYN的确认信息,所以会回复一个RST报文,中止连接。
而如果是两次握手就可以建立连接的话,就会发生接收方在收到旧的SYN之后建立连接,然后就可以发送数据,而发送方在接收确认信息之后才能回复RST报文。造成了资源浪费。
其实最开始的时候三次握手是四次的,因为中间的第二次发送的其实是ACK+SYN,由于优化之后成为了一次握手,可以更少的握手次数建立连接。
:讲了这么多的TCP,问你个别的吧,看你是网安专业的,了解过 syn 洪范攻击吗?
:了解过,SYN洪范攻击是指在TCP进行三次握手阶段频繁的发送SYN报文,以至于服务端不停的发送SYN-ACK报文,但是又没有得到发送方的ACK报文,就会逐渐占满半连接队列,使得正常的SYN无法得到响应。
:可以讲一下半连接队列是啥吗?
:好的,Linux内核在进行TCP三次握手的时候会维护两个队列,一个是半连接队列(SYN队列),一个是全连接队列(accept队列),在接收方收到发送方的SYN信号之后就会发送一个SYN+ACK的响应,然后这时候就会创建一个半连接对象放入半连接队列中,在发送方的ACK被接收方接收后,接收方会将半连接队列中对应的半连接对象取出来,生成一个全连接对象放入全连接队列中,然后应用需要发送数据就会调用Socket的accept方法,取出全连接对象。
接着你的简历里写着用SpringScheduler定时任务解决缓存预热,能讲一下怎样实现的吗?
好的,因为首页用户页会推荐给用户一些比较相似标签的用户,而我这里呢,如果说每次用户进入系统的时候都要在去查一遍数据库,数据量少的时候还好说,数据量大的话就会有很大的延迟,严重影响用户体验,所以我在这里增加了一层缓存,使用的是Redis数据库,然后就解决了用户访问的时候不会延迟过高。但是这样依旧存在用户第一次访问的时候延迟过高,所以这里采用了使用定时任务进行缓存预热,也就是在早上8点执行定时任务,将白名单内的重点用户的推荐用户都预先计算一遍放入缓存中,并设置12个小时的过期时间。另外,因为涉及到相似程度的计算,以及查询数据库,所以可能花费的时间比较长,所以可能导致锁到达过期时间依旧没有结束任务,所以这里采用了Redisson分布式锁中的看门狗机制,如果超过10秒为结束任务,就给锁的过期时间续期,直至完成缓存任务。
这里是一个业务有关的回答,所以就不像前面的那一个问题可以追问的那么深,不过如果刁钻的话,依旧是可以追问的。比如:
那你用到的这个分布式锁中的watchDOG里面的续期机制,你这个任务真的需要花费这么长时间的吗?你的查询SQL是一条吗?还是多条SQL查询?
是的,其实它的主要时间并不是花费在查询上面,而是计算编辑距离,因为是根据标签计算相似用户,所以是遍历数据库中的所有用户,所以会花费的时间比较久。我的实现方式是首先将数据库中的所有拥有标签的用户查出来,然后放入一个集合中,然后遍历集合中的每一个元素并计算与当前用户标签的差距。而查询呢,我并不是直接查询所有的数据,而是利用了MyBatisPlus中的分页插件,每次查询1000条数据,然后依次进行计算。
好的,了解,那既然聊到数据库了,你可以讲一下数据库的索引吗?(看到没,又被带到了另外一个坑里面)
数据库的索引是为了更快速的查询数据,然后比较常见的索引模型有B+数据模型,也就是MySQL中InnDB存储引擎用到的索引模型。(又来挖坑了,等着面试官来跳)
好的,那你可以讲一下MySQL的其他存储引擎吗?为什么MySQL要使用InnoDB来当默认的存储引擎呢?
我了解的存储引擎由MyISAM、InnoDB、Memory,其中MyISAM也是利用B+树充当索引,但是它不具有事务特性,而唯一支持事务的是InnoDB,而且一般需求都需要保证数据库的事务特性,所以就默认使用的是InnoDB,但是如果不需要保证事务的四大特性的时候就可以使用MyISAM,因为它的索引相对结构简单,查询速度会更快。
其实这里还有很多问题,但是我怕今天继续这样的话真的收不了尾,所以我在这里列举几个(你刚刚提到数据库的事务的四大特性,那你可以讲一下他们吗?然后再讲一下事务的四大特性是怎样实现的?)
然后你说用到了旁路缓存策略,可以讲一下这个策略吗?
好的,旁路缓存策略呢,就是在更新数据库的时候采用先更新数据库后删除缓存。然后在读取数据时读取缓存,如果缓存不存在就去数据库查询,并将查询数据放入缓存中。
为什么要先更新数据库再删除缓存呢?不能是先删除缓存再更新数据库吗?
不行,因为这样可能会出现数据错误,比如有两个请求,一个请求A,去更新数据A=21,原来的A=20,那么先删除缓存,然后更新数据库,而在请求A删除缓存之后,请求B需要获取A的数据,于是去查缓存,结果未命中,然后查到数据库是20,更新缓存为20,而这个时候请求A更新数据库A=21,就会发生缓存与数据库数据不一致。
然后大概就是整个项目我觉得可能会问到的点。算是挖得比较深了吧。感觉还不错,但是花的时间稍微有点多,因为今天是花了一整天来写这个面试系列(可能主要是画了几个图)。