diff --git a/docs/JUC/JUC.md b/docs/JUC/JUC.md index d6c4199..fc9f03f 100644 --- a/docs/JUC/JUC.md +++ b/docs/JUC/JUC.md @@ -141,9 +141,9 @@ ObjectMonitor() { ### 锁膨胀 -锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。 - +锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁,目的是为了提高获得锁和释放锁的效率。 +当一个线程访问同步块时设置为偏向锁,偏向锁存在锁竞争时升级为轻量级锁,轻量级锁自旋失败后升级为重量级锁。 #### 偏向锁 @@ -297,13 +297,11 @@ Java内存模型规定了所有的变量都存储在主内存中。每条线程 `volatile、synchronized和final`关键字保证可见性。 - - **有序性** 如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程, 所有的操作都是无序的,指“指令重排序”现象和“工作内存与主内存同步延迟”现象。 -Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性。 +在Java中使用了happens-before原则来确保有序性。 @@ -312,7 +310,7 @@ Java语言提供了volatile和synchronized两个关键字来保证线程之间 当一个变量被定义成volatile之后,它将具备两项特性: 1. 保证此变量对所有线程的可见性。而普通变量并不能做到这一点,普通变量的值在线程间传递时均需要通过主内存来完成。当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存;当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效,从主内存中读取共享变量。 -2. 禁止指令重排序优化,普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。 +2. 禁止指令重排序优化。普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。 @@ -356,6 +354,10 @@ volatile重排序规则: +volatile关键字在Java中主要通过内存屏障来禁止特定类型的指令重排序。 + + + synchronized 无法禁止指令重排和处理器优化,为什么可以保证有序性可见性? 1. 加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的 @@ -465,7 +467,7 @@ public class Singleton { | Runnable(运行) | Java线程将操作系统的就绪和运行状态合并。调用了 t.start() 方法 | | Blocked(阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态 | | Waiting(等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒。在这种状态下,线程将不会消耗CPU资源 | -| Timed Waiting (限期等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait | +| Timed Waiting (限期等待) | 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。 | | Teminated(终止) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 | ![](./JUC/线程状态.jpeg) @@ -626,7 +628,7 @@ Thread 类 API: | join() | 等待线程结束。调用某个线程的 `join()` 方法,当前线程会等待该线程执行完成。 | | | join(long millis) | 等待这个线程结束,最多 millis 毫秒,0 意味着永远等待 | | | wait() | 当前线程进入等待状态,直到被 `notify()` 或 `notifyAll()` 唤醒。必须在同步块或同步方法中调用。 | | -| notify() | 醒一个正在等待该对象监视器的线程。被唤醒的线程会进入 Runnable 状态,但不会立即获得锁。 | | +| notify() | 醒一个正在等待该对象监视器的线程。被唤醒的线程会进入 Runnable 状态,但不会立即获得锁。 | hotspot对notofy()的实现并不是我们以为的随机唤醒,,而是“先进先出”的顺序唤醒 | | notifyAll() | 唤醒所有正在等待该对象监视器的线程。 | | @@ -1182,7 +1184,7 @@ ReentrantLock 相对于 synchronized 具备如下特点: 1. 锁的实现:synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的 2. 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同 3. 使用:ReentrantLock 需要手动解锁,synchronized 执行完代码块自动解锁 -4. **可中断**:ReentrantLock 可中断,而 synchronized 不行。这里的可中断是指在获取锁的过程中,可以被取消获取锁的请求。 +4. **可中断**:ReentrantLock 可中断,而 synchronized 不行。这里的可中断是指线程在等待锁的过程中,可以被其他线程中断而提前结束等待。 5. **公平锁**:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。ReentrantLock 可以设置公平锁或不公平锁,synchronized 中的锁是非公平的 6. **锁超时**:在指定的截止时间之前获取锁,如果截止时间到了仍无法获取锁,则返回。 7. 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象,更细粒度的唤醒线程 @@ -2622,14 +2624,14 @@ Future 接口有 5 个方法,它们分别是取消任务的方法 `cancel()` ### 关闭方法 -可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。 +可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,它只能设置线程的中断标志。线程需要在执行中显式地检查这个标志,或者在阻塞操作(如 `sleep()`、`wait()`、`join()` 等)时捕获 `InterruptedException`,才能真正响应中断信号。 ExecutorService 类 API: | 方法 | 说明 | | ---------------------- | ------------------------------------------------------------ | -| void shutdown() | 将线程池的状态设置成 SHUTDOWN,然后中断所有没有正在执行任务的线程。 | -| List shutdownNow() | 将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表 | +| void shutdown() | 将线程池的状态设置成 SHUTDOWN,正在执行的任务会继续执行下去,没有被执行的则中断,不能再往线程池中添加任何任务。 | +| List shutdownNow() | 将线程池的状态设置成 STOP,试图停止所有正在执行的线程,不再处理还在池队列中等待的任务,返回那些未执行的任务。 | | boolean isShutdown() | 调用 `shutdown()` 或 `shutdownNow()` 方法后返回为 true。 | | boolean isTerminated() | 当所有的任务都已关闭后,才表示线程池关闭成功,返回为 true | @@ -3396,3 +3398,7 @@ private void resize() { `rehash()`首先是会进行探测式清理工作,从`table`的起始位置往后清理。清理完成之后,`table`中可能有一些`key`为`null`的`Entry`数据被清理掉,所以此时通过判断`size >= threshold - threshold / 4` 也就是`size >= threshold * 3/4` 来决定是否扩容。 扩容后的`tab`的大小为`oldLen * 2`,然后遍历老的散列表,重新计算`hash`位置,然后放到新的`tab`数组中,如果出现`hash`冲突则往后寻找最近的`entry`为`null`的槽位,遍历完成之后,`oldTab`中所有的`entry`数据都已经放入到新的`tab`中了。 + + + +## CompletableFuture diff --git a/docs/JVM/JVM.md b/docs/JVM/JVM.md index 718fac4..6fd5b70 100644 --- a/docs/JVM/JVM.md +++ b/docs/JVM/JVM.md @@ -363,9 +363,9 @@ protected synchronized Class loadClass(String name, boolean resolve) throws C -## 运行时数据区 +## 运行时数据区 -![](./JVM/运行时数据区.png) +![](./JVM/JVM (2).png) ### 程序计数器 @@ -509,13 +509,7 @@ Java服务端程序开发时,**建议将-Xmx和-Xms设置为相同的值**, ### 方法区 -方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。 - -方法区是《Java虚拟机规范》中设计的**虚拟概念**,是一块逻辑区域,主要包含三部分内容: - -- 类的元信息,保存了所有类的基本信息 -- 运行时常量池,保存了字节码文件中的常量池内容 -- 字符串常量池,保存了字符串常量 +方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的**类信息、常量、静态变量、即时编译器编译后的代码缓存**等数据。方法区是《Java虚拟机规范》中设计的**虚拟概念**,是一块逻辑区域。 @@ -523,7 +517,7 @@ Java服务端程序开发时,**建议将-Xmx和-Xms设置为相同的值**, * **JDK8**之前的版本方法区由**堆区域中的永久代空间(ps_perm_gen)**实现,堆的大小由虚拟机参数来控制。 -* **JDK8**及之后的版本方法区存由**元空间(metaspace)**实现,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。可以使用 `-XX:MaxMetaspaceSize=值` 将元空间最大大小进行限制。 +* **JDK8**及之后的版本方法区由**元空间(metaspace)**实现,元空间位于操作系统维护的直接内存中,默认情况下只要不超过操作系统承受的上限,可以一直分配。可以使用 `-XX:MaxMetaspaceSize=值` 将元空间最大大小进行限制。 @@ -775,7 +769,7 @@ HotSpot 虚拟机主要使用直接指针来进行对象访问。 Java使用的是可达性分析算法来判断对象是否可以被回收。 -基本思路就是通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。 +基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象到GC Roots间没有任何引用链相连, 或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的,可以被回收。 固定可作为GC Roots的对象包括: @@ -783,8 +777,8 @@ Java使用的是可达性分析算法来判断对象是否可以被回收。 * 本地方法栈中引用的对象。 * 在方法区中类静态属性引用的对象。 * 在方法区中常量引用的对象,譬如字符串常量池里的引用。 -* Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。 * 所有被同步锁(synchronized关键字)持有的对象(即该对象作为锁被使用)。 +* Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如 NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。 * 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。 @@ -809,9 +803,11 @@ Java里传统引用的定义: 如果reference类型的数据中存储的数值 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。 +弱引用的使用场景:缓存系统、对象池、避免内存泄漏 + **虚引用:** -虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的 存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。 +虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。 @@ -901,11 +897,13 @@ Java垃圾回收过程会通过单独的GC线程来完成,但是不管使用 #### 分代收集算法 -分代垃圾回收将整个内存区域划分为年轻代和老年代: +分代收集算法将整个内存区域划分为年轻代和老年代: - 新生代:复制算法 - 老年代:标记-清除算法、标记-整理算法 +对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值后,如果对象还存活,那么该对象会进入老年代。 + ![](./JVM/分代垃圾回收.png) 我们通过arthas来验证下内存划分的情况: @@ -946,7 +944,7 @@ tenured_gen指的是晋升区域,其实就是老年代。 部分收集 (Partial GC): -- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集; +- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集,包括Eden区和两个Survivor区(S0和S1); - 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集; - 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。 @@ -956,13 +954,14 @@ tenured_gen指的是晋升区域,其实就是老年代。 **Minor GC触发条件**:Eden区满时 +**Major GC触发条件**:当老年代空间不足时,或者系统检测到年轻代对象晋升到老年代的速度过快,可能会触发Major GC。 + **Full GC触发条件**: -1. 调用System.gc时,系统建议执行Full GC,但是不必然执行。可以通过 `-XX:+ DisableExplicitGC` 来禁止调用 `System.gc()`。 +1. 调用 `System.gc` 时,系统建议执行Full GC,但是不必然执行。可以通过 `-XX:+ DisableExplicitGC` 来禁止调用 `System.gc()`。 2. 老年代空间不足。 -3. 永久代空间不足。JVM 规范中运行时数据区域中的方法区,在 HotSpot 虚拟机中也称为永久代,存放一些类信息、常量、静态变量等数据,当系统要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,会触发 Full GC。 -4. 通过Minor GC后进入老年代的平均大小大于老年代的可用内存。 -5. 由Eden区、From Space区向To Space区复制时,对象大小大于To Space可用内存,则把该对象转存到老年代,且老年代的可用内存小于该对象大小。 +3. 当永久代(Java 8之前的版本)或元空间(Java 8及以后的版本)空间不足时。JVM 规范中运行时数据区域中的方法区,在 HotSpot 虚拟机中也称为永久代,存放一些类信息、常量、静态变量等数据,当系统要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,会触发 Full GC。 +4. Minor GC(新生代垃圾回收)时,如果存活的对象无法全部放入老年代,或者老年代空间不足以容纳存活的对象,则会触发Full GC,对整个堆内存进行回收。 @@ -989,13 +988,13 @@ tenured_gen指的是晋升区域,其实就是老年代。 ### 垃圾回收器 -垃圾回收器是垃圾回收算法的具体实现。 +垃圾回收器是垃圾回收算法的具体实现。 由于垃圾回收器分为年轻代和老年代,除了G1之外其他垃圾回收器必须成对组合进行使用。 ![](./JVM/垃圾回收器.png) -![](./JVM/新一代GC.png) +![](./JVM/垃圾收集器.png) #### Serial/Serial Old @@ -1104,13 +1103,24 @@ G1收集器的运作过程大致可划分为四个步骤: ![](./JVM/G1.jpeg) + + +| 垃圾回收器 | 使用范围 | STW | 垃圾回收算法 | 回收过程 | 浮动垃圾 | 使用场景 | +| ---------- | -------------- | ------------------------ | --------------- | ----------------------------------------------- | ------------ | ---------------------------- | +| CMS | 老年代 | 以最小的停顿时间为目标 | 标记-清除 | 1. 初始标记 2. 并发标记 3. 重新标记 4. 并发清除 | 产生浮动垃圾 | 低延迟需求;老年代收集; | +| G1 | 新生代和老年代 | 极高概率满足停顿时间要求 | 复制、标记-整理 | 1. 初始标记 2. 并发标记 3. 最终标记 4. 筛选回收 | 没有浮动垃圾 | 大堆内存;低停顿时间高吞吐量 | + + + #### Shenandoah GC + + #### ZGC -## 原理篇 +## 原理 ### 栈上的数据存储 diff --git a/docs/JVM/JVM/JVM (2).png b/docs/JVM/JVM/JVM (2).png new file mode 100644 index 0000000..c0deb44 Binary files /dev/null and b/docs/JVM/JVM/JVM (2).png differ diff --git "a/docs/JVM/JVM/\345\236\203\345\234\276\346\224\266\351\233\206\345\231\250.png" "b/docs/JVM/JVM/\345\236\203\345\234\276\346\224\266\351\233\206\345\231\250.png" new file mode 100644 index 0000000..3439e3b Binary files /dev/null and "b/docs/JVM/JVM/\345\236\203\345\234\276\346\224\266\351\233\206\345\231\250.png" differ diff --git a/docs/Java/Java.md b/docs/Java/Java.md index 9be2f49..a21c8ed 100644 --- a/docs/Java/Java.md +++ b/docs/Java/Java.md @@ -312,9 +312,7 @@ toString finalize -* 当对象被回收时,系统自动调用该对象的finalize方法,子类可以重写该方法用于一些资源释放操作 -* 当对象没有任何引用时,jvm就认为该对象是一个垃圾对象,就会使用垃圾回收机制销毁对象 -* 垃圾回收机制的调用,由系统决定,也可以通过 `System.gc()` 主动触发垃圾回收机制 +* 当对象被回收时,系统自动调用该对象的finalize方法,子类可以重写该方法用于一些资源释放操作。但它已被标记为过时,不建议使用。 @@ -1117,6 +1115,15 @@ getType:以Class形式返回类型 动态代理是在运行时动态生成类字节码,并加载到 JVM 中 + + +| 代理技术 | 使用条件 | +| ------------------ | ------------------------------------------------------------ | +| JDK 动态代理技术 | 目标类有接口,是基于接口动态生成实现类的代理对象 | +| Cglib 动态代理技术 | 目标类无接口且不能使用final修饰,是基于被代理对象动态生成子对象为代理对象 | + +![](./Java/动态代理.jpeg) + ### JDK动态代理 基于接口的,代理类一定是有定义的接口,在 Java 动态代理机制中 `InvocationHandler` 接口和 `Proxy` 类是核心。 @@ -1298,7 +1305,6 @@ public class Test { } } - public class AnnotationProcessor { public static void main(String[] args) throws Exception { Method method = Test.class.getMethod("annotatedMethod"); diff --git "a/docs/Java/Java/\345\212\250\346\200\201\344\273\243\347\220\206.jpeg" "b/docs/Java/Java/\345\212\250\346\200\201\344\273\243\347\220\206.jpeg" new file mode 100644 index 0000000..220f221 Binary files /dev/null and "b/docs/Java/Java/\345\212\250\346\200\201\344\273\243\347\220\206.jpeg" differ diff --git "a/docs/Java/Java\351\233\206\345\220\210.md" "b/docs/Java/Java\351\233\206\345\220\210.md" index 5ddcd10..c997f03 100644 --- "a/docs/Java/Java\351\233\206\345\220\210.md" +++ "b/docs/Java/Java\351\233\206\345\220\210.md" @@ -6,7 +6,7 @@ Collection接口没有直接的实现子类,是通过它的子接口Set、List ### List -有序,可重复,支持索引,常用的有ArrayList,LinkedList,Vector +有序,可重复,支持索引,常用的有ArrayList,LinkedList,Vector,Stack #### ArrayList @@ -31,11 +31,11 @@ Collection接口没有直接的实现子类,是通过它的子接口Set、List 如果使用指定大小的构造器,初始容量为指定大小,如果需要扩容则扩容为**1.5倍。** - +扩容时创建一个新的更大的数组,将原来数组中的元素逐个复制到新数组中,最后将ArrayList内部指向原数组的引用指向新数组。 #### Vector -底层是`Object[]` 数组。 +Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。 线程同步的,即线程安全, 操作方法带 `synchronized` @@ -59,16 +59,32 @@ Collection接口没有直接的实现子类,是通过它的子接口Set、List -### Set +#### CopyonWriteArraylist -**Comparable 和 Comparator 的区别** +CopyOnWriteArrayList底层也是通过一个数组保存数据,使用volatile关键字修饰数组,保证当前线程对数组对象重新赋值后,其他线程可以及时感知到。 -`Comparable` 接口和 `Comparator` 接口都是 Java 中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用: +在写入操作时,加了一把互斥锁ReentrantLock以保证线程安全。读是没有加锁的。 -- `Comparable` 接口实际上是出自`java.lang`包 它有一个 `compareTo(Object obj)`方法用来排序 -- `Comparator`接口实际上是出自 `java.util` 包它有一个`compare(Object obj1, Object obj2)`方法用来排序 +```java +public boolean add(E e) { + synchronized (lock) { + Object[] es = getArray(); + int len = es.length; + es = Arrays.copyOf(es, len + 1); + es[len] = e; + setArray(es); + return true; + } +} +``` +写入新元素时,首先会先将原来的数组拷贝一份并且让原来数组的长度+1后就得到了一个新数组,新数组里的元素和旧数组的元素一样并且长度比旧数组多一个长度,然后将新加入的元素放置都在新数组最后一个位置后,用新数组的地址替换掉老数组的地址就能得到最新的数据了。 +### Set + +Set不允许存在重复的元素,与List不同,set中的元素是无序的。常用的实现有HashSet,LinkedHashSet和TreeSet。 + +当向Set集合中插入元素时,会先根据元素的hashCode值来确定元素的存储位置,然后再通过equals方法来判断是否已经存在相同的元素,如果存在则不会再次插入,保证了元素的唯一性。 | | 线程安全 | 底层数据结构 | 应用场景 | | ------------- | -------- | ------------ | ------------------------ | @@ -80,9 +96,7 @@ Collection接口没有直接的实现子类,是通过它的子接口Set、List #### HashSet -无序,唯一 - -HashSet实际上是HashMap , HashMap底层是(数组+链表+红黑树) +HashSet通过HashMap实现,HashMap的Key即HashSet存储的元素,所有Key都是用相同的Value,一个名为PRESENT的Object类型常量。使用Key保证元素唯一性,但不保证有序性。由于HashSet是HashMap实现的,因此线程不安全。 HashSet如何检查键值重复?`HashSet`的`add()`方法直接调用`HashMap`的`put()`方法:先比较hashcode,如果发现有相同 `hashcode` 值的对象,这时会调用`equals()`方法来检查 `hashcode` 相等的对象是否真的相同。 @@ -114,13 +128,7 @@ public HashSet() { #### LinkedHashSet -HashSet的子类 - -底层是一个LinkedHashMap,底层维护了一个数组+双向链表 - -根据元素的hashCode值决定元素的存储位置,同时使用链表维护元素的次序,使得元素看起来是以插入顺序保存的 - -不允许元素重复 +HashSet的子类,底层是一个LinkedHashMap,使用双向链表维护元素插入顺序。 @@ -197,6 +205,8 @@ BlockingQueue的实现类: ## Map +主要实现有TreeMap、HashMap、HashTable、LinkedHashMap、ConcurrentHashMap + ![](./Java集合/map.jpeg) ### HashMap @@ -205,21 +215,15 @@ BlockingQueue的实现类: `HashMap` 可以存储 null 的 key 和 value,通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选。 -JDK1.8 之前 `HashMap` 由数组+链表组成的,数组是 `HashMap` 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 +JDK1.8 之前 `HashMap` 由数组+链表组成的,数组是 `HashMap` 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 -**`HashMap` 默认的初始化大小为 16。到达临界值(临界值是16*loadFactor(0.75)=12)之后,容量变为原来的 2 倍。并且, `HashMap` 总是使用 2 的幂作为哈希表的大小。**因为对长度取模的操作可以用位运算来替代(` hash%length==hash&(length-1)`),能够有效保留hashcode低位并且提高效率。 +![](./Java集合/HashMap-Java7.png) -初始化传的不是2的幂时,会向上寻找离得近的2的幂作为初始化大小。 +JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 -添加元素: +![](./Java集合/HashMap-Java8.png) -``` -1. 添加一个元素时,先得到hash值然后转换为索引值( (n - 1) & hash ) -2. 找到存储数据的table,看索引位置是否有元素 -3. 如果没有,直接插入(该节点直接放在数组中) -4. 如果有,调用equals比较判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同则覆盖,如果不同插入到链表末端。equals不能简单的认为是比较内容或是地址,程序员可以进行重写 -5. 在java8中,如果一条链表的元素个数 >= TREEIFY_THRESHOLD-1(默认8),并且table大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树) -``` +`HashMap` 默认的初始化大小为 16。到达临界值(临界值是16*loadFactor(0.75)=12)之后,容量变为原来的 2 倍。并且, `HashMap` 总是使用 2 的幂作为哈希表的大小。初始化传的不是2的幂时,会向上寻找离得近的2的幂作为初始化大小。 @@ -235,39 +239,77 @@ JDK1.8 之前 `HashMap` 由数组+链表组成的,数组是 `HashMap` 的主 死循环和数据丢失 -1. JDK1.7 及之前版本的 `HashMap` 在多线程环境下**扩容操作可能存在死循环问题**,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入**死循环**无法结束。[JDK 1.7 hashmap循环链表的产生(图文并茂,巨详细)_hashmap循环链表是如何产生的-CSDN博客](https://blog.csdn.net/qq_44833552/article/details/125575981) +1. JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。[JDK 1.7 hashmap循环链表的产生(图文并茂,巨详细)_hashmap循环链表是如何产生的-CSDN博客](https://blog.csdn.net/qq_44833552/article/details/125575981) + +2. 多个线程对 `HashMap` 的 `put` 操作会有数据覆盖的风险。并发环境下,推荐使用 `ConcurrentHashMap` 。 + + + +**添加元素的流程**: + +1. 根据要添加的键的哈希码计算在数组中的位置(索引)。 (n - 1) & hash +2. 检查该位置是否为空(即没有键值对存在)。如果为空,则直接在该位置创建一个新的Entry对象来存储键值对。将要添加的键值对作为该Entry的键和值,并保存在数组的对应位置。 +3. 如果该位置已经存在其他键值对,检查该位置的第一个键值对的哈希码和键是否与要添加的键值对相同。如果相同,则表示找到了相同的键,直接将新的值替换旧的值,完成更新操作。 +4. 如果第一个键值对的哈希码和键不相同,则需要遍历链表或红黑树来查找是否有相同的键:如果键值对集合是链表结构,从链表的头部开始逐个比较键的哈希码和equals()方法,直到找到相同的键或达到链表末尾。如果键值对集合是红黑树结构,在红黑树中使用哈希码和equals()方法进行查找。根据键的哈希码,定位到红黑树中的某个节点,然后逐个比较键,直到找到相同的键或达到红黑树末尾。 +5. 检查链表长度是否达到阈值(默认为8)。如果链表长度超过阈值,且HashMap的数组长度大于等于64,则会将链表转换为红黑树。 +6. 检查负载因子是否超过阈值(默认为0.75)。如果键值对的数量(size)与数组的长度的比值大于阈值,则需要进行扩容操作。 +7. 扩容操作:1. 创建一个新的两倍大小的数组。2. 将旧数组中的键值对重新计算哈希码并分配到新数组中的位置。3. 更新HashMap的数组引用和阈值参数。 -2. 多个线程对 `HashMap` 的 `put` 操作会有**数据覆盖**的风险。并发环境下,推荐使用 `ConcurrentHashMap` 。 +![](./Java集合/HashMap-put.png) -**HashMap遍历方式:** +**扩容机制**: -1. 使用迭代器(Iterator)EntrySet 的方式进行遍历; -2. 使用迭代器(Iterator)KeySet 的方式进行遍历; -3. 使用 For Each EntrySet 的方式进行遍历; -4. 使用 For Each KeySet 的方式进行遍历; -5. 使用 Lambda 表达式的方式进行遍历; -6. 使用 Streams API 单线程的方式进行遍历; -7. 使用 Streams API 多线程的方式进行遍历。 +hashMap默认的负载因子是0.75,即如果hashmap中的元素个数超过了总容量75%,则会触发扩容,扩容分为两个步骤: -`entrySet` 的性能比 `keySet` 的性能高出了一倍之多,因此我们应该尽量使用 `entrySet` 来实现 Map 集合的遍历。 +1. 对哈希表长度的扩展(2倍); -`EntrySet` 的性能比 `KeySet` 的性能高出了一倍,因为 `KeySet` 相当于循环了两遍 Map 集合,而 `EntrySet` 只循环了一遍。 +2. 将旧哈希表中的数据放到新的哈希表中。 +因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。 +因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。 -不能在遍历中使用集合 `map.remove()` 来删除数据,这是非安全的操作方式,但我们可以使用迭代器的 `iterator.remove()` 的方法来删除数据,这是安全的删除集合的方式。同样的我们也可以使用 Lambda 中的 `removeIf` 来提前删除数据,或者是使用 Stream 中的 `filter` 过滤掉要删除的数据进行循环,这样都是安全的,当然我们也可以在 `for` 循环前删除数据在遍历也是线程安全的。 + + +**hashmap key可以为null吗?** + +可以为 null。 + +hashMap中使用hash()方法来计算key的哈希值,当key为空时,直接另key的哈希值为0,不走key.hashCode()方法; + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` ### ConcurrentHashMap -Java 7 中 `ConcurrnetHashMap` 由很多个 `Segment` 组合,而每一个 `Segment` 是一个类似于 `HashMap` 的结构,所以每一个 `HashMap` 的内部可以进行扩容。但是 `Segment` 的个数一旦**初始化就不能改变**,默认 `Segment` 的个数是 16 个,可以认为 `ConcurrentHashMap` 默认支持最多 16 个线程并发。 +在 JDK 1.7 中,提供了一种机制叫分段锁。整个哈希表被分为多个段,每个段都独立锁定。 + +一个 ConcurrentHashMap 里包含一个 Segment 数组,Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁(使用ReentrantLock)。 + +![](./Java集合/concurrent-hashmap-Java7.png) + +Segment 的个数一旦初始化就不能改变,默认 Segment 的个数是 16 个,可以认为 `ConcurrentHashMap` 默认支持最多 16 个线程并发。 -Java 8 中 不再是之前的 **Segment 数组 + HashEntry 数组 + 链表**,而是 **Node 数组 + 链表 / 红黑树**。当冲突链表达到一定长度时,链表会转换成红黑树。 +在JDK1.8中,ConcurrentHashMap的实现原理摒弃了分段锁,而是选择了与HashMap类似的数组+链表+红黑树的方式实现,以某个位置的头结点(链表的头结点或红黑树的 root 结点)为锁,加锁则采用CAS和synchronized实现。 + +添加元素时首先会判断容器是否为空: + +- 如果为空则使用 volatile 加 CAS 来初始化 +- 如果容器不为空,则根据存储的元素计算该位置是否为空。 + - 如果根据存储的元素计算结果为空,则利用 CAS 设置该节点; + - 如果根据存储的元素计算结果不为空,则使用 synchronized ,然后,遍历桶中的数据,并替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了。 + +![](./Java集合/HashMap-Java8.png) @@ -324,29 +366,21 @@ for (int i = 1; i <= 5; i++) { ### Hashtable +Hashtable的底层数据结构主要是数组加上链表,数组是主体,链表是解决hash冲突存在的。 + 键和值都不能为null 使用方法基本和HashMap一样 -Hashtable是线程安全的,通过在每个⽅法上添加同步关键字来实现的,但这也可能导致性能下降。 - -``` -底层数组Hashtable$Entry[] 初始化大小 11 -临界值 threshold = 8 (11*0.75) -扩容机制 -``` - -| | 线程安全 | 效率 | 对null key/value的支持 | 扩容机制 | 底层数据结构 | -| --------- | ---------- | ---------------------- | ---------------------- | -------------------------------------- | ----------------------- | -| HashMap | 线程不安全 | 高 | 允许 | 默认初始化大小16,每次扩容为原来的2倍 | 数组+链表+红黑树 | -| HashTable | 线程安全 | 基本被淘汰,不建议使用 | 不允许 | 默认初始化大小11,每次扩容为原来的2n+1 | 数组+链表,没有树化机制 | - +Hashtable是线程安全的,通过在每个⽅法上添加 synchronized 关键字来实现的,但这也可能导致性能下降。 +| | 线程安全 | 效率 | 对null key/value的支持 | 扩容机制 | 底层数据结构 | +| ----------------- | ---------- | ---------------------- | ---------------------- | -------------------------------------- | ----------------------- | +| HashMap | 线程不安全 | 高 | 允许 | 默认初始化大小16,每次扩容为原来的2倍 | 数组+链表+红黑树 | +| HashTable | 线程安全 | 基本被淘汰,不建议使用 | 不允许 | 默认初始化大小11,每次扩容为原来的2n+1 | 数组+链表,没有树化机制 | +| ConcurrentHashMap | 线程安全 | 高 | 不允许 | 2倍扩容 | 数组+链表+红黑树 | -**ConcurrentHashMap 和 Hashtable 的区别:** -1. 底层数据结构:ConcurrentHashMap和HashMap一样,使用数组+链表/红黑树,HashTable使用数组+链表,没有树化机制 -2. 实现线程安全的方式:`ConcurrentHashMap` 取消了 `Segment` 分段锁,采用 `Node + CAS + synchronized` 来保证并发安全,`synchronized` 只锁定当前链表或红黑二叉树的首节点。HashTable使用 `synchronized` 来保证线程安全,效率非常低下 ### TreeMap diff --git "a/docs/Java/Java\351\233\206\345\220\210/HashMap-Java7.png" "b/docs/Java/Java\351\233\206\345\220\210/HashMap-Java7.png" new file mode 100644 index 0000000..e43b54c Binary files /dev/null and "b/docs/Java/Java\351\233\206\345\220\210/HashMap-Java7.png" differ diff --git "a/docs/Java/Java\351\233\206\345\220\210/HashMap-Java8.png" "b/docs/Java/Java\351\233\206\345\220\210/HashMap-Java8.png" new file mode 100644 index 0000000..0c3eb49 Binary files /dev/null and "b/docs/Java/Java\351\233\206\345\220\210/HashMap-Java8.png" differ diff --git "a/docs/Java/Java\351\233\206\345\220\210/HashMap-put.png" "b/docs/Java/Java\351\233\206\345\220\210/HashMap-put.png" new file mode 100644 index 0000000..53cd57d Binary files /dev/null and "b/docs/Java/Java\351\233\206\345\220\210/HashMap-put.png" differ diff --git "a/docs/Java/Java\351\233\206\345\220\210/concurentHashMap-Java7.jpeg" "b/docs/Java/Java\351\233\206\345\220\210/concurentHashMap-Java7.jpeg" deleted file mode 100644 index 7575f7f..0000000 Binary files "a/docs/Java/Java\351\233\206\345\220\210/concurentHashMap-Java7.jpeg" and /dev/null differ diff --git "a/docs/Java/Java\351\233\206\345\220\210/concurrent-hashmap-Java7.png" "b/docs/Java/Java\351\233\206\345\220\210/concurrent-hashmap-Java7.png" new file mode 100644 index 0000000..7577071 Binary files /dev/null and "b/docs/Java/Java\351\233\206\345\220\210/concurrent-hashmap-Java7.png" differ diff --git "a/docs/Java/Java\351\233\206\345\220\210/concurrent-hashmap-Java8.png" "b/docs/Java/Java\351\233\206\345\220\210/concurrent-hashmap-Java8.png" new file mode 100644 index 0000000..6c0e235 Binary files /dev/null and "b/docs/Java/Java\351\233\206\345\220\210/concurrent-hashmap-Java8.png" differ diff --git a/docs/MySQL/MySQL.md b/docs/MySQL/MySQL.md index 6ea272f..7c4b2cd 100644 --- a/docs/MySQL/MySQL.md +++ b/docs/MySQL/MySQL.md @@ -161,7 +161,7 @@ update执行流程:` update T set c=c+1 where ID=2;` -## MySQL一行记录的存储结构 +## 存储结构 先来看看 MySQL 数据库的文件存放在哪个目录? @@ -205,7 +205,7 @@ B+ 树中每一层都是通过双向链表连接起来的,如果是以页为 那具体怎么解决呢? -**在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。每个区的大小为 1MB,对于 16KB 的页来说,连续的 64 个页会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了**. +在表中数据量大的时候,为某个索引分配空间的时候就不再按照页为单位分配了,而是按照区(extent)为单位分配。每个区的大小为 1MB,对于 16KB 的页来说,连续的 64 个页会被划为一个区,这样就使得链表中相邻的页的物理位置也相邻,就能使用顺序 I/O 了. @@ -237,7 +237,7 @@ varchar(n) 和 char(n) 的区别是char 是定长的,varchar 是变长的, - phone 列的值为 123,真实数据占用的字节数是 3 字节,十六进制 0x03; - age 列和 id 列不是变长字段,所以这里不用管。 -这些变长字段的真实数据占用的字节数会按照列的顺序**逆序存放**,所以「变长字段长度列表」里的内容是「 03 01」,而不是 「01 03」。 +这些变长字段的真实数据占用的字节数会按照列的顺序**逆序存放**,所以「变长字段长度列表」里的内容是「 03 01」 ![](.\MySql\变长字段长度列表1.webp) @@ -294,7 +294,7 @@ varchar(n) 和 char(n) 的区别是char 是定长的,varchar 是变长的, 记录真实数据部分除了我们定义的字段,还有三个隐藏字段,分别为:row_id、trx_id、roll_pointer。 -* row_id:如果我们建表的时候指定了主键或者唯一约束列,那么就没有 row_id 隐藏字段了。如果既没有指定主键,又没有唯一约束,那么 InnoDB 就会为记录添加 row_id 隐藏字段。row_id不是必需的,占用 6 个字节。 +* row_id:如果我们建表的时候指定了主键或者唯一索引,那么就没有 row_id 隐藏字段了。如果既没有指定主键,又没有唯一索引,那么 InnoDB 就会为记录添加 row_id 隐藏字段。row_id不是必需的,占用 6 个字节。 - trx_id:事务id,表示这个数据是由哪个事务生成的。 trx_id是必需的,占用 6 个字节。 @@ -338,37 +338,37 @@ MySQL 中磁盘和内存交互的基本单位是页,一个页的大小一般 ## 索引 -索引分类: +### 分类 -按「数据结构」分类:**B+tree索引、Hash索引、Full-text索引**。 +按「数据结构」分类:B+tree索引、Hash索引、Full-text索引。 -按「物理存储」分类:**聚簇索引(主键索引)、二级索引(辅助索引)**。 +按「物理存储」分类:聚簇索引(主键索引)、二级索引(辅助索引)。 -按「字段特性」分类:**主键索引、唯一索引、普通索引、前缀索引**。 +按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引。 -按「字段个数」分类:**单列索引、联合索引**。 +按「字段个数」分类:单列索引、联合索引。 -### B+Tree索引 +#### B+Tree索引 -***1、B+Tree vs B Tree*** +***1、B+Tree vs 二叉树*** -B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的**磁盘 I/O 次数**下,就能查询更多的节点。 +对于有 N 个叶子节点的 B+Tree,其搜索复杂度为`O(logdN)`,其中 d 表示节点允许的最大子节点个数为 d 个。 -另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的**范围查询**,而 B 树无法做到这一点。 +在实际的应用当中, d 值是大于100的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右,也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据。 -B+Tree**插入和删除效率更高**,不会涉及复杂的树的变形 +而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 `O(logN)`,这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。如果索引的字段值是按顺序增长的,二叉树会转变为链表结构,检索的过程和全表扫描无异。 -***2、B+Tree vs 二叉树*** +**2、B+Tree vs 红黑树** -对于有 N 个叶子节点的 B+Tree,其搜索复杂度为`O(logdN)`,其中 d 表示节点允许的最大子节点个数为 d 个。 +红黑树虽然对比二叉树来说,树高有所降低,但数据量一大时,依旧会有很大的高度。每个节点中只存储一个数据,节点之间还是不连续的,依旧无法利用局部性原理。 -在实际的应用当中, d 值是大于100的,这样就保证了,即使数据达到千万级别时,B+Tree 的高度依然维持在 3~4 层左右,也就是说一次数据查询操作只需要做 3~4 次的磁盘 I/O 操作就能查询到目标数据。 +***3、B+Tree vs B Tree*** -而二叉树的每个父节点的儿子节点个数只能是 2 个,意味着其搜索复杂度为 `O(logN)`,这已经比 B+Tree 高出不少,因此二叉树检索到目标数据所经历的磁盘 I/O 次数要更多。如果索引的字段值是按顺序增长的,二叉树会转变为链表结构,检索的过程和全表扫描无异。 +B+Tree 只在叶子节点存储数据,而 B 树 的非叶子节点也要存储数据,所以 B+Tree 的单个节点的数据量更小,在相同的**磁盘 I/O 次数**下,就能查询更多的节点。 -**3、B+Tree vs 红黑树** +另外,B+Tree 叶子节点采用的是双链表连接,适合 MySQL 中常见的**范围查询**,而 B 树无法做到这一点。 -虽然对比二叉树来说,树高有所降低,但数据量一大时,依旧会有很大的高度。每个节点中只存储一个数据,节点之间还是不连续的,依旧无法利用局部性原理。 +B+Tree**插入和删除效率更高**,不会涉及复杂的树的变形 ***4、B+Tree vs Hash*** @@ -376,19 +376,29 @@ Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。 但是 Hash 表不适合做范围查询,它更适合做等值的查询,这也是 B+Tree 索引要比 Hash 表索引有着更广泛的适用场景的原因。 -### 聚集索引和二级索引 +#### 聚集索引和二级索引 ![](MySQL\聚集索引和二级索引.webp) -所以,在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。 +在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。 -### 主键索引和唯一索引 +如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引查询到数据,这个过程就是回表。 + +#### 主键索引和唯一索引 一张表最多只有一个主键索引,索引列的值不允许有空值。 唯一索引建立在 UNIQUE 字段上的索引,一张表可以有多个唯一索引,索引列的值必须唯一,但是允许有空值。 -### 联合索引 +#### 前缀索引 + +前缀索引的特点是短小精悍,我们可以利用一个字段的前`N`个字符创建索引,相较于使用一个完整字段创建索引,前缀索引能够更加节省存储空间。 + +但是无法通过前缀索引来完成`ORDER BY、GROUP BY`等分组排序工作,同时也无法完成覆盖扫描等操作。 + + + +#### 联合索引 使用联合索引时,存在**最左匹配原则**,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效。 @@ -400,9 +410,9 @@ Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。 由于联合索引(二级索引)是先按照 a 字段的值排序的,所以符合 a > 1 条件的二级索引记录肯定是相邻,于是在进行索引扫描的时候,可以定位到符合 a > 1 条件的第一条记录,然后沿着记录所在的链表向后扫描,直到某条记录不符合 a > 1 条件位置。所以 a 字段可以在联合索引的 B+Tree 中进行索引查询。 -**但是在符合 a > 1 条件的二级索引记录的范围里,b 字段的值是无序的**。所以 b 字段无法利用联合索引进行索引查询。 +但是在符合 a > 1 条件的二级索引记录的范围里,b 字段的值是无序的。所以 b 字段无法利用联合索引进行索引查询。 -**这条查询语句只有 a 字段用到了联合索引进行索引查询,而 b 字段并没有使用到联合索引**。 +这条查询语句只有 a 字段用到了联合索引进行索引查询,而 b 字段并没有使用到联合索引。 @@ -410,17 +420,17 @@ Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。 由于联合索引(二级索引)是先按照 a 字段的值排序的,所以符合 >= 1 条件的二级索引记录肯定是相邻,于是在进行索引扫描的时候,可以定位到符合 >= 1 条件的第一条记录,然后沿着记录所在的链表向后扫描,直到某条记录不符合 a>= 1 条件位置。所以 a 字段可以在联合索引的 B+Tree 中进行索引查询。 -虽然在符合 a>= 1 条件的二级索引记录的范围里,b 字段的值是「无序」的,**但是对于符合 a = 1 的二级索引记录的范围里,b 字段的值是「有序」的**. +虽然在符合 a>= 1 条件的二级索引记录的范围里,b 字段的值是「无序」的,但是对于符合 a = 1 的二级索引记录的范围里,b 字段的值是「有序」的. -于是,在确定需要扫描的二级索引的范围时,当二级索引记录的 a 字段值为 1 时,可以通过 b = 2 条件减少需要扫描的二级索引记录范围(b 字段可以利用联合索引进行索引查询的意思)。也就是说,从符合 a = 1 and b = 2 条件的第一条记录开始扫描,而不需要从第一个 a 字段值为 1 的记录开始扫描。 +于是,在确定需要扫描的二级索引的范围时,当二级索引记录的 a 字段值为 1 时,可以通过 b = 2 条件减少需要扫描的二级索引记录范围。也就是说,从符合 a = 1 and b = 2 条件的第一条记录开始扫描,而不需要从第一个 a 字段值为 1 的记录开始扫描。 -所以,**Q2 这条查询语句 a 和 b 字段都用到了联合索引进行索引查询**。 +所以,这条查询语句 a 和 b 字段都用到了联合索引进行索引查询。 > **`SELECT * FROM t_table WHERE a BETWEEN 2 AND 8 AND b = 2`,联合索引(a, b)哪一个字段用到了联合索引的 B+Tree?** -在 MySQL 中,BETWEEN 包含了 value1 和 value2 边界值,类似于 >= and =<,所以**这条查询语句 a 和 b 字段都用到了联合索引进行索引查询**。 +在 MySQL 中,BETWEEN 包含了 value1 和 value2 边界值,类似于 >= and =<,所以这条查询语句 a 和 b 字段都用到了联合索引进行索引查询。 @@ -428,9 +438,9 @@ Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。 由于联合索引(二级索引)是先按照 name 字段的值排序的,所以前缀为 ‘j’ 的 name 字段的二级索引记录都是相邻的, 于是在进行索引扫描的时候,可以定位到符合前缀为 ‘j’ 的 name 字段的第一条记录,然后沿着记录所在的链表向后扫描,直到某条记录的 name 前缀不为 ‘j’ 为止。 -虽然在符合前缀为 ‘j’ 的 name 字段的二级索引记录的范围里,age 字段的值是「无序」的,**但是对于符合 name = j 的二级索引记录的范围里,age字段的值是「有序」的** +虽然在符合前缀为 ‘j’ 的 name 字段的二级索引记录的范围里,age 字段的值是「无序」的,但是对于符合 name = j 的二级索引记录的范围里,age字段的值是「有序」的 -所以,**这条查询语句 a 和 b 字段都用到了联合索引进行索引查询**。 +所以,这条查询语句 a 和 b 字段都用到了联合索引进行索引查询。 @@ -438,15 +448,6 @@ Hash 在做等值查询的时候效率贼快,搜索复杂度为 O(1)。 -**索引下推:** - -对于联合索引(a, b),在执行 `select * from table where a > 1 and b = 2` 语句的时候,只有 a 字段能用到索引,那在联合索引的 B+Tree 找到第一个满足条件的主键值后,还需要判断其他条件是否满足(看 b 是否等于 2),那是在联合索引里判断?还是回主键索引去判断呢? - -- 在 MySQL 5.6 之前,只能从 ID2 (主键值)开始一个个回表,到「主键索引」上找出数据行,再对比 b 字段值。 -- 而 MySQL 5.6 引入的**索引下推优化**(index condition pushdown), **可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数**。 - - - **联合索引进行排序**: 这里出一个题目,针对针对下面这条 SQL,你怎么通过索引来提高查询效率呢? @@ -465,8 +466,6 @@ select * from order where status = 1 order by create_time asc - - ### 索引设计原则 什么时候适合索引? @@ -479,13 +478,15 @@ select * from order where status = 1 order by create_time asc 4. 如果是字符串类型的字段,字段的长度过长,可以针对字段的特点,建立前缀索引; -5. 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率; +5. 建立联合索引,应当遵循最左前缀原则,将多个字段之间按优先级顺序组合; -6. 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率; +6. 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引,节省存储空间,避免回表,提高查询效率; -7. 如果索引列不能存储null值,在创建表时使用not null约束它。当优化器知道每列是否包含null值时,它可以更好地确定哪个索引最有效地用于查询。 +7. 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率; -8. 表的主外键或连表字段,必须建立索引,因为能很大程度提升连表查询的性能。 +8. 如果索引列不能存储null值,在创建表时使用not null约束它。当优化器知道每列是否包含null值时,它可以更好地确定哪个索引最有效地用于查询。 + +9. 表的主外键或连表字段,必须建立索引,因为能很大程度提升连表查询的性能。 @@ -493,62 +494,20 @@ select * from order where status = 1 order by create_time asc 1. 大量重复值的字段 2. 当表的数据较少,不应当建立索引,因为数据量不大时,维护索引反而开销更大。 -3. 经常更新的字段,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能。 +3. 经常增删改的字段,因为索引字段频繁修改,由于要维护 B+Tree的有序性,那么就需要频繁的重建索引,这个过程是会影响数据库性能。 4. 索引不能参与计算,因此经常带函数查询的字段,并不适合建立索引。 5. 一张表中的索引数量并不是越多越好,一般控制在`3`,最多不能超过`5`。 6. 索引的字段值无序时,不推荐建立索引,因为会造成页分裂,尤其是主键索引。 -### 索引优化方法 - -#### 前缀索引优化 - -当字段类型为字符串(varchar, text, longtext等)时,有时候需要索引很长的字符串,导致索引较大,查询是浪费大量的磁盘IO,影响查询效率。此时可以只对字符串的一部分前缀建立索引,节约索引空间,提高索引效率。 - -不过,前缀索引有一定的局限性,例如: - -- order by 就无法使用前缀索引; -- 无法把前缀索引用作覆盖索引; - - - -#### 覆盖索引优化 - -SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能得到,从而不需要通过聚簇索引查询获得,可以避免回表的操作。 - -使用覆盖索引的好处就是,不需要查询出包含整行记录的所有信息,也就减少了大量的 I/O 操作。 - - - -#### 主键索引自增 - -**如果我们使用自增主键**,那么每次插入的新数据就会按顺序添加到当前索引节点的位置,不需要移动已有的数据,当页面写满,就会自动开辟一个新页面。因为每次**插入一条新记录,都是追加操作,不需要重新移动数据**,因此这种插入数据的方法效率非常高。 - -**如果我们使用非自增主键**,由于每次插入主键的索引值都是随机的,因此每次插入新的数据时,就可能会插入到现有数据页中间的某个位置,这将不得不移动其它数据来满足新数据的插入,甚至需要从一个页面复制数据到另外一个页面,我们通常将这种情况称为**页分裂**。**页分裂还有可能会造成大量的内存碎片,导致索引结构不紧凑,从而影响查询效率**。 - - - -主键字段的长度不要太大,因为**主键字段长度越小,意味着二级索引的叶子节点越小(二级索引的叶子节点存放的数据是主键值),这样二级索引占用的空间也就越小**。 - - - -#### 索引最好设置为 NOT NULL - -为了更好的利用索引,索引列要设置为 NOT NULL 约束。有两个原因: - -- 第一原因:索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化,因为可为 NULL 的列会使索引、索引统计和值比较都更复杂,比如进行索引统计时,count 会省略值为NULL 的行。 -- 第二个原因:NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式中**至少会用 1 字节空间存储 NULL 值列表** - - - ### 索引失效 1. 左或左右模糊查询 `like %x 或者 like %x%`。 因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。 2. 查询中对索引做了计算、函数、类型转换操作。因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。 3. 联合索引要遵循最左匹配原则 4. 联合索引中,出现范围查询(>,<),范围查询右侧的列索引失效。 -5. 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。因为 OR 的含义就是两个只要满足一个即可,只要有条件列不是索引列,就会进行全表扫描。 +5. 在 WHERE 子句中,如果 OR 前后有条件列不是索引列,那么索引会失效。因为 OR 的含义就是两个只要满足一个即可,只要有条件列不是索引列,就会进行全表扫描。 6. 隐式类型转换 @@ -557,43 +516,36 @@ SQL 中 query 的所有字段,在索引 B+Tree 的叶子节点上都能得到 如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,你会在执行计划的结果发现这条语句会走全表扫描; -但是如果索引字段是整型类型,查询条件中的输入参数即使字符串,是不会导致索引失效,还是可以走索引扫描。 +但是如果索引字段是整型类型,查询条件中的输入参数即使是字符串,也不会导致索引失效,还是可以走索引扫描。 MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。 -### SQL提示 +### 索引下推 -我们在查询时,可以使用mysql的sql提示,加入一些人为的提示来达到优化操作的目的: +对于联合索引(a, b),在执行 `select * from table where a > 1 and b = 2` 语句的时候,只有 a 字段能用到索引,那在联合索引的 B+Tree 找到第一个满足条件的主键值后,还需要判断其他条件是否满足(看 b 是否等于 2),那是在联合索引里判断?还是回主键索引去判断呢? -- user index:建议mysql使用哪一个索引完成此次查询(仅仅是建议,mysql内部还会再次进行评估); -- ignore index:忽略指定的索引; -- force index:强制使用索引。 - -```sql -explain select * from tb_user use index(idx_user_pro) where profession=’软件工程’; -explain select * from tb_user use index(idx_user_pro) where profession=’软件工程’; -explain select * from tb_user force index(idx_user_pro) where profession=’软件工程’; -``` +- 在 MySQL 5.6 之前,只能一个个回表,返回给server层再对比 b 字段值。 +- 而 MySQL 5.6 引入的**索引下推优化**(index condition pushdown), 可以在联合索引遍历过程中,对联合索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。 ## 事务 -> **事务的特性** +**事务的特性**: -* **原子性**:一个事务中的所有操作,要么全部完成,要么全部失败。 +* 原子性:一个事务中的所有操作,要么全部完成,要么全部失败。 -* **一致性**:是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。 +* 一致性:是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。 -* **隔离性**:多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。 +* 隔离性:多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。 -* **持久性**:一个事务一旦被提交,它会保持永久性,所更改的数据都会被写入到磁盘做持久化处理。 +* 持久性:一个事务一旦被提交,它会保持永久性,所更改的数据都会被写入到磁盘做持久化处理。 -> **InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?** +**InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?** - 持久性是通过 redo log (重做日志)来保证的,宕机后能数据恢复; - 原子性是通过 undo log(回滚日志) 来保证的,事务能够进行回滚; @@ -602,43 +554,56 @@ explain select * from tb_user force index(idx_user_pro) where profession=’软 -> **并行事务会引发的问题?** +**并行事务会引发的问题**: -* **脏读**:读到其他事务未提交的数据; +* 脏读:读到其他事务未提交的数据; -* **不可重复读**:一个事务内,前后读取的数据不一致; +* 不可重复读:一个事务内,前后读取的数据不一致; -* **幻读**:一个事务中,前后读取的记录数量不一致。事务中同一个查询在不同的时间产生不同的结果集。 +* 幻读:一个事务中,前后读取的记录数量不一致。事务中同一个查询在不同的时间产生不同的结果集。 严重性:脏读 > 不可重读读 > 幻读 -> **隔离级别** +**隔离级别**: + +- 读未提交(read uncommitted),指一个事务还没提交时,它做的变更就能被其他事务看到; +- 读提交(read committed),指一个事务提交之后,它做的变更才能被其他事务看到; +- 可重复读(repeatable read),指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别; +- 串行化(serializable);会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行; -- **读未提交(read uncommitted)**,指一个事务还没提交时,它做的变更就能被其他事务看到; -- **读提交(read committed)**,指一个事务提交之后,它做的变更才能被其他事务看到; -- **可重复读(repeatable read)**,指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,**MySQL InnoDB 引擎的默认隔离级别**; -- **串行化(serializable)**;会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行; +![](./MySql/隔离级别.png) - +**这四种隔离级别具体是如何实现的呢?** -> **这四种隔离级别具体是如何实现的呢?** +- 对于「读未提交」隔离级别的事务来说,写操作加排他锁,读操作不加锁; +- 对于「串行化」隔离级别的事务来说,所有写操作加临键锁,所有读操作加共享锁; +- 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 **Read View 来实现的,它们的区别在于创建 Read View 的时机不同,「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View**。 -- 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了; -- 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问; -- 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 **Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 Read View,然后整个事务期间都在用这个 Read View**。 +### MVCC -> **MVCC** +在`MySQL`众多的开源存储引擎中,几乎只有`InnoDB`实现了`MVCC`机制,仅在`RC`读已提交级别、`RR`可重复读级别才会使用`MVCC`机制。 -* Read View 中的字段:活跃事务(启动但还未提交的事务)id列表、活跃事务中的最小事务id、下一个事务id、创建RV的事务id -* 聚簇索引记录中两个跟事务有关的隐藏列:事务id、旧版本指针 +多版本主要依赖`Undo-log`日志来实现,而并发控制则通过表的隐藏字段+`ReadView`快照来实现。当一个事务尝试改动某条数据时,会将原本表中的旧数据放入`Undo-log`日志中;当一个事务尝试查询某条数据时,`MVCC`会生成一个`ReadView`快照。 -![](.\MySql\mvcc.webp) +Read View 中的字段: + +* `m_ids`:表示在生成当前`ReadView`时,系统内活跃的事务`ID`列表。启动但还未提交的事务ID +* `min_trx_id`:活跃的事务列表中,最小的事务`ID`。 +* `max_trx_id`:表示在生成当前`ReadView`时,系统中要给下一个事务分配的`ID`值。 +* `creator_trx_id`:代表创建当前这个`ReadView`的事务`ID`。 + +聚簇索引记录中两个跟事务有关的隐藏列: + +* `TRX_ID`:最近一次改动当前这条数据的事务`ID` +* `ROLL_PTR`:回滚指针。当一个事务对一条数据做了改动后,都会将旧版本的数据放到`Undo-log`日志中,而`ROLL_PTR`就是一个地址指针,指向`Undo-log`日志中旧版本的数据,当需要回滚事务时,就可以通过这个隐藏列,来找到改动之前的旧版本数据。 + +![](./MySql/mvcc.webp) 一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况: @@ -656,7 +621,7 @@ explain select * from tb_user force index(idx_user_pro) where profession=’软 如果记录中的 trx_id 小于 Read View中的最小事务id,或者在最小事务id和下一个事务id之间并且不在活跃事务id中,则直接读取记录值; -如果记录中的 trx_id 在最小事务id和下一个事务id之间并且在活跃事务id中,则**沿着 undo log 旧版本指针往下找旧版本的记录,直到找到 trx_id 「小于」当前事务 的 Read View 中的 min_trx_id 值的第一条记录**。 +如果记录中的 trx_id 在最小事务id和下一个事务id之间并且在活跃事务id中,则沿着 undo log 旧版本指针往下找旧版本的记录,直到找到 trx_id 「小于」当前事务 的 Read View 中的 min_trx_id 值的第一条记录。 @@ -711,7 +676,7 @@ T3 时刻:事务 A 再执行「当前读语句」 select * from t_test where i ### 表级锁 -MySql表级锁:表锁、元数据锁、意向锁、AUTO-INC锁 +表级锁包括表锁、元数据锁、意向锁、自增锁 **表锁** @@ -730,83 +695,82 @@ unlock tables; -**元数据锁(MDL)** +**元数据锁(Meta Data Lock)** -我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL。MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。 +我们不需要显示的使用 MDL,因为当我们对数据库表进行操作时,会自动给这个表加上 MDL。 -- 对一张表进行 CRUD 操作时,加的是 MDL 读锁; -- 对一张表做结构变更操作的时候,加的是 MDL 写锁; +MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。 -MDL 是在事务提交后才会释放,这意味着**事务执行期间,MDL 是一直持有的**。 +对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁; -申请 MDL 锁的操作会形成一个队列,队列中**写锁获取优先级高于读锁**,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。 +MDL 是在事务提交后才会释放,事务执行期间,MDL 是一直持有的。 +申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。 -**意向锁** + +**意向锁(Intention Lock)** 如果没有「意向锁」,那么加「独占表锁」时,就需要遍历表里所有记录,查看是否有记录存在独占锁,这样效率会很慢。 那么有了「意向锁」,由于在对记录加独占锁前,先会加上表级别的意向独占锁,那么在加「独占表锁」时,直接查该表是否有意向独占锁,如果有就意味着表里已经有记录被加了独占锁,这样就不用去遍历表里的记录。 -所以,**意向锁的目的是为了快速判断表里是否有记录被加锁**。 +所以,意向锁的目的是为了快速判断表里是否有记录被加锁。 - 在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前,需要先在表级别加上一个「意向共享锁」; - 在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前,需要先在表级别加上一个「意向独占锁」; -**意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁和独占表锁发生冲突。** +意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁和独占表锁发生冲突。 -**AUTO-INC 锁** +**自增锁(AUTO-INC Lock)** 声明 `AUTO_INCREMENT` 属性的字段数据库自动赋递增的值,主要是通过 AUTO-INC 锁实现的。 -AUTO-INC 锁是特殊的表锁机制,锁**不再是一个事务提交后才释放,而是再执行完插入语句后就会立即释放**。 - -在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 `AUTO_INCREMENT` 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。 +在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 `AUTO_INCREMENT` 修饰的字段赋值递增的值,等插入语句执行完成后,把 AUTO-INC 锁释放掉。 -在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种**轻量级的锁**来实现自增。一样也是在插入数据的时候,会为被 `AUTO_INCREMENT` 修饰的字段加上轻量级锁,**然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁**。 +在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。一样也是在插入数据的时候,会为被 `AUTO_INCREMENT` 修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁。 ### 行级锁 -共享锁(S锁)满足读读共享,读写互斥。独占锁(X锁)满足写写互斥、读写互斥。 +行级锁包括记录锁、间隙锁、临键锁、插入意向锁 在读已提交隔离级别下,行级锁的种类只有记录锁,也就是仅仅把一条记录锁上。 在可重复读隔离级别下,行级锁的种类除了有记录锁,还有间隙锁(目的是为了避免幻读) -**Record Lock** +**记录锁(Record Lock)** 记录锁,也就是仅仅把一条记录锁上; -**Gap Lock** +**间隙锁(Gap Lock)** 间隙锁,锁定一个范围,但是不包含记录本身; -间隙锁,只存在于可重复读隔离级别,**目的是为了解决可重复读隔离级别下幻读的现象**。 +间隙锁只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。 -**间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的**。 +间隙锁之间是兼容的,即两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系,因为间隙锁的目的是防止插入幻影记录而提出的。 -**Next-Key Lock** +**临键锁(Next-Key Lock)** -临键锁,Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。 +临键锁,Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身,即锁定左开右闭的区间。 -**如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。** +如果一个事务获取了 X 型的 next-key lock,那么另外一个事务在获取相同范围的 X 型的 next-key lock 时,是会被阻塞的。数据库默认加临键锁。 -**插入意向锁** +**插入意向锁(Insert Intention Lock)** 一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。 -如果有的话,插入操作就会发生**阻塞**,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个**插入意向锁**,表明有事务想在某个区间插入新记录,但是现在处于等待状态。 +如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。 @@ -1026,11 +990,39 @@ Insert 语句在正常执行时是不会生成锁结构的,它是靠聚簇索 ## 日志 -**undo log(回滚日志)**:是 Innodb 存储引擎层生成的日志,实现了事务中的**原子性**,主要**用于事务回滚和 MVCC**。 +### undo log + +undo log 是 Innodb 存储引擎层生成的日志,主要用于事务回滚和 MVCC。 + +`InnoDB`默认是将`Undo-log`存储在`xx.ibdata`共享表数据文件当中,默认采用段的形式存储。 + +也就是当一个事务尝试写某行表数据时,首先会将旧数据拷贝到`xx.ibdata`文件中,将表中行数据的隐藏字段:`roll_ptr`回滚指针会指向`xx.ibdata`文件中的旧数据,然后再写表上的数据。 + +那`Undo-log`究竟在`xx.ibdata`文件中怎么存储呢? + +在共享表数据文件中,有一块区域名为`Rollback Segment`回滚段,每个回滚段中有`1024`个`Undo-log Segment`,每个`Undo`段可存储一条旧数据,而执行写`SQL`时,`Undo-log`就是写入到这些段中。在`MySQL5.5`版本前,默认只有一个`Rollback Segment`,而在`MySQL5.5`版本后,默认有`128`个回滚段,即支持`128*1024`条`Undo`记录同时存在。 + +当一个事务需要回滚时,本质上并不会以执行反`SQL`的模式还原数据,而是直接将`roll_ptr`回滚指针指向的`Undo`记录,从`xx.ibdata`共享表数据文件中拷贝到`xx.ibd`表数据文件,覆盖掉原本改动过的数据。 + + -**redo log(重做日志)**:是 Innodb 存储引擎层生成的日志,实现了事务中的**持久性**,主要**用于掉电等故障恢复**; +### redo log -**binlog (归档日志)**:是 Server 层生成的日志,主要**用于数据备份和主从复制**; +redo log是 Innodb 存储引擎层生成的日志,记录当前`SQL`归属事务的状态,以及记录修改内容和修改页的位置。主要用于掉电等故障恢复。 + +redo log是一种预写式日志,会先记录日志再去更新缓冲区中的数据,所以就算缓冲区数据未被刷写到磁盘,MySQL重启时也可以根据redo log恢复数据,确保持久性。 + +写的`Redo-log`日志,也是写在内存中的`redo_log_buffer`缓冲区,刷盘策略(`innodb_flush_log_at_trx_commit`控制): + +* 0:有事务提交情况下,每间隔1秒刷写一次日志到磁盘; +* 1:每次提交事务时,都刷写一次日志到磁盘。默认 +* 2:每次提交事务时,把日志记录放到内核缓冲区,刷写实际交给操作系统控制。 + + + +### bin log + +bin log 是 Server 层生成的日志,记录每条`SQL`操作日志,主要是用于数据的主从复制与数据恢复/备份。 @@ -1152,8 +1144,71 @@ MySQL 引入了 binlog 组提交(group commit)机制,当有多个事务提 +## Buffer Pool + + + + + +## MyISAM引擎 vs InnoDB引擎 + +**磁盘文件** + +其中使用`MyISAM`引擎的表:`zz_myisam_index`,会在本地生成三个磁盘文件: + +- `zz_myisam_index.frm`:该文件中存储表的结构信息。 +- `zz_myisam_index.MYD`:该文件中存储表的行数据。 +- `zz_myisam_index.MYI`:该文件中存储表的索引数据。 + +从这里可得知一点:`MyISAM`引擎的表数据和索引数据,会分别放在两个不同的文件中存储。 + +而反观使用`InnoDB`引擎的表:`zz_innodb_index`,在磁盘中仅有两个文件: + +- `zz_innodb_index.frm`:该文件中存储表的结构信息。 +- `zz_innodb_index.ibd`:该文件中存储表的行数据和索引数据。 + +**索引支持** + +`MyISAM`表数据和索引数据是分别放在`.MYD、.MYI`文件中,所以注定了`MyISAM`引擎只支持非聚簇索引。而`InnoDB`引擎的表数据、索引数据都放在`.ibd`文件中存储,因此`InnoDB`是支持聚簇索引的。 + +聚簇索引的要求是:索引键和行数据必须在物理空间上也是连续的,而`MyISAM`表数据和索引数据,分别位于两个磁盘文件中,这也就注定了它无法满足聚簇索引的要求。 + +**事务机制** + +使用`InnoDB`存储引擎的表,可以借助`undo-log`日志实现事务机制。而`MyISAM`并未设计类似的技术,在启动时不会在内存中构建`undo_log_buffer`缓冲区,磁盘中也没有相应的日志文件,因此`MyISAM`并不支持事务机制。 + +**故障恢复** + +`InnoDB`引擎由于`redo-log`日志的存在,因此只要事务提交,机器断电、程序宕机等各种灾难情况,都可以用`redo-log`日志来恢复数据。但`MyISAM`引擎同样没有`redo-log`日志,所以并不支持数据的故障恢复,如果表是使用`MyISAM`引擎创建的,当一条`SQL`将数据写入到了缓冲区后,`SQL`还未被写到`bin-log`日志,此时机器断电、`DB`宕机了,重启之后由于数据在宕机前还未落盘,所以丢了也就无法找回。 + +**锁粒度** + +`MyISAM`仅支持表锁,而`InnoDB`同时支持表锁、行锁。 + + + +`MyISAM`引擎优势: + +1. `MyISAM`引擎中会记录表的行数,也就是当执行`count()`时,如果表是`MyISAM`引擎,则可以直接获取之前统计的值并返回。`InnoDB`引擎中是不具备的。 +2. 当使用`delete`命令清空表数据时,`MyISAM`会直接重新创建表数据文件,而`InnoDB`则是一行行删除数据,因此对于清空表数据的操作,`MyISAM`比`InnoDB`快上无数倍。同时`MyISAM`引擎的表,对于`delete`过的数据不会立即删除,而且先隐藏起来,后续定时删除或手动删除。 +3. `MyISAM`引擎中,所有已创建的索引都是非聚簇索引,每个索引之间都是独立的,在索引中存储的是直接指向行数据的地址,而并非聚簇索引的索引键,因此无论走任何索引,都仅需一次即可获得数据,无需做回表查询。 + + + ## SQL优化 +客户端与连接层的优化:调整客户端`DB`连接池的参数和`DB`连接层的参数。 + +`MySQL`结构的优化:合理的设计库表结构,表中字段根据业务选择合适的数据类型、索引。一张表最多最多只能允许设计`30`个字段左右,否则会导致查询时的性能明显下降。 + +`MySQL`参数优化:调整参数的默认值,根据业务将各类参数调整到合适的大小。 + +整体架构优化:引入中间件减轻数据库压力,优化`MySQL`架构提高可用性。例如redis、MQ、读写分离、分库分表。 + +编码层优化:根据库表结构、索引结构优化业务`SQL`语句,提高索引命中率。 + + + ### 主键优化 1. 满足业务需求的情况下,尽量降低主键的长度; @@ -1387,6 +1442,8 @@ show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里 EXPLAIN 或者 DESC命令获取 MySQL 如何执行 SELECT 语句的信息,包括在 SELECT 语句执行过程中,表如何连接和连接的顺序。 +`EXPLAIN` 并不会真的去执行相关的语句,而是通过 查询优化器 对语句进行分析,找出最优的查询方案,并显示对应的信息。 + 语法 :直接在 select 语句之前加上关键字 explain/desc; ![](MySQL\explain.webp) diff --git "a/docs/MySQL/MySQL/\351\200\273\350\276\221\345\255\230\345\202\250\347\273\223\346\236\204.jpeg" "b/docs/MySQL/MySQL/\351\200\273\350\276\221\345\255\230\345\202\250\347\273\223\346\236\204.jpeg" new file mode 100644 index 0000000..0c0e544 Binary files /dev/null and "b/docs/MySQL/MySQL/\351\200\273\350\276\221\345\255\230\345\202\250\347\273\223\346\236\204.jpeg" differ diff --git "a/docs/MySQL/MySQL/\351\232\224\347\246\273\347\272\247\345\210\253.webp" "b/docs/MySQL/MySQL/\351\232\224\347\246\273\347\272\247\345\210\253.png" similarity index 100% rename from "docs/MySQL/MySQL/\351\232\224\347\246\273\347\272\247\345\210\253.webp" rename to "docs/MySQL/MySQL/\351\232\224\347\246\273\347\272\247\345\210\253.png" diff --git "a/docs/Redis/Redis\345\237\272\347\241\200.md" "b/docs/Redis/Redis\345\237\272\347\241\200.md" index 987f478..b4cac6b 100644 --- "a/docs/Redis/Redis\345\237\272\347\241\200.md" +++ "b/docs/Redis/Redis\345\237\272\347\241\200.md" @@ -270,16 +270,14 @@ RDB保存的是dump.rdb文件。 **优势**: -1. 适合大规模的数据恢复 -2. 按照业务定时备份 -3. 父进程不会执行磁盘I/О或类似操作,会fork一个子进程完成持久化工作,性能高。 -4. RDB文件在内存中的**加载速度要比AOF快很多** - - +1. 适合大规模的数据恢复; +2. 按照业务定时备份; +3. 父进程不会执行磁盘I/О或类似操作,会fork一个子进程完成持久化工作,性能高; +4. RDB文件在内存中的加载速度要比AOF快很多。 **劣势**: -1. 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失从当前至最近一次快照期间的数据,**快照之间的数据会丢失** +1. 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失从当前至最近一次快照期间的数据,快照之间的数据会丢失。 2. 内存数据的全量同步,如果数据量太大会导致IO严重影响服务器性能。因为RDB需要经常fork()以便使用子进程在磁盘上持久化。如果数据集很大,fork()可能会很耗时,并且如果数据集很大并且CPU性能不是很好,可能会导致Redis停止为客户端服务几毫秒甚至一秒钟。 @@ -319,7 +317,7 @@ RDB 在执行快照的时候,数据能修改吗? #### AOF (Append Only File) -**以日志的形式来记录每个写操作**,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但是不可以改写文件,恢复时,以逐一执行命令的方式来进行数据恢复。 +**以日志的形式来记录每个写操作**,将Redis执行过的所有写指令记录下来,只许追加文件但是不可以改写文件,恢复时,以逐一执行命令的方式来进行数据恢复。 默认情况下,redis是没有开启AOF的。 @@ -588,7 +586,7 @@ Redis 主从节点发送的心态间隔是不一样的,而且作用也有一 - Redis 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态,可通过参数repl-ping-slave-period控制发送频率。 - Redis 从节点每隔 1 秒发送 `replconf ack ` 命令,作用: - 检测主从节点网络连接状态; - - 检测命令丢失。 + - 检测命令丢失。 @@ -598,37 +596,6 @@ Redis 主从节点发送的心态间隔是不一样的,而且作用也有一 -**配置:**sentinel.conf - -`sentinel monitor ` : 设置要监控的master服务器,quorum表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数 - -`sentinel auth-pass ` - -其他参数: - -``` -sentinel down-after-milliseconds :指定多少毫秒之后,主节点没有应答哨兵,此时哨兵主观上认为主节点下线 -sentinel parallel-syncs :表示允许并行同步的slave个数,当Master挂了后,哨兵会选出新的Master,此时,剩余的slave会向新的master发起同步数据 -sentinel failover-timeout :故障转移的超时时间,进行故障转移时,如果超过设置的毫秒,表示故障转移失败 -sentinel notification-script :配置当某一事件发生时所需要执行的脚本 -sentinel client-reconfig-script :客户端重新配置主节点参数脚本 -``` - -注意:主机后续可能会变成从机,所以也需要设置 masterauth 项访问密码,不然后续可能报错master_link_status:down - - - -**启动哨兵方式:** - -1. `redis-sentinel /path/to/sentinel.conf` -2. `redis-server /path/to/sentinel.conf --sentinel` - - - -文件的内容,在运行期间会被sentinel动态进行更改。Master-Slave切换后,master_redis.conf、slave_redis.conf和sentinel.conf的内容都会发生改变,即master_redis.conf中会多一行slaveof的配置,sentinel.conf的监控目标会随之调换 - - - **哨兵运行流程和选举原理:** 当一个主从配置中master失效后,哨兵可以选举出一个新的master用于自动接替原master的工作,主从配置中的其他redis服务器自动指向新的master同步数据 @@ -678,8 +645,6 @@ sentinel client-reconfig-script :客户端重新配 redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于16384槽中的的其中一个,集群中的每个节点可以处理0个或最多16384个槽。 -Redis集群有16384个哈希槽每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。 - 如果键所在的槽正好就指派给当前节点,那么节点直接执行这个命令;否则会指引客户端转向正确的节点再次发送要执行的命令。 ![](.\Redis\slot.jpg) diff --git "a/docs/Redis/Redis\351\253\230\347\272\247.md" "b/docs/Redis/Redis\351\253\230\347\272\247.md" index 886a71c..5086f9e 100644 --- "a/docs/Redis/Redis\351\253\230\347\272\247.md" +++ "b/docs/Redis/Redis\351\253\230\347\272\247.md" @@ -252,13 +252,15 @@ Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作 #### **数据库和缓存一致性的几种更新策略** -##### 1. 先更新数据库,再更新缓存 +**1. 先更新数据库,再更新缓存** 问题: -1. 更新数据库成功,更新缓存失败,读到redis脏数据 +1. 更新数据库成功,更新缓存失败,读到redis脏数据-数据不一致问题 -2. 多线程下 +2. 缓存利用率不高:每次更新都放入缓存,不一定用到,浪费缓存 + +3. 多线程下 ``` 【正常逻辑】 @@ -276,11 +278,14 @@ Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作 最终结果,mysql和redis数据不一致: mysql80,redis100 ``` -##### 2. 先更新缓存,再更新数据库 +**2. 先更新缓存,再更新数据库** 问题: -1. 多线程下 +1. 数据不一致问题 +2. 缓存利用率不高 + +3. 多线程下 ``` 【正常逻辑】 @@ -298,7 +303,7 @@ Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作 ----mysql100,redis80 ``` -##### 3. 先删除缓存,再更新数据库 +**3. 先删除缓存,再更新数据库** 问题: @@ -328,7 +333,7 @@ Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作 第二次删除可以使用异步删除,可以增加吞吐量 ``` -##### 4. 先更新数据库,再删除缓存 +**4. 先更新数据库,再删除缓存** 问题: @@ -351,7 +356,7 @@ Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作 -优先使用**先更新数据库,再删除缓存的方案(先更库→后删存)**。理由如下: +优先使用**先更新数据库,再删除缓存的方案**。理由如下: 1. 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满mysql。 @@ -359,85 +364,6 @@ Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作 -#### cannal - -[alibaba/canal: 阿里巴巴 MySQL binlog 增量订阅&消费组件 (github.com)](https://github.com/alibaba/canal) - -canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送 dump 协议 - -MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal ) - -canal 解析 binary log 对象(原始为 byte 流) - - - -[QuickStart · alibaba/canal Wiki (github.com)](https://github.com/alibaba/canal/wiki/QuickStart) - -1. mysql - - 1. 查看MySql binlog是否开启: show variables like 'log_bin'; - - 2. 开启 Binlog 写入功能 :my.cnf 中配置 - - ``` - [mysqld] - log-bin=mysql-bin # 开启 binlog - binlog-format=ROW # 选择 ROW 模式 - server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复 - ``` - - * ROW模式 除了记录sql语句之外,还会记录每个字段的变化情况,能够清楚的记录每行数据的变化历史,但会占用较多的空间。 - - * STATEMENT模式只记录了sql语句,但是没有记录上下文信息,在进行数据恢复的时候可能会导致数据的丢失情况; - * MIX模式比较灵活的记录,理论上说当遇到了表结构变更的时候,就会记录为statement模式。当遇到了数据更新或者删除情况下就会变为row模式; - - 3. 重启mysql - - 4. 授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant - -2. cannal服务端 - - 1. 下载cannal - - 2. 修改配置,主机、账户、密码等 - - 3. 启动 - -3. cannal客户端 [ClientExample · alibaba/canal Wiki (github.com)](https://github.com/alibaba/canal/wiki/ClientExample) - - - - - -### 布隆过滤器 Bloom Filter - -布隆过滤器由「初始值都为 0 的 bit 数组」和「 N 个哈希函数」两部分组成,用来快速判断集合是否存在某个元素。 - -当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。 - -布隆过滤器会通过 3 个操作完成标记: - -- 第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值; -- 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。 -- 第三步,将每个哈希值在位图数组的对应位置的值设置为 1; - -一个元素如果判断结果:存在--元素可能存在;不存在--元素一定不存在 - -布隆过滤器只能添加元素,不能删除元素,因为布隆过滤器的bit位可能是共享的,删掉元素会影响其他元素导致误判率增加 - - - -应用场景: - -1. 解决缓存穿透问题 -2. 黑白名单校验 - - - -为了解决布隆过滤器不能删除元素的问题,布谷鸟过滤器横空出世。https://www.cs.cmu.edu/~binfan/papers/conext14_cuckoofilter.pdf#:~:text=Cuckoo%20%EF%AC%81lters%20support%20adding%20and%20removing%20items%20dynamically,have%20lower%20space%20overhead%20than%20space-optimized%20Bloom%20%EF%AC%81lters. - - - ### 缓存预热/缓存雪崩/缓存击穿/缓存穿透 #### 缓存预热 @@ -446,7 +372,7 @@ canal 解析 binary log 对象(原始为 byte 流) #### 缓存雪崩 -redis**故障或**者redis中**大量的缓存数据同时失效,大量请求直接访问数据库,从而导致数据库压力骤增** +redis故障或者redis中大量的缓存数据同时失效,导致大量的请求直接打到数据库或其他后端系统,造成系统性能急剧下降甚至宕机的现象。 和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。 @@ -467,13 +393,13 @@ redis**故障或**者redis中**大量的缓存数据同时失效,大量请求 #### 缓存穿透 -**缓存和数据库中都没有数据** +缓存穿透是指缓存和数据库中都没有某些数据,而这些数据却被频繁地请求。由于缓存中不存在这些数据,请求每次都会直接打到数据库,导致数据库压力增大,甚至崩溃。这种情况通常发生在攻击者故意发送一些不存在的键值,绕过缓存层直接攻击数据库。 解决: 1. 空对象缓存或缺省值:如果发生了缓存穿透,我们可以针对要查询的数据,在Redis里存一个和业务部门商量后确定的缺省值(比如,零、负数、defaultNull等)。 2. 布隆过滤器:Google布隆过滤器Guava解决缓存穿透 [guava](https://github.com/google/guava/blob/master/guava/src/com/google/common/hash/BloomFilter.java) -3. 非法请求限制:在 API 入口处我们要判断求请求参数是否合理 +3. 参数校验:在接口层对请求参数进行严格校验,过滤掉明显不合法的请求 4. 增强id复杂度,避免被猜测id规律(可以采用雪花算法) 5. 做好数据的基础格式校验 6. 加强用户权限校验 @@ -481,9 +407,7 @@ redis**故障或**者redis中**大量的缓存数据同时失效,大量请求 #### 缓存击穿 -**缓存中没有但数据库中有数据** - -热点key突然失效,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。 +缓存击穿是指某些热点数据由于访问量大,且该数据的缓存刚好在某一时刻失效,导致大量并发请求同时击中数据库,从而造成数据库瞬时压力过大甚至宕机的现象。与缓存雪崩不同,缓存击穿主要集中在某一条或少量几条缓存失效的热点数据上。 解决: @@ -605,6 +529,35 @@ Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方 +### 布隆过滤器 Bloom Filter + +布隆过滤器由「初始值都为 0 的 bit 数组」和「 N 个哈希函数」两部分组成,用来快速判断集合是否存在某个元素。 + +当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。 + +布隆过滤器会通过 3 个操作完成标记: + +- 第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值; +- 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。 +- 第三步,将每个哈希值在位图数组的对应位置的值设置为 1; + +一个元素如果判断结果:存在--元素可能存在;不存在--元素一定不存在 + +布隆过滤器只能添加元素,不能删除元素,因为布隆过滤器的bit位可能是共享的,删掉元素会影响其他元素导致误判率增加 + + + +应用场景: + +1. 解决缓存穿透问题 +2. 黑白名单校验 + + + +为了解决布隆过滤器不能删除元素的问题,布谷鸟过滤器横空出世。https://www.cs.cmu.edu/~binfan/papers/conext14_cuckoofilter.pdf#:~:text=Cuckoo%20%EF%AC%81lters%20support%20adding%20and%20removing%20items%20dynamically,have%20lower%20space%20overhead%20than%20space-optimized%20Bloom%20%EF%AC%81lters. + + + ### 分布式锁 基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。 @@ -829,4 +782,5 @@ AOF: * 主服务器删除一个过期键后,会向从服务器发送DEL命令告知删除过期键。 * 从服务器在执行客户端发送的读命令时,即使碰到过期键也不会删除,而是像处理未过期键一样处理过期键。 -* 从服务器只有接收到主服务器的DEL命令后,才会删除过期键。 \ No newline at end of file +* 从服务器只有接收到主服务器的DEL命令后,才会删除过期键。 + diff --git "a/docs/Spring/Spring/AOP\345\216\237\347\220\206\345\211\226\346\236\220.jpeg" "b/docs/Spring/Spring/AOP\345\216\237\347\220\206\345\211\226\346\236\220.jpeg" new file mode 100644 index 0000000..5094bbd Binary files /dev/null and "b/docs/Spring/Spring/AOP\345\216\237\347\220\206\345\211\226\346\236\220.jpeg" differ diff --git "a/docs/Spring/Spring/AOP\346\200\235\346\203\263\345\256\236\347\216\260\346\226\271\346\241\210.jpeg" "b/docs/Spring/Spring/AOP\346\200\235\346\203\263\345\256\236\347\216\260\346\226\271\346\241\210.jpeg" new file mode 100644 index 0000000..10b9f68 Binary files /dev/null and "b/docs/Spring/Spring/AOP\346\200\235\346\203\263\345\256\236\347\216\260\346\226\271\346\241\210.jpeg" differ diff --git "a/docs/Spring/Spring/AOP\347\233\270\345\205\263\346\246\202\345\277\265.jpeg" "b/docs/Spring/Spring/AOP\347\233\270\345\205\263\346\246\202\345\277\265.jpeg" new file mode 100644 index 0000000..c57acc0 Binary files /dev/null and "b/docs/Spring/Spring/AOP\347\233\270\345\205\263\346\246\202\345\277\265.jpeg" differ diff --git a/docs/Spring/Spring/ApplicationContext.png b/docs/Spring/Spring/ApplicationContext.png new file mode 100644 index 0000000..bfe715a Binary files /dev/null and b/docs/Spring/Spring/ApplicationContext.png differ diff --git a/docs/Spring/Spring/ApplicationContextWeb.jpeg b/docs/Spring/Spring/ApplicationContextWeb.jpeg new file mode 100644 index 0000000..7ed30c9 Binary files /dev/null and b/docs/Spring/Spring/ApplicationContextWeb.jpeg differ diff --git "a/docs/Spring/Spring/ApplicationContext\345\237\272\347\241\200\347\273\247\346\211\277\344\275\223\347\263\273.jpeg" "b/docs/Spring/Spring/ApplicationContext\345\237\272\347\241\200\347\273\247\346\211\277\344\275\223\347\263\273.jpeg" new file mode 100644 index 0000000..3aff102 Binary files /dev/null and "b/docs/Spring/Spring/ApplicationContext\345\237\272\347\241\200\347\273\247\346\211\277\344\275\223\347\263\273.jpeg" differ diff --git a/docs/Spring/Spring/AspectJAwareAdvisorAutoProxyCreator.png b/docs/Spring/Spring/AspectJAwareAdvisorAutoProxyCreator.png new file mode 100644 index 0000000..dda4ef9 Binary files /dev/null and b/docs/Spring/Spring/AspectJAwareAdvisorAutoProxyCreator.png differ diff --git "a/docs/Spring/Spring/IoC\346\225\264\344\275\223\346\265\201\347\250\213.jpeg" "b/docs/Spring/Spring/IoC\346\225\264\344\275\223\346\265\201\347\250\213.jpeg" new file mode 100644 index 0000000..40e806b Binary files /dev/null and "b/docs/Spring/Spring/IoC\346\225\264\344\275\223\346\265\201\347\250\213.jpeg" differ diff --git "a/docs/Spring/Spring/MapperScannerConfigurer\345\216\237\347\220\206.png" "b/docs/Spring/Spring/MapperScannerConfigurer\345\216\237\347\220\206.png" new file mode 100644 index 0000000..e1ac11b Binary files /dev/null and "b/docs/Spring/Spring/MapperScannerConfigurer\345\216\237\347\220\206.png" differ diff --git "a/docs/Spring/Spring/advice\346\216\245\345\217\243.jpeg" "b/docs/Spring/Spring/advice\346\216\245\345\217\243.jpeg" new file mode 100644 index 0000000..9ff4685 Binary files /dev/null and "b/docs/Spring/Spring/advice\346\216\245\345\217\243.jpeg" differ diff --git "a/docs/Spring/Spring/bean\345\220\216\347\275\256\345\244\204\347\220\206\345\231\250.jpeg" "b/docs/Spring/Spring/bean\345\220\216\347\275\256\345\244\204\347\220\206\345\231\250.jpeg" new file mode 100644 index 0000000..d09d5d1 Binary files /dev/null and "b/docs/Spring/Spring/bean\345\220\216\347\275\256\345\244\204\347\220\206\345\231\250.jpeg" differ diff --git "a/docs/Spring/Spring/bean\345\256\236\344\276\213\345\214\226\346\265\201\347\250\213.jpeg" "b/docs/Spring/Spring/bean\345\256\236\344\276\213\345\214\226\346\265\201\347\250\213.jpeg" new file mode 100644 index 0000000..512297b Binary files /dev/null and "b/docs/Spring/Spring/bean\345\256\236\344\276\213\345\214\226\346\265\201\347\250\213.jpeg" differ diff --git "a/docs/Spring/Spring/bean\345\267\245\345\216\202\345\220\216\347\275\256\345\244\204\347\220\206\345\231\250.jpeg" "b/docs/Spring/Spring/bean\345\267\245\345\216\202\345\220\216\347\275\256\345\244\204\347\220\206\345\231\250.jpeg" new file mode 100644 index 0000000..3ac2c5d Binary files /dev/null and "b/docs/Spring/Spring/bean\345\267\245\345\216\202\345\220\216\347\275\256\345\244\204\347\220\206\345\231\250.jpeg" differ diff --git a/docs/Spring/Spring/spring-bean-lifestyle.png b/docs/Spring/Spring/spring-bean-lifestyle.png new file mode 100644 index 0000000..ac87e05 Binary files /dev/null and b/docs/Spring/Spring/spring-bean-lifestyle.png differ diff --git "a/docs/Spring/Spring/\344\270\211\347\272\247\347\274\223\345\255\230.png" "b/docs/Spring/Spring/\344\270\211\347\272\247\347\274\223\345\255\230.png" new file mode 100644 index 0000000..f1f5f90 Binary files /dev/null and "b/docs/Spring/Spring/\344\270\211\347\272\247\347\274\223\345\255\230.png" differ diff --git "a/docs/Spring/Spring/\344\270\211\347\272\247\347\274\223\345\255\230\346\272\220\347\240\201\345\211\226\346\236\220\346\265\201\347\250\213.pdf" "b/docs/Spring/Spring/\344\270\211\347\272\247\347\274\223\345\255\230\346\272\220\347\240\201\345\211\226\346\236\220\346\265\201\347\250\213.pdf" new file mode 100644 index 0000000..662af65 Binary files /dev/null and "b/docs/Spring/Spring/\344\270\211\347\272\247\347\274\223\345\255\230\346\272\220\347\240\201\345\211\226\346\236\220\346\265\201\347\250\213.pdf" differ diff --git "a/docs/Spring/Spring/\345\210\207\345\205\245\347\202\271\350\241\250\350\276\276\345\274\217.png" "b/docs/Spring/Spring/\345\210\207\345\205\245\347\202\271\350\241\250\350\276\276\345\274\217.png" new file mode 100644 index 0000000..f839acf Binary files /dev/null and "b/docs/Spring/Spring/\345\210\207\345\205\245\347\202\271\350\241\250\350\276\276\345\274\217.png" differ diff --git "a/docs/Spring/Spring/\346\263\250\350\247\243\350\247\243\346\236\220\345\216\237\347\220\206.jpeg" "b/docs/Spring/Spring/\346\263\250\350\247\243\350\247\243\346\236\220\345\216\237\347\220\206.jpeg" new file mode 100644 index 0000000..f99860a Binary files /dev/null and "b/docs/Spring/Spring/\346\263\250\350\247\243\350\247\243\346\236\220\345\216\237\347\220\206.jpeg" differ diff --git "a/docs/Spring/Spring/\351\205\215\347\275\256\347\261\273\347\273\204\344\273\266\346\211\253\346\217\217.jpeg" "b/docs/Spring/Spring/\351\205\215\347\275\256\347\261\273\347\273\204\344\273\266\346\211\253\346\217\217.jpeg" new file mode 100644 index 0000000..a4e020e Binary files /dev/null and "b/docs/Spring/Spring/\351\205\215\347\275\256\347\261\273\347\273\204\344\273\266\346\211\253\346\217\217.jpeg" differ diff --git a/docs/Spring/Spring6.md b/docs/Spring/Spring6.md new file mode 100644 index 0000000..2be7138 --- /dev/null +++ b/docs/Spring/Spring6.md @@ -0,0 +1,1744 @@ +## IoC、DI和AOP思想提出 + +**IoC 控制反转思想的提出**: + +IoC思想: Inversion of Control,控制反转,强调的是原来在程序中创建Bean的权利反转给第三方。例如:原来在程序中手动的去 new UserServiceImpl(),手动的去new UserDaoImpl(),而根据IoC思想的指导, 寻求一个第三方去创建UserServiceImpl对象和UserDaoImpl对象。这样程序与具体对象就失去的直接联系。 + +工厂设计模式,BeanFactory来充当第三方的角色,以使用配置文件配置Bean的基本信息,来产生Bean实例 + +**DI 依赖注入思想的提出**: + +DI:Dependency Injection,依赖注入,某个Bean的完整创建依赖于其他Bean(或普通参数)的注入 + +IoC和DI的关系: + +* IoC强调的是Bean创建权的反转,而DI强调的是Bean的依赖关系 +* IoC强调的是Bean创建权的反转,而DI强调的是通过注入的方式反转Bean的创建权,认为DI 是IoC的其中一种实现方式 + +**AOP 面向切面思想的提出**: + +AOP,Aspect Oriented Programming,面向切面编程,是对面向对象编程OOP的升华。OOP是纵向对一个事物的抽象,一个对象包括静态的属性信息,包括动态的方法信息等。而AOP是横向的对不同事物的抽象,属性与属性、方法与方法、对象与对象都可以组成一个切面,而用这种思维去设计编程的方式叫做面向切面编程 + + + +## IoC + +### BeanFactory + +1)导入Spring的jar包或Maven坐标; + +2)定义UserService接口及其UserServiceImpl实现类; + +3)创建beans.xml配置文件,将UserServiceImpl的信息配置到该xml中; + +```xml + +``` + +4)编写测试代码,创建BeanFactory,加载配置文件,获取UserService实例对象。 + +```java +//创建BeanFactory +DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); +//创建读取器 +XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory); +//加载配置文件 +reader.loadBeanDefinitions("beans.xml"); +//获取Bean实例对象 +UserDao userService = (UserService) beanFactory.getBean("userService"); +``` + + + +### ApplicationContext + +ApplicationContext 称为Spring容器,内部封装了BeanFactory,比BeanFactory功能更丰富更强大,使用 ApplicationContext 进行开发时,xml配置文件的名称习惯写成applicationContext.xml + +```java +//创建ApplicationContext,加载配置文件,实例化容器 +ApplicationContext applicationContext = new ClassPathxmlApplicationContext(“applicationContext.xml"); +//根据beanName获得容器中的Bean实例 +UserService userService = (UserService) applicationContext.getBean("userService"); +System.out.println(userService); +``` + +BeanFactory与ApplicationContext的关系: + +1. BeanFactory是Spring的早期接口,称为Spring的Bean工厂,ApplicationContext是后期更高级接口,称之为 Spring 容器; +2. ApplicationContext在BeanFactory基础上对功能进行了扩展,例如:监听功能、国际化功能等。BeanFactory的 API更偏向底层,ApplicationContext的API大多数是对这些底层API的封装; +3. Bean创建的主要逻辑和功能都被封装在BeanFactory中,ApplicationContext不仅继承了BeanFactory,而且 ApplicationContext内部还维护着BeanFactory的引用,所以,ApplicationContext与BeanFactory既有继承关系,又 有融合关系。 +4. Bean的初始化时机不同,原始BeanFactory是在首次调用getBean时才进行Bean的创建,而ApplicationContext则是配置文件加载,容器一创建就将Bean都实例化并初始化好 + +ApplicationContext除了继承了BeanFactory外,还继承了ApplicationEventPublisher(事件发布器)、 ResouresPatternResolver(资源解析器)、MessageSource(消息资源)等。但是ApplicationContext的核心功能还是BeanFactory。 + +![](./Spring/ApplicationContext.png) + +只在Spring基础环境下,即只导入spring-context坐标时,此时ApplicationContext的继承体系: + +![](./Spring/ApplicationContext基础继承体系.jpeg) + +如果Spring基础环境中加入了其他组件解决方案,如web层解决方案,即导入spring-web坐标,此时 ApplicationContext的继承体系: + +![](./Spring/ApplicationContextWeb.jpeg) + +| 实现类 | 简介 | +| ------------------------------------- | -------------------------------------------------------- | +| ClassPathXmlApplicationContext | 通过读取类路径下的 XML 格式的配置文件创建 IoC 容器对象 | +| FileSystemXmlApplicationContext | 通过文件系统路径读取 XML 格式的配置文件创建 IoC 容器对象 | +| AnnotationConfigApplicationContext | 通过读取Java配置类注解创建 IoC 容器对象 | +| XmlWebApplicationContext | web环境下,加载类路径下的xml配置的ApplicationContext | +| AnnotationConfigWebApplicationContext | web环境下,加载磁盘路径下的xml配置的ApplicationContext | + + + +### 基于XML的Spring应用 + +#### SpringBean 的配置 + +##### Bean的常用配置 + +| XML配置方式 | 描述 | +| ------------------------------------------- | ------------------------------------------------------------ | +| `` | Bean的id和全限定名配置 | +| `` | 通过name设置Bean的别名,通过别名也能直接获取到Bean实例 | +| `` | Bean的作用范围,BeanFactory作为容器时取值singleton和prototype | +| `` | Bean的实例化时机,是否延迟加载。BeanFactory作为容器时无效 | +| `` | Bean实例化后自动执行的初始化方法,method指定方法名 | +| `` | Bean实例销毁前的方法,method指定方法名 | +| `` | 设置自动注入模式,常用的有按照类型byType,按照名字byName | +| `` | 指定哪个工厂Bean的哪个方法完成Bean的创建 | + +例如:配置UserDaoImpl由Spring容器负责管理 + +```xml + +``` + +此时存储到Spring容器(singleObjects单例池)中的Bean的beanName是userDao,值是UserDaoImpl对象,可 以根据beanName获取Bean实例 + +```java +applicationContext.getBean("userDao"); +``` + +如果不配置id,则Spring会把当前Bean实例的全限定名作为beanName + +```java +applicationContext.getBean("com.itheima.dao.impl.UserDaoImpl"); +``` + + + +Bean的范围配置: + +默认情况下,单纯的Spring环境Bean的作用范围有两个:Singleton和Prototype + +* singleton:单例,默认值,Spring容器创建的时候,就会进行Bean的实例化,并存储到容器内部的单例池中 ,每次getBean时都是从单例池中获取相同的Bean实例; +* prototype:原型,Spring容器初始化时不会创建Bean实例,当调用getBean时才会实例化Bean,每次 getBean都会创建一个新的Bean实例。 + + + +Bean的延迟加载: + +当lazy-init设置为true时为延迟加载,也就是当Spring容器创建的时候,不会立即创建Bean实例,等待用到时在创 建Bean实例并存储到单例池中去,后续在使用该Bean直接从单例池获取即可,本质上该Bean还是单例的。 + + + +Bean的初始化和销毁方法配置: + +Bean在被实例化后,可以执行指定的初始化方法完成一些初始化的操作,Bean在销毁之前也可以执行指定的销毁方法完成一些操作。 + +除此之外,我们还可以通过实现 InitializingBean 接口,完成一些Bean的初始化操作。 + +```java +public class UserDaoImpl implements UserDao, InitializingBean { + public UserDaoImpl() {System.out.println("UserDaoImpl创建了...");} + public void init(){System.out.println("初始化方法...");} + public void destroy(){System.out.println("销毁方法...");} + //执行时机早于init-method配置的方法 + public void afterPropertiesSet() throws Exception { + System.out.println("InitializingBean..."); + } +} +``` + + + +##### Bean的实例化配置 + +Spring的实例化方式主要如下两种: + +* 构造方式实例化:底层通过构造方法对Bean进行实例化 +* 工厂方式实例化:底层通过调用自定义的工厂方法对Bean进行实例化 + +构造方式实例化Bean又分为无参构造方法实例化和有参构造方法实例化,Spring中配置的几乎都是无参构造方式。 + +有参构造在实例化Bean时,需要参数的注入,通过标签,嵌入在标签内部提供构造参数: + +```xml + + + +``` + +工厂方式实例化Bean,又分为如下三种: + +1. 静态工厂方法实例化Bean + +2. 实例工厂方法实例化Bean + +3. 实现FactoryBean规范延迟实例化Bean + +1)静态工厂方法实例化Bean,其实就是定义一个工厂类,提供一个静态方法用于生产Bean实例,在将该工厂类及其静态方法配置给Spring即可。 + +```java +//工厂类 +public class UserDaoFactoryBean { + //非静态工厂方法 + public static UserDao getUserDao(String name){ + //可以在此编写一些其他逻辑代码 + return new UserDaoImpl(); + } +} + +``` + +```xml + + + +``` + +constructor-arg 标签不仅仅是为构造方法传递参数,只要是为了实例化对象而传递的参数都可以通过标签完成,例如上面通过静态工厂方法实例化Bean所传递的参数也是要通过 constructor-arg 进行传递的。 + +2)实例工厂方法,也就是非静态工厂方法产生Bean实例,与静态工厂方式比较,该方式需要先有工厂对象,再用工厂对象去调用非静态方法,所以在进行配置时,要先配置工厂Bean,再配置目标Bean。 + +```java +//工厂类 +public class UserDaoFactoryBean2 { + //非静态工厂方法 + public UserDao getUserDao(String name){ + //可以在此编写一些其他逻辑代码 + return new UserDaoImpl(); + } +} +``` + +```xml + + + + + + +``` + +3)Spring提供了FactoryBean的接口规范,实现该接口产生Bean实例: + +```java +public class UserDaoFactoryBean3 implements FactoryBean { + public UserDao getObject() throws Exception { //获得实例对象方法 + return new UserDaoImpl(); + } + public Class getObjectType() { //获得实例对象类型方法 + return UserDao.class; + } +} +``` + +```XML + +``` + +Spring容器创建时,FactoryBean被实例化并存储到了单例池singletonObjects中,但是 getObject() 方法尚未被执行,UserDaoImpl也没被实例化,当首次用到UserDaoImpl时,才调用getObject() , 此工厂方式产生的Bean实例不会存储到单例池singletonObjects中,会存储到 factoryBeanObjectCache 缓存池中,并且后期每次使用到userDao都从该缓存池中返回的是同一个userDao实例。 + +**FactoryBean和BeanFactory区别**: + +* **FactoryBean **是 Spring 中一种特殊的 bean,可以在 getObject() 工厂方法自定义的逻辑创建Bean!是一种能够生产其他 Bean 的 Bean。FactoryBean 在容器启动时被创建,而在实际使用时则是通过调用 getObject() 方法来得到其所生产的 Bean。因此,FactoryBean 可以自定义任何所需的初始化逻辑,生产出一些定制化的 bean。一般情况下,整合第三方框架,都是通过定义FactoryBean实现。 + +* **BeanFactory** 是 Spring 框架的基础,其作为一个顶级接口定义了容器的基本行为,例如管理 bean 的生命周期、配置文件的加载和解析、bean 的装配和依赖注入等。BeanFactory 接口提供了访问 bean 的方式,例如 getBean() 方法获取指定的 bean 实例。它可以从不同的来源(例如 Mysql 数据库、XML 文件、Java 配置类等)获取 bean 定义,并将其转换为 bean 实例。同时,BeanFactory 还包含很多子类(例如,ApplicationContext 接口)提供了额外的强大功能。 + +总的来说,FactoryBean 和 BeanFactory 的区别主要在于前者是用于创建 bean 的接口,它提供了更加灵活的初始化定制功能,而后者是用于管理 bean 的框架基础接口,提供了基本的容器功能和 bean 生命周期管理。 + + + +##### Bean的依赖注入配置 + +| 注入方式 | 配置方式 | +| -------------------------- | ------------------------------------------------------------ | +| 通过Bean的set方法注入 | ``
`` | +| 通过Bean的构造方法进行注入 | ``
`` | + +ref 用于引用其他Bean的id。value 用于注入普通属性值。 + +依赖注入的数据类型有如下三种: + +* 普通数据类型,例如:String、int、boolean等,通过value属性指定。 +* 引用数据类型,例如:UserDaoImpl、DataSource等,通过ref属性指定。 +* 集合数据类型,例如:List、Map、Properties等。 + +List 集合 – 普通数据: + +```xml + + + haohao + miaomiao + + +``` + +List 集合 – 引用数据: + +```xml + + + + + + + +``` + +```xml + + + + + + + + + + + + + +``` + + Set 集合: + +```xml + + + + muzi + muran + + + + + + + + + + +``` + +Map 集合: + +```xml + + + + + + + + + + + + + + + + +``` + +注入 Properties 键值对: + +```xml + + + XXX + YYY + + +``` + + + +##### 自动装配方式 + +如果被注入的属性类型是Bean引用的话,那么可以在标签中使用 autowire 属性去配置自动注入方式,属性值有两个: + +* byName:通过属性名自动装配,即去匹配 setXxx 与 id="xxx"(name="xxx")是否一致; +* byType:通过Bean的类型从容器中匹配,匹配出多个相同Bean类型时,报错。 + +```xml + +``` + + + +##### Spring的其他配置标签 + +``标签,除了经常用的做为根标签外,还可以嵌套在根标签内,使用profile属性切换开发环境。指定被激活的环境: + +1. 使用命令行动态参数,虚拟机参数位置加载 -Dspring.profiles.active=test + +2. 使用代码的方式设置环境变量 System.setProperty("spring.profiles.active","test") + +```xml + + + + + + +``` + + + +``标签,用于导入其他配置文件 + +```xml + + +``` + + + +``标签是为某个Bean添加别名,与在 标签上使用name属性添加别名的方式一样: + +```xml + + + + + + + +``` + +在beanFactory中维护着一个名为aliasMap的Map集合,存储别名和 beanName 之间的映射关系. + + + +#### Spring 的get方法 + +| 方法定义 | 返回值和参数 | +| --------------------------------------- | ------------------------------------------------------------ | +| Object getBean (String beanName) | 根据beanName从容器中获取Bean实例,要求容器中Bean唯一,返回值为Object,需要强转 | +| T getBean (Class type) | 根据Class类型从容器中获取Bean实例,要求容器中Bean类型唯一,返回值为Class类型实例, 无需强转 | +| T getBean (String beanName,Class type) | 根据beanName从容器中获得Bean实例,返回值为Class类型实例,无需强转 | + +```java +//根据beanName获取容器中的Bean实例,需要手动强转 +UserService userService = (UserService) applicationContext.getBean("userService"); + +//根据Bean类型去容器中匹配对应的Bean实例,如存在多个匹配Bean则报错 +UserService userService2 = applicationContext.getBean(UserService.class); + +//根据beanName获取容器中的Bean实例,指定Bean的Type类型 +UserService userService3 = applicationContext.getBean("userService", UserService.class); +``` + + + +#### Spring 配置非自定义Bean + +配置非自定义的Bean需要考虑如下两个问题: + +* 被配置的Bean的实例化方式是什么?无参构造、有参构造、静态工厂方式还是实例工厂方式; +* 被配置的Bean是否需要注入必要属性。 + +例如,产生一个指定日期格式的对象,原始代码按如下: + +```java +String currentTimeStr = "2023-08-27 07:20:00"; +SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); +Date date = simpleDateFormat.parse(currentTimeStr); +``` + +可以看成是实例工厂方式,使用Spring配置方式产生Date实例: + +```xml + + + + + + +``` + + + +#### Bean 实例化的基本流程 + +1)加载xml配置文件,解析获取配置中的每个的信息,封装成一个个的BeanDefinition对象; + +2)将BeanDefinition存储在一个名为beanDefinitionMap的Map中; + +3)ApplicationContext底层遍历beanDefinitionMap,使用反射创建Bean实例对象; + +4)创建好的Bean实例对象,被存储到一个名为singletonObjects的Map中; + +5)当执行applicationContext.getBean(beanName)时,从singletonObjects去匹配Bean实例返回。 + +![](./Spring/bean实例化流程.jpeg) + +DefaultListableBeanFactory对象内部维护着一个Map用于存储封装好的BeanDefinitionMap: + +```java +public class DefaultListableBeanFactory extends ... implements ... { + //存储标签对应的BeanDefinition对象 + //key:是Bean的beanName,value:是Bean定义对象BeanDefinition + private final Map beanDefinitionMap; +} +``` + +Spring框架会取出beanDefinitionMap中的每个BeanDefinition信息,反射构造方法或调用指定的工厂方法生成Bean实例对象,所以只要将BeanDefinition注册到beanDefinitionMap这个Map中,Spring就会进行对应的Bean的实例化操作。 + +Bean实例及单例池singletonObjects, beanDefinitionMap中的BeanDefinition会被转化成对应的Bean实例对象 ,存储到单例池singletonObjects中去,在DefaultListableBeanFactory的上四级父类 DefaultSingletonBeanRegistry中,维护着singletonObjects,源码如下: + +```java +public class DefaultSingletonBeanRegistry extends ... implements ... { + //存储Bean实例的单例池 + ////key:是Bean的beanName,value:是Bean的实例对象 + private final Map singletonObjects = new ConcurrentHashMap(256); +} +``` + + + +#### Spring的后处理器 + +Spring的后处理器是Spring对外开发的重要扩展点,允许我们介入到Bean的整个实例化流程中来,以达到动态注册 BeanDefinition,动态修改BeanDefinition,以及动态修改Bean的作用。Spring主要有两种后处理器: + +* BeanFactoryPostProcessor:Bean工厂后处理器,在BeanDefinitionMap填充完毕,Bean实例化之前执行; +* BeanPostProcessor:Bean后处理器,一般在Bean实例化之后,填充到单例池singletonObjects之前执行。 + +##### BeanFactoryPostProcessor + +![](./Spring/bean工厂后置处理器.jpeg) + +BeanFactoryPostProcessor是一个接口规范,实现了该接口的类只要交由Spring容器管理的话,那么Spring就会回调该接口的方法,用于对BeanDefinition注册和修改的功能。 BeanFactoryPostProcessor 定义如下: + +```java +public interface BeanFactoryPostProcessor { + void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory); +} +``` + +ConfigurableListableBeanFactory参数本质就是 DefaultListableBeanFactory,拿到BeanFactory的引用,自然就可以对beanDefinitionMap中的BeanDefinition进行操作了 ,例如对UserDaoImpl的BeanDefinition进行修改操作 + +```java +public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + BeanDefinition userDaoBD = beanFactory.getBeanDefinition("userDao");//获得UserDao定义对象 + userDaoBD.setBeanClassName("com.itheima.dao.impl.UserDaoImpl2"); //修改class + //userDaoBD.setInitMethodName(methodName); //修改初始化方法 + //userDaoBD.setLazyInit(true); //修改是否懒加载 + //... 省略其他的设置方式 ... + } +} +``` + +Spring 提供了一个BeanFactoryPostProcessor的子接口BeanDefinitionRegistryPostProcessor专门用于注册 BeanDefinition操作。 + +```java +public class MyBeanFactoryPostProcessor2 implements BeanDefinitionRegistryPostProcessor { + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory + configurableListableBeanFactory) throws BeansException {} + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) + throws BeansException { + BeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setBeanClassName("com.itheima.dao.UserDaoImpl2"); + beanDefinitionRegistry.registerBeanDefinition("userDao2",beanDefinition); + } +} +``` + +注解开发方式的基本原理就是通过扫描包下的注解,获取添加了注解的类的全类名,然后使用Bean工厂后置处理器注册 BeanDefinition。 + +##### BeanPostProcessor + +![](./Spring/bean后置处理器.jpeg) + +Bean被实例化后,到最终缓存到名为singletonObjects单例池之前,中间会经过Bean的初始化过程,例如:属性的填充、初始方法init的执行等,其中有一个对外进行扩展的点BeanPostProcessor,称为Bean后处理。跟上面的 Bean工厂后处理器相似,它也是一个接口,实现了该接口并被容器管理的BeanPostProcessor,会在流程节点上被 Spring自动调用。 + +```java +public interface BeanPostProcessor { + @Nullable + //在属性注入完毕,init初始化方法执行之前被回调 + default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + @Nullable + //在初始化方法执行之后,被添加到单例池singletonObjects之前被回调 + default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } +} +``` + + + +#### Spring Bean的生命周期 + +Spring Bean的生命周期是从 Bean 实例化之后,即通过反射创建出对象之后,到Bean成为一个完整对象,最终存储到单例池中,这个过程被称为Spring Bean的生命周期。Spring Bean的生命周期大体上分为三个阶段: + +* Bean的实例化阶段:Spring框架会取出BeanDefinition的信息进行判断当前Bean的范围是否是singleton的, 是否不是延迟加载的,是否不是FactoryBean等,最终将一个普通的singleton的Bean通过反射进行实例化; +* Bean的初始化阶段:Bean创建之后还仅仅是个"半成品",还需要对Bean实例的属性进行填充、执行一些Aware 接口方法、执行BeanPostProcessor方法、执行InitializingBean接口的初始化方法、执行自定义初始化init方法 等。该阶段是Spring最具技术含量和复杂度的阶段,Aop增强功能,后面要学习的Spring的注解功能等、 spring高频面试题Bean的循环引用问题都是在这个阶段体现的; +* Bean的完成阶段:经过初始化阶段,Bean就成为了一个完整的Spring Bean,被存储到单例池 singletonObjects中去了,即完成了Spring Bean的整个生命周期。 + + + +Spring Bean的初始化过程涉及如下几个过程: + +* Bean实例的属性填充 +* Aware接口属性注入 +* BeanPostProcessor的before()方法回调 +* InitializingBean接口的初始化方法回调 +* 自定义初始化方法init回调 +* BeanPostProcessor的after()方法回调 + + + +Aware 接口:用于向Spring管理的Bean提供对Spring容器的访问能力。这些接口允许Bean获取Spring框架中一些特定的资源或服务,如BeanFactory、ApplicationContext等。 + + + +Spring在进行属性注入时,会分为如下几种情况: + +* 注入普通属性,String、int或存储基本类型的集合时,直接通过set方法的反射设置进去; +* 注入单向对象引用属性时,从容器中getBean获取后通过set方法反射设置进去,如果容器中没有,则先创建被注入对象Bean实例(完成整个生命周期)后,再进行注入操作; +* 注入双向对象引用属性时,就比较复杂了,涉及了循环引用(循环依赖)问题。 + + + +##### 循环依赖 + +循环依赖问题在Spring中主要有三种情况: + +- 第一种:通过构造方法进行依赖注入时产生的循环依赖问题。Spring 无法解决循环依赖,因为构造器注入需要所有依赖在实例化时就准备就绪。 +- 第二种:通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。Spring 无法解决循环依赖,因为每次都会创建新的实例,无法利用缓存机制。 +- 第三种:通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。三级缓存解决。 + + + +Spring提供了三级缓存存储完整Bean实例 和 半成品Bean实例 ,用于解决循环引用问题。在DefaultListableBeanFactory的上四级父类DefaultSingletonBeanRegistry中提供如下三个Map: + +```java +public class DefaultSingletonBeanRegistry ... { + //1、最终存储单例Bean成品的容器,即实例化和初始化都完成的Bean,称之为"一级缓存" + Map singletonObjects = new ConcurrentHashMap(256); + //2、早期Bean单例池,缓存半成品对象,且当前对象已经被其他对象引用了,称之为"二级缓存" + Map earlySingletonObjects = new ConcurrentHashMap(16); + //3、单例Bean的工厂池,缓存半成品对象,对象未被引用,使用时在通过工厂创建Bean,称之为"三级缓存" + Map> singletonFactories = new HashMap(16); +} +``` + +UserService和UserDao循环依赖的过程: + +1. UserService 实例化对象,但尚未初始化,将UserService存储到三级缓存; + +2. UserService 属性注入,需要UserDao,从缓存中获取,没有UserDao; + +3. UserDao实例化对象,但尚未初始化,将UserDao存储到到三级缓存; + +4. UserDao属性注入,需要UserService,从三级缓存获取UserService,UserService从三级缓存移入二级缓存; + +5. UserDao执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存; + +6. UserService 注入UserDao; + +7. UserService执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存。 + +![](./Spring/三级缓存.png) + + + +#### Spring IoC 整体流程总结 + +![](./Spring/IoC整体流程.jpeg) + +#### Spring xml方式整合第三方框架 + +xml整合第三方框架有两种整合方案: + +* 不需要自定义名空间,不需要使用Spring的配置文件配置第三方框架本身内容,例如:MyBatis; +* 需要引入第三方框架命名空间,需要使用Spring的配置文件配置第三方框架本身内容,例如:Dubbo。 + +##### 不需要自定义名空间 + +Spring整合MyBatis的步骤如下: + +1. 导入MyBatis整合Spring的相关坐标 mybatis-spring; + +2. 编写Mapper和Mapper.xml; + + ```java + public interface UserMapper { + List findAll(); + } + ``` + + ```xml + + + + + + ``` + +3. 配置SqlSessionFactoryBean和MapperScannerConfigurer; + + ```xml + + + + + + + + + + + + + + + ``` + +4. 编写测试代码 + + ```java + ClassPathxmlApplicationContext applicationContext = + new ClassPathxmlApplicationContext("applicationContext.xml"); + UserMapper userMapper = applicationContext.getBean(UserMapper.class); + List all = userMapper.findAll(); + System.out.println(all); + ``` + + + +配置SqlSessionFactoryBean作用是向容器中提供SqlSessionFactory,SqlSessionFactoryBean实现了 FactoryBean 和 InitializingBean 两个接口,所以会自动执行 getObject() 和 afterPropertiesSet() 方法。 + +```java +SqlSessionFactoryBean implements FactoryBean, InitializingBean{ + public void afterPropertiesSet() throws Exception { + //创建SqlSessionFactory对象 + this.sqlSessionFactory = this.buildSqlSessionFactory(); + } + public SqlSessionFactory getObject() throws Exception { + return this.sqlSessionFactory; + } +} +``` + + + +配置MapperScannerConfigurer作用是扫描Mapper,向容器中注册Mapper对应的MapperFactoryBean, MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor和InitializingBean两个接口,会在 postProcessBeanDefinitionRegistry方法中向容器中注册MapperFactoryBean + +```java +class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean{ + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { + ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); + scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ",; \t\n")); + } +} +``` + +```Java +class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner { + public Set doScan(String... basePackages) { + Set beanDefinitions = super.doScan(basePackages); + if (beanDefinitions.isEmpty()) { + } else { + this.processBeanDefinitions(beanDefinitions); + } + } + private void processBeanDefinitions(Set beanDefinitions) { + //设置Mapper的beanClass是org.mybatis.spring.mapper.MapperFactoryBean + definition.setBeanClass(this.mapperFactoryBeanClass); + definition.setAutowireMode(2); //设置MapperBeanFactory进行自动注入 1是根据名称自动装配,2是根据类型自动装配 + } +} +``` + +```java +class ClassPathBeanDefinitionScanner{ + public int scan(String... basePackages) { + this.doScan(basePackages); + } + protected Set doScan(String... basePackages) { + //将扫描到的类注册到beanDefinitionMap中,此时beanClass是当前类全限定名 + this.registerBeanDefinition(definitionHolder, this.registry); + return beanDefinitions; + } +} +``` + +```java +public class MapperFactoryBean extends SqlSessionDaoSupport implements FactoryBean { + public MapperFactoryBean(Class mapperInterface) { + this.mapperInterface = mapperInterface; + } + public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { + this.sqlSessionTemplate = this.createSqlSessionTemplate(sqlSessionFactory); + } + public T getObject() throws Exception { + return this.getSqlSession().getMapper(this.mapperInterface); + } +} +``` + +![](./Spring/MapperScannerConfigurer原理.png) + +整合包里提供了一个SqlSessionFactoryBean和一个扫描Mapper的配置对象,SqlSessionFactoryBean一旦被实例化,就开始扫描Mapper并通过动态代理产生Mapper的实现类存储到Spring容器中。相关的有如下四个类: + +* SqlSessionFactoryBean:需要进行配置,用于提供SqlSessionFactory; +* MapperScannerConfigurer:需要进行配置,用于扫描指定mapper注册BeanDefinition; +* MapperFactoryBean:Mapper的FactoryBean,获得指定Mapper时调用getObject方法; +* ClassPathMapperScanner:definition.setAutowireMode(2) 修改了自动注入状态,所以 MapperFactoryBean中的setSqlSessionFactory会自动注入进去 + + + +##### 需要引入第三方框架命名空间 + +Spring 的 context 是命名空间扩展方式: + +引入context命名空间,在使用context命名空间的标签,使用SpEL表达式在xml或注解中根据key获得value + +```xml + + + + + + + + +``` + +1)在创建DefaultNamespaceHandlerResolver时,为处理器映射地址handlerMappingsLocation属性赋值,并加载命名空间处理器到handlerMappings 中去,Map集合handlerMappings就被填充了很多XxxNamespaceHandler。 + +2)在DefaultBeanDefinitionDocumentReader的parseBeanDefinitions方法中: + +```java +//如果是默认命名空间 +if (delegate.isDefaultNamespace(ele)) { + this.parseDefaultElement(ele, delegate); + //否则是自定义命名空间 +} else { + delegate.parseCustomElement(ele); +} +``` + +```java +public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { + //解析命名空间 + String namespaceUri = this.getNamespaceURI(ele); + //获得命名空间解析器 + NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); + //解析执行的标签 + return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); +} +``` + +在执行resovle方法时,就是从handlerMappings中根据命名空间名称获得对应的处理器对象,此处是ContextNamespaceHandler,最终执行NamespaceHandler的parse方法。 + +ContextNamespaceHandler源码如下,间接实现了NamespaceHandler接口,初始化方法init会被自动调用。由于 context命名空间下有多个标签,所以每个标签又单独注册了对应的解析器,注册到了其父类 NamespaceHandlerSupport的parsers中去了。 + +```java +public class ContextNamespaceHandler extends NamespaceHandlerSupport { + public ContextNamespaceHandler() { + } + public void init() { + this.registerBeanDefinitionParser("property-placeholder", new + PropertyPlaceholderBeanDefinitionParser()); + this.registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser()); + this.registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser()); + this.registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser()); + this.registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser()); + this.registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); + this.registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser()); + this.registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser()); + } +} +``` + + + +外部命名空间标签的执行流程,如下: + +1. 将自定义标签的约束与 物理约束文件与网络约束名称的约束 以键值对形式存储到一个spring.schemas文件里 ,该文件存储在类加载路径的 META-INF里,Spring会自动加载到; +2. 将自定义命名空间的名称 与 自定义命名空间的处理器映射关系 以键值对形式存在到一个叫spring.handlers文件里,该文件存储在类加载路径的 META-INF里,Spring会自动加载到; +3. 准备好NamespaceHandler,如果命名空间只有一个标签,那么直接在parse方法中进行解析即可,一般解析结果就是注册该标签对应的BeanDefinition。如果命名空间里有多个标签,那么可以在init方法中为每个标签都注册一个BeanDefinitionParser,在执行NamespaceHandler的parse方法时在分流给不同的 BeanDefinitionParser进行解析(重写doParse方法即可)。 + + + +Spring xml方式整合第三方框架步骤分析: + +1. 确定命名空间名称、schema虚拟路径、标签名称; + +2. 编写schema约束文件xxx.xsd + +3. 在类加载路径下创建META目录,编写约束映射文件spring.schemas和处理器映射文件spring.handlers + +4. 编写命名空间处理器 XxxNamespaceHandler,在init方法中注册XxxBeanDefinitionParser + + ```java + public class XxxNamespaceHandler extends NamespaceHandlerSupport { + @Override + public void init() { + this.registerBeanDefinitionParser("annotation-driven",new XxxBeanDefinitionParser()); + } + } + ``` + +5. 编写标签的解析器 XxxBeanDefinitionParser,在parse方法中编写实现 + + ```java + public class XxxBeanDefinitionParser implements BeanDefinitionParser { + public BeanDefinition parse(Element element, ParserContext parserContext) { + + } + } + ``` + +使用步骤: + +1. 在applicationContext.xml配置文件中引入命名空间 +2. 在applicationContext.xml配置文件中使用自定义的标 + + + +### 基于注解的Spring应用 + +#### Bean基本注解开发 + +使用@Component 注解替代``标签,被该注解标识的类,会在指定扫描范围内被Spring加载并实例化。可以通过@Component注解的value属性指定当前Bean实例的beanName,也可以省略不写,不写的情况下为当前类名首字母小写。 + +由于JavaEE开发是分层的,为了每层Bean标识的注解语义化更加明确,@Component又衍生出如下三个注解: + +| 注解 | 说明 | +| ----------- | ------------------------------------------------------------ | +| @Component | 该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。 | +| @Repository | 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 | +| @Service | 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 | +| @Controller | 该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 | + + + +使用注解对需要被Spring实例化的Bean进行标注,但是需要告诉Spring去哪找这些Bean,要配置组件扫描路径: + +```xml + +``` + + + +其他基础注解: + +| xml配置 | 注解 | 描述 | +| -------------------------- | -------------- | ------------------------------------------------------------ | +| ` ` | @Scope | 在类上或使用了@Bean标注的方法上,标注Bean的作用范围,取值为 singleton或prototype | +| `` | @Lazy | 在类上或使用了@Bean标注的方法上,标注Bean是否延迟加载,取值为 true和false | +| `` | @PostConstruct | 在方法上使用,标注Bean的实例化后执行的方法 | +| `` | @PreDestroy | 在方法上使用,标注Bean的销毁前执行方法 | + + + +#### Bean依赖注入注解开发 + +Bean依赖注入的注解,主要是使用注解的方式替代xml的 标签完成属性的注入操作: + +```xml + + + + +``` + +Spring主要提供如下注解,用于在Bean内部进行属性注入的: + +| 属性注入注解 | 描述 | +| ------------ | ------------------------------------------------------ | +| @Value | 使用在字段或方法上,用于注入普通数据 | +| @Autowired | 使用在字段或方法上,用于根据类型(byType)注入引用数据 | +| @Qualifier | 使用在字段或方法上,结合@Autowired,根据名称注入 | +| @Resource | 使用在字段或方法上,根据类型或名称进行注入 | + + + +@Autowired注解,当容器中同一类型的Bean实例有多个时,会尝试自动根据名字进行匹配;当容器中同一类型的Bean实例有多个时,且名字与被注入Bean名称不匹配时会报错。 + +@Qualifier配合@Autowired可以完成根据名称注入Bean实例,使用@Qualifier指定名称。 + +@Resource注解既可以根据类型注入,也可以根据名称注入,无参就是根据类型注入,有参数就是根据名称注入。 + + + +依赖注入方式: + +1. 属性注入:属性上加上注解完成注入,不需要生成setter()方法 +2. set注入:setter()方法之上加上注解 +3. 构造方法注入:构造方法之上加上注解 +4. 普通方法注入:普通方法之上加上注解,可以注入单个属性,或者集合 +5. 形参注入:`public UserServiceImpl(@Autowired UserDao userDao) {this.userDao = userDao;}` +6. 只有一个构造器,无注解:当有参数的构造方法只有一个时,注解可以省略。 + + + +@Resource和@Autowired注解有什么区别? + +1. @Resource注解是JDK扩展包中的,也就是说属于JDK的一部分。所以该注解是标准注解,更加具有通用性。@Autowired注解是Spring框架自己的。 + +2. @Resource注解既可以根据类型注入,也可以根据名称注入,无参就是根据类型注入,有参数就是根据名称注入。@Autowired注解默认根据类型装配byType,如果想根据名称装配,需要配合@Qualifier注解一起用。 + +3. @Autowired 支持在构造函数、方法、字段和参数上使用。@Resource 主要用于字段和方法上的注入,不支持在构造函数或参数上使用。 + + +@Resource注解属于JDK扩展包,所以不在JDK当中,需要额外引入以下依赖:【如果是JDK8的话不需要额外引入依赖。高于JDK11或低于JDK8需要引入以下依赖。】 + +```xml + + jakarta.annotation + jakarta.annotation-api + 2.1.1 + +``` + + + +#### 非自定义Bean注解开发 + +非自定义Bean不能像自定义Bean一样使用@Component进行管理,非自定义Bean要通过工厂的方式进行实例化, 使用@Bean标注方法即可,@Bean的属性为beanName,如不指定为当前工厂方法名称。 + +工厂方法所在类必须要被Spring管理。 + +如果@Bean工厂方法需要参数的话,则有如下几种注入方式: + +* 使用@Autowired 根据类型自动进行Bean的匹配,@Autowired可以省略 ; +* 使用@Qualifier 根据名称进行Bean的匹配; +* 使用@Value 根据名称进行普通数据类型匹配。 + +```java +@Bean +@Autowired //根据类型匹配参数 +public Object objectDemo01(UserDao userDao){ + System.out.println(userDao); + return new Object(); +} +@Bean +public Object objectDemo02(@Qualifier("userDao") UserDao userDao, + @Value("${jdbc.username}") String username){ + System.out.println(userDao); + System.out.println(username); + return new Object(); +} +``` + + + +#### Bean配置类的注解开发 + +```xml + + + + + + +``` + +定义一个配置类替代原有的xml配置文件,一般都是在配置类上使用注解完成的: + +```java +@Configuration +@ComponentScan("com.itheima") +@PropertySource("classpath:jdbc.properties") +@Import(OtherConfig.class) +public class ApplicationContextConfig {} +``` + +* @Configuration注解标识的类为配置类,替代原有xml配置文件,该注解第一个作用是标识该类是一个配置类,第 二个作用是具备@Component作用 + +* @ComponentScan 组件扫描配置,替代原有xml文件中的 `` + + base-package的配置方式: + + * 指定一个或多个包名:扫描指定包及其子包下使用注解的类 + + * 不配置包名:扫描当前@componentScan注解配置类所在包及其子包下的类 + +* @PropertySource 注解用于加载外部properties资源配置,替代原有xml中``的配置 + +* @Import 用于加载其他配置类,替代原有xml中的``配置 + +#### Spring 配置其他注解 + +@Primary注解用于标注相同类型的Bean优先被使用权,@Primary 是Spring3.0引入的,与@Component 和@Bean一起使用,标注该注解的Bean的优先级更高,在通过类型获取Bean或通过@Autowired根据类型进行注入时, 会选用优先级更高的。 + +@Profile 标注在类或方法上,标注当前产生的Bean从属于哪个环境,只有激活了当前环境,被标注的Bean才 能被注册到Spring容器里,不指定环境的Bean,任何环境下都能注册到Spring容器里。可以使用以下两种方式指定被激活的环境: + +* 使用命令行动态参数,虚拟机参数位置加载 -Dspring.profiles.active=test +* 使用代码的方式设置环境变量 System.setProperty("spring.profiles.active","test") + +#### Spring注解的解析原理 + +Spring注解的解析原理使用@Component等注解配置完毕后,要配置组件扫描才能使注解生效 + +* xml配置组件扫描: + + ```xml + + ``` + +* 配置类配置组件扫描: + + ```java + @Configuration + @ComponentScan("com.itheima") + public class AppConfig { + } + ``` + + + +xml方式配置组件扫描,component-scan的原理是context命名空间下的自定义标签: + +1)找到对应的命名空间处理器NamespaceHandler 和解析器,查看spring-context包下的spring.handlers文件 + +``` +http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNames +paceHandler +``` + +2)查看 ContextNamespaceHandler 类: + +```java +public class ContextNamespaceHandler extends NamespaceHandlerSupport { + public void init() { + this.registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser()); + } +} +``` + +3)ComponentScanBeanDefinitionParser#parse 扫描标注了@Component的类,生成对应的BeanDefiition并进行注册 + +```java +public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser { + public BeanDefinition parse(Element element, ParserContext parserContext) { + // ... + // Actually scan for bean definitions and register them. + ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element); + Set beanDefinitions = scanner.doScan(basePackages); + // ... + } +} +``` + +```java +public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider { + protected Set doScan(String... basePackages) { + // ... + registerBeanDefinition(definitionHolder, this.registry); + // ... + } +} +``` + + + +使用配置类配置组件扫描,使用AnnotationConfigApplicationContext容器在进行创建时,内部调用了如下代码, 该工具注册了几个Bean后处理器: + +```java +AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); +``` + +![](./Spring/配置类组件扫描.jpeg) + +其中,ConfigurationClassPostProcessor 是 一个 BeanDefinitionRegistryPostProcessor ,经过一系列源码调用,最终也别指定到了 ClassPathBeanDefinitionScanner 的 doScan 方法(与xml方式最终终点一致) + +![](./Spring/注解解析原理.jpeg) + +#### Spring注解方式整合第三方框架 + +整合MyBatis: + +使用@Bean将DataSource和SqlSessionFactoryBean存储到Spring容器中,而MapperScannerConfigurer使用注解@MapperScan进行指明需要扫描的Mapper在哪个包下,使用注解整合MyBatis配置方式如下: + +```java +@Configuration +@ComponentScan("com.itheima") +@MapperScan("com.itheima.mapper") +public class ApplicationContextConfig { + @Bean + public DataSource dataSource(){ + DruidDataSource dataSource = new DruidDataSource(); + //省略部分代码 + return dataSource;} + @Bean + public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource){ + SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); + sqlSessionFactoryBean.setDataSource(dataSource); + return sqlSessionFactoryBean; + } +} +``` + + + +@MapperScan 源码: + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Documented +@Import({MapperScannerRegistrar.class}) +@Repeatable(MapperScans.class) +public @interface MapperScan { + String[] value() default {}; + String[] basePackages() default {}; + Class[] basePackageClasses() default {}; + Class annotationClass() default Annotation.class; + // ... ... +} +``` + +当@MapperScan被扫描加载时,会解析@Import注解,从而加载指定的类,此处就是加载了MapperScannerRegistrar。 + +MapperScannerRegistrar 实现了 ImportBeanDefinitionRegistrar 接口,Spring会自动调用 registerBeanDefinitions方法,该方法中又注册MapperScannerConfigurer类,而MapperScannerConfigurer类作用是扫描Mapper,向容器中注册Mapper对应的MapperFactoryBean,... + +```java +public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware { + //默认执行registerBeanDefinitions方法 + void registerBeanDefinitions(AnnotationMetadata annoMeta, AnnotationAttributes annoAttrs, + BeanDefinitionRegistry registry, String beanName) { + BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class); + //... 省略其他代码 ... + //注册BeanDefinition + registry.registerBeanDefinition(beanName, builder.getBeanDefinition()); + } +} +``` + + + +@Import可以导入如下三种类: + +* 普通的配置类 +* 实现ImportSelector接口的类:返回一个字符串数组,其中每个字符串都是需要被导入的配置类的全限定类名。 +* 实现ImportBeanDefinitionRegistrar接口的类:可以注册BeanDefinition + + + +## AOP + +AOP,Aspect Oriented Programming,面向切面编程,是对面向对象编程OOP的升华。OOP是纵向对一个 事物的抽象,一个对象包括静态的属性信息,包括动态的方法信息等。而AOP是横向的对不同事物的抽象,属性与属性、方法与方法、对象与对象都可以组成一个切面,而用这种思维去设计编程的方式叫做面向切面编程。 + +AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。 + + + +AOP思想的实现方案: + +![](./Spring/AOP思想实现方案.jpeg) + +可以在BeanPostProcessor的after方法中使用动态代理对Bean进行增强,实际存储到单例池singleObjects中的不是当前目标对象本身,而是当前目标对象的代理对象Proxy,这样在调用目标对象方法时,实际调用的是代理对象Proxy的同名方法,起到了目标方法前后都进行增强的功能。 + +```java +public class MockAopBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { + private ApplicationContext applicationContext;//注入Spring容器对象 + + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + MyAdvice myAdvice = applicationContext.getBean(MyAdvice.class);//获得Advice对象 + String packageName = bean.getClass().getPackage().getName(); + if("com.itheima.service.impl".equals(packageName)){ + //对Bean进行动态代理,返回的是Proxy代理对象 + Object proxyBean = Proxy.newProxyInstance( + bean.getClass().getClassLoader(), + bean.getClass().getInterfaces(), + (Object proxy, Method method, Object[] args) -> { + myAdvice.beforeAdvice();//执行Advice的before方法 + Object result = method.invoke(bean, args);//执行目标 + myAdvice.afterAdvice();//执行Advice的after方法 + return result; }); + //返回代理对象 + return proxyBean; + } + return bean; + } + + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} +``` + + + +相关概念: + +![](./Spring/AOP相关概念.jpeg) + + + +### 基于xml配置的AOP + +xml方式配置AOP的步骤: + +1. 导入AOP相关坐标: Spring-aop (Spring Context下包含spring aop) 和 aspectjweaver +2. 准备目标类、准备增强类,并配置给Spring管理; +3. 配置切点表达式(哪些方法被增强); +4. 配置织入(切点被哪些通知方法增强,是前置增强还是后置增强) + +```xml + + + + + + + + + + + +``` + + + +#### 切入点表达式 + +切点表达式是配置要对哪些连接点(哪些类的哪些方法)进行通知的增强。 + +![](./Spring/切入点表达式.png) + +``` +execution([访问修饰符]返回值类型 包名.类名.方法名(参数)) +``` + +* 访问修饰符可以省略不写; +* 返回值类型、某一级包名、类名、方法名可以使用 * 表示任意; +* 包名与类名之间使用单点 . 表示该包下的类,使用双点 .. 表示该包及其子包下的类; +* 参数列表可以使用两个点 .. 表示任意参数 + + + +#### 通知类型 + +| 通知名称 | 配置方式 | 执行时机 | +| -------- | ------------------------- | -------------------------------------------------------- | +| 前置通知 | `< aop:before >` | 目标方法执行之前执行 | +| 后置通知 | `< aop:after-returning >` | 目标方法执行之后执行,目标方法异常时,不在执行 | +| 环绕通知 | `< aop:around >` | 目标方法执行前后执行,目标方法异常时,环绕后方法不在执行 | +| 异常通知 | `< aop:after-throwing > ` | 目标方法抛出异常时执行 | +| 最终通知 | `< aop:after >` | 不管目标方法是否有异常,最终都会执行 | + + + +通知方法在被调用时,Spring可以为其传递一些必要的参数: + +| 参数类型 | 作用 | +| ------------------- | ------------------------------------------------------------ | +| JoinPoint | 连接点对象,任何通知都可使用,可以获得当前目标对象、目标方法参数等信息 | +| ProceedingJoinPoint | JoinPoint子类对象,主要是在环绕通知中执行proceed(),进而执行目标方法 | +| Throwable | 异常对象,使用在异常通知中,需要在配置文件中指出异常对象名称 | + + + +#### Advice配置方式 + +AOP的另一种配置方式,该方式需要通知类实现Advice的子功能接口 + +![](./Spring/advice接口.jpeg) + +```java +public class Advices implements MethodBeforeAdvice, AfterReturningAdvice { + public void before(Method method, Object[] objects, Object o) throws Throwable { + System.out.println("This is before Advice ..."); + } + public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws + Throwable { + System.out.println("This is afterReturn Advice ..."); + } +} +``` + +```xml + + + + +``` + + + +使用aspect和advisor配置区别如下: + +1. 配置语法不同:advisor通过实现接口来确定通知类型;aspect通过配置确定通知类型,更灵活。 +2. 可配置的切面数量不同: 一个advisor只能配置一个固定通知和一个切点表达式; 一个aspect可以配置多个通知和多个切点表达式任意组合,粒度更细。 +3. 使用场景不同:如果通知类型多、允许随意搭配情况下可以使用aspect进行配置; 如果通知类型单一、且通知类中通知方法一次性都会使用到的情况下可以使用advisor进行配置; 在通知类型已经固定,不用人为指定通知类型时,可以使用advisor进行配置,例如Spring事务控制的配置; + + + +#### xml方式AOP原理 + +1)通过xml方式配置AOP时,引入了AOP的命名空间,spring-aop包下的META-INF中的spring.handlers文件含命名空间处理器AopNamespaceHandler + +2)AopNamespaceHandler的init方法中注册了config标签对应的解析器ConfigBeanDefinitionParser + +3)以ConfigBeanDefinitionParser作为入口进行源码剖析,最终会注册一个AspectJAwareAdvisorAutoProxyCreator 进入到Spring容器中 + +![](./Spring/AspectJAwareAdvisorAutoProxyCreator.png) + +AspectJAwareAdvisorAutoProxyCreator 的上上级父类AbstractAutoProxyCreator中的 postProcessAfterInitialization方法返回一个代理对象。 + +```java +//参数bean:为目标对象 +public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { + if (bean != null) { + Object cacheKey = this.getCacheKey(bean.getClass(), beanName); + if (this.earlyProxyReferences.remove(cacheKey) != bean) { + //如果需要被增强,则wrapIfNecessary方法最终返回的就是一个Proxy对象 + return this.wrapIfNecessary(bean, beanName, cacheKey); + } + } + return bean; +} +``` + +最终通过JDK动态代理或Cglib代理创建代理对象放入单例池中。 + +```java +==> this.wrapIfNecessary(bean, beanName, cacheKey) +==> Object proxy = this.createProxy(参数省略) +==> proxyFactory.getProxy(classLoader) +==> this.createAopProxy().getProxy(classLoader) +==> getProxy()是一个接口方法,实现类有两个JdkDynamicAopProxy和CglibAopProxy +==> Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this) +``` + + + +### 基于注解配置的AOP + +```java +@Component +@Aspect //第一步 +public class AnnoAdvice { + @Around("execution(* com.itheima.aop.*.*(..))") //第二步 + public void around(ProceedingJoinPoint joinPoint) throws Throwable { + System.out.println("环绕前通知..."); + joinPoint.proceed(); + System.out.println("环绕后通知..."); + } +} +``` + +注解@Aspect、@Around需要被Spring解析,所以在Spring核心配置文件中需要配置aspectj的自动代理: + +```xml + +``` + +如果核心配置使用的是配置类的话,需要配置注解方式的aop自动代理: + +```java +@Configuration +@ComponentScan("com.itheima.aop") +@EnableAspectJAutoProxy //第三步 +public class ApplicationContextConfig { +} +``` + + + +通知类型: + +```java +//前置通知 +@Before("execution(* com.itheima.aop.*.*(..))") +public void before(JoinPoint joinPoint){} +//后置通知 +@AfterReturning(value="execution(* com.itheima.aop.*.*(..))", value = "retValue") +public void AfterReturning(JoinPoint joinPoint, Object retValue){} +//环绕通知 +@Around("execution(* com.itheima.aop.*.*(..))") +public void around(ProceedingJoinPoint joinPoint) throws Throwable {} +//异常通知 +@AfterThrowing(pointcnt="execution(* com.itheima.aop.*.*(..))", throwing="e") +public void AfterThrowing(Throwable e){} +//最终通知 +@After("execution(* com.itheima.aop.*.*(..))") +public void After(JoinPoint joinPoint){} +``` + + + +切点表达式的抽取,使用一个空方法,将切点表达式标注在空方法上,其他通知方法引用即可: + +```java +@Component +@Aspect +public class AnnoAdvice { + //切点表达式抽取 + @Pointcut("execution(* com.itheima.aop.*.*(..))") + public void pointcut(){} + //前置通知 + @Before("pointcut()") + public void before(JoinPoint joinPoint){} + //后置通知 + @AfterReturning("AnnoAdvice.pointcut()") + public void AfterReturning(JoinPoint joinPoint){} + // ... 省略其他代码 ... +} +``` + + + +#### 注解方式AOP原理 + +在使用xml配置AOP时,是借助的Spring的外部命名空间的加载方式完成的,该标签最终加载了名为AspectJAwareAdvisorAutoProxyCreator 的 BeanPostProcessor ,最终在该BeanPostProcessor中完成了代理对象的生成。 + + + +如果使用 `` 标签开启aop aspectj的自动代理,同样从aspectj-autoproxy标签的解析器入手: + +```java +this.registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser()); +``` + +而AspectJAutoProxyBeanDefinitionParser代码内部,最终也是执行了和xml方式AOP一样的代码: + +```java +registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source) +``` + +注册一个AnnotationAwareAspectJAutoProxyCreator(是AspectJAwareAdvisorAutoProxyCreator的子类) 进入到Spring容器中,然后postProcessAfterInitialization 方法返回一个代理对象放入单例池。 + + + +如果使用的是核心配置类的话,查看@EnableAspectJAutoProxy源码,使用的是@Import导入相关解析类AspectJAutoProxyRegistrar,而AspectJAutoProxyRegistrar类最终也是注册了 AnnotationAwareAspectJAutoProxyCreator 这个类: + +```java +registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source) +``` + + + +![](./Spring/AOP原理剖析.jpeg) + +### 基于AOP的声明式事务控制 + +Spring事务编程相关的类主要有如下三个: + +| 事务控制相关类 | 解释 | +| ----------------------------------------- | ------------------------------------------------------------ | +| 平台事务管理器 PlatformTransactionManager | 是一个接口标准,实现类都具备事务提交、回滚和获得事务对象的功能,不同持久层框架可能会有不同实现方案 | +| 事务定义 TransactionDefinition | 封装事务的隔离级别、传播行为、过期时间等属性信息 | +| 事务状态 TransactionStatus | 存储当前事务的状态信息,如果事务是否提交、是否回滚、是否有回滚点等 | + + + +#### 基于xml声明式事务控制 + +结合AOP技术,很容易就可以想到,可以使用AOP对Service的方法进行事务的增强: + +* 目标类:自己定义的类 +* 切点:业务类中的所有业务方法 +* 通知类:Spring提供的,通知方法已经定义好,只需要配置即可 + +步骤: + +1. 通知类是Spring提供的,需要导入Spring事务的相关的坐标:导入Spring事务的相关的坐标,spring-jdbc坐标已经引入的spring-tx坐标 +2. 配置目标类放入Spring容器 +3. 使用advisor标签配置切面。 + +```xml + + + + + + + + + + + + + +``` + + + +平台事务管理器PlatformTransactionManager是Spring提供的封装事务具体操作的规范接口,封装了事 务的提交和回滚方法。 + +```java +public interface PlatformTransactionManager extends TransactionManager { + TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException; + void commit(TransactionStatus var1) throws TransactionException; + void rollback(TransactionStatus var1) throws TransactionException; +} +``` + +不同的持久层框架事务操作的方式有可能不同,所以不同的持久层框架有可能会有不同的平台事务管理器实现, 例如,MyBatis作为持久层框架时,使用的平台事务管理器实现是DataSourceTransactionManager。 Hibernate作为持久层框架时,使用的平台事务管理器是HibernateTransactionManager。 + + + +每个事务有很多特性,例如:隔离级别、只读状态、超时时间等,这些信息在开发时可以通过connection进行指定,而此处要通过配置文件进行配置. + +```xml + + + +``` + +name属性:指定哪个方法要进行哪些事务的属性配置,此处需要区分的是切点表达式指定的方法与此处指定的方法的区别?切点表达式,是过滤哪些方法可以进行事务增强;事务属性信息的name,是指定哪个方法要进行哪些事务属性的配置。 + +read-only属性:设置当前的只读状态,如果是查询则设置为true,可以提高查询性能,如果是更新(增删改)操作则设置为false + +timeout属性:设置事务执行的超时时间,单位是秒,如果超过该时间限制但事务还没有完成,则自动回滚事务 ,不在继续执行。默认值是-1,即没有超时时间限制。 + +隔离级别: + +| 隔离级别 | 解释 | +| ---------------- | ------------------------------------------------------------ | +| DEFAULT | 默认隔离级别,取决于当前数据库隔离级别,例如MySQL默认隔离级别是REPEATABLE_READ | +| READ_UNCOMMITTED | A事务可以读取到B事务尚未提交的事务记录,不能解决任何并发问题,安全性最低,性能最高 | +| READ_COMMITTED | A事务只能读取到其他事务已经提交的记录,不能读取到未提交的记录。可以解决脏读问题,但是不能解决不可重复读和幻读 | +| REPEATABLE_READ | A事务多次从数据库读取某条记录结果一致,可以解决不可重复读,不可以解决幻读 | +| SERIALIZABLE | 串行化,可以解决任何并发问题,安全性最高,但是性能最低 | + +传播行为:指在多个事务方法相互调用时,控制事务如何传播和影响彼此的行为的规则 + +| 传播行为 | 解释 | +| ------------------ | ------------------------------------------------------------ | +| REQUIRED(默认值) | A调用B,B需要事务,如果A有事务B就加入A的事务中,如果A没有事务,B就自己创建一个事务。**没有就新建,有就加入** | +| REQUIRED_NEW | A调用B,B需要新事务,如果A有事务就挂起,B自己创建一个新的事务。**挂起之前事务,开启新事务** | +| SUPPORTS | A调用B,B有无事务无所谓,A有事务就加入到A事务中,A无事务B就以非事务方式执行。**有无事务无所谓** | +| NOT_SUPPORTS | A调用B,B以无事务方式执行,A如有事务则挂起。**挂起之前事务,无事务方式执行** | +| NEVER | A调用B,B以无事务方式执行,A如有事务则抛出异常。**有抛出异常,无事务方式执行** | +| MANDATORY | A调用B,B要加入A的事务中,如果A无事务就抛出异常。**有就加入,没有就抛异常** | +| NESTED | A调用B,B创建一个新事务,A有事务就作为嵌套事务存在,A没事务就以创建的新事务执行。 | + + + +xml方式声明式事务控制的原理: + +`` 标签使用的命名空间处理器是TxNamespaceHandler,内部注册的是解析器是 TxAdviceBeanDefinitionParser + +```java +this.registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); +``` + +TxAdviceBeanDefinitionParser中指定了要注册的BeanDefinition: + +```java +protected Class getBeanClass(Element element) { + return TransactionInterceptor.class; +} +``` + +TxAdviceBeanDefinitionParser二级父类AbstractBeanDefinitionParser的parse方法将TransactionInterceptor 以配置的名称注册到了Spring容器中 + +```java +parserContext.registerComponent(componentDefinition); +``` + +TransactionInterceptor中的invoke方法会被执行,跟踪invoke方法,最终会看到事务的开启和提交。 + + + +#### 基于注解声明式事务控制 + +@Transactional 可用在方法或者类上,同样,使用的事务的注解,平台事务管理器仍然需要配置,还需要进行事务注解开关的开启 + +```xml + + + + + +``` + +如果使用全注解的话,使用如下配置类的形式代替配置文件: + +```java +@Configuration +@ComponentScan("com.itheima.service") +@PropertySource("classpath:jdbc.properties") +@MapperScan("com.itheima.mapper") +@EnableTransactionManagement +public class ApplicationContextConfig { + @Bean + public DataSource dataSource(@Value("${atguigu.url}")String url, + @Value("${atguigu.driver}")String driver, + @Value("${atguigu.username}")String username, + @Value("${atguigu.password}")String password){ + DruidDataSource dataSource = new DruidDataSource(); + dataSource.setDriverClassName(driver); + dataSource.setUrl(url); + dataSource.setUsername(username); + dataSource.setPassword(password); + + return dataSource; + } + + @Bean + public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) { + SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean(); + sqlSessionFactoryBean.setDataSource(dataSource); + return sqlSessionFactoryBean; + } + + @Bean + public TransactionManager transactionManager(DataSource dataSource){ + return new DataSourceTransactionManager(dataSource); + } +} +``` + + + +**注意事项**: + +1. 不要在接口上声明@Transactional ,而要在具体类的方法上使用 @Transactional 注解,否则注解可能无效。 + +2. 不要图省事,将@Transactional放置在类级的声明中,放在类声明,会使得所有方法都有事务。故@Transactional应该放在方法级别,不需要使用事务的方法,就不要放置事务,比如查询方法。否则对性能是有影响的。 + +3. 使用了@Transactional的方法,对同一个类里面的方法调用, @Transactional无效。比如有一个类Test,它的一个方法A,A再调用Test本类的方法B(不管B是否public还是private),但A没有声明注解事务,而B有。则外部调用A之后,B的事务是不会起作用的。(经常在这里出错) + +4. 使用了@Transactional的方法,只能是public,@Transactional注解的方法都是被外部其他类调用才有效,故只能是public。在 protected、private 或者默认的方法上使用 @Transactional 注解,它也不会报错,但事务无效。 + +5. spring的事务在抛异常的时候会回滚,如果是catch捕获了,事务无效。可以在catch里面加上throw new RuntimeException(); + +6. 和锁同时使用需要注意:由于Spring事务是通过AOP实现的,所以在方法执行之前会有开启事务,之后会有提交事务逻辑。而synchronized代码块执行是在事务之内执行的,可以推断在synchronized代码块执行完时,事务还未提交,其他线程进入synchronized代码块后,读取的数据不是最新的。 所以必须使synchronized锁的范围大于事务控制的范围,把synchronized加到Controller层或者大于事务边界的调用层! diff --git a/mkdocs.yml b/mkdocs.yml index 2032f59..30e40db 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,11 +18,13 @@ nav: - Java集合: Java/Java集合.md - JUC: JUC/JUC.md - JVM: JVM/JVM.md + - MySQL: MySQL/MySQL.md - Redis: - Redis基础: Redis/Redis基础.md - Redis数据结构: Redis/数据结构.md - Redis高级: Redis/Redis高级.md - 设计模式: 设计模式/设计模式.md + - Spring: Spring/Spring6.md # Theme theme: