大家好!今天我要和各位分享一个在 MySQL 项目中经常让开发者头疼的问题——InnoDB 的死锁问题。相信不少朋友都遇到过这样的情况:一个好好运行的系统突然报错,日志里冒出"Deadlock found when trying to get lock; try restarting transaction",然后你就开始了漫长的排查之旅...
别担心,这篇文章会用真实案例带你从现象到根源,彻底掌握死锁的排查技巧和解决方法。无论你是数据库管理员还是后端开发,这些内容都能帮你在实际工作中少走弯路。
InnoDB 锁机制基础知识
在深入案例前,我们先用通俗的话聊聊 InnoDB 的锁机制。
InnoDB 有几种主要的锁类型:
- 共享锁(S 锁):大家一起看,不能改(SELECT ... LOCK IN SHARE MODE)
- 排他锁(X 锁):我改数据时谁都别动(SELECT ... FOR UPDATE, UPDATE, DELETE)
- 意向锁(IS/IX 锁):打个招呼说"我要在这张表的某些行上加锁了"
- 记录锁:锁住单条记录
- 间隙锁:锁住一个范围,但不包含记录本身
- 临键锁(Next-Key Lock):记录锁+间隙锁的组合,防止幻读的重要机制
- 插入意向锁:一种特殊的间隙锁,表示插入操作的意向,多个事务可以在同一间隙中设置插入意向锁而不冲突,但会与间隙锁冲突
需要特别强调的是:间隙锁和临键锁只在 REPEATABLE READ 隔离级别下默认生效。在 READ COMMITTED 隔离级别下,InnoDB 不使用间隙锁,临键锁会退化为记录锁,这是减少死锁的重要知识点。
想象一下,锁就像是在图书馆看书。共享锁就像大家一起看同一本书但不能写批注,排他锁就像你借走了这本书,别人就看不了了。
什么是死锁?
死锁就像两个人互相给对方让路,结果谁都动不了的尴尬局面。在数据库中,当两个或多个事务互相等待对方释放锁,导致所有事务都无法继续执行时,就形成了死锁。
举个简单例子:小明要拿筷子和碗才能吃饭,小红也一样。现在小明拿了筷子,小红拿了碗,两人都在等对方放下手中的东西,结果谁都吃不了饭。
案例一:经典的行锁更新死锁
场景描述
假设我们有一个订单系统,有一张orders
表,结构如下:
CREATE TABLE `orders` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`product_id` int NOT NULL,
`status` int NOT NULL,
`amount` decimal(10,2) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_product_id` (`product_id`)
) ENGINE=InnoDB;
系统中存在两个并发执行的事务,导致了死锁:
- 事务 A:更新用户 1 的所有订单状态
- 事务 B:更新特定产品相关的所有订单金额
问题重现
以下是导致死锁的操作序列:
这里需要注意一个重要点:InnoDB 的行锁是通过索引实现的。在这个案例中,事务 A 使用user_id
索引,事务 B 使用product_id
索引,导致锁定的记录顺序不同,增加了死锁风险。如果查询条件未命中索引,情况会更糟,可能导致表锁或大范围的临键锁。
死锁日志分析
当死锁发生时,MySQL 会在错误日志中记录详细信息。使用SHOW ENGINE INNODB STATUS
可以查看最近一次死锁的详情:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-04-17 14:32:51 0x7f9a1c3a2700
*** (1) TRANSACTION:
TRANSACTION 10795, ACTIVE 2 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 155, OS thread handle 140301189614336, query id 9697 localhost root updating
UPDATE orders SET status=2 WHERE user_id=1 AND id>2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 24 page no 4 n bits 72 index PRIMARY of table `test`.`orders` trx id 10795 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 6; hex 000000002a25; asc *%;;
2: len 7; hex 81000000110137; asc 7;;
3: len 4; hex 80000002; asc ;;
4: len 4; hex 80000065; asc e;;
5: len 4; hex 80000001; asc ;;
*** (2) TRANSACTION:
TRANSACTION 10794, ACTIVE 5 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 156, OS thread handle 140301235861248, query id 9696 localhost root updating
UPDATE orders SET amount=amount*1.1 WHERE product_id=101 LIMIT 1 OFFSET 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 24 page no 4 n bits 72 index PRIMARY of table `test`.`orders` trx id 10794 lock_mode X locks rec but not gap
Record lock, heap no 4 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 6; hex 000000002a25; asc *%;;
2: len 7; hex 81000000110137; asc 7;;
3: len 4; hex 80000002; asc ;;
4: len 4; hex 80000065; asc e;;
5: len 4; hex 80000001; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 24 page no 4 n bits 72 index PRIMARY of table `test`.`orders` trx id 10794 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000000002a23; asc *#;;
2: len 7; hex 81000000110135; asc 5;;
3: len 4; hex 80000001; asc ;;
4: len 4; hex 80000065; asc e;;
5: len 4; hex 80000001; asc ;;
*** WE ROLL BACK TRANSACTION (1)
从日志中我们可以看到:
- 事务 A(
TRANSACTION 10795
)想获取 id=3 的行锁,但被事务 B 持有 - 同时事务 B(
TRANSACTION 10794
)想获取 id=1 的行锁,但被事务 A 持有 - 形成循环等待,MySQL 检测到死锁并回滚了事务 A
日志中一些专业术语解释:
lock_mode X
:代表排他锁(X 锁)locks rec but not gap
:表示只锁记录,不锁间隙(记录锁)heap no
:表示记录在索引页中的位置
解决方案
对于这种情况,以下是按实用性排序的解决方法:
- 统一访问顺序(最有效):确保所有事务按相同的顺序访问记录,例如总是按主键顺序
-- 事务A
BEGIN;
UPDATE orders SET status=2 WHERE user_id=1 ORDER BY id;
COMMIT;
-- 事务B
BEGIN;
UPDATE orders SET amount=amount*1.1 WHERE product_id=101 ORDER BY id;
COMMIT;
- 减小事务范围:不要在一个大事务中做太多事情,拆分为多个小事务
-- 原来的大事务
BEGIN;
UPDATE orders SET status=2 WHERE user_id=1;
-- 其他操作...
COMMIT;
-- 拆分后
BEGIN;
UPDATE orders SET status=2 WHERE user_id=1 AND id BETWEEN 1 AND 100;
COMMIT;
BEGIN;
UPDATE orders SET status=2 WHERE user_id=1 AND id BETWEEN 101 AND 200;
COMMIT;
- 添加适当的锁超时设置:
SET innodb_lock_wait_timeout = 50; -- 设置锁等待超时
- 使用乐观锁替代悲观锁(适合读多写少场景):
-- 使用版本号控制
UPDATE orders SET amount=amount*1.1, version=version+1
WHERE product_id=101 AND version=当前版本;
需要注意的是,乐观锁在并发冲突频繁的场景下可能导致大量重试,反而降低性能。此时应考虑悲观锁加合理的锁超时设置。
案例二:Gap 锁导致的死锁
场景描述
在 REPEATABLE READ 隔离级别下,InnoDB 会使用间隙锁(Gap Lock)来防止幻读。这种锁可能导致一些不直观的死锁。
假设有一个用户积分表:
CREATE TABLE `user_points` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`points` int NOT NULL,
`created_at` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB;
表中已有数据:
id | user_id | points | created_at
1 | 1 | 100 | 2023-04-15 10:00:00
3 | 3 | 150 | 2023-04-15 11:00:00
5 | 5 | 200 | 2023-04-15 12:00:00
注意这里 user_id 为 2 和 4 的记录不存在。
问题重现
这里的关键是理解:在 RR 隔离级别下,即使查询的记录不存在,FOR UPDATE
也会在该位置获取间隙锁或临键锁。当执行 INSERT 操作时,事务会先请求一个插入意向锁(Insert Intention Lock),这是一种特殊的间隙锁。虽然多个事务可以在同一个间隙内持有不同的插入意向锁(允许并发插入不同位置),但插入意向锁会与普通间隙锁冲突,导致等待。
当两个事务交叉持有间隙锁并尝试插入时,就会形成死锁。
临键锁案例补充
为更好理解临键锁,考虑以下场景:
-- 在REPEATABLE READ隔离级别下
BEGIN;
SELECT * FROM orders WHERE id BETWEEN 5 AND 15 FOR UPDATE;
-- 其他操作...
COMMIT;
这个 SELECT 语句会做什么?它会:
- 对 id 值为 5 到 15 的记录加记录锁(Record Lock)
- 对 id 值范围(15, "下一个索引值")加间隙锁(Gap Lock)
- 这两种锁合起来形成临键锁(Next-Key Lock)
这种锁定策略可以防止其他事务在锁定范围内插入新记录(防幻读),同时允许对锁定范围之外的记录进行修改。
死锁日志分析
------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-04-17 15:12:45 0x7f9a1c3a2700
*** (1) TRANSACTION:
TRANSACTION 10801, ACTIVE 6 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 157, OS thread handle 140301189614336, query id 9712 localhost root update
INSERT INTO user_points(user_id,points,created_at) VALUES(4,120,NOW())
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 25 page no 4 n bits 72 index idx_user_id of table `test`.`user_points` trx id 10801 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000005; asc ;;
1: len 4; hex 80000005; asc ;;
*** (2) TRANSACTION:
TRANSACTION 10802, ACTIVE 3 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 158, OS thread handle 140301235861248, query id 9713 localhost root update
INSERT INTO user_points(user_id,points,created_at) VALUES(2,110,NOW())
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 25 page no 4 n bits 72 index idx_user_id of table `test`.`user_points` trx id 10802 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 4; hex 80000003; asc ;;
*** WE ROLL BACK TRANSACTION (2)
日志中的关键信息:
lock_mode X locks gap before rec insert intention waiting
:表示事务尝试获取插入意向锁,但被另一个事务的间隙锁阻止- 两个事务分别持有对方需要的间隙锁,形成死锁
解决方案
按照有效性排序:
- 降低隔离级别(最直接有效):将隔离级别从 REPEATABLE READ 降低到 READ COMMITTED,这样就不会使用 Gap 锁
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
注意:隔离级别调整需谨慎评估业务一致性需求,避免幻读问题对应用造成影响。
- 使用 INSERT ON DUPLICATE KEY UPDATE:替代先 SELECT 再 INSERT 的模式
INSERT INTO user_points(user_id,points,created_at)
VALUES(2,110,NOW())
ON DUPLICATE KEY UPDATE points=110, created_at=NOW();
- 拆分事务:不要在同一个事务中先查询再插入
-- 原来的模式
BEGIN;
SELECT * FROM user_points WHERE user_id=2 FOR UPDATE;
-- 如果不存在则插入
INSERT INTO user_points(user_id,points,created_at) VALUES(2,110,NOW());
COMMIT;
-- 改进后
-- 查询阶段(可以使用共享锁或不加锁)
SELECT * FROM user_points WHERE user_id=2;
-- 插入阶段(单独事务)
BEGIN;
INSERT INTO user_points(user_id,points,created_at) VALUES(2,110,NOW())
ON DUPLICATE KEY UPDATE points=110, created_at=NOW();
COMMIT;
- 索引优化:确保查询条件有合适的索引
案例三:外键约束导致的死锁
场景描述
外键约束也是死锁的常见原因。考虑以下两个表:
CREATE TABLE `departments` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
CREATE TABLE `employees` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`dept_id` int NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_dept_id` (`dept_id`),
CONSTRAINT `fk_dept_id` FOREIGN KEY (`dept_id`) REFERENCES `departments` (`id`)
) ENGINE=InnoDB;
问题重现
这里的关键点:外键约束会导致额外的锁请求。当我们修改引用字段时,InnoDB 需要验证引用的完整性:
- 如果更新子表的外键值(如 dept_id),InnoDB 会检查父表(departments)中对应的值是否存在,需要在父表相应记录上添加共享锁(S 锁),而非排他锁
- 如果更新或删除父表中被引用的记录,InnoDB 会检查子表是否有依赖,可能添加父子表间的额外锁
这些额外的锁请求大大增加了死锁的可能性。
死锁日志分析
------------------------
LATEST DETECTED DEADLOCK
------------------------
2023-04-17 16:03:11 0x7f9a1c3a2700
*** (1) TRANSACTION:
TRANSACTION 10809, ACTIVE 8 sec updating or deleting
mysql tables in use 2, locked 2
LOCK WAIT 5 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 1
MySQL thread id 159, OS thread handle 140301189614336, query id 9725 localhost root updating
UPDATE employees SET dept_id=2 WHERE id=1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 27 page no 4 n bits 72 index PRIMARY of table `test`.`employees` trx id 10809 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000000002a3d; asc *=;;
2: len 7; hex 81000000110110; asc ;;
3: len 3; hex 426f62; asc Bob;;
4: len 4; hex 80000001; asc ;;
*** (2) TRANSACTION:
TRANSACTION 10810, ACTIVE 4 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 160, OS thread handle 140301235861248, query id 9726 localhost root update
INSERT INTO departments(id,name) VALUES(3,'HR')
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 26 page no 4 n bits 72 index PRIMARY of table `test`.`departments` trx id 10810 lock_mode X locks rec but not gap waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (2)
解决方案
按照有效性排序:
- 调整事务顺序(最有效):按照固定顺序访问相关表
-- 统一先操作父表,再操作子表
BEGIN;
UPDATE departments SET name='IT' WHERE id=1;
UPDATE employees SET dept_id=2 WHERE id=1;
COMMIT;
- 设置外键约束时使用
RESTRICT
而非CASCADE
:
ALTER TABLE employees DROP FOREIGN KEY fk_dept_id;
ALTER TABLE employees ADD CONSTRAINT fk_dept_id FOREIGN KEY (dept_id)
REFERENCES departments(id) ON DELETE RESTRICT ON UPDATE RESTRICT;
- 分割事务:避免在一个事务中同时操作多个相关表
-- 分两个事务操作
-- 事务1:更新父表
BEGIN;
UPDATE departments SET name='IT' WHERE id=1;
COMMIT;
-- 事务2:更新子表
BEGIN;
UPDATE employees SET dept_id=2 WHERE id=1;
COMMIT;
- 减少外键使用(谨慎考虑):在高并发系统中考虑减少外键约束,由应用程序保证数据一致性
案例四:自增锁死锁
场景描述
自增锁是一种特殊的锁,用于处理 AUTO_INCREMENT 列的值生成。在高并发场景下,自增锁争用也可能导致死锁。
MySQL 通过innodb_autoinc_lock_mode
参数控制自增锁行为:
innodb_autoinc_lock_mode=0
:传统模式,所有插入语句都需要获取表级锁innodb_autoinc_lock_mode=1
(默认值):混合模式,其特点是:- 对于行数已知的插入(如
INSERT ... VALUES
),使用轻量级互斥锁 - 仅对于行数未知的插入(如
INSERT ... SELECT
),才使用表级锁
- 对于行数已知的插入(如
innodb_autoinc_lock_mode=2
:交叉模式,所有插入使用轻量级互斥锁,不保证自增值连续性
问题重现
考虑以下场景:
CREATE TABLE `order_items` (
`id` int NOT NULL AUTO_INCREMENT,
`order_id` int NOT NULL,
`product_id` int NOT NULL,
`quantity` int NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`)
) ENGINE=InnoDB;
这里的关键点是理解:在innodb_autoinc_lock_mode=1
(默认)模式下,INSERT ... SELECT
等行数不确定的语句会获取表级自增锁,而INSERT ... VALUES
等行数确定的语句仅获取轻量级互斥锁。当两种类型的插入混合使用时,容易导致锁冲突和死锁。
解决方案
- 调整自增锁模式(最有效):
-- 全局设置,需要重启MySQL生效
SET GLOBAL innodb_autoinc_lock_mode = 2;
-- 或在配置文件中设置
[mysqld]
innodb_autoinc_lock_mode = 2
注意:mode=2
不保证自增值连续性,但能显著减少锁冲突。
- 批量插入优化:尽量在一个语句中插入多行数据,明确指定字段顺序
-- 替代多次单行插入
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (101, 1, 2), (101, 2, 1), (101, 3, 5);
- 使用应用生成的 ID:对于高并发系统,考虑使用应用层生成唯一 ID
-- 使用应用生成的UUID或其他唯一ID
INSERT INTO order_items (id, order_id, product_id, quantity)
VALUES (GENERATED_ID, 101, 1, 2);
死锁排查工具与技巧
1. 使用 SHOW ENGINE INNODB STATUS 查看死锁信息
SHOW ENGINE INNODB STATUS\G
关注输出中的"LATEST DETECTED DEADLOCK"部分。
2. 开启死锁日志记录
SET GLOBAL innodb_print_all_deadlocks = 1;
这会将所有死锁信息记录到 MySQL 错误日志中。
3. 使用 performance_schema 监控锁等待
-- 启用performance_schema
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE 'wait/lock/metadata/%' OR NAME LIKE 'wait/lock/innodb/%';
-- 查询当前锁等待
SELECT * FROM performance_schema.events_waits_current
WHERE EVENT_NAME LIKE 'wait/lock%';
-- 查询锁等待历史
SELECT * FROM performance_schema.events_waits_history
WHERE EVENT_NAME LIKE 'wait/lock%';
4. 使用工具分析死锁
- pt-deadlock-logger(Percona 工具集)
- MySQL 企业版监控工具
5. 死锁监控与预警
其他常见死锁类型简述
元数据锁死锁
当 DDL 操作(如 ALTER TABLE)与 DML 操作(如 INSERT、UPDATE)并发执行时,可能出现元数据锁死锁。
解决方案:
- 将 DDL 操作安排在低峰期
- 使用在线 DDL 工具(如 pt-online-schema-change):这些工具通过创建临时表、复制数据和表结构交换来避免长时间锁表
- MySQL 5.6+的
ALGORITHM=INPLACE
或 8.0+的ALGORITHM=INSTANT
参数可实现某些 DDL 操作的在线执行 - 避免长事务与 DDL 并发
预防死锁的实用建议
控制事务大小和持续时间
- 保持事务短小、快速完成
- 只在必要时使用事务
合理设计数据访问顺序
- 按照主键或索引顺序访问数据
- 使用 ORDER BY 确保访问顺序一致
选择合适的隔离级别
- 不需要可重复读的场景使用 READ COMMITTED
- 了解每个隔离级别的锁行为
优化索引设计
- 确保查询条件有合适的索引
- 避免使用不必要的锁定查询
谨慎使用外键
- 高并发系统可考虑减少外键约束
- 使用 RESTRICT 代替 CASCADE
应用层重试机制
- 捕获死锁异常并实现重试逻辑
int retries = 3;
boolean success = false;
while (retries > 0 && !success) {
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false); // 开启事务
// 数据库操作
PreparedStatement ps = conn.prepareStatement("UPDATE...");
ps.executeUpdate();
conn.commit(); // 提交事务
success = true;
} catch (SQLException e) {
if (conn != null) {
try {
conn.rollback(); // 回滚事务
} catch (SQLException ex) {
// 处理回滚异常
}
}
if (e.getErrorCode() == 1213 && retries > 1) { // MySQL死锁错误码1213
retries--;
Thread.sleep(100); // 短暂延迟后重试
} else {
throw e; // 重试失败或其他错误,继续抛出
}
} finally {
if (conn != null) {
conn.close();
}
}
}
使用乐观锁替代悲观锁
- 适合读多写少的场景
- 使用版本号或时间戳实现
- 注意:高冲突场景下乐观锁可能导致频繁重试
死锁排查流程
总结
死锁类型 | 典型特征 | 解决方案 | 开发成本 | 适用场景 |
---|---|---|---|---|
行锁更新冲突 | 多个事务更新相同或相关行数据 | 统一访问顺序、减小事务范围、使用乐观锁 | 低 | 高并发多表更新业务 |
Gap 锁冲突 | REPEATABLE READ 隔离级别下插入操作死锁 | 降低隔离级别、使用 ON DUPLICATE KEY UPDATE | 中 | 需要频繁插入查询的应用 |
外键约束死锁 | 父子表并发操作导致锁冲突 | 减少外键使用、调整事务顺序、使用 RESTRICT 约束 | 中高 | 具有复杂关系模型的系统 |
元数据锁死锁 | DDL 和 DML 语句混合执行 | 将 DDL 操作放在低峰期、使用在线 DDL 工具 | 高 | 需要频繁架构变更的应用 |
自增锁死锁 | 多事务同时插入自增列 | 调整 innodb_autoinc_lock_mode、批量插入 | 低 | 高并发写入场景 |
通过本文的案例和分析,相信你已经对 InnoDB 死锁有了更深入的理解。记住,死锁不可完全避免,但可以通过合理的设计和实践大大减少其发生频率和影响。当死锁发生时,保持冷静,按照本文提供的排查流程,你一定能够找到问题所在并解决它。
感谢您耐心阅读到这里!如果觉得本文对您有帮助,欢迎点赞 👍、收藏 ⭐、分享给需要的朋友,您的支持是我持续输出技术干货的最大动力!
如果想获取更多 Java 技术深度解析,欢迎点击头像关注我,后续会每日更新高质量技术文章,陪您一起进阶成长~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。