
小伙伴们大家好,我是阿秀。
今天分享一位同学面试滴滴的二面面经,总的来说问的还是比较基础,并且我发现滴滴很喜欢问数据库相关的内容,尤其是 Redis 相关,滴滴问的很多,基本都是实际生产过程中可能会遇到的一些真实场景问题,从面经也能看出来,滴滴二面这位面试官是真想招人,而不是来刷KPI的。。。
其实这不奇怪,因为很多人在校期间所学的计算机基础科目里在实际工作中用到最多的就是数据库了,尤其是 MySQL 和Redis,至于操作系统、计算机网络、数据机构与算法在工作中很少有场景用到,98% 的时候都用不到,实际工作中根本没什么机会让你去写什么接雨水算法题的。
估计很多人又要问了,既然用不到,那为什么又要考?尤其是算法,考的这么难是为什么?原因也很简单,提高门槛,进一步筛选求职者罢了。
这次滴滴二面中主要是考察Redis、内存泄露、操作系统、计算机网络以及两道手撕算法题。
1、问项目
聊了四五分钟左右的项目
2、Redis持久化机制及特点
主要有以下有两种持久化机制:
快照(snapshotting)持久化(RDB持久化)
Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行 备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性 能),还可以将快照留在原地以便重启服务器的时候使用。
快照持久化是Redis默认采用的持久化方式,在Redis.conf配置文件中默认有此下配置:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令
创建快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
AOF(append-only file)持久化
与快照持久化相比,AOF持久化的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启 AOF(append only file)方式的持久化,可以通过appendonly参数开启:appendonly yes
开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的 保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。
在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能 几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
这里也补充一下Redis 4.0 对于持久化机制的优化
Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
3、Redis有哪些数据类型?
字符串、链表、字典、哈希表、跳表以及压缩列表等
4.、Redis各数据类型的特点和用途?
简单动态字符串(Simple Dynamic String,SDS)
Redis没有直接使用C语言传统的字符串,而是自己构建了一种名为简单动态字符串(Simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
其实SDS等同于C语言中的char * ,但它可以存储任意二进制数据,不能像C语言字符串那样以字符’\0’来标识字符串的结 束,因此它必然有个长度字段。
定义
struct sdshdr {
// 记录buf数组中已使用字节的数量
// 等于sds所保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
}
优点
获取字符串长度的复杂度为O(1)。 杜绝缓冲区溢出。 减少修改字符串长度时所需要的内存重分配次数。 二进制安全。 兼容部分C字符串函数。
它具有很常规的 set/get 操作,value 可以是String也可以是数字,一般做一些复杂的计数功能的缓存。
链表
当有一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的额字符串时,Redis就会使用链表作为列表建的底层实现。
节点底层结构
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
list底层结构
typedef struct list {
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 链表所包含的节点数量
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值是放过函数
void (*free)(void *ptr);
// 节点值对比函数
int(*match)(void *ptr, void *key);
} list;
特性
链表被广泛用于实现Redis的各种功能,比如列表建、发布与订阅、慢查询、监视器等。 每个链表节点由一个listNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以Redis的链表实现是双端链表。 每个链表使用一个list结构表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息。 因为链表表头的前置节点和表尾节点的后置节点都指向NULL,所以Redis的链表实现是无环链表。 通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值。
字典
字典的底层是哈希表,类似 C++中的 map ,也就是键值对。
哈希表
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于size-1
unsigned long sizemark;
// 该哈希表已有节点的数量
unsigned long used;
} dichht;
特性
字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键。 Redis中的字典使用哈希表作为底层结构实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。 Redis使用MurmurHash2算法来计算键的哈希值。 哈希表使用链地址法来解决键冲突。
跳跃表
先看这样一张图:
如上图,我们要查找一个元素,就需要从头节点开始遍历,直到找到对应的节点或者是第一个大于要查找的元素的节点(没找到)。时间复杂度为O(N)。
这个查找效率是比较低的,但如果我们把列表的某些节点拔高一层,如下图,例如把每两个节点中有一个节点变成两层。那么第二层的节点只有第一层的一半,查找效率也就会提高。
查找的步骤是从头节点的顶层开始,查到第一个大于指定元素的节点时,退回上一节点,在下一层继续查找。
特性
跳跃表是有序集合的底层实现之一 Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点 每个跳跃表节点的层高都是1至32之间的随机数 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的。 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序。 跳表是一种实现起来很简单,单层多指针的链表,它查找效率很高,堪比优化过的二叉平衡树,且比平衡树的实现。
压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
特性
看他的名字就能看出来,是为了节省内存造的列表结构。
5、有没有碰到过内存溢出和内存泄露,他们的概念是什么
碰到过,一般我们常说的内存泄漏是指堆内存的泄漏。
堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。
应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了
6、写一个内存溢出的案例?
下面是一个使用Golang语言编写的内存溢出案例:
package main
import (
"fmt"
"time"
)
func main() {
go allocateMemory()
time.Sleep(5 * time.Second)
}
func allocateMemory() {
var memorySlice []int
for {
memorySlice = append(memorySlice, 1)
fmt.Println("Allocated memory:", len(memorySlice)*4, "bytes")
time.Sleep(100 * time.Millisecond)
}
}
运行这个程序,到切片的长度不断增加,最终会导致内存不断增长,直到达到操作系统的内存限制,从而触发内存溢出错误。
7、编码中我们怎么防止内存泄露?
内存泄露的几种方式
计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露 一定要将基类的析构函数声明为虚函数 对象数组的释放一定要用delete [] 有new就有delete,有malloc就有free,保证它们一定成对出现
8、StringBuilder和stringBuffer的区别
StringBuilder和StringBuffer是Java中用于操作字符串的类,它们的主要区别在于线程安全性和性能。
StringBuffer是线程安全的,它的方法都使用了synchronized关键字来确保多线程环境下的安全性。这意味着在多线程环境下,多个线程可以同时访问和修改一个StringBuffer对象,而不会引发数据竞争或不一致的问题。然而,由于使用了同步机制,StringBuffer的性能相对较低,特别是在高并发的情况下。
StringBuilder是非线程安全的,它的方法没有使用synchronized关键字,因此在多线程环境下不能保证安全性。如果多个线程同时访问和修改同一个StringBuilder对象,可能会导致数据竞争和不一致的问题。然而,由于没有同步开销,StringBuilder相对于StringBuffer具有更好的性能,特别是在单线程环境下或者不需要考虑线程安全的情况下。
因此,如果你的代码在多线程环境下运行,或者需要保证线程安全性,那么应该使用StringBuffer。如果你的代码在单线程环境下运行,或者不需要考虑线程安全问题,并且追求更好的性能,那么应该使用StringBuilder。
9、怎么理解线程安全的?我们要怎么处理线程安全问题?
线程安全是指在多线程环境下,多个线程同时访问和修改同一份资源时,不会发生数据竞争、不一致或者其他不可预期的问题。
处理线程安全问题需要采取一些措施来保证多个线程能够正确地访问和修改共享资源,常见的处理方法包括:
使用同步机制:在关键代码段中使用synchronized关键字或者Lock对象等同步机制来确保同一时间只有一个线程能够访问共享资源。
使用原子操作:使用原子操作如AtomicInteger、AtomicLong等可以保证某个操作的执行是不可分割的,从而避免了数据竞争问题。
使用线程安全的数据结构:一些线程安全的数据结构,如ConcurrentHashMap、CopyOnWriteArrayList可以在多线程环境下安全地访问和修改数据。
除此外还用避免共享资源、使用不可变对象等方向,编码时要充分考虑线程安全性,并且进行适当的测试和调试,以确保代码在多线程环境下能够正确地运行。
10、进程和线程的区别
1、进程是资源分配的基本单位,运行一个可执行程序会创建一个或多个进程,进程就是运行起来的可执行程序
2、线程是资源调度的基本单位,也是程序执行的基本单位,是轻量级的进程。每个进程中都有唯一的主线程,且只能有一个,主线程和进程是相互依存的关系,主线程结束进程也会结束。多提一句:协程是用户态的轻量级线程,线程内部调度的基本单位
| 进程 | 线程 | |
|---|---|---|
| 定义 | 资源分配和拥有的基本单位 | 程序执行的基本单位 |
| 切换情况 | 进程CPU环境(栈、寄存器、页表和文件句柄等)的保存以及新调度的进程CPU环境的设置 | 保存和设置程序计数器、少量寄存器和栈的内容 |
| 切换者 | 操作系统 | 操作系统 |
| 切换过程 | 用户态->内核态->用户态 | 用户态->内核态->用户态 |
| 调用栈 | 内核栈 | 内核栈 |
| 拥有资源 | CPU资源、内存资源、文件资源和句柄等 | 程序计数器、寄存器、栈和状态字 |
| 并发性 | 不同进程之间切换实现并发,各自占有CPU实现并行 | 一个进程内部的多个线程并发执行 |
| 系统开销 | 切换虚拟地址空间,切换内核栈和硬件上下文,CPU高速缓存失效、页表切换,开销很大 | 切换时只需保存和设置少量寄存器内容,因此开销很小 |
| 通信方面 | 进程间通信需要借助操作系统 | 线程间可以直接读写进程数据段(如全局变量)来进行通信 |
11、死锁以及怎么预防死锁?
死锁是指两个(多个)线程相互等待对方数据的过程,死锁的产生会导致程序卡死,不解锁程序将永远无法进行下去。
死锁预防:在程序运行之前预防发生死锁。
破坏互斥条件,例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
破坏请求和保持条件,一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。
破坏不剥夺条件,允许抢占资源
破坏循环请求等待,给资源统一编号,进程只能按编号顺序来请求资源。
12、TCP和UDP的区别
1.TCP是面向连接的协议,建立和释放连接需要进行三次握手和四次挥手。UDP是面向无连接的协议,无需进行三次握手和四次挥手。说明udp比TCP实时性更强。
2.TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
3.TCP连接的可靠性强,UDP的可靠性不强。
4.TCP只能一对一,UDP支持一对多和多对多。
5.TCP的头部开销比UDP大。TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
13、手撕算法
搜索旋转排序数组 和 数组中重复的数据 两道题
往期精选面经推荐

你好,我是阿秀,普通学校毕业,校招时拿到字节跳动SP、百度、华为、农业银行等6个互联网中大厂offer,毕业后先于抖音部门担任全栈开发工程师,目前在上海某外企带领团队继续从事全栈开发,负责一个对印项目。
研三快毕业那年就组建了一个阿秀的学习圈,一直持续分享校招/社招跳槽找工作的经验,都是自己一路走过来的经验,目前已经累计服务超过 3300 +人,欢迎点此了解一二。