线上BUG:MySQL死锁分析实战

本文通过线上告警信息分析了MySQL死锁的原因,解释了INSERT语句的锁行为,并通过一个模拟并发请求的单元测试展示了死锁的产生过程。在实践中,事务一因尝试插入已存在记录而等待S锁,而事务二持有X锁并等待插入意向锁,导致死锁。最终,InnoDB回滚了事务一以解除死锁。解决线上问题的方法是移除不必要的初始化逻辑,避免并发批量插入。

1 线上告警

我们不需要关注截图中得其他信息,只要能看到打印得org.springframework.dao.DeadlockLoserDataAccessException就足够了,就是MySQL发生死锁导致服务抛异常。
在这里插入图片描述
关于接口得逻辑,可以大概描述为:C端调用接口查询店铺得追踪事件列表,如果查询为空列表则顺便给初始化,这里的初始化是批量插入一批事件追踪列表,然后再返回,这里要给到一个关于表的信息点:这个表有主键索引和唯一索引。

1.1 云日志&死锁日志

不管是云日志还是死锁日志,都是显示着是并发重复插入导致的死锁,下面我就简单放一下云日志的截图,关于死锁的日志就不放了,因为下面将自己弄个demo来仿造并发重复请求导致批量插入发生死锁。
在这里插入图片描述

2 相关锁概念

2.1 INSERT语句如何加锁

首先我们得先知道在执行 INSERT 语句时,引擎(默认InnoDb)是如何加锁的。

默认情况下,执行 INSERT 语句是不用加锁的,
不过如果事务中执行一条 INSERT 语句,会先定位到该记录在 B+ 树的位置时,接着判断该位置的下一条记录被加了 grap 锁;如果是的话会在记录上加上一种类型为插入意向锁的锁,最后事务进入等待状态。

插入意向锁是行级别的,也是一种间隙锁。插入意向锁之间互相兼容,多个事务可以同时对同一区间加上插入意向锁;还有在事务中,如果成功插入记录并且还未提交事务,那么当前事务还会持有插入记录的行锁。

2.2 键发生重复冲突

如果当插入记录时遇到重复键,当前事务会在生成错误信息前,对记录加上S锁,如果是唯一索引发生的重复键,会加上S型的next-key锁

3 实践出真知

下面我们开始上例子了。

3.1 表信息

使用现有的表:

  • 用户表
  • 字段有:id、name、gender、user_type
  • id为主键,name加了唯一索引;这里加唯一索引是要和上面的告警对齐。
CREATE TABLE `user` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(20) NOT NULL,
  `gender` char(1) NOT NULL,
  `user_type` varchar(2) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

3.2 单元测试

接着是单元测试:
entity、mapper和service就直接省略过了。

单元测试主要是为了模拟线上的场景,前端并发发起请求,导致发生并发批量插入同一批数据

3.2.1 代码如下

模拟并发数3:

@SpringBootTest(classes = MysqlInActionApplication.class)
@RunWith(SpringRunner.class)
public class MysqlInActionApplicationTests {

	@Autowired
	private UserService userService;
	
	/**
	 * 线程数
	 */
	private static final int threadNum = 3;
	/**
	 * 控制同时请求
	 */
	private static final CountDownLatch countDownLatch = new CountDownLatch(threadNum);

	/**
	 * 用户请求
	 */
	class UserRequest implements Runnable{

		@Override
		public void run() {
			try {
				// 等待
				countDownLatch.await();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// 批量插入用户,插入数据不变
			saveUserList();
		}
	}

	@Test
	public void contextLoads() {
		for (int i = 0; i < threadNum; i++) {
			new Thread(new UserRequest()).start();
			// 计数减一
			countDownLatch.countDown();
		}
		try {
			Thread.currentThread().join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	/**
	 * 批量插入用户
	 */
	public void saveUserList(){
		List<User> userList = new ArrayList<>();
		userList.add(new User().setName("winfun").setGender("m").setUserType("1"));
		userList.add(new User().setName("fenghao").setGender("w").setUserType("2"));
		userList.add(new User().setName("luff").setGender("m").setUserType("1"));
		this.userService.saveBatch(userList);
	}
}

3.2.2 运行结果

我们可以看到,基本和上面的告警信息是保持一致的了,直接抛出死锁的异常。在这里插入图片描述

3.3 MySQL 日志

我们再看看mysql的死锁日志:

show engine innodb status;
------------------------
LATEST DETECTED DEADLOCK
------------------------
2021-07-03 12:36:02 0x7000082df000
*** (1) TRANSACTION:
TRANSACTION 25374, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 17, OS thread handle 123145438982144, query id 356 localhost 127.0.0.1 root update
INSERT INTO user  ( gender,
name,
user_type )  VALUES  ( 'm',
'winfun',
'1' )
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 62 page no 4 n bits 72 index uk_name of table `test`.`user` trx id 25374 lock mode S waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 6; hex 77696e66756e; asc winfun;;
 1: len 8; hex 0000000000000001; asc         ;;

*** (2) TRANSACTION:
TRANSACTION 25373, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 19, OS thread handle 123145439539200, query id 369 localhost 127.0.0.1 root update
INSERT INTO user  ( gender,
name,
user_type )  VALUES  ( 'w',
'fenghao',
'2' )
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 62 page no 4 n bits 72 index uk_name of table `test`.`user` trx id 25373 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 6; hex 77696e66756e; asc winfun;;
 1: len 8; hex 0000000000000001; asc         ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 62 page no 4 n bits 72 index uk_name of table `test`.`user` trx id 25373 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 6; hex 77696e66756e; asc winfun;;
 1: len 8; hex 0000000000000001; asc         ;;

*** WE ROLL BACK TRANSACTION (1)

3.3.1 事务一信息

*** (1) TRANSACTION:
TRANSACTION 25374, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s), undo log entries 1
MySQL thread id 17, OS thread handle 123145438982144, query id 356 localhost 127.0.0.1 root update
INSERT INTO user  ( gender,
name,
user_type )  VALUES  ( 'm',
'winfun',
'1' )

事务一的trascationId为25374,存活0秒
事务一执行的SQL为:INSERT INTO user ( gender,name,user_type ) VALUES ( 'm','winfun','1' )语句

3.3.2 事务一正在等待的锁

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 62 page no 4 n bits 72 index uk_name of table `test`.`user` trx id 25374 lock mode S waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 6; hex 77696e66756e; asc winfun;;
 1: len 8; hex 0000000000000001; asc         ;;

事务一正在等待插入记录的S型的next-key锁。

3.3.3 事务二的信息

*** (2) TRANSACTION:
TRANSACTION 25373, ACTIVE 0 sec inserting
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 19, OS thread handle 123145439539200, query id 369 localhost 127.0.0.1 root update
INSERT INTO user  ( gender,
name,
user_type )  VALUES  ( 'w',
'fenghao',

事务二的事务ID为25373,存活0秒
事务一执行的SQL为:INSERT INTO user ( gender,name,user_type ) VALUES ( 'w','fenghao','2' )语句

3.3.4 事务二持有锁信息

*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 62 page no 4 n bits 72 index uk_name of table `test`.`user` trx id 25373 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 6; hex 77696e66756e; asc winfun;;
 1: len 8; hex 0000000000000001; asc         ;;

事务二持有 name 为 winfun 这一行唯一二级索引的X锁,但不是gap锁。

3.3.5 事务二等待的锁

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 62 page no 4 n bits 72 index uk_name of table `test`.`user` trx id 25373 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
 0: len 6; hex 77696e66756e; asc winfun;;
 1: len 8; hex 0000000000000001; asc         ;;

事务二在添加插入意向锁时发现记录已经被加上X型的间隙锁,所以无法添加,只能等待锁释放。

3.3.6 最后的解决

*** WE ROLL BACK TRANSACTION (1)

InnoDb 回滚了事务一,从而让事务一接触

3.4 分析总结:

  1. 事务一和事务二是并发批量插入同一批数据
  2. 事务二先执行,成功插入 winfun这条记录,然后对这条记录加上了行锁
  3. 接着事务一进来了,发现winfun这个key是重复冲突了,接着在返回报错信息前,对winfun这条记录加上S型的next-key锁,但是发现winfun这条记录上已经有一个行锁,所以只能等待
  4. 接着事务二进行第二条记录的插入,即插入fenghao;此时发现它的下一条记录,即winfun记录处已经有事务一要加入next-key锁,导致产生冲突,所以事务二也进入等待
  5. 最后,只能回滚事务一,从而让事务二完整执行下去。

4 最后

最后如何解决线上这个问题呢?

其实很简单,可以上分布式锁,但是我们这场景没有必要,反而会一定程度上增加接口的耗时;并且我们这个是C端接口,完全没有必要拥有初始化店铺数据的能力,把这能力保留在Admin端的接口即可;所以最后将初始化,即批量插入初始化数据的逻辑干掉即可~

<think>嗯,用户是一位二本软件工程专业的女生,想要提升技术能力和准备面试以进入大厂。从她的专业背景来看,她可能已经具备一定的编程基础,但需要更系统化的提升路径和面试策略。 用户提到"二本"和"女生"这两个身份标签,说明她可能对自己的学历背景和性别在技术领域的竞争力存在顾虑。其实大厂更看重实际能力而非学历或性别,但需要针对性补足项目经验和算法能力。她真正需要的可能是:如何弥补学历差距的竞争力方案、适合女工程师的高效学习路径、以及克服面试焦虑的具体方法。 参考之前提到的引用内容[^2],大厂求职的核心确实是"刷题+项目深度"。不过对于女生而言,可能需要更强调技术社区参与和沟通能力的展示——这往往是女性工程师的隐性优势。从用户提问方式看,她应该处于大三或大四的求职准备期,时间还算充裕。 专业层面需要分三步走:首先是基础巩固,重点在数据结构和操作系统这类笔试高频考点;其次是工程能力,建议通过开源项目积累协作经验;最后是面试技巧,特别是系统设计环节的表达逻辑。考虑到软件工程专业,可以推荐她主攻Java或Go生态,这两个方向在大厂的后端岗位需求量大。 特别要注意的是,用户可能低估了"软技能"的价值。在技术能力达标的情况下,清晰的架构表达能力和需求理解能力反而会成为二本学生的破局点。建议她在GitHub建立技术博客,定期输出技术分析——这既能巩固知识,又能形成可视化的能力证明。 安全方面要提醒避免两个误区:一是不要购买保过简历服务,二是初期别死磕算法竞赛。更务实的做法是参与GSOC这类有背书的中型项目,或者给Apache基金会项目提交PR。如果时间紧张,优先掌握LeetCode前200题和《设计数据密集型应用》的核心章节比海量刷题更有效。</think>以下是为二本软件工程专业女生量身定制的技术提升与面试准备方案,结合大厂招聘核心要求分步实施: --- ### **一、基础能力强化(6-8个月周期)** 1. **算法与数据结构** - **每日必做**: - LeetCode/HackerRank刷题(前200道高频题精刷) - 重点掌握:动态规划(背包问题)、二叉树(DFS/BFS)、图论(Dijkstra)、字符串处理(KMP) - 目标:Easy题15分钟内解出,Medium题25分钟内解出 - **工具辅助**: ```python # 使用Python collections库优化代码 from collections import defaultdict, deque # 例:层级遍历二叉树 def level_order(root): if not root: return [] queue = deque([root]) res = [] while queue: level = [] for _ in range(len(queue)): node = queue.popleft() level.append(node.val) if node.left: queue.append(node.left) if node.right: queue.append(node.right) res.append(level) return res ``` 2. **计算机核心课补强** - **操作系统**:进程调度、死锁检测、内存管理(分页/分段) - **网络**:TCP/IP协议栈、HTTP/2特性、QUIC协议原理 - **数据库**:索引优化(B+树)、事务隔离级别、Redis持久化机制 - **实践建议**: - 用Wireshark抓包分析TCP三次握手 - 在Linux环境下实现简单文件系统 --- ### **二、技术栈深度聚焦(选择1-2个方向)** | 方向 | 必学技术栈 | 实战项目建议 | |------------|----------------------------|-----------------------------| | **后端开发** | Spring Cloud, MySQL调优, Kafka | 高并发秒杀系统(QPS>3000) | | **前端开发** | React Hooks, Webpack优化, TypeScript | 微前端架构落地 | | **测试开发** | Selenium, Jmeter压测, DevOps流水线 | 自动化测试平台开发 | > 📌 关键策略:**在GitHub构建技术博客**,定期更新源码解析(如Spring启动流程),Star数>100的项目可写进简历[^2] --- ### **三、差异化竞争力打造** 1. **项目经验强化** - **避免学生项目**:参与开源项目(Apache基金会/GSoC),贡献文档或修复bug - **数据说话**:在项目中体现性能优化成果,例如: > “通过Redis缓存策略,将API响应时间从1200ms降至200ms” 2. **软技能包装** - **沟通优势**:在简历中突出需求分析、跨部门协作经验 - **技术文档**:编写过API文档/技术方案可单独设立简历模块 --- ### **四、大厂面试闯关技巧** 1. **简历黄金法则** ```markdown ## 项目经验 - 分布式任务调度系统(Java) * 采用时间轮算法替代Quartz,降低调度延迟40% * 设计熔断机制防止雪崩,系统可用性达99.95% * [源码链接] | [压测报告] ``` 2. **行为面试准备** - 必考题: ```markdown Q: “遇到技术难点如何解决?” A: “示例:在开发订单模块时发现超卖问题→分析日志锁定并发场景→ 对比悲观锁/CAS方案→选用Redis分布式锁→压测验证→产出技术文档” ``` 3. **技术面应对策略** - 系统设计题四步法: ```mermaid graph LR A[需求澄清] --> B[估算QPS] B --> C[架构框图] C --> D[关键问题分析] ``` --- ### **五、资源清单** 1. **免费学习平台** - 算法:LeetCode专栏《代码随想录》 - 操作系统:MIT 6.S081 (xv6实验) - 数据库:CMU 15-445 (BusTub项目) 2. **女性工程师社区** - Grace Hopper Celebration中国分会 - TechLadies线上技术分享会 > 💡 **关键提醒**:大厂每年9月启动应届提前批,建议大三暑期完成首轮技术储备,冲刺大厂实习转正机会[^2]。 --- **
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值