字节后端面经:黑马点评项目引发的Redis、MySQL等问题大拷问

本文系经授权转载自小林coding(ID:CodingLin)平台。

字节后端面经:黑马点评项目引发的Redis、MySQL等问题大拷问

作者丨小林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、

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

24阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

27阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

23阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读