commit 成功为什么数据只更新了部分?

前言

我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!(抖音、公众号同号)

昨天在看【java深度调试技术】的时候看到数据库死锁这一章节.然后我就在思考平时写的代码 死锁的场景太多了,为什么工作了七八年从来没遇到因为死锁导致的业务阻塞问题,也从来没有解决过数据库的死锁问题。

今天这篇文章的主题就讲一下为什么我们很少去解决数据死锁问题,围绕这个话题我们会带出一个新的知识点:数据库在某些情况下只会回滚部分SQL 以及 我们通过在什么情况下应该去看日志,什么情况下去查询当前事务的情况。

为什么很少遇到数据库死锁

业务并发低并发低这个是很少发生死锁的主要原因. 除了这个问题之外和数据库本身的死锁机制也有很大的关系. 主流的SQL数据都是有死锁检测机制大同小异罢了。所以当我们的数据库检测到死锁之后,就会回滚部分死锁事务,从不会一直阻塞我们的业务。

MySQL数据库的死锁检测机制

今天就来说一下主流的SQL数据库它的死锁检测机制,当然主要还是讨论一下MySQL就行了。 mysql 的死锁检测机制或者说死锁兜底机制主要有个关键的配置:

  • 第一个是否开启死锁检测配置(默认开启): innodb_deadlock_detect = ON
  • 第二配置等待锁超时时间(默认50s): innodb_lock_wait_timeout = 50

第一步:innodb_deadlock_detect

MySQL的InnoDB引擎通过 等待图(Wait-for Graph)算法来检测死锁。系统会周期性地检查是否存在事务间的循环等待链,一旦发现,就会立即触发死锁处理机制。会回滚一个或者多个死锁事务,选择回滚代价小的事务(插入、更新、删除行数小的)。

当然这个死锁检测机制是可以通过配置关闭的,这个机制也会存在一定的性能开销,所以在极高的并发下可以考虑关闭死锁检测.

🔈 死锁检测配置(默认开启):innodb_deadlock_detect = ON

📒SQL官方文档:dev.mysql.com/doc/refman/...

测试(mysql8.0)
js 复制代码
-----------------------事务1------------------------------
// 第一步:会话 01 ,关闭自动提交事务,更新 navigation
set autocommit = 0;
UPDATE navigation SET name = 'txt01';

//第三步:更新 navigation_relation_station,一直等待锁
UPDATE navigation_relation_station SET navigation_id = 101;

---------------------事务2--------------------------------
//第二步:会话 02,关闭自动提交事务,更新 navigation_relation_station
set autocommit = 0;
UPDATE navigation_relation_station SET navigation_id = 2;
// 第四步: 更新 navigation ,形成死锁

UPDATE navigation SET name = 'txt02';

当执行第四步时就会形成死锁,事务1报错回滚,错误信息如下:

text 复制代码
UPDATE navigation_relation_station SET navigation_id = 101
1213 - Deadlock found when trying to get lock; try restarting transaction
查询时间: 5.445s

第二步:innodb_lock_wait_timeout

这个配置算是一个等待锁的一个兜底机制,不管是发生死锁(死锁探测机制关闭) ,还是因为锁被其他事务长时间占用 导致的锁等待超时,这个配置就显得非常重要了。默认50s,根据实际场景调整。 如果超过这个这个配置的设置时间了会发生么?

✅默认会回滚等待超时事务中获取锁的那条SQL,没错不是回滚整个事务. 当然这只是MySQL的默认策略,这个回滚策略也是可以通过配置控制的。可通过配置(默认关闭,回滚等待锁的SQL)让其回滚整个事务:innodb_rollback_on_timeout = OFF

测试(MySQL8.0)

💦本次测试当事务在等待锁超时的情况的下,数据库是只回滚部分SQL。并且会话保持,出错之后继续commit,看看能否执行成功,成功的部分是否为事务中未回滚的前半部分语句。

mysql 数据如下,ID为主键索引(避免锁范围扩大)

id realname
1 零零一
2 零零二

做一下测试,模拟等待锁超时的情况:

js 复制代码
//第一步,事务1,抢占 ID 为1的行锁,不提交事务
set autocommit = 0;
update t_user set realname = "事务1" WHERE id = "1";

// 第二步,事务2,先更新ID为2的数据,再更新ID= 1的数据(等待锁)
set autocommit = 0;
update t_user set realname = "事务2-2" WHERE id = "2";
update t_user  set realname = "事务2-1" WHERE id = "1";
commit;

第二步的事务等待锁超时,输出结果如下:

js 复制代码
update t_user set realname = "事务2-2" WHERE id = "2"
> Affected rows: 1
> 查询时间: 0s

update t_user  set realname = "事务2-1" WHERE id = "1"
> 1205 - Lock wait timeout exceeded; try restarting transaction
> 查询时间: 51.353s

再次commit第二的事务,发现能提交成功,刷新列表,发现只有事务中的第一条SQL执行成功了

测试结论:当事务因为等待锁超时(没有开启回滚整个事务),数据会报错,并且事务会回滚等待锁的SQL语句。此时会话依然保持,可以继续commit,事务等待锁之前的SQL将会被重新提交。

如何解决死锁问题

官网和博客好多说使用SHOW ENGINE INNODB STATUS,这个命令能执行成功,但是我从来没有看到过任何信息(不知道怎么回事儿,模拟发生过死锁)。

看死锁日志

日志准备:MySQL8.0 windows 环境中,配置文件需要指定error日志输出文件,以及开启死锁日志记录。

js 复制代码
# my.ini 文件配置
[mysqld]
innodb_print_all_deadlocks = 1
log_error = /var/log/mysql/error.log

📟同时还要确保我们的死锁检测配置时开启的,不然也看不到死锁日志

模拟死锁SQL:

js 复制代码
------------------- 事务49421-----------------------
第一步
set autocommit = 0 ;
UPDATE t_user SET realname = 'tx01-1' WHERE id = '101';
第三步
UPDATE t_user SET realname = 'tx02-1' WHERE id = '102';
COMMIT

-----------------事务49422--------------------------
第二步
set autocommit = 0 ;
UPDATE t_user SET realname = 'tx122222-1' WHERE id = '102';
第四步
UPDATE t_user SET realname = 'tx1111-1' WHERE id = '101';
COMMIT;

死锁日志如下:

js 复制代码
TRANSACTION 49421, ACTIVE 12 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 8, OS thread handle 31912, query id 130 localhost ::1 root updating
UPDATE t_user SET realname = 'tx02-1' WHERE id = '102'
RECORD LOCKS space id 10 page no 17899 n bits 216 index PRIMARY of table `test`.`t_user` trx id 49421 lock_mode X locks rec but not gap waiting (等待锁 102)
Record lock, heap no 150 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 3; hex 313032; asc 102;;
 1: len 6; hex 00000000c10e; asc       ;;
 2: len 7; hex 02000001540b3b; asc     T ;;;
 3: len 13; hex 31303231323334333238623363; asc 1021234328b3c;;
 4: len 10; hex 74783132323232322d31; asc tx122222-1;;
 5: len 3; hex 386266; asc 8bf;;

TRANSACTION 49422, ACTIVE 8 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), undo log entries 1
MySQL thread id 9, OS thread handle 28276, query id 134 localhost ::1 root updating
UPDATE t_user SET realname = 'tx1111-1' WHERE id = '101'
RECORD LOCKS space id 10 page no 17899 n bits 216 index PRIMARY of table `test`.`t_user` trx id 
49422 lock_mode X locks rec but not gap (持有锁102)
Record lock, heap no 150 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 3; hex 313032; asc 102;;
 1: len 6; hex 00000000c10e; asc       ;;
 2: len 7; hex 02000001540b3b; asc     T ;;;
 3: len 13; hex 31303231323334333238623363; asc 1021234328b3c;;
 4: len 10; hex 74783132323232322d31; asc tx122222-1;;
 5: len 3; hex 386266; asc 8bf;;

RECORD LOCKS space id 10 page no 17899 n bits 216 index PRIMARY 
of table `test`.`t_user` trx id 49422 lock_mode X locks rec but not gap waiting(等待锁101)

Record lock, heap no 149 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 3; hex 313031; asc 101;;
 1: len 6; hex 00000000c10d; asc       ;;
 2: len 7; hex 01000001010c84; asc        ;;
 3: len 13; hex 31303131323334333238623363; asc 1011234328b3c;;
 4: len 6; hex 747830312d31; asc tx01-1;;
 5: len 3; hex 386266; asc 8bf;;

🔱不会分析丢给AI就行了。 从上面的信息能看到具体的代码阻塞到的是哪行,这样自己再去定位代码其实也比较简单了。

查询事务表信息

因为我们mysql 自带的死锁检测机制会自动回滚 其中代价小的事务,所以因为死锁导致阻塞的事务就很难通过 查询运行中事务的状态来定位了。

当然如果是系统并发非常高,并且死锁检测机制被关闭,或者说用来查询长事务,可以去通过查看下面的信息来定位问题。

下面这些操作需要数据开启性能模式,默认开启:performance_schema = ON

  • MySQL 8.0 及以上版本:使用 performance_schema.data_locksperformance_schema.data_lock_waits 查看锁信息,配合 INNODB_TRX 查看事务详情。
  • MySQL 5.7 及以下版本:仍可使用 INNODB_LOCKSINNODB_LOCK_WAITSINNODB_TRX 这三个表。

✅推荐使用:performance_schema.INNODB_TRXperformance_schema.data_locks 表就行了;

data_locks :表关键信息事务ID,事务获取锁数据(比如主键值)、等待锁数据,所以这个是很方便定位到死锁的,定位到死锁的事务ID,然后再去INNODB_TRX回查,就能定位到指定的SQL

INNODB_TRX :表关键信息有事务ID,事务状态,等待执行的SQL。

事务状态字段: trx_state (事务状态,如果在等待锁就会显示:LOCK_WAIT); 事务ID:trx_id;事务等待执行SQL:trx_query ,事务的开始时间等(💢用来定位挂起的事务就非常方便了)。

死锁的预防

🔊这个就很简单了,首先就是保持更新表的顺序,比如各业务操作更新多表的时候,可以按照先更新主表再更新副表的顺序,或者直接按照 表名的 字符串排序更新。第二就是减少事务占用的时间 减少锁占用的时间,合理使用索引减少锁的范围,

监控死锁的第三方工具

上面都是基于MySQL 自带日志和事务表来分析的。下面介绍一下一些三方工具吧。 当然现在很多企业都使用大厂的云服务器,基本都自带数据库监控工具。

1. Percona Toolkit(PT 工具集)
  • 作用pt-deadlock-logger专门用于监控和记录 MySQL 死锁信息,可实时抓取死锁日志并输出到文件或数据库,支持定时运行和历史分析。
  • 用法示例pt-deadlock-logger --user=root --password=xxx localhost会持续监控并打印新发生的死锁,也可指定输出到文件:--dest file:/var/log/mysql/deadlocks.log
2. MySQL Workbench
  • 作用:MySQL 官方图形化工具,内置 "Performance" 模块,可直观查看当前事务、锁等待、死锁历史等信息(需连接数据库后在 "Data Export" 或 "Performance Schema" 面板中操作)。
  • 优势:可视化展示锁等待关系,适合新手快速定位问题。
3. Orchestrator
  • 作用:一款开源的 MySQL 高可用管理工具,除了主从切换,还能监控数据库锁状态、死锁事件,并提供告警功能。
  • 适用场景:分布式 MySQL 集群环境,需结合监控平台使用。
4. 监控平台集成(Prometheus + Grafana)
  • 原理 :通过 mysqld_exporter 采集 MySQL 锁相关指标(如 innodb_deadlocks 计数器),在 Grafana 中配置仪表盘,实时监控死锁发生频率,并设置告警(如死锁数突增时触发邮件 / 短信告警)。
  • 关键指标innodb_deadlocks(累计死锁次数)、innodb_lock_waits(锁等待次数)等。

MySQL什么场景下数据库会回滚部分SQL

上文说了等待锁超时,默认情况下会回滚等待锁的SQL。除了这种情况还存在其他情况也有这个问题。比如:

📃官方文档:dev.mysql.com/doc/refman/...

错误类型 InnoDB 的处理方式 简要说明与案例
表空间满 回滚SQL语句 例如,批量插入大量数据时,如果ibdata文件或独立表空间达到上限
事务死锁 回滚整个事务 例如,事务A持有行1锁等待行2,事务B持有行2锁等待行1。InnoDB会选择回滚其中一个事务(让另一个成功)。
锁等待超时 默认回滚当前语句 例如,一条更新语句等待锁超过innodb_lock_wait_timeout设置,该语句被回滚。启用innodb_rollback_on_timeout会回滚整个事务。
重复键错误 回滚单行操作 例如,INSERT多条数据,其中某条违反唯一键约束,仅该行插入失败。
行过长错误 回滚SQL语句 例如,尝试插入一个VARCHAR字段长度超过列定义的数据
其他多数错误 回滚SQL语句 通常由MySQL上层代码检测到,如语法错误等,会回滚对应的SQL语句。

📢 博主测试过 锁等待超时,和 重复键插入,报错之后,继续commit ,报错行之后的SQL不会再被执行,只会提交回滚行之前的SQL。

总结

从为什么很少遇到死锁的原因,到死锁相关的配置,到死锁的解决思路,再到哪些场景数据库在回滚的时候会回滚当前阻塞的SQL。写这篇文章确实比较耗费心力,因为博主也做了很多测试。希望各位兄弟姐妹点个赞 收藏吧。

相关推荐
煎蛋学姐2 小时前
SSM网上旅游订票服务系统10r27(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·ssm 框架·网上旅游订票系统·旅游服务数字化
追逐时光者2 小时前
小伙伴们学习 C#/.NET 相关技术栈的学习心得和路线
后端·.net
gelald2 小时前
Spring Security 核心组件
后端·spring
码上淘金2 小时前
在 YAML 中如何将 JSON 对象作为字符串整体赋值?——兼谈 Go Template 中的 fromJson 使用
java·golang·json
码事漫谈2 小时前
Blazor现状调研分析:2025年全栈开发的新选择
后端
码事漫谈2 小时前
C++的开发难点在哪里?
后端
海南java第二人2 小时前
数据库范式详解:从冗余到规范的升华之旅
数据库·oracle·ffmpeg
刘一说3 小时前
Spring Boot 应用的指标收集与监控体系构建指南
java·spring boot·后端