先说一次真实的线上事故。业务方凌晨两点发来消息:活动商品明明有库存,用户下单却频繁报“库存不足”。查日志发现同一件商品的库存字段被多个并发事务覆盖,两个扣减操作同时读到旧值,导致最终库存比实际销售多扣了不少。这种场景就是典型的数据一致性问题,也是MySQL事务和锁机制存在的根本意义。
这篇文章围绕MySQL事务与锁机制展开,重点拆解数据一致性是怎么被破坏的、隔离级别如何取舍、InnoDB的锁到底锁什么、MVCC和当前读如何配合,以及应用层事务注解和死锁排查的完整链路。适合正在准备数据库方向面试的后端开发、刚接手线上MySQL的DBA,以及所有被“数据对不上”折磨过的同学。
1. 先看清数据一致性从哪里被破坏的
很多人背过ACID,知道事务有原子性、一致性、隔离性、持久性,但一遇到真实并发场景就懵。问题不在于不懂概念,而在于没有把“不一致”具体到某一种可复现的操作序列上。
1.1 一个并发扣库存的丢失更新现场
假设有一张库存表:
sql复制CREATE TABLE inventory (
product_id INT PRIMARY KEY,
stock INT NOT NULL
) ENGINE=InnoDB;
INSERT INTO inventory VALUES (1001, 10);
两个用户同时下单购买同一商品,后台代码各自执行:
sql复制UPDATE inventory SET stock = stock - 1 WHERE product_id = 1001;
直觉上会觉得两条UPDATE是先后执行的,最终库存从10变成8。但真实数据库里两个事务可能这样交错执行:
| 时间线 | 事务A | 事务B | 库存实际值 |
|---|---|---|---|
| T1 | BEGIN | 10 | |
| T2 | BEGIN | 10 | |
| T3 | 读到stock=10 | 10 | |
| T4 | 读到stock=10 | 10 | |
| T5 | stock=9,提交 | 9 | |
| T6 | stock=9,提交 | 9 |
两个事务都认为自己的扣减是成功的,但最终库存只减了1,而不是2。更隐蔽的是,如果程序里先查询库存再在Java代码里做减法,最后再UPDATE,问题会进一步放大:
java复制// 典型的错误写法
int stock = selectStock(productId); // SELECT stock FROM inventory WHERE product_id = ?
if (stock <= 0) { throw new RuntimeException("库存不足"); }
updateStock(productId, stock - 1); // UPDATE inventory SET stock = ? WHERE product_id = ?
这就是丢失更新。两个事务基于同一个旧值计算结果,后提交的一方覆盖前提交的一方的结果。要解决它,最直接的方法就是让两个事务串行执行,或者让后执行者基于最新值做运算。MySQL的锁机制和乐观锁方案都是为了处理这类场景。
1.2 脏读、不可重复读与幻读到底长什么样
丢失更新之外,还有三类经典异常。
脏读是事务A读到了事务B尚未提交的数据。如果事务B回滚,事务A就用了一个根本不存在的结果做后续判断。比如商品库存从10改成5,事务B还没有提交,事务A就按5去生成采购计划,结果B回滚,A就白忙一场。
不可重复读是同一事务内两次查询同一行,得到不同的结果。事务A第一次查到单价100元,事务B把单价改成了80元并提交,事务A再查一次变成80元。A在同一个事务里看到同一个商品两个价格,业务报表就没法做了。
幻读针对的是范围查询。事务A执行了SELECT * FROM orders WHERE status = '待支付',第一次查到100条,事务B插入了一条新的待支付订单并提交,事务A再查一次变成101条。多出来的那条记录就像幻觉一样。
这三类异常加上丢失更新,构成了并发事务需要解决的四大问题。不同隔离级别对它们的容忍程度不同,所以不能笼统地说“开了事务就一定安全”,要看你选了什么隔离级别。
1.3 一致性的边界:单库事务不等于分布式一致
MySQL的ACID一致性是在单个数据库实例内保障的。它通过回滚机制、约束条件和锁来保证事务从一个合法状态切换到另一个合法状态,不破坏业务规则。但如果你的事务涉及多个MySQL实例、MySQL和Redis、或者订单服务加库存服务两个独立应用,单库事务就管不住了。
这也是为什么热词里总会出现“分布式事务”“分布式事务一致性”。它们解决的是跨资源的一致性问题,需要TCC、SAGA、本地消息表或最终一致性方案。理解MySQL事务和锁机制时,务必先把边界画清楚:InnoDB的锁只对本实例内的行和表生效,跨库跨服务的原子性超出了它的管辖范围。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 隔离级别的取舍:从读未提交到可串行化的真实代价
事务隔离级别是连接“一致性需求”和“数据库性能”的中间层。SQL标准定义了四个级别,但不同数据库实现并不完全一致,MySQL的InnoDB在默认隔离级别下还有自己的特殊行为。
2.1 四种隔离级别解决了什么、漏掉了什么
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 直接读最新版本,不加锁 |
| READ COMMITTED | 不可能 | 可能 | 可能 | 每次读都生成新的快照 |
| REPEATABLE READ | 不可能 | 不可能 | 部分可能(InnoDB下基本不可能) | 首次读生成快照,事务 |
