MySQL并发引起的死锁问题

通过实验发现高并发下INSERT...ON DUPLICATE KEY UPDATE导致MySQL性能下降及死锁问题。分析表明,大量连接争夺行锁是主要原因。通过增加联合主键优化表结构,有效分散锁竞争,显著提升性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MySQL并发引起的死锁问题

背景:

平台的某个数据库上面有近千个连接,每个连接对应一个爬虫,爬虫将爬来的数据放到cdb里供后期分析查询使用。前段时间经常出现cdb查询缓慢,cpu占有率高的现象。通过show processlist后发现,大量的连接卡在了执行INSERT ... ON DUPLICATE KEY UPDATE这样的语句上面。难道并发执行INSERT ... ON DUPLICATE KEY UPDATE会导致cpu负荷直线上升吗,下面我们做一个实验。

实验:

先创建一张表TestA:

CREATE TABLE `TestA` ( `id` int(11) NOT NULL, `num` int(1) DEFAULT NULL,

再编写一个压测测试脚本,分别在并发为1、2、5、10,20,50,100,125,200的情况下测试执行1000次 INSERT INTO

TestA

VALUES (1,1) ON DUPLICATE KEY UPDATE num=num+1语句。
import gevent,timefrom gevent import monkey

运行结果如下图,随着并发数的增加执行sql语句耗时呈现先下降后增加的趋势,与之相对应的是cpu使用率随着并发数增加不断增加。可以看出,当并发数大于一定125的时候,系统发生了雪崩,性能急剧下降。而在图上没有标出来的是,当并发数大于200的时候,mysql直接返回了Deadlock found when trying to get lock; try restarting transaction错误,已经无法正常执行语句了。

MySQL并发引起的死锁问题

分析:

通过perf来分析造成上述雪崩的原因,发现是卡在了lock_rec_get_prev函数上面。

MySQL并发引起的死锁问题

INSERT INTO  TestA  VALUES (1,1) ON DUPLICATE KEY UPDATE num=num+1

这个语句先在表TestA中找到是否存在id=1的行,因为id是主键,所以很快就定位到这一行上面。接下来需要执行update操作,在执行update之前需要获取该行的X锁。由于大量的连接都在执行这个操作,因此在抢夺行锁上产生了大量的竞争,因为行锁的分配也涉及了自旋锁。很多连接就卡在了自旋锁上面,白白的消耗了cpu资源。

解决方案:

其实最好的解决方案就是不要将这些爬虫直接连到mysql上面,通过一个中间层维护一个mysql的连接池,这样既能满足实际业务需求,也不会造成死锁。当然对于这个具体场景也是有简单的优化方案的。造成死锁的原因是大量连接对行锁进行争夺。既然这个行锁是性能瓶颈,那我们可以通过增加行锁来减少争夺的成本。

我们稍微改造一下表结构,添加一个联合主键(id、thread_id),每个连接都执行

INSERT INTO  TestB  VALUES (1,{thread_id},1) ON DUPLICATE KEY UPDATE num=num+1。

这样每个连接都有了属于自己的行锁,不会互相争夺而产生死锁了。最后只需要执行一下sum就可以获取最终结果了。

CREATE TABLE `TestB` ( `id` int(11) NOT NULL, `thread_id` int(11) NOT NULL, `num` int(1) DEFAULT NULL,

压测测试结果如图,随着连接数的增加,耗时减少至稳定,cpu使用率增加至稳定。

MySQL并发引起的死锁问题

在高并发环境下,MySQL死锁问题常常会导致数据库性能下降甚至服务不可用。死锁的发生通常是由于多个事务在并发执行时相互等待对方持有的锁资源,从而形成循环依赖[^1]。为了有效分析和解决这类问题,需要从以下几个方面入手: ### 1. 死锁的分析方法 #### 1.1 查看死锁日志 MySQL 提供了 `SHOW ENGINE INNODB STATUS` 命令,可以用来查看最新的死锁信息。通过该命令,可以获取到死锁发生时各个事务的详细锁等待情况,包括每个事务当前持有的锁和等待的锁。这对于定位死锁的根本原因非常关键[^4]。 #### 1.2 SQL 加锁分析 在高并发场景下,某些 SQL 语句可能会对多个索引加锁,尤其是在涉及多个字段的更新操作时。例如,`UPDATE` 语句可能先对某个索引加锁,然后再对另一个索引加锁。如果多个事务以不同的顺序对这些索引加锁,就可能导致死锁。因此,分析 SQL 语句的加锁顺序是解决死锁问题的重要步骤[^5]。 ### 2. 死锁的解决方案 #### 2.1 调整事务的执行顺序 为了避免死锁,可以在设计事务时尽量保证所有事务以相同的顺序访问资源。例如,如果多个事务都需要访问 `user_id` 和 `item_id` 这两个字段,可以统一规定事务必须先访问 `user_id`,再访问 `item_id`,从而避免不同事务以不同顺序加锁导致的循环等待[^5]。 #### 2.2 减少事务的粒度 事务的执行时间越长,持有锁的时间也就越长,从而增加了与其他事务发生冲突的可能性。因此,可以通过优化事务逻辑,减少事务中涉及的操作数量,缩短事务的执行时间,降低死锁发生的概率。 #### 2.3 使用乐观锁或重试机制 在某些场景下,可以考虑使用乐观锁(Optimistic Locking)来替代悲观锁。乐观锁通常通过版本号(Version)或时间戳(Timestamp)来控制并发更新,只有在提交时才会检查是否有冲突。如果检测到冲突,可以抛出异常并由客户端决定是否重试。这种方式可以有效减少锁的竞争,降低死锁的发生概率。 #### 2.4 避免不必要的索引更新 在执行 `UPDATE` 语句时,应尽量避免对不必要的字段进行更新。例如,如果某个字段的值在大多数情况下不会发生变化,可以在应用层判断其值是否需要修改,避免无意义的更新操作,从而减少对索引的加锁次数。 ### 3. 示例:事务加锁顺序不一致导致的死锁 假设存在三个事务,分别执行以下 `UPDATE` 语句: - 事务 A: `UPDATE table SET column1 = value1 WHERE user_id = 100;` - 事务 B: `UPDATE table SET column2 = value2 WHERE item_id = 200;` - 事务 C: `UPDATE table SET column3 = value3 WHERE user_id = 100;` 在这种情况下,事务 A 和 C 可能会先对 `user_id` 索引加锁,再对 `item_id` 或其他索引加锁,而事务 B 则可能先对 `item_id` 索引加锁,再对 `user_id` 索引加锁。如果这些事务并发执行,并且加锁顺序不一致,就可能导致死锁[^5]。 ### 4. 示例代码:乐观锁的实现 以下是一个使用乐观锁的简单示例,通过版本号来控制并发更新: ```sql -- 假设有一个表 users,包含 id、name 和 version 字段 START TRANSACTION; -- 查询当前版本号 SELECT name, version FROM users WHERE id = 1; -- 更新操作前检查版本号 UPDATE users SET name = 'new_name', version = version + 1 WHERE id = 1 AND version = current_version; COMMIT; ``` 如果在更新时发现版本号不匹配(即其他事务已经修改了该记录),则说明发生了冲突,应用层可以选择重试操作。 ### 5. 预防死锁的最佳实践 - **统一加锁顺序**:确保所有事务按照相同的顺序访问资源,避免因加锁顺序不一致而导致的死锁。 - **减少事务执行时间**:尽量在事务中执行必要的操作,减少事务的执行时间和锁的持有时间。 - **合理设计索引**:确保查询语句能够高效地使用索引,减少不必要的锁竞争。 - **使用合适的隔离级别**:根据业务需求选择适当的事务隔离级别,避免过高的隔离级别导致更多的锁竞争。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值