编程导航八股文话题讨论

八股文

9 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

submit()和execute()的区别

submit()和execute()的区别 --------------------- `submit()`和`execute()`都是用来向线程池提交任务的方法。 **execute()** **是Executor接口中定义的方法,只能提交Runnable任务,没有返回值不能获取任务执行结果**,如果任务抛出异常,异常会由线程池的未捕获异常处理器处理。 源码体现 ```java public interface Executor { /** * Executes the given command at some time in the future. The command * may execute in a new thread, in a pooled thread, or in the calling * thread, at the discretion of the {@code Executor} implementation. * * @param command the runnable task * @throws RejectedExecutionException if this task cannot be * accepted for execution * @throws NullPointerException if command is null */ void execute(Runnable command); } ``` **submit() 是ExeCutorSevice 接口中定义的方法,可以提交Runnable任务以及Callable任务,返回**`**Future**`**对象,可以用来获取任务执行结果或取消任务,对于**`**Runnable**`**任务,返回的**`**Future**`**在任务完成后会返回null,**异常会被捕获并存储在`Future`中,只有在调用`Future.get()`时才会抛出 源码体现 ```java // ExecutorService 继承Executor public interface ExecutorService extends Executor { /** * Submits a value-returning task for execution and returns a * Future representing the pending results of the task. The * Future's {@code get} method will return the task's result upon * successful completion. * * <p> * If you would like to immediately block waiting * for a task, you can use constructions of the form * {@code result = exec.submit(aCallable).get();} * * <p>Note: The {@link Executors} class includes a set of methods * that can convert some other common closure-like objects, * for example, {@link java.security.PrivilegedAction} to * {@link Callable} form so they can be submitted. * * @param task the task to submit * @param <T> the type of the task's result * @return a Future representing pending completion of the task * @throws RejectedExecutionException if the task cannot be * scheduled for execution * @throws NullPointerException if the task is null */ <T> Future<T> submit(Callable<T> task); /** * Submits a Runnable task for execution and returns a Future * representing that task. The Future's {@code get} method will * return the given result upon successful completion. * * @param task the task to submit * @param result the result to return * @param <T> the type of the result * @return a Future representing pending completion of the task * @throws RejectedExecutionException if the task cannot be * scheduled for execution * @throws NullPointerException if the task is null */ <T> Future<T> submit(Runnable task, T result); /** * Submits a Runnable task for execution and returns a Future * representing that task. The Future's {@code get} method will * return {@code null} upon <em>successful</em> completion. * * @param task the task to submit * @return a Future representing pending completion of the task * @throws RejectedExecutionException if the task cannot be * scheduled for execution * @throws NullPointerException if the task is null */ Future<?> submit(Runnable task); } ``` 解释一下,上述三种的重载submit方法(); 第一种 ` <T> Future<T> submit(Callable<T> task);` 可以提交一个Callable任务,可以返回返回该线程的执行结果。 ```java Future<String> future = threadPool.submit(() -> { Thread.sleep(1000); return "Hello from Callable!"; }); System.out.println(future.get()); // 输出 "Hello from Callable!" ``` 第二种: <T> Future<T> submit(Runnable task, T result); 可以提交一个Runnable 任务,同时可以指定执行后的返回结果。我们知道Runnable是没有返回值的,有时候我们需要一个表示来判断程序是否执行完毕,就可以执行返回值 ```java Future<String> future = threadPool.submit( () -> { try { System.out.println("Processing data..."); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }, "SUCCESS" // 任务完成后返回这个字符串 ); System.out.println(future.get()); // 输出 "SUCCESS"(而不是 null) ``` 假设你有一个任务,它不返回计算结果(`Runnable`),但你仍然希望 `Future` 返回一个状态信息(如 `"SUCCESS"` 或 `"FAILED"`),而不是 `null`。这时就可以用这个方法。 第三种:Future<?> submit(Runnable task); 提交一个Runnable 任务,没有返回值。 ```java Future<?> future = threadPool.submit(() -> { System.out.println("Running a Runnable task"); }); future.get(); // 返回 null ``` 在分析一下异常处理的不同 在使用execute 提交任务并执行线程的时候,发生异常,如果没有被捕获,直接打印在控制台中 ```java public class ExecuteUncaughtException { public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); threadPool.execute(() -> { System.out.println("任务开始执行..."); throw new RuntimeException("这是一个未捕获的异常!"); // 未捕获的异常 }); // 后续任务可能不会执行(因为线程可能已终止) threadPool.execute(() -> System.out.println("后续任务")); threadPool.shutdown(); } } ``` 输出 ```plain 任务开始执行... Exception in thread "pool-1-thread-1" java.lang.RuntimeException: 这是一个未捕获的异常! at ExecuteUncaughtException.lambda$main$0(ExecuteUncaughtException.java:10) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) ``` execute处理异常的缺点。 * 异常如果没有被捕获,直接抛出,打印到控制台。 * **线程池中的线程可能因异常终止**,导致后续任务无法执行(特别是 `newSingleThreadExecutor` 只有一个线程时)。 再来分析一下submit,处理异常的方式 在使用submit 提交任务并执行线程的时候,发生异常,会存在FatureTask中,只要当ft.get()的时候,这个异常才会抛出. ```java import java.util.concurrent.*; public class SubmitUncaughtException { public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); Future<?> future = threadPool.submit(() -> { System.out.println("任务开始执行..."); throw new RuntimeException("这是一个未捕获的异常!"); }); try { //使用future.get,就在编译时必须处理异常 future.get(); // 在这里抛出 ExecutionException } catch (ExecutionException e) { System.err.println("捕获到 Future 中的异常: " + e.getCause()); // 获取原始异常 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 后续任务正常执行 threadPool.execute(() -> System.out.println("后续任务")); threadPool.shutdown(); } } ``` 输出 ```plain 任务开始执行... 捕获到 Future 中的异常: java.lang.RuntimeException: 这是一个未捕获的异常! 后续任务 ``` 上述,先要异常不影响,线程的执行,还是使用trycatch,处理异常了,才不影响线程的执行的。 那我使用execute,也捕获异常,不就和sumbit一样的了吗 ```java ExecutorService threadPool = Executors.newSingleThreadExecutor(); threadPool.execute(() -> { try { System.out.println("任务开始执行..."); throw new RuntimeException("这是一个捕获的异常!"); } catch (RuntimeException e) { System.err.println("捕获到异常: " + e.getMessage()); // 捕获并处理 } }); // 后续任务正常执行 threadPool.execute(() -> System.out.println("后续任务")); threadPool.shutdown(); ``` 输出 ```java 任务开始执行... 捕获到异常: 这是一个捕获的异常! 后续任务 ``` 结果显示,当使用execute时,捕获并处理异常和sumbit的执行过程时一样的。 那么为什么,那么多人说使用sumbit处理异常更加强大呢? 说比execute 处理异常强大的理由都是:execute 在执行中,如果异常未被捕获,就会直接打印在控制台,同时终止该线程的声明周期,submit 在执行过程中,出现异常会由Future 捕获,线程不会终止,这种说法,没啥逻辑可言,使用submit执行线程,线程没打断的原因是,trycatch 处理了异常,同样的在execute 使用try catch 处理异常,也可以做到相同的效果。 我觉得submit比execute 好用的原因,有两点:第一点**使用submit执行线程时,通过future.get()获取执行结果,需要显示的try catch,(为啥说显示的,原因就是future.get() 是受检时异常,也就是不try catch 在编译简短就报错),**其实这点也不算太强大,有好的编程习惯,以及熟练使用execute的人,使用execute 在线程中try catch.也可以达到相同的效果。**第二点:才是真正的强大之处,execute****异常信息无法向上传递,**`**execute()**` **的异常被消化后,调用方完全不知道任务失败了**(除非手动记录日志)。`**submit()**` **的异常可通过** `**Future.get()**` **传递给调用方**,适合需要统一错误处理的场景。 做一下总结,在实际开发的过程种,经常使用ExcutorSevice接口,因为其继承了Excutor接口,也就是可以ExcutorService 既可以使用execute方法和submit方法。不过submit更加强大,**支持返回值(甚至Runnable 也有预期的返回值)**,**更好的异常处理(execute** 如果 `Runnable` 任务中抛出未捕获的异常**异常会直接传播到线程池的未捕获异常处理器**(默认打印堆栈,但程序不会停止**)线程会因异常退出**,可能导致线程池中的线程减少,影响后续任务执行。 更多请看:[Java开发面试指南](https://www.yuque.com/hnsqls/rkzi78) 以及Github:https://github.com/hnsqls/interview 如果有用,感谢关注

JUC 面试知识总结

整理了一下之前面试,学习,总结的知识如果觉得有用的话可以关注一下我的的语雀文档 [https://www.yuque.com/g/hnsqls/rkzi78/ovpy9dgdnuw0yu04/collaborator/join?token=dVoOmnNfF0y47fLv&source=doc_collaborator#](https://www.yuque.com/g/hnsqls/rkzi78/ovpy9dgdnuw0yu04/collaborator/join?token=dVoOmnNfF0y47fLv&source=doc_collaborator#) 以及 Github:[https://github.com/hnsqls/interview](https://github.com/hnsqls/interview) 最近一个月的主要内容就是找工作面试,所以分享出来一是督促自己记录学习,二是希望产出点有用的东西,让大家共同学习进步。 1.并发与并行的区别 ---------- **并发(concurrent)是同一时间内执行**、 **并行(parallel)是同一时刻执行** > 对于单核cpu来说 * 单核CPU下线程实际还是串行执行的 * 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为 15 毫秒)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的 。 * 总结为一句话就是: 微观串行,宏观并行 一般会将这种线程轮流使用CPU的做法称为并发(concurrent)、 > 对于多核cpu来说 每个核(core)都可以调度运行线程,这时候线程可以是并行的。 2.创建线程的方式有那些? ------------- 总体来说有四种方法:继承Thread类、实现runnable接口、实现Callable接口、线程池创建线程。 继承Thread类 ```java public class MyThread extends Thread { @Override public void run() { System.out.println("MyThread...run..."); } public static void main(String[] args) { // 创建MyThread对象 MyThread t1 = new MyThread() ; MyThread t2 = new MyThread() ; // 调用start方法启动线程 t1.start(); t2.start(); } } ``` 实现Runnable接口 重写run方法,并将这个自定义类作为参数给Thread类 ```java public class MyRunnable implements Runnable{ @Override public void run() { System.out.println("MyRunnable...run..."); } public static void main(String[] args) { // 创建MyRunnable对象 MyRunnable mr = new MyRunnable() ; // 创建Thread对象 Thread t1 = new Thread(mr) ; Thread t2 = new Thread(mr) ; // 调用start方法启动线程 t1.start(); t2.start(); } } ``` 实现Callable接口 重写call方法,并利用Fast接口的FutureTask实现类接收线程返回的结果 。 ```java public class MyCallable implements Callable<String> { @Override public String call() throws Exception { System.out.println("MyCallable...call..."); return "OK"; } public static void main(String[] args) throws ExecutionException, InterruptedException { // 创建MyCallable对象 MyCallable mc = new MyCallable() ; // 创建F FutureTask<String> ft = new FutureTask<String>(mc) ; // 创建Thread对象 Thread t1 = new Thread(ft) ; Thread t2 = new Thread(ft) ; // 调用start方法启动线程 t1.start(); // 调用ft的get方法获取执行结果 String result = ft.get(); // 输出 System.out.println(result); } } ``` 通过线程池创建 ```java public class MyExecutors implements Runnable{ @Override public void run() { System.out.println("MyRunnable...run..."); } public static void main(String[] args) { // 创建线程池对象 ExecutorService threadPool = Executors.newFixedThreadPool(3); threadPool.submit(new MyExecutors()) ; // 关闭线程池 threadPool.shutdown(); } } ``` 3.线程的 run()和 start()有什么区别? -------------------------- start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次。 run(): 封装了要被线程执行的代码,可以被调用多次 4.新建 T1、T2、T3 三个线程,如何保证它们按顺序执行? ------------------------------- 在多线程中有多种方法让线程按特定顺序执行,你可以用线程类的**join**()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。 代码举例: 为了确保三个线程的顺序你应该先启动最后一个(T3调用T2,T2调用T1),这样T1就会先完成而T3最后完成 ```java public class JoinTest { public static void main(String[] args) { // 创建线程对象 Thread t1 = new Thread(() -> { System.out.println("t1"); }) ; Thread t2 = new Thread(() -> { try { t1.join(); // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t2"); }) ; Thread t3 = new Thread(() -> { try { t2.join(); // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t3"); }) ; // 启动线程 t1.start(); t2.start(); t3.start(); } } ``` 5.在 java 中 wait 和 sleep 方法的不同? ------------------------------ 共同点 * wait() ,wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃 CPU 的使用权,进入阻塞状态 不同点 * 方法归属不同 * sleep(long) 是 Thread 的静态方法 * 而 wait(),wait(long) 都是 Object 的成员方法,每个对象都有 * 醒来时机不同 * 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来 * wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等下去 * 它们都可以被打断唤醒 * 锁特性不同(重点) * wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制 * wait 方法执行后会释放对象锁,允许其它线程获得该对象锁(我放弃 cpu,但你们还可以用) * 而 sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃 cpu,你们也用不了) 6.notify()和 notifyAll()有什么区别? ----------------------------- notifyAll:唤醒所有wait的线程 notify:只随机唤醒一个 wait 线程 ```java public class WaitNotify { static boolean flag = false; static Object lock = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (lock){ while (!flag){ System.out.println(Thread.currentThread().getName()+"...wating..."); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"...flag is true"); } }); Thread t2 = new Thread(() -> { synchronized (lock){ while (!flag){ System.out.println(Thread.currentThread().getName()+"...wating..."); try { lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"...flag is true"); } }); Thread t3 = new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() + " hold lock"); lock.notifyAll(); flag = true; try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); t2.start(); t3.start(); } } ``` 7.volatile了解吗? -------------- volatile是java中的关键字。作用在变量上,目的是保证变量在多线程的可见性和禁止指令重排序。 可见性 使用volatile表示的变量,会在更新后,立刻刷新到主内存上(JMM的知识),其他的线程在读取变量时可以立刻获取最新的值,这样可以避免线程间由于缓存导致一致性的问题导致“看到旧数据”的现象。 禁止指令重排序 * `<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">volatile</font>`还禁止了指令重排序优化,这确保了代码的执行顺序与编写顺序一致。 * 在多线程编程中,指令重排序有时会导致意想不到的结果,因为线程的执行顺序可能与预期不同。通过`<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">volatile</font>`关键字,可以避免这种由于指令重排序引起的潜在问题。 8.AQS是什么 -------- 全称是 AbstractQueuedSynchronizer,即抽象队列同步器,它是构建锁或者其他同步组件的基础框架(工具)。比如ReentrantLock,Semaphore,CountDownLatch。 工作原理 锁的状态字段 `<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">private volatile int state</font>` 0表示无锁,1表示有锁。volatile 保证了多线程下state可见性。 在多线程的获取锁的情况下,通过CAS操作更改锁的字段state 0 -> 1,哪个线程成功修改了锁的state字段,就获得了锁的资源。其他线程在请求获取该锁,判断锁对象的state = 1,就无法获取锁。就在FIFO双向的等待队列里,当获得锁的线程释放锁时,同时唤醒FIFO的队列头元素,如果此时又来了新线程,那么新线程会和刚唤醒的线程争夺,通过cas操作确保获得锁的原子性。. 核心原理 AQS维护了一个 volatile state字段表示是否获被线程获取,和一个FIFO的双向队列来维护等待队列。并且是非公平锁。 9.死锁产生的原因以及排查 ------------- 死锁产生的原因是:两个线程或多个线程争夺临界资源的情况,而一直获取不到资源,这种情况是死锁。具体原因是一个线程获得了临界资源因为还需要其他资源目前无法执行完成,而一直占有临界资源,导致别的线程请求该临界资源不被允许,一直死锁。 ```java public class Mytest { static final Object A =new Object(); static final Object B =new Object(); public static void main(String[] args) { new Thread(()-> { synchronized (A) { System.out.println(Thread.currentThread().getName() + "获取A锁"); try { sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (B) { System.out.println(Thread.currentThread().getName() + "获取B锁"); } } },"t1").start(); new Thread(()-> { synchronized (B) { System.out.println(Thread.currentThread().getName() + "获取B锁"); try { sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } synchronized (A) { System.out.println(Thread.currentThread().getName() + "获取A锁"); } } },"t2").start(); } } ``` 那么怎么排查死锁的线程呢? 如果逻辑简单,就需要理获取锁的线程,是否可能会导致死锁。 还可以使用JDK提供的工具 jps 和jstack jps: 列出jvm中进程的执行,列出进程的标识和进行id jstack:jstack <PID>,列出该进程的详细信息 10.阻塞队列都有那些? ------------ workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务 比较常见的有4个,用的最多是ArrayBlockingQueue和LinkedBlockingQueue 1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO。 2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO。 3.DelayedWorkQueue :是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的 4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。 **ArrayBlockingQueue的LinkedBlockingQueue区别** > **ArrayBlockingQueue** 要指定容量大小。 ```java ArrayBlockingQueue<Runnable> arrayBlockingQueue = new ArrayBlockingQueue<>(10); ``` > LinkedBlockingQueue 可以不指定大小(不推荐),默认Integer.MAX_VALUE ```java LinkedBlockingQueue<Runnable> linkedBlockingQueue = new LinkedBlockingQueue<>(10); ``` ```java /** * Creates a {@code LinkedBlockingQueue} with a capacity of * {@link Integer#MAX_VALUE}. */ public LinkedBlockingQueue() { this(Integer.MAX_VALUE); } ``` | LinkedBlockingQueue | ArrayBlockingQueue | | --- | --- | | 默认无界,支持有界 | 强制有界 底层是链表 | 底层是数组 是懒惰的,创建节点的时候添加数据 | 提前初始化 Node 数组 入队会生成新 Node | Node需要是提前创建好的 两把锁(头尾) | 一把锁 | 左边是LinkedBlockingQueue加锁的方式,右边是ArrayBlockingQueue加锁的方式 * LinkedBlockingQueue读和写各有一把锁,性能相对较好 * ArrayBlockingQueue只有一把锁,读和写公用,性能相对于LinkedBlockingQueue差一些。 11.如何停止一个正在运行的线程? ----------------- 有三种方式可以停止线程 * 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止 * 使用stop方法强行终止(不推荐,方法已作废) * 使用interrupt方法中断线程 * 若打断阻塞的线程如wait,sleep,join,线程会抛出InterrupterException异常 12.synchronized关键字的底层原理? ------------------------ synchronized的基本使用 ```java public class TicketDemo { static Object lock = new Object(); int ticketNum = 10; public synchronized void getTicket() { synchronized (this) { if (ticketNum <= 0) { return; } System.out.println(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum); // 非原子性操作 ticketNum--; } } public static void main(String[] args) { TicketDemo ticketDemo = new TicketDemo(); for (int i = 0; i < 20; i++) { new Thread(() -> { ticketDemo.getTicket(); }).start(); } } } ``` synchronized关键字的底层原理是什么? 底层核心就是monitor。 monitor 是jvm中的对象。是实现synchronized的关键。 具体来说,一下例子 ```java public class Mytest { static final Object lock = new Object(); public static void main(String[] args) { synchronized(lock){ System.out.println("111"); } } } ``` 通过jdk提供的反编译工具 `javap -v SyncTest.class` 可以看出synchronized关键字的原理就是monitor的作用,`monitorenter`是加锁,`monitorexit`是解锁。为什么有两个`monitorexit`是因为synchronized隐式的使用的finally{},防止异常发生而不能解锁。 那么monitor是什么呢?Monitor 被翻译为监视器,是由jvm提供,c++语言实现。 在使用了synchornized代码块时需要指定一个对象,所以synchornized也被称为对象锁 monitor主要就是跟这个对象产生关联,如下图 Monitor内部具体的存储结构: * Owner:存储当前获取锁的线程的,只能有一个线程可以获取 * EntryList:关联没有抢到锁的线程,处于Blocked状态的线程 * WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程 具体的流程: * 线程进入synchorized代码块,先让lock(对象锁)关联的monitor,然后判断Owner是否有线程持有 * 如果没有线程持有,则让当前线程持有,表示该线程获取锁成功 * 如果有线程持有,则让当前线程进入entryList进行阻塞,如果Owner持有的线程已经释放了锁,在EntryList中的线程去竞争锁的持有权(非公平) * 如果代码块中调用了wait()方法,则会进去WaitSet中进行等待。 对象是怎么关联上的monitor? 这就要说一下对象的内存结构 在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充。 最重要的就是MarkWord * hashcode:25位的对象标识Hash码 * age:对象分代年龄占4位 * biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁 * thread:持有偏向锁的线程ID,占23位 * epoch:偏向时间戳,占2位 * ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位 * ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位 我们可以通过lock的标识,来判断是哪一种锁的等级 * 后三位是001表示无锁 * 后三位是101表示偏向锁 * 后两位是00表示轻量级锁 * 后两位是10表示重量级锁 如果使用 synchronized 给对象上锁(重量级)之后,**该对象头的Mark Word 中就被设置指向 Monitor 对象的指针**。 Monitor实现的锁属于重量级锁,你了解过锁升级吗? * Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。 * 在JDK 1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。 > 轻量级锁 在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。 ```java static final Object obj = new Object(); public static void method1() { synchronized (obj) { // 同步块 A method2(); } } public static void method2() { synchronized (obj) { // 同步块 B } } ``` **加锁的流程** 1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。 2.通过CAS指令将Lock Record的地址存储在对象头的mark word中(数据进行交换),如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。 3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。 4.如果CAS修改失败,说明发生了竞争,需要升级为重量级锁。 **解锁过程** 1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。 2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。 3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。 > 偏向锁 就一个线程使用,只有第一次使用 CAS 将**线程 ID** 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有 **加锁的流程** 1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。 2.通过CAS指令将Lock Record的**线程id**存储在对象头的mark word中,同时也设置偏向锁的标识为101,如果对象处于无锁状态则修改成功,代表该线程获得了偏向锁。 3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。与轻量级锁不同的时,这里不会再次进行cas操作,只是判断对象头中的线程id是否是自己,因为缺少了cas操作,性能相对轻量级锁更好一些 13.JMM是什么? ---------- JMM(Java Memory Model)Java内存模型,定义了**共享内存**中**多线程**程序读写的行为规范,通过这些规范对内存的读写操作保证指令的正确性。 1. 在JMM中,内存被划分为两个主要区域:主内存和工作内存。 1. **主内存**:是共享内存区域,所有线程都可以访问,用于存储共享变量。在Java中,堆和方法区是主内存的一部分。 2. **工作内存**:是线程私有的内存区域,每个线程都有一个独立的工作内存,用于存储线程的私有变量以及从主内存中复制的共享变量副本。在Java中,程序计数器、虚拟机栈和本地方法栈是工作内存的一部分。 2. 特征 3. **可见性**:指一个线程修改了共享变量的值后,其他线程能够立即看到这个修改。在JMM中,通过read、load、store和write四种原子操作来实现主内存和工作内存之间的数据交互,从而保证可见性。 4. **有序性**:指程序中的指令按照特定的顺序执行,前一个指令执行完毕,后一个指令才能执行。JMM通过一系列规则(如happens-before规则)来确保指令的有序性。 5. **原子性**:指一个操作是不可分割的,在执行期间不能被中断。JMM通过lock和unlock两种原子操作来确保原子性。当一个线程对共享变量进行加锁操作时,其他线程无法访问该变量,直到锁被释放。 14.CAS 是什么? ----------- CAS的全称是: Compare And Swap(比较再交换),将比较和交换封装成一个指令确保原子性。它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。 > 工作原理: * 比较(Compare): CAS会检查内存中的某个值是否与预期值相等。 * 交换(Swap):如果相等,将将内存中的值更新为新值。 * 失败重试:如果不想等,说明由其他线程已经修改了该值,CAS操作失败,一般会用重试(锁的自旋),直到成功。 > 举个例子: 在 JMM中,操作数据的过程 同时两个线程: 线程1:从主内存中取出数据a = 100,到线程1的工作内存,进行a++操作 线程2:从主内存中取出数据a = 100,到线程2的工作内存,进行a--操作 过程: 线程1 * 线程1拿A的值与主内存V的值进行比较,判断是否相等 * 如果相等,则把B的值101更新到主内存中 * 由于比较和交换是原子操作,确保了比较时候的数据是一样的但是交换时候数据发生改变(同时cas具体的体现,库存超卖,使用乐观锁 sql解决)。 线程2:从主内存中取出数据a = 100,到线程2的工作内存,进行a--操作 * 线程2拿A的值与主内存V的值进行比较,判断是否相等(目前不相等,因为线程1已更新V的值99) * 不相等,则线程2更新失败 * 自旋锁操作 * 因为没有加锁,所以线程不会陷入阻塞,效率较高 * 如果竞争激烈,重试频繁发生,效率会受影响 ```java //不断尝试 while(true){ int 工作内存中的A = 共享变量A; int 结果A = 工作内存中的A - 1; if(compareAndSwap(工作内存中的A,共享变量的A){ //相等就 赋值新结果 跳出循环。 //不相等就再次尝试 }) } ``` > CAS的底层实现 CAS 底层依赖于一个 Unsafe 类来直接调用操作系统底层的 CAS 指令。 <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/VTYwxSD1Kq64QeRD.webp" alt="" width="100%" /> 都是native修饰的方法,由系统提供的接口执行,并非java代码实现,一般的思路也都是自旋锁实现 在java中比较常见使用有很多,比如ReentrantLock和Atomic开头的线程安全类,都调用了Unsafe中的方法 ReentrantLock中的一段CAS代码 <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/jDcmVIluzRiPvdba.webp" alt="" width="100%" /> > CAS的优缺点 优点 * 无锁并发:没有使用锁,不会影响性能。 * 原子性:保证了线程安全。 缺点 * ABA问题:CAS操作中,一个变量的值从A变成B,又变回A,CAS无法检测到这种变化。可能导致错误 * 自旋开销:CAS操作通过自旋实现,可能导致CPU资源浪费 * 单变量限制:CAS操作仅适用于单个变量的更新 > ABA问题的解决 引入版本号或者时间戳,本次变化都改变版本号或时间戳,用版本号和时间戳来表示是否变化 14.ReentrantLock实现的原理 --------------------- ReentrantLock翻译过来是可重入锁。 ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。 构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。 查看ReentrantLock源码中的构造方法: <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/1WyomYbSHQr41fCn.webp" alt="" width="100%" /> 其中sync 是Sync类 继承了AQS接口。 NofairSync和FairSync 类继承了Sync类。 > 工作流程 <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/CzPsBXakIWZMnR6F.webp" alt="" width="100%" /> * 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功。 * 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部。 * 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程。 * 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁。 15.线程池的种类有哪些? ------------- 线程的创建和关闭会消耗大量的资源,同时业务如果需要很多线程,我们创建很多线程,但是单核cpu一次只能执行一个线程,创建大量的线程会消耗资源,而且还得不到cpu的控制权,所以业务中都是选择线程池。 在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有四种 > newFixedThreadPool 创建使用固定线程数的线程池 使用 ```java public class Mytest { static class FixedThreadDemo implements Runnable{ @Override public void run() { String name = Thread.currentThread().getName(); for (int i = 0; i < 2; i++) { System.out.println(name + ":" + i); } } } public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(5); for (int i = 0; i < 5; i++) { executorService.execute(new FixedThreadDemo()); sleep(10); } executorService.shutdown(); System.out.println("Author:hnsqls"); } } ``` 源码 ```java public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); } ``` 解释: 只有核心线程数,没有临时线程,阻塞队列是LinkedBlockingQueue,并且没有指定阻塞队列空间,那么默认是Integer.Value_MAX。 拒绝策略是默认策略,直接抛出异常。 > newSingleThreadExecutor 单线程化的线程池 ```plain ExecutorService executorService = Executors.newSingleThreadExecutor(); ``` 源码 ```java /** * Creates an Executor that uses a single worker thread operating * off an unbounded queue. (Note however that if this single * thread terminates due to a failure during execution prior to * shutdown, a new one will take its place if needed to execute * subsequent tasks.) Tasks are guaranteed to execute * sequentially, and no more than one task will be active at any * given time. Unlike the otherwise equivalent * {@code newFixedThreadPool(1)} the returned executor is * guaranteed not to be reconfigurable to use additional threads. * * @return the newly created single-threaded Executor */ public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); } ``` 解释: 核心线程数1,最大线程为1,阻塞队列是LinkedBlockingQueue且没指定空间。拒绝策略为默认策略。 > newCachedThreadPool 可缓存线程池 源码 ```java /** * Creates a thread pool that creates new threads as needed, but * will reuse previously constructed threads when they are * available. These pools will typically improve the performance * of programs that execute many short-lived asynchronous tasks. * Calls to {@code execute} will reuse previously constructed * threads if available. If no existing thread is available, a new * thread will be created and added to the pool. Threads that have * not been used for sixty seconds are terminated and removed from * the cache. Thus, a pool that remains idle for long enough will * not consume any resources. Note that pools with similar * properties but different details (for example, timeout parameters) * may be created using {@link ThreadPoolExecutor} constructors. * * @return the newly created thread pool */ public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); } ``` 解释 核心线程为0,最大线程数为MAX_Value. 阻塞队列是SychronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。 > ScheduledThreadPoolExecutor 提供了“延迟”和“周期执行”功能的ThreadPoolExecutor。 源码 ```java /** * Creates a new {@code ScheduledThreadPoolExecutor} with the * given core pool size. * * @param corePoolSize the number of threads to keep in the pool, even * if they are idle, unless {@code allowCoreThreadTimeOut} is set * @throws IllegalArgumentException if {@code corePoolSize < 0} */ public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS, new DelayedWorkQueue()); } ``` 解释 核心线程数自定义,最大线程数max_value 阻塞队列 DelayedWorkQueue。 > 扩展 不建议使用Executors创建线程池 阿里开发手册 <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/BiyPdJwdziqjIajO.webp" alt="" width="100%" /> 16.线程池的核心参数以及执行原理 ----------------- 参考ThreadPoolExecutor <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/4A4YjgqOTYK5XCDn.webp" alt="" width="100%" /> * corePoolSize 核心线程数目 * maximumPoolSize 最大线程数目 = (核心线程+救急线程的最大数目) * keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放 * unit 时间单位 - 救急线程的生存时间单位,如秒、毫秒等 * workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务 * threadFactory 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等 * handler 拒绝策略 - 当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略 > 工作流程 <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/SLs2hkFCDEpMosqR.webp" alt="" width="100%" /> 1,任务在提交的时候,首先判断核心线程数是否已满,如果没有满则直接添加到工作线程执行 2,如果核心线程数满了,则判断阻塞队列是否已满,如果没有满,当前任务存入阻塞队列 3,如果阻塞队列也满了,则判断线程数是否小于最大线程数,如果满足条件,则使用临时线程执行任务 如果核心或临时线程执行完成任务后会检查阻塞队列中是否有需要执行的线程,如果有,则使用非核心线程执行任务 4,如果所有线程都在忙着(核心线程+临时线程),则走拒绝策略 17.进程与线程的区别 ----------- **进程** 是操作系统中 **资源分配的基本单位**,代表一个**正在运行的程序**。每个进程都有 **独立的内存空间** 和 **系统资源**,不同进程之间相互独立。 * **独立性**:进程拥有独立的地址空间,一个进程的崩溃不会影响其他进程。 * **资源分配**:每个进程都有自己独立的 **内存、文件句柄、全局变量** 等资源。 * **进程间通信(IPC)**:由于进程相互独立,它们需要通过 **进程间通信**(如管道、消息队列、共享内存、Socket等)进行数据交换。 * **切换开销大**:进程切换涉及到资源回收和重新加载,开销较大。 **线程** 是 **CPU 调度的基本单位**,是 **进程中的执行流**。一个进程可以包含多个线程,它们**共享进程的资源(如内存、文件句柄)**,但**有自己独立的栈空间和程序计数器(PC)**。 **进程中的执行流** 解释:进程是程序的实体,程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。 * 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

Redis 面试知识总结

整理了一下之前面试,学习,总结的知识如果觉得有用的话可以关注一下我的的语雀文档 [https://www.yuque.com/g/hnsqls/rkzi78/ovpy9dgdnuw0yu04/collaborator/join?token=dVoOmnNfF0y47fLv&source=doc_collaborator#](https://www.yuque.com/g/hnsqls/rkzi78/ovpy9dgdnuw0yu04/collaborator/join?token=dVoOmnNfF0y47fLv&source=doc_collaborator#) 以及 Github:[https://github.com/hnsqls/interview](https://github.com/hnsqls/interview) 最近一个月的主要内容就是找工作面试,所以分享出来一是督促自己记录学习,二是希望产出点有用的东西,让大家共同学习进步。 1.Redis 缓存穿透问题 -------------- 缓存穿透:当请求的数据在缓存和数据库中不存在时,该请求就跳出我们使用缓存的架构(先从缓存找,再从数据库查找、这样就导致了一直去数据库中找),因为这个数据缓存中永远也不会存在。导致后续所有的这个请求(被恶意的人发现后)都会直接请求数据库。恶意用户一直发送该请求会导致数据库服务宕机。 解决方法常用的两种 一:缓存空数据,二,使用布隆过滤器进行校验。 > 缓存空数据 在数据库查询到不存在的数据时,对该数据进行缓存为空(可以设置稍短的3~5分钟的TTL),之后相同的请求,就会在缓存中查到,而不去请求数据库。 代码案列 ```java /** * 查询商户信息 * @param id * @return */ @Override public Result queryById(Long id) { //查询缓存 String string = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id); //hutool 工具类 符合条件“adc" 不符合条件“”,null, "/t/n" if (StrUtil.isNotBlank(string)){ Shop shop = JSONUtil.toBean(string, Shop.class); return Result.ok(shop); } //若是 " " 上面已经判断了不是“” 不是null , if(string != null){ return Result.fail("商户不存在"); } // 缓存不存在 查数据库 Shop shop = getById(id); if (shop ==null) { //将空值写入缓存 stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES); return Result.fail("商户不存在"); } //写入缓存 stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES); return Result.ok(shop); } ``` 优点 * 实现简单 缺点 * 缓存空值,会占用redis的内存空间(可以设置过期时间),可能会导致短期数据不一致问题(除非在新增数据的时候,删除redis中的数据)。 > 布隆过滤器 > 扩展 其实不仅仅是这两种解决缓存穿透的方案。 此外还可以 * 增强id的复杂性,避免id被猜到规律。 * 加强用户权限校验 * 做好热点数据限流。 此外,还应该做好数据的校验,对于一些不符合业务逻辑数据的请求直接拦截掉,不在请求数据库。 还可以采用对接口进行限流。甚至黑名单封禁。 2.Redis的哨兵模式 ------------ ``` 为了提高Redis的性能搭建主从集群后,当主节点出现问题,Redis服务就不可以进行写操作,服务就不可用,Redis提供了哨兵机制,来实现主从集群的自动故障恢复。 哨兵的结构和作用 ``` * 监控:Sentinel 会不断检查您的master和slave是否按预期工作。 * 具体来说就是Sentinel 一直发送ping,接收pang,说明该节点正常可用,反之就是主观下线,当多个Sentinel (一般是一半哨兵监控redis节点为不可用)检测一个redis节点都说明该节点不可用后,该节点是客观下线(服务不可用)。 * 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主。 * 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端。 * <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/QZZ98tqsYZiCY1Jj.webp" alt="" width="100%" /> > 补充 Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令: 主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。 哨兵选主规则 首先判断主与从节点断开时间长短,如超过指定值就排该从节点 然后判断从节点的slave-priority值,越小优先级越高 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高 最后是判断slave节点的运行id大小,越小优先级越高。 > 脑裂问题 当哨兵网络与Redis主节不在同一个网络下,监控就会出问题,但是Redis主节点并没有问题,服务仍在Redis主节点写,由于网络问题哨兵通过监控认为Redis主节点出现了问题,就会在从节点选一个做为主节点,这样就出现了两个主节点,这就是脑裂问题,当网络原因回复后,原来的主节点为变成从节点,以新的主节点为主,原来的主节点会同步新的主节点信息,就会导致数据丢失。 解决方法 redis.config ```shell min-replicas-to-write 1 表示最少的salve节点为1个 min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒 ``` 3.Redis 主从复制,同步流程 ----------------- ``` 单节点的Redis服务并不可靠,并发有上限。 ``` * 如果服务器发生了宕机,由于数据恢复是需要点时间,那么这个期间是无法服务新的请求的; * 如果这台服务器的硬盘出现了故障,可能数据就都丢失了。为了提高Redis 服务的可靠性,以及高性能,采用集群模式------主从复制。在主节点进行写操作,在从节点进行读操作。 <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/rvmDwvrHyXBoCz9n.webp" alt="" width="100%" /> ``` 具体的主从Redis节点的同步流程是这样子,分为首次同步,和增量同步。 ``` 首次同步,也就是全量同步 <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/LuJr1uZQrgwZqDUD.webp" alt="" width="100%" /> 增量同步 <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/Vu4kJ2C6fOINFShI.webp" alt="" width="100%" /> * Replication Id:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid * offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。 4.Redis持久化策略 ------------ ``` Redis是内存服务,但是提供了两个持久化策略AOF,RDB来持久化Redis的数据。 ``` > AOF 日志文件 Redis 每执行一条写操作命令成功后,就把该命令以追加的方式写入到一个文件里,然后重启Redis 的时候,先去读取这个文件里的命令,并且执行它,这不就相当于恢复了缓存数据了 <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/VyoRoLu0eaTEO2ZV.webp" alt="" width="100%" /> 在Redis中AOF持久化功能默认是不开启的,在redis.config文件中设置 <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/d4O5HS6ROW6jrAxJ.webp" alt="" width="100%" /> AOF的记录命令的频率可以通过redis.config文件来配置 ```shell # 表示每执行一次写命令,立即记录到AOF文件 appendfsync always # 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案 appendfsync everysec # 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘 appendfsync no ``` ![](https://pic.code-nav.cn/post_picture/1813946725418893314/9uDEDMrLdQT9vqxc.webp) 因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。 Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置: ```shell # AOF文件比上次文件 增长超过多少百分比则触发重写 auto-aof-rewrite-percentage 100 # AOF文件体积最小多大以上才触发重写 auto-aof-rewrite-min-size 64mb ``` > RDB RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据. Redis 服务默认开启。用户可以手动备份 ```shell redis -cli save # 由Redis主进程来执行RDB,会阻塞所有的命令 bgsave # 开启子进程执行RDB,避免主进程收到影响 ``` 当然Redis内部有自动触发RDB的机制,在redis.config中 ```plain # 900秒内,如果至少有1个key被修改,则执行bgsave save 900 1 save 300 10 save 60 10000 ``` ``` 这里提一点,Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。 所以可以认为,执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更 多。 通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失 5 分钟数据。 这就是 RDB 快照的缺点,在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能 太频繁,否则会影响 Redis 性能,而 AOF 日志可以以秒级的方式记录操作命令,所以丢失的数据就相对更少。 ``` > 二者比较 这两种技术都会用各用一个日志文件来记录信息,但是记录的内容是不同的。 * AOF 文件的内容是操作命令; * RDB 文件的内容是二进制数据。 RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作 命令的步骤才能恢复数据 5.Redis与数据库数据一致性问题 ------------------ ``` 为了提高查询效率引入了redis作为缓存,但是出现了新的问题就是,缓存中的数据和数据库中的数据不一致问题。 解决数据不一致问题的方法,最简单的就是对缓存数据设置较短的过期时间,在过期时间后,会从数据库查询新的数据更新缓存,但是这种被动的等待过期时间,一致性是不符合大部分应用场景的。 ``` 业内的解决方案 * Cache Aside Pattern 人工编码方式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案。 * Read/Write Through Pattern : 由系统本身完成,数据库与缓存的问题交由系统本身去处理。 * Write Behind Caching Pattern :调用者只操作缓存,其他线程去异步处理数据库,实现最终一致。 **通常使用第一种方案,在更新数据库的同时更新缓存(删除缓存)。** 如果采用第一个方案,那么假设我们每次操作数据库后,都操作缓存,但是中间如果没有人查询,那么这个更新缓存动作实际上只有最后一次生效,中间的更新动作意义并不大,我们可以把缓存删除,等待再次查询时,将缓存中的数据加载出来。 我们需要考虑一下几点 1. 在数据库更新时,缓存是更新还是删除? 1. 更新缓存:每次更新数据库都更新缓存,无效写操作较多。 2. 删除缓存:更新数据库时让缓存失效,查询时再更新缓存。**√** 2. 怎么确保数据库更新,缓存也更新(删除), 1. 单体系统,将缓存与数据库操作放在一个事务 2. 分布式系统,利用TCC等分布式事务方案 3. 先操作缓存还是先操作数据库? 应该具体操作缓存还是操作数据库,**我们应当是先操作数据库,再删除缓存**,原因在于,如果你选择第一种方案,在两个线程并发来访问时,假设线程1先来,他先把缓存删了,此时线程2过来,他查询缓存数据并不存在,此时他写入缓存,当他写入缓存后,线程1再执行更新动作时,实际上写入的就是旧的数据,新的数据被旧数据覆盖了。 ``` 先操作数据库在删除缓存,理论上也会出现问题,线程1查询在缓存中没有的数据,就会查询数据库将数据库查到的age =20,写入缓存,但在这时还未写入缓存,线程2,操作数据库age=21,删除缓存。此时线程1继续写入缓存age=20,又会出现不一致现象。但是在实际中并不太可能发生,因为写入缓存的时间是极快的。 - 先删除缓存,再操作数据库 - 先操作数据库,再删除缓存 ``` 6.Redis 缓存击穿 ------------ 缓存击穿是指在高并发的情况下,当某个**热点数据的缓存突然失效**(过期或被删除)并且**缓存重建业务较复杂**,大量请求直接穿透到后端数据库,导致数据库负载过高,甚至崩溃的问题。由于并发用户特别多,同时读缓存没读到数据,又去数据库中取数据,引起数据库压力瞬间增大。 解决方法:互斥锁构建缓存和逻辑过期时间 > 互斥锁构建缓存 在热点key失效后,加锁,确保只有一个线程查询数据库并构建缓存。其他的线程等待并重试在缓存中取值。 优点 * 一致性高 缺点 * 由于使用了锁,线程等待,性能低,还可以能出现死锁。 代码实现 ```java /** * 获取锁 * @param key * @return */ private boolean tryLock(String key){ Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 20, TimeUnit.SECONDS); return BooleanUtil.isTrue(flag); } /** * 释放锁 * @param key */ private void unlock(String key){ stringRedisTemplate.delete(key); } /** * 查询商户信息 缓存击穿互斥锁 * @param id * @return */ public Shop queryWithMutex(Long id){ String shopKey = CACHE_SHOP_KEY+ id; // 1. 从redis中查询店铺缓存 String shopJson = stringRedisTemplate.opsForValue().get(shopKey); //2.判断是否命中缓存 isnotblank false: "" or "/t/n" or "null" if(StrUtil.isNotBlank(shopJson)){ // 3.若命中则返回信息 Shop shop = JSONUtil.toBean(shopJson, Shop.class); return shop; } //数据穿透判空 不是null 就是空串 "" if (shopJson != null){ //返回错误信息 // return Result.fail("没有该商户信息(缓存)"); return null; } //4.没有命中缓存,查数据库 //todo :解决缓存击穿 不能直接查数据库。 利用互斥锁解决 /** * 实现缓存重建 * 1. 获取互斥锁 * 2. 判断是否成功 * 3. 失败就休眠重试 * 4.成功 查数据库 * 5 数据库存在该数据写入缓存 * 6 不存在返回错误信息并写入缓存“” * 7 释放锁 * */ //获取互斥锁 失败 休眠重试 String lockKey = "lock:shop" + id; Shop shop=null; try { boolean isLock = tryLock(lockKey); //获取锁失败 if (!isLock) { System.out.println("获取锁失败,重试"); Thread.sleep(50); return queryWithMutex(id);//递归 重试 } // 获取锁成功,再次检测缓存是否存在,存在就无需构建缓存,因为可能有的线程刚构建好缓存并释放锁,其他线程获取了锁 //检测缓存是否存在 存在 shopJson = stringRedisTemplate.opsForValue().get(shopKey); if (StrUtil.isNotBlank(shopJson)) { return JSONUtil.toBean(shopJson, Shop.class); } if (shopJson !=null){ return null; } // 缓存不存在 // 查数据库 shop = super.getById(id); Thread.sleep(200);//模拟你测试环境 热点key失效模拟重建延迟 if (shop == null){ //没有该商户信息 stringRedisTemplate.opsForValue().set(shopKey,"",CACHE_NULL_TTL,TimeUnit.SECONDS); return null; } //有该商户信息 stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+ id, JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new RuntimeException(e); } finally { unlock(lockKey); } return shop; } ``` > 逻辑过期时间 对热点key不设置过期时间,仅仅添加个expire 字段。 当使用该缓存时,根据过期字段判断是否更新缓存,若在期限内就直接使用改缓存,若不在期限内就更新缓存。 更新缓存的具体细节,利用锁确保一个线程重构缓存,防止数据库压力过大。在获得锁后,异步执行,新开线程执行重构缓存,同时原线程直接使用已经过期的数据。在此期间其他线程也发现缓存逻辑过期了,也会获得锁,但是获取锁失败,那就使用原来的老数据。 优点 * 性能高 缺点 * 数据不一致 代码实现 对类添加一个过期字段,为了满足开闭原则,可以自定义个新的类继承原来的类并添加expire字段,不过推荐如下写法 自定义个逻辑过期类,所有的逻辑过期类都可以使用(Object data 存原来的类)。 ```java /** * 逻辑过期类 */ @Data public class RedisData { private LocalDateTime expireTime; private Object data; } ``` 数据预热 ```java /** * 添加逻辑过期时间 * @param id * @param expireSeconds */ public void savaShop2Redis(Long id ,Long expireSeconds){ // 查询店铺数据 Shop shop = getById(id); //封装逻辑过期时间 RedisData redisData = new RedisData(); redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds)); redisData.setData(shop); //写入redis stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData)); } ``` 正式代码 ```java private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10); public Shop queryWithLogicalExpire( Long id ) { String key = CACHE_SHOP_KEY + id; // 1.从redis查询商铺缓存 String json = stringRedisTemplate.opsForValue().get(key); // 2.判断是否存在 if (StrUtil.isBlank(json)) { // 3.存在,直接返回 return null; } // 4.命中,需要先把json反序列化为对象 RedisData redisData = JSONUtil.toBean(json, RedisData.class); Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class); LocalDateTime expireTime = redisData.getExpireTime(); // 5.判断是否过期 if(expireTime.isAfter(LocalDateTime.now())) { // 5.1.未过期,直接返回店铺信息 return shop; } // 5.2.已过期,需要缓存重建 // 6.缓存重建 // 6.1.获取互斥锁 String lockKey = LOCK_SHOP_KEY + id; boolean isLock = tryLock(lockKey); // 6.2.判断是否获取锁成功 if (isLock){ CACHE_REBUILD_EXECUTOR.submit( ()->{ try{ //重建缓存 this.saveShop2Redis(id,20L); }catch (Exception e){ throw new RuntimeException(e); }finally { unlock(lockKey); } }); } // 6.4.返回过期的商铺信息 return shop; } ``` 7.内存淘汰策略(Redis内存满了怎么办) ---------------------- Redis中,在配置文件有设置`maxmemory`大小,当超过这个大小,Redis回触发内存淘汰机制,默认的淘汰策略就是`noeviction` Redis服务中提供的内存淘汰策略有八种 * `noeviction` :它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。 * `volatile-ttl`:优先淘汰更早过期的键值。 * `allkeys-random`: 对所有key随机删除。 * `volatile-random` :对设置ttl的key随机删除 * `allkeys-lru` :对所有最近最久使用的key随机删除 * `volatile-lru` : 对设置ttl并且最近最久使用的key随机删除 * `allkeys-lfu` : 所有使用最近最少使用的key随机删除 * `volatile-lfu` 设置ttl的并且最近最少使用的key随机删除 8.Redis 缓存雪崩 ------------ 缓存雪崩出现的原因是同一时间内大量的key同时失效或者Redis服务宕机,导致所有的请求都到数据库,数据库压力过大宕机。 根据产生雪崩的原因进行分析 > key同时失效导致的雪崩 我们在做缓存预热时和添加缓存时,设置有效期的同时,额外的增加(1~3)的随机过期时间。 同时当key过期后,构建缓存,利用互斥锁构建缓存,防止数据库压力过大。 > Redis服务宕机 利用Redis集群提高服务的可用性。 * 哨兵模式 * 集群模式 > 其他 给业务添加多级缓存, 如Guava或者Caffeine. 使用服务熔断或请求限流机制 我们可以启动**服务熔断**机制,**暂停业务应用对缓存服务的访问,直接返回错误**,不用再继续访问数据库,从而降低对数据库的访问压力,保证数据库系统的正常运行,然后等到 Redis 恢复正常后,再允许业务应用访问缓存服务。 但是这样在Redis服务宕机期间所有的业务都不可用,为了减少对业务的影响,可以启用**请求限流**机制,只将少部分请求发送到数据库, 更多的请求就只能拒绝服务,等Redis服务正常后并缓存预热完成在解除**请求限流机制**。 9.Redis 数据过期删除策略 ---------------- Redis 对 key 设置过期时间后,需要有相应的机制将已过期的键值对删除,而做这个工作的就是过期键值删除策略。 Redis的过期策略:惰性删除和定期删除相配合使用。 > 惰性删除 在设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。 * 优点 :对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查 * 缺点 :对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放 > 定期删除 redis中的一个定时任务(100ms)执行一次,扫描设置了过期时间的键并判断是否过期。 具体细节: redis并不会一次性扫描所有的设置过期时间的键,因为这样会浪费大量的过cpu资源,它会每次扫描时限制扫扫秒时间和数量,以免性能过大对redis正常的使用产生影响。 默认的话,每次获取20个key判断是否过期,如果过期的key占比超过25%,则继续拉20个,如果小于25%则停止。还有一次删除时间不能超过25ms,如果发现占比超过25%,就要判断目前是否花费了25ms,如果到时间也会结束。 定期清理有两种模式: * SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的hz 选项来调整这个次数 * FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms。 优缺点: * 优点:能有效释放过期键占用的内存,可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。 * 缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对 CPU 不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。 可以看到上面两种各自的优点,所以Redis使用惰性删除和定期删除两种策略相互使用,以求在合理的使用cpu和避免使用内存浪费之前取平衡。

Mysql 面试知识总结

整理了一下之前面试,学习,总结的知识如果觉得有用的话可以关注一下我的的语雀文档 https://www.yuque.com/g/hnsqls/rkzi78/ovpy9dgdnuw0yu04/collaborator/join?token=dVoOmnNfF0y47fLv&source=doc_collaborator# 以及 Github:https://github.com/hnsqls/interview 最近一个月的主要内容就是找工作面试,所以分享出来一是督促自己记录学习,二是希望产出点有用的东西,让大家共同学习进步。 1.Mysql的中从同步原理 -------------- 主从的模式 ![](https://pic.code-nav.cn/post_picture/1813946725418893314/DOrgtnliBd6PQx4t.webp) 主从同步的原理 ![](https://pic.code-nav.cn/post_picture/1813946725418893314/IQqaRSn6jGYoTzCL.webp) 核心就是binlog日志,Mysql的主库在事务提交时会有个binlog日志记录数据库的变化,从库有个线程IOthread去读去主库的binlog日志写入到从库的replylog日志中。然后从库的线程SqlThread执行relylog日志实现主从同步。 1. 主库开启binlog 日志 2. 主库的增删改操作,都会记录到binlog日志中。 3. 从库感知主库binlog的binlog日志的变化,每当发生改变,启用一个IO线程去读取主库binlog文件,并记录下来存在 Relay log(中继文件)中。 4. 最后利用Relay log 重放主库的bin log,完成数据同步。 tips: ```java 1. `bin log` 日志怎么开启? 2. 从库怎么感知主库的binlog 变化? 3. `bin log` 日志到底是什么,怎么能重放就能完成数据同步? ``` MySQL 的Binlog,它记录了所有的 DDL 和 DML语句,也就是记录了数据库中数据的变化。 `bin log`的应用 * 数据复制(主从同步)。 * 数据恢复(误操作恢复、灾难恢复)。 * 变更数据捕获(实时数据同步、数据异构)。 * 审计和日志分析 **Bin log 日志** 1. **如何开启binlog 日志** `MySQL 5. 7`默认情况下是不开启Binlog,因为记录Binlog日志需要消耗时间,官方给出的数据是有1%的性能损耗。最主要的是,早期阶段,那个时期往往是单机mysql。往往不需要进行数据复制,而且开启bin log 还要一些格外的配置 比如,设置 `server_id`:主从复制时,每个 MySQL 实例需要唯一的 `server_id`;选择 `binlog_format`:需要根据业务需求选择合适的 binlog 格式(STATEMENT、ROW 或 MIXED)。对于不熟悉 MySQL 的用户来说,这些配置可能会增加使用难度。 总的来说 `Mysql 5.7`默认不开启`binlog`是因为以下三个方面 1. 往往是单机mysql。往往不需要进行数据复制。 2. 而且开启bin log 还要一些格外的配置,对不熟悉Mysql 的用户来说,会导致使用难度的增加。 3. 性能消耗(在现在来看微乎其微) **MySQL 8.0 及以上版本**:binlog 默认是**开启**的。 可以通过sql 查看是否开启 ```sql SHOW VARIABLES LIKE 'log_bin'; ``` * 如果返回结果为 `ON`,则表示 binlog 已开启。 * 如果返回结果为 `OFF`,则表示 binlog 未开启。 如果 binlog 未开启,你可以通过以下步骤启用它: **修改 MySQL 配置文件** * 找到 MySQL 的配置文件(通常是 `my.cnf` 或 `my.ini`)。 * 在 `[mysqld]` 部分添加或修改以下配置: ```plain [mysqld] log_bin = /var/lib/mysql/mysql-bin.log # binlog 文件路径 server-id = 1 # 服务器唯一 ID(主从复制时需要) binlog_format = ROW # 推荐使用 ROW 模式 expire_logs_days = 7 # 设置 binlog 过期时间(单位:天) ``` * 配置说明: * `log_bin`:指定 binlog 文件的路径和名称。 * `server_id`:每个 MySQL 实例需要唯一的 ID(主从复制时必需)。 * `binlog_format`:binlog 的格式,推荐使用 `ROW` 模式(支持更精确的变更数据捕获)。 * `expire_logs_days`:设置 binlog 文件的保留时间,避免磁盘空间被占满。 * 重启服务, 再次验证是否开启 `SHOW VARIABLES LIKE 'log_bin';` **binlog 的格式** binlog 有三种格式,可以通过 `binlog_format` 参数设置: 1. **STATEMENT**: * 记录 SQL 语句。 * 优点:日志文件较小。 * 缺点:某些操作(如非确定性函数)可能导致主从不一致。 2. **ROW**(推荐): * 记录每一行数据的变更。 * 优点:精确记录数据变更,适用于数据同步和恢复。 * 缺点:日志文件较大。 3. **MIXED**: * 结合 STATEMENT 和 ROW 模式。 * 默认使用 STATEMENT,在某些情况下自动切换到 ROW。 可以通过以下命令查看当前 binlog 格式: ```shell SHOW VARIABLES LIKE 'log_bin'; ``` MySQL 的Binlog,它记录了所有的 DDL 和 DML语句,也就是记录了数据库中数据的变化。 `bin log`的应用 * 数据复制(主从同步)。 * 数据恢复(误操作恢复、灾难恢复)。 * 变更数据捕获(实时数据同步、数据异构)。 * 审计和日志分析 2.Mysql InnoDB和MyISMA 的区别 ------------------------- 1. InnoDB和MyISAM的区别 主要的区别: 2. **1数据的存储结构不一样** * **InnoDB**: * 数据和索引存储在同一个文件中(`.ibd` 文件)。支持聚簇索引(Clustered Index),索引和值放在一起(二级索引是聚簇索引吗)。 * **MyISAM**: * 数据(`.MYD` 文件)和索引(`.MYI` 文件)分开存储。不支持聚簇索引。 tips:数据的存储结构不一样,导致查询效率也不一样,**首先**,innoDB数据和索引在一个文件中,文件更大查询的较慢,MyISAM只存储索引,所以相同索引下,MyISAM使用的存储空间更少(页更少),查的页数就少,所以MyISAM查的比InnoDB快;**其次** 聚簇索引的存储方式,可能会发生回表(使用二级索引),而MyISAM不会发生回表,因为他的索引(叶子节点存储的是指向这个数据的物理地址,所以可以一下获得全部的信息,而不用回表) **2. 锁的级别不一样** * InnoDB * 支持行锁,锁的粒度较低,所以并发能力高,同时行锁的性能开销更大。 * MyISAM * 支持表锁,锁的粒度较高,所以锁发能力低。 tips:对一行数据修改,InnoDB会使用行锁,MyISAM会使用表锁,表锁的管理简单,只需要维护一个锁对象(整个表)行锁的管理复杂,需要为每一行维护锁信息。 **3. 事务支持** * InnoDB支持事务 * redo log (持久性), MVCC 和 锁(隔离性), undo log(原子性), 以上三种特性加上外键构成一致性。 * MyISAM 不支持事务 * 不支持事务,反而没有那么多性能消耗,所以时候读多写少的情况。 总结: ``` 以上特性总结,InnoDB 支持行锁,事务,(外键),索引和数据存放在一起(.idb文件)(可能会回表),能够承受更多的并发,并且发生错误有回顾机制,更加安全。 MyISAM,支持表锁,不支持事务,索引和数据单独存放(.myd和.myi),查询效率更高,但是不够安全。 ``` 3.Mysql的锁机制 ----------- > 全局锁 对整个数据库加锁,只允许事务的读操作,写操作会处于等待状态 ```sql flush tables with read lock; # 加锁 unlock tables; # 解锁,或者推出终端也会解锁 ``` 使用场景: 数据备份 ![](https://pic.code-nav.cn/post_picture/1813946725418893314/9MIGh11VWy2ytYeI.webp) 如果不加锁,就会出现数据不一致问题。因为在备份的时候,有业务导致数据库在备份的过程中发生变化,导致备份后数据不一致。 如何备份操作呢? 首先进入mysql 终端加锁 ```sql flush tables with read lock; # 加锁 ``` 新开一个mysql命令行,尝试修改数据,发现修改不了,一直阻塞状态。直到释放全局锁。 然后用mysql提供的工具 mysqldump(主语不是sql语句,直接在我们的电脑终端执行即可) ```shell C:\Users\26611>mysqldump -uroot -p studb > d:/studb.sql Enter password: **** ``` 缺点: 1. 对整个数据库加锁,数据库只能读,那么业务就不能写,会造成业务停滞。 ``` 2.对从库备份,使用全局锁,会导致在使用全局锁时,导致binlog日志文件不能写,会导致延迟。 ``` 使用全局锁,会影响业务写操作,那么怎么解决呢? 有些数据库引擎(Innodb)支持可重复读的事务隔离级别,在备份前开启事务会创建 Read View,备份期间业务仍可更新数据。可重复读下,即使其他事务更新,备份的 Read View 不受影响,确保了数据的一致性。使用 mysqldump 备份时,可加 –single-transaction 参数以适应支持此隔离级别的引擎,如 InnoDB。对于 MyISAM 这种不支持事务的引擎,备份时需使用全局锁。 > 表级锁 对表加锁,表锁,元数据锁,意向锁 表共享锁(Read Lock)的使用 ```sql use studb # 选择数据库 lock tables students read; # 对表加共享锁 unlock tables; # 解锁 ``` 共享锁,对表加共享锁,允许所有的事务可以读表,但是不能进行写操作;加锁的mysql终端,进行写操作,会报错,其他mysql终端写操作会阻塞。 排他锁(Write Lock) 的使用 ```sql use studb # 选择数据库 lock tables students write; # 对标加排他锁 unlock tables; # 解锁 ``` 排他锁,对表加排他锁,只有拿到排他锁的事务才可以对标读,写操作,其他事务会阻塞,直到释放锁(unlock tables 或者关闭加锁的终端)。 同时对表加共享锁和排他锁后,加锁的mysql终端不能对其他表操作。如下 ```shell mysql> lock tables students read; Query OK, 0 rows affected (0.00 sec) mysql> select * from test; ERROR 1100 (HY000): Table 'test' was not locked with LOCK TABLES ``` > 元数据锁 ``` 元数据锁(MDL)主要用于保护数据库表结构。当一个事务试图修改数据库的结构(如通过`ALTER TABLE`命令添加或删除列)时,它需要先获得相应对象的元数据锁。这种锁确保了在表结构被修改的同时,不会有其他事务尝试访问或修改该表的结构,从而避免数据不一致或错误。 MDL加锁过程是系统自动控制,无序显示使用,在访问一张表的时候会自动加上。在表上有活动事务的时候(也就是加上了元数据锁),当一个事务要修改表结构的时候,如果表上加了元数据锁,那么就会进入阻塞状态。 ``` > 意向锁 意向锁(Intention Lock)主要用于解决行级锁和表级锁之间的冲突,提高数据库的并发性和性能. 意向锁是一种表级别的锁,用于表明当前事务在表中行级锁的特性。它的主要目的是协调行锁和表锁之间的关系,避免锁冲突,从而提高数据库的并发性能。怎么理解呢? 在进行update 操作时会自动加上行级锁,然后其他事务对这个表加表锁,会扫描每一行检查行锁的类型,判断释放能加上表锁。但是有了意向锁之后,进行update 操作(行锁),会在表上加一个意向锁(IS和IX),其他事务在加表锁直接和意向锁比较判断是否能加锁。 1. **意向共享锁(IS Lock)**: * 当事务打算对某一行加共享锁时,它首先在表级加一个意向共享锁。 * 意向共享锁允许多个事务在表级别并发地读取数据,但不允许修改数据。 2. **意向排他锁(IX Lock)**: * 当事务打算对某一行加排他锁时,它首先在表级加一个意向排他锁。 * 意向排他锁意味着事务将在行级上加排他锁,其他事务不能在该行上加意向共享锁或意向排他锁。 **意向共享锁与共享锁可以兼容,而意向排他锁与排他锁是互斥的。** > 行锁 1. **Record Lock(记录锁)**:单独在记录上加锁,适合如主键记录更新等场景。 举个例子,当一个事务执行了下面这条语句: ```sql mysql > begin; mysql > select * from t_test where id = 1 for update; ``` 就是对 t_test 表中主键 id 为 1 的这条记录加上 X 型的记录锁,这样其他事务就无法对这条记录进行修改了。 当事务执行 commit 后,事务过程中生成的锁都会被释放。 ``` - 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容); - 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。 ``` 2. **Gap Lock(间隙锁)**:Gap Lock 称为间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生. 3. **Next-Key Lock(临键锁)**:Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。,锁定一个范围,并且锁定记录本身。也不能修改 id = 5 这条记录。所以,next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。 4.Mysql 默认的隔离级别为什么是RR --------------------- 首先,我们先从四种隔离级别中排除Serializable和Read Uncommitted这两种,主要是因为这两个级别一个隔离级别太高,一个太低。太高的就会影响并发度,太低的就有脏读现象。 RR 和 RC 的选择 在MySQL设计之处,他的定位就是提供一个稳定的关系型数据库。而为了要解决MySQL单点故障带来的问题,MySQL采用主从复制的机制。为了保证主从服务器之间的数据的一致性,就需要进行**数据同步**,大致的同步过程如下,简单理解就是主服务器把数据变更记录到bin log中,然后再把bin log同步传输给从服务器,从服务器接收到bin log之后,再把其中的数据恢复到自己的数据库存储中。 MySQL的bin log主要支持三种格式,分别是statement、row以及mixed。MySQL是在5.1.5版本开始支持row的、在5.1.8版本中开始支持mixed。 * statement: 这是MySQL最早支持的binlog格式。录的是SQL语句的原文。 * 优点:日志记录量相对较小,可以节约磁盘及网络IO,提升性能。 主从版本可以不一样,从服务器版本可以比主服务器版本高。 * 缺点: 可能导致主从同步的数据不一致问题。例如,使用DELETE或UPDATE时指定了LIMIT但没有使用ORDER BY,那么最终这条语句在主库和从库上的执行结果可能不一样。对于包含不确定操作或特定函数的SQL语句(如LOAD_FILE()、UUID()、USER()、FOUND_ROWS()、SYSDATE()等),可能无法被正确复制。 * MySQL从5.1.5版本开始支持row格式的binlog。记录每个数据更改的具体行的细节,即二进制日志中的每个条目都会详细列出发生变更的行的内容和修改。 * 优点: 可以避免MySQL复制中出现主从不一致的问题;对每一行数据的修改比Statement模式高效。在误删改数据后,同时无备份可以恢复时,通过分析binlog日志进行反向处理可以达到恢复数据的目的。 * 缺点:由于需要记录每一行的具体修改,可能导致binlog日志量增大,占用更多存储空间,增加网络传输负担;在复杂的回滚场景中,binlog中会包含大量的数据。 * MySQL从5.1.8版本开始支持mixed格式的binlog,是对Statement和Row两种格式的综合运用。MySQL会根据执行的具体SQL语句选择合适的日志记录方式。对于大多数常规SQL语句,MySQL会选择使用Statement格式记录binlog;当遇到在备库上直接执行原始SQL语句无法达到与主库相同效果的情况(如涉及不确定性的函数、存储过程、触发器等)时,MySQL会自动切换到Row格式,以确保复制的准确性。 ### 因为MySQL早期只有statement这种bin log格式,这时候,如果使用提交读(Read Committed)、未提交读(Read Uncommitted)这两种隔离级别会出现问题 ```sql CREATE TABLE t1 ( a int(11) DEFAULT NULL, b int(11) DEFAULT NULL, KEY a (a) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; insert into t1 values(10,2),(20,1); ``` 数据库记录 (10,2)(20,1) ![](https://pic.code-nav.cn/post_picture/1813946725418893314/lSGnLi13jvRIY832.webp) 以上两个事务执行之后,数据库里面的记录会变成(11,2)和(20,2). 以上两个事务执行之后,会在bin log中记录两条记录,因为事务2先提交,所以`UPDATE t1 SET b=2 where b=1;`会被优先记录,然后再记录`UPDATE t1 SET a=11 where b=2;`(statement格式的bin log记录的是SQL语句的原文) 这样bin log同步到备库之后,SQL语句回放时,会先执行`UPDATE t1 SET b=2 where b=1;`,再执行`UPDATE t1 SET a=11 where b=2;`。这时候,数据库中的数据就会变成(11,2)和(11,2)。这就导致主库和备库的数据不一致了!!! 为了避免这样的问题发生。MySQL就把数据库的默认隔离级别设置成了Repetable Read,那么,Repetable Read的隔离级别下是如何解决这样问题的那? 设置默认的隔离级别外,MySQL还禁止在使用statement格式的bin log的情况下,使用READ COMMITTED作为事务隔离级别。 一旦用户主动修改隔离级别,尝试更新时,会报错: 选择默认RR级别就是为了兼容历史上的那种statement格式的bin log。 5.Mysql 如何sql调优 --------------- 平时sql调优主要是通过explain分析慢sql。 可以从以下几个方面考虑 * 建表字段的选择 * 选择合适的字段 * 索引的设计 * 如果经常有多个条件查询尽量建立联合联合索引 * 在经常查询的条件或则排序分组的字段加索引 * 表的修改频率远大于查询频率,考虑是否要建立索引 * 索引不是越多越好,索引需要占用空间,其次增删改操作也会带来索引建立的额外开销,还可能导致页分裂 * sql语句的编写 * 避免 select * ,使用索引覆盖减少回表 * 避免索引失效,遵循最左原则;不使用like%x;不在索引字段操作或则类型转换;范围查询也会导致失效。 * 小表驱动大表 * 架构设计 * 中从复制,读写分离 此外,还可以利用缓存来优化,将一些访问频繁或者变化少的数据设置到缓存,减轻数据库的压力。 6.Mysql 如何分析慢sql ---------------- 我们通常会使用mysql自动的执行计划explain来去查看这条sql的执行情况。 首先如果本身已经添加了索引,查看key和key_len是否命中了索引,判断索引是否失效,如果失效就排查失效的原因。 其次可以通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描。 还可以通过extra建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复。 如下分析的字段 ![](https://pic.code-nav.cn/post_picture/1813946725418893314/DQl2Cx6xGUlGEKq2.webp) type字段的标识sql的性能(由高到低) * system : 查系统表 * const :查询的表只有一个匹配结果,通常就是主键或唯一索引查询,并且是常量。 * eq_ref:使用唯一索引查找单个匹配的行。因为索引是唯一的,所以优化器可以确定最多只有一个匹配的行。 * ref :使用非唯一索引或前缀索引来查找单个匹配的行。这通常发生在连接操作中,其中一个表的列与另一个表的索引列相关联。 * range:使用索引来检索给定范围内的行。例如,使用`BETWEEN`、`<`、`>`、`<=`、`>=`等条件时。这种访问方式通常比全索引扫描更高效,因为它只扫描索引的一部分。 * index:表示通过遍历整个索引来查找匹配的行。这通常比全表扫描要快,因为索引通常比表数据小。 * all :表示进行全表扫描来找到匹配的行。通常,这种访问方式效率较低,尤其是当表数据量很大时。 7.Mysql 索引的创建如何考虑 ----------------- 索引不是越多越好,索引不仅仅回占用空间,而且每次修改都要维护索引的数据结构消耗资源。所以创建索引要考虑是否要创建。 索引创建的前提是数据量大,数据查询效率低。 以下是创建索引的建议 1. 作为常用的查询条件建议添加索引 2. order by / group by /distinct 后字段考虑建立索引 3. 考虑联合索引,减少回表 4. 表的修改频率远大于查询频率,考虑是否要建立索引 8.Mysql使用了索引就一定有效吗?如何排查 ----------------------- 不一定有效。 以下场景就会导致索引失效 对于联合索引 * 违反最左匹配原则 * 使用了> < 查询 导致右边的索引失效 对于单个索引 * like %x 的使用 * 对索引进行运算(使用函数)以及类型隐式转化。 * order by 后面不是主键或者不是覆盖索引 * or 两者中要都是索引才能生效 索引失效的原因 总的来说就是在索引构建的时候都是有序的,在使用索引进行查询的时候如果不能和有序的索引结构匹配那就会导致索引失效。 可以使用EXPlAIN 来排查 1. 通过key和key_len看是否命中了索引 2. 通过type字段看sql是否由优化的空间,避免all,index 全盘扫描 3. 通过extra 看是否出现回表,若出现会表可以使用修改返回的所需字段或者使用联合索引。 9.Mysql 回表 ---------- 回表是指,使用非聚簇索引查询,由于非聚簇索引B+树叶子节点只存储索引值以及主键值,只能查到索引值以及主键值,如果需要其他字段则会根据主键值再去查表这个过程叫做回表。 回表不仅仅在查一次表,还会产生随机IO,因为使用非聚簇索引查到的id可能不唯一并且查到的id是无序的,所以会产生随机IO。 所以要尽量避免回表 * 不使用select * ,明确写出所需的字段 * 索引覆盖 * 索引下推 10.Mysql 为什么选择为使用B+ 树作为数据结构 --------------------------- 索引是为了高效获取数据的一种数据结构。 选择B + 树的原因有以下三点 * 查询效率稳定: 相较于二叉树而言,二叉树的最坏情况查询效率是O(N),而B+树是一种自平衡树,每个叶子节点到根节点的距离相同。查找效率O(log(N))。 * 树的高度不会增长过快,磁盘IO少:B+树是多叉树,不像红黑树数据越多高度增长越快;并且相较于B树比较,B+树的非叶子节点只存储页指针和索引值,所以相同页大小可以存更多的索引,使得磁盘IO减少。 * 范围查询快:B+树的叶子节点维护了一个双向链表。B+树在进行范围查询的时候,会根据索引先找到第一个元素,然后根据链表依次获取范围内的元素。 ![](https://pic.code-nav.cn/post_picture/1813946725418893314/SQFjzIsrXRrzy2N0.webp) 11.Mysql 最左匹配原则 --------------- Mysql 的最左匹配原则是指在使用联合索引时,查询条件必须从最左侧匹配,依次向右匹配。违反这个规则就会导致索引失效。 原因:因为联合索引的创建是从左到右有序的创建。 比如,创建(a,b)联合索引,查询条件是where a = 1,;where a = 1 and b = 2;都可以正常使用索引。但是where b = 2 就索引失效。 联合索引的结构 ```sql where a > 1, b = 2; ``` 原因:a先经过范围查询 查找到第一个大于a > 2 的元素,然后根据双向链表获得其他a>2的元素,获取的数据b不是有序的,所以要全部遍历判断一下b是否等于2。所以就导致了索引失效。 但是使用 >= , <= 索引 索引不失效 ```sql where a>=1,b = 2; ``` 原因: a = 1,b = 2 可以。使用索引查到这个数据,其他的a >1,b = 2 的数据由于在a不相等的情况下,b是无序的,所以还只能走(a)索引,而不是(a,b)索引。 12.Mysql存储引擎 ------------ mysql 支持的存储引擎很多可以查看。 ```sql show engines ``` ![](https://pic.code-nav.cn/post_picture/1813946725418893314/qtJBhjOLWZCf8q8V.webp) 上表的解释 * **Engine**: 存储引擎的名称。 * **Support**: 表示该存储引擎是否被MySQL支持(YES)或不支持(NO)。某些存储引擎可能因为特定版本的MySQL或特定的配置而不被支持。 * **Comment**: 对存储引擎的简短描述,解释了它的基本特性和用途。 * **Transactions**: 表示该存储引擎是否支持事务(YES)或不支持(NO)。事务是数据库操作的一个逻辑单元,它可以确保一系列操作的原子性、一致性、隔离性和持久性(ACID属性)。 * **XA**: 表示该存储引擎是否支持XA(eXtended Architecture)事务,这是一种分布式事务的规范,允许跨多个资源(如数据库)执行事务。 * **Savepoints**: 表示该存储引擎是否支持保存点(YES)或不支持(NO)。保存点允许在事务中设置一个点,可以回滚到这个点而不必回滚整个事务。 主要了解这三个引擎 一、InnoDB 1. **特点** * 支持事务:InnoDB提供了具有提交、回滚和崩溃恢复能力的事务安全,即ACID(原子性、一致性、隔离性、持久性)兼容的事务支持。 * 行级锁定:InnoDB支持行级锁定,避免了对整个表或大部分表的加锁,提高了并发性能。 * 索引数据结构: 底层是B+树,快速查找数据,降低磁盘IO(只在叶子节点存储数据),范围查询(叶子节点存储的数据维护一个双向列表) * 外键约束:InnoDB支持外键约束,确保了数据的完整性和一致性。 * 缓冲池:InnoDB拥有自己的缓冲池,用于在主内存中缓存数据和索引,提高了查询和写入速度。 * 崩溃恢复:InnoDB通过redolog来保证崩溃后的数据恢复,当数据库异常崩溃后,重新启动时会根据redolog进行数据恢复,保证数据库恢复到崩溃前的状态。 2. **适用场景** * 适用于需要高事务完整性和并发性能的应用场景,如电子商务网站、金融系统等。 二、MyISAM 1. **特点** * 不支持事务:MyISAM不支持事务处理和崩溃恢复功能,因此不适用于需要高事务完整性的应用场景。 * 表级锁定:MyISAM只支持表级锁定,并发性能较差,同时读操作会阻塞写操作,写操作也会阻塞读操作(但读操作之间不会相互阻塞)。 * 索引数据结构:和innodb一样采用了基于B+树的索引机制,但是在叶子节点存储的数据只是索引的值而非整行数据。 * 占用空间较小:MyISAM对数据的压缩和文件大小的管理相对简单,因此在数据管理方面能够占用较小的存储空间。 * 全文索引:MyISAM支持全文索引,适用于需要全文搜索的应用场景。 2. **适用场景** * 适用于读操作远远多于写操作的场景,如数据仓库、日志记录等。 三、MEMORY 1. **特点** * 数据存储在内存中:MEMORY存储引擎将数据存储在内存中,因此读写速度非常快。 * 不支持持久化存储:当MySQL服务器关闭或重启时,MEMORY引擎中的数据将丢失,因此不适用于需要长期保存数据的应用场景。 * 支持哈希索引:可以快速查询数据,但是不能范围查询。 * 表级锁定:MEMORY引擎使用表级锁定,可能导致并发性能问题。 * 受限于可用内存大小:由于数据存储在内存中,因此受限于可用的内存大小,如果表过大,可能无法完全缓存在内存中,导致性能下降。 2. **适用场景** * 适用于临时表、缓存表和高性能临时存储,如缓存数据等。 13.Mysql索引失效的场景以及原理 ------------------- 有以下表 ```sql CREATE TABLE `user` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `age` int NOT NULL, `phone` varchar(50) NOT NULL, PRIMARY KEY (`id`), KEY `idx_username` (`username`), KEY `idx_phone` (`phone`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ``` > 主键索引 id ![](https://pic.code-nav.cn/post_picture/1813946725418893314/ACu9STGI9BxADU3Z.webp) 索引的叶子节点存整行的数据。 > 二级索引 username ![](https://pic.code-nav.cn/post_picture/1813946725418893314/gQvnkgKgoaPoSmpg.webp) 二级索引的叶子节点存的值是主键的值。若使用二级索引查找,就只能找到该索引本身的值以及该数据的主键的值,若需要该行数据的其他数据,就会根据获得的主键值再去查找,也叫回表。 > 索引失效的场景 1. 使用模糊匹配 ```sql select * from user where username like '%李' select * from user where username like '%李%' select * from user where username like '李%' # 这个可以正常使用索引 ``` 可以使用explain 分析以上sql ![](https://pic.code-nav.cn/post_picture/1813946725418893314/3zNzTcn1lj6KkPbc.webp) * * * ![](https://pic.code-nav.cn/post_picture/1813946725418893314/FDMwhk4zzjLCiXFf.webp) * * * 为什么使用左模糊匹配会导致索引失效呢? 结合索引的结构就很容易想明白。索引的构建是先根据索引字段的值排序的,对于字符类型是字典序排序。要首先判断前面的字,才能够有序查找,而左模糊匹配找的是最后一个字匹配,索引就不能够使用索引找,而是全局扫描了一遍。 2. 对索引字使用函数 ```sql select *from user where length(username) = 6; ``` 失效的原因是:索引保存的索引字段的初始值,而不是经过函数计算后的值,自然就没法走索引。 3. 对索引进行表达式计算 ```sql select * from user where id + 1 = 10; ``` 失效的原因:索引保存的索引字段的值,而不是id+1后的值,所以无法走索引。 4. 对索引隐式的类型转换 ```sql # phone 是 vachar类型 select * from user where phone = 123456789; # 不走索引 select * from user where phone = '123456789'; # 走索引 ``` 失效的原因,mysql看到参数是整形数字,将phone改为int 类型。相当于 ```sql select * from user where CAST(phone AS signed int) = 123456789 ``` 索引构建存的是字符串类型,查询的时候将索引的类型变了。 5. 联合索引违反最左匹配原则联合索引,索引的构建是先根据左边的索引排序,在根据右边的索引排序。比如,创建(a,b)联合索引,查询条件是where a = 1,;where a = 1 and b = 2;都可以正常使用索引。但是where b = 2 就索引失效。联合索引的结构 ![](https://pic.code-nav.cn/post_picture/1813946725418893314/7f44cDVx4ftrmdM4.webp) 失效的原理:索引的构建是从左到右排序构建索引,不按照最左匹配原则查询,查询的条件就是无序了,也就无法走索引。 总的来说,索引失效的原理,都是通过条件无法二分查询值,就只能走全表扫描。判断索引是否失效不仅仅要要求我们编写sql的时候注意导致索引失效的sql的编写,还要使用explain查看sql的执行情况。 14.Mysql 三层B+树能存多少数据 -------------------- 要计算三层B+能存储多少数据,要清楚几个数据,数据页大小,索引值大小,指针大小,数据行大小。 Mysql数据存储的基本单位是页。默认大小是16kb。 ```sql show global status like 'innodb_page_size'; ``` | Variable_name | Value | | --- | --- | | innodb_page_size | 16384 | * 16KB = 16 * 1024 = 16384 字节 假设 数据行大小为 1Kb,索引大小8字节(bigint)指针6字节。 第一层 B+ 树 只存储指针和索引值所以第一层的结点 16384 / (8 + 6) = 1170 第二层 B+ 树 在第一层1170 的节点上再分 即 1170 *1170 = 1,368,900 第三层 B+ 树 存储数据行,1368900*16kb / 1kb = 21,902,400 大概可以存2000w条数据。 数据结构大概这样 ![](https://pic.code-nav.cn/post_picture/1813946725418893314/02pen6bX3GgrbOdi.webp) 15.Mysql的索引类型 ------------- 按照字段特性划分 * 主键索引 :修饰的字段唯一且为非null,通常是数据唯一的标识,同时在为主键构建索引时,索引结构的叶子结点存储整行的数据,查询效率较高。 * 唯一索引:修饰的字段唯一性约束可以为null。 * 普通索引:非主键索引非唯一索引。 * 联合索引:多个列共同组成的索引,适用于多个条件查询提升查询效率,联合索引的构建是有顺序的,索引查询的时候要遵循**最左匹配原则**,同时联合索引也适用于**索引覆盖**。 按照索引结构存储数据来分: * 聚簇索引 : 索引的叶子节点存储整行信息。 * 非聚簇索引 : 叶子节点只存储索引值信息。 按照索引数据结构来分 * B+树索引:是MySQL中一种常见的索引类型,适用于范围查询、排序操作和等值查询。InnoDB和MyISAM存储引擎都支持B树索引。 * Hash索引: 基于哈希表的数据结构,适用于精确匹配查询,但不支持范围查询或部分匹配查询。MEMORY存储引擎默认使用哈希索引,而InnoDB不直接支持,但可以通过覆盖索引来实现类似效果。 * 倒排索引 :基于倒排索引的数据结构,专门用于全文搜索,适用于需要进行复杂文本匹配的操作,如文章内容、评论等。InnoDB和MyISAM存储引擎都支持全文索引。 16.聚簇索引和非聚簇索引的区别 ---------------- **聚簇索引**:索引数据结构的叶子节点存储**整行的信息**,在一个表中**只有一个**聚簇索引,一般是主键/唯一键/隐藏row_id。 **非聚簇索引**:索引数据结构的叶子节点只存储主键值以及索引值。使用非聚簇索引查询的时候要注意**回表**,尽量避免select * 这种查询,并且添加联合索引,使得查询的列能在叶子节点全部获取。 补充: ``` **回表**:回表和聚簇索引和非聚簇索引是有关系的,回表的意思就是通过二级索引找到对应的主键值,然后再通过主键值找到聚集索引中所对应的整行数据,这个过程就是回表。 **索引覆盖**:覆盖索引是指select查询语句使用了索引,在返回的列,必须在索引中全部能够找到,如果我们使用id查询,它会直接走聚集索引查询,一次索引扫描,直接返回数据,性能高。 ``` 如果按照二级索引查询数据的时候,返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *,尽量在返回的列中都包含添加索引的字段。 17.MVCC 是什么? ------------ ``` MVCC 的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突。 它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undo log日志,第三个是readView读视图。 ``` > 隐藏字段 * DB_TRX_ID :最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID。 * DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本。 * DB_ROW_ID:隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段。 > undo log 日志 回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。 当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。 而update、delete的时候,产生的undo log日志不仅在回滚时需要,mvcc版本访问也需要,不会立即被删除。 在多个事务对一条记录修改的情况况下,undolog日志记录了版本链。 ![](https://pic.code-nav.cn/post_picture/1813946725418893314/PN4JgYsV1tPq3P0c.webp) > ReadView 读视图 ReadView(读视图)是 **快照读** SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。 * 当前读 :读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select ... lock in share mode(共享锁),select ... for update、update、insert、delete(排他锁)都是一种当前读。 * 快照读:简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。 ReadView的四个核心字段 * m_ids : 当前活跃的事务ID集合 * min_trx_id : 最小活跃事务ID * max_trx_id : 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的) * creator_trx_id : ReadView创建者的事务ID 版本链数据的访问规则 <img src="https://pic.code-nav.cn/post_picture/1813946725418893314/kVBcC3GdMgI5zp4Q.webp" alt="" width="100%" /> 不同的隔离级别,产生ReadView的时机不同。 * Read Committed:每次select,都生成一个快照读。 * Repeatable Read:开启事务后第一个select语句才是快照读的地方。

背诵八股文效率低怎么办

### 学习目标 希望在两个月里掌握java面试的八股文,尽量详细,已经有非常全的面试库可以供我学习 ### 个人情况 我是目前在职,上课有空余时间来学习 ### 学习内容 已经掌握了java基础和集合 ### 学习问题 继续往下学,io、jvm、多线程,后端框架等,但是发现我的效率太低,很难集中注意力去学习,而且,多线程、jvm我光看文档有点吃力,就很难往下进行,导致我的效率非常低。 ### 期望帮助 大佬们有啥好点的意见,让我取取经。 ### 相关资料 #小程序://Java面试库/xOhYv8021J6rJdE

Java基础八股吟唱 02_day

# Java基础八股吟唱 02_day - **hashCode的作用** - Java的集合有两类,分别是List和Set。前者存储的元素有序且可重复,后者存储的元素无序不重复。当我们在向Set集合中插入元素的时候,如何判断两个元素是否重复呢?很显然,调用equals嘛,但是这样最坏的情况下,时间复杂度是O(n)(即一个一个的比较,直到最后一个元素为止) - 于是乎,有人发明出了哈希算法来提高集合中元素的查找效率。这种方式将集合划分成若干个存储区域,每个对象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据对象的哈希码就可以确定该对象应该存储的那个区域了 - hashCode方法在Object基类中定义,自然,Java中的所有对象都会有一个hashCode方法。它返回一个根据对象内存地址换算出的一个值。这样一来,当集合要添加新元素是,判重的话就直接调用该对象的hashCode方法,就能定位到它应该放置的物理位置。如果这个位置已经有元素了,就调用它的equals方法进行比较,相同的话就不存了,不相同(即发生了哈希碰撞)就采取其他方式(再次进行散列,或者将这些元素用链表“串起来”)。这样一来实际低矮用equals方法的次数就大大降低了 - **String、StringBuilder和StringBuffer的区别** - String是只读字符串,是一个不可变对象。从底层源码上看是一个final类型的字符数组,所引用的字符串不能被改变,每次对String对象的操作都会生成新的String对象 - 每次+操作:隐式在堆上new了一个和原字符串相同的StringBuilder对象,再调用append方法拼接+后面的字符 - StringBuffer和StringBuilder他们都继承了AbstractStringBuilder抽象类,从AbstractStringBuilder抽象类源码中我们可以得知它们的底层都是可变的字符数组,所以在进行频繁的字符串操作时,建议使用StringBuilder和Stringbuffer来进行操作 - StringBuffer 对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的 - **ArrayList和LinkedList的区别** - ArrayList可以看做是一个自动扩容的数组,当然,本质上是用一个Object类型的数组进行存储的,该数组的默认大小是10。达到容量上限后想要扩容,会创建一个大小是原来1.5倍大小的新数组,然后将原数组的内容copy过去,改变内部数组引用指向新创建的数组。知道索引查询效率是O(1),否则是O(n)。插入和删除的效率不高(最坏为O(n)) - LinkedList是一个双向链表,在添加和删除元素时具有比ArrayList更好的性能(O(1)),但是get和set方面弱于ArrayList。当然,这些都是建立在数据量较大的前提下。其实对于LinkedList有一个小优化:get方法如果气索引大于size()/2的话,会从尾端开始查找元素(LinkedList也支持索引查找的,不过不推荐,使用一种数据结构之前应当利用该数据结构的优势,而不是去做它本就不擅长的事情) - **HashMap和HashTable的区别** - 父类不同 - HashMap是继承自AbstractMap类,而Hashtable是继承自Dictionary类 - 不过,他们都实现了Map,Cloneable,Serializable这三个接口 - 对外提供的接口不同 - Hashtable比HashMap多提供了elments() 和contains() 两个方法 - elments() 方法继承自Hashtable的父类Dictionnary。elements() 方法用于返回此Hashtable中的value的枚举 - contains()方法判断该Hashtable是否包含传入的value。它的作用与containsValue()一致。事实 上,contansValue() 就只是调用了一下contains() 方法 - 对null的支持不同 - Hashtable:key和value都不能为null - HashMap:key可以为null,但是这样的key只能有一个,因为必须保证key的唯一性;可以有多个 key值对应的value为null - 安全性不同 - HashMap是线程不安全的,在多线程并发的环境下,可能会产生死锁等问题,因此需要开发人员自 己处理多线程的安全问题 - Hashtable是线程安全的,它的每个方法上都有synchronized 关键字,因此可直接用于多线程中 - 虽然HashMap是线程不安全的,但是它的效率远远高于Hashtable,这样设计是合理的,因为大部 分的使用场景都是单线程。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap - ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为 ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定 - 初始容量大小和每次扩充容量大小不同 - 计算hash值的方法不同 - **Collection和Collections的区别** - Collection是集合类的上级接口,子接口有 Set、List、LinkedList、ArrayList、Vector、Stack、 Set - Collections是集合类的一个帮助类, 它包含有各种有关集合操作的静态多态方法,用于实现对各种 集合的搜索、排序、线程安全化等操作。此类不能实例化,就像一个工具类,服务于Java的 Collection框架

Java基础八股吟唱 01_day

# Java基础八股吟唱 01_day - **八种基本数据类型的大小,以及他们的包装类** - int是基本数据类型,Integer是int包装类,是引用类型。int的默认值是0,而Integer默认值是null,所以Integer能区分出0和null的情况。一个对象变量的值为null表示改变变量没有引用任何对象,使用未进行引用赋值的对象变量是一件很危险的事情,很容易跑出罪恶的空指针异常 - 基本数据类型在声明时,系统会自动给它分配空间,而引用类型声明时,只是分配了引用空间,必须通过实例化开辟数据空间之后才可以赋值。数组对象也是对象,所以某些时候不要进行直接赋值哦,除非你有这需求 - Java虚拟机中没有任何供boolean值专用的字节码指令,Java语言表达式所操作的boolean值,在编译之后都使用Java虚拟机中的int数据类型来代替,而boolean数组将会被编码成Java虚拟机的byte数组,每个boolean元素占8位。这样我们可以得出boolean类型单独使用是4个字节,在数组中又是1个字 节。使用int的原因是,对于当下32位的处理器(CPU)来说,一次处理数据是32位(这里不是指的 是32/64位系统,而是指CPU硬件层面),具有高效存取的特点 - **instanceof** 关键字的作用 - instanceof 严格来说是Java中的一个双目运算符,用来测试一个对象是否为某一个类的实例或者其父类的实例 - 编译器会检查 **被检测对象** 是否能转换成**instanceof**右边的class类型,如果不能转换则直接报错,如果不能确定类型,则通过编译,具体看运行时定 - **自动装箱与拆箱** - 概念 - 装箱就是自动将基本数据类型转换为包装器类型(**int-->Integer**);调用方法:**Integer**的 **valueOf(int)** 方法 - 拆箱就是自动将包装器类型转换为基本数据类型(**Integer-->int**);调用方法:**Integer**的 **intValue**方法 - 缓冲池 - 面试题 ~~~java public class Main { public static void main(String[] args) { Integer i1 = 100; Integer i2 = 100; Integer i3 = 200; Integer i4 = 200; System.out.println(i1==i2); // true System.out.println(i3==i4); // false } } ~~~ 原因在于Integer类中有一个缓冲池,一定范围内的整数会复用包装类对象。编译器会**在缓冲池范围内的基本类型**自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。Integer缓冲池的大小默认为-128~127。很明显200超出了这个范围 ~~~java public class Main { public static void main(String[] args) { Double i1 = 100.0; Double i2 = 100.0; Double i3 = 200.0; Double i4 = 200.0; System.out.println(i1==i2); // false System.out.println(i3==i4); // false } } ~~~ 很显然嘛,这个没有所谓的缓冲池,为什么呢?原因: **在某个范围内的整型数值的个数是有限的,而浮点数却不是** - 基本类型对应的缓冲池 - boolean —— true、false - byte —— 其能表示的值都是 - short、int —— -128~127 - char —— \u0000 ~ \u007F - **重载和重写的区别** - 重写**(Override)** - 发生在父类与子类之间 - 方法名,参数列表必须相同,返回值类型可以是父类中该方法返回值类型的子类 - 访问修饰符要大于等于父类中定义的访问权限 - 重写的方法不能抛出新的检查型异常或者比被重写方法更加宽泛的检查型异常 - 重载(**Overload**) - 发生在同一个类中 - 方法名相同,参数个数、顺序、类型其中一个或多个不同 - 与返回值无关 - **equals**与**==**的区别 - **==** - 基本类型比较的是值是否相等 - 引用类型比较的是其引用地址是否相等,即两个对象是否相同,或者说两个对象变量是否引用同一对象 - 两边的操作数必须是同一类型的(可以是父子类之间)才能编译通过 - **equals** - equals用来判断两个对象的状态是否一致,即两个对象的字段值是否相等(当字段值是引用时,应该调用其对应对象的equals方法) - 所有的类如果不明确的声明所继承的类,那么都会继承Object基类,Object基类中的定义的equals方法使用的是“==”,所以我们需要进行equals判断的时候,必须重写equals方法 - **"=="为true,equals一定为true(相同必相等),equals为true,“==”不一定为true(相等不一定相同)**

鱼皮老师您好,我这段时间在背诵

鱼皮老师您好,我这段时间在背诵八股文,但是感觉不是很顺畅,我主要有两个来源背诵: 一个就是看哔哩哔哩上面的面试题,直接背题目; 一个就是看javaguide上面的文章,兼顾知识点和面试题。 但是我现在经常遇到一个情况,就是在背诵的时候,觉得视频上给的答案有一些小错误(事实上确实有),然后我又去javaguide、CSDN上面搜索比对这个问题,但是有时候答案并不一致,甚至于说出入很大。 而且还有个情况就是guide上面的文章,他常常会冗余的讲解那个面试题,有些面试题没有一个面试题模板。 这两个情况导致于我每天背的题大量减少了,因为有些题是热门题,我就去反复核实,但是又是众说纷纭,导致我反复修改自己的笔记。 而且还有一个问题就是,之前我背的题也可能出现这种问题(背的内容里面有一些错误的地方),这样反反复复核实就很难受,我就想找一个比较标准的面试题文档,然后按部就班记忆学习。 总结就是两个问题: 一是自己整理笔记非常难以统一,网上好多各说各的,有些问题又是热门题,不得不重视。 二是想找到一个很标准的文档,问题加回答即可,不需要多余的知识点。 请问鱼皮老师有什么建议吗?

【柒夭八股】Fail-safe 机制和 Fail-fast 机制分别有什么作用?

<html> <head></head> <body> <div class="content ql-editor"> <p>柒夭八股 Day01</p> <p>今天来说一下,Fail-safe 机制和 Fail-fast 机制分别有什么作用?</p> <p>Fail-safe 和 Fail-fast 机制都是操作集合的时候出现的一种失败处理机制,现在我们分别来说一下这两种机制:</p> <ol> <li data-list="bullet"><span class="ql-ui"></span>Fail-fast:表示快速失败,在集合使用迭代器遍历的过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException 异常,从而导致遍历失败,如下图所示:</li> </ol> <p><br></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1627889630378479618/m8onjfb5.jpeg"></p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><span style="color: rgb(8, 15, 23);">Fail-safe,表示失败安全,也就是在这种机制下,出现集合元素的修改,不会抛出以上集合修改时失败出现的</span>ConcurrentModificationException 的异常。其原因如下,采用安全失败机制的集合容器,其在遍历的时候不是直接在集合内容上进行访问的,其是先复制原有集合的内容,在拷贝的集合上面去进行遍历,由于迭代时是对原集合的拷贝进行遍历的。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对于原集合所作的修改并不能被迭代器检测到,如下图所示:</li> </ol> <p><br></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1627889630378479618/9hqx4zft.jpeg"></p> <p><br></p> <p>在这里我们定义了一<span style="color: rgb(8, 15, 23);">个 CopyOnWriteArrayList 的集合,在对这个集合使用迭代器进行遍历的时候,我们对于集合中的元素进行修改,此时并不会抛出异常,但同时不会打印出增加的元素。</span></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><span style="color: rgb(8, 15, 23);">总结</span></li> <li data-list="ordered"><span class="ql-ui"></span> 在 <strong><u style="color: rgb(86, 120, 149);"><a href="http://java.util/" target="_blank">Java.util</a></u> </strong>包下的集合类都是<strong>快速失败机制</strong>的,常见的使用 Fail-fast 方式遍历的容器有 HashMap 和 ArrayList 等。</li> <li data-list="ordered"><span class="ql-ui"></span><strong style="color: rgb(8, 15, 23);"><u style="font-family: inherit;font-style;font-variant-ligatures;font-variant-caps;">J<a href="http://java.util.concurrent/" target="_blank" style="">ava.util.concurrent</a></u></strong><span style="color: rgb(8, 15, 23);"> 包下的容器都是</span><strong style="color: rgb(8, 15, 23);">安全失败</strong><span style="color: rgb(8, 15, 23);">的,可以在多线程下并发使用,并发修改。</span>然后常见的 Fail-Safe 方式遍历的容器有 ConcerrentHashMap 和 CopyOnWriteArrayList 等。</li> </ol> <p><br></p> <p><br></p> </div> </body> </html>

Java 集合框架高频面试题(43 道)

#八股文# 学习 #打卡 准备了很久的集合八股文今晚熬了个大夜,终于完成了[流泪][流泪],里面一共有43道集合的高频面试题(24 道Map 的题目),里面很多地方使用了图解帮助你理解,还有一道题,据说是快手常问的手写 HashMap,这里面我也自己进行了简单地实现,不过不是红黑树的实现哈(小生功力还不及鱼总那么雄厚),就是简单的数组加上链表的方式,感兴趣的同学可以自行观看,然后观看过程中如果发现错误了,还请各位多多指教,在此先谢谢大家了[抱拳][抱拳],然后感兴趣的同学可以 look 一下 语雀:https://www.yuque.com/zeovo-10k9s/lqwlrb/ig8g5o2853ofn9nn?singleDoc#xMGEx 掘金:https://juejin.cn/post/7284158980112384035?share_token=79b8ea75-296e-4227-9cd4-c713a3afb305 PDF 版本如下:

下载 APP