秋招提前批开始了!

小林coding

共 10323字,需浏览 21分钟

 ·

2024-06-25 15:24

图解学习网站:https://xiaolincoding.com

大家好,我是小林。

最近看到不少公司都开始秋招提前批了,很多同学都傻眼了,我都还在找实习,咋提前批就开始了?

目前已经开展秋招提前批的公司,有深信服、tplink 等公司,互联网大厂还没开始,预计7 月份中旬左右,各大互联网公司就会陆陆续续开展秋招提前批的公告了。

同学们多关注这些公司的招聘公众号,一个小技巧,通常它们的公众号都是公司名+招聘两个字,比如「腾讯招聘」、「字节跳动招聘」根据这样去搜索,就能找到官方的招聘公众号了。

那问题来了,秋招提前批适合什么人去面呢?

企业开展秋招提前批的目的是为了提前招到优秀的人才,针对优秀的同学会发 sp offer,注意是优秀的人才,所以你必须是有过人的经历,比如是名校毕业、有互联网中大厂实习、有 acm 奖项,这类同学就可以尝试面面看,你获得面试的机会才会比较大。

本身别人就是以高标准的方式去招聘优秀的同学,只适合少数优秀的同学,不适合大多数普通同学,普通同学在这段时间多积累项目,实习可以继续找,准备 9 月份的正式秋招就可以了,这时候凑热闹去投提前批,可能容易被打击你的自信心,影响了复习的心态。

当然,并不说提前批拿到的 offer 一定是 sp offer,也见过提前批面试通过的同学,拿到的是白菜档,也不是说正式秋招就没有 sp offer 了,也见过秋招末尾 12 月份的时候,不少同学拿到 sp、ssp 档的 offer。

那最近看到不少同学已经去提前批面试了,今天就来分享深信服的提前批 Java 后端开发面经,给大家感受一下提前批面试的氛围。

考察的知识点,主要围绕Redis+MySQL+项目+数据结构+算法。Java 语言问的不多,因为深信服大多数面试官的技术栈是 C++

Redis

为什么使用redis?

主要是因为 Redis 具备「高性能」和「高并发」两种特性

1、Redis 具备高性能

假如用户第一次访问 MySQL 中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据缓存在 Redis 中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了,操作 Redis 缓存就是直接操作内存,所以速度相当快。

如果 MySQL 中的对应数据改变的之后,同步改变 Redis 缓存中相应的数据即可,不过这里会有 Redis 和 MySQL 双写一致性的问题。

2、 Redis 具备高并发

单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。

所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。

为什么redis比mysql要快?

  • 内存存储:Redis 是基于内存存储的 NoSQL 数据库,而 MySQL 是基于磁盘存储的关系型数据库。由于内存存储速度快,Redis 能够更快地读取和写入数据,而无需像 MySQL 那样频繁进行磁盘 I/O 操作。
  • 简单数据结构:Redis 是基于键值对存储数据的,支持简单的数据结构(字符串、哈希、列表、集合、有序集合)。相比之下,MySQL 需要定义表结构、索引等复杂的关系型数据结构,因此在某些场景下 Redis 的数据操作更为简单高效,比如 Redis 用哈希表查询, 只需要O1 时间复杂度,而MySQL引擎的底层实现是B+Tree,时间复杂度是O(logn)
  • 线程模型:Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。

redis挂了怎么办?

可以通过构建 Redis 集群,提高Redis 高可用,比如 Redis 的主从复制、哨兵模式、切片集群。

主从复制

主从复制是 Redis 高可用服务的最基础的保证,实现方案就是将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,且主从服务器之间采用的是「读写分离」的方式。

主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。

也就是说,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的。

注意,主从服务器之间的命令复制是异步进行的。

具体来说,在主从服务器命令传播阶段,主服务器收到新的写命令后,会发送给从服务器。

但是,主服务器并不会等到从服务器实际执行完命令后,再把结果返回给客户端,而是主服务器自己在本地执行完命令后,就会向客户端返回结果了。如果从服务器还没有执行主服务器同步过来的命令,主从服务器间的数据就不一致了。

所以,无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。

哨兵模式

在使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复。

为了解决这个问题,Redis 增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。

切片集群模式

当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。

Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系。在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中,具体执行过程分为两大步:

  • 根据键值对的 key,按照 CRC16 算法计算一个 16 bit 的值。
  • 再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。

接下来的问题就是,这些哈希槽怎么被映射到具体的 Redis 节点上的呢?有两种方案:

  • 平均分配: 在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群节点上。比如集群中有 9 个节点,则每个节点上槽的个数为 16384/9 个。
  • 手动分配: 可以使用 cluster meet 命令手动建立节点间的连接,组成集群,再使用 cluster addslots 命令,指定每个节点上的哈希槽个数。

为了方便你的理解,我通过一张图来解释数据、哈希槽,以及节点三者的映射分布关系。

上图中的切片集群一共有 2 个节点,假设有 4 个哈希槽(Slot 0~Slot 3)时,我们就可以通过命令手动分配哈希槽,比如节点 1 保存哈希槽 0 和 1,节点 2 保存哈希槽 2 和 3。

redis-cli -h 192.168.1.10 –p 6379 cluster addslots 0,1
redis-cli -h 192.168.1.11 –p 6379 cluster addslots 2,3

然后在集群运行的过程中,key1 和 key2 计算完 CRC16 值后,对哈希槽总个数 4 进行取模,再根据各自的模数结果,就可以被映射到哈希槽 1(对应节点1) 和 哈希槽 2(对应节点2)。

需要注意的是,在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。

有没有测过加了redis和没加redis的qps ?

单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。

如何实现redis 原子性?

redis 执行一条命令的时候是具备原子性的,因为 redis 执行命令的时候是单线程来处理的,不存在多线程安全的问题。

如果要保证 2 条命令的原子性的话,可以考虑用 lua 脚本,将多个操作写到一个 Lua 脚本中,Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了 Lua 脚本中操作的原子性。

比如说,在用 redis 实现分布式锁的场景下,解锁期间涉及 2 个操作,分别是先判断锁是不是自己的,是自己的才能删除锁,为了保证这 2 个操作的原子性,会通过 lua 脚本来保证原子性。

// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

除了lua有没有什么也能保证redis的原子性?

redis 事务也可以保证多个操作的原子性。

如果 redis 事务正常执行,没有发生任何错误,那么使用 MULTI 和 EXEC 配合使用,就可以保证多个操作都完成。

但是,如果事务执行发生错误了,就没办法保证原子性了。比如说 2 个操作,第一个操作执行成果了,但是第二个操作执行的时候,命令出错了,那事务并不会回滚,因为Redis 中并没有提供回滚机制。

举个小例子。事务中的 LPOP 命令对 String 类型数据进行操作,入队时没有报错,但是,在 EXEC 执行时报错了。LPOP 命令本身没有执行成功,但是事务中的 DECR 命令却成功执行了。

#开启事务
127.0.0.1:6379> MULTI
OK
#发送事务中的第一个操作,LPOP命令操作的数据类型不匹配,此时并不报错
127.0.0.1:6379> LPOP a:stock
QUEUED
#发送事务中的第二个操作
127.0.0.1:6379> DECR b:stock
QUEUED
#实际执行事务,事务第一个操作执行报错
127.0.0.1:6379> EXEC
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) (integer) 8

因此,Redis 对事务原子性属性的保证情况:

  • Redis 事务正常执行,可以保证原子性;
  • Redis 事务执行中某一个操作执行失败,不保证原子性;

MySQL

为什么要用索引?

索引可以减少扫描的数据量,提高查询效率。

  • 如果查询的时候,没有用到索引就会全表扫描,这时候查询的时间复杂度是On
  • 如果用到了索引,那么查询的时候,可以基于二分查找算法,通过索引快速定位到目标数据, mysql 索引的数据结构一般是 b+树,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。

为什么索引用b➕树

MySQL 是会将数据持久化在硬盘,而存储功能是由 MySQL 存储引擎实现的,所以讨论 MySQL 使用哪种数据结构作为索引,实际上是在讨论存储引使用哪种数据结构作为索引,InnoDB 是 MySQL 默认的存储引擎,它就是采用了 B+ 树作为索引的数据结构。

要设计一个 MySQL 的索引数据结构,不仅仅考虑数据结构增删改的时间复杂度,更重要的是要考虑磁盘 I/0 的操作次数。因为索引和记录都是存放在硬盘,硬盘是一个非常慢的存储设备,我们在查询数据的时候,最好能在尽可能少的磁盘 I/0 的操作次数内完成。

二分查找树虽然是一个天然的二分结构,能很好的利用二分查找快速定位数据,但是它存在一种极端的情况,每当插入的元素都是树内最大的元素,就会导致二分查找树退化成一个链表,此时查询复杂度就会从 O(logn)降低为 O(n)。

为了解决二分查找树退化成链表的问题,就出现了自平衡二叉树,保证了查询操作的时间复杂度就会一直维持在 O(logn) 。但是它本质上还是一个二叉树,每个节点只能有 2 个子节点,随着元素的增多,树的高度会越来越高。

而树的高度决定于磁盘 I/O 操作的次数,因为树是存储在磁盘中的,访问每个节点,都对应一次磁盘 I/O 操作,也就是说树的高度就等于每次查询数据时磁盘 IO 操作的次数,所以树的高度越高,就会影响查询性能。

B 树和 B+ 都是通过多叉树的方式,会将树的高度变矮,所以这两个数据结构非常适合检索存于磁盘中的数据。

但是 MySQL 默认的存储引擎 InnoDB 采用的是 B+ 作为索引的数据结构,原因有:

  • B+ 树的非叶子节点不存放实际的记录数据,仅存放索引,因此数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
  • B+ 树有大量的冗余节点(所有非叶子节点都是冗余索引),这些冗余索引让 B+ 树在插入、删除的效率都更高,比如删除根节点的时候,不会像 B 树那样会发生复杂的树的变化;
  • B+ 树叶子节点之间用链表连接了起来,有利于范围查询,而 B 树要实现范围查询,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。

数据结构

红黑树和b➕的区别?

红黑树是一种自平衡的二叉查找树

B+树是一种多路平衡查找树

对于有 N 个叶子节点的 B+Tree,其搜索复杂度为O(logdN),其中 d 表示节点允许的最大子节点个数为 d 个。

在实际的应用当中, d 值是大于100的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右,也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据。

而红黑树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 O(logN),这已经比 B+Tree 高出不少,因此红黑树检索到目标数据所经历的磁盘 I/O 次数要更多。

二叉树搜索最坏的时间复杂度,为什么会这样?以及用什么结果解决?

**当每次插入的元素都是二叉查找树中最大的元素,二叉查找树就会退化成了一条链表,查找数据的时间复杂度变成了 O(n)**,如下动图演示:

二叉查找树由于存在退化成链表的可能性,会使得查询操作的时间复杂度从 O(logn) 升为 O(n)。

为了解决二叉查找树会在极端情况下退化成链表的问题,后面就有人提出平衡二叉查找树(AVL 树)

主要是在二叉查找树的基础上增加了一些条件约束:每个节点的左子树和右子树的高度差不能超过 1。也就是说节点的左子树和右子树仍然为平衡二叉树,这样查询操作的时间复杂度就会一直维持在 O(logn) 。

下图是每次插入的元素都是平衡二叉查找树中最大的元素,可以看到,它会维持自平衡:

除了平衡二叉查找树,还有很多自平衡的二叉树,比如红黑树,它也是通过一些约束条件来达到自平衡,不过红黑树的约束条件比较复杂。下面是红黑树插入节点的过程,这左旋右旋的操作,就是为了自平衡。

Java

为什么使用springboot

  • 简化开发:Spring Boot通过提供一系列的开箱即用的组件和自动配置,简化了项目的配置和开发过程,开发人员可以更专注于业务逻辑的实现,而不需要花费过多时间在繁琐的配置上。
  • 快速启动:Spring Boot提供了快速的应用程序启动方式,可通过内嵌的Tomcat、Jetty或Undertow等容器快速启动应用程序,无需额外的部署步骤,方便快捷。
  • 自动化配置:Spring Boot通过自动配置功能,根据项目中的依赖关系和约定俗成的规则来配置应用程序,减少了配置的复杂性,使开发者更容易实现应用的最佳实践。

手撕

用栈实现队列

可以用 2 个栈来实现队列,其中一个栈用于入队操作,另一个栈用于出队操作:

class MyQueue {
    private Stack<Integer> stack1; // 第一个栈用于入队
    private Stack<Integer> stack2; // 第二个栈用于出队

    public MyQueue() {
        stack1 = new Stack<>();
        stack2 = new Stack<>();
    }

    public void push(int x) {
        stack1.push(x);
    }

    public int pop() {
        if (stack2.isEmpty()) { // 如果第二个栈为空,将第一个栈中的元素依次弹出并压入第二个栈
            while (!stack1.isEmpty()) {
                stack2.push(stack1.pop());
            }
        }
        return stack2.pop(); // 然后从第二个栈弹出元素
    }

    public int peek() {
        if (stack2.isEmpty()) {
            while (!stack1.isEmpty()) {
                stack2.push(stack1.pop());
            }
        }
        return stack2.peek(); // 查看第二个栈的栈顶元素
    }

    public boolean empty() {
        return stack1.isEmpty() && stack2.isEmpty(); // 判断队列是否为空
    }
}

其他

跟项目关系比较大,根据具体的项目的回答。

  • 从项目中学到了什么
  • redis 存储什么数据?
  • mysql存储什么数据?
  • 项目里面怎么优化sql的


推荐阅读:
美团基架,我先开冲了!
好难!腾讯面试体验已结束。。。
激动!再次被阿里云捞了一波
艰难走到字节终面了!

浏览 778
3点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
3点赞
评论
收藏
分享

手机扫一扫分享

分享
举报