在MySQL里,事务并不是一句“要么全做要么不做”的口号,而是一组由存储引擎落地的机制。以InnoDB为例,它用重做日志、回滚段、锁系统和多版本并发控制,把原子性、一致性、隔离性、持久性真正变成了可观测、可调试的行为。搞清楚这些机制怎么配合,才能在实际业务里少写出脏数据。
一、原子性(Atomicity)与回滚段
原子性要求一个事务里的所有操作,要么全部生效,要么全部不生效。InnoDB实现原子性的核心是回滚段(rollback segment)和undo log。当你执行一条UPDATE时,引擎并不会直接覆盖旧数据,而是把修改前的数据镜像写入undo log,再改页内存。如果事务中途报错或者主动ROLLBACK,就按undo log反向生成回滚操作,把数据恢复成事务开始前的样子。
下面这段伪代码演示了原子性在应用层的直观表现:即使第二步失败,第一步的扣款也必须被撤销。
START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE id = 1; -- 假设下面这条因为余额不足或网络断开而失败 UPDATE account SET balance = balance + 100 WHERE id = 2; ROLLBACK; -- 第一步的扣款随回滚段被还原
需要注意的是,undo log不只是给回滚用,它还支撑了后面要讲的隔离性里的多版本读。原子性保证的是“单个事务内部不可见半成品”,而回滚能力正是建立在undo链的完整之上。
二、一致性(Consistency)由约束与事务共同维护
一致性常被误解成数据库自动帮你算对账,其实它更像是“事务执行前后,所有声明的规则都成立”。这些规则包括主键唯一、外键关联、NOT NULL、以及业务层面的总账平衡。MySQL通过约束在写入时校验,而更复杂的业务一致,需要开发者把相关操作放进同一个事务,靠原子性和隔离性兜底。
例如账户表要求余额不能为负,可以借助检查约束(MySQL 8.0支持)或者触发器。但跨表的“A减B加总额不变”,只能靠事务把两步包起来,再配合恰当的隔离级别防止并发篡改。
CREATE TABLE account ( id INT PRIMARY KEY, balance INT CHECK (balance >= 0) );
一致性不是孤立特性,它是原子性、隔离性、持久性共同作用后的结果状态。如果其他三个特性有漏洞,一致性就会被打破,比如脏写导致两笔转账同时扣了同一个余额。
三、隔离性(Isolation)与锁、MVCC
隔离性解决的是“多个事务并发时,彼此能不能看到对方的中间状态”。MySQL提供读未提交、读已提交、可重复读、串行化四种级别。InnoDB默认是可重复读,它用MVCC(多版本并发控制)让普通SELECT读快照,避免被别的事务未提交数据干扰;用行锁和间隙锁防止并发修改冲突。
以下示例展示两个会话在可重复读下的表现:会话B的修改在会话A的事务里不可见,直到A也提交。
-- 会话A START TRANSACTION; SELECT balance FROM account WHERE id = 1; -- 得到100 -- 会话B START TRANSACTION; UPDATE account SET balance = 50 WHERE id = 1; COMMIT; -- 会话A再次读 SELECT balance FROM account WHERE id = 1; -- 仍是100,靠undo快照 COMMIT;
隔离级别越高,一致性越好但并发越低。串行化会对读也加锁,基本退化为单线程;而读未提交可能出现脏读。实际系统常在可重复读基础上,用SELECT ... FOR UPDATE显式加锁处理库存扣减这类强一致场景。
四、持久性(Durability)与重做日志
持久性承诺一旦事务提交,结果就不会因宕机丢失。InnoDB的秘诀是写前日志(WAL):事务提交时,先把修改记入redo log并刷盘,再异步把脏页写回表空间。即使马上断电,重启后也能重放redo log恢复已提交数据。
下面示意了提交时的日志顺序,innodb_flush_log_at_trx_commit参数控制刷盘策略,设为1最安全。
-- 参数建议配置 SET GLOBAL innodb_flush_log_at_trx_commit = 1; START TRANSACTION; UPDATE account SET balance = balance - 10 WHERE id = 1; COMMIT; -- 此时redo log已落盘,断电也可恢复
持久性也有代价:每次提交刷盘会带来磁盘写放大。若业务能容忍秒级丢失,可放宽该参数提升吞吐,但必须清楚这是在拿持久性换性能。
五、ACID在实战中的配合
一个典型的电商下单,要扣库存、写订单、减余额。这三步必须包在同一事务里,靠原子性保证不完整就回滚;靠隔离性防止超卖;靠持久性保证支付成功后数据不丢;靠一致性约束保证库存不为负。只有把四个特性串起来看,才能写出靠谱的SQL。
当遇到死锁或长事务时,往往是锁粒度或隔离级别没选对。此时应检查事务是否过大、是否混入了网络调用,并用SHOW ENGINE INNODB STATUS观察锁等待,而不是盲目重试。
START TRANSACTION; SELECT stock FROM item WHERE id = 10 FOR UPDATE; UPDATE item SET stock = stock - 1 WHERE id = 10; INSERT INTO orders VALUES (NULL, 10, 1); COMMIT;
理解MySQL事务的ACID,本质是理解InnoDB如何用日志、锁和版本链把理论变成工程现实。带着这套视角去调优和排错,会比单纯背诵概念有用得多。