首页 > 数据库 > NoSQL > 被字节拷打了!基础还是太重要了...
来源:小林coding
大家好,我是小林。
今天分享一篇一位同学去字节面试的实习面经,技术栈是java,投了go后端岗位,主要拷打了 redis+mysql+网络+系统+java+算法,面试问题主要集中在 mysql、redis、网络这三部门,因为面试官是搞 go 的,java 只是随便问了几个题目。
不同厂的面试风格都不同,如果 java 同学去面阿里、美团、京东这类的 java 大厂,面试的问题大概率是集中在 java 相关的问题,比如 java 并发、java 集合、jvm 这三块,所以大家可以根据要面试的公司,可以重点去准备这家公司倾向问的问题的方向。
Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。
Redis 提供了多种数据类型来支持不同的业务场景,比如 String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流),并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题。
除此之外,Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等。
官方使用基准测试的结果是,单线程的 Redis 吞吐量可以达到 10W/每秒,如下图所示:

之所以 Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:
Redis 的读写操作都是在内存中,所以 Redis 性能才会高,但是当 Redis 重启后,内存中的数据就会丢失,那为了保证内存中的数据不会丢失,Redis 实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样在 Redis 重启就能够从磁盘中恢复原有的数据。
Redis 共有两种数据持久化的方式:
虽然 Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,但是在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理。
Redis 官方表示,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上。
可以在系统部署多个 redis docker 容器来处理,达到充分利用 cpu 多核心的效果
当大量缓存数据在同一时间过期或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力增加,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
解决方法
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮。
解决方案:
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
解决方案
基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。
满足这三个条件的分布式命令如下:
SET lock_key unique_value NX PX 10000
而解锁的过程就是将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。
**原子性(atomicity)**:一个事务必须视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。
**一致性(consistency)**:数据库总是从一个一致性的状态转换到另一个一致性的状态。
**隔离性(isolation)**:一个事务所做的修改在最终提交以前,对其他事务是不可见的。
**持久性(durability)**:一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。
实现:
主要有三中,记录锁、间隙锁、临建锁。
可以按照四个角度来分类索引。
B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,每个节点里的数据是按主键顺序存放的。在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都指向下一个叶子节点,形成一个链表。B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,千万级的表查询目标数据最多需要 3-4 次磁盘 I/O。
B+树和B树相比:
优势:
虽然哈希可以在O1 时间复杂度查询到数据,但是哈希表的元素都是无须存放的,没办法进行范围查询。
通过将多个字段组合成一个索引,该索引就被称为联合索引。
比如,将商品表中的 product_no 和 name 字段组合成联合索引(product_no, name),创建联合索引的方式如下:
CREATE INDEX index_product_no_name ON product(product_no, name);
联合索引(product_no, name) 的 B+Tree 示意图如下(图中叶子节点之间我画了单向链表,但是实际上是双向链表,原图我找不到了,修改不了,偷个懒我不重画了,大家脑补成双向链表就行)。

可以看到,联合索引的非叶子节点用两个字段的值作为 B+Tree 的 key 值。当在联合索引查询数据时,先按 product_no 字段比较,在 product_no 相同的情况下再按 name 字段比较。
也就是说,联合索引查询的 B+Tree 是先按 product_no 进行排序,然后再 product_no 相同的情况再按 name 字段排序。
因此,使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。
当查询的数据是能在二级索引的 B+Tree 的叶子节点里查询到,这时就不用再查主键索引查,比如下面这条查询语句:
select id from product where product_no = '0002';
这种在二级索引的 B+Tree 就能查询到结果的过程就叫作「覆盖索引」,也就是只需要查一个 B+Tree 就能找到数据。
分为应用层、表示层、会话层、运输层、网络层、链路层、物理层。
属于传输层
区别:
连接性:TCP是面向连接的协议,通过三次握手建立连接,然后进行数据传输,传输完成后通过四次挥手关闭连接。而UDP是无连接的协议,发送数据之前不需要建立连接,也没有连接的关闭过程。
可靠性:TCP提供可靠的传输,通过序号、确认和重传机制来确保数据的可靠性。它使用滑动窗口和累计确认来保证数据的按序到达,并通过超时重传机制来处理丢失的数据包。而UDP不提供可靠性保证,它只是简单地将数据包发送出去,不保证数据的可靠性和按序到达。
传输方式:TCP提供面向字节流的传输,将数据划分为多个TCP报文段进行传输,保证数据的完整性和顺序性。而UDP提供面向报文的传输,每个UDP数据包都是独立的,不保证数据的完整性和顺序性。
TCP如何保证可靠传输:
序号和确认:TCP使用序号和确认机制来保证数据的按序到达。发送方给每个数据包分配一个序号,接收方通过确认序号告知发送方已成功接收到数据包。
滑动窗口:TCP使用滑动窗口机制来控制发送方发送数据的速率和接收方接收数据的速率。滑动窗口大小可以动态调整,以适应网络状况。
超时重传:TCP使用超时重传机制来处理丢失的数据包。发送方在发送数据后启动一个定时器,如果在超时时间内未收到确认,就认为数据包丢失,进行重传。
流量控制:TCP使用流量控制机制来控制发送方发送数据的速率,以避免接收方被淹没。接收方通过窗口大小告知发送方自己的接收能力,发送方根据接收方的窗口大小来控制发送速率。
拥塞控制:TCP使用拥塞控制机制来避免网络拥塞。通过动态调整发送方的发送速率,根据网络的拥塞程度进行拥塞窗口的调整,以保持网络的稳定性和公平性。
主要有 arp 协议,ARP 是借助 ARP 请求与 ARP 响应两种类型的包确定 MAC 地址的。

正向代理的主动方是用户,主要用来解决跨域问题,还有隐藏用户访问记录的作用。

正向代理:
反向代理的主动方是服务器,主要是提供负载均衡、安全防护等作用。

反向代理:
用户空间分布的情况,以 32 位系统为例,我画了一张图来表示它们的关系:

通过这张图你可以看到,用户空间内存,从低到高分别是 6 种不同的内存段:
8 MB。当然系统也提供了参数,以便我们自定义大小;上图中的内存布局可以看到,代码段下面还有一段内存空间的(灰色部分),这一块区域是「保留区」,之所以要有保留区这是因为在大多数的系统里,我们认为比较小数值的地址不是一个合法地址,例如,我们通常在 C 的代码里会将无效的指针赋值为 NULL。因此,这里会出现一段不可访问的内存保留区,防止程序因为出现 bug,导致读或写了一些小内存地址的数据,而使得程序跑飞。
在这 7 个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。
主要存放函数的局部变量,函数返回后,局部变量会自动销毁。
进程是由内核管理和调度的,所以进程的切换只能发生在内核态。
所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB 取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:

对于线程上下文切换的话,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。所以,线程的上下文切换相比进程,开销要小很多。
语法:Java是一种面向对象的编程语言,使用类和对象来组织代码。它使用强类型的静态类型检查,并且有丰富的面向对象特性。而Go是一种并发编程语言,它采用了简洁的语法和结构,更注重代码的可读性和简洁性。
并发性:Go在语言级别提供了并发编程的支持,通过goroutine和channel来实现轻量级的并发和通信。而Java需要使用线程和锁等机制来实现并发编程,相对来说更加复杂。
性能:Go在性能方面表现出色,它具有高效的垃圾回收机制和协程调度器,适用于高并发和高性能的应用场景。Java也具有良好的性能,但相对于Go来说,在某些场景下可能会有一些性能损失。
生态系统:Java拥有庞大的生态系统和丰富的第三方库和框架支持,广泛应用于企业级应用开发。而Go的生态系统相对较小,但在一些领域(如网络编程和云原生应用)有着独特的优势。
开发体验:Go注重简洁性和可读性,语法简单明了,对于开发者来说比较友好。Java的语法相对较复杂,需要更多的代码量和工具支持。
volatile关键字是Java中用来修饰变量的关键字,它的作用是保证变量的可见性和禁止指令重排序。
可见性是指当一个线程修改了共享变量的值后,其他线程能够立即看到最新的值。在多线程环境下,由于线程的工作内存和主内存之间存在缓存不一致的情况,普通的变量在一个线程中的修改可能对其他线程是不可见的。
使用volatile关键字修饰的变量,当一个线程修改了该变量的值后,会立即将最新的值刷新到主内存中,并且当其他线程读取该变量时,会从主内存中重新获取最新的值,而不是使用线程自己的工作内存中的旧值。
volatile关键字通过使用内存屏障的机制来实现可见性。内存屏障会强制刷新缓存并保证读写操作的顺序性,从而保证变量的可见性。
具体来说,当一个线程对volatile变量进行写操作时,会在写操作之后插入写屏障,将最新的值刷新到主内存中。当其他线程对该变量进行读操作时,会在读操作之前插入读屏障,从主内存中获取最新的值。
垃圾回收算法有四种,分别是标记清除法、标记整理法、复制算法、分代收集算法。
标记清除算法
首先利用可达性去遍历内存,把存活对象和垃圾对象进行标记。标记结束后统一将所有标记的对象回收掉。这种垃圾回收算法效率较低,并且会产生大量不连续的空间碎片。
复制清除算法
半区复制,用于新生代垃圾回收。将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。
特点:实现简单,运行高效,但可用内存缩小为了原来的一半,浪费空间。
标记整理算法
根据老年代的特点提出的一种标记算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
分类收集算法
根据各个年代的特点采用最适当的收集算法。
一般将堆分为新生代和老年代。
在新生代中,每次垃圾收集时都有大批对象死去,只有少量存活,使用复制算法比较合适,只需要付出少量存活对象的复制成本就可以完成收集。老年代对象存活率高,适合使用标记-清理或者标记-整理算法进行垃圾回收。
主要拷打数据库这方面了,虽然问题不算难,但是疏忽复习,有些问题当时没有想出来,后面还得加强巩固一下数据库的内容,算法做了两题,只做出来一道,另外一题leetcode也做过,但是太紧张没想出来,算法还是得多练练。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70027826/viewspace-2964737/ ,如需转载,请注明出处,否则将追究法律责任。