🍊 引用计数器算法、可达性分析、强软弱虚引用、GC的过程、三色标记、跨代引用
🎉 引用计数器算法、可达性分析
在JVM中,所有的对象都存在一个对象头。对象头包括了对象的类型信息、对象的状态信息和对象的引用信息。在对象的引用信息中,有一个重要的字段是“引用计数器”,它记录了该对象被引用的次数。当该对象被引用时,计数器增加1;当该对象不被引用时,计数器减少1。当计数器的值为0时,该对象就可以被垃圾回收了。
但是,引用计数器算法存在一个问题,就是无法解决循环引用的问题。如果两个对象相互引用,它们的引用计数器的值始终不为0,就无法进行垃圾回收。因此,JVM采用了可达性分析算法。
如果一个对象已经不再被任何其他对象引用,那么该对象就是不可达的,即它不再被程序使用,可以被回收。在 JVM 中,可达性分析是通过根对象来判断对象是否可达的,比如:当前正在执行的方法中的局部变量和输入参数,线程栈中的对象,静态对象等。判断一个对象是否可达,首先从根对象开始对所有引用进行遍历,找到所有被引用的对象。将这些被引用的对象标记为活动对象,其它对象则被标记为垃圾对象。从活动对象开始对所有引用进行遍历,找到所有被引用的对象,将这些被引用的对象标记为活动对象,其它对象则被标记为垃圾对象。这个过程一直进行下去,直到没有对象可遍历,所有被遍历的非垃圾对象都被标记为活动对象,其它对象都被标记为垃圾对象。
JVM 对不可达对象的处理一般是通过垃圾回收机制来完成的。当 JVM 发现某个对象不再被任何根对象引用时,该对象就变成了不可达对象,这个对象会被标记为垃圾对象。垃圾回收器会在 JVM 空闲时根据特定算法对这些垃圾对象进行回收,回收的过程包括两个阶段:标记和清除。标记阶段:从根对象开始向下遍历所有引用,标记所有被引用的对象,其它对象则被标记为垃圾对象。清除阶段:清除所有被标记为垃圾对象的内存空间,回收这些空间。
🎉 强软弱虚引用
JVM中强软弱虚引用是Java中内存管理的重要概念。
- 强引用是最为常见的引用类型,是指存在一个对象的引用,它会防止对象被垃圾回收器回收。即使内存不足时,JVM也不会回收被强引用引用的对象,除非该对象的引用被明确地赋值为null。
Object obj = new Object(); // obj是一个强引用
- 软引用是比较常用的引用类型之一,它用于描述一些还有用但并非必需的对象,软引用通常用于缓存数据,当内存不足时,JVM可以回收软引用的对象,从而释放缓存空间。当JVM需要内存时,会先回收这些软引用,如果空间仍然不足,才会抛出OOM异常。可以通过SoftReference类来实现软引用。
SoftReference<Object> softRef = new SoftReference<>(new Object()); // softRef是一个软引用
- 弱引用与软引用类似,它也是用于描述一些还有用但并非必需的对象,但是与软引用不同,弱引用被回收的时机更加快速,我们可以使用弱引用来实现一些临时性的对象,比如缓存中的某些对象,当不再需要这些对象时,JVM会自动回收它们。在垃圾回收时,只要发现存在弱引用引用的对象,就会被回收。可以通过WeakReference类来实现弱引用。
WeakReference<Object> weakRef = new WeakReference<>(new Object()); // weakRef是一个弱引用
- 虚引用是最为特殊的引用类型,它与前面的三种引用类型不同,虚引用并不会影响对象的生命期,而是用于在对象被回收时收到一个系统通知,可以实现资源的释放,比如文件句柄、网络连接等,如果我们直接使用强引用进行管理,容易出现资源泄露的问题。而使用虚引用则可以避免这个问题,因为虚引用在对象被回收时,会收到一个通知,然后程序可以在收到通知之后及时地释放资源。这样,程序员可以在对象被回收时进行一些清理操作。虚引用必须与ReferenceQueue(虚引用队列)一起使用。
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), referenceQueue); // phantomRef是一个虚引用
🎉 GC的过程
在进行垃圾回收前,GC需要首先找出哪些内存对象是需要被回收的。这个过程称为垃圾标记,通常需要遍历整个堆空间,找出所有还在使用的对象。为了标记一个对象是否为垃圾,GC需要维护一个活动对象集合(Active Set)。一开始,所有对象都被认为是活动对象。然后,从根对象(如程序计数器、虚拟机栈、本地方法栈)开始,GC深度遍历所有可以被访问到的对象。如果一个对象无法被访问到,那么它就被认为是垃圾对象。
标记完垃圾对象后,GC便开始对其进行回收。垃圾回收完毕后,堆中的内存空间可能会变得非常零散。为了避免这种情况,GC会对堆中的对象进行移动和整理,使得所有的存活对象都能够在连续的内存空间中占据位置。这个过程称为内存整理。内存整理的主要工作是将所有存活对象移动到一端,然后清理出空闲的内存块。这个过程会涉及到对象的引用修改,需要将所有指向存活对象的引用进行更新。
当一个对象变成不可达时,它就成为了垃圾,需要被垃圾收集器回收。但是,垃圾收集器不会立即回收这个对象,而是把它放到F-Queue队列中,等待一个低优先级的线程在后台去读取这些不可达的对象。当线程调用这些对象的finalize()方法时,如果这个方法被覆盖过并且被调用过,那么虚拟机将视这个对象为不需要再执行finalize()方法了,否则它会被放回到待回收的集合中,等待下一次垃圾回收。如果在第二次标记时,这个对象还没有被重新关联到引用链上,那么就真的可以被垃圾回收器回收了。所以,finalize()方法实际上是一个对象的最后一次机会去逃脱垃圾回收的命运。
🎉 三色标记
三色标记算法是一种用于垃圾回收的算法,它可以识别并回收不再使用的内存空间,从而避免内存泄漏的问题。该算法实现的核心思想是通过将内存对象标记为三种状态中的一种来实现垃圾回收。三色标记算法将内存对象标记为白色、灰色和黑色三种状态。一开始,所有的对象都是白色的,表示这些对象都是可回收的垃圾。当程序运行时,每次访问一个对象时,该对象的状态会从白色变成灰色;灰色对象表示正在被垃圾回收器扫描的对象。当垃圾回收器遍历某个对象时,该对象被标记为灰色。在遍历完该对象的所有引用之后,该对象就被标记为黑色。如果某个灰色对象引用了某个白色对象,则该白色对象也被标记为灰色;黑色对象表示已经被垃圾回收器扫描到的对象。
通过三色标记算法,可以有效地避免内存泄漏问题,并实现高效的垃圾回收。值得注意的是,该算法需要在程序运行时频繁地标记对象的状态,因此可能会对程序的性能产生一定的影响。在三色标记算法中,如果存在循环引用问题,会导致算法无法正确地标记对象的颜色。例如,如果对象A引用了对象B,而对象B也引用了对象A,则在第一次标记时,A和B都会被标记为灰色,但是在扫描完A后,由于B还未被扫描,因此B的颜色仍然为灰色,而垃圾收集器并不知道这是一个循环引用的问题,因此会将B标记为黑色,从而造成垃圾回收器无法回收B。为了解决JVM三色标记算法中的循环引用问题,可以打破循环引用,常用的方法是使用“延迟引用”。具体来说,当遍历到一个对象的引用时,不立即标记为灰色,而是将它暂时记录下来,等到该对象被标记为黑色时,再将它标记为灰色。这样可以避免循环引用问题,同时也不会增加太多的开销。
JVM三色标记的工作原理可以概括为以下几个步骤:首先,垃圾回收器将所有对象都涂成白色。然后,从根对象开始遍历所有的对象,将所有可达的对象涂成灰色。在遍历过程中,如果发现某个灰色对象引用了某个白色对象,则将该白色对象涂成灰色。当所有可达对象都被涂成灰色后,垃圾回收器将所有黑色对象保留下来,将其余白色对象清除。最后,将所有黑色对象重新涂成白色。
🎉 跨代引用
跨代引用是指在堆内存中,年轻代中的对象被老年代中的对象引用的情况。当进行年轻代的垃圾回收(minor gc)时,需要判断哪些对象还需要保留,哪些对象可以被回收。如果按照常规思路,需要遍历老年代中所有的对象,非常耗费时间和性能。为了优化跨代引用的垃圾回收,JVM引入了一种抽象数据结构——记忆集。记忆集是非收集区域指向收集区域的指针集合,记录了老年代对象引用年轻代对象的指针。在进行年轻代垃圾回收时,只需要遍历记忆集中被标记的指针,就可以确定哪些对象需要保留,哪些对象可以被回收。
跨代引用主要有几种情况:第一种是将对象从年轻代移动到老年代时,需要将指向该对象的引用从年轻代的引用表中复制到老年代的引用表中,以确保对象在移动后仍能够被访问。第二种是在进行Full GC(Full Garbage Collection,即对整个堆空间进行垃圾收集)时,会遍历整个堆空间。如果在堆空间中发现一个对象被另一个对象所引用,且该被引用的对象在老年代中,而引用该对象的对象在年轻代中,就需要进行跨代引用。第三种是在进行压缩垃圾收集时,需要将所有可达对象移动到内存区域的起始位置。如果一个对象在年轻代中,而它所引用的对象在老年代中,就需要进行跨代引用。
记忆集采用了一些优化机制,如卡表和写屏障,避免了全局扫描老年代的低效率问题。卡表是一个大小等于老年代的位图,它将老年代按照固定大小(默认为512B)分成很多个区域,每个区域对应卡表中的一个位。当年轻代中的对象与老年代中的对象建立关联时,虚拟机会将这个老年代区域对应的卡表位标记为“脏”,表明它需要被扫描。这样,GC时只需要扫描所有被标记为“脏”的老年代区域,而不是全局扫描老年代。写屏障也是一种优化机制,它用于捕获在年轻代中产生的对象引用,将其放入到卡表中。当年轻代中的对象被分配内存时,虚拟机会通过写屏障来监视对象的引用情况。如果有一个对象的引用发生了变化,比如一个对象被移动到了另一个区域,虚拟机会通过写屏障将这个对象的新引用信息更新到相应的卡表中,保证卡表的准确性和正确性。这样,JVM在进行垃圾回收时,可以避免不必要的扫描和浪费,提高了垃圾回收的效率和性能。
🍊 内存泄漏与堆积、溢出
内存泄漏是程序在分配内存后,由于设计或编写缺陷无法释放已分配的内存,从而导致系统或进程逐渐耗尽可用的内存空间。一般有三种原因:第一种是变量未销毁,即定义并分配内存的变量在程序运行结束后未被销毁,会导致内存泄漏;第二种是指针未及时释放内存,以指针的形式分配内存后未及时释放会产生内存泄漏;第三种是内存管理错误,通常是程序中使用错误的内存分配和释放方法,例如使用了malloc/new分配内存但未使用free/delete释放内存。
内存泄漏通常会导致程序运行变慢或崩溃,因此可以使用编译器调试工具如Visual Studio等捕获内存泄漏,然后跟踪变量,检查变量是否及时释放,还可以使用内存管理工具如Valgrind检测和调试内存泄漏,最后可以使用智能指针来避免内存泄漏,智能指针可以自动管理内存空间,避免内存泄漏的发生。
内存泄漏会让内存不停地增加,最后会爆满,导致程序崩溃。这种情况通常是由代码导致的。我们可以用visualVM这个工具来进行内存转储,查看哪个类占用了太多的内存空间,然后再检查它所引用的实例和引用。最后,我们可以定位到代码的具体问题。如果我们的堆内存很大,使用visualVM产生的资源成本太高,我们可以尝试使用轻量级的jmap工具来生成堆转储快照进行分析,这种方法与使用visualVM的思路相同。
内存溢出就是当程序试图向内存申请空间时,由于申请的空间太大超出了系统或进程可分配的内存空间,导致程序无法正常运行。内存溢出的原因主要有三种,第一种是申请空间过大,当程序向内存申请过大的空间时,容易导致内存溢出,可以使用分片申请空间的方法来避免。第二种是内存泄漏,即使程序本身没有缺陷,也可能因为内存泄漏导致内存耗尽从而造成内存溢出。第三种是错误的内存管理,例如使用了错误的内存分配和释放方法或指针操作错误等。为了避免内存溢出,可以在程序开始时预留一定的空间,使用内存池提高程序效率,使用Memcheck、Purify等工具进行内存溢出分析报告,改进内存管理方法使用智能指针等方法减少内存泄漏和溢出的问题,采用一些有效的内存优化技术减少内存占用提高程序效率和稳定性。
🍊 JVM调优经验
在JVM中,FGC指的是全垃圾收集,这是一个对整个堆内存进行垃圾回收的过程。然而,它也会让应用程序暂停,并且会影响应用程序的性能,这是我们不想看到的。FGC通常在以下情况下发生:首先是堆内存不足,当堆内存不足时,JVM会启动FGC以释放内存空间。其次是大量对象生成,当应用程序生成大量对象时,堆内存可能会很快被占满,此时JVM会触发FGC。还有一种情况是对象生命周期短,如果应用程序中大量对象的生命周期很短,那么这些对象很快就会成为垃圾,导致JVM启动FGC。为了减少FGC的出现,我们可以采取以下策略。首先,增加堆内存的大小可以减少由于内存不足而导致的FGC。其次,通过对代码进行优化,减少不必要的对象生成,可以减少FGC的发生。此外,我们可以在对象的生命周期结束后尽可能地重用这些对象,避免频繁的对象生成和回收。还有一种方法是使用对象池等技术,这可以减少对象的创建和销毁,从而减少FGC的发生。最后,在程序需要暂停的空闲时间,可以手动触发System.gc()方法,对垃圾进行回收,从而减少FGC的发生。
JVM调优步骤:首先,我们需要收集数据。我们可以使用jstat命令来监视JVM的内存和处理器使用信息,也可以使用jmap命令生成堆转储快照。另外,我们还可以使用GUI工具如JConsole或VisualVM对CPU、内存或堆使用状态进行监视。第二步,我们需要分析数据。通过使用工具分析收集到的数据,我们可以计算GC吞吐量和新生代大小等,也可以查看堆转储信息,分析堆中对象的分布情况,是否有内存泄漏等问题。接下来,第三步,我们需要制定具体的优化方案。我们可以根据分析的数据确定具体的优化方案,比如适当调整内存大小、调整垃圾回收机制、优化代码等。对于GC调优,可以尝试调整GC算法、分配大对象空间、增加GC并行度等。对于内存调优,可以尝试减少对象的创建、复用对象等。第四步,我们需要验证优化效果。我们可以使用性能测试工具如jmeter或ab进行压力测试,以验证优化效果是否符合预期。最后,第五步,我们需要持续监控。在优化后,我们需要持续监控应用程序,及时发现并解决新问题,进行JVM调优。
JVM调优其实十分复杂,针对不同场景的问题,我们可以从以下几个角度进行设计:
首先,如果是大访问压力下,MGC频繁一些是正常的,只要MGC延迟不导致停顿时间太长或者引发FGC,可以适当增大Eden空间大小,降低频繁程度。当然,要注意空间增大对垃圾回收产生的停顿时间增长是否可以接受。
其次,如果是MinorGC频繁且容易引发Full GC,需要分析MGC存活对象的大小,是否能够全部移动到S1区。如果S1区大小小于MGC存活对象大小,这批对象会直接进入老年代。这种情况下,应该在系统压测的情况下,实时监控MGC存活对象的大小,并合理调整Eden和S区的大小以及比例。
第三,如果由于大对象创建频繁导致Full GC频繁,可以通过控制JVM参数来优化对象的大小。如果代码层面无法优化,则需要考虑调高参数的大小,或者定时脚本触发Full GC,尽量保证该对象确实是长时间使用的。
第四,如果MGC和FGC的停顿时间长导致影响用户体验,需要考虑减少堆内存大小,包括新生代和老年代。也要考虑线程是否及时达到了安全点,查看安全点日志并对代码进行针对性调整。
最后,如果出现内存泄漏导致MGC和FGC频繁,就需要对代码进行大范围的调整,例如大循环体中的new对象,未使用合理容器进行对象托管等等。无论如何,JVM调优的目的就是在系统可接受的情况下达到一个合理的MGC和FGC的频率以及可接受的回收时间。
🌟 2.深入理解MySQL关系型数据库
索引数据结构、脏读、 不可重复读、幻读、隔离级别、原子性底层实现原理(undo log日志 )、 一致性底层实现原理、持久性底层实现原理(redo log机制)、隔离性底层实现原理(MVCC多版本并发控制)、BufferPool缓存机制、行锁、表锁、间隙锁、死锁、主键自增长实现原理、索引失效、聚集索引、辅助索引、覆盖索引、联合索引、SQL的执行流程、有MySQL调优经验,如表结构设计优化、SQL优化、灾备处理、异常发现处理、数据服务、数据分区分库分表、主从复制、读写分离、高可用(双主故障切换、高可用性与可伸缩性、组复制)经验。
🍊 索引数据结构
B树和B+树都是基于平衡多叉树的结构,用于快速查找和排序大量数据。B树的每个节点可以存储关键码和数据,而B+树只在叶子节点中存储数据,非叶子节点仅存储索引信息。B+树相比B树具有更高效的磁盘IO、更适合范围查询和排序以及插入和删除操作更加高效等优势。在查询数据时,B+树的叶子节点包含所有的关键字数据,而非叶子节点仅仅包含索引数据,从而能够更好地适应范围查找和排序操作。
MySQL是从磁盘读取数据到内存的,是以磁盘块为基本单位的,位于同一磁盘块中的数据会被一次性读取出来,不是按需读取。InnoDB存储引擎使用页作为数据读取单位,页面是其磁盘管理的最小单位,一页的大小默认为16kb。系统的一个磁盘块的存储空间往往没有这么大,所以InnoDB每次申请磁盘空间时都会是多个地址连续磁盘块来达到页的大小16KB。在查询数据时,一个页中的每条数据都能定位数据记录的位置,这会减少磁盘I/O的次数,提高查询效率。InnoDB存储引擎在设计时是将根节点常驻内存的,力求达到树的深度不超过3,也就是说I/O不超过3次。
结合B树和B+树的特点以及对磁盘的分析,我们可以看出,B+树更适合大量数据的储存和查询。B+树的叶子节点之间通过指针串联,形成一个有序链表,因此在进行区间查询时只需要遍历叶子节点即可,数据访问效率更高。B+树的非叶子节点数目比B树的节点数目大得多,因为B+树的非叶子节点只存储关键码,因此可以显得更矮胖。B+树相比于B树,高度更低,因而访问更快。通过对数据库索引结构和磁盘基础设施的了解,我们可以更好地理解和优化数据库查询性能。
🍊 隔离级别、脏读、 不可重复读、幻读、幻影行
在数据库中隔离级别是多个事务之间可以看到对方对数据的更改情况。比如,一个事务在修改数据时,另一个事务能不能够看到数据在修改,这些修改能不能可以取消。目前常见的隔离级别有四种:读未提交、读已提交、可重复读和串行化。
举个例子,假设有两个人Tom和Jerry同时向银行存款,Tom存了100元,Jerry存了200元。
如果他们的事务隔离级别为读未提交,那么在Tom存款未提交之前,Jerry就可以看到Tom的存款已经生效了。但如果Tom的存款被回滚,Jerry之前看到的数据就是脏数据。读未提交隔离级别是最低的隔离级别,它允许一个事务读取另一个事务未提交的数据。这可能会导致脏读的情况,也就是读取到了未提交的数据,如果数据回滚,读取的数据将变得无效。
如果隔离级别为读已提交,那么只有在Tom的存款事务提交后,Jerry才能看到已经生效,这意味着读已提交隔离级别会引入小幅的延迟,因为Jerry必须等待Tom的事务提交才能看到结果。读已提交隔离级别要求一个事务只能读取另一个已经提交了的数据,这样就避免了脏读出现的情况。但它可能会导致不可重复读的问题,也就是在同一事务内,同样的查询条件下多次查询同一数据,但是得到的结果不同。这是因为另一个事务在该事务两次查询之间修改了数据。
如果隔离级别为可重复读,那么Jerry可以在Tom的事务提交前多次查询,因此数据的一致性得到更好的保障,但是会消耗更多的系统资源来维护一致性。可重复读隔离级别要求一个事务在执行过程中多次查看同样的数据,它能够保证在一个事务内多次查询同一数据时得到的结果是一致的。但它可能会导致幻读的问题,也就是在同一事务内,同样的查询条件下多次查询数据,但是得到的结果不同,这与不可重复读的区别在于幻读是由于另一个事务插入了新数据导致的,而不是修改数据。
如果隔离级别为串行化,那么Tom和Jerry的存款事务必须一个一个地执行,不能同时进行,这意味着一个事务必须在另一个事务完成之后才能执行,这将会带来更高的延迟和更大的系统资源开销。串行化隔离级别是最高的隔离级别,它要求所有的事务串行执行,避免了并发访问产生的所有问题。但它会导致更高的延迟和更大的系统资源开销。
MySQL默认的隔离级别是可重复读,这是因为MySQL认为可重复读是一个良好的默认隔离级别,可以提供足够的隔离性和性能。在可重复读隔离级别下,每个事务读取的数据都是一致的,即使其他事务对数据进行了修改,它们的修改也不会影响到当前事务的读取结果。另外,可重复读隔离级别也可以提供足够的性能。因为它不会对读取数据加锁,而是使用多版本并发控制(MVCC)机制来实现隔离性。这可以避免了对数据的过度访问和锁竞争,从而提高了并发性能。
可重复读可以避免脏读和不可重复读的问题,但存在幻读问题,并且在MySQL 5.7版本中将其作为一个已知的问题公开了。在MySQL 8.0版本中引入了一种新的隔离级别——可重复读快照隔离级别,它可以解决幻读问题,同时保持了可重复读级别的并发性能。它是在可重复读隔离级别的基础上做的优化。
可重复读快照隔离级别的实现方式是在事务开始时,创建一个事务快照,这个快照包含了所有在事务开始之前已提交的数据。在事务执行过程中,读取的都是这个快照中的数据,而不是直接读取数据库中的数据。事务执行过程中,其他事务对数据的修改不会影响到正在执行的事务。这样的话,对于同一个事务,在可重复读隔离级别下,多次读取同一数据时,得到的结果都是一样的。可重复读快照隔离级别与可重复读隔离级别最大的区别在于当有新的事务加入时,可重复读隔离级别下的事务会重新建立快照,而在可重复读快照隔离级别中,事务快照只会在事务开始时被建立,因此这个隔离级别的并发性能更好。
只不过可重复读快照隔离级别不是绝对安全的,因为在事务执行过程中,如果有其他事务对数据进行了删除操作,那么当前事务在读取数据时可能会出现“幻影行”的情况。在数据库中,幻影行指的是一个事务在执行查询操作时,可能会发现一些之前不存在的行或者少了一些行,这些行就像幻影一样突然出现或消失了。可重复读快照隔离级别只能保证读取到的数据与事务开始时相同,但它并不能防止其他并发事务在事务执行过程中更新或插入数据。所以,当一个事务在读取数据时,如果同时有其他事务在对数据进行增删改操作,就可能会出现幻影行的情况。
为了解决这个问题,需要使用行级锁或使用串行化隔离级别。行级锁是指在读取数据时,锁定当前使用的行,防止其他事务同时对该行进行修改,保证当前事务读取的是一致的数据。对于幻影行问题,当一个事务在执行查询时,如果发现其他事务正在进行插入、更新或删除操作,该事务会锁定当前查询的行,直到其他事务操作完成后再进行查询,从而避免出现幻影行。
使用串行化隔离级别时,所有事务都将被串行化执行,即每个事务执行时都需要等待前一个事务执行完成后才能开始执行,从而避免出现幻影行。在串行化隔离级别下,所有的数据读取和修改操作都需要通过共享锁或独占锁来保证数据的一致性和可靠性。虽然串行化隔离级别可以解决幻影行的问题,但由于会对并发性能造成较大的影响,因此只有在确实需要时才应该使用。
🍊 行锁、表锁、间隙锁、死锁
🎉 行锁的表现
将mysql数据库改为手动提交
步骤1:
打开窗口1:更新数据,update test_innodb_lock set a = 1 where b = 2;
然后查询select * from test_innodb_lock where b = 2;
,发现a已经改为1了。
由于还没有提交事务,所以b=2这行数据还是被update持有锁,对于其他事务是不可见的,避免了脏读。
打开窗口2:查询select * from test_innodb_lock where b = 2;
发现a的值还是没变。更新数据,update test_innodb_lock set a = 2 where b = 2;
发现一直阻塞,没有继续往下执行。
由于第一个会话持有了这一行的锁,第二个窗口的会话就对这一行进行修改会阻塞。
步骤2:
窗口1:提交事务
窗口2:查询select * from test_innodb_lock where b = 2;
发现a的值改为1了。更新数据,update test_innodb_lock set a = 2 where b = 2;
发现可以更新成功。
🎉 表锁的表现
将mysql数据库改为手动提交
步骤1:
窗口1:更新数据,update test_innodb_lock set b = 0 where a = 1 or a = 2
窗口2:更新数据,update test_innodb_lock set b = 3 where a = 3
,发现阻塞,没有继续往下执行
由于还没有提交事务,并且使用了or导致索引失效,行级锁升级为表锁,窗口1只要没有提交事务,那么窗口2任何对test_innodb_lock表的操作都会阻塞,直到窗口1提交事务,窗口2才可以继续执行下去。
🎉 间隙锁的表现
假设有一张表,test_innodb_lock表有a和b二个字段,a字段里面的数据缺了2,4,6,8,这些就是间隙,这个间隙引发的锁就叫做间隙锁,一般发生在范围查询里面。
将mysql数据库改为手动提交
步骤1:
窗口1:更新数据,update test_innodb_lock set b = 5 where a >1 and a < 9
窗口2:更新数据,insert into test_innodb_lock values(4,4)
发现阻塞了,没有继续往下执行。
窗口1进行了一个范围查询,会把a >1 and a < 9加上锁,窗口2这个会话想插入2,4,6,8是无法插入的,因为它已经被窗口1的会话持有了锁。
间隙锁(Gap Lock)是MySQL中的一种特殊锁机制,用于保证事务的隔离性。
假设有这样一张表:
CREATE TABLE students ( id INT PRIMARY KEY, name VARCHAR(50), age INT );
现在有两个事务:
- 事务A:要插入数据
INSERT INTO students(id, name, age) VALUES (1, 'Alice', 18)
; - 事务B:要查询数据
SELECT * FROM students WHERE id = 2
。
假设事务A先执行,会在id为1的记录上加上一个间隙锁,这个间隙锁会锁定id为2的那个间隙,如下图所示:
| TX A | | TX B | |--------------------+---------+--------------------| |id=1 (Gap Lock) | |id=2 |
事务B现在要查询id为2的记录,但由于id为2的间隙被事务A锁定,所以事务B需要等待事务A提交或回滚才能进行查询,如下图:
| TX A | | TX B | |--------------------+---------+--------------------| |id=1 (Gap Lock) | | | | | |waiting for id=2|
如果在事务A提交或回滚之前,有其他事务想要在id为2的间隙中插入数据,那么该事务会被阻塞,直到间隙锁被释放。
间隙锁的存在,可以防止幻读现象的发生。例如,如果去掉间隙锁,那么在事务A执行插入数据之前,如果事务B插入了一条id为2的记录,那么事务A在执行插入时,会发现id=2已经存在,从而引发幻读。
因此,间隙锁是MySQL中一个非常实用的锁机制。
🎉 死锁
MySQL死锁是指两个或多个事务正在相互等待对方持有的锁,导致它们都无法继续执行。这时,MySQL会检测到死锁并强制终止其中一个事务,以便另一个事务可以继续执行。
以下是一些常见的MySQL死锁面试相关问题及其答案:
- 什么是MySQL死锁?
MySQL死锁是指两个或多个事务都在等待对方持有的锁,导致它们都无法继续执行的情况。
- MySQL如何检测到死锁?
MySQL会不定期地进行死锁检测,如果检测到死锁,会把其中一个事务终止,并回滚该事务执行的操作。
- 如何避免MySQL死锁?
避免MySQL死锁的方法主要有三种:
(1)通过减少交叉事务的数量来降低死锁发生的概率;
(2)通过加锁时的顺序来避免死锁;
(3)通过增加超时时间来解决死锁。
- 如何解决MySQL死锁?
解决MySQL死锁的方法主要有两种:
(1)终止其中一个事务并回滚该事务执行的操作;
(2)调整事务执行的顺序来避免死锁。
- 如何排查MySQL死锁?
排查MySQL死锁的方法主要有两种:
(1)查看MySQL日志,找到死锁发生的时间和事务ID;
(2)使用SHOW ENGINE INNODB STATUS命令查看当前正在运行和等待的事务信息,并分析死锁情况。
总之,MySQL死锁是MySQL数据库中常见的问题之一,对此需要进行深入的了解和掌握,以避免和解决这种情况。