今天分享一位读者春招的字节二面面经,岗位是后端开发。 一个编程语言都没问,都是问网络项目mysqlredis。问题记录使用消息中间件降低消息持久化的压力是怎么做的,为什么可以降低? 读者答:在突发大量消息的情况下可以做到流量削峰,在消费者消费能力达不到生产者产生消息的速度时也能够正常运行。怎么解决消息队列上的消息堆压? 读者答:(1)自身场景下,消息堆压是暂时的,消息堆压只是突发状况,就算不额外处理,随着时间流逝也可消费完毕。(2)假如存在持续性消息堆压,可以考虑临时增加消费者的数量,提升消费者的消费能力。 小编补充: 如果是线上突发问题,要临时扩容,增加消费端的数量,与此同时,降级一些非核心的业务。通过扩容和降级承担流量,这是为了表明你对应急问题的处理能力。其次,才是排查解决异常问题,如通过监控,日志等手段分析是否消费端的业务逻辑代码出现了问题,优化消费端的业务处理逻辑。在并发编程时,在需要加锁时,不加锁会有什么问题? 读者答:两个线程使用同一个全局变量会有不一致的问题,比如a线程把全局变量加1,b线程读的时候,如果还是从缓存中读的,那么会没有发现这个更新,就会产生不一致的问题。如何避免出现死锁? 读者答:在使用之前,考虑死锁产生的条件:互斥访问、占有并保持、循环等待。 针对以上几点,可以:资源一次性分配、占有时可被打断、考虑资源分配顺序。HTTP状态码有哪些? 读者答:1xx信息的响应,比如100表示临时响应。2xx成功。3xx重定向,301永久更改,308临时更改(是308么?)哦不,是307,记得不是很清楚了。4xx客户端问题,404notfound找不到资源,403客户端没有访问权限。5xx服务端问题,502网关问题。 面试官:307其实是临时没错,他是比较新的版本,旧的是302,现在业界还是旧的用的比较多。 小编补充: 五大类HTTP状态码 1xx类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。 2xx类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。200OK是最常见的成功状态码,表示一切正常。如果是非HEAD请求,服务器返回的响应头都会有body数据。204NoContent也是常见的成功状态码,与200OK基本相同,但响应头没有body数据。206PartialContent是应用于HTTP分块下载或断点续传,表示响应返回的body数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态。 3xx类状态码表示客户端请求的资源发生了变动,需要客户端用新的URL重新发送请求获取资源,也就是重定向。301MovedPermanently表示永久重定向,说明请求的资源已经不存在了,需改用新的URL再次访问。302Found表示临时重定向,说明请求的资源还在,但暂时需要用另一个URL来访问。 301和302都会在响应头里使用字段Location,指明后续要跳转的URL,浏览器会自动重定向新的URL。304NotModified不具有跳转的含义,表示资源未修改,重定向已存在的缓冲文件,也称缓存重定向,也就是告诉客户端可以继续使用缓存资源,用于缓存控制。 4xx类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。400BadRequest表示客户端请求的报文有错误,但只是个笼统的错误。403Forbidden表示服务器禁止访问资源,并不是客户端的请求出错。404NotFound表示请求的资源在服务器上不存在或未找到,所以无法提供给客户端。 5xx类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。500InternalServerError与400类型,是个笼统通用的错误码,服务器发生了什么错误,我们并不知道。501NotImplemented表示客户端请求的功能还不支持,类似即将开业,敬请期待的意思。502BadGateway通常是服务器作为网关或代理时返回的错误码,表示服务器自身工作正常,访问后端服务器发生了错误。503ServiceUnavailable表示服务器当前很忙,暂时无法响应客户端,类似网络服务正忙,请稍后重试的意思。HTTP1。0和2。0的区别,或者2。0和3。0的区别? 读者答:(1)每次请求他都会与服务器建立一个tcp的连接。请求处理完以后他会立刻断开tcp,他有短暂的一个tcp链接。在1。1中,他稍微有些改善。(2)2。0提供了一个多路复用的功能。还有二进制分帧和首部压缩。 小编补充: HTTP2相比HTTP1。1性能上的改进:头部压缩二进制格式并发传输服务器主动推送资源 1。头部压缩 HTTP2会压缩头(Header)如果你同时发出多个请求,他们的头是一样的或是相似的,那么,协议会帮你消除重复的部分。 这就是所谓的HPACK算法:在客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。 2。二进制格式 HTTP2不再像HTTP1。1里的纯文本形式的报文,而是全面采用了二进制格式,头信息和数据体都是二进制,并且统称为帧(frame):头信息帧(HeadersFrame)和数据帧(DataFrame)。 HTTP1与HTTP2 这样虽然对人不友好,但是对计算机非常友好,因为计算机只懂二进制,那么收到报文后,无需再将明文的报文转成二进制,而是直接解析二进制报文,这增加了数据传输的效率。 3。并发传输 我们都知道HTTP1。1的实现是基于请求响应模型的。同一个连接中,HTTP完成一个事务(请求与响应),才能处理下一个事务,也就是说在发出请求等待响应的过程中,是没办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了队头阻塞的问题。 而HTTP2就很牛逼了,引出了Stream概念,多个Stream复用在一条TCP连接。 从上图可以看到,1个TCP连接包含多个Stream,Stream里可以包含1个或多个Message,Message对应HTTP1中的请求或响应,由HTTP头部和包体构成。Message里包含一条或者多个Frame,Frame是HTTP2最小单位,以二进制压缩格式存放HTTP1中的内容(头部和包体)。 针对不同的HTTP请求用独一无二的StreamID来区分,接收端可以通过StreamID有序组装成HTTP消息,不同Stream的帧是可以乱序发送的,因此可以并发不同的Stream,也就是HTTP2可以并行交错地发送请求和响应。 比如下图,服务端并行交错地发送了两个响应:Stream1和Stream3,这两个Stream都是跑在一个TCP连接上,客户端收到后,会根据相同的StreamID有序组装成HTTP消息。 4、服务器推送 HTTP2还在一定程度上改善了传统的请求应答工作模式,服务端不再是被动地响应,可以主动向客户端发送消息。 客户端和服务器双方都可以建立Stream,StreamID也是有区别的,客户端建立的Stream必须是奇数号,而服务器建立的Stream必须是偶数号。 比如下图,Stream1是客户端向服务端请求的资源,属于客户端建立的Stream,所以该Stream的ID是奇数(数字1);Stream2和4都是服务端主动向客户端推送的资源,属于服务端建立的Stream,所以这两个Stream的ID是偶数(数字2和4)。 再比如,客户端通过HTTP1。1请求从服务器那获取到了HTML文件,而HTML可能还需要依赖CSS来渲染页面,这时客户端还要再发起获取CSS文件的请求,需要两次消息往返,如下图左边部分: img 如上图右边部分,在HTTP2中,客户端在访问HTML时,服务器可以直接主动推送CSS文件,减少了消息传递的次数。 HTTP2有什么缺陷? HTTP2通过Stream的并发能力,解决了HTTP1队头阻塞的问题,看似很完美了,但是HTTP2还是存在队头阻塞的问题,只不过问题不是在HTTP这一层面,而是在TCP这一层。 HTTP2是基于TCP协议来传输数据的,TCP是字节流协议,TCP层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给HTTP应用,那么当前1个字节数据没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这1个字节数据到达时,HTTP2应用层才能从内核中拿到数据,这就是HTTP2队头阻塞问题。 img 举个例子,如下图: img 图中发送方发送了很多个packet,每个packet都有自己的序号,你可以认为是TCP的序列号,其中packet3在网络中丢失了,即使packet46被接收方收到后,由于内核中的TCP数据不是连续的,于是接收方的应用层就无法从内核中读取到,只有等到packet3重传后,接收方的应用层才可以从内核中读取到数据,这就是HTTP2的队头阻塞问题,是在TCP层面发生的。 所以,一旦发生了丢包现象,就会触发TCP的重传机制,这样在一个TCP连接中的所有的HTTP请求都必须等待这个丢了的包被重传回来。 前面我们知道了HTTP1。1和HTTP2都有队头阻塞的问题:HTTP1。1中的管道(pipeline)虽然解决了请求的队头阻塞,但是没有解决响应的队头阻塞,因为服务端需要按顺序响应收到的请求,如果服务端处理某个请求消耗的时间比较长,那么只能等响应完这个请求后,才能处理下一个请求,这属于HTTP层队头阻塞。HTTP2虽然通过多个请求复用一个TCP连接解决了HTTP的队头阻塞,但是一旦发生丢包,就会阻塞住所有的HTTP请求,这属于TCP层队头阻塞。 HTTP2队头阻塞的问题是因为TCP,所以HTTP3把HTTP下层的TCP协议改成了UDP! HTTP1HTTP3 UDP发送是不管顺序,也不管丢包的,所以不会出现像HTTP2队头阻塞的问题。大家都知道UDP是不可靠传输的,但基于UDP的QUIC协议可以实现类似TCP的可靠性传输。 QUIC有以下3个特点。无队头阻塞更快的连接建立连接迁移 1、无队头阻塞 QUIC协议也有类似HTTP2Stream与多路复用的概念,也是可以在同一条连接上并发传输多个Stream,Stream可以认为就是一条HTTP请求。 QUIC有自己的一套机制可以保证传输的可靠性的。当某个流发生丢包时,只会阻塞这个流,其他流不会受到影响,因此不存在队头阻塞问题。这与HTTP2不同,HTTP2只要某个流中的数据包丢失了,其他流也会因此受影响。 所以,QUIC连接上的多个Stream之间并没有依赖,都是独立的,某个流发生丢包了,只会影响该流,其他流不受影响。 img 2、更快的连接建立 对于HTTP1和HTTP2协议,TCP和TLS是分层的,分别属于内核实现的传输层、openssl库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先TCP握手,再TLS握手。 HTTP3在传输数据前虽然需要QUIC协议握手,但是这个握手过程只需要1RTT,握手的目的是为确认双方的连接ID,连接迁移就是基于连接ID实现的。 但是HTTP3的QUIC协议并不是与TLS分层,而是QUIC内部包含了TLS,它在自己的帧会携带TLS里的记录,再加上QUIC使用的是TLS1。3,因此仅需1个RTT就可以同时完成建立连接与密钥协商,如下图: TCPHTTPS(TLS1。3)和QUICHTTPS 甚至,在第二次连接的时候,应用数据包可以和QUIC握手信息(连接信息TLS信息)一起发送,达到0RTT的效果。 如下图右边部分,HTTP3当会话恢复时,有效负载数据与第一个数据包一起发送,可以做到0RTT(下图的右下角): img 3、连接迁移 基于TCP传输协议的HTTP协议,由于是通过四元组(源IP、源端口、目的IP、目的端口)确定一条TCP连接。 TCP四元组 那么当移动设备的网络从4G切换到WIFI时,意味着IP地址变化了,那么就必须要断开连接,然后重新建立连接。而建立连接的过程包含TCP三次握手和TLS四次握手的时延,以及TCP慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。 而QUIC协议没有用四元组的方式来绑定连接,而是通过连接ID来标记通信的两个端点,客户端和服务器可以各自选择一组ID来标记自己,因此即使移动设备的网络变化后,导致IP地址变化了,只要仍保有上下文信息(比如连接ID、TLS密钥等),就可以无缝地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。口述回答题目:MySQL联合索引 一开始给了个写sql语句的题,我说我平时用orm框架访问数据库,很少写sql,于是换了个索引的问题。 题目: index(abc)selectfromTwhereaxandbyandczselectfromTwhereaxandbyandczselectfromTwhereczandaxandbyselect(a,b)fromTwhereaxandbyselectcount()fromTwhereaxselectcount()fromTwherebyselectcount()formT 读者答:(1)根据最左匹配原则,走索引,先匹配a再匹配b再匹配c。(2)a走索引,b因为在索引中有序,依旧可以走索引,c需要回表查询。(3)因为mysql查询器优化,和(1)一样。(4)a走索引,b也走索引,因为ab都在索引上,最后不用回表。(5)走索引。(6)只有b,无法使用联合索引,需要回表查询。(7)不需要回表。 小编补充:a、b、c三个字段都可以走联合索引a和b都会走联合索引,但是由于最左匹配原则,范围查找后面的字段是无法走联合索引的,但是在mysql5。6版本后,c字段虽然无法走联合索引,但是因为有索引下推的特性,c字段在inndob层过滤完满足查询条件的记录后,才返回给server层进行回表,相比没有索引下推,减少了回表的次数。查询条件的顺序不影响,优化器会优化,所以a、b、c三个字段都可以走联合索引a和b都会走联合索引,查询是覆盖索引,不需要回表a可以走联合索引只有b,无法使用联合索引,由于表存在联合索引,所以count()选择的扫描方式是扫描联合索引来统计个数,扫描的方式是typeindex由于表存在联合索引,所以count()选择的扫描方式是扫描联合索引来统计个数,扫描的方式是typeindex 关于count()为什么选择扫描联合索引(二级索引),而不扫描聚簇索引的原因:这是因为相同数量的二级索引记录可以比聚簇索引记录占用更少的存储空间,所以二级索引树比聚簇索引树小,这样遍历二级索引的IO成本比遍历聚簇索引的IO成本小,因此优化器优先选择的是二级索引。Redis怎么实现分布式锁 读者答:应用程序获取redis的连接,然后发送一个命令获取锁。(面试官:什么命令?)SETNX。因为这个指令是Redis里的原子命令,可以把一个key设置其对应的value。然后这个时候这个应用程序就就开始去临界区进行一些操作。如果说如果说他看来运他执行那个命令失败了。就说明这个锁已经被其他的户端持有了,这样的话他就需要等待。成功获取锁并且执行完临界区操作之后,便可以用del命令在redis删除这个key,实现释放锁的目的。 小编补充: Redis的SET命令有个NX参数可以实现key不存在才插入,所以可以用它来实现分布式锁:如果key不存在,则显示插入成功,可以用来表示加锁成功;如果key存在,则会显示插入失败,可以用来表示加锁失败。 基于Redis节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用SET命令带上NX选项来实现加锁;锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在SET命令执行时加上EXPX选项,设置其过期时间;锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用SET命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端; 满足这三个条件的分布式命令如下:SETlockkeyuniquevalueNXPX10000lockkey就是key键;uniquevalue是客户端生成的唯一的标识,区分来自不同客户端的锁操作;NX代表只在lockkey不存在时,才对lockkey进行设置操作;PX10000表示设置lockkey的过期时间为10s,这是为了避免客户端发生异常而无法释放锁。 而解锁的过程就是将lockkey键删除(dellockkey),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的uniquevalue是否为加锁客户端,是的话,才将lockkey键删除。 可以看到,解锁是有两个操作,这时就需要Lua脚本来保证解锁的原子性,因为Redis在执行Lua脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。释放锁时,先比较uniquevalue是否相等,避免锁的误释放ifredis。call(get,KEYS〔1〕)ARGV〔1〕thenreturnredis。call(del,KEYS〔1〕)elsereturn0end 这样一来,就通过使用SET命令和Lua脚本在Redis单节点上完成了分布式锁的加锁和解锁。思考题:抛硬币,先抛到正面算赢,否则轮流抛。问先抛的人获胜的概率。 读者答:阿巴阿巴考虑一轮,(正,)(反,正)(反,反),其中,(正,)可以还原成(正,正)(正,)两份,而(反,反)相当于再来一次,这样(正,)占2份,(反,正)占1份,赢的概率就算23算法题:寻找峰值 给定一个输入数组nums,其中nums〔i〕!nums〔i1〕,找到峰值元素并返回其索引。 数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。 你可以假设nums〔1〕nums〔n〕。 读者答:(1)遍历递增序列,遇到第一个非递增的位置返回,时间复杂度为O(n)。(2)寻找最大值,时间复杂度依旧为O(n)。(3)二分查找变体,在二分查找时,取中间位置m,并与它相邻位置m1进行比较,如果m大于m1,说明峰值应该在左侧,否则应该在右侧,移动对应的左右边界。反问 (1)在面试的过程中,我存在什么问题,需要在哪些方面加强学习?或者说想要从事后端开发,我还欠缺什么知识? 面试官:数据库的部分你可以多研究一下。微服务的部分可能也可以研究一下,因为像我刚刚其实问你能不能讲微服务,我其实是希望你能懂的,你懂的话我就问这方面的问题。 (2)假如我有机会加入进行实习,大概会做些什么样的内容? 面试官:后端这块,我们做的B2B的,我们的产品形态会跟数据库有一点像。如果能够深度的去,了解一下数据库的实现、怎么设计的,那可能会比较好一点。面试总结感觉 感觉整体沟通比较顺畅,面试官很和蔼,在我回答有问题或者卡壳的时候能很好的引导,给人感觉很舒服。不足之处有些没准备的问题回答的不是很好,比如对比http1。0和2。0,答的很没有条理。回答过程中不必要的语气词和废话过多,对于自己不确定的问题给人感觉印象很差。 来源:https:mp。weixin。qq。comsZvRS6bmB64nihfzz72c46g