2022面试遇到的问题总结

JVM 调优

  1. 什么是JVM调优: 减少full gc、降低gc的停顿时间、提高吞吐量;
  2. 调优步骤: ===提高吞吐量 > 降低gc停顿时间 (在满足提高吞吐量的前提下再去减低gc的停顿时间;若不能同时满足上面两个条件则选择一个最适合系统的一种调优结果。)

JVM常用调优参数

  1. -Xmx1024m: 最大的堆内存,当物理内存不超过192m的时候最大堆内存为物理内存的一半,否则为物理内存的四分之一。
  2. -Xms1024m: 最小堆内存,一般设置和最大内存一样。
  3. -XX:+PrinGCDetails: 输出gc的详细日志。
  4. -XX:+PrinGCTimeStamps或者-XX:PrinGCDateStamps : 输出gc信息时带上时间戳。
  5. jcmd: 用于查看jvm状态,可以查看正在运行的线程,会显示出进程号。
  6. imap-heap 进程号: 查看某一进程号的堆使用信息。

调优从哪里入手

  1. 减少YoungGC次数
  2. 减少YoungGC的耗时
  3. FullGC一天不超过6次
  4. 减少FucllGC耗时

syschronized 与 Lock区别?

2022面试遇到的问题总结_第1张图片

区别

  1. syschronized是一个关键字而Lock是一个接口
  2. 发生异常时: syschronized在发生异常的时候会自动释放锁,因此不会出现死锁,而Lock发生异常的时候不会主动释放锁,要手动unlock释放。(所以最好将同步代码块用try catch包起来,finally中写入unlock,避免死锁的发生。)
  3. 在等待状态中: Lock可以用interrupt来手动中断等待状态,而syschronized不可以手动中断,只能一直等待锁的释放。
  4. 是否获得锁的状态: Lock可以通过trylock来知道有没有获取锁,而synchronized不能。
  5. Lock可以提高多个线程进行读操作的效率。(可以通过readwritelock实现读写分离)。
  6. 性能方面: 如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。
    7.调度机制: synchronized可以使用Object对象本身的wait 、notify、notifyAll调度机制,而Lock可以使用Condition进行线程之间的调度。
//Condition定义了等待/通知两种类型的方法
Lock lock=new ReentrantLock();
Condition condition=lock.newCondition();
...
condition.await();
...
condition.signal();
condition.signalAll();

MySQL char与varChar区别?

char的长度是不可变的,varChar的长度是可变的
char默认占用10个字节,varChar最大的长度可达65,532字节
而在效率上面char比varChar更高

多线程实现方式等相关知识

多线程的实现方式

  1. 继承Thread类,重写run方法。
public class ThreadDemo01 extends Thread{
    public ThreadDemo01(){
        //编写子类的构造方法,可缺省
    }
    public void run(){
        //编写自己的线程代码
        System.out.println(Thread.currentThread().getName());
    }
    public static void main(String[] args){ 
        ThreadDemo01 threadDemo01 = new ThreadDemo01(); 
        threadDemo01.setName("我是自定义的线程1");
        threadDemo01.start();       
        System.out.println(Thread.currentThread().toString());  
    }
}
  1. 实现Runnable接口,重写run方法。
public class ThreadDemo02 {
 
    public static void main(String[] args){ 
        System.out.println(Thread.currentThread().getName());
        Thread t1 = new Thread(new MyThread());
        t1.start(); 
    }
}
 
class MyThread implements Runnable{
    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName()+"-->我是通过实现接口的线程实现方式!");
    }   
}
  1. 通过Callable和FutureTask创建线程。
public class ThreadDemo03 {
 
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
 
        Callable<Object> oneCallable = new Tickets<Object>();
        FutureTask<Object> oneTask = new FutureTask<Object>(oneCallable);
 
        Thread t = new Thread(oneTask);
 
        System.out.println(Thread.currentThread().getName());
 
        t.start();
 
    }
 
}
 
class Tickets<Object> implements Callable<Object>{
 
    //重写call方法
    @Override
    public Object call() throws Exception {
        // TODO Auto-generated method stub
        System.out.println(Thread.currentThread().getName()+"-->我是通过实现Callable接口通过FutureTask包装器来实现的线程");
        return null;
    }   
}
  1. 通过线程池创建线程。

线程池原理

什么是线程池?

就像一个鱼塘一样,鱼塘里面有很多条鱼,而这里的鱼就相当于线程。

为什么要建线程池?

  1. 首先,每一个线程都很浪费资源,我们不能频繁而且无限制的创建。
  2. 充分利用资源,将线程都放在一个池子里面,当有需要的时候就拿出来用,这样就可以重复利用线程。(因为创建与销毁线程都会消耗资源)

线程池怎么实现的?

  1. Executors.newCachedThreadPool():无限线程池。
  2. Executors.newFixedThreadPool(nThreads):创建固定大小的线程池。
  3. Executors.newSingleThreadExecutor():创建单个线程的线程池。
 public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue());
    }

// 看了底层的源码你就会发现,其实还是使用了ThreadPoolExecutor 来实现线程池的。

ThreadPoolExecutor 的核心参数

ThreadPoolExecutor(
		int corePoolSize, 
		int maximumPoolSize,
		long keepAliveTime,
 		TimeUnit unit, 
  		BlockingQueue workQueue,
   		RejectedExecutionHandler handler) 
  1. corePoolSize(线程池的默认大小): 创建线程池的时候默认创建多少个线程。
  2. maximumPoolSize(线程池的最大线程数)
  3. keppAliveTime(线程活动保持时间): 当线程池空闲的时候线程保持存活的时间。如果你的场景是任务很多,并且每一个任务执行的时间较短,那么你可以适当的调大这个参数。
  4. TimeUnit(keppAliveTime的单位): 可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
  5. BlockingQueue (存放任务的阻塞队列): 当线程池任务满了,这些新增过来的任务会先存储在这个队列里面。有以下四种:

ArrayBlockingQueue
//是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
LinkedBlockingQueue
//一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
SynchronousQueue
//一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
PriorityBlockingQueue
//一个具有优先级得无限阻塞队列。
  1. RejectedExecutionHandler(饱和策略也叫拒绝策略): 当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。有以下四种:
//1.当线程池的线程数等于于最大线程时,就会抛出 java.util.concurrent.RejectedExecutionException异常,涉及到该异常的任务也不会执行。
ThreadPoolExecutor.AbortPolicy();

//2.当线程池的线程数等于于最大线程时,后台静默的丢弃不能执行的任务,并不会报错。
ThreadPoolExecutor.DiscardPolicy();

//3. 当线程池的线程数等于于最大线程时,它会一直重试添加当前任务,并且一直调用execute()方法,直到成功为止。
ThreadPoolExecutor.CallerRunsPolicy();

//4. 当线程池的线程数等于于最大线程时,抛弃线程池中工作时间最久的任务,并执行当前任务。
ThreadPoolExecutor.DiscardOldestPolicy();

线程的五种状态

2022面试遇到的问题总结_第2张图片

如何关闭线程池?

  1. shutdown() : 停止接受新任务,然后先将队列里面的任务执行完之后再关闭线程池。
  2. shutdownNow() : 停止接受新任务,立刻中断所有任务,将线程池状态改为stop。

BeanFactory和FactoryBean的区别?

BeanFactory (一个可以根据需求生成bean的bean工厂)

  1. 它是IOC容器,并且提供方法支持外部程序对这些bean的访问,在程序启动时 根据传入的参数产生各种类型的bean,并且添加到IOC容器(实现BeanFactory接口的类)的singletonObject的属性中。
  2. 使用场景:
    通过名字或者类型从容器里面获取bean。
    判断容器中是否包含指定的bean。
    判断bean是不是单例的。

FactoryBean(一个工厂bean具有工厂模式的功能)

  1. 它是一个bean,也存在BeanFactory里面。但是它具有工厂方法的功能,可以在程序运行中 产生指定的一种类型的bean ,并且添加到IOC容器的factoryBeanObjectCache属性里面。
  2. 当在IOC容器中的Bean实现了FactoryBean后,通过getBean(String BeanName)获取到的Bean的对象并不是FactoryBean的实现类对象,而是这个实现类中的getObject()方法返回的对象。要想获取FactoryBean的实现类,就要getBean(&BeanName),在BeanName之前加上&。

区别

  1. 它们两个都是接口。
  2. FactoryBean是个Bean。
  3. 在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。
  4. 但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似

@SpringBootApplication注解

它是一个复合注解,它包括了
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan

三个注解

@SpringBootConfiguration

可以将某个类定义成一个配置类,在Spring IOC容器启动的时候将该类和该类中被@Bean标注的方法返回值加载到容器中。

@EnableAutoConfiguration

导入自动化配置类,主要就是解析类路径下META-INF/spring.factories文件中定义的自动化配置类,导入到spring ioc容器中。

@ComponentScan

主要就是将@SpringBootApplication标注的类所在的当前包及其子包中的被@Controller,@Service,@Repository,@Component这4个注解标注的类加入到spring容器中
excludeFilters属性就是排除哪些类不需要扫描并装配到spring容器中,源码中有两种类型的类不需要扫描装配到容器中,
一是:TypeExcludeFilter.class,如果用户需要将某个类不装配到spring容器中,即使这个类上面被标注上述4个注解中的某一个,就需要自定义类继承TypeExcludeFilter来实现具体的不装配规则。
二是:AutoConfigurationExcludeFilter.class,因为在@EnableAutoConfiguration注解中已经将自动化配置类装配到了spring容器中,所有在这里就不需要再次将这些自动化配置类扫描导入到容器中。

MySQL sql语句执行流程?

查询

2022面试遇到的问题总结_第3张图片

  1. 首先客户端会和数据库进行连接
  2. 然后会提交SQL到分析器里面,这里这个分析器会分析这条SQL的语法和语义有没有问题,你所访问的字段或表有没有权限等等,如果有错则返回错误,没有的话就会返回一个解析树。(解析树就是将这条SQL拆解为一个一个组件信息,如 select u.id,u.name from user as u where u.name=“张三” 它就会把这条SQL拆解为大概四五个组件,他会根据查询的信息,查询的表,查询条件进行拆解)
  3. 再然后就是在优化器里面对解析树进行查询优化,优化器会选择出一个最优的执行计划,让这个执行更快(可以使用explain来查看sql的执行计划)。如:explain select u.id,u.name from user as u where u.name=“张三”
  4. 然后就会将这个执行计划交给执行器来获取数据,最后返回数据。

SQL优化

2022面试遇到的问题总结_第4张图片

  1. 在整个SQL执行流程里面我们可以优化的就是在优化器的部分,其实在很多时候,我们执行一条SQL,它的执行计划是一样的,所以这个时候就可以将这里执行计划缓存下来,下次直接使用这个执行计划就行了,而不是要重新计算
  2. 其次就是对热点数据进行缓存,下次SQL进来的时候就会先到缓存区进行查找,符合条件则返回。

HashMap与ConcurrentHashMap的区别?

HashMap

  1. HashMap 的数据结构是:数组+链表+红黑树
  2. 非线程安全。
  3. 应用场景:高并发下进行put、remove操作成员变量的时候可能会产生线程安全问题,需要加锁,操作非成员变量不会产生线程安全问题,不用加锁。
  4. 扩容:当元素超过数组长度的0.75倍的时候就会进行扩容,每次扩容是原来的两倍。

ConcurrentHashMap

  1. ConcurrentHashMap的数据结构是:分段数组+链表+红黑树。
  2. 线程安全
  3. 性能比HashMap低。
  4. 扩容机制和HashMap一样。

redis缓存是怎么保证与MySQL的一致性的?

方案1:同步删除

当你更新数据到数据库的时候就同步删除缓存数据,但是会有几个问题。

  1. 当有并发场景的话就会存在脏数据。
  2. 更新数据的地方多很难保证所有地方都会同步删除缓存,其次会造成很大的冗余。
  3. 当你删除缓存失败的时候会存在脏数据。
  4. 当你不通过接口更新数据时也会产生脏数据。

2022面试遇到的问题总结_第5张图片

该例子中,由于线程2在查询完数据库数据之后,写入缓存之前,数据库的数据被线程1更新并且执行完同步删除操作了,所以最终导致脏数据问题,并且脏数据可能会持续很久。

当然,由于更新数据库操作耗时一般比写缓存更久,所以该例子发生的概率并不会太大,但还是有可能的。

最典型的场景就是,线程2查询完数据库之后,写缓存之前,线程2所在服务器发生了YGC,这个时候线程2可能就需要等待几十毫秒才能执行写缓存操作,这种情况就很容易出现上面这个例子了。

小结:由于难以收拢所有更新数据库入口,同时可能存在长期的脏数据问题,该方案一般不会被单独使用,但是可以作为一个补充,下面的方案会提到。

方案2:延时双删

在方案一的基础上删延时再删除一遍缓存,但是也会有问题:

  1. 延时的时间无法确定,万一太长了其中产生新的操作,或者太短了,都会产生脏数据。
  2. 延时无法绝对保证数据的一致性,当有主从数据库的时候,如下图。
    2022面试遇到的问题总结_第6张图片
    该例子中,由于数据库主从同步完成之间,存在并发的请求,从而导致脏数据问题,并且脏数据可能会持续很久。

可能有的人觉得稍微调大点延迟时间就可以解决这个问题,但是其实主库在写压力比较大的时候,主从之间的同步延迟甚至可能是分钟级的。

小结:由于延迟时间难以确认,同时无法绝对保障数据的一致性,该方案一般不会使用。

方案3:异步监听binlog + 重试

使用binlog来监听删除缓存,如果缓存删除失败则使用MQ进行不断重试删除。流程如下:
2022面试遇到的问题总结_第7张图片
该方案是当前的主流方案,整体上没太大的问题,但是极端场景下可能还是有一些小问题。

存在的问题:

  1. 脏数据时间窗口“较大”

    这个脏数据时间窗口较大,是相对方案一来说的。在你收到binlog之前,他中间要经过:binlog从主库同步到从库、binlog从库到binlog监听组件、binlog从监听组件发送到MQ、消费MQ消息,这些操作每个都是有一定的耗时的,可能是几十毫秒甚至几百毫秒,所以说它其实整体是有一个脏数据的时间窗口。

    而同步删除是在更新完数据库后马上删除,时间窗口大概也就是1毫秒左右,所以说binlog的方式相对于同步删除,可能存在的脏数据窗口会稍微大一点。(如果你在意这个问题甚至可以结合方案一来使用,这样,这个方案就会比较完美了)

  2. 极端场景下存在长期脏数据问题

    binlog抓取组件宕机导致脏数据。该方案强依赖于监听binlog的组件,如果监听binlog组件出现宕机,则会导致大量脏数据。还有就是拆库拆表流程中可能存在并发脏数据。

    我们来看下面这个例子:
    表A正在进行数据库拆分,当前进行到灰度切读流量阶段:部分读新库,部分读老库
    数据库拆分大致流程增量数据同步(双写)、全量数据迁移、数据一致性校验、灰度切读、切读完毕后停写老库。

    此时表A存在数据 a=1,并发情况下可能有以下流程
    2022面试遇到的问题总结_第8张图片
    该例子中,灰度切读阶段中,我们还是优先保障老库的流程,因此还是先写老库,由于写新库和写老库之间存在时间间隔,导致线程2并发查询到新库的老数据,同时在监听binlog删除缓存流程之后将老数据写入缓存,从而导致脏数据问题,并且脏数据可能会持续很久。

    双写的方式有很多种,我们使用的是通过公司的中间件直接将老库数据通过binlog的方式同步到新库,该方案通过监控发现在写压力较大的情况下,延迟可能会达到几秒,因此出现了上述问题。

    而如果是使用代码进行同步双写,双写之间的时间间隔会较小,该问题出现的概率会相对低很多,但是还是无法保障绝对不会出现,就像上面提过的,写老库和写新库2个操作之间如果发生了YGC或者FGC,就可能导致这两个操作之间的时间间隔比较大,从而可能发生上面的案例。

    还有就是代码双写的方式必须收敛所有的写入口,上文提到过的,通过命令行或者数据库管理平台的方式修改的数据,代码双写也是无法覆盖的,需要执行者在新老库都执行一遍,如果遗漏了新库,则可能导致数据问题。

小结:该方案在大多数场景下没有太大问题,业务比较小的场景可以使用,或者在其基础上进行适当补充。

最终方案:缓存三重删除 + 数据一致性校验 + 更新流程禁用缓存 + 强制读Redis主节点

这个方案也是我现在在我们项目中里面使用的方案,这里面有很多很多的思考,踩了各种坑之后不断优化而来的方案。该方案整体以方案3加上方案1作为主体,然后增加了各种优化。

整体方案如下:

  1. 更新数据库同步删除缓存

  2. 监听数据库的binlog异步删除缓存:带重试,保障一定会最终删除成功

  3. 缓存数据带过期时间,过期后自动删除,越近更新的数据过期时间越短,主要用于进一步防止并发下的脏数据问题,解决一些由于未知情况,导致需要更换缓存结构的问题

  4. 监听数据库的binlog延迟N秒后进行数据一致性校验,解决一些极端场景下的脏数据问题

  5. 存在数据库更新的链路禁用对应缓存,防止并发下短期内的脏数据影响到更新流程

  6. 强制读Redis主节点

  7. 查询异步数据一致性校验、灰度放量

整体流程图
2022面试遇到的问题总结_第9张图片
下面我们来细说各个方案点的设计初衷。

  1. 更新数据库后同步删除缓存

    这个同步删除缓存其实是为了解决我们上面说的那个异步binlog删除不一致时间窗口比较大的问题。更新完数据之后,我们马上进行一次同步删除,不一致的时间窗口非常小。

  2. 监听数据库的binlog异步删除缓存

    该步骤是整个方案的核心,也就是方案3,因为binlog理论上是绝对不会丢的,他不像同步删除存在无法收敛入口的问题。因此,我们会保障该步骤一定能删除成功,如果出现失败,则通过MQ不断重试。

通过前面两个方案点,我们其实已经保障了绝大多数场景下数据是正确的。

  1. 缓存数据带过期时间,过期后自动删除,越近更新的数据过期时间越短

    该策略的设计初衷,是因为我们前面讲的那些并发问题其实都是在存在并发更新跟一些并发查询的场景下出现的,因此最近刚刚更新过的数据,他出现不一致的概率相对于那种很久没更新过的数据来说会大很多。
    例如最近一个小时内更新的数据,我可能给他设计的过期时间很短,当然这个过期时间很多是相对于其他数据而言,绝对时间还是比较长的,例如我们使用的是一个小时

    这边是因为我们整体的请求量和数据量太大,如果使用的过期时间太短,会导致写缓存流量特别大,导致缓存集群压力很大。
    因此,如果使用该策略,建议过期时间一开始可以设置大一点,然后逐渐往下调,同时观察缓存集群的压力情况。
    该方案可以进一步保证我们数据的一个最终一致性。

    同时带过期时间可以解决另一个问题,如果你在缓存上线后发现缓存数据结构设计不合理,你想把该缓存替换掉。如果该缓存有过期时间,你不需要处理存量数据,让他到期自动删除就行了。如果该缓存没有过期时间,则你需要将存量数据进行删除,不然可能会占用大量空间。

  2. 监听数据库的binlog延迟N秒后进行数据一致性校验

    这个操作也是非常关键,方案3存在的问题就可以通过这个操作来解决掉。就如上面提过的,脏数据都是在更新操作之后的很短时间内触发的。

    因此,我们对每一个更新操作,都在延迟一段时间后去校验其缓存数据是否正确,如果不正确,则进行修复,这样就保障了绝大多数并发导致的脏数据问题。
    至于延迟多久,我个人建议是延迟几分钟,不能延迟太短,否则起不到兜底的效果。

  3. 存在数据库更新的链路禁用对应缓存

    在数据库更新的场景里面,我们可能会有一些查询操作。例如我更新完这个数据之后,我马上又查了一下。这个时候其实如果你去走缓存,很有可能是会存在脏数据。因为他更新完之后,马上读取这个间隔是非常短的。你的缓存其实可能还没有删除完,或者存在短期内的不一致,我们还没有修复。

    这种更新场景他对数据的一致性要求一般是比较高的。因为更新完之后,他要拿这个查询出来的数据去做一些其他操作。

    例如记录数据变更的操作日志。
    我把一个数据从a=1改成a=2,我在更新之前,我先查出来a=1,更新完之后我立马就去查出来a=2,这个时候我就记录一条操作日志,内容是a从1变成2。
    这种情况下,如果你在更新完之后的这个查询去走缓存,就有很大的概率查到a=1,这时候你的操作日志就变成a从1变成1,导致操作日志是错的。
    所以说这种更新后的查询,我们一般会让他不走缓存,因为他这个时效性就是太快了,缓存流程可能还没处理完成。

    这个方案点其实是借鉴了MySQL事务的设计思想,MySQL中,事务对于自己更新过的内容都是实时可见的。因此,我们这边也做了一个类似的设计。

  4. 强制读Redis主节点
    Redis跟MySQL一样,也会有主从副本,也会有主从延迟。当你将数据写入Redis后,马上去查Redis,可能由于查询从副本,导致读取到的是老数据,因此我们可以通过直接强制读主节点来解决这个问题,进一步增加数据的准确性。

    Redis 不像 MySQL 有主节点压力过大的问题,Redis 是分布式的,可以将16384个槽分摊到多个分片上,每个分片的主节点部署在不同的机器上,这样强制读主时,流量也会分摊到多个机器上,不会存在MySQL的单节点压力过大问题。
    2022面试遇到的问题总结_第10张图片

  5. 查询异步数据一致性校验、灰度放量
    这一步是缓存功能使用前的一些保障措施,保障缓存数据是准确的。

    对于查询异步数据一致性校验,我们一般在查询完数据库之后,通过线程池异步的再查询一次缓存,然后把这个缓存的数据跟刚才数据库查出来的数据进行比较,然后将结果进行打点统计。

    然后查看数据一致性校验的一致率有多少,如果不一致的概率超过了1%,那可能说明我们的流程还是有问题,我们需要分析不一致的例子,找出原因,进行优化。

    如果不一致的概率低于0.01%,那说明整个流程可能基本上已经没啥问题了。这边理论上一定会存在一些不一致的数据,因为我们查询数据库和缓存之间还是有一定的时间间隔的,可能是1毫秒这样,在高并发下,可能这个间隔之间数据已经被修改过了,所以你拿到的缓存数据和数据库数据可能其实不是一个版本,这种情况下的不一致是正常的。

    对于灰度放量,其实就是保护我们自己的一个措施。因为缓存流程毕竟还没经过线上的验证,我们一下全切到缓存,如果万一有问题,那可能就导致大量问题,从而可能导致线上事故。
    如果我们一开始只是使用几个门店来进行灰度,如果有问题,影响其实很小,可能是一个简单的事件,对我们基本没影响。
    在有类似比较大的改造时,通过灰度放量的方式来逐渐上线,是一种比较安全的措施,也是比较规范的措施。

小结:该方案的链路确实比较长,但是高并发下确实会有很多问题,因此我们需要有很多措施去保障。

当然,这个方案也不是一下子就是这样的,也是通过不断的实践和采坑才逐渐演进而来的。目前该方案在我们线上环境使用了挺长一段时间了,基本没有什么问题,整体还是比较完善的。

最后缓存和数据库一致性保障方案,目前网上的资料大多是类似于方案3,如果你能在面试中说出我给的这个方案,相信可以让面试官眼前一亮,这其实就是你的加分项,可以帮助你从众多候选人之中脱颖而出。

以上关于redis缓存一致性问题参考于@程序员囧辉的博客
参考原文链接:https://blog.csdn.net/v123411739/article/details/124237900

Kafka消息队列怎么解决消息不丢失以及重复消费问题?

消失丢失

2022面试遇到的问题总结_第11张图片

丢失的原因

  1. Kafka生产端异步发送消息后,不管broke是否响应,立即返回,伪代码:produce.send(msg),由于网络问题导致消息没有发送到broke端。
  2. Kafka发出的消息大小超出broke端接收的限制。

解决方法

  1. 生产者调用异步回调消息。生产者发送消息给broke端,broke端确认收到消息后发个回调给生产者确认。伪代码:produce.send(msg,callback)
  2. 生产者增加消息确认机制,设置参数为:acks = all ,意思是partition的leader副本接受到消息,等待所有的follower副本都同步完成之后,才认为本次生产者发送消息成功了。
  3. 生产者设置重试参数:retries>=3,增加重试次数保证消失不丢失。
  4. 定义本地消息日志表,当时任务扫描这个表自动补偿,做好监控警告。
  5. 后台提供一个补偿消息的工具,可以手动补偿。

重复消费

问题的产生

Kafka本身会有一个策略来避免重复消费问题,这个策略就是,Kafka 的Broker块上面存储的消息都会有一个offSet标记,然后Kafka的消费者是通过这个offSet这个标记来维护当前已经消费了的数据,然后消费者每消费一批数据Kafka Broker就会去更新offSet的一个值从而避免重复消费问题

默认情况下,消费者消费完这个消息的时候会自动提交offSet的值来避免重复消费。

Kafka消费端的自动提交逻辑里面会有一个默认的5S的一个时间间隔,意思是这个消息消费完成了,会在5S后的下一次向Broker去获取消息的同时提交offSet,所以在这5S内应用程序强制被kill掉或者是宕机的时候就会可能导致offSer没有被提交,从而当程序恢复后就会导致重复消费问题。

其次,Kafka里面会有一个叫Partition Balance的机制,这个机制的意思是把多个partition均衡的分配给多个消费者,然后消费者会从分配到的partition里面去消费消息,但是如果消费端在默认的5分钟内没有把这些分配到的partition全部消费完成,就会触发Kafka的Rebalance(重新分配)机制从而导致offset自动提交失败,而在重新Rebalance之后,消费端还是会从之前上一次提交的offset的位置开始消费,从而产生重复消费问题。

如何解决?

提高消费端消费消息的性能,从而避免触发Rebalance机制。

  1. 可以使用异步的方式来处理消息,缩短单个消息的消费时长。
  2. 调整消息处理的超市时间,比如把5分钟改为6分钟等。
  3. 减少一次性从Broker上拉去数据的条数。

使用幂等性方法

针对每条消息用md5进行加密,然后保存在MySQL或者redis里面,在消费信息之前去数据库里面查找判断是否已经被消费过。

redis穿透击穿雪崩的区别以及解决方法?

redis支持的数据类型(set zset String List Hash)

缓存击穿是什么?

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

怎么解决

  1. 设置热点数据永远不过期。
  2. 加分布式锁(第一次进去查到数据更新到缓存里面,后续的查找就会到缓存里面查找了)

缓存穿透是什么?

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

怎么解决

  1. 缓存空对象。
  2. 使用布隆过滤器
  3. 接口加鉴权

缓存雪崩是什么?

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是, 缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方法

  1. 建redis集群。
  2. 限流降级。
  3. 数据预热。
  4. 设置过期时间不同。

sql优化

  1. 对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
  2. 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描。
  3. 不要在索引列进行计算。
  4. 少用select * ,使用别名.字段。
  5. 用union all 代替 union。
  6. 正确建立索引。
  7. 尽量使用inner join,避免left join。
  8. 联合索引来说,如果存在范围查询,比如between、>、<等条件时,会造成后面的索引字段失效。
  9. 不建议使用%前缀模糊查询。
  10. explain 关键字来分析sql。

jvm内存模型

Java文件执行流程,首先将Java文件编译成为.class文件,然后在JVM里面转化为相对应的机器码运行。
2022面试遇到的问题总结_第12张图片

.class文件加载到JVM虚拟机,由类装载子系统加载到运行时数据区,然后在输出到执行引擎
2022面试遇到的问题总结_第13张图片

垃圾回收机制

  1. jvm通过引用计数法和可达性分析法 来判断是否需要回收。
  2. 通过标记-清除、标记清除整理、复制、分代 算法进行回收。

SpringMVC执行流程

浏览器发送请求到DisPatcherServlet
-----然后转发到HandMapping
-----然后由HandMapping返回的执行链去找的对应的Control
-----然后Control会返回一个视图解析器对象
-----然后去视图解析器找到view
-----最后渲染返回给前端

MySQL索引实现原理

索引是什么?

索引是帮助MySQL高效获取数据的排好序数据结构

索引的数据结构(B+Tree)

点这里可以跳转到数据结构的可视化工具

  1. 二叉树(key -value)key是值,value是当前值的行的内存地址
    当索引的数据结构是二叉树的时候,建索引的列的值不能是自增的,否则会导致索引失效,查询数据时间没有减少。(因为当索引列是自增的话,最终建成的二叉树就会是一个链表)如下图:
    2022面试遇到的问题总结_第14张图片

  2. 红黑树(自旋平衡二叉树)
    红黑树的底层逻辑与二叉树一样,都是小的在左边,大的在右边,但是它比二叉树多出了一个自动平衡功能。当你插入1到7的数据就不会生成链表,而是会自动平衡,如图。(jdk1.8后hashMap的数据结构就变成了数组与红黑树了),这样就没有二叉树的缺点了。但是,这样的数据结构的深度会有问题,当你数据量非常大的时候,你的磁盘io就会非常大,所以它不适用于大数据量的场景
    2022面试遇到的问题总结_第15张图片

  3. Hash表
    hash表的原理是(key - value)结构,key是索引列的字段的hash值,对应的value就是当前行的内存地址,它的优点就是不会像上面的一样要很大的磁盘io,它只需要计算(一次磁盘io)找到相应的hash值就可以找到目标行了。但是它有一个很大的缺点,就是当你的sql的条件不是等于,而是大于或小于这种查询范围的,这个索引就不会生效了

  4. B-Tree(红黑树的升级版)
    我们发现红黑树的一个很严重的问题就是深度,而B-Tree比红黑树的优点就是解决了这个深度的问题。
    问题的引出: 既然是因为深度的问题,那么我们思考,是什么原因造成的,怎么优化?
    首先最直接的原因就是: 红黑树的每一个节点只能存储一个元素
    思考: 那么我们想,一个节点能不能存储多个元素呢?比如十个百个上万个等。或者直接将全部的元素都放在一个节点上面,那么不管怎么查找,深度都只是1了。但是,如果你将所有的元素(索引+数据–目标所在行的内存地址)都放在一个节点里面,特别是数据量大的时候就会占用很大的空间,几乎是上百M甚至是G,而一次磁盘io加载到内存根本加载不到这么大的内存,即使可以加载得到,所耗费的时间空间又是一个成本,而且我们每次sql查询需要用到的数据都不足一张表的百分之二十,这样就会造成很大的内存空间的浪费,所以说一次性加载完所以元素是不现实的。
    并且MySQL限制了一个节点的大小是16kb

如图:B-Tree虽然解决了深度问题,但是仍然没有解决范围查找问题。

2022面试遇到的问题总结_第16张图片

  1. B+Tree(B-Tree的升级版)
    B+Tree相对于B-Tree的不同有:
    首先: B-Tree的每一个节点都会存放着数据与索引,而B+Tree则是在叶子节点才会存储数据与索引,非叶子节点只存储索引。
    其次: B+Tree的叶子点都会有一个连接着下一个叶子点的双向指针,这样就可以实现范围查找了。
    并且: 因为非叶子结点只存一个索引,这样一个节点就可以存储很多个元素,这个就是为什么索引可以支撑千万级数据量的快速查找了。

    如果索引使用bigint类型的话,一个索引就是8B然后指针是6B,一个节点最多放16kb,所以一个节点可以存放1170个索引数据,而叶子节点可以存放16个元素,那么(1170 x 1170 x 16 大约等于两千多万)
    还有就是,MySQL会把存有数据的叶子节点存在内存中,所以只需要进行两次io就可以找到目标了
    2022面试遇到的问题总结_第17张图片

联合索引和单值索引

联合索引就是一个表里面有多个索引列,单值索引只有一个索引列。

MySQL存储引擎有几种,区别是什么?

以下举例的是常用的存储引擎

存储引擎是表级别的

MyISAM: 索引文件和数据文件是分离的(非聚集索引)

MyISAM存储引擎的表,会有三种类型的文件如下图:
在这里插入图片描述
.frm: 存储表结构的相关信息。
.MYD: 存储表的数据。
.MYI: 存储表的索引。

MyISAM引擎查找数据的流程(比如找30): 首先是在B+Tree的索引上面找到30,然后获取它所对应的数据的内存地址0xF3,最后根据内存地址 (磁盘文件指针).MYD 文件里面找到相应的数据。

MyISAM 引擎底层存储数据的结构图2022面试遇到的问题总结_第18张图片

InnoDB: 索引文件和数据文件是同一个文件(聚集索引)

InnoDB存储引擎的表,只有两种类型的文件如下图:
在这里插入图片描述
.frm: 存储表结构的相关信息。
.ibd: 存储表的数据以及表的索引,.ibd=.MYI+.MYD

InnoDB引擎查找数据的流程(比如找20): 首先是在B+Tree的索引上面找到20,然后获取它所对应的叶子节点,由于它的索引以及数据都是存储在同一个文件里面,所以就不需要像MyISAM 一样再去数据文件找数据,而是直接就拿到数据了,这个也是MyISAM是有指针而InnoDB没有指针的原因

InnoDB 引擎底层存储数据的结构图
2022面试遇到的问题总结_第19张图片
这里引出两个面试题

什么是聚集索引?

聚集索引(InnoDB主键索引)就是索引以及数据存储在同一个文件里,非聚集索引就是分开,就像InnoDB与MyISAM一样。

为什么InnoDB必须要有主键,并且强烈推荐整型且自增的主键?

首先: 如果你没有给InnoDB类型的表设置主键,那么MySQL底层就会自动帮你计算选取一个可以唯一标识的列来充当主键,如果找不到则会生成一个隐藏的列来充当主键。
其次: 设置主键的根本原因是为了查找数据比较快。
最后: 相信大家都认识uuid,这里为什么建议使用自增并且是整型数据当主键:

  1. 为什么是要整型 ?

    是因为B+Tree存储或查找数据是根据大小来对比的,如果你使用uuid的是String类型的话又要转换为可比较大小的ASCII类型,其次String所占用的空间是比较大的,所以使用整型的优点是性能高节约并且存储空间

  2. 为什么要自增?

    我们都知道B+Tree的叶子节点都是从左到右是依次递增的,如果说你使用的不是自增的,那么当你的叶子节点里面有一个2和一个4这时你需要向里面插入一个3,这个时候就会需要重新分配索引以及分裂平衡树,但是如果是自增的,则永远都是在叶子节点后面追加就行了。

StringBuilder与StringBuffer的区别?

StringBuilder线程不安全(因为它底层的方法都是用synchronized 修饰的的),性能高
StringBuffer线程安全,性能低

sleep()与wait()方法的区别?

  1. 方法作用
    sleep:Thread类中定义的方法,表示线程休眠,会自动唤醒;
    wait:Object中定义的方法,需要手工调用notify()或者notifyAll()方法。
  2. 使用范围
    wait只能在同步代码块里面使用;
    sleep可以在任何地方使用。

线程的run()和start()有什么区别?

  1. start()方法是用来启动线程的,他不需要等待run方法体的代码执行完毕,可以直接继续执行下面的代码,而通过start()方法启动的线程处于一个就绪的状态
  2. run()是一个普通的方法。

请举出数据库索引失效的场景

  1. 使用不等于查询;
  2. NULL值;
  3. 索引列参与了计算或者函数;
  4. 在字符串like左边的通配符,比如: %xxx;
  5. 当MySQL分析全表扫描比使用索引快的时候不使用索引;
  6. 当使用联合索引,前面一个条件为范围查询,后面的即使符合左前缀原则,也无法使用索引;

nacos 服务注册与发现流程原理?

分布式事务原理?

你可能感兴趣的:(JAVA面试,面试,java,职场和发展)