字节回暖了?要招4000人!

轶睿看教育 2024-08-16 00:51:11

字节跳动宣布今年秋招有 4000+ hc,这很明显比去年多了很多,整体感受是今年25 届秋招比去年回暖了不少。

没想到字节今年提前批都没搞,直接开始正式批了,还记得去年字节是 7 月份开的提前批,今年直接 8 月份进入正式批了,提前批和正式批的边界也在慢慢淡化了。

字节的办公遍布很多城市,但是建议大家还是先不要考虑自己的城市意向,直接投 hc 比较多的城市,北京和上海算是比较多 hc,如果投了一个偏门的城市,hc 就几个,不一定能拿到面试机会。

我们来看看去年字节校招开的薪资,字节的 base 整体会比较高

字节年总包构成 = 月薪 x 15 + 签字费

普通 offer:25k~26k*15+签字费 1w,年包:38w~40wsp offer:28k*15+签字费 1w,年包:43wssp offer:30k~34*15+签字费 1w,年包:46w~52w

有的同学会有期权,但是 base 就会比较低一点,对于字节平均在职员工是 7-8 个月的情况,我会愿意选高 base,不要期权。

既然字节跳动校招已经开始,直接来一个同学字节跳Java 校招一二面的面经,我把一二面的问题都根据技术类型归类好了。

主要是考察了Java+Redis+MySQL+消息队列+算法这些内容,每一面都有一道算法题,有的同学也会遇到过,一场面试写2-3 个算法题,字节还是比较看重算法能力的公司。

一二面的问题内容如下,大家看看难度如何?虽然说是校招,但是感觉有些问的问题跟社招差不多了,现在大厂的校招面试,越来越趋向社招风格了,问的比较多,也比较深。

考察的知识点,帮大家罗列一下:

Java:锁、线程池、JVM、CompletableFutureMySQL:联合索引、并发插入、锁Redis: 批量查询MQ: 延迟消息、消息有序性、重复消费、选型算法:链表倒数第K个值、LRU

Java

讲一讲Java的锁?

Java中的锁是用于管理多线程并发访问共享资源的关键机制。锁可以确保在任意给定时间内只有一个线程可以访问特定的资源,从而避免数据竞争和不一致性。Java提供了多种锁机制,可以分为以下几类:

内置锁(synchronized):Java中的synchronized关键字是内置锁机制的基础,可以用于方法或代码块。当一个线程进入synchronized代码块或方法时,它会获取关联对象的锁;当线程离开该代码块或方法时,锁会被释放。如果其他线程尝试获取同一个对象的锁,它们将被阻塞,直到锁被释放。其中,syncronized加锁时有无锁、偏向锁、轻量级锁和重量级锁几个级别。偏向锁用于当一个线程进入同步块时,如果没有任何其他线程竞争,就会使用偏向锁,以减少锁的开销。轻量级锁使用线程栈上的数据结构,避免了操作系统级别的锁。重量级锁则涉及操作系统级的互斥锁。ReentrantLock:java.util.concurrent.locks.ReentrantLock是一个显式的锁类,提供了比synchronized更高级的功能,如可中断的锁等待、定时锁等待、公平锁选项等。ReentrantLock使用lock()和unlock()方法来获取和释放锁。其中,公平锁按照线程请求锁的顺序来分配锁,保证了锁分配的公平性,但可能增加锁的等待时间。非公平锁不保证锁分配的顺序,可以减少锁的竞争,提高性能,但可能造成某些线程的饥饿。读写锁(ReadWriteLock):java.util.concurrent.locks.ReadWriteLock接口定义了一种锁,允许多个读取者同时访问共享资源,但只允许一个写入者。读写锁通常用于读取远多于写入的情况,以提高并发性。乐观锁和悲观锁:悲观锁(Pessimistic Locking)通常指在访问数据前就锁定资源,假设最坏的情况,即数据很可能被其他线程修改。synchronized和ReentrantLock都是悲观锁的例子。乐观锁(Optimistic Locking)通常不锁定资源,而是在更新数据时检查数据是否已被其他线程修改。乐观锁常使用版本号或时间戳来实现。自旋锁:自旋锁是一种锁机制,线程在等待锁时会持续循环检查锁是否可用,而不是放弃CPU并阻塞。通常可以使用CAS来实现。这在锁等待时间很短的情况下可以提高性能,但过度自旋会浪费CPU资源。

讲一讲线程池参数有哪些?

线程池的构造函数有7个参数:

corePoolSize:线程池核心线程数量。默认情况下,线程池中线程的数量如果 <= corePoolSize,那么即使这些线程处于空闲状态,那也不会被销毁。maximumPoolSize:线程池中最多可容纳的线程数量。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程且当前线程池的线程数量小于corePoolSize,就会创建新的线程来执行任务,否则就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略。keepAliveTime:当线程池中线程的数量大于corePoolSize,并且某个线程的空闲时间超过了keepAliveTime,那么这个线程就会被销毁。unit:就是keepAliveTime时间的单位。workQueue:工作队列。当没有空闲的线程执行新任务时,该任务就会被放入工作队列中,等待执行。threadFactory:线程工厂。可以用来给线程取名字等等handler:拒绝策略。当一个新任务交给线程池,如果此时线程池中有空闲的线程,就会直接执行,如果没有空闲的线程,就会将该任务加入到阻塞队列中,如果阻塞队列满了,就会创建一个新线程,从阻塞队列头部取出一个任务来执行,并将新任务加入到阻塞队列末尾。如果当前线程池中线程的数量等于maximumPoolSize,就不会创建新线程,就会去执行拒绝策略

如何设置核心线程数?

CPU 密集型任务配置尽可能小的线程,cpu核数+1。IO 密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*cpu核数。

核心线程数设置为0可不可以?

可以,当核心线程数为0的时候,会创建一个非核心线程进行执行。

从下面的源码也可以看到,当核心线程数为 0 时,来了一个任务之后,会先将任务添加到任务队列,同时也会判断当前工作的线程数是否为 0,如果为 0,则会创建线程来执行线程池的任务。

讲一讲JVM内存模型

根据 JVM8 规范,JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。

JVM的内存结构主要分为以下几个部分:

元空间:元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。Java 虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫“栈帧”的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。本地方法栈:与虚拟机栈类似,区别是虚拟机栈执行java方法,本地方法站执行native方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。程序计数器:程序计数器可以看成是当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,我们称这类内存区域为“线程私有”内存。堆内存:堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象和数组都在堆上进行分配。这部分空间可通过 GC 进行回收。当申请不到空间时会抛出 OutOfMemoryError。堆是JVM内存占用最大,管理最复杂的一个区域。其唯一的用途就是存放对象实例:所有的对象实例及数组都在对上进行分配。jdk1.8后,字符串常量池从永久代中剥离出来,存放在队中。直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是Java 虚拟机规范中农定义的内存区域。在JDK1.4 中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O 方式,它可以使用native 函数库直接分配堆外内存,然后通脱一个存储在Java堆中的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

实际操作中,如何设置堆内存大小

-Xms:设置初始堆内存大小,也就是JVM启动时分配给堆内存的初始大小。这相当于-XX:InitialHeapSize参数。-Xmx:设置堆内存的最大大小,这是堆内存可以扩展到的最大值。这相当于-XX:MaxHeapSize参数。-Xmn:设置新生代的大小。注意,如果你使用的是G1垃圾收集器,这个参数可能不会产生预期的效果,因为G1收集器使用不同的方式管理堆内存。-XX:NewRatio=:设置新生代和老年代之间的比例。例如,如果NewRatio为2,则老年代将是新生代大小的两倍。-XX:MaxNewSize=:设置新生代的最大大小。这个参数可以限制新生代的增长,避免它占用太多的空间。-XX:SurvivorRatio=:设置Eden区和Survivor区之间的比例。例如,如果SurvivorRatio为8,则意味着每9个单位的新生代空间,Eden区占8个单位,而两个Survivor区各占半单位。-XX:+UseSerialGC、-XX:+UseParallelGC、-XX:+UseConcMarkSweepGC、-XX:+UseG1GC:这些参数分别用于选择串行、并行、并发标记清扫或G1垃圾收集器。不同的垃圾收集器对堆内存的管理方式不同,可能会影响堆内存的性能。

在实际项目中,新生代和老年代的比例一般设置为多少

在实际操作中,可以使用-XX:NewRatio参数来调整这个比例。

例如,设置-XX:NewRatio=2表示老年代是年轻代的两倍大小。如果设置为-XX:NewRatio=1,则意味着年轻代和老年代的大小相等。

默认情况下,新生代与老年代的比例通常是1:2,这意味着新生代占据堆总大小的大约1/3,而老年代占据剩余的2/3。这

种设置适用于那些有很多短期存活对象的应用程序,这些对象在几次年轻代垃圾收集后就会被回收。应用程序如果发现年轻代垃圾收集过于频繁,或者老年代垃圾收集导致的暂停时间过长,可以尝试调整这个比例。

例如,如果年轻代的垃圾收集太频繁,可以增加年轻代的大小,降低年轻代和老年代的比例(比如设置为1:1或更高);相反,如果老年代的垃圾收集成为瓶颈,可以增加老年代的大小,提高年轻代和老年代的比例(比如设置为1:3或更低)。

以上都是一些指导性的建议,在实际操作中,新生代和老年代的比例并没有固定的最佳实践,而是取决于应用程序的具体需求和工作负载特性。

CompletableFuture原理是什么?

CompletableFuture是由Java 8引入的,在Java8之前我们一般通过Future实现异步。

Future用于表示异步计算的结果,只能通过阻塞或者轮询的方式获取结果,而且不支持设置回调方法,Java 8之前若要设置回调一般会使用guava的ListenableFuture,回调的引入又会导致臭名昭著的回调地狱CompletableFuture对Future进行了扩展,可以通过设置回调的方式处理计算结果,同时也支持组合操作,支持进一步的编排,同时一定程度解决了回调地狱的问题。

因此,CompletableFuture的主要作用是弥补Future需要阻塞获取结果,CompletableFuture根据主任务执行结果自动执行依赖任务,无需阻塞主线程等待主任务执行完。当然是在CompletableFuture这个操作定义是异步执行的,无需获取同步结果的前提下

CompletableFuture原理为:异步执行主任务,将依赖任务加入到链表中,主任务执行完从链表中获取依赖任务执行。这时会有个疑问,依赖任务还没加入到链表中,主任务就执行完了,怎么办?所以添加依赖任务的时候,会判断主任务是否执行完,如果执行完则立即执行依赖任务,不添加到链表中。

Redis

Redis批量查询是怎么做的

Redis 的批量查询实现主要依赖于以下几个核心特性:MGET 命令、Pipeline 技术,以及针对哈希表的 HMGET 命令。下面分别解释这些技术的底层实现原理:

MGET 命令:MGET 命令用于批量获取多个键的值。在 Redis 的底层实现中,MGET 接受多个键作为输入,然后在服务器端一次性查找这些键对应的值。实现原理:MGET 在 Redis 服务器端实现为一次循环遍历所有给定的键,然后从数据库中获取对应的值。如果键不存在,则返回 nil。所有结果会被收集在一个数组中并一次性返回给客户端。Pipeline 技术:Pipeline 技术允许客户端将多个命令打包成一个请求发送给服务器,这样可以显著减少客户端和服务器之间的网络往返次数。实现原理:在客户端,可以使用 pipeline() 方法创建一个管道,然后在这个管道中连续调用多个命令。这些命令会被缓存在客户端,直到调用 execute() 方法时,所有的命令才会被一次性发送给 Redis 服务器。在服务器端,这些命令会被依次执行,并且结果会被收集起来返回给客户端。HMGET 命令:HMGET 命令用于从哈希表中批量获取多个字段的值。与 MGET 类似,它可以减少网络传输次数,但是它针对的是哈希表类型的数据。实现原理:HMGET 命令在 Redis 服务器端实现为对哈希表的一次性读取。它会遍历给定的字段名,从哈希表中获取对应的值。如果字段不存在,则返回 nil。所有结果会被收集在一个数组中并一次性返回给客户端。

MySQL

介绍一下联合索引,以及创建联合索引时需要注意什么

通过将多个字段组合成一个索引,该索引就被称为联合索引。比如,将商品表中的 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 字段排序。

因此,使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。比如,如果创建了一个 (a, b, c) 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:

where a=1;where a=1 and b=2 and c=3;where a=1 and b=2;

需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:

where b=2;where c=3;where b=2 and c=3;

上面这些查询条件之所以会失效,是因为(a, b, c) 联合索引,是先按 a 排序,在 a 相同的情况再按 b 排序,在 b 相同的情况再按 c 排序。所以,b 和 c 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。

联合索引范围查询

联合索引有一些特殊情况,并不是查询过程使用了联合索引查询,就代表联合索引中的所有字段都用到了联合索引进行索引查询,也就是可能存在部分字段用到联合索引的 B+Tree,部分字段没有用到联合索引的 B+Tree 的情况。这种特殊情况就发生在范围查询。联合索引的最左匹配原则会一直向右匹配直到遇到「范围查询」就会停止匹配。也就是范围查询的字段可以用到联合索引,但是在范围查询字段的后面的字段无法用到联合索引。

创建联合索引时的注意事项

最左匹配原则:联合索引只有在查询条件从最左边的列开始连续地使用索引列时才有效。例如,联合索引(A, B, C)在查询WHERE A = ? AND B = ?时有效,但在WHERE B = ?或WHERE A = ? AND C = ?时无效。列顺序:索引中列的顺序很重要,应根据查询模式和列的区分度来选择。通常,区分度高的列应放在前面,以便更快地过滤掉不相关的行。更新频率:如果某列经常被修改,频繁的更新可能增加维护索引的成本。因此,考虑将更新频率低的列放在索引的前面。

并发插入怎么避免重复

在数据库表中定义唯一约束或唯一索引,确保某一列或某几列的组合值在表中是唯一的。如果尝试插入一个已经存在的值,数据库将抛出一个错误,应用程序可以捕获并处理这个错误。

MySQL除了行锁还有什么锁,区别是什么

全局锁:通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。表级锁:MySQL 里面表级别的锁有这几种:表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。意向锁的目的是为了快速判断表里是否有记录被加锁。行级锁:InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

MQ

RocketMQ延时消息的底层原理

总体的原理示意图,如下所示:

broker 在接收到延时消息的时候,会将延时消息存入到延时Topic的队列中,然后ScheduleMessageService中,每个 queue 对应的定时任务会不停地被执行,检查 queue 中哪些消息已到设定时间,然后转发到消息的原始Topic,这些消息就会被各自的 producer 消费了。

RocketMQ怎么保证消息有序性?

RocketMQ在默认的情况下消息发送会采取Round Robin轮询方式把消息发送到不同的queue(分区队列);RocketMQ在消费消息的时候从多个queue上拉取消息,这种情况发送和消费是不能保证顺序。

但是如果控制发送的顺序消息只依次发送到同一个queue中,消费的时候只从这个queue上依次拉取,则就保证了顺序。RocketMQ能严格保证消息的有序性,将顺序消息分为全局顺序消息与分区顺序消息。

全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。分区顺序:对于指定的一个 Topic,所有消息根据hashKey进行区块分区。同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。

RocketMQ实现消息的有序性,需要从三个方面一起保证消息的有序性:

生产者生产顺序消息:多线程发送的消息无法保证有序性,因此,需要业务方在发送时,针对同一个业务编号(如同一笔订单)的消息需要保证在一个线程内顺序发送,在上一个消息发送成功后,在进行下一个消息的发送。对应到mq中,消息发送方法就得使用同步发送,异步发送无法保证顺序性Broker保存顺序消息:mq的topic下会存在多个queue,要保证消息的顺序存储,同一个业务编号的消息需要被发送到一个queue中。对应到mq中,需要使用MessageQueueSelector来选择要发送的queue,即对业务编号进行hash,然后根据队列数量对hash值取余,将消息发送到一个queue中消费者顺序消费消息:RocketMQ消费端默认消费逻辑:1、负载均衡,指定消费者负责某些队列;2、当前消费者开启多个线程开始同时消费这个队列,远程拉取消息。从上面消费逻辑可以看到,如果要保证消息有序消费,就要解决这两个问题,需要用到锁来保证一个队列同时只有一个消费者线程进行消费,在broker端,rocketmq是通过锁定MessageQueue的方式,来保证同一时刻,只能有一个消费者进行消费,而在消费端,rocketmq是通过synchronized锁定ConsumeRequest中的run方法,来保证一个消费者同时只能有一个线程进行消费

RocketMQ怎么保证消息不被重复消费

在业务逻辑中实现幂等性,确保即使消息被重复消费,也不会影响业务状态。

例如,对于支付或转账类操作,可以使用唯一订单号或事务ID作为幂等性的标识符,确保同样的操作只会被执行一次。

RocketMQ和Kafka的区别是什么?如何做技术选型?

Kafka的优缺点:

优点:首先,Kafka的最大优势就在于它的高吞吐量,在普通机器4CPU8G的配置下,一台机器可以抗住十几万的QPS,这一点还是相当优越的。Kafka支持集群部署,如果部分机器宕机不可用,则不影响Kafka的正常使用。缺点:Kafka有可能会造成数据丢失,因为它在收到消息的时候,并不是直接写到物理磁盘的,而是先写入到磁盘缓冲区里面的。Kafka功能比较的单一 主要的就是支持收发消息,高级功能基本没有,就会造成适用场景受限。

RocketMQ是阿里巴巴开源的消息中间件,优缺点:

优点:支持功能比较多,比如延迟队列、消息事务等等,吞吐量也高,单机吞吐量达到 10 万级,支持大规模集群部署,线性扩展方便,Java语言开发,满足了国内绝大部分公司技术栈缺点:性能相比 kafka 是弱一点,因为 kafka 用到了 sendfile 的零拷贝技术,而 RocketMQ 主要是用 mmap+write 来实现零拷贝。

该怎么选择呢?

如果我们业务只是收发消息这种单一类型的需求,而且可以允许小部分数据丢失的可能性,但是又要求极高的吞吐量和高性能的话,就直接选Kafka就行了,就好比我们公司想要收集和传输用户行为日志以及其他相关日志的处理,就选用的Kafka中间件。如果公司的需要通过 mq 来实现一些业务需求,比如延迟队列、消息事务等,公司技术栈主要是Java语言的话,就直接一步到位选择RocketMQ,这样会省很多事情。

算法

链表倒数第k个值带过期时间的LRU
0 阅读:1

轶睿看教育

简介:感谢大家的关注