今天分享一篇小林哥的文章。
一线城市由于限车油牌,很多我身边的朋友,买的第一辆车都是电车,基本上人均特斯拉。
国内也有很多优秀的新能源车企,理想、小鹏、比亚迪、蔚来等等,今年小米也入局这个赛道。
这些新能源车企中,发现理想汽车的校招薪资开的特别高,堪比互联网大厂水平了,总包能达到 40w+。
从目前已经开奖同学来看,25 届理想汽车后端开发的校招薪资如下:
-
28k x 16 = 44w
,base 北京,大部分同学是这个,应该是普通 offer
-
32k x 16 = 51w
,base 北京,个别同学才有,可能是 sp offer
话说回来,理想开这么高薪资,面试难度如何呢?
之前分享了很多互联网公司后端面经,这次给大家分享一位同学面试
理想汽车的 Java 后端校招面经
,这个面经还是比较经典,基本后端的知识都问了遍。
我也把问到的知识点,罗列了一下
-
Java:线程池、垃圾回收、juc、spring aop
-
-
-
-
-
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 and b=2 and c=3;
-
需要注意的是,因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。
但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:
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网络套接字吗?
基于 TCP 协议的客户端和服务端工作
-
服务端和客户端初始化
socket
,得到文件描述符;
-
服务端调用
bind
,将 socket 绑定在指定的 IP 地址和端口;
-
-
-
客户端调用
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