本文题目来自于牛客上面的面经分享,会贴上来源帖子(删掉了一些需要面试人当时回答场景的题目,不清楚场景不敢乱答)
本次题目来源:感谢分享
个人想法,结合其他资料整理的,文章有问题希望大佬可以指出,目前正在准备春招,希望上岸🙏🏻
目录
自我介绍
继续自信。
mysq索引分类讲讲
mysql索引分类可以按照下面这几类分:
- 数据结构:也就是按照索引的底层数据结构来区分,在mysql中分为B+tree索引、Hash索引、Full-text索引。基本上都会采用B+树来当做索引的数据结构。
- 存储内容:按照索引存储的内容区分,可以分为聚簇索引、二级索引。其中聚簇索引也被叫做主键索引,索引存放了每一条数据的所有数据,而二级索引存储的是主键值,所以有时会导致回表查询(因为二级索引只存了主键值,其他数据还是要回表查询主键索引才会查到)。
- 功能特性:按照索引的功能特性区分,可以分为:主键索引、普通索引、唯一索引、前缀索引。
- 字段数量:按照索引字段数量区分,也就是一个索引包含了多少个字段,可以分为:单列索引、联合索引。
mysql事务讲讲
首先,讲讲什么是事务:事务是一组数据库操作,它们被视为一个不可分割的工作单元,要么全部成功执行,要么全部失败并回滚到之前的状态。
事务有四个很重要的特性:原子性、一致性、隔离性、持久性。(简称ACID特性,分别取每个单词的首字母):
- 原子性:一个事务中的所有操作,要么全部完成,要么全部不完成,不完成可能是出错或者手动回滚导致的,这个时候就要恢复到事务启动前的状态。
- 一致性:指的是事务执行后,数据库从一个一致的状态转换到另一个一致的状态,也就是各个部分或元素之间不存在矛盾或不协调的情况。
- 隔离性:指的是事务与事务之间的操作是隔离开的,谁都不会影响谁。
- 持久性:指的是事务提交以后,对于数据的修改是永久的。
在mysql中,原子性是通过undo log保证的;隔离性是通过 MVCC(多版本并发控制) 或锁机制来保证的;持久性是通过redo log和bin log实现的;而一致性就是通过上面三个特性来保障的。
讲讲用过的java并发处理用过的类
按照类或接口的功能讲个别几个吧:
类/接口 | 描述 |
---|---|
Thread | 表示一个线程的基本单位。 |
Runnable | 定义线程的执行任务。 |
ExecutorService | 提供管理线程池的接口。 |
Lock | 显式锁定机制的接口。 |
ReentrantLock | 显式锁定机制的实现类,提供了可重入锁的功能。 |
Condition | 与Lock接口配合使用的条件对象,用于线程通信和协调。 |
Semaphore | 计数信号量,用于控制对共享资源的访问。 |
CountDownLatch | 同步辅助工具,用于等待一组线程执行完毕。 |
CyclicBarrier | 同步辅助工具,允许一组线程相互等待,直到所有线程达到屏障点。 |
Concurrent集合类 | 提供线程安全的并发集合类,如ConcurrentHashMap、ConcurrentLinkedQueue等。 |
多线程顺序打印值的方案
模拟场景为ABC三个线程按照顺序打印1到100
- 通过加锁:基本上所有的线程问题都可以通过锁的方式来解决,直接看代码:
// 当前的数字大小
static volatile int num=0;
// 一共打印的次数(100次)
static int max=100;
// 用来计算当前是轮到哪一个线程打印
static int state;
static Lock lock;
public static void main(String[] args) {
lock=new ReentrantLock();
Thread a=new Thread(()->test(0,"A"));
Thread b=new Thread(()->test(1,"B"));
Thread c=new Thread(()->test(2,"C"));
a.start();
b.start();
c.start();
}
public static void test(int target,String threadName){
while(true){
try {
//上锁
lock.lock();
//到了100就直接返回
if(num>=max){
return ;
}
// 用来判定当前是轮到哪一个线程了
if(state%3==target){
state+=1;
num+=1;
System.out.println(threadName+":"+num);
}
}finally {
lock.unlock();
}
}
}
- 通过 wait 和 notify() 方法实现,也就是通过线程间的通信。wait()方法用于在一个线程内部暂停其执行,同时释放该对象上的锁;notify()方法则是用于唤醒等待在该对象上的一个线程。(这两个方法都是Object类中的方法)
// 当前的数字大小
static volatile int num = 0;
// 一共打印的次数(100次)
static int max = 100;
// 用来计算当前是轮到哪一个线程打印
static int state;
// 用来当做监视器,也就是通过这个监视器来进行线程间的通信
static final Object lock = new Object();
public static void main(String[] args) {
Thread a = new Thread(() -> test(0, "A"));
Thread b = new Thread(() -> test(1, "B"));
Thread c = new Thread(() -> test(2, "C"));
a.start();
b.start();
c.start();
}
public static void test(int target, String threadName) {
while (true) {
synchronized (lock) {
// 判断当前状态是否符合
while (state % 3 != target) {
// 不符合的时候就让当前线程暂停,等待其他线程执行完后通过notify唤醒。
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(num>=max){
return ;
}
state+=1;
num+=1;
System.out.println(threadName+":"+num);
// 当前线程完成本次打印,唤醒其他线程
lock.notifyAll();
}
}
}
- 通过Semaphore信号量:Semaphore可以用来控制同时访问特定资源的线程数量,其中acquire方法相当于信号量减一,release方法相当于信号量加一,当信号量为0的时候,acquire方法会阻塞等待信号量大于0,线程才会继续往下走。
// 当前的数字大小
static volatile int num = 0;
// 一共打印的次数(100次)
static int max = 100;
// 一开始先将A的信号量置1
static Semaphore A = new Semaphore(1);
static Semaphore B =new Semaphore(0);
static Semaphore C=new Semaphore(0);
public static void main(String[] args) {
Thread a = new Thread(() -> {
while(true){
try {
A.acquire();
if(num>=max){
return ;
}
num+=1;
System.out.println("A:"+num);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
B.release();
// 这时A的信号量为0,B的信号量为1,C的信号量为0
}
}
});
Thread b = new Thread(() -> {
while(true){
try {
B.acquire();
if(num>=max){
return ;
}
num+=1;
System.out.println("B:"+num);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
C.release();
// 这时A的信号量为0,B的信号量为0,C的信号量为1
}
}
});
Thread c = new Thread(() -> {
while(true){
try {
C.acquire();
if(num>=max){
return ;
}
num+=1;
System.out.println("C:"+num);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
A.release();
// 这时A的信号量为1,B的信号量为0,C的信号量为0
}
}
});
a.start();
b.start();
c.start();
}
- 通过Lock和Condition:Condition配合Lock的方法其实和方法2类似,但是Condition是可以唤醒特定条件的线程,而Object的唤醒是随机的。
// 当前的数字大小
static volatile int num = 0;
// 一共打印的次数(100次)
static int max = 100;
// 用于表示当前轮到哪个线程执行
static int state=0;
static Lock lock = new ReentrantLock();
static Condition A = lock.newCondition();
static Condition B = lock.newCondition();
static Condition C = lock.newCondition();
public static void main(String[] args) {
Thread a = new Thread(() -> {
while (true) {
try {
lock.lock();
if(state%3!=0){
// 没轮到A,A进入等待状态,并释放锁
A.await();
}
if (num >= max) {
return;
}
num += 1;
System.out.println("A:" + num);
state+=1;
// 唤醒B
B.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
});
Thread b = new Thread(() -> {
while (true) {
try {
lock.lock();
if(state%3!=1){
// 没轮到B,B进入等待状态,并释放锁。
B.await();
}
if (num >= max) {
return;
}
num += 1;
System.out.println("B:" + num);
state+=1;
//唤醒C
C.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
});
Thread c = new Thread(() -> {
while (true) {
try {
lock.lock();
if(state%3!=2){
// 没轮到C,B进入等待状态,并释放锁。
C.await();
}
if (num >= max) {
return;
}
num += 1;
System.out.println("C:" + num);
state+=1;
//唤醒A
A.signal();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
});
a.start();
b.start();
c.start();
}
总结:一共四种(个人能想到的,求补充)
- 通过加锁实现
- 通过线程间通信实现,使用Object提供的wait和notify
- 通过信号量实现
- 通过Condition和Lock实现
wait的时候线程是什么状态
首先得知道线程一共有六种种状态:
-
NEW(新建):当线程对象被创建但尚未启动时,处于此状态。
-
RUNNABLE(可运行):线程正在Java虚拟机中执行,可以是正在运行,也可以是等待CPU时间片。
-
BLOCKED(阻塞):线程被阻塞并等待监视器锁以进入同步块/方法。例如,线程试图获得一个已经被其他线程持有的锁时,就会被阻塞。
-
WAITING(等待):线程处于等待状态,等待其他线程显式地唤醒它。这种状态通常是由调用wait()、join()或LockSupport.park()等方法引起的。
-
TIMED_WAITING(计时等待):线程在等待一个具有超时限制的条件时处于此状态。例如,调用了带有超时参数的wait()、join()或sleep()方法。
-
TERMINATED(终止):线程已经执行完毕,结束了其生命周期。
用图片的方式比较直观的看到各个线程之间的切换:
所以,通过wait方法可能会使线程处于两种状态,如果wait方法带了参数,则是超时等待状态;如果不带,则是等待状态。
java线程状态其实也可以分为五种(新建、就绪、运行、结束、阻塞),和六种的区别在于他是按照操作系统层面来划分的,而六种则是通过java代码区分的,按照五种的话那么调用了wait方法就处于阻塞状态
分析下wait需要消耗cpu资源吗
wait方法调用以后,他会释放掉线程所占用的锁,并且将线程状态转化为等待状态并让出CPU供其他线程使用,所以调用完wait以后该线程是不会占用CPU资源的。
但是,线程的上下文切换是会占用CPU资源的,也就是说线程从运行状态转为等待状态,并且由等待状态转变为运行状态等状态转换的时候,是会占用CPU资源的,并且如果出现线程平凡的上下文切换,会消耗大量的CPU资源,所以说多线程也是一把双刃剑。
kafka基本结构构成
知识盲区,没咋了解消息队列,只是会使用消息队列,如果有错误希望大佬能指出。
如果我没理解错题目的话,应该说的是kafka的架构设计,上面的图片是来自于腾讯技术公众号的文章。
首先消息队列最基础的生产者和消费者那肯定是要有的;消息队列,那么队列肯定也是要有的,在kafka中,用来做数据流传输的叫做topic(主题),这些就是消息队列共有的东西,下面看看kafka特有的东西:
- 代理(Broker):代理看就是kafka的服务器节点,他就是负责存储和处理消息。
- ZooKeeper:是Kafka集群的协调服务,负责管理集群的元数据信息、分区的分配和副本的同步等。Kafka使用ZooKeeper来协调集群中的各个节点,并确保集群的稳定运行。
再来往细的看看,我们可以看到Broker里面,相同的topic出现了不同的分区(partition)在kafka中,那是因为在kafka中,topic是消息归类的单位,但是他是一个逻辑单位,具体消息的存放还是需要存放到磁盘中的,分区是消息持久化的基本单元,所以我们具体的消息是存放在分区中的,而一个主题可以分为多个分区,其目的就是通过分区将数据水平扩展,以提高并发性能和数据吞吐量。分区的构造如下:
- segment(段):我们将topic分为了很多个区,一个区又可以分成多个段,这么做肯定是有目的的:我们可以将一个分区看成一个日志(这里是日志,不是日志文件),生产者产生的数据会被追加写到日志后面,如果只有一个日志文件的话,数据量大的时候对于数据的读取效率会降低,所以将他分成多个段,每个段维护自己的分段日志,会便于数据的维护和查找。
- 偏移量(offset):每一条数据都会有一个偏移量,用来表示他的具体位置,就类似于数据库中的主键,是唯一标识;但是偏移量的唯一是在分区中唯一,也就是说偏移量的唯一只是针对分区而言的,并不是针对主题。
总结一下:kafka基本架构由代理节点,生产者,消费者构成,生产者发送数据到代理节点,消费者从代理节点消费数据;代理节点由一个个主题构成,主题又由一个个分区构成,分区又有一个个段方构成;最后节点是由Zookeper管理的(注意,在2.8.0以后的kafka已经将Zookeper抛弃了,他的功能由kafka自己实现)。
说说分区处理的方案
个人理解,这道题目问的应该是分区的相关设置,可以从下面几点谈谈:
- 分区数量:通常建议将分区数量设置为消费者数量的整数倍,以确保每个消费者能够独立地消费一个或多个分区中的消息。这样可以实现消费者之间的负载均衡和并行处理,提高系统的吞吐量和性能。
- 分区写入策略:消费者产生的数据要写入分区中,kafka提供了三种写入策略:轮询策略、随机策略和按键保存策略:轮询就是顺序写入了ABC三条数据,按照分区顺序123分别放入;随机就是随机;按键保存就是通过计算hash值放入到对应的分区中。选择合适的分区策略有助于分区间数据密度的平均,避免数据都堆在一个分区里,这样分区的意义就不大了。(默认是轮询策略)
- 消费者分区分配策略:首先我们得知道一个概念—重平衡,指的是消费者组的重新分配分区的过程,当消费者加入或离开消费者组时,或者分区的数量发生变化时,Kafka会触发重平衡操作。那么对于分区的分配就有以下四种:
1️⃣.Round-Robin:该策略将分区依次分配给每个消费者,循环进行。例如,如果有4个分区,2个消费者,则第一个消费者可能被分配分区0和2,而第二个消费者可能被分配分区1和3。
2️⃣.Range:该策略将每个消费者分配一定范围内的连续分区。例如,如果有4个分区,2个消费者,则第一个消费者可能被分配分区0和1,而第二个消费者可能被分配分区2和3。
3️⃣.Sticky:该策略从名字上就可以看出来,他是想让分区分配的时候,尽量保持原来的分区分配;首先他要保证分到的分区尽可能平均,然后再考虑分到分区尽可能和之前一样。
4️⃣.CooperativeSticky:该策略的核心思想和人Sticky策略一样,也是保障分区平均并且稳定(与之前分区保持不变),主要区别是Sticky策略是通过Kafka协调器来计算出分区的情况,而CooperativeSticky策略则需要消费者在再平衡时向协调器提供有关自身状态的信息,从而达到协调器可以根据消费者情况更好的进行适合当前消费者的分区。
具体分区策略详情可以看这位大佬的文章:文章路径
分析kafka乱序消费可能的原因和解决方案
先来说说原因:
- 再平衡问题:分区的重新分配可能导致消费者开始处理新分配的分区,这可能会引起消息的乱序。
- 消息重试机制:如果消息在传输过程中发生重试,可能会导致消息的重复发送,从而引发乱序消费。
- 发送端问题:发送端发送的数据就是乱序的,那也没办法。
- 分区资源倾斜问题:分区分配消息不均匀,导致有的分区负载过重,消费变慢;有的分区负载轻,消费就快,就有可能会导致消费乱序的问题。
- 消费者并发度:当多个消费者并行消费同一个主题或分区时,消费者的并发度可能导致消息被处理的顺序变得混乱。
解决方案有:
- 单分区单消费:将一个分区只给一个消费者消费,这样子就可以解决并发的问题(原因5)。
- 生产方设置KEY值:在生产者端使用分区的键(key)来保障消息的有序性(可以使用递增的方式)。Kafka保证同一个键的消息会被写入同一个分区,并且在该分区中按照顺序被写入。
- 消费端使用锁机制:简单粗暴的方法就是消费端使用锁,这样可以保障数据的有序性,但是会降低性能。
求根号x
可能是力扣69题,所以就按照这道题来写了(采用二分法):
public int mySqrt(int x) {
if(x==0){
return 0;
}
int l=1,r=x;
while(l<r){
int mid=r-(r-l)/2;
if(x/mid==mid){
return mid;
}else if(x/mid<mid){
r=mid-1;
}else{
l=mid;
}
}
return l;
}