今天分享一篇小林哥的文章。
一线城市由于限车油牌,很多我身边的朋友,买的第一辆车都是电车,基本上人均特斯拉。
国内也有很多优秀的新能源车企,理想、小鹏、比亚迪、蔚来等等,今年小米也入局这个赛道。
这些新能源车企中,发现理想汽车的校招薪资开的特别高,堪比互联网大厂水平了,总包能达到 40w+。
从目前已经开奖同学来看,25 届理想汽车后端开发的校招薪资如下:
28k x 16 = 44w,base 北京,大部分同学是这个,应该是普通 offer 32k x 16 = 51w,base 北京,个别同学才有,可能是 sp offer
话说回来,理想开这么高薪资,面试难度如何呢?
之前分享了很多互联网公司后端面经,这次给大家分享一位同学面试理想汽车的 Java 后端校招面经,这个面经还是比较经典,基本后端的知识都问了遍。
我也把问到的知识点,罗列了一下
Java:线程池、垃圾回收、juc、spring aop MySQL:索引失效 Redis:缓存三兄弟、布隆过滤器 Kafka:topic、分区、消费线程的关系 操作系统:线程间通信、socket 编程 网络:访问网站到显示的全流程
MySQL
索引失效的场景知道哪些?
对索引使用左或者左右模糊匹配,会索引失效
当我们使用左或者左右模糊匹配的时候,也就是 like %xx
或者 like %xx%
这两种方式都会造成索引失效。
比如下面的 like 语句,查询 name 后缀为「林」的用户,执行计划中的 type=ALL 就代表了全表扫描,而没有走索引。
// name 字段为二级索引
select * from t_user where name like '%林';
对索引使用函数,会索引失效
有时候我们会用一些 MySQL 自带的函数来得到我们想要的结果,这时候要注意了,如果查询条件中对索引字段使用函数,就会导致索引失效。
比如下面这条语句查询条件中对 name 字段使用了 LENGTH 函数,执行计划中的 type=ALL,代表了全表扫描:
// name 为二级索引
select * from t_user where length(name)=6;
对索引进行表达式计算,会索引失效
在查询条件中对索引进行表达式计算,也是无法走索引的。
比如,下面这条查询语句,执行计划中 type = ALL,说明是通过全表扫描的方式查询数据的:
explain select * from t_user where id + 1 = 10;
对索引隐式类型转换,会索引失效
如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,你会在执行计划的结果发现这条语句会走全表扫描。
我在原本的 t_user 表增加了 phone 字段,是二级索引且类型是 varchar。
然后我在条件查询中,用整型作为输入参数,此时执行计划中 type = ALL,所以是通过全表扫描来查询数据的。
select * from t_user where phone = 1300000001;
这是因为 phone 字段为字符串,所以 MySQL 要会自动把字符串转为数字,所以这条语句相当于:
select * from t_user where CAST(phone AS signed int) = 1300000001;
可以看到,CAST 函数是作用在了 phone 字段,而 phone 字段是索引,也就是对索引使用了函数!而前面我们也说了,对索引使用函数是会导致索引失效的
联合索引非最左匹配,会索引失效
联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配。
比如,如果创建了一个 (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;
Redis
什么是缓存雪崩、缓存击穿和缓存穿透?怎么解决?
缓存雪崩:当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃,这就是缓存雪崩的问题。
缓存击穿:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
缓存穿透:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。
缓存雪崩解决方案:
均匀设置过期时间:如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。 互斥锁:当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。 后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
缓存击穿解决方案:
互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
缓存穿透解决方案:
非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。 缓存空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。 布隆过滤器:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
布隆过滤器原理是什么?
布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
布隆过滤器会通过 3 个操作完成标记:
第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值; 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。 第三步,将每个哈希值在位图数组的对应位置的值设置为 1;
举个例子,假设有一个位图数组长度为 8,哈希函数 3 个的布隆过滤器。
在数据库写入数据 x 后,把数据 x 标记在布隆过滤器时,数据 x 会被 3 个哈希函数分别计算出 3 个哈希值,然后在对这 3 个哈希值对 8 取模,假设取模的结果为 1、4、6,然后把位图数组的第 1、4、6 位置的值设置为 1。当应用要查询数据 x 是否数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中。
布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。
所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据。
操作系统
线程间有哪些通信方式?
共享内存:线程可以通过访问共享的内存区域来进行数据交换和共享。Linux提供了共享内存的机制,可以使用
shmget()
、shmat()
等函数进行共享内存的创建和映射。信号量:线程可以使用信号量来进行同步和互斥操作。Linux提供了信号量机制,可以使用
sem_init()
、sem_wait()
、sem_post()
等函数来操作信号量。互斥锁:线程可以使用互斥锁来实现对共享资源的互斥访问。Linux提供了互斥锁机制,可以使用
pthread_mutex_init()
、pthread_mutex_lock()
、pthread_mutex_unlock()
等函数来操作互斥锁。条件变量:线程可以使用条件变量来等待和通知特定的条件。Linux提供了条件变量机制,可以使用
pthread_cond_init()
、pthread_cond_wait()
、pthread_cond_signal()
等函数来操作条件变量。管道:线程可以使用管道进行简单的数据传输。Linux提供了管道机制,可以使用
pipe()
函数来创建管道,并使用read()
和write()
函数进行数据的读写。
有了解过Socket网络套接字吗?
服务端和客户端初始化 socket
,得到文件描述符;服务端调用 bind
,将 socket 绑定在指定的 IP 地址和端口;服务端调用 listen
,进行监听;服务端调用 accept
,等待客户端连接;客户端调用 connect
,向服务端的地址和端口发起连接请求;服务端 accept
返回用于传输的socket
的文件描述符;客户端调用 write
写入数据;服务端调用read
读取数据;客户端断开连接时,会调用 close
,那么服务端read
读取数据的时候,就会读取到了EOF
,待处理完数据后,服务端调用close
,表示连接关闭。
这里需要注意的是,服务端调用 accept
时,连接成功了会返回一个已完成连接的 socket,后续用来传输数据。
所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。
网络
键入网址到浏览器显示出来的过程?
应用层DNS解析,传输层TCP连接,网络层IP,数据链路MAC,真实物理层,接收到之后再一层层扒皮。
更详细传输层->网络层->数据链路层->路由器的过程,看图解网络->基础篇->键入网址到网页显示期间发生了什么?。
Java
Java中线程池有哪些?
ScheduledThreadPool:可以设置定期的执行任务,它支持定时或周期性执行任务,比如每隔 10 秒钟执行一次任务,我通过这个实现类设置定期执行任务的策略。 FixedThreadPool:它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了。 CachedThreadPool:可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到 Integer.MAX_VALUE,为 2^31-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。 SingleThreadExecutor:它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。 SingleThreadScheduledExecutor:它实际和 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程。
线程池淘汰策略有哪些?
当线程池的任务队列满了之后,线程池会执行指定的拒绝策略来应对,常用的四种拒绝策略包括:CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy,此外,还可以通过实现RejectedExecutionHandler接口来自定义拒绝策略。
四种预置的拒绝策略:
CallerRunsPolicy,使用线程池的调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的任务队列已有空缺。 AbortPolicy,直接抛出一个任务被线程池拒绝的异常。 DiscardPolicy,不做任何处理,静默拒绝提交的任务。 DiscardOldestPolicy,抛弃最老的任务,然后执行该任务。 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略。
GC是什么?
GC 是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致程序或系统的不稳定甚至崩溃。
Java 虚拟机提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的,Java 语言没有提供释放已分配内存的显示操作方法。Java 程序员不用担心内存管理, 因为垃圾收集器会自动进行管理。
说一下G1垃圾回收器?
G1(Garbage First) 垃圾收集器,是关注最小时延的垃圾回收器,也同样适合大尺寸堆内存的垃圾收集,官方推荐选择使用 G1 来替代 CMS 。
G1最大的特点是引入分区的思路,弱化了分代的概念。合理利用垃圾收集各个周期的资源,解决了其他收集器、甚至 CMS 的众多缺陷。
G1 相比 CMS的改进主要是这几个方面:
算法:G1 基于标记--整理算法, 不会产生空间碎片,在分配大对象时,不会因无法得到连续的空间,而提前触发一次 FULL GC 。 停顿时间可控:G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象。 并行与并发:G1 能更充分的利用 CPU 多核环境下的硬件优势,来缩短 stop the world 的停顿时间。
G1 收集器的主要应用在多 CPU 大内存的服务中,在满足高吞吐量的同时,尽可能的满足垃圾回收时的暂停时间。在以下场景中,G1 更适合:
服务端多核 CPU、JVM 内存占用较大的应用(至少大于4G); 应用在运行过程中,会产生大量内存碎片、需要经常压缩空间; 想要更可控、可预期的 GC 停顿周期,防止高并发下应用雪崩现象。
了解volatile吗?
volatile关键字保证了两个性质:
可见性:可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 有序性:对一个volatile变量的写操作,执行在任意后续对这个volatile变量的读操作之前。
volatile 汇编是怎么实现的?
对于JVM的内存屏障实现中,也采取了内存屏障。JVM的内存屏障有四种,我们来看一下这四种屏障和他们的作用:
LoadLoad屏障:对于这样的语句
第一大段读数据指令;
LoadLoad;
第二大段读数据指令;
LoadLoad指令作用:在第二大段读数据指令被访问前,保证第一大段读数据指令执行完毕
StoreStore屏障:对于这样的语句
第一大段写数据指令;
StoreStore;
第二大段写数据指令;
StoreStore指令作用:在第二大段写数据指令被访问前,保证第一大段写数据指令执行完毕
LoadStore屏障:对于这样的语句
第一大段读数据指令;
LoadStore;
第二大段写数据指令;
LoadStore指令作用:在第二大段写数据指令被访问前,保证第一大段读数据指令执行完毕。
StoreLoad屏障:对于这样的语句
第一大段写数据指令;
StoreLoad;
第二大段读数据指令;
StoreLoad指令作用:在第二大段读数据指令被访问前,保证第一大段写数据指令执行完毕。
针对volatile变量,JVM采用的内存屏障是:
针对volatile修饰变量的写操作:在写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障; 针对volatile修饰变量的读操作:在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
通过这种方式,就可以保证被volatile修饰的变量具有线程间的可见性和禁止指令重排序的功能了。
Synchronized 和 ReentrantLock 有什么区别?
主要区别有以下 5 个:
用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。 获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。 锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。
Kafka
对Kafka有什么了解吗?
Kafka特点如下:
高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。 可扩展性:kafka集群支持热扩展 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败) 高并发:支持数千个客户端同时读写
如果有一个消费主题topic,有一个消费组group,topic有10个分区,消费线程数和分区数的关系是怎么样的?
topic下的一个分区只能被同一个consumer group下的一个consumer线程来消费,但反之并不成立,即一个consumer线程可以消费多个分区的数据,比如Kafka提供的ConsoleConsumer,默认就只是一个线程来消费所有分区的数据。
所以,分区数决定了同组消费者个数的上限。
如果你的分区数是N,那么最好线程数也保持为N,这样通常能够达到最大的吞吐量。超过N的配置只是浪费系统资源,因为多出的线程不会被分配到任何分区。
Spring
Spring AOP的概念了解吗?
Spring AOP是Spring框架中的一个重要模块,用于实现面向切面编程。
可以把Spring AOP看作是对Spring的补充,它使得Spring不需要EJB就能提供声明式事务管理;或者 使用Spring AOP框架的全部功能来实现自定义的方面。
AOP概念:
方面(Aspect):一个关注点的模块化,这个关注点实现可能 另外横切多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的 Advisor或拦截器实现。 连接点(Joinpoint): 程序执行过程中明确的点,如方法的调 用或特定的异常被抛出。 通知(Advice): 在特定的连接点,AOP框架执行的动作。各种类 型的通知包括“around”、“before”和“throws”通知。通知类型将在下面讨论。许多AOP框架 包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器 链。 切入点(Pointcut): 指定一个通知将被引发的一系列连接点 的集合。AOP框架必须允许开发者指定切入点:例如,使用正则表达式。 引入(Introduction): 添加方法或字段到被通知的类。Spring允许引入新的接口到任何被通知的对象。例如,你可以使用一个引入使任何对象实现 IsModified
接口,来简化缓存。目标对象(Target Object): 包含连接点的对象。也被称作 被通知或被代理对象。 AOP代理(AOP Proxy): AOP框架创建的对象,包含通知。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。 织入(Weaving): 组装方面来创建一个被通知对象。这可以在编译时 完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样, 在运行时完成织入。
AOP和OOP的关系是什么?
AOP
与 OOP
不是相互对立的关系,可以把 AOP
看作是弥补 OOP
的不足,以此之长、补彼之短,两者结合使用效果最佳。
OOP
是针对业务实体及其属性 和行为 进行抽象封装 ,这个不难理解,例如:用户模块、订单模块 等。AOP
是针对业务切面进行提取,它所面对的是处理过程中的某个 步骤 或 阶段 ,以达到逻辑处理过程中各部分之间低耦合性的 隔离效果 ,例如:日志记录、权限验证 等。
举个例子,如果单纯使用 OOP
,需要在日志模块、订单模块中进行权限验证、日志记录怎么办?难道要在每个方法前都加入权限验证、日志记录的代码吗?那么如果需要在每个方法前和方法后都记录日志怎么办?
这时如果使用 AOP
,就可以借助代理完成这些重复的操作,就可以不在每个方法前加入权限验证、日志记录的代码,降低各部分之间的耦合。
AOP底层实现是什么?
Spring AOP的底层实现原理主要依赖于动态代理。在Spring AOP中,通过动态代理技术,可以在运行时动态地创建一个代理对象,将切面逻辑织入到目标对象的方法调用中。
Spring AOP主要有两种类型的代理:基于接口的代理和基于类的代理。
对于基于接口的代理,Spring AOP使用JDK动态代理来实现。JDK动态代理要求目标对象实现一个或多个接口,然后通过Proxy类的静态方法创建一个代理对象。代理对象实现了目标对象的接口,并且在方法调用前后添加了切面逻辑。
对于基于类的代理,Spring AOP使用CGLIB(Code Generation Library)来实现。CGLIB是一个强大的代码生成库,它通过继承的方式创建一个目标对象的子类,并在子类中重写目标对象的方法,从而实现切面逻辑的织入。
在运行时,当客户端调用目标对象的方法时,实际上是调用了代理对象的方法。代理对象会在方法调用前后执行切面逻辑,并最终将方法调用委托给目标对象。
项目
在项目中主要负责什么? 性能调优遇到了什么瓶颈,以及是如何优化的?
👇🏻 点击下方阅读原文,获取鱼皮的编程学习路线、原创项目教程、求职面试宝典、编程交流圈子。
往期推荐
这 10 套项目,一个比一个惊艳!
我按这路线刷题,拿了8个 Offer
我发现很多程序员都不会打日志。。
1 分钟,让你的网站充满吸引力!
5 年前,我也曾在字节跳动实习。。
鱼厂实习,光速转正了!