<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>wojiecihuo</title><description>代码、记录与日常 · 追悔莫及之物皆美不胜收</description><link>https://wojiecihuo.cn</link><item><title>MySQL锁</title><link>https://wojiecihuo.cn/blog/mysql/mysql%E9%94%81</link><guid isPermaLink="true">https://wojiecihuo.cn/blog/mysql/mysql%E9%94%81</guid><description>MySQL锁相关知识(表级锁、行级锁)</description><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;全局锁&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;? &lt;strong&gt;全局锁是怎么用的？&lt;/strong&gt;
要使用全局锁，则要执行这条命令：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;flush tables with read lock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行后， &lt;strong&gt;整个数据库就处于只读状态了&lt;/strong&gt; ，这时其他线程执行以下操作，都会被阻塞：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对数据的增删改操作，比如 insert、delete、update等语句；&lt;/li&gt;
&lt;li&gt;对表结构的更改操作，比如 alter table、drop table 等语句。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果要释放全局锁，则要执行这条命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;unlock tables
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然，当会话断开了，全局锁会被自动释放。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? &lt;strong&gt;全局锁应用场景是什么？&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;全局锁主要应用于做 &lt;strong&gt;全库逻辑备份&lt;/strong&gt; ，这样在备份数据库期间，不会因为数据或表结构的更新，而出现备份文件的数据与预期的不一样。&lt;/p&gt;
&lt;p&gt;举个例子大家就知道了。
在全库逻辑备份期间，假设不加全局锁的场景，看看会出现什么意外的情况。&lt;/p&gt;
&lt;p&gt;如果在全库逻辑备份期间，有用户购买了一件商品，一般购买商品的业务逻辑是会涉及到多张数据库表的更新，比如在用户表更新该用户的余额，然后在商品表更新被购买的商品的库存。
那么，有可能出现这样的顺序：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先备份了用户表的数据；&lt;/li&gt;
&lt;li&gt;然后有用户发起了购买商品的操作；&lt;/li&gt;
&lt;li&gt;接着再备份商品表的数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;也就是在备份用户表和商品表之间，有用户购买了商品。
这种情况下，备份的结果是用户表中该用户的余额并没有扣除，反而商品表中该商品的库存被减少了，如果后面用这个备份文件恢复数据库数据的话，用户钱没少，而库存少了，等于用户白嫖了一件商品。
所以，在全库逻辑备份期间，加上全局锁，就不会出现上面这种情况了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? &lt;strong&gt;加全局锁又会带来什么缺点呢？&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;加上全局锁，意味着整个数据库都是只读状态。&lt;/p&gt;
&lt;p&gt;那么如果数据库里有很多数据，备份就会花费很多的时间，关键是备份期间，业务只能读数据，而不能更新数据，这样会造成业务停滞。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? 既然备份数据库数据的时候，使用全局锁会影响业务，那有什么其他方式可以避免？
有的，如果数据库的引擎支持的事务支持 &lt;strong&gt;可重复读的隔离级别&lt;/strong&gt; ，那么在备份数据库之前先开启事务，会先创建 Read View，然后整个事务执行期间都在用这个 Read View，而且由于 MVCC 的支持，备份期间业务依然可以对数据进行更新操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为在可重复读的隔离级别下，即使其他事务更新了表的数据，也不会影响备份数据库时的 Read View，这就是事务四大特性中的隔离性，这样备份期间备份的数据一直是在开启事务时的数据。&lt;/p&gt;
&lt;p&gt;备份数据库的工具是 mysqldump，在使用 mysqldump 时加上 &lt;code&gt;–single-transaction&lt;/code&gt; 参数的时候，就会在备份数据库之前先开启事务。这种方法只适用于支持「可重复读隔离级别的事务」的存储引擎。&lt;/p&gt;
&lt;p&gt;InnoDB 存储引擎默认的事务隔离级别正是可重复读，因此可以采用这种方式来备份数据库。
但是，对于 MyISAM 这种不支持事务的引擎，在备份数据库时就要使用全局锁的方法。&lt;/p&gt;
&lt;h1&gt;表级锁&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;? &lt;strong&gt;MySQL 表级锁有哪些？具体怎么用的。&lt;/strong&gt;
MySQL 里面表级别的锁有这几种：&lt;/li&gt;
&lt;li&gt;表锁；&lt;/li&gt;
&lt;li&gt;元数据锁（MDL）;&lt;/li&gt;
&lt;li&gt;意向锁；&lt;/li&gt;
&lt;li&gt;AUTO-INC 锁；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;表锁&lt;/h2&gt;
&lt;p&gt;先来说说 &lt;strong&gt;表锁&lt;/strong&gt; 。
如果我们想对学生表（t_student）加表锁，可以使用下面的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;//表级别的共享锁，也就是读锁；
//允许当前会话读取被锁定的表，但阻止其他会话对这些表进行写操作。
lock tables t_student read;

//表级别的独占锁，也就是写锁；
//允许当前会话对表进行读写操作，但阻止其他会话对这些表进行任何操作（读或写）。
lock tables t_stuent write;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;需要注意的是，表锁除了会限制别的线程的读写外，也会限制本线程接下来的读写操作，即开启表锁的线程也不能读写其他表了&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;举个例子， 如果在某个线程 A 中执行 &lt;code&gt;lock tables t1 read, t2 write&lt;/code&gt;; 这个语句，则其他线程写 t1、读写 t2 的语句都会被阻塞。同时，线程 A 在执行 unlock tables 之前，也只能执行读 t1、读写 t2 的操作。连写 t1 都不允许，自然也不能访问其他表。&lt;/p&gt;
&lt;p&gt;现在我对 t_test 表执行 &lt;code&gt;lock tables t_test read&lt;/code&gt; ，也就是对 t_test 表加了表级别的共享锁。
&lt;img src=&quot;https://cdn.xiaolincoding.com//picgo/image-20241127180137790.png&quot; alt=&quot;image-20241127180137790&quot;&gt;&lt;/p&gt;
&lt;p&gt;这时候本线程（会话）可以读 t_test 表的数据
&lt;img src=&quot;https://cdn.xiaolincoding.com//picgo/image-20241127180220156.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是不能写 t_test 表的数据，报错如下：
&lt;img src=&quot;https://cdn.xiaolincoding.com//picgo/image-20241127180322351.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;同时 &lt;strong&gt;本线程不能访问其他表&lt;/strong&gt; ，比如下面我在本线程读 t_student 表就报错了。
&lt;img src=&quot;https://cdn.xiaolincoding.com//picgo/image-20241127180404359.png&quot; alt=&quot;image-20241127180404359&quot;&gt;&lt;/p&gt;
&lt;p&gt;其他线程可以对 t_test 表进行读操作，但是也不能对 t_test 表进行写操作，这时候写操作会发生阻塞。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.xiaolincoding.com//picgo/image-20241127180537031.png&quot; alt=&quot;image-20241127180537031&quot;&gt;&lt;/p&gt;
&lt;p&gt;要释放表锁，可以使用下面这条命令，会释放当前会话的所有表锁：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;unlock tables
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，当会话退出后，也会释放所有表锁。&lt;/p&gt;
&lt;p&gt;在还没有出现更细粒度的锁的时候，表锁是最常用的处理并发的方式，不过尽量避免在使用 InnoDB 引擎的表使用表锁，因为表锁的颗粒度太大，会影响并发性能， &lt;strong&gt;InnoDB 牛逼的地方在于实现了颗粒度更细的行级锁&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;元数据锁&lt;/h2&gt;
&lt;p&gt;再来说说 &lt;strong&gt;元数据锁&lt;/strong&gt; （MDL）。
我们不需要显示的使用 MDL，因为当我们对数据库表进行操作时，会自动给这个表加上 MDL：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对一张表进行 CRUD 操作时，加的是 &lt;strong&gt;MDL 读锁&lt;/strong&gt; ；&lt;/li&gt;
&lt;li&gt;对一张表做结构变更操作的时候，加的是 &lt;strong&gt;MDL 写锁&lt;/strong&gt; ；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;MDL 是为了保证当用户对表执行 CRUD 操作时，防止其他线程对这个表结构做了变更。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;当有线程在执行 select 语句（ 加 MDL 读锁）的期间，如果有其他线程要更改该表的结构（ 申请 MDL 写锁），那么将会被阻塞，直到执行完 select 语句（ 释放 MDL 读锁）。&lt;/p&gt;
&lt;p&gt;反之，当有线程对表结构进行变更（ 加 MDL 写锁）的期间，如果有其他线程执行了 CRUD 操作（ 申请 MDL 读锁），那么就会被阻塞，直到表结构变更完成（ 释放 MDL 写锁）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? &lt;strong&gt;MDL 不需要显示调用，那它是在什么时候释放的?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MDL 是在事务提交后才会释放，这意味着 &lt;strong&gt;事务执行期间，MDL 是一直持有的&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;那如果数据库有一个长事务（所谓的长事务，就是开启了事务，但是一直还没提交），那在对表结构做变更操作的时候，可能会发生意想不到的事情，比如下面这个顺序的场景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先，线程 A 先启用了事务（但是一直不提交），然后执行一条 select 语句，此时就先对该表加上 MDL 读锁；&lt;/li&gt;
&lt;li&gt;然后，线程 B 也执行了同样的 select 语句，此时并不会阻塞，因为「读读」并不冲突；&lt;/li&gt;
&lt;li&gt;接着，线程 C 修改了表字段，此时由于线程 A 的事务并没有提交，也就是 MDL 读锁还在占用着，这时线程 C 就无法申请到 MDL 写锁，就会被阻塞，&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么在线程 C 阻塞后，后续有对该表的 select 语句，就都会被阻塞，如果此时有大量该表的 select 语句的请求到来，就会有大量的线程被阻塞住，这时数据库的线程很快就会爆满了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? &lt;strong&gt;为什么线程 C 因为申请不到 MDL 写锁，而导致后续的申请读锁的查询操作也会被阻塞？&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是因为申请 MDL 锁的操作会形成一个队列，队列中 &lt;strong&gt;写锁获取优先级高于读锁&lt;/strong&gt; ，一旦出现 MDL 写锁等待，会阻塞后续该表的所有 CRUD 操作。&lt;/p&gt;
&lt;p&gt;所以为了能安全的对表结构进行变更，在对表结构变更前，先要看看数据库中的长事务，是否有事务已经对表加上了 MDL 读锁，如果可以考虑 kill 掉这个长事务，然后再做表结构的变更。&lt;/p&gt;
&lt;h2&gt;意向锁&lt;/h2&gt;
&lt;p&gt;接着，说说 &lt;strong&gt;意向锁&lt;/strong&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在使用 InnoDB 引擎的表里对某些记录加上「共享锁」之前，需要先在表级别加上一个「意向共享锁」；&lt;/li&gt;
&lt;li&gt;在使用 InnoDB 引擎的表里对某些纪录加上「独占锁」之前，需要先在表级别加上一个「意向独占锁」；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;也就是，当执行插入、更新、删除操作，需要先对表加上「意向独占锁」，然后对该记录加独占锁。
而普通的 select 是不会加行级锁的，普通的 select 语句是利用 MVCC 实现一致性读，是无锁的。&lt;/p&gt;
&lt;p&gt;不过，select 也是可以对记录加共享锁和独占锁的，具体方式如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;//先在表上加上意向共享锁，然后对读取的记录加共享锁
select ... lock in share mode;

//先表上加上意向独占锁，然后对读取的记录加独占锁
select ... for update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;意向共享锁和意向独占锁是表级锁，不会和行级的共享锁和独占锁发生冲突，而且意向锁之间也不会发生冲突，只会和共享表锁（ &lt;em&gt;lock tables... read&lt;/em&gt; ）和独占表锁（ &lt;em&gt;lock tables... write&lt;/em&gt; ）发生冲突。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;表锁和行锁是满足读读共享、读写互斥、写写互斥的。
如果没有「意向锁」，那么加「独占表锁」时，就需要遍历表里所有记录，查看是否有记录存在独占锁，这样效率会很慢。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那么有了「意向锁」，由于在对记录加独占锁前，先会加上表级别的意向独占锁，那么在加「独占表锁」时，直接查该表是否有意向独占锁，如果有就意味着表里已经有记录被加了独占锁，这样就不用去遍历表里的记录。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以， &lt;strong&gt;意向锁的目的是为了快速判断表里是否有记录被加锁&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;AUTO-INC 锁&lt;/h2&gt;
&lt;p&gt;表里的主键通常都会设置成自增的，这是通过对主键字段声明 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; 属性实现的。
之后可以在插入数据时，可以不指定主键的值，数据库会自动给主键赋值递增的值，这主要是通过 &lt;strong&gt;AUTO-INC 锁&lt;/strong&gt; 实现的。&lt;/p&gt;
&lt;p&gt;AUTO-INC 锁是特殊的表锁机制，锁 &lt;strong&gt;不是再一个事务提交后才释放，而是在执行完插入语句后就会立即释放&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;在插入数据时，会加一个表级别的 AUTO-INC 锁&lt;/strong&gt; ，然后为被 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; 修饰的字段赋值递增的值，等插入语句执行完成后，才会把 AUTO-INC 锁释放掉。&lt;/p&gt;
&lt;p&gt;那么，一个事务在持有 AUTO-INC 锁的过程中，其他事务的如果要向该表插入语句都会被阻塞，从而保证插入数据时，被 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; 修饰的字段的值是连续递增的。&lt;/p&gt;
&lt;p&gt;但是， AUTO-INC 锁在对大量数据进行插入的时候，会影响插入性能，因为另一个事务中的插入会被阻塞。&lt;/p&gt;
&lt;p&gt;因此， 在 MySQL 5.1.22 版本开始，InnoDB 存储引擎提供了一种 &lt;strong&gt;轻量级的锁&lt;/strong&gt; 来实现自增。&lt;/p&gt;
&lt;p&gt;一样也是在插入数据的时候，会为被 &lt;code&gt;AUTO_INCREMENT&lt;/code&gt; 修饰的字段加上轻量级锁， &lt;strong&gt;然后给该字段赋值一个自增的值，就把这个轻量级锁释放了，而不需要等待整个插入语句执行完后才释放锁&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;InnoDB 存储引擎提供了个 innodb_autoinc_lock_mode 的系统变量，是用来控制选择用 AUTO-INC 锁，还是轻量级的锁。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 innodb_autoinc_lock_mode = 0，就采用 AUTO-INC 锁，语句执行结束后才释放锁；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;当 innodb_autoinc_lock_mode = 2，就采用轻量级锁，申请自增主键后就释放锁，并不需要等语句执行后才释放。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当 innodb_autoinc_lock_mode = 1：
&lt;ul&gt;
&lt;li&gt;普通 insert 语句，自增锁在申请之后就马上释放；&lt;/li&gt;
&lt;li&gt;类似 insert … select 这样的批量插入数据的语句，自增锁还是要等语句结束后才被释放；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当 innodb_autoinc_lock_mode = 2 是性能最高的方式，但是当搭配 binlog 的日志格式是 statement 一起使用的时候，在「主从复制的场景」中会发生 &lt;strong&gt;数据不一致的问题&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;举个例子，考虑下面场景：
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/innodb_autoinc_lock_mode=2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;session A 往表 t 中插入了 4 行数据，然后创建了一个相同结构的表 t2，然后 &lt;strong&gt;两个 session 同时执行向表 t2 中插入数据&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;如果 innodb_autoinc_lock_mode = 2，意味着「申请自增主键后就释放锁，不必等插入语句执行完」。那么就可能出现这样的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;session B 先插入了两个记录，(1,1,1)、(2,2,2)；&lt;/li&gt;
&lt;li&gt;然后，session A 来申请自增 id 得到 id=3，插入了（3,5,5)；&lt;/li&gt;
&lt;li&gt;之后，session B 继续执行，插入两条记录 (4,3,3)、 (5,4,4)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看到， &lt;strong&gt;session B 的 insert 语句，生成的 id 不连续&lt;/strong&gt; 。
当「主库」发生了这种情况，binlog 面对 t2 表的更新只会记录这两个 session 的 insert 语句，如果 binlog_format=statement，记录的语句就是原始语句。记录的顺序要么先记 session A 的 insert 语句，要么先记 session B 的 insert 语句。&lt;/p&gt;
&lt;p&gt;但不论是哪一种，这个 binlog 拿去「从库」执行，这时从库是按「顺序」执行语句的，只有当执行完一条 SQL 语句后，才会执行下一条 SQL。因此，在 &lt;strong&gt;从库上「不会」发生像主库那样两个 session 「同时」执行向表 t2 中插入数据的场景。所以，在备库上执行了 session B 的 insert 语句，生成的结果里面，id 都是连续的。这时，主从库就发生了数据不一致&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;要解决这问题，binlog 日志格式要设置为 row，这样在 binlog 里面记录的是主库分配的自增值，到备库执行的时候，主库的自增值是什么，从库的自增值就是什么。&lt;/p&gt;
&lt;p&gt;所以， &lt;strong&gt;当 innodb_autoinc_lock_mode = 2 时，并且 binlog_format = row，既能提升并发性，又不会出现数据一致性问题&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;binlog_format = row&lt;/code&gt;时，记录&lt;strong&gt;每一行&lt;/strong&gt;被更改后的实际数据值，而不是 SQL 语句|&lt;/p&gt;
&lt;h1&gt;行级锁&lt;/h1&gt;
&lt;p&gt;InnoDB 引擎是支持行级锁的，而 MyISAM 引擎并不支持行级锁。&lt;/p&gt;
&lt;p&gt;前面也提到，普通的 select 语句是不会对记录加锁的，因为它属于快照读。如果要在查询时对记录加行锁，可以使用下面这两个方式，这种查询会加锁的语句称为 &lt;strong&gt;锁定读&lt;/strong&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;// 对读取的记录加共享锁
select ... lock in share mode;

// 对读取的记录加独占锁
select ... for update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这两条语句必须在一个事务中， &lt;strong&gt;因为当事务提交了，锁就会被释放&lt;/strong&gt; ，所以在使用这两条语句的时候，要加上 begin、start transaction 或者 set autocommit = 0。&lt;/p&gt;
&lt;p&gt;共享锁（S锁）满足读读共享，读写互斥。独占锁（X锁）满足写写互斥、读写互斥。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/x%E9%94%81%E5%92%8Cs%E9%94%81.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;不同隔离级别下，行级锁的种类是不同的。&lt;/p&gt;
&lt;p&gt;在读已提交隔离级别下，行级锁的种类只有记录锁，也就是仅仅把一条记录锁上。
在可重复读隔离级别下，行级锁的种类除了有记录锁，还有间隙锁（目的是为了避免幻读），所以行级锁的种类主要有三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Record Lock：&lt;/strong&gt; 记录锁，也就是仅仅把一条记录锁上；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gap Lock：&lt;/strong&gt; 间隙锁，锁定一个范围，但是不包含记录本身；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Next-Key Lock：&lt;/strong&gt; Record Lock + Gap Lock 的组合，锁定一个范围，并且锁定记录本身。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;记录锁&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Record Lock 称为记录锁，锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当一个事务对一条记录加了 S 型记录锁后，其他事务也可以继续对该记录加 S 型记录锁（S 型与 S 锁兼容），但是不可以对该记录加 X 型记录锁（S 型与 X 锁不兼容）;&lt;/li&gt;
&lt;li&gt;当一个事务对一条记录加了 X 型记录锁后，其他事务既不可以对该记录加 S 型记录锁（S 型与 X 锁不兼容），也不可以对该记录加 X 型记录锁（X 型与 X 锁不兼容）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个例子，当一个事务执行了下面这条语句：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql &gt; begin;
mysql &gt; select * from t_test where id = 1 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是对 t_test 表中主键 id 为 1 的这条记录加上 X 型的记录锁，这样其他事务就无法对这条记录进行修改了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E8%AE%B0%E5%BD%95%E9%94%81.drawio.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当事务执行 commit 后，事务过程中生成的锁都会被释放。&lt;/p&gt;
&lt;h2&gt;间隙锁&lt;/h2&gt;
&lt;p&gt;Gap Lock 称为间隙锁，存在于可重复读隔离级别和串行化隔离级别，目的是为了解决可重复读隔离级别下幻读的现象。&lt;/p&gt;
&lt;p&gt;假设，表中有一个范围 id 为（3，5）间隙锁，那么其他事务就无法插入 id = 4 这条记录了，这样就有效的防止幻读现象的发生。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/gap%E9%94%81.drawio.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;间隙锁虽然存在 X 型间隙锁和 S 型间隙锁，但是并没有什么区别， &lt;strong&gt;间隙锁之间是兼容的，即两个事务可以同时持有包含共同间隙范围的间隙锁，并不存在互斥关系，因为间隙锁的目的是防止插入幻影记录而提出的&lt;/strong&gt; 。&lt;/p&gt;
&lt;h2&gt;Next-Key Lock&lt;/h2&gt;
&lt;p&gt;Next-Key Lock 称为临键锁，是 Record Lock + Gap Lock 的组合，锁定一个范围，并且锁定记录本身。
假设，表中有一个范围 id 为（3，5] 的 next-key lock，那么其他事务即不能插入 id = 4 记录，也不能修改 id = 5 这条记录。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E4%B8%B4%E9%94%AE%E9%94%81.drawio.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;所以，next-key lock 即能保护该记录，又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。
&lt;strong&gt;next-key lock 是包含间隙锁+记录锁的，如果一个事务获取了 X 型的 next-key lock，那么另外一个事务在获取相同范围的 X 型的 next-key lock 时，是会被阻塞的&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;比如，一个事务持有了范围为 (1, 10] 的 X 型的 next-key lock，那么另外一个事务在获取相同范围的 X 型的 next-key lock 时，就会被阻塞。&lt;/p&gt;
&lt;p&gt;虽然相同范围的间隙锁是多个事务相互兼容的，但对于记录锁，我们是要考虑 X 型与 S 型关系，X 型的记录锁与 X 型的记录锁是冲突的。&lt;/p&gt;
&lt;h2&gt;插入意向锁&lt;/h2&gt;
&lt;p&gt;一个事务在插入一条记录的时候，需要判断插入位置是否已被其他事务加了间隙锁（next-key lock 也包含间隙锁）。&lt;/p&gt;
&lt;p&gt;如果有的话，插入操作就会发生 &lt;strong&gt;阻塞&lt;/strong&gt; ，直到拥有间隙锁的那个事务提交为止（释放间隙锁的时刻），在此期间会生成一个&lt;strong&gt;插入意向锁&lt;/strong&gt; ，表明有事务想在某个区间插入新记录，但是现在处于等待状态。&lt;/p&gt;
&lt;p&gt;举个例子，假设事务 A 已经对表加了一个范围 id 为（3，5）间隙锁。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/gap%E9%94%81.drawio.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当事务 A 还没提交的时候，事务 B 向该表插入一条 id = 4 的新记录，这时会判断到插入的位置已经被事务 A 加了间隙锁，于是事物 B 会生成一个插入意向锁，然后将锁的状态设置为等待状态（ &lt;strong&gt;PS：MySQL 加锁时，是先生成锁结构，然后设置锁的状态，如果锁状态是等待状态，并不是意味着事务成功获取到了锁，只有当锁状态为正常状态时，才代表事务成功获取到了锁&lt;/strong&gt;），此时事务 B 就会发生阻塞，直到事务 A 提交了事务。&lt;/p&gt;
&lt;p&gt;插入意向锁名字虽然有意向锁，但是它并&lt;strong&gt;不是意向锁，它是一种特殊的间隙锁，属于行级别锁&lt;/strong&gt; 。
如果说间隙锁锁住的是一个区间，那么「插入意向锁」锁住的就是一个点。因而从这个角度来说，插入意向锁确实是一种特殊的间隙锁。&lt;/p&gt;
&lt;p&gt;插入意向锁与间隙锁的另一个非常重要的差别是：尽管「插入意向锁」也属于间隙锁，但两个事务却不能在同一时间内，一个拥有间隙锁，另一个拥有该间隙区间内的插入意向锁（当然，插入意向锁如果不在间隙锁区间内则是可以的）。&lt;/p&gt;
&lt;h2&gt;mysql的什么命令会加上间隙锁&lt;/h2&gt;
&lt;p&gt;在可重复读隔离级别下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当你使用非唯一索引进行查询时，InnoDB可能会在索引间隙上加上间隙锁，例如 &lt;code&gt;SELECT * FROM t WHERE a = ? FOR UPDATE;&lt;/code&gt; 如果a是非唯一索引，MySQL会在该值的前后加上间隙锁，以防止其他事务在这些间隙插入新记录。&lt;/li&gt;
&lt;li&gt;在执行带有WHERE条件的DELETE语句时，如果使用的是非唯一索引，MySQL会在符合条件的记录的前后加上间隙锁。&lt;/li&gt;
&lt;li&gt;类似地，在执行带有WHERE条件的UPDATE语句时，如果使用的是非唯一索引，MySQL也会在符合条件的记录的前后加上间隙锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;MySQL是怎么加锁的&lt;/h1&gt;
&lt;h2&gt;什么SQL语句会加行级锁&lt;/h2&gt;
&lt;p&gt;普通的 select 语句是不会对记录加锁的（除了串行化隔离级别），因为它属于快照读，是通过 MVCC（多版本并发控制）实现的。&lt;/p&gt;
&lt;p&gt;如果要在查询时对记录加行级锁，可以使用下面这两个方式，这两种查询会加锁的语句称为&lt;strong&gt;锁定读&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;// 对读取的记录加共享锁（S型锁）
select ... lock in share mode;

//对读取的记录加独占锁(X型锁)
select ... for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面这两条语句必须在一个事务中，&lt;strong&gt;因为当事务提交了，锁就会被释放&lt;/strong&gt;，所以在使用这两条语句的时候，要加上 begin 或者 start transaction 开启事务的语句。&lt;/p&gt;
&lt;p&gt;除了上面这两条锁定读语句会加行级锁之外，update 和 delete 操作都会加行级锁，且锁的类型都是独占锁(X型锁)。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;//对操作的记录加独占锁(X型锁)
update table .... where id = 1;

//对操作的记录加独占锁(X型锁)
delete from table where id = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;共享锁（S锁）满足读读共享，读写互斥。独占锁（X锁）满足写写互斥、读写互斥。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;MySQL是怎么加行级锁的&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;加锁的对象是索引，加锁的基本单位是 next-key lock&lt;/strong&gt;，它是由记录锁和间隙锁组合而成的 ，&lt;strong&gt;next-key lock 是前开后闭区间，而间隙锁是前开后开区间&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;但是，next-key lock 在一些场景下会退化成记录锁或间隙锁。&lt;/p&gt;
&lt;p&gt;那到底是什么场景呢？总结一句：&lt;strong&gt;在能使用记录锁或者间隙锁就能避免幻读现象的场景下， next-key lock 就会退化成记录锁或间隙锁&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这次会以下面这个表结构来进行实验说明：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE `user` (
	`id` bigint NOT NULL AUTO_INCREMENT,
	`name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
	`age` int NOT NULL,
	 PRIMARY KEY (`id`),
	 KEY `index_age` (`age`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，id 是主键索引（唯一索引），age 是普通索引（非唯一索引），name 是普通的列。&lt;/p&gt;
&lt;h2&gt;唯一索引（主键索引）等值查询&lt;/h2&gt;
&lt;p&gt;当我们用唯一索引进行等值查询的时候，查询的记录存不存在，加锁的规则也会不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当查询的记录是「存在」的，在索引树上定位到这一条记录后，将该记录的索引中的 next-key lock 会&lt;strong&gt;退化成「记录锁」&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;当查询的记录是「不存在」的，在索引树找到第一条大于该查询记录的记录后，将该记录的索引中的 next-key lock 会&lt;strong&gt;退化成「间隙锁」&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;记录存在的情况&lt;/h3&gt;
&lt;p&gt;假设事务 A 执行了这条等值查询语句，查询的记录是「存在」于表中的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql&gt; begin;
mysql&gt; select * from user where id = 1 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么，事务 A 会为 id 为 1 的这条记录就会加上 &lt;strong&gt;X 型的记录锁&lt;/strong&gt;。
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250912221647.CDTagGDJ_e8LyR.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;接下来，如果有其他事务，对 id 为 1 的记录进行更新或者删除操作的话，这些操作都会被阻塞，因为更新或者删除操作也会对记录加 X 型的记录锁，而 X 锁和 X 锁之间是互斥关系。
比如，下面这个例子：
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250912221706.DEQpscfH_EEi0U.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们可以通过&lt;code&gt;select * from performance_schema.data_locks;&lt;/code&gt;这条语句，查看事务执行 SQL 过程中加了什么锁。
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250912222207.DAL-WMLL_Z7bEEn.webp&quot; alt=&quot;image.png&quot;&gt;
从上图可以看到，共加了两个锁，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表锁：X 类型的意向锁；&lt;/li&gt;
&lt;li&gt;行锁：X 类型的记录锁；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里我们重点关注行级锁，图中 LOCK_TYPE 中的 RECORD 表示行级锁，而不是记录锁的意思。
&lt;strong&gt;通过 LOCK_MODE 可以确认是 next-key 锁，还是间隙锁，还是记录锁：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 LOCK_MODE 为 &lt;code&gt;X&lt;/code&gt;，说明是 next-key 锁；&lt;/li&gt;
&lt;li&gt;如果 LOCK_MODE 为 &lt;code&gt;X, REC_NOT_GAP&lt;/code&gt;，说明是记录锁；&lt;/li&gt;
&lt;li&gt;如果 LOCK_MODE 为 &lt;code&gt;X, GAP&lt;/code&gt;，说明是间隙锁；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，&lt;strong&gt;此时事务 A 在 id = 1 记录的主键索引上加的是记录锁，锁住的范围是 id 为 1 的这条记录。&lt;/strong&gt; 这样其他事务就无法对 id 为 1 的这条记录进行更新和删除操作了。&lt;/p&gt;
&lt;p&gt;从这里我们也可以得知：&lt;strong&gt;加锁的对象是针对索引&lt;/strong&gt;
因为这里查询语句扫描的 B+ 树是聚簇索引树，即主键索引树，所以是对主键索引加锁。将对应记录的主键索引加 记录锁后，就意味着其他事务无法对该记录进行更新和删除操作了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? &lt;strong&gt;为什么唯一索引等值查询并且查询记录存在的场景下，该记录的索引中的 next-key lock 会退化成记录锁？&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原因就是在唯一索引等值查询并且查询记录存在的场景下，仅靠记录锁也能避免幻读的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;幻读的定义就是，当一个事务前后两次查询的结果集，不相同时，就认为发生幻读。所以，要避免幻读就是避免结果集的某一条记录被其他事务删除，或者有其他事务插入了一条新记录，这样前后两次查询的结果集就不会出现不相同的情况。&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于主键具有唯一性，所以&lt;strong&gt;其他事务插入id = 1的时候，会因为主键冲突，导致无法插入id = 1的新纪录&lt;/strong&gt;。这样事务 A 在多次查询 id = 1 的记录的时候，不会出现前后两次查询的结果集不同，也就避免了幻读的问题。&lt;/li&gt;
&lt;li&gt;由于对 id = 1 加了记录锁，&lt;strong&gt;其他事务无法删除该记录&lt;/strong&gt;，这样事务 A 在多次查询 id = 1 的记录的时候，不会出现前后两次查询的结果集不同，也就避免了幻读的问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;记录不存在的情况&lt;/h3&gt;
&lt;p&gt;假设事务 A 执行了这条等值查询语句，查询的记录是「不存在」于表中的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql&gt; begin;
mysql&gt; select * from user where id = 2 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来，通过 &lt;code&gt;select * from performance_schema.data_locks;&lt;/code&gt;这条语句，查看事务执行 SQL 过程中加了什么锁。
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250913113030.BgNQxH8m_Z1hpOpS.webp&quot; alt=&quot;image.png&quot;&gt;
从上图可以看到，共加了两个锁，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表锁：X 类型的意向锁；&lt;/li&gt;
&lt;li&gt;行锁：X 类型的间隙锁；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，&lt;strong&gt;此时事务 A 在 id = 5 记录的主键索引上加的是间隙锁，锁住的范围是 (1, 5)。&lt;/strong&gt;
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250913113058.DQqnqaVq_Z19bgM9.webp&quot; alt=&quot;image.png&quot;&gt;
接下来，如果有其他事务插入 id 值为 2、3、4 这一些记录的话，这些插入语句都会发生阻塞。
注意，如果其他事务插入的 id = 1 或者 id = 5 的记录话，并不会发阻塞，而是报主键冲突的错误，因为表中已经存在 id = 1 和 id = 5 的记录了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? &lt;strong&gt;间隙锁的范围&lt;code&gt;(1, 5)&lt;/code&gt; ，是怎么确定的？&lt;/strong&gt;
如果 LOCK_MODE 是 next-key 锁或者间隙锁，那么 LOCK_DATA 就表示锁的范围「右边界」，此次的事务 A 的 LOCK_DATA 是 5。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后锁范围的「左边界」是表中 id 为 5 的上一条记录的 id 值，即 1。
因此，间隙锁的范围&lt;code&gt;(1, 5)&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? 为什么唯一索引等值查询并且查询记录「不存在」的场景下，在索引树找到第一条大于该查询记录的记录后，要将该记录的索引中的 next-key lock 会退化成「间隙锁」？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;原因就是在唯一索引等值查询并且查询记录不存在的场景下，仅靠间隙锁就能避免幻读的问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;为什么 id = 5 记录上的主键索引的锁不可以是 next-key lock？如果是 next-key lock，就意味着其他事务无法删除 id = 5 这条记录，但是这次的案例是查询 id = 2 的记录，只要保证前后两次查询 id = 2 的结果集相同，就能避免幻读的问题了，所以即使 id =5 被删除，也不会有什么影响，那就没必须加 next-key lock，因此只需要在 id = 5 加间隙锁，避免其他事务插入 id = 2 的新记录就行了。&lt;/li&gt;
&lt;li&gt;为什么不可以针对不存在的记录加记录锁？&lt;strong&gt;锁是加在索引上的&lt;/strong&gt;，而这个场景下查询的记录是不存在的，自然就没办法锁住这条不存在的记录。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;唯一索引（主键索引）范围查询&lt;/h2&gt;
&lt;p&gt;范围查询和等值查询的加锁规则是不同的。&lt;/p&gt;
&lt;p&gt;当唯一索引进行范围查询时，&lt;strong&gt;会对每一个扫描到的索引加 next-key 锁，然后如果遇到下面这些情况，会退化成记录锁或者间隙锁：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;情况一：针对「大于等于」的范围查询，因为存在等值查询的条件，那么如果等值查询的记录是存在于表中，那么该记录的索引中的 next-key 锁会&lt;strong&gt;退化成记录锁。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;情况二：针对「小于或者小于等于」的范围查询，要看条件值的记录是否存在于表中：
&lt;ul&gt;
&lt;li&gt;当条件值的记录不在表中，那么不管是「小于」还是「小于等于」条件的范围查询，扫描到终止范围查询的记录时，该记录的索引的 next-key 锁会退化成间隙锁，其他扫描到的记录，都是在这些记录的索引上加 next-key 锁。 当条件值的记录在表中，如果是「小于」条件的范围查询，扫描到终止范围查询的记录时，该记录的索引的 next-key 锁会退化成间隙锁，其他扫描到的记录，都是在这些记录的索引上加 next-key 锁；&lt;/li&gt;
&lt;li&gt;如果「小于等于」条件的范围查询，扫描到终止范围查询的记录时，该记录的索引 next-key 锁不会退化成间隙锁。其他扫描到的记录，都是在这些记录的索引上加 next-key 锁。 接下来，通过几个实验，才验证我上面说的结论&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;针对大于或大于等于的范围查询&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;实验一：针对大于的范围查询的情况&lt;/strong&gt;
假设事务 A 执行了这条范围查询语句：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql&gt; begin;

mysql&gt; select * from user where id &gt; 15 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务 A 加锁变化过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最开始要找的第一行是 id = 20，由于查询该记录不是一个等值查询（不是大于等于条件查询），所以对该主键索引加的是范围为 (15, 20] 的 next-key 锁；&lt;/li&gt;
&lt;li&gt;由于是范围查找，就会继续往后找存在的记录，虽然我们看见表中最后一条记录是 id = 20 的记录，但是实际在 Innodb 存储引擎中，会用一个特殊的记录来标识最后一条记录，该特殊的记录的名字叫 supremum pseudo-record ，所以扫描第二行的时候，也就扫描到了这个特殊记录的时候，会对该主键索引加的是范围为 (20, +∞] 的 next-key 锁。&lt;/li&gt;
&lt;li&gt;停止扫描
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250913120737.CqjG3bqc_Z27ROpc.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实验二：针对大于等于的范围查询&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql&gt; begin;

mysql&gt; select * from user where id &gt;= 15 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务A加锁变化过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最开始要找的第一行是 id = 15，由于查询该记录是一个等值查询（等于 15），所以该主键索引的 next-key 锁会退化成记录锁，也就是仅锁住 id = 15 这一行记录。&lt;/li&gt;
&lt;li&gt;由于是范围查找，就会继续往后找存在的记录，扫描到的第二行是 id = 20，于是对该主键索引加的是范围为 (15, 20] 的 next-key 锁；&lt;/li&gt;
&lt;li&gt;接着扫描到第三行的时候，扫描到了特殊记录（ supremum pseudo-record），于是对该主键索引加的是范围为 (20, +∞] 的 next-key 锁。&lt;/li&gt;
&lt;li&gt;停止扫描&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250913122230.DRcF0bZU_Z1fv3WF.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h3&gt;针对小于或者小于等于的范围查询&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;针对小于的范围查询时，查询条件值的纪录不存在表中的情况&lt;/strong&gt;
假设事务 A 执行了这条范围查询语句，注意查询条件值的记录（id 为 6）并不存在于表中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql&gt; begin;

mysql&gt; select * from user where id &amp;#x3C; 6 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务 A 加锁变化过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;最开始要找的第一行是 id = 1，于是对该主键索引加的是范围为 (-∞, 1] 的 next-key 锁；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于是范围查找，就会继续往后找存在的记录，扫描到的第二行是 id = 5，所以对该主键索引加的是范围为 (1, 5] 的 next-key 锁；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于扫描到的第二行记录（id = 5），满足 id &amp;#x3C; 6 条件，而且也没有达到终止扫描的条件，接着会继续扫描。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;扫描到的第三行是 id = 10，该记录不满足 id &amp;#x3C; 6 条件的记录，所以 id = 10 这一行记录的锁会&lt;strong&gt;退化成间隙锁&lt;/strong&gt;，于是对该主键索引加的是范围为 (5, 10) 的间隙锁。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;由于扫描到的第三行记录（id = 10），不满足 id &amp;#x3C; 6 条件，达到了终止扫描的条件，于是停止扫描。
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250913123139.BoZjxaoY_18wUif.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;? &lt;strong&gt;为什么id(5,10)也要加间隙锁么？&lt;/strong&gt;
第三个间隙锁是加在id=10 索引上的，这个例子不太好说明，如果是id&amp;#x3C;7，id=10索引不加间隙锁的话，中途插入一个 6，就发生幻读了&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，&lt;strong&gt;针对「小于或者小于等于」的唯一索引范围查询，如果条件值的记录不在表中，那么不管是「小于」还是「小于等于」的范围查询，扫描到终止范围查询的记录时，该记录中索引的 next-key 锁会退化成间隙锁，其他扫描的记录，则是在这些记录的索引上加 next-key 锁。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实验二：针对「小于等于」的范围查询时，查询条件值的记录「存在」表中的情况。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql&gt; begin;

mysql&gt; select * from user where id &amp;#x3C;= 5  for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务 A 加锁变化过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最开始要找的第一行是 id = 1，于是对该记录加的是范围为 (-∞, 1] 的 next-key 锁；&lt;/li&gt;
&lt;li&gt;由于是范围查找，就会继续往后找存在的记录，扫描到的第二行是 id = 5，于是对该记录加的是范围为 (1, 5] 的 next-key 锁。&lt;/li&gt;
&lt;li&gt;由于主键索引具有唯一性，不会存在两个 id = 5 的记录，所以不会再继续扫描，于是停止扫描。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从上面的分析中，可以得到&lt;strong&gt;事务 A 在主键索引上加了 2 个 X 型的锁&lt;/strong&gt;：
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250913124735.-pwnyW1Q_258qOy.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果查的是id &amp;#x3C; 5,则next-key lock会退化为间隙锁！！&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在针对「小于或者小于等于」的唯一索引（主键索引）范围查询时，存在这两种情况会将索引的 next-key 锁会退化成间隙锁的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当条件值的记录&lt;strong&gt;不在&lt;/strong&gt;表中时，那么不管是「小于」还是「小于等于」条件的范围查询，扫描到终止范围查询的记录时，该记录的主键索引中的 next-key 锁会退化成间隙锁，其他扫描到的记录，都是在这些记录的主键索引上加 next-key 锁。&lt;/li&gt;
&lt;li&gt;当条件值的记录「在」表中时：
&lt;ul&gt;
&lt;li&gt;如果是「小于」条件的范围查询，扫描到终止范围查询的记录时，该记录的主键索引中的 next-key 锁会退化成间隙锁，其他扫描到的记录，都是在这些记录的主键索引上，加 next-key 锁。&lt;/li&gt;
&lt;li&gt;如果是「小于等于」条件的范围查询，扫描到终止范围查询的记录时，该记录的主键索引中的 next-key 锁「不会」退化成间隙锁，其他扫描到的记录，都是在这些记录的主键索引上加 next-key 锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;非唯一索引等值查询&lt;/h2&gt;
&lt;p&gt;当我们用非唯一索引进行等值查询的时候，&lt;strong&gt;因为存在两个索引，一个是主键索引，一个是非唯一索引（二级索引），所以在加锁时，同时会对这两个索引都加锁，但是对主键索引加锁的时候，只有满足查询条件的记录才会对它们的主键索引加锁。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;针对非唯一索引等值查询时，查询的记录存不存在，加锁的规则也会不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当查询的记录「存在」时，由于不是唯一索引，所以肯定存在索引值相同的记录，于是非唯一索引等值查询的过程是一个扫描的过程，直到扫描到第一个不符合条件的二级索引记录就停止扫描，然后在扫描的过程中，对扫描到的二级索引记录加的是 next-key 锁，而对于第一个不符合条件的二级索引记录，该二级索引的 next-key 锁会退化成间隙锁。同时，在符合查询条件的记录的主键索引上加记录锁。&lt;/li&gt;
&lt;li&gt;当查询的记录「不存在」时，**扫描到第一条不符合条件的二级索引记录，该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录，所以不会对主键索引加锁。 **&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;记录不存在的情况&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql&gt; begin;

mysql&gt; select * from user where age = 25 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务 A 加锁变化过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定位到第一条不符合查询条件的二级索引记录，即扫描到 age = 39，于是该二级索引的 next-key 锁会退化成间隙锁，范围是 (22, 39)。&lt;/li&gt;
&lt;li&gt;停止查询&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;事务 A 在 age = 39 记录的二级索引上，加了 X 型的间隙锁，范围是 (22, 39)。意味着其他事务无法插入 age 值为 23、24、25、26、....、38 这些新记录。不过对于插入 age = 22 和 age = 39 记录的语句，在一些情况是可以成功插入的，而一些情况则无法成功插入，具体哪些情况，会在后面说。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250913220756.BFaq41M1_Z24419q.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;当有一个事务持有二级索引的间隙锁 (22, 39) 时，什么情况下，可以让其他事务的插入 age = 22 或者 age = 39 记录的语句成功？又是什么情况下，插入 age = 22 或者 age = 39 记录时的语句会被阻塞？&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们先要清楚，什么情况下插入语句会发生阻塞。
&lt;strong&gt;插入语句在插入一条记录之前，需要先定位到该记录在 B+树 的位置，如果插入的位置的下一条记录的索引上有间隙锁，才会发生阻塞&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在分析二级索引的间隙锁是否可以成功插入记录时，我们要先要知道二级索引树是如何存放记录的？&lt;/p&gt;
&lt;p&gt;二级索引树是按照二级索引值（age列）按顺序存放的，在相同的二级索引值情况下， 再按主键 id 的顺序存放。知道了这个前提，我们才能知道执行插入语句的时候，插入的位置的下一条记录是谁。&lt;/p&gt;
&lt;p&gt;基于前面的实验，事务 A 是在 age = 39 记录的二级索引上，加了X型的间隙锁，范围是 (22, 39)。&lt;/p&gt;
&lt;p&gt;插入 age = 22 记录的成功和失败的情况分别如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当其他事务插入一条 age = 22，id = 3 的记录的时候，在二级索引树上定位到插入的位置，而&lt;strong&gt;该位置的下一条是 id = 10、age = 22 的记录，该记录的二级索引上没有间隙锁，所以这条插入语句可以执行成功。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当其他事务插入一条 age = 22，id = 12 的记录的时候，在二级索引树上定位到插入的位置，而&lt;strong&gt;该位置的下一条是 id = 20、age = 39 的记录，正好该记录的二级索引上有间隙锁，所以这条插入语句会被阻塞，无法插入成功。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;插入 age = 39 记录的成功和失败的情况分别如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当其他事务插入一条 age = 39，id = 3 的记录的时候，在二级索引树上定位到插入的位置，而&lt;strong&gt;该位置的下一条是 id = 20、age = 39 的记录，正好该记录的二级索引上有间隙锁，所以这条插入语句会被阻塞，无法插入成功。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当其他事务插入一条 age = 39，id = 21 的记录的时候，在二级索引树上定位到插入的位置，而该位置的下一条记录不存在，也就没有间隙锁了，所以这条插入语句可以插入成功。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，&lt;strong&gt;当有一个事务持有二级索引的间隙锁 (22, 39) 时，插入 age = 22 或者 age = 39 记录的语句是否可以执行成功，关键还要考虑插入记录的主键值，因为「二级索引值（age列）+主键值（id列）」才可以确定插入的位置，确定了插入位置后，就要看插入的位置的下一条记录是否有间隙锁，如果有间隙锁，就会发生阻塞，如果没有间隙锁，则可以插入成功。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;知道了这个结论之后，我们再回过头看，非唯一索引等值查询时，查询的记录不存在时，执行&lt;code&gt;select * from performance_schema.data_locks;&lt;/code&gt;输出的结果。
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250913222257.DHptQNaf_Qlvle.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;在前面分析输出结果的时候，说的结论是：「事务 A 在 age = 39 记录的二级索引上（INDEX_NAME: index_age ），加了范围为 (22, 39) 的 X 型间隙锁」。这个结论其实还不够准确，&lt;strong&gt;因为只考虑了 LOCK_DATA 第一个数值（39），没有考虑 LOCK_DATA 第二个数值（20）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;那 &lt;code&gt;LOCK_DATA：39，20&lt;/code&gt; 是什么意思？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LOCK_DATA 第一个数值，也就是 39， 它代表的是 age 值。从前面我们也知道了，LOCK_DATA 第一个数值是 next-key 锁和间隙锁锁住的范围的右边界值。&lt;/li&gt;
&lt;li&gt;LOCK_DATA 第二个数值，也就是 20， 它代表的是 id 值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;之所以 LOCK_DATA 要多显示一个数值（ID值），是因为针对「当某个事务持有非唯一索引的 (22, 39) 间隙锁的时候，其他事务是否可以插入 age = 39 新记录」的问题，还需要考虑插入记录的 id 值。&lt;strong&gt;而 LOCK_DATA 的第二个数值，就是说明在插入 age = 39 新记录时，哪些范围的 id 值是不可以插入的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因此，&lt;code&gt;LOCK_DATA：39，20 + LOCK_MODE : X, GAP&lt;/code&gt;的意思是，事务 A 在 age = 39 记录的二级索引上&lt;code&gt;（INDEX_NAME: index_age ）&lt;/code&gt;，加了 age 值范围为 (22, 39) 的 X 型间隙锁，&lt;strong&gt;同时针对其他事务插入 age 值为 39 的新记录时，不允许插入的新记录的 id 值小于 20&lt;/strong&gt;。如果插入的新记录的 id 值大于 20，则可以插入成功。&lt;/p&gt;
&lt;p&gt;但是我们无法从&lt;code&gt;select * from performance_schema.data_locks;&lt;/code&gt; 输出的结果分析出「在插入 age =22 新记录时，哪些范围的 id 值是可以插入成功的」，这时候就&lt;strong&gt;得自己画出二级索引的 B+ 树的结构，然后确定插入位置后，看下该位置的下一条记录是否存在间隙锁，如果存在间隙锁，则无法插入成功，如果不存在间隙锁，则可以插入成功。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;记录存在的情况&lt;/h3&gt;
&lt;p&gt;假设事务 A 对非唯一索引（age）进行了等值查询，且表中存在 age = 22 的记录。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql&gt; begin;

mysql&gt; select * from user where age = 22 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务 A 加锁变化过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于不是唯一索引，所以肯定存在值相同的记录，于是非唯一索引等值查询的过程是一个扫描的过程，最开始要找的第一行是 age = 22，于是对该二级索引记录加上范围为 (21, 22] 的 next-key 锁。同时，因为 age = 22 符合查询条件，于是对 age = 22 的记录的主键索引加上记录锁，即对 id = 10 这一行加记录锁。&lt;/li&gt;
&lt;li&gt;接着继续扫描，扫描到的第二行是 age = 39，该记录是第一个不符合条件的二级索引记录，所以该二级索引的 next-key 锁会退化成间隙锁，范围是 (22, 39)。&lt;/li&gt;
&lt;li&gt;停止查询。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以看到，事务 A 对主键索引和二级索引都加了 X 型的锁：
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250913223238.CR8a7vr5_SzjN.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们也可以通过 select * from performance_schema.data_locks\G; 这条语句来看看事务 A 加了什么锁。 输出结果如下，我这里只截取了行级锁的内容。
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250913223730.DgzeVzPs_Z1C2rQh.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;从上图的分析，可以看到，事务 A 对二级索引（INDEX_NAME: index_age ）加了两个 X 型锁，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 age = 22 这条记录的二级索引上，加了范围为 (21, 22] 的 next-key 锁，意味着其他事务无法更新或者删除 age = 22 的这一些新记录，针对是否可以插入 age = 21 和 age = 22 的新记录，分析如下：
&lt;ul&gt;
&lt;li&gt;是否可以插入 age = 21 的新记录，还要看插入的新记录的 id 值，&lt;strong&gt;如果插入 age = 21 新记录的 id 值小于 5，那么就可以插入成功&lt;/strong&gt;，因为此时插入的位置的下一条记录是 id = 5，age = 21 的记录，该记录的二级索引上没有间隙锁。&lt;strong&gt;如果插入 age = 21 新记录的 id 值大于 5，那么就无法插入成功&lt;/strong&gt;，因为此时插入的位置的下一条记录是 id = 10，age = 22 的记录，该记录的二级索引上有间隙锁。&lt;/li&gt;
&lt;li&gt;是否可以插入 age = 22 的新记录，还要看插入的新记录的 id 值，从 LOCK_DATA : 22, 10 可以得知，其他事务插入 age 值为 22 的新记录时，&lt;strong&gt;如果插入的新记录的 id 值小于 10，那么插入语句会发生阻塞；如果插入的新记录的 id 大于 10，还要看该新记录插入的位置的下一条记录是否有间隙锁，如果没有间隙锁则可以插入成功，如果有间隙锁，则无法插入成功&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;在 age = 39 这条记录的二级索引上，加了范围 (22, 39) 的间隙锁。意味着其他事务无法插入 age 值为 23、24、..... 、38 的这一些新记录，针对是否可以插入 age = 22 和 age = 39 的新记录，分析如下：
&lt;ul&gt;
&lt;li&gt;是否可以插入 age = 22 的新记录，还要看插入的新记录的 id 值，&lt;strong&gt;如果插入 age = 22 新记录的 id 值小于 10，那么插入语句会被阻塞，无法插入&lt;/strong&gt;，因为此时插入的位置的下一条记录是 id = 10，age = 22 的记录，该记录的二级索引上有间隙锁（ age = 22 这条记录的二级索引上有 next-key 锁）。&lt;strong&gt;如果插入 age = 22 新记录的 id 值大于 10，也无法插入&lt;/strong&gt;，因为此时插入的位置的下一条记录是 id = 20，age = 39 的记录，该记录的二级索引上有间隙锁。&lt;/li&gt;
&lt;li&gt;是否可以插入 age = 39 的新记录，还要看插入的新记录的 id 值，从 LOCK_DATA : 39, 20 可以得知，其他事务插入 age 值为 39 的新记录时，&lt;strong&gt;如果插入的新记录的 id 值小于 20，那么插入语句会发生阻塞，如果插入的新记录的 id 大于 20，则可以插入成功&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时，事务 A 还对主键索引（INDEX_NAME: PRIMARY ）加了 X 型的记录锁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;在 id = 10 这条记录的主键索引上，加了记录锁，意味着其他事务无法更新或者删除 id = 10 的这一行记录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;? &lt;strong&gt;为什么这个实验案例中，需要在二级索引索引上加范围 (22, 39) 的间隙锁？&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要找到这个问题的答案，我们要明白 MySQL 在可重复读的隔离级别场景下，为什么要引入间隙锁？其实是为了避免幻读现象的发生。&lt;/p&gt;
&lt;p&gt;如果这个实验案例中：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select * from user where age = 22 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果事务 A 不在二级索引索引上加范围 (22, 39) 的间隙锁，只在二级索引索引上加范围为 (21, 22] 的 next-key 锁的话，那么就会有幻读的问题。&lt;/p&gt;
&lt;p&gt;前面我也说过，在非唯一索引上加了范围为 (21, 22] 的 next-key 锁，是无法完全锁住 age = 22 新记录的插入，因为对于是否可以插入 age = 22 的新记录，还要看插入的新记录的 id 值，从&lt;code&gt;LOCK_DATA : 22, 10&lt;/code&gt;可以得知，其他事务插入 age 值为 22 的新记录时，如果插入的新记录的 id 值小于 10，那么插入语句会发生阻塞，如果插入的新记录的 id 值大于 10，则可以插入成功。&lt;/p&gt;
&lt;p&gt;也就是说，只在二级索引索引（非唯一索引）上加范围为 (21, 22] 的 next-key 锁，其他事务是有可能插入 age 值为 22 的新记录的（比如插入一个 age = 22，id = 12 的新记录），那么如果事务 A 再一次查询 age = 22 的记录的时候，前后两次查询 age = 22 的结果集就不一样了，这时就发生了幻读的现象。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那么当在 age = 39 这条记录的二级索引索引上加了范围为 (22, 39) 的间隙锁后，其他事务是无法插入一个 age = 22，id = 12 的新记录，因为当其他事务插入一条 age = 22，id = 12 的新记录的时候，在二级索引树上定位到插入的位置，而该位置的下一条是 id = 20、age = 39 的记录，正好该记录的二级索引上有间隙锁，所以这条插入语句会被阻塞，无法插入成功，这样就避免幻读现象的发生。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以，为了避免幻读现象的发生，就需要在二级索引索引上加范围 (22, 39) 的间隙锁。&lt;/p&gt;
&lt;h2&gt;非唯一索引范围查询&lt;/h2&gt;
&lt;p&gt;非唯一索引和主键索引的范围查询的加锁也有所不同，&lt;strong&gt;不同之处在于非唯一索引范围查询，索引的 next-key lock 不会有退化为间隙锁和记录锁的情况，也就是非唯一索引进行范围查询时，对二级索引记录加锁都是加 next-key 锁。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;就带大家简单分析一下，事务 A 的这条范围查询语句：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;mysql&gt; begin;
mysql&gt; select * from user where age &gt;= 22 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;事务 A 的加锁变化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;最开始要找的第一行是 age = 22，虽然范围查询语句包含等值查询，&lt;strong&gt;但是这里不是唯一索引范围查询，所以是不会发生退化锁的现象，因此对该二级索引记录加 next-key 锁，范围是 (21, 22]。同时，对 age = 22 这条记录的主键索引加记录锁，即对 id = 10 这一行记录的主键索引加记录锁。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;由于是范围查询，接着继续扫描已经存在的二级索引记录。扫面的第二行是 age = 39 的二级索引记录，于是对该二级索引记录加 next-key 锁，范围是 (22, 39]，同时，对 age = 39 这条记录的主键索引加记录锁，即对 id = 20 这一行记录的主键索引加记录锁。&lt;/li&gt;
&lt;li&gt;虽然我们看见表中最后一条二级索引记录是 age = 39 的记录，但是实际在 Innodb 存储引擎中，会用一个特殊的记录来标识最后一条记录，该特殊的记录的名字叫&lt;code&gt;supremum pseudo-record&lt;/code&gt;，所以扫描第二行的时候，也就扫描到了这个特殊记录的时候，会对该二级索引记录加的是范围为 (39, +∞] 的 next-key 锁。&lt;/li&gt;
&lt;li&gt;停止查询&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250913225110.Clnr21dD_1zjxGw.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? &lt;strong&gt;在 age &gt;= 22 的范围查询中，明明查询 age = 22 的记录存在并且属于等值查询，为什么不会像唯一索引那样，将 age = 22 记录的二级索引上的 next-key 锁退化为记录锁？&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;因为 age 字段是非唯一索引，不具有唯一性，所以如果只加记录锁（记录锁无法防止插入，只能防止删除或者修改），就会导致其他事务插入一条 age = 22 的记录，这样前后两次查询的结果集就不相同了，出现了幻读现象。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;没有加索引的查询&lt;/h2&gt;
&lt;p&gt;前面的案例，我们的查询语句都有使用索引查询，也就是查询记录的时候，是通过索引扫描的方式查询的，然后对扫描出来的记录进行加锁。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;如果锁定读查询语句，没有使用索引列作为查询条件，或者查询语句没有走索引查询，导致扫描是全表扫描。那么，每一条记录的索引上都会加 next-key 锁，这样就相当于锁住的全表，这时如果其他事务对该表进行增、删、改操作的时候，都会被阻塞。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;不只是锁定读查询语句不加索引才会导致这种情况，update 和 delete 语句如果查询条件不加索引，那么由于扫描的方式是全表扫描，于是就会对每一条记录的索引上都会加 next-key 锁，这样就相当于锁住的全表。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因此，&lt;strong&gt;在线上在执行 update、delete、select ... for update 等具有加锁性质的语句，一定要检查语句是否走了索引，如果是全表扫描的话，会对每一个索引加 next-key 锁，相当于把整个表锁住了&lt;/strong&gt;，这是挺严重的问题。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;#x26; &lt;strong&gt;如何避免这种事故的发生&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大致的意思是，当 sql_safe_updates 设置为 1 时。
update 语句必须满足如下条件之一才能执行成功：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 where，并且 where 条件中必须有索引列；&lt;/li&gt;
&lt;li&gt;使用 limit；&lt;/li&gt;
&lt;li&gt;同时使用 where 和 limit，此时 where 条件中可以没有索引列；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;delete 语句必须满足以下条件能执行成功：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同时使用 where 和 limit，此时 where 条件中可以没有索引列；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果 where 条件带上了索引列，但是优化器最终扫描选择的是全表，而不是索引的话，我们可以使用&lt;code&gt;force index([index_name])&lt;/code&gt;可以告诉优化器使用哪个索引，以此避免有几率锁全表带来的隐患。&lt;/p&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;唯一索引等值查询：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当查询的记录是「存在」的，在索引树上定位到这一条记录后，&lt;strong&gt;将该记录的索引中的 next-key lock 会退化成「记录锁」。&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;当查询的记录是「不存在」的，在索引树找到第一条大于该查询记录的记录后，将该记录的索引中的 next-key lock 会退化成「间隙锁」。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;非唯一索引等值查询：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当查询的记录「存在」时，由于不是唯一索引，所以肯定存在索引值相同的记录，于是非唯一索引等值查询的过程是一个扫描的过程，直到扫描到第一个不符合条件的二级索引记录就停止扫描，然后&lt;strong&gt;在扫描的过程中，对扫描到的二级索引记录加的是 next-key 锁，而对于第一个不符合条件的二级索引记录，该二级索引的 next-key 锁会退化成间隙锁。同时，在符合查询条件的记录的主键索引上加记录锁&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;当查询的记录「不存在」时，&lt;strong&gt;扫描到第一条不符合条件的二级索引记录，该二级索引的 next-key 锁会退化成间隙锁。因为不存在满足查询条件的记录，所以不会对主键索引加锁。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;非唯一索引和主键索引的范围查询的加锁规则不同之处在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;唯一索引在满足一些条件的时候，索引的 next-key lock 退化为间隙锁或者记录锁。&lt;/li&gt;
&lt;li&gt;非唯一索引范围查询，索引的 next-key lock 不会退化为间隙锁和记录锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实理解 MySQL 为什么要这样加锁，主要要以避免幻读角度去分析，这样就很容易理解这些加锁的规则了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;还有一件很重要的事情，在线上在执行 update、delete、select ... for update 等具有加锁性质的语句，一定要检查语句是否走了索引，如果是全表扫描的话，会对每一个索引加 next-key 锁，相当于把整个表锁住了，这是挺严重的问题。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;MySQL死锁了，怎么办&lt;/h1&gt;
&lt;h2&gt;死锁的发生&lt;/h2&gt;
&lt;p&gt;我建了一张订单表，其中 id 字段为主键索引，order_no 字段普通索引，也就是非唯一索引：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE `t_order` (
  `id` int NOT NULL AUTO_INCREMENT,
  `order_no` int DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `index_order` (`order_no`) USING BTREE
) ENGINE=InnoDB ;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，&lt;code&gt;t_order&lt;/code&gt; 表里现在已经有了 6 条记录：
&lt;img src=&quot;https://cdn.xiaolincoding.com//mysql/other/54fc00f9f87a60ab7b5ba92d824a892d.png&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;
&lt;p&gt;假设这时有两事务，一个事务要插入订单 1007 ，另外一个事务要插入订单 1008，因为需要对订单做幂等性校验，所以两个事务先要查询该订单是否存在，不存在才插入记录，过程如下：
&lt;img src=&quot;https://cdn.xiaolincoding.com//mysql/other/90c1e01d0345de639e3426cea0390e80.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，两个事务都陷入了等待状态（前提没有打开死锁检测），也就是发生了死锁，因为都在相互等待对方释放锁。&lt;/p&gt;
&lt;h2&gt;为什么会产生死锁？&lt;/h2&gt;
&lt;p&gt;事务 A 在执行下面这条语句的时候：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select id from t_order where order_no = 1007 for update;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们可以通过 &lt;code&gt;select * from performance_schema.data_locks\G;&lt;/code&gt; 这条语句，查看事务执行 SQL 过程中加了什么锁。
&lt;img src=&quot;https://cdn.xiaolincoding.com//mysql/other/1cf8614eba3b45b9874dc6204b4d0cd1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;从上图可以看到，共加了两个锁，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表锁：X 类型的意向锁；&lt;/li&gt;
&lt;li&gt;行锁：X 类型的next-key 锁（临键锁）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;这里我们重点关注行锁，图中 LOCK_TYPE 中的 RECORD 表示行级锁，而不是记录锁的意思，通过 LOCK_MODE 可以确认是 next-key 锁，还是间隙锁，还是记录锁：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果 LOCK_MODE 为 &lt;code&gt;X&lt;/code&gt; ，说明是 X 型的 next-key 锁；&lt;/li&gt;
&lt;li&gt;如果 LOCK_MODE 为 &lt;code&gt;X, REC_NOT_GAP&lt;/code&gt; ，说明是 X 型的记录锁；&lt;/li&gt;
&lt;li&gt;如果 LOCK_MODE 为 &lt;code&gt;X, GAP&lt;/code&gt; ，说明是 X 型的间隙锁；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;因此，此时事务 A 在二级索引（INDEX_NAME: index_order）上加的是 X 型的 next-key 锁，锁范围是 &lt;code&gt;(1006, +∞]&lt;/code&gt;&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;next-key 锁的范围 (1006, +∞]，是怎么确定的？&lt;/p&gt;
&lt;p&gt;根据我的经验，如果 LOCK_MODE 是 next-key 锁或者间隙锁，那么 LOCK_DATA 就表示锁的范围最右值，此次的事务 A 的 LOCK_DATA 是 supremum pseudo-record，表示的是 +∞。然后锁范围的最左值是 t_order 表中最后一个记录的 index_order 的值，也就是 1006。因此，next-key 锁的范围 (1006, +∞]。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;为什么上面事务 A 的 next-key lock 并没有退化为间隙锁？&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果表中最后一个记录的 order_no 为 1005，那么等值查询 order_no = 1006（不存在），就是 next key lock，如上面事务 A 的情况。&lt;/li&gt;
&lt;li&gt;如果表中最后一个记录的 order_no 为 1010，那么等值查询 order_no = 1006（不存在），就是间隙锁，比如下图：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://cdn.xiaolincoding.com//mysql/other/fb6709207ac445ddbc175e3cdf993ff2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;当事务 B 往事务 A next-key 锁的范围 (1006, +∞] 里插入 id = 1008 的记录就会被锁住：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;Insert into t_order (order_no, create_date) values (1008, now());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为当我们执行以下插入语句时，会在插入间隙上获取插入意向锁， &lt;strong&gt;而插入意向锁与间隙锁是冲突的，所以当其它事务持有该间隙的间隙锁时，需要等待其它事务释放间隙锁之后，才能获取到插入意向锁。而间隙锁与间隙锁之间是兼容的，所以所以两个事务中 &lt;code&gt;select ... for update&lt;/code&gt; 语句并不会相互影响&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;案例中的事务 A 和事务 B 在执行完后 &lt;code&gt;select ... for update&lt;/code&gt; 语句后都持有范围为 &lt;code&gt;(1006,+∞]&lt;/code&gt; 的next-key 锁，而接下来的插入操作为了获取到插入意向锁，都在等待对方事务的间隙锁释放，于是就造成了循环等待，导致死锁。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? &lt;strong&gt;为什么间隙锁与间隙锁之间是兼容的？&lt;/strong&gt;
&lt;strong&gt;间隙锁的意义只在于阻止区间被插入&lt;/strong&gt; ，因此是可以共存的。 &lt;strong&gt;一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁&lt;/strong&gt; ，共享和排他的间隙锁是没有区别的，他们相互不冲突，且功能相同，即两个事务可以同时持有包含共同间隙的间隙锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这里的共同间隙包括两种场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其一是两个间隙锁的间隙区间完全一样；&lt;/li&gt;
&lt;li&gt;其二是一个间隙锁包含的间隙区间是另一个间隙锁包含间隙区间的子集。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但是有一点要注意， &lt;strong&gt;next-key lock 是包含间隙锁+记录锁的，如果一个事务获取了 X 型的 next-key lock，那么另外一个事务在获取相同范围的 X 型的 next-key lock 时，是会被阻塞的&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;比如，一个事务持有了范围为 (1, 10] 的 X 型的 next-key lock，那么另外一个事务在获取相同范围的 X 型的 next-key lock 时，就会被阻塞。&lt;/p&gt;
&lt;p&gt;虽然相同范围的间隙锁是多个事务相互兼容的，但对于记录锁，我们是要考虑 X 型与 S 型关系。X 型的记录锁与 X 型的记录锁是冲突的，比如一个事务执行了&lt;code&gt;select... where id = 1 for update&lt;/code&gt;，后一个事务在执行这条语句的时候，就会被阻塞的。&lt;/p&gt;
&lt;p&gt;但是还要注意！对于这种范围为 (1006, +∞] 的 next-key lock，两个事务是可以同时持有的，不会冲突。因为 +∞ 并不是一个真实的记录，自然就不需要考虑 X 型与 S 型关系。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;插入意向锁是什么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;注意！插入意向锁名字虽然有意向锁，但是它并不是意向锁，它是一种特殊的间隙锁。&lt;/p&gt;
&lt;p&gt;在MySQL的官方文档中有以下重要描述：&lt;/p&gt;
&lt;p&gt;这段话表明尽管 &lt;strong&gt;插入意向锁是一种特殊的间隙锁，但不同于间隙锁的是，该锁只用于并发插入操作&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;如果说间隙锁锁住的是一个区间，那么「插入意向锁」锁住的就是一个点。因而从这个角度来说，插入意向锁确实是一种特殊的间隙锁。&lt;/p&gt;
&lt;p&gt;插入意向锁与间隙锁的另一个非常重要的差别是：尽管「插入意向锁」也属于间隙锁，但两个事务却不能在同一时间内，一个拥有间隙锁，另一个拥有该间隙区间内的插入意向锁（当然，插入意向锁如果不在间隙锁区间内则是可以的）。&lt;/p&gt;
&lt;p&gt;另外，我补充一点，插入意向锁的生成时机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每插入一条新记录，都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁，如果已加间隙锁，此时会生成一个插入意向锁，然后锁的状态设置为等待状态（ &lt;em&gt;PS：MySQL 加锁时，是先生成锁结构，然后设置锁的状态，如果锁状态是等待状态，并不是意味着事务成功获取到了锁，只有当锁状态为正常状态时，才代表事务成功获取到了锁&lt;/em&gt; ），现象就是 Insert 语句会被阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Insert 语句是怎么加行级锁的？&lt;/h2&gt;
&lt;p&gt;Insert 语句在正常执行时是不会生成锁结构的，它是靠聚簇索引记录自带的 trx_id 隐藏列来作为&lt;strong&gt;隐式锁&lt;/strong&gt;来保护记录的。&lt;/p&gt;
&lt;h3&gt;隐式锁&lt;/h3&gt;
&lt;p&gt;当事务需要加锁的时，如果这个锁不可能发生冲突，InnoDB会跳过加锁环节，这种机制称为隐式锁。隐式锁是 InnoDB 实现的一种延迟加锁机制，其特点是只有在可能发生冲突时才加锁，从而减少了锁的数量，提高了系统整体性能。&lt;/p&gt;
&lt;p&gt;隐式锁就是在 Insert 过程中不加锁，只有在特殊情况下，才会将隐式锁转换为显示锁，这里我们列举两个场景。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果记录之间加有间隙锁，为了避免幻读，此时是不能插入记录的；&lt;/li&gt;
&lt;li&gt;如果 Insert 的记录和已有记录存在唯一键冲突，此时也不能插入记录；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;记录之间加有间隙锁&lt;/h3&gt;
&lt;p&gt;每插入一条新记录，都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁，如果已加间隙锁，此时会生成一个插入意向锁，然后锁的状态设置为等待状态（ &lt;strong&gt;PS：MySQL 加锁时，是先生成锁结构，然后设置锁的状态，如果锁状态是等待状态，并不是意味着事务成功获取到了锁，只有当锁状态为正常状态时，才代表事务成功获取到了锁&lt;/strong&gt; ），现象就是 Insert 语句会被阻塞。&lt;/p&gt;
&lt;p&gt;举个例子，现在 t_order 表中，只有这些数据， &lt;strong&gt;order_no 是二级索引&lt;/strong&gt; 。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/5%E6%9D%A1%E6%95%B0%E6%8D%AE.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;现在，事务 A 执行了下面这条语句。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;# 事务 A
mysql&gt; begin;
Query OK, 0 rows affected (0.01 sec)

mysql&gt; select * from t_order where order_no = 1006 for update;
Empty set (0.01 sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，我们执行 &lt;code&gt;select * from performance_schema.data_locks;&lt;/code&gt; 语句 ，确定事务 A 加了什么类型的锁，这里只关注在记录上加锁的类型。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E4%BA%8B%E5%8A%A1A%E9%97%B4%E9%9A%99%E9%94%81.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;本次的例子加的是 next-key 锁（记录锁+间隙锁），锁范围是 &lt;code&gt;（1005, +∞]&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;然后，有个事务 B 在这个间隙锁中，插入了一个记录，那么此时该事务 B 就会被阻塞：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;# 事务 B 插入一条记录
mysql&gt; begin;
Query OK, 0 rows affected (0.01 sec)

mysql&gt; insert into t_order(order_no, create_date) values(1010,now());
### 阻塞状态。。。。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着，我们执行 &lt;code&gt;select * from performance_schema.data_locks\G;&lt;/code&gt; 语句 ，确定事务 B 加了什么类型的锁，这里只关注在记录上加锁的类型。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E4%BA%8B%E5%8A%A1b%E6%8F%92%E5%85%A5%E6%84%8F%E5%90%91%E9%94%81.png&quot; alt=&quot;&quot;&gt;
可以看到，事务 B 的状态为等待状态（LOCK_STATUS: WAITING），因为向事务 A 生成的 next-key 锁（记录锁+间隙锁）范围 &lt;code&gt;（1005, +∞]&lt;/code&gt; 中插入了一条记录，所以事务 B 的插入操作生成了一个插入意向锁（ &lt;code&gt;LOCK_MODE: X,INSERT_INTENTION&lt;/code&gt; ），锁的状态是等待状态，意味着事务 B 并没有成功获取到插入意向锁，因此事务 B 发生阻塞。&lt;/p&gt;
&lt;h3&gt;遇到唯一键冲突&lt;/h3&gt;
&lt;p&gt;如果在插入新记录时，插入了一个与「已有的记录的主键或者唯一二级索引列值相同」的记录（不过可以有多条记录的唯一二级索引列的值同时为NULL，这里不考虑这种情况），此时插入就会失败，然后对于这条记录加上了 &lt;strong&gt;S 型的锁&lt;/strong&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果主键索引重复，插入新记录的事务会给已存在的主键值重复的聚簇索引记录 &lt;strong&gt;添加 S 型记录锁&lt;/strong&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果唯一二级索引重复，插入新记录的事务都会给已存在的二级索引列值重复的二级索引记录 &lt;strong&gt;添加 S 型 next-key 锁&lt;/strong&gt; 。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&amp;#x26; &lt;strong&gt;主键索引冲突&lt;/strong&gt;
下面举个「主键冲突」的例子，MySQL 8.0 版本，事务隔离级别为可重复读（默认隔离级别）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;t_order 表中的 id 字段为主键索引，并且已经存在 id 值为 5 的记录，此时有个事务，插入了一条 id 为 5 的记录，就会报主键索引冲突的错误。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E4%B8%BB%E9%94%AE%E5%86%B2%E7%AA%81.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是除了报错之外，还做一个很重要的事情，就是对 id 为 5 的这条记录加上了 &lt;strong&gt;S 型的记录锁&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;可以执行 &lt;code&gt;select * from performance_schema.data_locks\G;&lt;/code&gt; 语句，确定事务加了什么锁。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E4%B8%BB%E9%94%AE%E5%86%B2%E7%AA%81%E9%94%81.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，主键索引为 5 （LOCK_DATA）的这条记录中加了锁类型为 S 型的记录锁。注意，这里 LOCK_TYPE 中的 RECORD 表示行级锁，而不是记录锁的意思。如果是 S 型记录锁的话，LOCK_MODE 会显示&lt;code&gt;S, REC_NOT_GAP&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;所以，在隔离级别是「可重复读」的情况下，如果在插入数据的时候，发生了主键索引冲突，插入新记录的事务会给已存在的主键值重复的聚簇索引记录 &lt;strong&gt;添加 S 型记录锁&lt;/strong&gt; 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&amp;#x26; &lt;strong&gt;唯一二级索引冲突&lt;/strong&gt;
下面举个「唯一二级索引冲突」的例子，MySQL 8.0 版本，事务隔离级别为可重复读（默认隔离级别）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;t_order 表中的 order_no 字段为唯一二级索引，并且已经存在 order_no 值为 1001 的记录，此时事务 A，插入了 order_no 为 1001 的记录，就出现了报错。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E6%8F%92%E5%85%A5%E5%A4%B1%E8%B4%A5.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是除了报错之外，还做一个很重要的事情，就是对 order_no 值为 1001 这条记录加上了 &lt;strong&gt;S 型的 next-key 锁&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;我们可以执行 &lt;code&gt;select * from performance_schema.data_locks;&lt;/code&gt; 语句 ，确定事务加了什么类型的锁，这里只关注在记录上加锁的类型。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/s%E7%B1%BB%E5%9E%8B%E9%94%81.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到， &lt;strong&gt;index_order 二级索引加了 S 型的 next-key 锁，范围是(-∞, 1001]&lt;/strong&gt; 。注意，这里 LOCK_TYPE 中的 RECORD 表示行级锁，而不是记录锁的意思。如果是记录锁的话，LOCK_MODE 会显示 &lt;code&gt;S, REC_NOT_GAP&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;此时，事务 B 执行了&lt;code&gt;select * from t_order where order_no = 1001 for update;&lt;/code&gt;就会阻塞，因为这条语句想加 X 型的锁，是与 S 型的锁是冲突的，所以就会被阻塞。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E5%94%AF%E4%B8%80%E7%B4%A2%E5%BC%95%E5%86%B2%E7%AA%81.drawio.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;我们也可以从 performance_schema.data_locks 这个表中看到，事务 B 的状态（LOCK_STATUS）是等待状态，加锁的类型 X 型的记录锁（LOCK_MODE: X,REC_NOT_GAP ）。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E4%BA%8B%E5%8A%A1b%E7%AD%89%E5%BE%85%E7%8A%B6%E6%80%81.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;上面的案例是针对唯一二级索引重复而插入失败的场景。&lt;/p&gt;
&lt;h3&gt;执行了相同的insert语句的场景。&lt;/h3&gt;
&lt;p&gt;现在 t_order 表中，只有这些数据， &lt;strong&gt;order_no 为唯一二级索引&lt;/strong&gt; 。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/5%E6%9D%A1%E6%95%B0%E6%8D%AE.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;在隔离级别可重复读的情况下，开启两个事务，前后执行相同的 Insert 语句，此时 &lt;strong&gt;事务 B 的 Insert 语句会发生阻塞&lt;/strong&gt; 。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E5%94%AF%E4%B8%80%E7%B4%A2%E5%BC%95%E5%8A%A0%E9%94%81.drawio.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;两个事务的加锁过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;事务 A 先插入 order_no 为 1006 的记录，可以插入成功，此时对应的唯一二级索引记录被「隐式锁」保护，此时还没有实际的锁结构（执行完这里的时候，你可以看查 performance_schema.data_locks 信息，可以看到这条记录是没有加任何锁的）；&lt;/li&gt;
&lt;li&gt;接着，事务 B 也插入 order_no 为 1006 的记录，由于事务 A 已经插入 order_no 值为 1006 的记录，所以事务 B 在插入二级索引记录时会遇到重复的唯一二级索引列值，此时事务 B 想获取一个 S 型 next-key 锁，但是事务 A 并未提交， &lt;strong&gt;事务 A 插入的 order_no 值为 1006 的记录上的「隐式锁」会变「显示锁」且锁类型为 X 型的记录锁，所以事务 B 向获取 S 型 next-key 锁时会遇到锁冲突，事务 B 进入阻塞状态&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们可以执行 &lt;code&gt;select * from performance_schema.data_locks;&lt;/code&gt; 语句 ，确定事务加了什么类型的锁，这里只关注在记录上加锁的类型。&lt;/p&gt;
&lt;p&gt;先看事务 A 对 order_no 为 1006 的记录加了什么锁？&lt;/p&gt;
&lt;p&gt;从下图可以看到， &lt;strong&gt;事务 A 对 order_no 为 1006 记录加上了类型为 X 型的记录锁&lt;/strong&gt; （ &lt;strong&gt;注意，这个是在执行事务 B 之后才产生的锁，没执行事务 B 之前，该记录还是隐式锁&lt;/strong&gt; ）。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E4%BA%8B%E5%8A%A1a%E6%98%BE%E7%A4%BA%E9%94%81.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后看事务 B 想对 order_no 为 1006 的记录加什么锁？&lt;/p&gt;
&lt;p&gt;从下图可以看到， &lt;strong&gt;事务 B 想对 order_no 为 1006 的记录加 S 型的 next-key 锁，但是由于事务 A 在该记录上持有了 X 型的记录锁，这两个锁是冲突的，所以导致事务 B 处于等待状态&lt;/strong&gt; 。
&lt;img src=&quot;https://cdn.xiaolincoding.com/gh/xiaolincoder/mysql/%E9%94%81/%E4%BA%8B%E5%8A%A1b%E7%AD%89%E5%BE%85.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;从这个实验可以得知，并发多个事务的时候，第一个事务插入的记录，并不会加锁，而是会用隐式锁保护唯一二级索引的记录。&lt;/p&gt;
&lt;p&gt;但是当第一个事务还未提交的时候，有其他事务插入了与第一个事务相同的记录，第二个事务就会 &lt;strong&gt;被阻塞&lt;/strong&gt; ， &lt;strong&gt;因为此时第一事务插入的记录中的隐式锁会变为显示锁且类型是 X 型的记录锁，而第二个事务是想对该记录加上 S 型的 next-key 锁，X 型与 S 型的锁是冲突的&lt;/strong&gt; ，所以导致第二个事务会等待，直到第一个事务提交后，释放了锁。&lt;/p&gt;
&lt;p&gt;如果 order_no 不是唯一二级索引，那么两个事务，前后执行相同的 Insert 语句，是不会发生阻塞的，就如前面的这个例子。
&lt;img src=&quot;https://cdn.xiaolincoding.com//mysql/other/8ae18f10f1a89aac5e93f0e9794e469e-20230310003449585.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;如何避免死锁？&lt;/h2&gt;
&lt;p&gt;死锁的四个必要条件： &lt;strong&gt;互斥、占有且等待、不可强占用、循环等待&lt;/strong&gt; 。只要系统发生死锁，这些条件必然成立，但是只要破坏任意一个条件就死锁就不会成立。&lt;/p&gt;
&lt;p&gt;在数据库层面，有两种策略通过「打破循环等待条件」来解除死锁状态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;设置事务等待锁的超时时间&lt;/strong&gt; 。当一个事务的等待时间超过该值后，就对这个事务进行回滚，于是锁就释放了，另一个事务就可以继续执行了。在 InnoDB 中，参数 &lt;code&gt;innodb_lock_wait_timeout&lt;/code&gt; 是用来设置超时时间的，默认值时 50 秒。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开启主动死锁检测&lt;/strong&gt; 。主动死锁检测在发现死锁后，主动回滚死锁链条中的某一个事务，让其他事务得以继续执行。将参数 &lt;code&gt;innodb_deadlock_detect&lt;/code&gt; 设置为 on，表示开启这个逻辑，默认就开启。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面这个两种策略是「当有死锁发生时」的避免方式。&lt;/p&gt;
&lt;p&gt;我们可以回归业务的角度来预防死锁，对订单做幂等性校验的目的是为了保证不会出现重复的订单，那我们可以直接将 order_no 字段设置为唯一索引列，利用它的唯一性来保证订单表不会出现重复的订单，不过有一点不好的地方就是在我们插入一个已经存在的订单记录时就会抛出异常。&lt;/p&gt;
&lt;h1&gt;面试题：死锁场景分析&lt;/h1&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE `t_student` (
	`id` int NOT NULL,
	`no` varchar(255) DEFAULT NULL,
	`name` varchar(255) DEFAULT NULL,
	`age` int DEFAULT NULL,
	`score` int DEFAULT NULL,
	PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，插入相关的数据后，t_student 表中的记录如下：
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250914150544.CZzY7EDo_2bapwQ.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;接着进行了以下的查询
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250914150738.B6aVkhNr_Z2unOeb.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，事务 A 和 事务 B 都在执行 insert 语句后，都陷入了等待状态（前提没有打开死锁检测），也就是发生了死锁，因为都在相互等待对方释放锁。&lt;/p&gt;
&lt;h2&gt;Time1阶段加锁分析&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250914153315.DUX1ZzbZ_ZdEcbo.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;从上图可以看到，共加了两个锁，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;表锁：X 类型的意向锁；&lt;/li&gt;
&lt;li&gt;行锁：X 类型的间隙锁；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;事务 A 在主键索引（INDEX_NAME : PRIMARY）上加的是间隙锁，锁范围是&lt;code&gt;(20, 30)&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Time2 阶段加锁分析&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250914153943.BLFfF0vk_1AW6UT.webp&quot; alt=&quot;image.png&quot;&gt;
从上图可以看到，行锁是 X 类型的间隙锁，间隙锁的范围是&lt;code&gt;(20, 30)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;两个事务的间隙锁之间是相互兼容的，不会产生冲突。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;间隙锁的意义只在于阻止区间被插入，因此是可以共存的。一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁，共享（S型）和排他（X型）的间隙锁是没有区别的，他们相互不冲突，且功能相同。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Time3阶段加锁分析&lt;/h2&gt;
&lt;p&gt;Time 3，事务 A 插入了一条记录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;# Time 3 阶段，事务 A 插入了一条记录
mysql&gt; insert into t_student(id, no, name, age,score) value (25, &apos;S0025&apos;, &apos;sony&apos;, 28, 90);
/// 阻塞等待......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，事务 A 就陷入了等待状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250914154151.CT1dGsYJ_1pGDM5.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看到，事务 A 的状态为等待状态&lt;code&gt;（LOCK_STATUS: WAITING）&lt;/code&gt;，因为向事务 B 生成的间隙锁（范围 (20, 30)）中插入了一条记录，所以事务 A 的插入操作生成了一个插入意向锁&lt;code&gt;（LOCK_MODE:INSERT_INTENTION）&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;? &lt;strong&gt;插入意向锁是什么？&lt;/strong&gt;
&lt;strong&gt;插入意向锁是一种特殊的间隙锁，但不同于间隙锁的是，该锁只用于并发插入操作&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果说间隙锁锁住的是一个区间，那么「插入意向锁」锁住的就是一个点。因而从这个角度来说，插入意向锁确实是一种特殊的间隙锁。&lt;/p&gt;
&lt;p&gt;插入意向锁与间隙锁的另一个非常重要的差别是：&lt;strong&gt;尽管「插入意向锁」也属于间隙锁，但两个事务却不能在同一时间内，一个拥有间隙锁，另一个拥有该间隙区间内的插入意向锁（当然，插入意向锁如果不在间隙锁区间内则是可以的）。所以，插入意向锁和间隙锁之间是冲突的。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;另外，补充一点，插入意向锁的生成时机：
每插入一条新记录，都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁，如果已加间隙锁，此时会生成一个插入意向锁，然后锁的状态设置为等待状态（PS：&lt;strong&gt;MySQL 加锁时，是先生成锁结构，然后设置锁的状态，如果锁状态是等待状态，并不是意味着事务成功获取到了锁，只有当锁状态为正常状态时，才代表事务成功获取到了锁&lt;/strong&gt;），现象就是 Insert 语句会被阻塞。&lt;/p&gt;
&lt;h2&gt;Time4&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;# Time 4 阶段，事务 B 插入了一条记录
mysql&gt; insert into t_student(id, no, name, age,score) value (26, &apos;S0026&apos;, &apos;ace&apos;, 28, 90);

/// 阻塞等待......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250914154818.Dy7kgS2X_1GNSBL.webp&quot; alt=&quot;image.png&quot;&gt;
可以看到，事务 B 在生成插入意向锁时而导致被阻塞，这是因为事务 B 向事务 A 生成的范围为 (20, 30) 的间隙锁插入了一条记录，而插入意向锁和间隙锁是冲突的，所以事务 B 在获取插入意向锁时就陷入了等待状态。&lt;/p&gt;
&lt;p&gt;本次案例中，事务 A 和事务 B 在执行完后 update 语句后都持有范围为(20, 30）的间隙锁，而接下来的插入操作为了获取到插入意向锁，都在等待对方事务的间隙锁释放，于是就造成了循环等待，满足了死锁的四个条件：&lt;strong&gt;互斥、占有且等待、不可强占用、循环等待&lt;/strong&gt;，因此发生了死锁。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250914154914.Bahrg1s4_Z21Xr7u.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;</content:encoded><category>MySQL</category><category>八股</category></item><item><title>Java基础</title><link>https://wojiecihuo.cn/blog/java%E5%85%AB%E8%82%A1/java%E5%9F%BA%E7%A1%80</link><guid isPermaLink="true">https://wojiecihuo.cn/blog/java%E5%85%AB%E8%82%A1/java%E5%9F%BA%E7%A1%80</guid><description>Java基础八股整理(自用)</description><pubDate>Sat, 09 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;1. 数据类型&lt;/h1&gt;
&lt;h2&gt;1.1 引用类型vs基础类型&lt;/h2&gt;
&lt;p&gt;Java中的数据类型可以分为两类：基本类型和引用类型。基本类型包括：整型（byte，short，int，long）、浮点型（float，double）、字符型（char）、布尔型（boolean）。&lt;strong&gt;引用类型是指除了基本的变量类型之外的所有类型（如通过 class 定义的类型）。&lt;/strong&gt; 基本类型只有一块存储空间（分配在stack中），而引用类型有两块存储空间（一块在stack中，一块中heap中）
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250512203346.BUzs-kvV_1rfkP2.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;1.2 Integer相比int有什么优点&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;基本类型和引用类型：int是一种基本数据类型，而Integer是一种引用类型。基本数据类型是预定义的，不需要实例化就可以使用。而引用类型则需要通过实例化对象来使用，必须为对象分配内存。在性能方面，基本数据类型的操作通常比相应的引用类型快。&lt;/li&gt;
&lt;li&gt;自动装箱和拆箱：Integer可以实现自动装箱和拆箱。&lt;/li&gt;
&lt;li&gt;空指针异常：如果对一个未经初始化的Integer变量进行操作，就会出现空指针异常。这是因为它被赋予了null值，而null值是无法进行自动拆箱的。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;一个Integer对象占用16个字节的存储空间，而一个int类型数据只占用4字节存储空间&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;1.3 integer的缓存&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;Java的Integer类内部实现了一个静态缓存池，用于存储特定范围内的整数值对应的Integer对象。 默认情况下，这个范围是-128至127。当通过Integer.valueOf(int)方法创建一个在这个范围内的整数对象时，并不会每次都生成新的对象实例，而是复用缓存中的现有对象，会直接从内存中取出，不需要新建一个对象。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;2. Object&lt;/h1&gt;
&lt;h2&gt;2.1 == 和 equals&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;==&lt;/code&gt;&lt;/strong&gt; 对于基本类型和引用类型的作用效果是不同的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于基本数据类型来说，&lt;code&gt;==&lt;/code&gt; 比较的是值。&lt;/li&gt;
&lt;li&gt;对于引用数据类型来说，&lt;code&gt;==&lt;/code&gt; 比较的是对象的内存地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;equals()&lt;/code&gt;&lt;/strong&gt; 不能用于判断基本数据类型的变量，只能用来判断两个对象是否相等。&lt;code&gt;equals()&lt;/code&gt;方法存在于&lt;code&gt;Object&lt;/code&gt;类中，而&lt;code&gt;Object&lt;/code&gt;类是所有类的直接或间接父类，因此所有的类都有&lt;code&gt;equals()&lt;/code&gt;方法。
&lt;code&gt;Object&lt;/code&gt; 类 &lt;code&gt;equals()&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public boolean equals(Object obj) {
     return (this == obj);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;equals()&lt;/code&gt; 方法存在两种使用情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类没有重写 equals()方法：通过equals()比较该类的两个对象时，等价于通过&lt;code&gt;==&lt;/code&gt;比较这两个对象，使用的默认是 Object类equals()方法。&lt;/li&gt;
&lt;li&gt;类重写了 equals()方法：一般我们都重写equals()方法来比较两个对象中的属性是否相等(例如String)；若它们的属性相等，则返回 true(即，认为这两个对象相等)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;举个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String a = new String(&quot;ab&quot;); // a 为一个引用 
String b = new String(&quot;ab&quot;); // b为另一个引用,对象的内容一样 
String aa = &quot;ab&quot;; // 放在常量池中 
String bb = &quot;ab&quot;; // 从常量池中查找 
System.out.println(aa == bb);// true 
System.out.println(a == b);// false
System.out.println(a.equals(b));// true 
System.out.println(42 == 42.0);// true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;String 中的 equals 方法是被重写过的，因为 Object 的 equals 方法是比较的对象的内存地址，而 String 的 equals 方法比较的是对象的值。
当创建 String 类型的对象时，虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象，如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 String 对象。&lt;/p&gt;
&lt;h2&gt;2.2 hashCode()&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;hashCode&lt;/code&gt;主要用于获取哈希码(int 整数)，也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250414204513.CoBKaZVV_Z1YwQxC.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;当你把对象加入 &lt;code&gt;HashSet&lt;/code&gt; 时，&lt;code&gt;HashSet&lt;/code&gt; 会先计算对象的 &lt;code&gt;hashCode&lt;/code&gt; 值来判断对象加入的位置，同时也会与其他已经加入的对象的 &lt;code&gt;hashCode&lt;/code&gt; 值作比较，如果没有相符的 &lt;code&gt;hashCode&lt;/code&gt;，&lt;code&gt;HashSet&lt;/code&gt; 会假设对象没有重复出现。但是如果发现有相同 &lt;code&gt;hashCode&lt;/code&gt; 值的对象，这时会调用 &lt;code&gt;equals()&lt;/code&gt; 方法来检查 &lt;code&gt;hashCode&lt;/code&gt; 相等的对象是否真的相同。如果两者相同，&lt;code&gt;HashSet&lt;/code&gt; 就不会让其加入操作成功。如果不同的话，就会重新散列到其他位置。这样我们就大大减少了 &lt;code&gt;equals&lt;/code&gt; 的次数，相应就大大提高了执行速度。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;其实， &lt;code&gt;hashCode()&lt;/code&gt; 和 &lt;code&gt;equals()&lt;/code&gt;都是用于比较两个对象是否相等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那为什么 JDK 还要同时提供这两个方法呢？&lt;/strong&gt;
这是因为在一些容器（比如 &lt;code&gt;HashMap&lt;/code&gt;、&lt;code&gt;HashSet&lt;/code&gt;）中，有了 &lt;code&gt;hashCode()&lt;/code&gt; 之后，判断元素是否在对应容器中的效率会更高（参考添加元素进&lt;code&gt;HashSet&lt;/code&gt;的过程）！&lt;/p&gt;
&lt;p&gt;我们在前面也提到了添加元素进&lt;code&gt;HashSet&lt;/code&gt;的过程，如果 &lt;code&gt;HashSet&lt;/code&gt; 在对比的时候，同样的 &lt;code&gt;hashCode&lt;/code&gt; 有多个对象，它会继续使用 &lt;code&gt;equals()&lt;/code&gt; 来判断是否真的相同。也就是说 &lt;code&gt;hashCode&lt;/code&gt; 帮助我们大大缩小了查找成本。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那为什么不只提供 &lt;code&gt;hashCode()&lt;/code&gt; 方法呢？&lt;/strong&gt;
这是因为两个对象的&lt;code&gt;hashCode&lt;/code&gt; 值相等并不代表两个对象就相等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;那为什么两个对象有相同的 &lt;code&gt;hashCode&lt;/code&gt; 值，它们也不一定是相等的？&lt;/strong&gt;
因为 &lt;code&gt;hashCode()&lt;/code&gt; 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞，但这也与数据值域分布的特性有关（所谓哈希碰撞也就是指的是不同的对象得到相同的 &lt;code&gt;hashCode&lt;/code&gt; )。&lt;/p&gt;
&lt;p&gt;总结下来就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果两个对象的&lt;code&gt;hashCode&lt;/code&gt; 值相等，那这两个对象不一定相等（哈希碰撞）。&lt;/li&gt;
&lt;li&gt;如果两个对象的&lt;code&gt;hashCode&lt;/code&gt; 值相等并且&lt;code&gt;equals()&lt;/code&gt;方法也返回 &lt;code&gt;true&lt;/code&gt;，我们才认为这两个对象相等。&lt;/li&gt;
&lt;li&gt;如果两个对象的&lt;code&gt;hashCode&lt;/code&gt; 值不相等，我们就可以直接认为这两个对象不相等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2.3 为什么重写 equals() 时必须重写 hashCode() 方法？&lt;/h2&gt;
&lt;p&gt;因为两个相等的对象的 &lt;code&gt;hashCode&lt;/code&gt; 值必须是相等。也就是说如果 &lt;code&gt;equals&lt;/code&gt; 方法判断两个对象是相等的，那这两个对象的 &lt;code&gt;hashCode&lt;/code&gt; 值也要相等。&lt;/p&gt;
&lt;p&gt;如果重写 &lt;code&gt;equals()&lt;/code&gt; 时没有重写 &lt;code&gt;hashCode()&lt;/code&gt; 方法的话就可能会导致 &lt;code&gt;equals&lt;/code&gt; 方法判断是相等的两个对象，&lt;code&gt;hashCode&lt;/code&gt; 值却不相等。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;思考&lt;/strong&gt;：重写 &lt;code&gt;equals()&lt;/code&gt; 时没有重写 &lt;code&gt;hashCode()&lt;/code&gt; 方法的话，使用 &lt;code&gt;HashMap&lt;/code&gt; 可能会出现什么问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;equals&lt;/code&gt; 方法判断两个对象是相等的，那这两个对象的 &lt;code&gt;hashCode&lt;/code&gt; 值也要相等。&lt;/li&gt;
&lt;li&gt;两个对象有相同的 &lt;code&gt;hashCode&lt;/code&gt; 值，他们也不一定是相等的（哈希碰撞）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;String&lt;/h1&gt;
&lt;h2&gt;String、StringBuffer、StringBuilder的区别&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;可变性：&lt;/strong&gt;
&lt;code&gt;String&lt;/code&gt; 是不可变的（后面会详细分析原因）。
&lt;code&gt;StringBuilder&lt;/code&gt; 与 &lt;code&gt;StringBuffer&lt;/code&gt; 都继承自 &lt;code&gt;AbstractStringBuilder&lt;/code&gt; 类，在 &lt;code&gt;AbstractStringBuilder&lt;/code&gt; 中也是使用字符数组保存字符串，不过没有使用 &lt;code&gt;final&lt;/code&gt; 和 &lt;code&gt;private&lt;/code&gt; 关键字修饰，最关键的是这个 &lt;code&gt;AbstractStringBuilder&lt;/code&gt; 类还提供了很多修改字符串的方法比如 &lt;code&gt;append&lt;/code&gt; 方法。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;线程安全性：&lt;/strong&gt;
&lt;code&gt;String&lt;/code&gt; 中的对象是不可变的，也就可以理解为常量，线程安全。&lt;code&gt;AbstractStringBuilder&lt;/code&gt; 是 &lt;code&gt;StringBuilder&lt;/code&gt; 与 &lt;code&gt;StringBuffer&lt;/code&gt; 的公共父类，定义了一些字符串的基本操作，如 &lt;code&gt;expandCapacity&lt;/code&gt;、&lt;code&gt;append&lt;/code&gt;、&lt;code&gt;insert&lt;/code&gt;、&lt;code&gt;indexOf&lt;/code&gt; 等公共方法。&lt;code&gt;StringBuffer&lt;/code&gt; 对方法加了同步锁或者对调用的方法加了同步锁，所以是线程安全的。&lt;code&gt;StringBuilder&lt;/code&gt; 并没有对方法进行加同步锁，所以是非线程安全的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;性能&lt;/strong&gt;
每次对 &lt;code&gt;String&lt;/code&gt; 类型进行改变的时候，都会生成一个新的 &lt;code&gt;String&lt;/code&gt; 对象，然后将指针指向新的 &lt;code&gt;String&lt;/code&gt; 对象。&lt;code&gt;StringBuffer&lt;/code&gt; 每次都会对 &lt;code&gt;StringBuffer&lt;/code&gt; 对象本身进行操作，而不是生成新的对象并改变对象引用。相同情况下使用 &lt;code&gt;StringBuilder&lt;/code&gt; 相比使用 &lt;code&gt;StringBuffer&lt;/code&gt; 仅能获得 10%~15% 左右的性能提升，但却要冒多线程不安全的风险。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;对于三者使用的总结：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;操作少量的数据: 适用 &lt;code&gt;String&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;单线程操作字符串缓冲区下操作大量数据: 适用 &lt;code&gt;StringBuilder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;多线程操作字符串缓冲区下操作大量数据: 适用 &lt;code&gt;StringBuffer&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;String 为什么是不可变的&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt; 类中使用 &lt;code&gt;final&lt;/code&gt; 关键字修饰字符数组来保存字符串，~~所以&lt;code&gt;String&lt;/code&gt; 对象是不可变的。~~&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public final class String implements java.io.Serializable, Comparable&amp;#x3C;String&gt;, CharSequence {
    private final char value[];
  //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;纠正观点：&lt;/strong&gt; String 类内部确实使用了 &lt;code&gt;final char[] value&lt;/code&gt; 来保存字符数组，这个 value 数组的引用不能变，但如果只是这样，数组里的内容其实仍然可以被修改（因为 final 修饰引用类型，只限制引用不变，不限制对象内容改变）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;修饰类&lt;/strong&gt;：该类不能被继承。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修饰方法&lt;/strong&gt;：该方法不能被子类重写。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修饰变量（基本数据类型）&lt;/strong&gt;：变量值不能改变。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修饰变量（引用类型）&lt;/strong&gt;：变量不能再指向其他对象，但&lt;strong&gt;引用的对象内容可以改变&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt; 真正不可变有下面几点原因：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保存字符串的数组被 &lt;code&gt;final&lt;/code&gt; 修饰且为私有的，并且&lt;code&gt;String&lt;/code&gt; 类没有提供/暴露修改这个字符串的方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;String&lt;/code&gt; 类被 &lt;code&gt;final&lt;/code&gt; 修饰导致其不能被继承，进而避免了子类破坏 &lt;code&gt;String&lt;/code&gt; 不可变。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;字符串拼接用“+” 还是 StringBuilder?&lt;/h2&gt;
&lt;p&gt;Java 语言本身并不支持运算符重载，“+”和“+=”是专门为 String 类重载过的运算符，也是 Java 中仅有的两个重载过的运算符。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String str1 = &quot;he&quot;;
String str2 = &quot;llo&quot;;
String str3 = &quot;world&quot;;
String str4 = str1 + str2 + str3;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码对应的字节码如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/image-20220422161637929.ZmFDRNIT_12eeOS.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;可以看出，字符串对象通过“+”的字符串拼接方式，实际上是通过 &lt;code&gt;StringBuilder&lt;/code&gt; 调用 &lt;code&gt;append()&lt;/code&gt; 方法实现的，拼接完成之后调用 &lt;code&gt;toString()&lt;/code&gt; 得到一个 &lt;code&gt;String&lt;/code&gt; 对象 。&lt;/p&gt;
&lt;p&gt;不过，在循环内使用“+”进行字符串的拼接的话，存在比较明显的缺陷：&lt;strong&gt;编译器不会创建单个 &lt;code&gt;StringBuilder&lt;/code&gt; 以复用，会导致创建过多的 &lt;code&gt;StringBuilder&lt;/code&gt; 对象&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String[] arr = {&quot;he&quot;, &quot;llo&quot;, &quot;world&quot;};
String s = &quot;&quot;;
for (int i = 0; i &amp;#x3C; arr.length; i++) {
    s += arr[i];
}
System.out.println(s);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250714224103.Dbg_42Ow_Z2hLVPs.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;如果直接使用 &lt;code&gt;StringBuilder&lt;/code&gt; 对象进行字符串拼接的话，就不会存在这个问题了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String[] arr = {&quot;he&quot;, &quot;llo&quot;, &quot;world&quot;};
StringBuilder s = new StringBuilder();
for (String value : arr) {
    s.append(value);
}
System.out.println(s);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/image-20220422162327415.w00RutZr_1ay2JU.webp&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2&gt;String#equals() 和 Object#equals() 有何区别？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;String&lt;/code&gt; 中的 &lt;code&gt;equals&lt;/code&gt; 方法是被重写过的，比较的是 String 字符串的值是否相等。 &lt;code&gt;Object&lt;/code&gt; 的 &lt;code&gt;equals&lt;/code&gt; 方法是比较的对象的内存地址。&lt;/p&gt;
&lt;h2&gt;String s1 = new String(&quot;abc&quot;); 这句话创建了几个字符串对象？&lt;/h2&gt;
&lt;p&gt;会创建 1 或 2 个字符串对象。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;字符串常量池中不存在 &quot;abc&quot;：会创建 2 个 字符串对象。一个在字符串常量池中，由 &lt;code&gt;ldc&lt;/code&gt; 指令触发创建。一个在堆中，由 &lt;code&gt;new String()&lt;/code&gt; 创建，并使用常量池中的 &quot;abc&quot; 进行初始化。&lt;/li&gt;
&lt;li&gt;字符串常量池中已存在 &quot;abc&quot;：会创建 1 个 字符串对象。该对象在堆中，由 &lt;code&gt;new String()&lt;/code&gt; 创建，并使用常量池中的 &quot;abc&quot; 进行初始化。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;String s1 = &quot;abc&quot;; 这句话创建了几个字符串对象？&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;String s = &quot;三妹&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会创建 0 或 1 个字符串对象。
当执行 &lt;code&gt;String s = &quot;三妹&quot;&lt;/code&gt; 时，Java 虚拟机会先在字符串常量池中查找有没有“abc”这个字符串对象，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果有，则不创建任何对象，直接将字符串常量池中这个“三妹”的对象地址返回，赋给变量 s；&lt;/li&gt;
&lt;li&gt;如果没有，在字符串常量池中创建“三妹”这个对象，然后将其地址返回，赋给变量 s。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;String#intern 方法有什么作用?&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;String.intern()&lt;/code&gt; 是一个 &lt;code&gt;native&lt;/code&gt; (本地) 方法，用来处理字符串常量池中的字符串对象引用。它的工作流程可以概括为以下两种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;常量池中已有相同内容的字符串对象&lt;/strong&gt;：如果字符串常量池中已经有一个与调用 &lt;code&gt;intern()&lt;/code&gt; 方法的字符串内容相同的 &lt;code&gt;String&lt;/code&gt; 对象，&lt;code&gt;intern()&lt;/code&gt; 方法会直接返回该对象的引用。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常量池中没有相同内容的字符串对象&lt;/strong&gt;：如果字符串常量池中还没有一个与调用 &lt;code&gt;intern()&lt;/code&gt; 方法的字符串内容相同的对象，&lt;code&gt;intern()&lt;/code&gt; 方法会将当前字符串对象的引用添加到字符串常量池中，并返回该引用。
总结：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;intern()&lt;/code&gt; 方法的主要作用是确保字符串引用在常量池中的唯一性。&lt;/li&gt;
&lt;li&gt;当调用 &lt;code&gt;intern()&lt;/code&gt; 时，如果常量池中已经存在相同内容的字符串，则返回常量池中已有对象的引用；否则，将该字符串添加到常量池并返回其引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;// s1 指向字符串常量池中的 &quot;Java&quot; 对象
String s1 = &quot;Java&quot;;
// s2 也指向字符串常量池中的 &quot;Java&quot; 对象，和 s1 是同一个对象
String s2 = s1.intern();
// 在堆中创建一个新的 &quot;Java&quot; 对象，s3 指向它
String s3 = new String(&quot;Java&quot;);
// s4 指向字符串常量池中的 &quot;Java&quot; 对象，和 s1 是同一个对象
String s4 = s3.intern();
// s1 和 s2 指向的是同一个常量池中的对象
System.out.println(s1 == s2); // true
// s3 指向堆中的对象，s4 指向常量池中的对象，所以不同
System.out.println(s3 == s4); // false
// s1 和 s4 都指向常量池中的同一个对象
System.out.println(s1 == s4); // true
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;String 类型的变量和常量做“+”运算时发生了什么？&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;String str1 = &quot;str&quot;;
String str2 = &quot;ing&quot;;
String str3 = &quot;str&quot; + &quot;ing&quot;;
String str4 = str1 + str2;
String str5 = &quot;string&quot;;
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;对于编译期可以确定值的字符串，也就是常量字符串 ，jvm 会将其存入字符串常量池。并且，字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池，这个得益于编译器的优化。&lt;/strong&gt;
对于 &lt;code&gt;String str3 = &quot;str&quot; + &quot;ing&quot;;&lt;/code&gt; 编译器会给你优化成 &lt;code&gt;String str3 = &quot;string&quot;;&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;并不是所有的常量都会进行折叠，只有编译器在程序编译期就可以确定值的常量才可以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基本数据类型( &lt;code&gt;byte&lt;/code&gt;、&lt;code&gt;boolean&lt;/code&gt;、&lt;code&gt;short&lt;/code&gt;、&lt;code&gt;char&lt;/code&gt;、&lt;code&gt;int&lt;/code&gt;、&lt;code&gt;float&lt;/code&gt;、&lt;code&gt;long&lt;/code&gt;、&lt;code&gt;double&lt;/code&gt;)以及字符串常量。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;final&lt;/code&gt; 修饰的基本数据类型和字符串变量&lt;/li&gt;
&lt;li&gt;字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算（加减乘除）、基本数据类型的位运算（&amp;#x3C;&amp;#x3C;、&gt;&gt;、&gt;&gt;&gt; ）
&lt;strong&gt;引用的值在程序编译期是无法确定的，编译器无法对其进行优化。&lt;/strong&gt;
对象引用和“+”的字符串拼接方式，实际上是通过 &lt;code&gt;StringBuilder&lt;/code&gt; 调用 &lt;code&gt;append()&lt;/code&gt; 方法实现的，拼接完成之后调用 &lt;code&gt;toString()&lt;/code&gt; 得到一个 &lt;code&gt;String&lt;/code&gt; 对象 。
str1 和 str2 是变量，虽然它们的值在这里是常量，但因为编译器无法确定变量值是否改变，它不会进行编译期拼接，而是在运行时通过 &lt;code&gt;StringBuilder&lt;/code&gt; 拼接生成&lt;strong&gt;新的 String 对象&lt;/strong&gt;，放在堆上，不是常量池中的引用。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;String str4 = new StringBuilder().append(str1).append(str2).toString();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在平时写代码的时候，尽量避免多个字符串对象拼接，因为这样会重新创建对象。如果需要改变字符串的话，可以使用 &lt;code&gt;StringBuilder&lt;/code&gt; 或者 &lt;code&gt;StringBuffer&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;字符串使用 &lt;code&gt;final&lt;/code&gt; 关键字声明之后，可以让编译器当做常量来处理。&lt;/p&gt;
&lt;p&gt;示例代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;final String str1 = &quot;str&quot;;
final String str2 = &quot;ing&quot;;
// 下面两个表达式其实是等价的
String c = &quot;str&quot; + &quot;ing&quot;;// 常量池中的对象
String d = str1 + str2; // 常量池中的对象
System.out.println(c == d);// true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;被 &lt;code&gt;final&lt;/code&gt; 关键字修饰之后的 &lt;code&gt;String&lt;/code&gt; 会被编译器当做常量来处理，编译器在程序编译期就可以确定它的值，其效果就相当于访问常量。&lt;/p&gt;
&lt;p&gt;如果 ，编译器在运行时才能知道其确切值的话，就无法对其优化。&lt;/p&gt;
&lt;p&gt;示例代码（&lt;code&gt;str2&lt;/code&gt; 在运行时才能确定其值）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;final String str1 = &quot;str&quot;;
final String str2 = getStr();
String c = &quot;str&quot; + &quot;ing&quot;;// 常量池中的对象
String d = str1 + str2; // 在堆上创建的新的对象
System.out.println(c == d);// false
public static String getStr() {
      return &quot;ing&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3. 面向对象&lt;/h1&gt;
&lt;h2&gt;3.1 重载和重写的区别&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;重载是指在同一个类中，可以有多个同名方法，它们具有不同的参数列表（参数类型，参数个数或参数顺序不同），编译器根据调用时的参数类型来决定调用哪个方法&lt;/li&gt;
&lt;li&gt;重写指的是子类可以重新定义父类中的方法，方法名，参数列表和返回类型必须与父类中德方法一致，通过&lt;code&gt;@override&lt;/code&gt;注解来明确这是对父类方法的重写。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.2 抽象类和接口的区别&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;实现方式不同：实现接口的关键字是&lt;code&gt;implements&lt;/code&gt;，继承抽象类的关键字为&lt;code&gt;extends&lt;/code&gt;。一个类可以实现多个接口，但一个类只能继承一个抽象类。&lt;/li&gt;
&lt;li&gt;方法方式：接口只有定义，不能有方法的实现，java 1.8中可以定义default方法体，而抽象类可以有定义与实现，方法可在抽象类中实现。&lt;/li&gt;
&lt;li&gt;访问修饰符：&lt;strong&gt;接口成员变量默认为&lt;code&gt;public static final&lt;/code&gt;，必须赋初值，不能被修改；其所有的成员方法都是&lt;code&gt;public、abstract&lt;/code&gt;的。&lt;/strong&gt; 抽象类中成员变量默认default，可在子类中被重新定义，也可被重新赋值；抽象方法被abstract修饰，不能被private、static、synchronized和native等修饰，必须以分号结尾，不带花括号。&lt;/li&gt;
&lt;li&gt;变量：抽象类可以包含实例变量和静态变量，而接口只能包含常量（即静态常量）。&lt;/li&gt;
&lt;li&gt;构造器：抽象类本身不能被实例化，但是&lt;strong&gt;抽象类可以有构造器&lt;/strong&gt;，这些构造器在子类实例化时会被调用，以便进行必要的初始化工作。但是&lt;strong&gt;接口是绝不能有构造函数的&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Java中的抽象类是用来被继承的，而final修饰符用于禁止类被继承或方法被重写，因此，抽象类和final修饰符是互斥的，不能同时使用。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;3.3 非静态内部类和静态内部类的区别&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;非静态内部类依赖于外部类的实例，而静态内部类不依赖于外部类的实例。&lt;/li&gt;
&lt;li&gt;非静态内部类可以访问外部类的实例变量和方法，而静态内部类只能访问外部类的静态成员。&lt;/li&gt;
&lt;li&gt;非静态内部类不能定义静态成员，而静态内部类可以定义静态成员。&lt;/li&gt;
&lt;li&gt;非静态内部类在外部类实例化后才能实例化，而静态内部类可以独立实例化。&lt;/li&gt;
&lt;li&gt;非静态内部类可以访问外部类的私有成员，而静态内部类不能直接访问外部类的私有成员，需要通过实例化外部类来访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3.4 非静态内部类可以直接访问外部方法，编译器是怎么做到的？&lt;/h2&gt;
&lt;p&gt;非静态内部类可以直接访问外部方法是因为编译器在生成字节码的时候会&lt;strong&gt;为非静态内部类维护一个指向外部类实例的引用。&lt;/strong&gt; 这个引用使得非静态内部类能够访问外部类的实例变量和方法。编译器会在生成非静态内部类的构造方法时，将外部类实例作为参数传入，并在内部类的实例化过程中建立外部类实例与内部类实例之间的联系，从而实现直接访问外部方法的功能。&lt;/p&gt;
&lt;h1&gt;4. 深拷贝和浅拷贝&lt;/h1&gt;
&lt;h2&gt;4.1 实现深拷贝的三种方法&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1.实现&lt;code&gt;Cloneable&lt;/code&gt;接口并重写&lt;code&gt;clone()&lt;/code&gt;方法&lt;/strong&gt;
这种方法要求对象及其所有引用类型字段都实现&lt;code&gt;Cloneable&lt;/code&gt;接口，并且重写&lt;code&gt;clone()&lt;/code&gt;方法。在&lt;code&gt;clone()&lt;/code&gt;方法中，通过递归克隆引用类型字段来实现深拷贝。
&lt;code&gt;super.clone()&lt;/code&gt;：调用 &lt;code&gt;Object&lt;/code&gt; 类的 &lt;code&gt;clone()&lt;/code&gt; 方法，它会进行 &lt;strong&gt;浅拷贝&lt;/strong&gt;，即复制所有字段的值，包括引用字段的“地址”。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class MyClass implements Cloneable {
    private String field1;
    private NestedClass nestedObject;

	@Override
	protected Object clone() throws CloneNotSupportedException {
	    MyClass cloned = (MyClass) super.clone(); // 浅拷贝
	    cloned.nestedObject = (NestedClass) nestedObject.clone(); // 深拷贝
	    return cloned;
	}
}
class NestedClass implements Cloneable {
    private int nestedField;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2.使用序列化和反序列化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.手动递归复制&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;5. 泛型&lt;/h1&gt;
&lt;p&gt;泛型是 Java 编程语言中的一个重要特性，它允许类、接口和方法在定义时使用一个或多个类型参数，这些类型参数在使用时可以被指定为具体的类型。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Demo {
    private static &amp;#x3C;T extends Number&gt; double add(T a, T b) {
        System.out.println(a + &quot;+&quot; + b + &quot;=&quot; + (a.doubleValue() + b.doubleValue()));
        return a.doubleValue() + b.doubleValue();
    }

    public static void main(String[] args) {
        add(3, 5);         // 输出: 3+5=8.0
        add(2.5, 4.1);     // 输出: 2.5+4.1=6.6
        add(3L, 7L);       // 输出: 3+7=10.0
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;#x3C;T extends Number&gt;&lt;/code&gt;
使用泛型 &lt;code&gt;T&lt;/code&gt;，并限制 &lt;code&gt;T&lt;/code&gt; 必须是 &lt;code&gt;Number&lt;/code&gt; 类或其子类（例如 &lt;code&gt;Integer&lt;/code&gt;、&lt;code&gt;Double&lt;/code&gt;、&lt;code&gt;Float&lt;/code&gt;、&lt;code&gt;Long&lt;/code&gt; 等）。
&lt;code&gt;double add(T a, T b)&lt;/code&gt;
限制返回值类型为Double&lt;/p&gt;
&lt;h1&gt;5. 反射&lt;/h1&gt;
&lt;h2&gt;5.1 反射基础&lt;/h2&gt;
&lt;p&gt;通常情况下，我们写的代码在编译时类型就已经确定了，要调用哪个方法、访问哪个字段都是明确的。但反射允许我们在&lt;strong&gt;运行时&lt;/strong&gt;才去探知一个类有哪些方法、哪些属性、它的构造函数是怎样的，甚至可以动态地创建对象、调用方法或修改属性，哪怕这些方法或属性是私有的。&lt;/p&gt;
&lt;p&gt;反射具有以下特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;运动时类信息访问：&lt;/strong&gt; 反射机制允许程序在运行时获取类的完整结构信息，包括类名，包名，父类，方法和字段等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态对象创建：&lt;/strong&gt; 可以使用反射API动态地创建对象实例，即使在编译时不知道具体的类名。这是通过Class类的&lt;code&gt;newInstance()&lt;/code&gt;方法或Constructor对象的&lt;code&gt;newInstance()&lt;/code&gt;方法实现的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态方法调用：&lt;/strong&gt; 可以在运行时动态地调用对象的方法，包括私有方法。这通过Method类的&lt;code&gt;invoke()&lt;/code&gt;方法实现，允许你传入对象实例和参数值来执行方法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;访问和修改字段名：&lt;/strong&gt; 反射还允许程序在运行时访问和修改对象的字段值，即使是私有的。这是通过Field类的&lt;code&gt;get()&lt;/code&gt;和&lt;code&gt;set()&lt;/code&gt;方法完成的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5.2 Class类&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Class本身也是一个类&lt;/li&gt;
&lt;li&gt;Class对象只能由系统创建对象&lt;/li&gt;
&lt;li&gt;一个加载的类在JVM中只有一个Class实例&lt;/li&gt;
&lt;li&gt;一个Class对象对应的是一个加载到JVM中的一个.class文件&lt;/li&gt;
&lt;li&gt;每个类的实例都会记得自己是由哪个Class实例所生成&lt;/li&gt;
&lt;li&gt;通过Class可以完整地得到 一个类中的所有被加载的结构&lt;/li&gt;
&lt;li&gt;Class类是Reflection的根源，针对任何你想动态加载，运行的类，唯有先获得对应的Class对象&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2.1 Class类对象的获取&lt;/h3&gt;
&lt;p&gt;如果我们动态获取到这些信息，我们需要依靠 Class 对象。Class 类对象将一个类的方法、变量等信息告诉运行的程序。Java 提供了四种方式获取 Class 对象:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. 知道具体类的情况下可以使用：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class alunbarClass = TargetObject.class;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是我们一般是不知道具体类的，基本都是通过遍历包下面的类来获取 Class 对象，通过此方式获取 Class 对象不会进行初始化&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 通过 &lt;code&gt;Class.forName()&lt;/code&gt;传入类的全路径获取：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class alunbarClass1 = Class.forName(&quot;cn.javaguide.TargetObject&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. 通过对象实例&lt;code&gt;instance.getClass()&lt;/code&gt;获取：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;TargetObject o = new TargetObject();
Class alunbarClass2 = o.getClass();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. 通过类加载器&lt;code&gt;xxxClassLoader.loadClass()&lt;/code&gt;传入类路径获取:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;ClassLoader.getSystemClassLoader().loadClass(&quot;cn.javaguide.TargetObject&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过类加载器获取 Class 对象不会进行初始化，意味着不进行包括初始化等一系列步骤，静态代码块和静态对象不会得到执行&lt;/p&gt;
&lt;h2&gt;5.3 反射应用场景&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1.依赖注入与控制反转（IoC）&lt;/strong&gt;
以 Spring/Spring Boot 为代表的 IoC 框架，会在启动时扫描带有特定注解（如 &lt;code&gt;@Component&lt;/code&gt;, &lt;code&gt;@Service&lt;/code&gt;, &lt;code&gt;@Repository&lt;/code&gt;, &lt;code&gt;@Controller&lt;/code&gt;）的类，利用反射实例化对象（Bean），并通过反射注入依赖（如 &lt;code&gt;@Autowired&lt;/code&gt;、构造器注入等）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2.注解处理&lt;/strong&gt;
注解本身只是个“标记”，得有人去读这个标记才知道要做什么。反射就是那个“读取器”。框架通过反射检查类、方法、字段上有没有特定的注解，然后根据注解信息执行相应的逻辑。比如，看到 &lt;code&gt;@Value&lt;/code&gt;，就用反射读取注解内容，去配置文件找对应的值，再用反射把值设置给字段。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3.动态代理与 AOP&lt;/strong&gt;
想在调用某个方法前后自动加点料（比如打日志、开事务、做权限检查）？AOP（面向切面编程）就是干这个的，而动态代理是实现 AOP 的常用手段。JDK 自带的动态代理（Proxy 和 InvocationHandler）就离不开反射。代理对象在内部调用真实对象的方法时，就是通过反射的 &lt;code&gt;Method.invoke&lt;/code&gt; 来完成的。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class DebugInvocationHandler implements InvocationHandler {
    private final Object target; // 真实对象

    public DebugInvocationHandler(Object target) { this.target = target; }

    // proxy: 代理对象, method: 被调用的方法, args: 方法参数
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(&quot;切面逻辑：调用方法 &quot; + method.getName() + &quot; 之前&quot;);
        // 通过反射调用真实对象的同名方法
        Object result = method.invoke(target, args);
        System.out.println(&quot;切面逻辑：调用方法 &quot; + method.getName() + &quot; 之后&quot;);
        return result;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4.对象关系映射（ORM）&lt;/strong&gt;
像 MyBatis、Hibernate 这种框架，能帮你把数据库查出来的一行行数据，自动变成一个个 Java 对象。它是怎么知道数据库字段对应哪个 Java 属性的？还是靠反射。它通过反射获取 Java 类的属性列表，然后把查询结果按名字或配置对应起来，再用反射调用 setter 或直接修改字段值。反过来，保存对象到数据库时，也是用反射读取属性值来拼 SQL。&lt;/p&gt;
&lt;h3&gt;反射举例——输出任意对象数据&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public  void printObject(Object obj) throws IllegalAccessException {  
    Class clazz = obj.getClass();  
    Field[] fields = clazz.getDeclaredFields();  
    for (Field field : fields) {  
        field.setAccessible(true);  
        String name = field.getName();  
        Object value = field.get(obj);  
        System.out.println(name + &quot; = &quot; + value);  
    }  
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;6. 注解&lt;/h1&gt;
&lt;p&gt;什么是注解？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;注解（Annotation）在 JVM 层面其实是一种特殊的“标记接口”实现&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;定义在&lt;strong&gt;class文件的元数据区域&lt;/strong&gt;（类似class、method的属性表），由编译器生成字节码&lt;/li&gt;
&lt;li&gt;若注解使用 &lt;code&gt;@Retention(RetentionPolicy.RUNTIME)&lt;/code&gt;，则它会被编译进 class 文件，并且在运行时可通过反射读取&lt;/li&gt;
&lt;li&gt;在代理逻辑里，利用 &lt;strong&gt;反射读取注解&lt;/strong&gt;，决定如何增强目标方法&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;6.1 注解举例&lt;/h2&gt;
&lt;p&gt;注解本质是一个继承了Annotation的特殊接口，其具体实现类是Java运行时生成的动态代理类。
我们通过反射获取注解时，返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法，会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。&lt;/p&gt;
&lt;h2&gt;6.2 注解的底层实现&lt;/h2&gt;
&lt;p&gt;注解本质上是一种特殊的接口，它继承于&lt;code&gt;java.lang.annotation.Annotation&lt;/code&gt;接口，所以注解也叫做声明式接口，定义一个简单的接口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.FIELD) 
public @interface JsonField {
	public String value() default &quot;&quot;; 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译后，Java 编译器会将其转换为一个继承自 Annotation 的接口，并生成相应的字节码文件。&lt;/p&gt;
&lt;p&gt;根据注解的作用范围，Java 注解可以分为以下几种类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;源码级别注解 ：仅存在于源码中，编译后不会保留&lt;code&gt;@Retention(RetentionPolicy.SOURCE)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;类文件级别注解 ：保留在 .class 文件中，但运行时不可见&lt;code&gt;@Retention(RetentionPolicy.CLASS)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;运行时注解 ：保留在 .class 文件中，并且可以通过反射在运行时访问&lt;code&gt;@Retention(RetentionPolicy.RUNTIME)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;只有运行时注解可以通过反射机制进行解析。&lt;/strong&gt;
当注解被标记为 RUNTIME 时，Java 编译器会在生成的 .class 文件中保存注解信息。这些信息存储在字节码的属性表（Attribute Table）中，具体包括以下内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RuntimeVisibleAnnotations ：存储运行时可见的注解信息。&lt;/li&gt;
&lt;li&gt;RuntimeInvisibleAnnotations ：存储运行时不可见的注解信息。&lt;/li&gt;
&lt;li&gt;RuntimeVisibleParameterAnnotations 和 RuntimeInvisibleParameterAnnotations ：存储方法参数上的注解信息。
通过工具（如&lt;code&gt;javap -v&lt;/code&gt;）可以查看&lt;code&gt;.class&lt;/code&gt;文件中的注解信息。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;注解的解析主要依赖于Java的反射机制&lt;/strong&gt;。以下是解析注解的基本流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取注册信息：通过反射API可以获取类，方法，字段等元素上的注解&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;Class&amp;#x3C;?&gt; clazz = MyClass.class;
MyAnnotation annotation = clazz.getAnnotation(MyAnnotation.class); 
if (annotation != null) { 
	System.out.println(annotation.value()); 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;底层原理：反射机制的核心类是&lt;code&gt;java.lang.reflect.AnnotatedElement&lt;/code&gt;，它是所有可以被注解修饰的元素（如&lt;code&gt;Class&lt;/code&gt;,&lt;code&gt;Method&lt;/code&gt;,&lt;code&gt;Field&lt;/code&gt;等）的父接口，该接口提供了以下方法：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;getAnnotation(Class&amp;#x3C;T&gt; annotationClass)&lt;/code&gt;：获取指定类型的注解。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getAnnotations()&lt;/code&gt;：获取所有注解。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;isAnnotationPresent(Class&amp;#x3C;? extends Annotation&gt; annotationClass)&lt;/code&gt;：判断是否包含指定注解。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JVM在加载类时会解析&lt;code&gt;.class&lt;/code&gt;文件中的注解信息，并将其存储在内存中，供反射机制使用。
因此，注解解析的底层实现主要依赖于Java的反射机制和字节码文件的存储。通过&lt;code&gt;@Retention&lt;/code&gt;元注解可以控制注解的保留策略，&lt;strong&gt;当使用 RetentionPolicy.RUNTIME 时，可以在运行时通过反射 API 来解析注解信息。在 JVM 层面，会从字节码文件中读取注解信息，并创建注解的代理对象来获取注解的属性值。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;7. 异常&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250508104532.5lLW88Re_Z1m1vE.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;finally块中的return语句会覆盖try块中的return返回，因此该语句会返回’b&apos;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;try{
	return &apos;a&apos;;
}finally{
	return &apos;b&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;8. Java8的一些特性&lt;/h1&gt;
&lt;h2&gt;8.1 Lambda表达式&lt;/h2&gt;
&lt;p&gt;Lambda表达式用于创建匿名函数，主要用于简化函数式接口（只有一个抽象方法的接口）的使用，基本语法有两种形式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;(parameters) -&gt; expression&lt;/code&gt;：当Lambda体只有一个表达式时使用，表达式的结果会作为返回值。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;(parameters) -&gt; { statements; }&lt;/code&gt;：当 Lambda 体包含多条语句时，需要使用大括号将语句括起来，若有返回值则需要使用 return 语句。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;9. I/O&lt;/h1&gt;
&lt;h2&gt;9.1 Java NIO&lt;/h2&gt;
&lt;p&gt;Java NIO，是一种同步非阻塞的I/O模型，也是I/O多路复用的基础&lt;/p&gt;
&lt;p&gt;传统的BIO里&lt;code&gt;socket.read()&lt;/code&gt;，如果TCP RecvBuffer里没有数据，函数会一直阻塞，直到收到数据，返回读到的数据，如果使用BIO想要并发处理多个客户端的I/O，那么会使用多线程模式，一个线程专门处理一个客户端io，这种模式随着客户端越来越多，所需要创建的线程也越来越多，会急剧消耗系统的性能。
&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250512224201.DTBYaUgD_2mGTeg.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;NIO 是基于I/O多路复用实现的，它可以只用一&lt;/p&gt;
&lt;p&gt;个线程处理多个客户端I/O，如果你需要同时管理成千上万的连接，但是每个连接只发送少量数据，例如一个聊天服务器，用NIO实现会更好一些。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250512224205.DppDQRBA_1MfotO.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h2&gt;9.2 NIO是怎么实现的&lt;/h2&gt;
&lt;p&gt;NIO是一种同步非阻塞的IO模型，所以也可以叫NON-BLOCKINGIO。同步是指线程不断轮询IO事件是否就绪，非阻塞是指线程在等待IO的时候，可以同时做其他任务。&lt;/p&gt;
&lt;p&gt;同步的核心就Selector（I/O多路复用），Selector代替了线程本身轮询IO事件，避免了阻塞同时减少了不必要的线程消耗；非阻塞的核心就是通道和缓冲区，当IO事件就绪时，可以通过写到缓冲区，保证IO的成功，而无需线程阻塞式地等待。&lt;/p&gt;
&lt;p&gt;NIO由一个专门的线程处理所有IO事件，并负责分发。事件驱动机制，事件到来的时候触发操作，不需要阻塞的监视事件。线程之间通过wait,notify通信，减少线程切换。&lt;/p&gt;
&lt;p&gt;NIO主要有三大核心部分：Channel(通道)，Buffer(缓冲区), Selector。传统IO基于字节流和字符流进行操作，而NIO基于Channel和Buffer(缓冲区)进行操作，数据总是从通道读取到缓冲区中，或者从缓冲区写入到通道中。&lt;/p&gt;
&lt;p&gt;Selector(选择区)用于监听多个通道的事件（比如：连接打开，数据到达）。因此，单个线程可以监听多个数据通道。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://wojiecihuo.cn/_astro/20250512224330.CwMd6JJF_1ty7nx.webp&quot; alt=&quot;image.png&quot;&gt;&lt;/p&gt;
&lt;h1&gt;Java值传递&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;值传递&lt;/strong&gt;：方法接收的是实参值的拷贝，会创建副本。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;引用传递&lt;/strong&gt;：方法接收的直接是实参所引用的对象在堆中的地址，不会创建副本，对形参的修改将影响到实参。
很多程序设计语言（比如 C++、 Pascal）提供了两种参数传递的方式，不过，在 Java 中只有值传递。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么Java只有值传递&lt;/h2&gt;
&lt;h3&gt;传递基本类型参数&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public static void main(String[] args) {
    int num1 = 10;
    int num2 = 20;
    swap(num1, num2);
    System.out.println(&quot;num1 = &quot; + num1);
    System.out.println(&quot;num2 = &quot; + num2);
}

public static void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    System.out.println(&quot;a = &quot; + a);
    System.out.println(&quot;b = &quot; + b);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;a = 20
b = 10
num1 = 10
num2 = 20
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;传递引用类型参数&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;  public static void main(String[] args) {
      int[] arr = { 1, 2, 3, 4, 5 };
      System.out.println(arr[0]);
      change(arr);
      System.out.println(arr[0]);
  }

  public static void change(int[] array) {
      // 将数组的第一个元素变为0
      array[0] = 0;
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1
0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了这个案例很多人肯定觉得 Java 对引用类型的参数采用的是引用传递。&lt;/p&gt;
&lt;p&gt;实际上，并不是的，这里传递的还是值，不过，这个值是实参的地址罢了！&lt;/p&gt;
&lt;p&gt;也就是说 &lt;code&gt;change&lt;/code&gt; 方法的参数拷贝的是 &lt;code&gt;arr&lt;/code&gt; （实参）的地址，因此，它和 &lt;code&gt;arr&lt;/code&gt; 指向的是同一个数组对象。这也就说明了为什么方法内部对形参的修改会影响到实参。&lt;/p&gt;
&lt;p&gt;为了更强有力地反驳 Java 对引用类型的参数采用的不是引用传递，我们再来看下面这个案例！&lt;/p&gt;
&lt;h3&gt;传递引用类型参数2&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;public class Person {
    private String name;
   // 省略构造函数、Getter&amp;#x26;Setter方法
}

public static void main(String[] args) {
    Person xiaoZhang = new Person(&quot;小张&quot;);
    Person xiaoLi = new Person(&quot;小李&quot;);
    swap(xiaoZhang, xiaoLi);
    System.out.println(&quot;xiaoZhang:&quot; + xiaoZhang.getName());
    System.out.println(&quot;xiaoLi:&quot; + xiaoLi.getName());
}

public static void swap(Person person1, Person person2) {
    Person temp = person1;
    person1 = person2;
    person2 = temp;
    System.out.println(&quot;person1:&quot; + person1.getName());
    System.out.println(&quot;person2:&quot; + person2.getName());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;person1:小李
person2:小张
xiaoZhang:小张
xiaoLi:小李
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解析：
怎么回事？？？两个引用类型的形参互换并没有影响实参啊！
&lt;code&gt;swap&lt;/code&gt; 方法的参数 &lt;code&gt;person1&lt;/code&gt; 和 &lt;code&gt;person2&lt;/code&gt; 只是拷贝的实参 &lt;code&gt;xiaoZhang&lt;/code&gt; 和 &lt;code&gt;xiaoLi&lt;/code&gt; 的地址。因此， &lt;code&gt;person1&lt;/code&gt; 和 &lt;code&gt;person2&lt;/code&gt; 的互换只是拷贝的两个地址的互换罢了，并不会影响到实参 &lt;code&gt;xiaoZhang&lt;/code&gt; 和 &lt;code&gt;xiaoLi&lt;/code&gt; 。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;Java 中将实参传递给方法（或函数）的方式是 &lt;strong&gt;值传递&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果参数是基本类型的话，很简单，传递的就是基本类型的字面量值的拷贝，会创建副本。&lt;/li&gt;
&lt;li&gt;如果参数是引用类型，传递的就是实参所引用的对象在堆中地址值的拷贝，同样也会创建副本。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>Java</category><category>八股</category></item></channel></rss>