news 2026/10/20 15:21:22

MySQL事务从原理到实践:ACID、隔离级别与常见坑一次讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL事务从原理到实践:ACID、隔离级别与常见坑一次讲透

做后端开发的人应该都有过这种经历:一条UPDATE语句把整张表的金额字段都改错了,或者订单创建到一半程序崩溃,结果库存扣了、订单却没生成。这种数据“半吊子”状态,正是事务要解决的问题。MySQL里的事务,说白了就是一组SQL操作打包成一个整体,要么全部成功,要么全部失败,不存在中间状态。这篇文章我就从实际操作出发,把事务的开启、提交、回滚,到ACID四个特性的底层逻辑,一次性讲透。

不管是刚入门的学生,还是写了好几年业务代码的开发,事务这块都值得花时间理清楚。很多线上事故,本质上就是事务边界画错了、隔离级别选错了、回滚没处理好。下面这些内容都是我实际排查问题时的经验总结,希望对你有用。

1. 为什么需要事务:先搞清楚它解决了什么事

1.1 从一次转账说起

假设你的账户系统里有这样两条SQL:

UPDATE account SET balance = balance - 500 WHERE user_id = 1; UPDATE account SET balance = balance + 500 WHERE user_id = 2;

第一条是扣款,第二条是入账。如果第一步执行成功,第二步执行前数据库突然断电、进程被杀、或者网络闪断,那么钱扣了但对方没收到,账就平不上。

这就是典型的"部分成功"问题。事务的存在,就是让这两条SQL变成一个不可分割的整体。用事务包裹后,要么两步都执行完,要么两步都不生效,系统重启后数据依然是对得上的。

1.2 并发场景下的数据一致性

单线程场景可能还不明显,一旦并发上来,问题就多了。比如两个用户同时抢购最后一件商品:

  • 用户A和B同时读到库存为1;
  • A执行扣减,库存变0;
  • B不知道库存已变,也执行扣减,库存变-1。

如果不做控制,超卖就发生了。事务配合锁机制,能保证同一时刻只有一个事务能修改这条库存记录,另一个事务必须等它提交后才能继续。

1.3 事务不是MySQL独有的概念

其实所有主流关系型数据库都有事务,MySQL只是实现方式有所不同。你明白了MySQL里的运作机制,以后再接触其他数据库,核心概念都是相通的。差别主要在语法细节、锁策略、日志实现上。

注意:MyISAM引擎是不支持事务的,支持事务的是InnoDB引擎。MySQL 5.5之后InnoDB是默认引擎,但建表时依然可以指定MyISAM,所以排查事务不生效的问题时,第一反应应该是看表引擎。

2. 事务的完整操作手册

2.1 开启一个事务

MySQL默认是自动提交模式,也就是每一条SQL执行完就立刻提交,不需要你手动管。真正要操作事务时,第一件事是关闭自动提交,或者显式开启事务。

常用写法:

-- 方式一:标准SQL START TRANSACTION; -- 或者 BEGIN; -- 执行你的业务SQL... UPDATE account SET balance = balance - 500 WHERE user_id = 1; UPDATE account SET balance = balance + 500 WHERE user_id = 2; -- 确认无误后提交 COMMIT;

BEGIN和START TRANSACTION在大多数场景下效果一样,但有一个细节值得注意:START TRANSACTION是标准SQL语法,写起来也更语义化,我个人的习惯是用它。另外START TRANSACTION后面还可以跟修饰符,比如START TRANSACTION WITH CONSISTENT SNAPSHOT,这是针对一致性读的,后面讲隔离级别时会再提到。

2.2 提交与回滚

事务结束只有两种正确方式:COMMIT提交,或者ROLLBACK回滚。

-- 提交 COMMIT; -- 回滚 ROLLBACK;

提交的含义是把事务期间的所有修改持久化到磁盘,其他事务从此能读到这些改动。回滚则相反,事务期间做的所有修改全部撤销,就像从来没发生过。

实际操作中,业务代码里最常见的回滚逻辑是这样:

START TRANSACTION; UPDATE account SET balance = balance - 500 WHERE user_id = 1; -- 模拟异常:比如这里通过程序抛出了一个错误 -- 如果发生异常,执行 ROLLBACK ROLLBACK;

生产环境里一般不靠人眼判断该提交还是回滚,而是靠程序里的异常捕获机制。比如Java里就是try-catch块,catch到异常就执行rollback,正常执行完就走commit。

2.3 中途存档点:SAVEPOINT

有些场景下,事务内部操作很多,但你不希望因为最后一步失败就把前面所有操作都回滚掉。这时候可以用SAVEPOINT,也就是保存点。

START TRANSACTION; UPDATE account SET balance = balance - 500 WHERE user_id = 1; SAVEPOINT sp1; UPDATE account SET balance = balance + 500 WHERE user_id = 2; -- 发现入账金额错了,回滚到sp1位置 ROLLBACK TO SAVEPOINT sp1; -- 继续执行其他操作 UPDATE account SET balance = balance + 600 WHERE user_id = 2; -- 最终提交 COMMIT;

这个机制在实际业务里用得不算多,但一旦用上就很救命。比如批量导入数据时,你按1000条一个批次处理,希望跳过出错的那一条而保留前面正确的数据,SAVEPOINT就是干这个的。

注意:ROLLBACK TO SAVEPOINT并不会释放所有锁,也不会结束事务。它只是撤销保存点之后的变更。事务本身还活着,直到你执行COMMIT或ROLLBACK。

2.4 自动提交和隐式提交

默认情况下,MySQL每个单独的SQL语句都是一个自动提交的事务。也就是说你执行一条UPDATE,成功就自动commit了,失败就自动回滚,不需要你干预。

要查看当前是否开启自动提交:

SHOW VARIABLES LIKE 'autocommit';

输出结果里autocommit的值是ON的话,就是自动提交模式。临时关闭可以这样:

SET autocommit = 0;

注意,这个设置只对当前会话有效,不影响其他连接。而且从autocommit = 0切换到事务操作时,不需要额外写BEGIN,因为关闭自动提交后,你执行的第一条SQL就隐式开启了事务,直到你手动COMMIT或ROLLBACK。

另外还有一个"隐式提交"的坑。有些SQL语句执行时会强制提交当前事务,哪怕你正在一个事务中间:

  • DDL语句,比如CREATE TABLE、ALTER TABLE、DROP TABLE;
  • 账户和权限管理语句,比如CREATE USER、GRANT等;
  • 锁相关操作,比如LOCK TABLES;
  • 管理类语句。

我踩过的坑是:在一个事务里先更新了业务数据,然后执行了一个ALTER TABLE加字段,结果前面的更新被自动提交了,后面业务异常回滚时,发现前面的操作根本没撤销掉。所以事务边界内尽量别混用DDL。

2.5 事务操作速查表

操作SQL说明
开启事务START TRANSACTION / BEGIN显式开启事务
提交事务COMMIT持久化所有修改
回滚事务ROLLBACK撤销所有未提交修改
设置保存点SAVEPOINT xxx在事务内标记位置
回滚到保存点ROLLBACK TO SAVEPOINT xxx撤销保存点后的操作
释放保存点RELEASE SAVEPOINT xxx删除保存点
查看自动提交SHOW VARIABLES LIKE 'autocommit'查看当前会话状态

3. 四大特性拆解:ACID背后的实现逻辑

3.1 原子性(Atomicity):要么全做,要么全不做

原子性保证事务是一个不可分割的工作单元。事务内的所有操作,要么全部执行成功,要么全部不执行。这个特性靠的是undo log(回滚日志)。

事务每次修改数据之前,会先把修改前的旧值写入undo log。如果事务中途失败需要回滚,InnoDB就根据undo log里的旧值把数据还原回去。这个机制很像写作文之前先复印一份原稿,涂坏了就照着原稿重新抄一遍。

以那条转账SQL为例:

START TRANSACTION; UPDATE account SET balance = balance - 500 WHERE user_id = 1; UPDATE account SET balance = balance + 500 WHERE user_id = 2; ROLLBACK;

执行完第一条UPDATE后,旧值5000被写入undo log。执行第二条前发现有问题,直接ROLLBACK,InnoDB根据undo log把user_id=1的余额从4500还原成5000,数据回到初始状态。

3.2 一致性(Consistency):数据永远符合约束

一致性指的是事务执行前后,数据库都处于合法状态,不会破坏完整性约束、唯一索引、外键、触发器等规则。

这里容易混淆的是:事务机制本身只是手段,一致性的保证需要应用层和数据库层配合。比如账户余额不能为负数这种业务规则,数据库并不知道,需要你在SQL或代码层面判断。

从实现角度看,一致性部分是靠原子性、隔离性共同支撑的。原子性保证了没有中间状态被持久化,隔离性保证并发事务之间不会互相干扰,两者都满足了,最终呈现出来的数据自然是一致的。

举一个反面例子:

-- user_id=1 的余额只有 300 START TRANSACTION; UPDATE account SET balance = balance - 500 WHERE user_id = 1; COMMIT;

这条SQL在事务层面没有任何问题,MVCC机制、锁机制都会正常工作。但余额变成了-200,业务上完全说不通。这就是为什么说一致性需要开发者主动参与,数据库只能保证它“知道”的约束,业务规则还得靠你自己把关。

3.3 隔离性(Isolation):并发事务互不干扰

隔离性保证多个事务并发执行时,一个事务的中间状态对另一个事务是不可见的。这就要提到事务隔离级别。

我先说结论,具体操作下一节展开。InnoDB支持四种隔离级别:

  • READ UNCOMMITTED(读未提交)
  • READ COMMITTED(读已提交)
  • REPEATABLE READ(可重复读)
  • SERIALIZABLE(串行化)

MySQL默认是REPEATABLE READ。隔离级别越高,并发性能越低,但数据一致性越强。实际选型时不是越高越好,而是根据业务需求匹配。

隔离性的实现主力是锁和MVCC(多版本并发控制)。InnoDB用了行锁、间隙锁、next-key lock去控制并发写,用MVCC读快照去控制一致性读。这两个机制配合,才保证了不同隔离级别下的不同表现。

3.4 持久性(Durability):提交了就别想丢

持久性是指事务一旦提交,修改就是永久性的,即使数据库立刻宕机,数据也不会丢。这个特性靠的是redo log(重做日志)。

InnoDB有个经典的策略:修改数据页时不会立即写磁盘(因为随机IO太慢),而是先写redo log,再异步刷盘。redo log是追加写的顺序IO,速度快得多。

流程是:

  1. 事务提交时,把修改记录写入redo log buffer;
  2. 把redo log buffer刷到磁盘上的redo log文件;
  3. 事务才算真正提交成功;
  4. 之后数据页什么时候刷到磁盘,由后台线程慢慢处理。

如果刷盘前数据库崩了,重启时InnoDB读取redo log,把已经提交的事务重新应用一遍,数据就找回来了。这就是"崩溃恢复"的底层逻辑。

注意:redo log是物理日志,记录的是"对哪个页的哪个偏移做了什么修改";undo log是逻辑日志,记录的是"怎么把数据撤销回去"。两者作用完全不同,别搞混。

4. 隔离级别与并发实践:实操中最容易踩坑的地方

4.1 隔离级别的表现

四种隔离级别对应的并发问题,用表格列出来最直观:

隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不会可能可能
REPEATABLE READ不会不会InnoDB下基本不会
SERIALIZABLE不会不会不会
  • 脏读:一个事务读到了另一个事务未提交的数据。如果那个事务回滚了,你读到的就是坏数据。
  • 不可重复读:同一个事务里,两次读取同一行数据,结果不一样。因为另一个事务在这期间提交了修改。
  • 幻读:同一个事务里,两次查询同一范围的数据,结果行数不一样。另一个事务插入或删除了符合条件的行。

4.2 InnoDB默认级别为什么是可重复读

MySQL官方把默认隔离级别设为REPEATABLE READ,很多人以为是为了和标准SQL对齐,其实不是。根本原因是InnoDB在REPEATABLE READ级别下,通过next-key lock和MVCC消除了幻读现象,所以REPEATABLE READ在MySQL里实际达到了SQL标准的串行化读效果。

这里有个具体表现:在REPEATABLE READ下,事务第一次执行SELECT时创建了一个一致性快照,之后事务内所有普通SELECT都读这个快照,不会看到其他事务后来提交的新数据。这就是为什么同一个事务里两次查询同一行,结果永远一致。

但要注意,这个"看不到新数据"只针对普通一致性读。如果你用SELECT ... FOR UPDATE或者LOCK IN SHARE MODE,那就走当前读,会读到最新已提交数据,会加锁。

4.3 修改隔离级别的两种方式

全局设置,影响所有新连接的会话:

SET GLOBAL transaction_isolation = 'READ-COMMITTED';

当前会话设置,只影响当前连接:

SET SESSION transaction_isolation = 'REPEATABLE-READ';

注意:MySQL 8.0里控制隔离级别的变量名是transaction_isolation,5.7及以前是tx_isolation。很多人从5.7迁移到8.0时,脚本里写的还是老变量名,会直接报错。

查看当前隔离级别:

SELECT @@transaction_isolation;

4.4 四种级别怎么选,我的建议

互联网高并发业务,大部分场景我推荐用READ COMMITTED。理由是这种级别下锁的范围更小,死锁概率低,吞吐量更高。MySQL在READ COMMITTED下,会把行锁退化掉一些间隙锁的持有,并发度更好。

金融类、订单类对强一致要求高的场景,用REPEATABLE READ或者SERIALIZABLE更稳。SERIALIZABLE几乎就是所有并发操作排队执行,性能代价很大,我实际中很少见到生产环境用它。

实操心得:隔离级别不是越高越好。我自己经历过一个系统,使用默认REPEATABLE READ,一个批量更新操作因为间隙锁互相阻塞,导致大量超时。改成READ COMMITTED后,阻塞降了一个数量级。所以如果你发现系统有频繁的锁等待,优先排查隔离级别是不是设高了。

5. 事务实操中常见的翻车现场与定位方法

5.1 事务没生效

这是新手最容易遇到的问题。排查顺序如下:

第一,看表的存储引擎。使用命令确认:

SHOW TABLE STATUS LIKE 'your_table'\G;

或者:

SELECT engine FROM information_schema.tables WHERE table_schema = 'your_db' AND table_name = 'your_table';

如果是MyISAM,事务直接不生效。建议直接改成InnoDB:

ALTER TABLE your_table ENGINE = InnoDB;

线上表改引擎前先评估锁表时间,数据量大会阻塞读写。

第二,看是不是有框架层面的自动提交。很多ORM框架默认把每条SQL当作一个事务自动提交,你还需要手动开启事务才能包住多条SQL。比如用某些框架的数据库访问层,如果没有显式调用事务方法,就相当于没开启事务。

第三,看事务的传播行为和边界。比如在一个方法里开了事务,调了另一个方法,那个方法内部又开了新事务,两个事务相互独立,一个回滚不影响另一个。这种问题用日志和SHOW ENGINE INNODB STATUS里的活跃事务数可以判断。

5.2 锁等待超时

出现Lock wait timeout exceeded是并发写同一行数据最常见的错误。默认锁等待时间是50秒,可通过参数调整:

SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';

如果只是偶尔锅不在这,调大也没用,反而拖慢了故障感知。优先做的是:

  1. 定位持有锁的事务;
  2. 分析事务的SQL,看是不是没用索引,导致行锁升级成了表锁;
  3. 看是不是事务长时间不提交。

定位持锁事务,可以用:

SELECT * FROM information_schema.innodb_trx;

主要关注trx_started字段。如果某个事务开启时间很长,基本就是它一直占着锁不释放,后面的只能排队。

5.3 死锁怎么处理

死锁的特征是互相等待对方持有的锁,谁都没法前进。InnoDB检测到死锁后,会回滚其中一个事务,另一个继续执行,并抛出一个报错:Deadlock found when trying to get lock。

死锁常见原因是多个事务以不同顺序加锁。比如事务A先更新表1再更新表2,事务B先更新表2再更新表1,两者就可能互相卡住。

解法有几种:

  • 强制所有事务按相同顺序访问表或行;
  • 尽量缩小事务范围,快速提交,减少持有锁的时间;
  • 在业务里加锁重试机制,死锁发生后重试整个事务;
  • 检查SQL的索引使用情况,避免全表扫描导致锁范围扩大。

死锁不是数据库故障,而是并发冲突的必然现象,设计上要接受它可能发生,并做好重试。

5.4 事务到底有没有提交成功

有些时候,程序日志显示事务执行完了,但数据没变化。先检查:

  • 是不是连的不是同一个MySQL实例,读写分离下你查的从库落后;
  • 事务是不是被框架级别静默回滚了;
  • 有没有在COMMIT之前抛异常,异常被吞了。

排查事务状态,最直接的是看information_schema.innodb_trx:

SELECT trx_id, trx_state, trx_started, trx_query FROM information_schema.innodb_trx;

trx_state为RUNNING表示事务还活着,如果长时间没提交,基本可以判定是有问题的事务。

5.5 常见问题速查表

现象可能原因排查方式
事务里的SQL没生效表引擎不支持事务SHOW TABLE STATUS
回滚没效果事务已被隐式提交检查事务中是否混入DDL
频繁锁等待隔离级别过高/没走索引查看慢查询和锁监控
事务开启过多连接池里的会话自动提交关闭后没重置检查连接池配置和回话状态
数据不一致读写分离,从库延迟检查主从延迟用时
死锁报错加锁顺序不一致统一访问顺序、加锁重试

6. 写在最后的实操心得

事务这个东西,概念上不难,真正难的是边界划分和并发控制。我见过太多系统出事,不是不懂ACID,而是把一堆网络请求、远程调用、消息发送都塞进了一个大事务里,事务长时间不提交,最后锁住了半张表。所以有个非常实用的建议:控制事务的粒度和时长,事务里尽量只保留数据库操作,别把外部调用放进来。

另一个建议是,给业务表设计时提前考虑好隔离级别和唯一约束。比如用唯一索引天然挡住重复提交,很多时候比锁和事务更高效。事务能保证你对数据库的操作安全,但保证不了业务逻辑本身是对的。

我这几年的体感是:把事务用好的核心不在于背下多少命令,而在于你在写SQL之前,先想清楚这句话会不会被并发影响、会不会产生中间状态、失败时能不能干净地退回去。带着这三个问题去写代码,很多坑还没踩就绕过去了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 15:19:49

AIR780E驱动安装避坑指南:USB串口识别、固件烧录与AT验证

简介:合宙4G模组AIR780E的驱动程序,是一份面向物联网嵌入式开发者的底层通信中间件,专注于CAT1 4G联网和GPS/GNSS定位的硬件控制。资源包为rar压缩格式,仅2个文件、4KB大小,分为实现文件与头文件:.c文件涵盖…

作者头像 李华
网站建设 2026/10/20 15:18:45

R语言字符串处理完全指南:从向量化到正则与文本清洗

1. R 里的字符串到底特殊在哪:先理解向量化的思维转换很多从 Python 或 JavaScript 转过来的朋友,刚开始用 R 做文本处理的时候,都会有一种"浑身难受"的感觉。拿 Python 来说,一个字符串就是一个个字符组成的有序序列&a…

作者头像 李华
网站建设 2026/10/20 15:17:23

深度学习量化交易的隐性失效与微观结构校准

简介:本资源是一套面向高校学生与AI初学者的深度学习量化交易实践项目,适用于毕业设计、课程设计及期末大作业等综合性实践场景,聚焦于用人工智能方法解决股票价格预测与自动化交易策略构建这一典型金融科技问题。压缩包共49个文件&#xff0…

作者头像 李华
网站建设 2026/10/20 15:16:53

Ascend高性能张量计算:Catlass算子库的模板化设计与AI Core优化实践

模块刚出来的时候,群里讨论热度很高。因为很长一段时间里,想在 Ascend AI 处理器上做高性能张量计算,基本只有一条路:从 CANN 的算子包里找现成的接口,找不到就自己写 TBE 算子或者自定义算子工程。自己写又绕不开硬件…

作者头像 李华
网站建设 2026/10/20 15:15:51

ASP+MSSQL多用户进销存源码实战:部署、二次开发与避坑指南

简介:这是一套基于ASP与MSSQL开发的Web进销存与仓库管理系统多用户版源码,面向中小企业、门店及需要搭建内部进销存平台的技术人员,可用于学习或二次开发一套完整的采购、销售、库存与财务一体化流程。压缩包为rar格式,大小约3.43…

作者头像 李华