# 滴滴 Java 面试

大家好,我是小林。

之前有读者留言说,想看滴滴的校招薪资和面经。

img

25 届滴滴开发岗位的校招薪资如下,普通 offer 31.5w年薪,sp offer 37.5w~40.5w 年薪,ssp offer 45w 年薪。

img

滴滴的普通 offer 相比其他大厂确实比较一般了,能进滴滴的同学,手上也会有其他大厂的 offer,这种情况下大概率会把滴滴给拒掉了。

不过,也有同学跟我反馈,他原本滴滴开的是 21k,但是自己后面拿到了其他 offer,后面滴滴给他加到了 25k,offer 档次提升到了 sp offer,所以多积攒 offer,对谈薪是有非常大的好处的。

滴滴是 11 月底开奖,训练营也有同学拿到了滴滴 sp offer,滴滴 hr 当时跟他聊了很久,还是非常有诚意的。

img

那话说回来,滴滴面试难度如何呢?

这次来分享一位同学滴滴Java 后端开发的校招面经,涵盖一二面,主要考察的知识点是网络协议、Redis、MySQL、操作系统、算法这些内容, 编程语言方面的基本没有问,大概率滴滴这个部门用 Go 技术栈的,面试官对 Go 熟悉一些,Java 方面就问的比较少。

img

# 滴滴一面

# 开场三连问

  • 自我介绍
  • 实习经历介绍
  • 项目介绍

# TCP和UDP区别是什么?

  • 连接:TCP 是面向连接的传输层协议,传输数据前先要建立连接;UDP 是不需要连接,即刻传输数据。
  • 服务对象:TCP 是一对一的两点服务,即一条连接只有两个端点。UDP 支持一对一、一对多、多对多的交互通信
  • 可靠性:TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。UDP 是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议
  • 拥塞控制、流量控制:TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
  • 首部开销:TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
  • 传输方式:TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。

# UDP怎么保证可靠性?

UDP 是不可靠传输的,但基于 UDP 的 QUIC 协议 可以实现类似 TCP 的可靠性传输,在http3 就用了 quic 协议。

  • 连接迁移:QUIC支持在网络变化时快速迁移连接,例如从WiFi切换到移动数据网络,以保持连接的可靠性。
  • 重传机制:QUIC使用重传机制来确保丢失的数据包能够被重新发送,从而提高数据传输的可靠性。
  • 前向纠错:QUIC可以使用前向纠错技术,在接收端修复部分丢失的数据,降低重传的需求,提高可靠性和传输效率。
  • 拥塞控制:QUIC内置了拥塞控制机制,可以根据网络状况动态调整数据传输速率,以避免网络拥塞和丢包,提高可靠性。

# HTTP原理是什么?

  • HTTP(超文本传输协议)是应用层协议,用于在 Web 浏览器和 Web 服务器之间传输超文本数据。它基于请求 - 响应模型进行通信,就像客户端(通常是浏览器)和服务器之间的对话。
  • HTTP 是基于 TCP 协议来实现的,发送 HTTP 请求的照顾去,需要先完成 TCP 三次握手。
  • 一个完整的 HTTP 请求从请求行开始。请求行包含了请求方法、请求的 URL(统一资源定位符)和 HTTP 协议版本。服务器收到请求后,会返回一个 HTTP 响应,响应的第一行是状态行。状态行包含了 HTTP 协议版本、状态码和状态消息。
  • HTTP 是一种无状态协议,这意味着每个请求都是独立的,服务器不会记住之前的请求信息。例如,当你在一个电商网站上浏览商品,每次请求一个商品页面时,服务器不会自动知道你之前浏览过哪些商品。为了实现一些需要记住用户状态的功能,如购物车、登录状态等,通常会使用 Cookie、Session 等技术来跟踪用户的状态。
  • HTTP 可以传输多种类型的数据,包括文本、图像、音频、视频等,并且可以通过各种请求方法和响应头来满足不同的应用场景。例如,通过Content - Type响应头可以灵活地告知客户端数据类型,通过Accept请求头可以让客户端表达自己对数据类型的需求。

# TCP协议里的TIME_WAIT状态是什么?

TIME_WAIT 状态的存在是为了确保网络连接的可靠关闭。只有主动发起关闭连接的一方(即主动关闭方)才会有 TIME_WAIT 状态。

TIME_WAIT 状态的需求主要有两个原因:

  • 防止具有相同「四元组」的「旧」数据包被收到:在网络通信中,每个 TCP 连接都由源 IP 地址、源端口号、目标 IP 地址和目标端口号这四个元素唯一标识,称为「四元组」。当一方主动关闭连接后,进入 TIME_WAIT 状态,它仍然可以接收到一段时间内来自对方的延迟数据包。这是因为网络中可能存在被延迟传输的数据包,如果没有 TIME_WAIT 状态的存在,这些延迟数据包可能会被错误地传递给新的连接,导致数据混乱。通过保持 TIME_WAIT 状态,可以防止旧的数据包干扰新的连接。
  • 保证「被动关闭连接」的一方能被正确关闭:当连接的被动关闭方接收到主动关闭方的 FIN 报文(表示关闭连接),它需要发送一个确认 ACK 报文给主动关闭方,以完成连接的关闭。然而,网络是不可靠的,ACK 报文可能会在传输过程中丢失。如果主动关闭方在收到 ACK 报文之前就关闭连接,被动关闭方将无法正常完成连接的关闭。TIME_WAIT 状态的存在确保了被动关闭方能够接收到最后的 ACK 报文,从而帮助其正常关闭连接。

# aio、nio、bio 区别是什么?

  • BIO(blocking IO):就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。优点是代码比较简单、直观;缺点是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。
  • NIO(non-blocking IO) :Java 1.4 引入的 java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层高性能的数据操作方式。
  • AIO(Asynchronous IO) :是 Java 1.7 之后引入的包,是 NIO 的升级版本,提供了异步非堵塞的 IO 操作方式,所以人们叫它 AIO(Asynchronous IO),异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。

# redis数据结构你知道哪些?你用到了哪种?

Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)

img

img

随着 Redis 版本的更新,后面又支持了四种数据类型:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。Redis 五种数据类型的应用场景:

  • String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
  • List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
  • Hash 类型:缓存对象、购物车等。
  • Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
  • Zset 类型:排序场景,比如排行榜、电话和姓名排序等。

Redis 后续版本又支持四种数据类型,它们的应用场景如下:

  • BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
  • HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
  • GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
  • Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。

# redis主节点挂了怎么办?

img

可以增加哨兵机制,哨兵当发现主节点挂了,会进行主从自动切换,它会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。

Redis Sentinel(哨兵)会定期向主节点和从节点发送 ping 命令来检查它们的状态。例如,每 1 秒(可配置)发送一次 ping,如果主节点在规定时间(如 down - after - milliseconds 配置项指定的时间,默认 30 秒)内没有响应,Sentinel 就会认为主节点挂了。

img

当 Sentinel 判定主节点挂了后,会在从节点中选举一个新的主节点。选举的依据主要是从节点的优先级(可以在 Redis 配置文件中设置,默认是 100,数字越大优先级越高)、数据复制的偏移量(与旧主节点数据同步程度,偏移量越大说明数据越新)和运行 ID(用于区分不同的节点)。

新主节点选举出来后,其他从节点会向新主节点进行数据同步。Redis 从节点通过复制偏移量(replica - offset)来确定从哪里开始复制数据。新主节点会将自己的数据发送给从节点,从节点接收并更新自己的数据,以达到数据的一致性。

# 线程、进程、协程区别是什么?

  • 首先,我们来谈谈进程。进程是操作系统中进行资源分配和调度的基本单位,它拥有自己的独立内存空间和系统资源。每个进程都有独立的堆和栈,不与其他进程共享。进程间通信需要通过特定的机制,如管道、消息队列、信号量等。由于进程拥有独立的内存空间,因此其稳定性和安全性相对较高,但同时上下文切换的开销也较大,因为需要保存和恢复整个进程的状态。
  • 接下来是线程。线程是进程内的一个执行单元,也是CPU调度和分派的基本单位。与进程不同,线程共享进程的内存空间,包括堆和全局变量。线程之间通信更加高效,因为它们可以直接读写共享内存。线程的上下文切换开销较小,因为只需要保存和恢复线程的上下文,而不是整个进程的状态。然而,由于多个线程共享内存空间,因此存在数据竞争和线程安全的问题,需要通过同步和互斥机制来解决。
  • 最后是协程。协程是一种用户态的轻量级线程,其调度完全由用户程序控制,而不需要内核的参与。协程拥有自己的寄存器上下文和栈,但与其他协程共享堆内存。协程的切换开销非常小,因为只需要保存和恢复协程的上下文,而无需进行内核级的上下文切换。这使得协程在处理大量并发任务时具有非常高的效率。然而,协程需要程序员显式地进行调度和管理,相对于线程和进程来说,其编程模型更为复杂。

# 手撕代码

  • 手撕代码,题目忘了,力扣原题

# 滴滴二面

# 开场二连问

  • 自我介绍
  • 实习中哪里体现了团队协作?

# redis分布式锁怎么实现?

分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。如下图所示:imgRedis 本身可以被多个客户端共享访问,正好就是一个共享存储系统,可以用来保存分布式锁,而且 Redis 的读写性能高,可以应对高并发的锁操作场景。Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:

  • 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
  • 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。

  • 加锁包括了读取锁变量、检查锁变量值和设置锁变量值三个操作,但需要以原子操作的方式完成,所以,我们使用 SET 命令带上 NX 选项来实现加锁;
  • 锁变量需要设置过期时间,以免客户端拿到锁后发生异常,导致锁一直无法释放,所以,我们在 SET 命令执行时加上 EX/PX 选项,设置其过期时间;
  • 锁变量的值需要能区分来自不同客户端的加锁操作,以免在释放锁时,出现误释放操作,所以,我们使用 SET 命令设置锁变量值时,每个客户端设置的值是一个唯一值,用于标识客户端;

满足这三个条件的分布式命令如下:

SET lock_key unique_value NX PX 10000
  • lock_key 就是 key 键;
  • unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
  • NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
  • PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。

而解锁的过程就是将 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 单节点上完成了分布式锁的加锁和解锁。

# redis的hashset底层数据结构是什么?

Hash 类型的底层数据结构是由压缩列表或哈希表实现的:

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

# redis持久化原理是什么?

Redis 的读写操作都是在内存中,所以 Redis 性能才会高,但是当 Redis 重启后,内存中的数据就会丢失,那为了保证内存中的数据不会丢失,Redis 实现了数据持久化的机制,这个机制会把数据存储到磁盘,这样在 Redis 重启就能够从磁盘中恢复原有的数据。Redis 共有三种数据持久化的方式:

  • AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
  • RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;

AOF 日志是如何实现的?

Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。

img我这里以「set name xiaolin」命令作为例子,Redis 执行了这条命令后,记录在 AOF 日志里的内容如下图:

imgRedis 提供了 3 种写回硬盘的策略, 在 Redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填:

  • Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
  • Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
  • No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

我也把这 3 个写回策略的优缺点总结成了一张表格:

img

RDB 快照是如何实现的呢?

因为 AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时,需要全量把日志都执行一遍,一旦 AOF 日志非常多,势必会造成 Redis 的恢复操作缓慢。为了解决这个问题,Redis 增加了 RDB 快照。

所谓的快照,就是记录某一个瞬间东西,比如当我们给风景拍照时,那一个瞬间的画面和信息就记录到了一张照片。所以,RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行:

  • 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
  • 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞

AOF和RDB优缺点

AOF:

  • 优点:首先,AOF提供了更好的数据安全性,因为它默认每接收到一个写命令就会追加到文件末尾。即使Redis服务器宕机,也只会丢失最后一次写入前的数据。其次,AOF支持多种同步策略(如everysec、always等),可以根据需要调整数据安全性和性能之间的平衡。同时,AOF文件在Redis启动时可以通过重写机制优化,减少文件体积,加快恢复速度。并且,即使文件发生损坏,AOF还提供了redis-check-aof工具来修复损坏的文件。
  • 缺点:因为记录了每一个写操作,所以AOF文件通常比RDB文件更大,消耗更多的磁盘空间。并且,频繁的磁盘IO操作(尤其是同步策略设置为always时)可能会对Redis的写入性能造成一定影响。而且,当问个文件体积过大时,AOF会进行重写操作,AOF如果没有开启AOF重写或者重写频率较低,恢复过程可能较慢,因为它需要重放所有的操作命令。

RDB:

  • 优点: RDB通过快照的形式保存某一时刻的数据状态,文件体积小,备份和恢复的速度非常快。并且,RDB是在主线程之外通过fork子进程来进行的,不会阻塞服务器处理命令请求,对Redis服务的性能影响较小。最后,由于是定期快照,RDB文件通常比AOF文件小得多。
  • 缺点: RDB方式在两次快照之间,如果Redis服务器发生故障,这段时间的数据将会丢失。并且,如果在RDB创建快照到恢复期间有写操作,恢复后的数据可能与故障前的数据不完全一致

# mysql索引底层原理是什么?

MySQL InnoDB 引擎是用了B+树作为了索引的数据结构。

B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。

主键索引的 B+Tree 如图所示:

img

比如,我们执行了下面这条查询语句:

select * from product where id= 5;

这条语句使用了主键索引查询 id 号为 5 的商品。查询过程是这样的,B+Tree 会自顶向下逐层进行查找:

  • 将 5 与根节点的索引数据 (1,10,20) 比较,5 在 1 和 10 之间,所以根据 B+Tree的搜索逻辑,找到第二层的索引数据 (1,4,7);
  • 在第二层的索引数据 (1,4,7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4,5,6);
  • 在叶子节点的索引数据(4,5,6)中进行查找,然后我们找到了索引值为 5 的行数据。

数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。

B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O,所以B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。

# 怎么解决脏读和幻读?

脏读和幻读的区别 :

  • 脏读:脏读是指一个事务读取了另一个未提交事务的数据。例如,事务 A 正在修改数据但尚未提交,事务 B 却读取了事务 A 修改的数据。如果事务 A 后来回滚了修改,那么事务 B 读取的数据就是无效的、“脏” 的数据。
  • 幻读:幻读是指在一个事务内,多次查询同一数据集,但每次查询的结果集可能因为其他事务的插入或删除操作而不同。比如,事务 A 先查询了一个表中有 5 条记录,在事务 A 还没结束时,事务 B 插入了新的记录,当事务 A 再次查询时,发现有了新的记录,就好像出现了 “幻觉” 一样。

解决脏读和幻读的方法 - 事务隔离级别:

  • 未提交读(Read Uncommitted) - 会出现脏读和幻读:这是最低的隔离级别。在这个级别下,一个事务可以读取另一个未提交事务的数据,会出现脏读、幻读以及不可重复读(一个事务内多次读取同一数据,结果不同,因为其他事务修改了该数据)等问题。一般不建议在实际应用中使用这个隔离级别来处理关键数据。
  • 已提交读(Read Committed) - 解决脏读:这个隔离级别保证一个事务只能读取另一个已经提交事务的数据,从而解决了脏读问题。当事务 A 读取数据时,它只能看到其他事务已经提交后的结果。例如,在一个银行转账系统中,事务 A 查询账户余额,它只会读取已经完成(提交)的转账交易后的余额,不会读取正在进行(未提交)的转账操作影响后的余额。但在这个级别下,幻读仍然可能出现。因为其他事务的插入或删除操作在提交后,本事务再次查询时就会看到变化后的结果。
  • **可重复读(Repeatable Read) - 解决脏读和幻读(MySQL 默认隔离级别):**在可重复读隔离级别下,一个事务在执行过程中多次读取同一数据,会得到相同的结果,不会受到其他事务对该数据修改的影响,从而解决了不可重复读的问题。同时,MySQL 的 InnoDB 引擎通过多版本并发控制(MVCC)机制来解决幻读问题。
  • 串行化(Serializable) - 最高隔离级别,完全避免脏读、幻读和不可重复读:在串行化隔离级别下,事务是串行执行的,一个事务必须等待前一个事务完成后才能开始。这样就完全避免了脏读、幻读和不可重复读的问题。例如,在一个售票系统中,如果采用串行化隔离级别,那么当一个事务在处理一张票的售卖操作时,其他事务必须等待这个事务完成后才能进行相关操作,这样可以保证数据的绝对一致性。不过,这种隔离级别的性能开销较大,因为它限制了并发性能,导致系统的吞吐量较低,只有在对数据一致性要求极高的场景下才会使用。

# 介绍一下cap理论

CAP 原则又称 CAP 定理, 指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性), 三者不可得兼

img

  • 一致性(C) : 在分布式系统中的所有数据备份, 在同一时刻是否同样的值(等同于所有节点访问同一份最新的数据副本)
  • 可用性(A): 在集群中一部分节点故障后, 集群整体是否还能响应客户端的读写请求(对数据更新具备高可用性)
  • 分区容忍性(P): 以实际效果而言, 分区相当于对通信的时限要求. 系统如果不能在时限内达成数据一致性, 就意味着发生了分区的情况, 必须就当前操作在 C 和 A 之间做出选择

# 手撕算法

  • 题目:最长回文字符串

对了,最新的互联网大厂后端面经都会在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。

img