字节后端面经:黑马点评项目引发的Redis、MySQL等问题大拷问
本文系经授权转载自小林coding(ID:CodingLin)平台。

作者丨小林coding
今日为大家带来一篇字节跳动后端面试经验分享,该项目的核心是黑马点评,其中对 Redis 的应用尤为频繁。
因此,本次面试主要针对redis相关的问题进行了深入探讨,包括大key和热key等关键问题。此外,还涉及了mysql数据库、操作系统、网络以及java编程等方面的考察。
Redis你用到了Redis,那么Redis还可以做什么?
Redis是一款内存型数据库,其数据读写操作均在内存层面进行,故而读写效率极高,广泛适用于缓存系统、消息队列以及分布式锁等多种应用场景。
img
Redis能够支持多样化的业务需求,涵盖了诸如String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)以及Stream(流)等多种数据类型。这些数据类型的操作均具有原子性,这是因为Redis的命令执行是由单个线程来完成的,从而避免了并发冲突的情况。
除此之外,Redis具备事务处理能力,并支持数据持久化功能。它还兼容Lua脚本执行,提供了多种集群解决方案,包括主从复制、哨兵监控以及切片机群模式。此外,它还实现了发布/订阅机制、内存淘汰策略以及过期数据删除机制等。
讲下Redis的ZSet
Zset 类型的底层数据结构是由压缩列表或跳表实现的:
在 Redis 7.0 版本中,原有的压缩列表数据结构已被淘汰,其功能已由 listpack 数据结构所取代。
ZSet 的范围查询时间复杂度是多少?
Redis的有序集合ZSet的查询范围命令,其时间复杂度为O(log(N)+M),这里N代表该集合中元素的总数,而M则表示查询结果中元素的数量。
讲一下跳表
链表在搜索特定元素时,由于必须逐个节点进行扫描,导致其查询速度相当缓慢,其时间复杂度达到O(N)。鉴于此,跳表应运而生。跳表是在传统链表的基础上进行优化升级的,它形成了一种包含“多级”结构的有序链表,这一设计使得数据定位更加迅速高效。
这跳表究竟呈现何种形态?让我以实例来为您说明,下面这张图便展示了一个层级达到三级的跳表样式。
img
图中该头节点配备了L0至L2共三个指针,这些指针分别指向了不同层级的节点,随后,各个层级的节点之间通过指针相互连接。
若要在链表中搜寻节点4,必须从链表首部逐个检查,需进行四次查找,但若运用跳表技术,仅需两次查找即可准确找到节点4,这得益于跳表能从顶层的L2层直接跳跃至节点3,随后再进行一次向前遍历即可定位到节点4。
观察可知,这一搜索过程涉及在多个级别间穿梭,直至准确锁定目标元素;在数据规模庞大时,跳表的查询难度可降至O(logN)级别。
跳表是怎么设置层高的?
在创建节点过程中,跳表会随机生成一个位于0到1之间的数值,若该数值低于0.25(即25%的概率),则节点层数将递增一层,随后继续产生新的随机数;此过程将持续进行,直至生成的随机数值超过0.25,从而最终确定该节点的层数。
哈希表是怎么扩容的?
为防止数据迁移时因复制数据所耗费的时间过长而降低 Redis 的性能,Redis 选择了渐进式 rehash 的策略,这意味着数据的迁移并非一次性完成,而是通过分阶段逐步进行。
渐进式 rehash 步骤如下:
如此一来,便巧妙地将大量数据一次性迁移所需成本,分散到了多次处理请求的流程中,成功规避了单次大规模rehash操作所耗费的时间。
在执行渐进式rehash操作时,系统会同时维护两张哈希表,因此在此过程中,对哈希表元素的删除、查找以及更新等操作,均需在这两张哈希表上分别进行。
哈希表扩容的时候,有读请求怎么查?
若需寻找某个键的数值,首先会在“哈希表1”中进行搜索,若未发现,则将转至“哈希表2”中继续查找。
Redis大key如何解决?什么是热key?
通常以其接收到的Key被请求频率来判定,例如:
如何解决热key问题?mysql索引优化详细讲讲
常见优化索引的方法:
防止索引失效:
主键索引和非主键索引有什么区别?
主键索引和非主键索引的主要区别在于:
主键索引,这是一种独特的唯一标识,其中不允许存在任何空缺值。在每一个数据表中,仅能设立一个主键。其核心功能在于,为主键索引提供了便捷的途径,以便迅速定位并获取表中的特定数据。
非主键索引,亦称作二级索引或辅助索引,其数量可以不止一个。此类索引支持包含空值,同时也接纳重复的值。
在进行索引覆盖查询操作时,只需在二级索引中进行搜索,便无需进行回表操作。
MySQL的事务的几个特性你知道吗?
要遵守 4 个特性,分别如下:
操作系统进程和线程的区别
进程构成了系统资源分配的基本单元,线程则是处理器调度的最基本实体。每一个进程都拥有独立的地址空间和系统资源,而线程则是在所属进程的资源范围内进行共享。
由于每个进程都拥有独立的地址空间,因此在进行进程间切换时,所需的成本相对较高。相对而言,线程间的切换仅涉及保存和调整少数几个寄存器的数据,其成本则相对较低。
数据在进程内得以共享,因为线程们可以共用诸如内存和文件描述符等资源,这使得线程间的数据共享和沟通变得相对简单。然而,在进程之间,为了实现数据的共享或沟通,就必须借助专门的进程间通信机制。
分配给进程的资源有哪些?
虚拟内存、文件描述符、信号等等
进程切换和线程切换的区别?
进程的转换涉及诸多方面,涵盖整个进程的地址空间、全局变量以及文件描述符等众多元素。正因如此,相较于线程的转换,进程的转换往往伴随着更大的成本。
线程的转换主要影响的是堆栈、寄存器以及程序执行的位置,这一过程并不触及到进程层面的资源分配,所以进行线程切换的成本相对较低。
为什么并发执行线程要加锁?
并发执行的线程之所以需要实施锁定措施,主要是出于对共享数据的保护目的,以避免“竞态条件”的发生。
所谓“竞态条件”,即指在多个线程对同一数据区域进行读写操作时,最终输出的结果会因线程的运行次序不同而有所差异,从而引发数据的不稳定现象。
通过设置锁定机制,我们能够保证在任何给定时间点,仅有一个线程得以访问那些共享资源,进而有效防止“竞态条件”的发生,进而确保数据的完整性与一致性。
线程死锁的条件?
死锁现象的出现,源于两个或更多线程在同时运行过程中,为了获取资源而相互等待,导致彼此无法继续执行。
死锁现象的出现,必须同时满足以下四个条件:一是资源互斥使用,二是进程持有资源并等待其他资源,三是资源不可被强制剥夺,四是存在进程间的环路等待关系。
为了防止出现死锁现象,只需打破任一条件即可,其中最常见的方式是通过实施资源按序分配策略,来打破环路等待这一条件。
网络TCP和UDP的区别TCP的连接指的是什么东西
为确保通信的稳定性和数据传输的监管,涉及一些关键性的状态数据,诸如套接字、序列标识以及数据窗口的尺寸,这些要素的集合构成了所谓的连接体。
imgTCP三次握手过程描述一下?
建立连接是通过三次握手来进行的。三次握手的过程如下图:
TCP 三次握手为什么要三次?
三个方面分析三次握手的原因:
我们设想一个情景,其中客户端首先发送了一个SYN(序列号为90)的报文,随后客户端突然停止运行,并且该SYN报文遭遇了网络拥堵,导致服务端未能接收到它。随后,客户端重新启动,并再次尝试与服务端建立连接,此时它发送了一个新的SYN(序列号为100)报文。请注意,这并非是对之前SYN报文的重复发送,因为重传的SYN报文序列号是相同的。
看看三次握手是如何阻止历史连接的:
三次握手避免历史连接
客户端在持续多次发送携带相同四元组的SYN报文以建立连接时,若遭遇网络拥堵状况:
上述提到的“旧 SYN 报文”被称为历史连接,TCP之所以采用三次握手来建立连接,其根本目的在于避免由“历史连接”导致的连接初始化问题。
若涉及两次握手建立连接,便无法阻挡历史连接的建立,那么,为何TCP协议中的两次握手机制同样不能阻止历史连接的形成呢?
我直接提出结论,这主要是因为在握手过程中,服务端未能为客户端设立任何中间状态以阻止历史连接的建立,因而服务端有可能创建出历史连接,进而导致资源的不必要消耗。
你不妨思考一下,在完成两次握手后,服务端在接收到 SYN 报文时便进入了 ESTABLISHED 状态,这表示此刻可以向对方传输数据。然而,客户端尚未达到 ESTABLISHED 状态。如果这次连接是历史性的,客户端会识别出这一点,并回应一个 RST 报文以终止连接。与此同时,服务端在初次握手时便已进入 ESTABLISHED 状态,因此它能够发送数据。但遗憾的是,它并未意识到这属于历史连接。只有在接收到 RST 报文后,服务端才会切断连接。
两次握手无法阻止历史连接
观察发现,在采用两次握手来建立TCP连接的过程中,服务端在向客户端传输数据之前,并未对之前的连接进行中断,结果服务端不仅保留了一个历史连接,还额外发送了数据,这无疑是对服务端资源的极大浪费。
因此,为了应对这一状况,最理想的办法是在数据传输至服务端之前,即在建立连接的前期阶段,对过往的连接进行拦截,以此避免资源的无谓消耗。而要达成这一目标,必须借助三次握手的过程。
因此,TCP通过三次握手来建立连接的主要目的,是为了避免由“历史连接”导致的初始化连接问题。
JavaGC算法有哪几种?
垃圾收集(GC)算法主要有以下几种:
标记-清除(Mark-Sweep)算法流程如下:先对需回收的对象进行标记,标记工作完成后,再对那些已被标记的对象进行统一回收处理。
复制机制:将内存区域划分为两部分,每次仅对其中一部分进行操作。当该部分内存资源耗尽后,将正在使用的对象迁移至另一部分。随后,对已使用过的内存区域进行集中清理。
标记-整理(Mark-Compact)算法与“标记-清除”算法在标记阶段保持一致,然而在后续操作中,它并非直接对可回收对象进行清理,而是促使所有存活对象向一端移动,随后直接清除掉端边界之外的内存空间。
分代收集技术,即Generational Collection算法,它将内存按照对象的生命周期划分为若干区域。通常情况下,会将Java堆区划分为新生代与老年代两部分。通过这种方式,我们可以针对不同年代的特征,选用最适宜的回收策略。
增量算法,亦称渐进式垃圾回收技术,其运作原理是将垃圾回收任务细分为若干个步骤,逐一进行,而非一次性完成整个垃圾回收过程,这样能有效缩短程序在垃圾回收期间的暂停时长。
项目算法
1、
2、
3、
4、
5、