凉透了!止步蚂蚁金服三面
共 18719字,需浏览 38分钟
·
2024-07-14 16:06
图解学习网站:https://xiaolincoding.com
大家好,我是小林。
今天来分享一位同学的蚂蚁金服的Java后端社招面试题,这位读者在过程中经历了三次面试,感觉自己答得不错,结果三面后挂了,着实让自己有点摸不着头脑。
三次面试过程当中,穿插了基础的八股题,当我看到面试问题时,心里暗暗说了一句“不愧是蚂蚁”,问的问题都比较偏难,问的是比较细节的,所以平常的技术积累是很重要的。
并且通过面经可以看到,蚂蚁非常注重算法能力,直接给了 3 个算法题,然后选择 2 个来做。
接下来,就让我们来一起看看蚂蚁基础题的难度吧!
考察的知识点,我给大家罗列了一下:
-
Java:volatile、弱引用、堆内存、垃圾回收、Spring、线程池 -
MySQL:索引、联合索引、行级锁、SQL语句 -
kafka:副本、ISR -
Redis:大key处理 -
算法:翻转二叉树、从左到右打印二叉树、给一个字符串清除特定字符前的所有字符
Java
volatile关键字的作用
volatite作用有 2 个:
-
保证变量对所有线程的可见性。当一个变量被声明为volatile时,它会保证对这个变量的写操作会立即刷新到主存中,而对这个变量的读操作会直接从主存中读取,从而确保了多线程环境下对该变量访问的可见性。这意味着一个线程修改了volatile变量的值,其他线程能够立刻看到这个修改,不会受到各自线程工作内存的影响。 -
禁止指令重排序优化。volatile关键字在Java中主要通过内存屏障来禁止特定类型的指令重排序。 -
1)写-写(Write-Write)屏障:在对volatile变量执行写操作之前,会插入一个写屏障。这确保了在该变量写操作之前的所有普通写操作都已完成,防止了这些写操作被移到volatile写操作之后。 -
2)读-写(Read-Write)屏障:在对volatile变量执行读操作之后,会插入一个读屏障。它确保了对volatile变量的读操作之后的所有普通读操作都不会被提前到volatile读之前执行,保证了读取到的数据是最新的。 -
3)写-读(Write-Read)屏障:这是最重要的一个屏障,它发生在volatile写之后和volatile读之前。这个屏障确保了volatile写操作之前的所有内存操作(包括写操作)都不会被重排序到volatile读之后,同时也确保了volatile读操作之后的所有内存操作(包括读操作)都不会被重排序到volatile写之前。
弱引用了解吗,举例说明在哪里可以用
Java中的弱引用是一种引用类型,它不会阻止一个对象被垃圾回收。
在Java中,弱引用是通过java.lang.ref.WeakReference
类实现的。弱引用的一个主要用途是创建非强制性的对象引用,这些引用可以在内存压力大时被垃圾回收器清理,从而避免内存泄露。弱引用的使用场景:
-
缓存系统:弱引用常用于实现缓存,特别是当希望缓存项能够在内存压力下自动释放时。如果缓存的大小不受控制,可能会导致内存溢出。使用弱引用来维护缓存,可以让JVM在需要更多内存时自动清理这些缓存对象。 -
对象池:在对象池中,弱引用可以用来管理那些暂时不使用的对象。当对象不再被强引用时,它们可以被垃圾回收,释放内存。 -
避免内存泄露:当一个对象不应该被长期引用时,使用弱引用可以防止该对象被意外地保留,从而避免潜在的内存泄露。
示例代码:假设我们有一个缓存系统,我们使用弱引用来维护缓存中的对象:
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
public class CacheExample {
private Map<String, WeakReference<MyHeavyObject>> cache = new HashMap<>();
public MyHeavyObject get(String key) {
WeakReference<MyHeavyObject> ref = cache.get(key);
if (ref != null) {
return ref.get();
} else {
MyHeavyObject obj = new MyHeavyObject();
cache.put(key, new WeakReference<>(obj));
return obj;
}
}
// 假设MyHeavyObject是一个占用大量内存的对象
private static class MyHeavyObject {
private byte[] largeData = new byte[1024 * 1024 * 10]; // 10MB data
}
}
在这个例子中,使用WeakReference
来存储MyHeavyObject
实例,当内存压力增大时,垃圾回收器可以自由地回收这些对象,而不会影响缓存的正常运行。
如果一个对象被垃圾回收,下次尝试从缓存中获取时,get()
方法会返回null
,这时我们可以重新创建对象并将其放入缓存中。因此,使用弱引用时要注意,一旦对象被垃圾回收,通过弱引用获取的对象可能会变为null
,因此在使用前通常需要检查这一点。
堆内存结构
Java堆是Java虚拟机中内存管理的一个重要区域,主要用于存放对象实例和数组。随着JVM的发展和不同垃圾收集器的实现,堆的具体划分可能会有所不同,但通常可以分为以下几个部分:
-
新生代:新生代分为Eden Space和Survivor Space。在Eden Space中, 大多数新创建的对象首先存放在这里。Eden区相对较小,当Eden区满时,会触发一次Minor GC(新生代垃圾回收)。在Survivor Spaces中,通常分为两个相等大小的区域,称为S0(Survivor 0)和S1(Survivor 1)。在每次Minor GC后,存活下来的对象会被移动到其中一个Survivor空间,以继续它们的生命周期。这两个区域轮流充当对象的中转站,帮助区分短暂存活的对象和长期存活的对象。 -
老年代:存放过一次或多次Minor GC仍存活的对象会被移动到老年代。老年代中的对象生命周期较长,因此Major GC(也称为Full GC,涉及老年代的垃圾回收)发生的频率相对较低,但其执行时间通常比Minor GC长。老年代的空间通常比新生代大,以存储更多的长期存活对象。 -
元空间(Metaspace):从Java 8开始,永久代(Permanent Generation)被元空间取代,用于存储类的元数据信息,如类的结构信息(如字段、方法信息等)。元空间并不在Java堆中,而是使用本地内存,这解决了永久代容易出现的内存溢出问题。 -
大对象区:在某些JVM实现中(如G1垃圾收集器),为大对象分配了专门的区域,称为大对象区或Humongous Objects区域。大对象是指需要大量连续内存空间的对象,如大数组。这类对象直接分配在老年代,以避免因频繁的年轻代晋升而导致的内存碎片化问题。
minorGC、majorGC、fullGC的区别,什么场景触发full GC
在Java中,垃圾回收机制是自动管理内存的重要组成部分。根据其作用范围和触发条件的不同,可以将GC分为三种类型:Minor GC(也称为Young GC)、Major GC(有时也称为Old GC)、以及Full GC。以下是这三种GC的区别和触发场景:
Minor GC (Young GC)
-
作用范围:只针对年轻代进行回收,包括Eden区和两个Survivor区(S0和S1)。 -
触发条件:当Eden区空间不足时,JVM会触发一次Minor GC,将Eden区和一个Survivor区中的存活对象移动到另一个Survivor区或老年代(Old Generation)。 -
特点:通常发生得非常频繁,因为年轻代中对象的生命周期较短,回收效率高,暂停时间相对较短。
Major GC
-
作用范围:主要针对老年代进行回收,但不一定只回收老年代。 -
触发条件:当老年代空间不足时,或者系统检测到年轻代对象晋升到老年代的速度过快,可能会触发Major GC。 -
特点:相比Minor GC,Major GC发生的频率较低,但每次回收可能需要更长的时间,因为老年代中的对象存活率较高。
Full GC
-
作用范围:对整个堆内存(包括年轻代、老年代以及永久代/元空间)进行回收。 -
触发条件: -
直接调用 System.gc()
或Runtime.getRuntime().gc()
方法时,虽然不能保证立即执行,但JVM会尝试执行Full GC。 -
Minor GC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发Full GC,对整个堆内存进行回收。 -
当永久代(Java 8之前的版本)或元空间(Java 8及以后的版本)空间不足时。 -
特点:Full GC是最昂贵的操作,因为它需要停止所有的工作线程(Stop The World),遍历整个堆内存来查找和回收不再使用的对象,因此应尽量减少Full GC的触发。
Spring bean的作用域
Spring框架中的Bean作用域(Scope)定义了Bean的生命周期和可见性。不同的作用域影响着Spring容器如何管理这些Bean的实例,包括它们如何被创建、如何被销毁以及它们是否可以被多个用户共享。
Spring支持几种不同的作用域,以满足不同的应用场景需求。以下是一些主要的Bean作用域:
-
Singleton(单例):在整个应用程序中只存在一个 Bean 实例。默认作用域,Spring 容器中只会创建一个 Bean 实例,并在容器的整个生命周期中共享该实例。 -
Prototype(原型):每次请求时都会创建一个新的 Bean 实例。次从容器中获取该 Bean 时都会创建一个新实例,适用于状态非常瞬时的 Bean。 -
Request(请求):每个 HTTP 请求都会创建一个新的 Bean 实例。仅在 Spring Web 应用程序中有效,每个 HTTP 请求都会创建一个新的 Bean 实例,适用于 Web 应用中需求局部性的 Bean。 -
Session(会话):Session 范围内只会创建一个 Bean 实例。该 Bean 实例在用户会话范围内共享,仅在 Spring Web 应用程序中有效,适用于与用户会话相关的 Bean。 -
Application:当前 ServletContext 中只存在一个 Bean 实例。仅在 Spring Web 应用程序中有效,该 Bean 实例在整个 ServletContext 范围内共享,适用于应用程序范围内共享的 Bean。 -
WebSocket(Web套接字):在 WebSocket 范围内只存在一个 Bean 实例。仅在支持 WebSocket 的应用程序中有效,该 Bean 实例在 WebSocket 会话范围内共享,适用于 WebSocket 会话范围内共享的 Bean。 -
Custom scopes(自定义作用域):Spring 允许开发者定义自定义的作用域,通过实现 Scope 接口来创建新的 Bean 作用域。
在Spring配置文件中,可以通过
<bean id="myBean" class="com.example.MyBeanClass" scope="singleton"/>
在Spring Boot或基于Java的配置中,可以通过@Scope注解来指定Bean的作用域。例如:
@Bean
@Scope("prototype")
public MyBeanClass myBean() {
return new MyBeanClass();
}
在Spring中,在bean加载/销毁前后,如果想实现某些逻辑,可以怎么做
在Spring框架中,如果你希望在Bean加载(即实例化、属性赋值、初始化等过程完成后)或销毁前后执行某些逻辑,你可以使用Spring的生命周期回调接口或注解。这些接口和注解允许你定义在Bean生命周期的关键点执行的代码。
使用init-method和destroy-method
在XML配置中,你可以通过init-method和destroy-method属性来指定Bean初始化后和销毁前需要调用的方法。
<bean id="myBean" class="com.example.MyBeanClass"
init-method="init" destroy-method="destroy"/>
然后,在你的Bean类中实现这些方法:
public class MyBeanClass {
public void init() {
// 初始化逻辑
}
public void destroy() {
// 销毁逻辑
}
}
实现InitializingBean和DisposableBean接口
你的Bean类可以实现org.springframework.beans.factory.InitializingBean和org.springframework.beans.factory.DisposableBean接口,并分别实现afterPropertiesSet和destroy方法。
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class MyBeanClass implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
// 初始化逻辑
}
@Override
public void destroy() throws Exception {
// 销毁逻辑
}
}
使用@PostConstruct和@PreDestroy注解
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
public class MyBeanClass {
@PostConstruct
public void init() {
// 初始化逻辑
}
@PreDestroy
public void destroy() {
// 销毁逻辑
}
}
使用@Bean注解的initMethod和destroyMethod属性
在基于Java的配置中,你还可以在@Bean注解中指定initMethod和destroyMethod属性。
@Configuration
public class AppConfig {
@Bean(initMethod = "init", destroyMethod = "destroy")
public MyBeanClass myBean() {
return new MyBeanClass();
}
}
Java线程池,5核心、10最大、20队列,第6个任务来了是什么状态?第26个任务来了是什么状态?队列满了以后执行队列的任务是从队列头 or 队尾取?核心线程和非核心线程执行结束后,谁先执行队列里的任务?
第6个任务来了是什么状态
-
当第一个任务到达时,线程池会创建一个核心线程来执行这个任务。 -
当第2至第5个任务到达时,线程池将继续创建核心线程直到达到核心线程数上限(即5个核心线程都在运行)。 -
当第6个任务到达时,由于所有核心线程都已经在运行,这个任务将被放入阻塞队列中等待执行。
第26个任务到达时的状态
-
当第6至第25个任务到达时,它们都将依次被加入到阻塞队列中,直到队列满(即20个任务在队列中)。 -
当第26个任务到达时,由于队列已满,而当前线程数小于最大线程数(10),线程池会创建新的非核心线程(即超出核心线程数的线程)来执行这个任务,直到达到最大线程数上限(即总共10个线程在运行)。
如果第26个任务到达时线程池已经有10个线程在运行(包括核心线程和非核心线程),那么根据线程池的拒绝策略,这个任务将被拒绝。默认的拒绝策略是AbortPolicy,它会抛出一个RejectedExecutionException异常。
阻塞队列的FIFO原则
阻塞队列通常遵循先进先出(FIFO)的原则,这意味着任务将按照它们被添加到队列中的顺序执行。当线程完成当前任务并准备获取下一个任务时,它会从队列的头部取出下一个等待的任务。
核心线程与非核心线程执行队列任务
无论是核心线程还是非核心线程,一旦有空闲,都会从队列头部获取任务来执行。线程池并不区分是核心线程还是非核心线程去执行队列中的任务,只要线程有空闲,就会尝试从队列中获取任务执行。
不过,在保持存活时间(keep-alive time)过后,非核心线程可能会被终止,而核心线程则会一直保留,除非线程池被显式关闭。
Redis
Redis中的大key的场景怎么处理
在Redis中,大key指的是那些存储了大量数据的键,这些键可能因为其值的大小或者其包含的元素数量巨大,导致在执行相关操作时对Redis服务器造成显著的性能影响。以下是处理大key的一些建议:
-
识别大key:使用 redis-cli
工具的SCAN
命令结合KEYS
或HGETALL
、LRANGE
等命令来定位哪些key占用过多的内存或哪些操作可能引起性能问题。 -
优化数据结构:使用更高效的数据结构,例如用 Set
、Sorted Set
或Hash
替换String
类型,当数据适合这些结构时。对于大型列表,考虑使用List
的LPUSH
和RPUSH
来限制列表的长度,或者使用ZADD
和ZREM
在有序集合中维护固定大小的滑动窗口。使用Bitmaps
来存储大量二进制数据,尤其是当数据是稀疏的或需要进行位操作时。 -
数据拆分:最好在设计阶段,就把大 key 拆分成一个一个小 key。或者,定时检查 Redis 是否存在大 key ,如果该大 key 是可以删除的,不要使用 DEL 命令删除,因为该命令删除过程会阻塞主线程,而是用 unlink 命令(Redis 4.0+)删除大 key,因为该命令的删除过程是异步的,不会阻塞主线程。 -
避免全量读取:对于大key,尽量避免一次性读取全部数据,而是使用范围查询如 HGET
、LPOP
、RPOP
等命令来分批次读取数据。
kafka
kafka副本了解吗,聊聊ISR
在Kafka中是有主题概念的,而每个主题又进一步划分成若干个分区。副本的概念实际上是在分区层级下定义的,每个分区配置有若干个副本。所谓副本(Replica),本质就是一个只能追加写消息的提交日志。根据Kafka副本机制的定义,同一个分区下的所有副本保存有相同的消息序列,这些副本分散保存在不同的Broker上,从而能够对抗部分Broker宕机带来的数据不可用。在kafka中采用基于领导者(Leader-based)的副本机制来确保副本中所有的数据的一致性。基于领导者的副本机制的工作原理如下图所示:第一,在Kafka中,副本分成两类:领导者副本(Leader Replica)和追随者副本(Follower Replica)。每个分区在创建时都要选举一个副本,称为领导者副本,其余的副本自动称为追随者副本。
第二,Kafka的副本机制比其他分布式系统要更严格一些。在Kafka中,追随者副本是不对外提供服务的。这就是说,任何一个追随者副本都不能响应消费者和生产者的读写请求。所有的请求都必须由领导者副本来处理,或者说,所有的读写请求都必须发往领导者副本所在的Broker,由该Broker负责处理。追随者副本不处理客户端请求,它唯一的任务就是从领导者副本异步拉取消息,并写入到自己的提交日志中,从而实现与领导者副本的同步。
第三,当领导者副本挂掉了,或者说领导者副本所在的Broker宕机时,Kafka依托于ZooKeeper提供的监控功能能够实时感知到,并立即开启新一轮的领导者选举,从追随者副本中选一个作为新的领导者。老Leader副本重启回来后,只能作为追随者副本加入到集群中。
一定要特别注意上面的第二点,即追随者副本是不对外提供服务的。还记得刚刚我们谈到副本机制的好处时,说过Kafka没能提供读操作横向扩展以及改善局部性吗?具体的原因就在于此。
对于客户端用户而言,Kafka的追随者副本没有任何作用,它既不能像MySQL那样帮助领导者副本“扛读”,也不能实现将某些副本放到离客户端近的地方来改善数据局部性。
既然如此,Kafka为什么要这样设计呢?其实这种副本机制有两个方面的好处。
-
方便实现“Read-your-writes”:所谓Read-your-writes,顾名思义就是,当你使用生产者API向Kafka成功写入消息后,马上使用消费者API去读取刚才生产的消息。举个例子,比如你平时发微博时,你发完一条微博,肯定是希望能立即看到的,这就是典型的Read-your-writes场景。如果允许追随者副本对外提供服务,由于副本同步是异步的,因此有可能出现追随者副本还没有从领导者副本那里拉取到最新的消息,从而使得客户端看不到最新写入的消息。 -
方便实现单调读(Monotonic Reads):什么是单调读呢?就是对于一个消费者用户而言,在多次消费消息时,它不会看到某条消息一会儿存在一会儿不存在。如果允许追随者副本提供读服务,那么假设当前有2个追随者副本F1和F2,它们异步地拉取领导者副本数据。倘若F1拉取了Leader的最新消息而F2还未及时拉取,那么,此时如果有一个消费者先从F1读取消息之后又从F2拉取消息,它可能会看到这样的现象:第一次消费时看到的最新消息在第二次消费时不见了,这就不是单调读一致性。但是,如果所有的读请求都是由Leader来处理,那么Kafka就很容易实现单调读一致性。
在kafka中,追随者副本不提供服务,只是定期地异步拉取领导者副本中的数据而已。既然是异步的,就存在着不可能与Leader实时同步的风险。在探讨如何正确应对这种风险之前,我们必须要精确地知道同步的含义是什么。或者说,Kafka要明确地告诉我们,追随者副本到底在什么条件下才算与Leader同步。
基于这个想法,Kafka引入了In-sync Replicas,也就是所谓的ISR副本集合。ISR中的副本都是与Leader同步的副本,相反,不在ISR中的追随者副本就被认为是与Leader不同步的。
那么,到底什么副本能够进入到ISR中呢?
我们首先要明确的是,Leader副本天然就在ISR中。也就是说,ISR不只是追随者副本集合,它必然包括Leader副本。甚至在某些情况下,ISR只有Leader这一个副本。
另外,能够进入到ISR的追随者副本要满足一定的条件。图中有3个副本:1个领导者副本和2个追随者副本。Leader副本当前写入了10条消息,Follower1副本同步了其中的6条消息,而Follower2副本只同步了其中的3条消息。那么问题来了,对于这2个追随者副本,你觉得哪个追随者副本与Leader不同步?
答案是,要根据具体情况来定。换成英文,就是那句著名的“It depends”。看上去好像Follower2的消息数比Leader少了很多,它是最有可能与Leader不同步的。的确是这样的,但仅仅是可能。
事实上,这张图中的2个Follower副本都有可能与Leader不同步,但也都有可能与Leader同步。也就是说,Kafka判断Follower是否与Leader同步的标准,不是看相差的消息数,而是另有“玄机”。
这个标准就是Broker端参数replica.lag.time.max.ms参数值。这个参数的含义是Follower副本能够落后Leader副本的最长时间间隔,当前默认值是10秒。这就是说,只要一个Follower副本落后Leader副本的时间不连续超过10秒,那么Kafka就认为该Follower副本与Leader是同步的,即使此时Follower副本中保存的消息明显少于Leader副本中的消息。
我们都知道,Follower副本唯一的工作就是不断地从Leader副本拉取消息,然后写入到自己的提交日志中。如果这个同步过程的速度持续慢于Leader副本的消息写入速度,那么在replica.lag.time.max.ms时间后,此Follower副本就会被认为是与Leader副本不同步的,因此不能再放入ISR中。此时,Kafka会自动收缩ISR集合,将该副本“踢出”ISR。
值得注意的是,倘若该副本后面慢慢地追上了Leader的进度,那么它是能够重新被加回ISR的。这也表明,ISR是一个动态调整的集合,而非静态不变的。
Unclean领导者选举(Unclean Leader Election)
既然ISR是可以动态调整的,那么自然就可以出现这样的情形:ISR为空。
因为Leader副本天然就在ISR中,如果ISR为空了,就说明Leader副本也“挂掉”了,Kafka需要重新选举一个新的Leader。可是ISR是空,此时该怎么选举新Leader呢?
Kafka把所有不在ISR中的存活副本都称为非同步副本。通常来说,非同步副本落后Leader太多,因此,如果选择这些副本作为新Leader,就可能出现数据的丢失。
毕竟,这些副本中保存的消息远远落后于老Leader中的消息。在Kafka中,选举这种副本的过程称为Unclean领导者选举。Broker端参数unclean.leader.election.enable控制是否允许Unclean领导者选举。
开启Unclean领导者选举可能会造成数据丢失,但好处是,它使得分区Leader副本一直存在,不至于停止对外提供服务,因此提升了高可用性。反之,禁止Unclean领导者选举的好处在于维护了数据的一致性,避免了消息丢失,但牺牲了高可用性。
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 是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。
我这里举联合索引(a,b)的例子,该联合索引的 B+ Tree 如下:可以看到,a 是全局有序的(1, 2, 2, 3, 4, 5, 6, 7 ,8),而 b 是全局是无序的(12,7,8,2,3,8,10,5,2)。因此,直接执行where b = 2这种查询条件没有办法利用联合索引的,利用索引的前提是索引里的 key 是有序的。
只有在 a 相同的情况才,b 才是有序的,比如 a 等于 2 的时候,b 的值为(7,8),这时就是有序的,这个有序状态是局部的,因此,执行where a = 2 and b = 7是 a 和 b 字段能用到联合索引的,也就是联合索引生效了。
创建联合索引时需要注意什么?
建立联合索引时的字段顺序,对索引效率也有很大影响。越靠前的字段被用于索引过滤的概率越高,实际开发工作中建立联合索引时,要把区分度大的字段排在前面,这样区分度大的字段越有可能被更多的 SQL 使用到。区分度就是某个字段 column 不同值的个数「除以」表的总行数,计算公式如下:比如,性别的区分度就很小,不适合建立索引或不适合排在联合索引列的靠前的位置,而 UUID 这类字段就比较适合做索引或排在联合索引列的靠前的位置。
因为如果索引的区分度很小,假设字段的值分布均匀,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为 MySQL 还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比(惯用的百分比界线是"30%")很高的时候,它一般会忽略索引,进行全表扫描。
联合索引ABC,现在有个执行语句是A = XXX and C < XXX,索引怎么走
根据最左匹配原则,A可以走联合索引,C不会走联合索引,但是C可以走索引下推
两个事务update同一条数据会发生什么
当事务 A 进行 update 的时候会记录加 X 型行级锁,如果事务 B 执行 update 的时候,发现记录已经加了 X 型行级锁之后,就会进入阻塞状态,因为发生了写写冲突。
事务 B 会阻塞到到事务A 提交事务之后,因为事务提交之后锁才会释放。
sql题:给学生表、课程成绩表,求不存在01课程但存在02课程的学生的成绩
可以使用SQL的子查询和LEFT JOIN
或者EXISTS
关键字来实现,这里我将展示两种不同的方法来完成这个查询。假设我们有以下两张表:
-
Student
表,其中包含学生的sid
(学生编号)和其他相关信息。 -
Score
表,其中包含sid
(学生编号),cid
(课程编号)和score
(分数)。
方法1:使用LEFT JOIN 和 IS NULL
SELECT s.sid, s.sname, sc2.cid, sc2.score
FROM Student s
LEFT JOIN Score AS sc1 ON s.sid = sc1.sid AND sc1.cid = '01'
LEFT JOIN Score AS sc2 ON s.sid = sc2.sid AND sc2.cid = '02'
WHERE sc1.cid IS NULL AND sc2.cid IS NOT NULL;
方法2:使用NOT EXISTS
SELECT s.sid, s.sname, sc.cid, sc.score
FROM Student s
JOIN Score sc ON s.sid = sc.sid AND sc.cid = '02'
WHERE NOT EXISTS (
SELECT 1 FROM Score sc1 WHERE sc1.sid = s.sid AND sc1.cid = '01'
);
计网
Websocket发一条阻塞了,后面的消息会怎么样
WebSocket发送一条消息时发生了阻塞,如果发送操作是同步的,那么发送一条消息时的阻塞会导致后续消息的发送被挂起,直到当前消息被成功发送。如果是异步发送,那么消息可能被加入到发送队列中,而不立即阻塞。
为了避免阻塞问题,因此可以使用非阻塞(异步)API,允许消息在后台排队和发送,而不会阻塞应用程序的其他部分。
算法(三选二)
-
翻转二叉树 -
给一个字符串清除特定字符前的所有字符 -
从左到右从上到下打印二叉树;
推荐阅读: