下面使用的 MySQL 版本为 8.0
ACID 事务支持
MySQL 中的 ACID 是数据库事务(Transaction)的四大核心特性,它们共同保证了数据库操作的可靠性和一致性,尤其在并发场景和系统故障时至关重要。以下是对每个特性的详细解析:
- 原子性(Atomicity):事务中的操作要么全部完成,要么全部不执行(如转账时,扣款和收款款必须同时成功或同时失败)。
- 一致性(Consistency):事务执行前后,数据库从一个一致状态切换到另一个一致状态,一致状态指的是数据满足所有预设的规则,比如主键唯一、外键关联有效、字段值符合类型限制等。
- 隔离性(Isolation):多个并发事务同时执行时,彼此不会相互干扰,避免脏读、不可重复读、幻读等问题(MySQL 提供多种隔离级别,如读未提交、读已提交、可重复读、串行化)。
- 持久性(Durability):事务提交后,修改会永久保存,即使系统崩溃也不会丢失。
原子性(Atomicity)
- 定义:事务中的所有操作被视为一个不可分割的整体,要么全部成功执行,要么全部失败回滚,不存在 “部分完成” 的中间状态。
- 作用:防止因系统崩溃、网络中断等意外导致的数据不完整。
- MySQL 实现:
- 依赖 InnoDB 存储引擎的 undo log(回滚日志):事务执行时,InnoDB 会记录操作的反向逻辑(如插入记录时记录删除操作,更新记录时记录旧值)。
- 若事务执行中发生错误,InnoDB 通过 undo log 撤销已执行的操作,恢复到事务开始前的状态。
- 示例:银行转账(A 扣 100 元,B 加 100 元)。若 B 账户不存在导致失败,A 的扣款会被回滚,确保两人余额不变。
一致性(Consistency)
- 定义:事务执行前后,数据库从一个合法的一致状态转换到另一个合法的一致状态,即数据始终满足预设的业务规则和约束(如主键唯一、外键关联、字段校验等)。
- 作用:保证数据的逻辑正确性,与业务规则严格匹配。
- MySQL 实现:
- 依赖数据库约束(主键、外键、CHECK、NOT NULL 等)自动校验。
- 结合原子性、隔离性和持久性共同保障:原子性确保操作不部分执行,隔离性避免并发干扰,持久性确保正确状态被保存。
- 示例:
- 若表中设置 “年龄不能为负数” 的约束,事务尝试插入年龄为 -5 的记录时,会因违反约束而失败并回滚,保证数据始终符合规则。
- 转账前后,A 和 B 的总金额保持不变(一致状态)。
隔离性(Isolation)
- 定义:多个并发事务同时操作数据库时,每个事务的执行应不受其他事务的干扰,仿佛它们在独立运行。
- 作用:解决并发场景下的数据冲突,避免 “脏读”“不可重复读”“幻读” 等问题。
- MySQL 隔离级别(由低到高,安全性递增,性能递减):
- 读未提交(Read Uncommitted):
- 事务可读取其他未提交事务的修改。
- 问题:可能出现脏读(读取到未最终确认的数据)。
- 实现:不使用 MVCC,直接读取最新数据,可能出现脏读。
- 读已提交(Read Committed):
- 事务只能读取其他已提交事务的修改。
- 解决:避免脏读。
- 问题:可能出现不可重复读(同一事务内两次读取同一数据,结果不一致)。
- 实现:利用 MVCC 实现,每次查询都会生成新的 Read View,确保只能看到已提交的数据,解决了脏读问题。
- 可重复读(Repeatable Read)(MySQL InnoDB 默认级别):
- 事务执行期间,多次读取同一数据的结果始终一致(基于事务开始时的快照)。
- 解决:避免脏读和不可重复读。
- 问题:可能出现幻读(事务期间,其他事务插入新数据,导致 “多出来” 的数据)。
- 实现:基于 MVCC 实现,整个事务期间使用同一个 Read View,保证了事务内多次读取结果一致,解决了不可重复读问题。
- 串行化(Serializable):
- 最高隔离级别,事务完全串行执行(类似单线程),无并发干扰。
- 解决:所有并发问题,但性能最差。
- 实现:主要通过锁机制(表级锁)实现,完全禁止并发操作,不依赖 MVCC。
- 读未提交(Read Uncommitted):
- MySQL 实现:
- 依赖 锁机制(行级锁、表级锁)和 MVCC(多版本并发控制):
- 锁机制:通过锁定数据防止并发修改冲突。
- MVCC:为每个事务提供独立的数据快照,实现 “读写不加锁”,提升并发性能。
- 依赖 锁机制(行级锁、表级锁)和 MVCC(多版本并发控制):
持久性(Durability)
- 定义:事务一旦提交(
COMMIT
),其对数据的修改将永久保存,即使后续发生系统崩溃、断电等故障,数据也不会丢失。 - 作用:确保数据的长期可靠性,避免因硬件或软件故障导致的修改丢失。
- MySQL 实现:
- 依赖 InnoDB 的 redo log(重做日志):
- 事务执行时,修改先写入内存缓冲池(Buffer Pool),同时记录 redo log(顺序写入磁盘,性能高)。
- 事务提交时,redo log 被刷新到磁盘(
fsync
操作),确保即使内存数据丢失,也可通过 redo log 恢复。
- 配合 双写缓冲(Double Write Buffer) 解决部分页写入失败的问题,进一步增强持久性。
- 依赖 InnoDB 的 redo log(重做日志):
- 示例:事务提交后,即使数据库进程崩溃,重启后 InnoDB 会通过 redo log 恢复已提交的修改。
事务隔离级别如何选择
MySQL 事务主要解决的是并发场景下的数据一致性问题,以及系统故障时的数据可靠性问题,核心目标是通过 ACID 特性和隔离机制,避免因多事务并行执行或异常中断导致的数据错误。
并发事务导致的问题(隔离性需解决的核心问题)
当多个事务同时操作相同数据时,若缺乏隔离机制,会产生以下典型问题:脏读、幻读、不可重复读
1. 脏读(Dirty Read)
- 定义:一个事务读取到另一个未提交事务的修改数据。
- 危害:若未提交的事务最终回滚,读取到的数据是 “无效的临时数据”,导致业务逻辑错误。
- 示例:事务 A 向用户账户转入 100 元(未提交),事务 B 读取到账户余额增加 100 元并基于此进行后续操作;若事务 A 因错误回滚,事务 B 基于错误数据的操作会导致结果异常。
- 解决:通过 “读已提交” 及更高隔离级别,确保只能读取已提交事务的数据。
2. 不可重复读(Non-Repeatable Read)
- 定义:同一事务内,多次读取同一数据时,结果不一致(因其他事务修改并提交了该数据)。
- 危害:破坏事务内数据的稳定性,导致基于多次读取的业务逻辑(如统计、校验)出错。
- 示例:事务 A 第一次读取商品库存为 10 件,事务 B 购买 5 件并提交,事务 A 再次读取时库存变为 5 件,若 A 基于第一次的 10 件库存做判断(如允许购买 8 件),会导致超卖。
- 解决:通过 “可重复读” 及更高隔离级别,基于事务开始时的快照读取数据,确保多次事务内多次读取结果一致。
3. 幻读(Phantom Read)
定义:同一事务内,两次执行相同的查询语句,结果集行数不一致(因其他事务插入或删除了符合条件的记录)。
危害:导致事务对 “符合条件的记录总数” 判断错误,影响批量操作逻辑。
示例:事务 A 查询 “年龄 < 18 岁的用户” 有 5 人,并计划对这些用户执行操作;此时事务 B 插入 1 个新的未成年用户并提交,事务 A 再次查询时结果变为 6 人,若 A 基于最初的 5 人执行批量更新,会遗漏新插入的用户。
解决:通过 “串行化” 隔离级别(完全禁止并发插入),或 InnoDB 在 “可重复读” 级别下通过间隙锁(Gap Lock)减少幻读(但无法完全避免)。
事务隔离级别
读已提交
- 事务 A 中能看到其他已提交事务(如事务 B)修改的数据
- 例:事务 B 修改并提交后,事务 A 再次查询会得到最新数据
读未提交
事务 A 能看到其他未提交事务(如事务 B)修改的数据(脏读)
可重复读(InnoDB 默认的隔离级别)
- 事务 A 开启后,无论其他事务(如事务 B)是否修改并提交了数据,事务 A 中多次查询同一数据都会得到一致的结果(开启事务时的快照)
- 例:事务 B 修改并提交了数据,事务 A 中查询到的仍是事务 A 开启时的数据版本
串行化
事务执行时会对数据加锁,事务 B 需等待事务 A 完成后才能修改数据
MVCC 机制
MVCC(Multi-Version Concurrency Control,多版本并发控制) 是 InnoDB 实现高并发读写的核心机制,它通过为数据维护多个版本,允许读写操作不冲突(读不加锁,写不阻塞读),同时保证事务隔离性。
- 允许读操作(SELECT)不加锁,直接读取数据的历史版本,避免被写操作阻塞。
- 允许写操作(INSERT/UPDATE/DELETE)只锁定当前版本,不阻塞其他读操作。
- 在 “可重复读” 隔离级别下,保证事务内多次读取数据的结果一致。
实现原理
1. 数据行的隐藏列
- DB_TRX_ID:记录最后一次修改该数据行的事务 ID(64 位)。
- DB_ROLL_PTR:回滚指针,指向该数据行的 undo log 记录(用于查找历史版本)。
- DB_ROW_ID:若表未定义主键,InnoDB 会自动生成该列作为隐含主键。
2. 事务 ID(Transaction ID)
- 每个事务开始时,InnoDB 会分配一个唯一的递增事务 ID(由
innodb_max_trx_id
维护)。 - 事务对数据的修改(INSERT/UPDATE/DELETE)会将自身 ID 写入该行的
DB_TRX_ID
。
3. Undo Log(回滚日志)
- 事务修改数据时,InnoDB 会在 undo log 中记录数据的历史版本(通过
DB_ROLL_PTR
串联)。 - 不同事务修改同一行数据时,undo log 会形成一个版本链(最新版本在表中,历史版本在 undo log 中)。
4. Read View(读视图)
- 事务执行查询时,InnoDB 会生成一个 Read View,用于判断数据行的哪个版本对当前事务可见。
- Read View 包含 4 个核心参数:
m_ids
:当前活跃(未提交)的事务 ID 列表。min_trx_id
:m_ids
中的最小事务 ID。max_trx_id
:下一个将要分配的事务 ID(即当前最大事务 ID + 1)。creator_trx_id
:当前事务的 ID。
不同隔离级别的 MVCC 差异
- 读已提交(Read Committed, RC):
- 每次执行
SELECT
时,都会重新创建 Read View。 - 结果:只能看到事务提交前已提交的版本,可能出现 “不可重复读”(同一事务内两次查询结果不同)。
- 每次执行
- 可重复读(Repeatable Read, RR):
- 仅在事务第一次执行
SELECT
时创建 Read View,之后复用该视图。 - 结果:事务内多次查询看到的是同一版本快照,避免 “不可重复读”(MySQL 默认级别)。
- 仅在事务第一次执行
- 读未提交(Read Uncommitted):
- 不使用 MVCC,直接读取最新版本(可能看到未提交的脏数据)。
- 串行化(Serializable):
- 不依赖 MVCC,通过加锁强制事务串行执行。
MVCC 就像给数据自动生成 “历史快照”,写操作改新快照,读操作按规则读合适的旧快照,实现了 “读写互不干扰”,是 InnoDB 高效支持多事务并发的底层基石。
锁机制
锁是为了解决并发环境下资源竞争的手段
乐观锁
- 版本控制机制:
- 通过添加
version
字段记录数据版本 - 每次更新时将版本号加 1
- 更新条件中必须包含原始版本号的检查
- 通过添加
- 冲突检测:
- 执行
UPDATE
后通过查看影响行数判断是否有冲突 - 如果影响行数为 0,表示数据已被其他事务修改,更新失败
- 需要业务层处理冲突(通常是提示用户刷新重试)
- 执行
- 优点:
- 不需要加锁,并发性能好
- 不会产生死锁问题
- 适用于读多写少的场景
- 缺点:
- 存在 “ABA 问题”(数据从 A→B→A,版本号相同但实际已被修改)
- 冲突发生时需要业务层处理
- 不适合并发冲突频繁的场景
-- 首先需要在订单表中添加版本号字段用于乐观锁控制
ALTER TABLE orders ADD COLUMN `version` int(11) NOT NULL DEFAULT 0 COMMENT '版本号,用于乐观锁';
-- 场景:模拟两个用户同时操作同一个订单
-- 会话1:用户A尝试更新订单
-- 1. 查询订单信息,获取当前版本号
SELECT id, status, version FROM orders WHERE id = 1;
-- 假设查询结果:id=1, status=0, version=0
-- 2. 执行业务逻辑后,更新订单并检查版本号
UPDATE orders
SET status = 1,
pay_type = 2,
payment_time = NOW(),
version = version + 1 -- 版本号自增
WHERE id = 1 AND status = 3 AND version = 0; -- 条件中包含版本号检查
-- 3. 检查影响行数,判断更新是否成功
-- 如果影响行数为1,表示更新成功
-- 如果影响行数为0,表示数据已被其他事务修改,需要处理冲突
-- 会话2:用户B同时尝试操作同一个订单
-- 1. 查询订单信息(此时可能还是获取到version=0)
SELECT id, status, version FROM orders WHERE id = 1;
-- 假设查询结果:id=1, status=0, version=0
-- 2. 尝试更新订单(此时会话1可能已经提交,version已变为1)
UPDATE orders
SET status = 4,
note = '用户取消订单',
version = version + 1
WHERE id = 1 AND status = 3 AND version = 0;
-- 3. 此时会发现影响行数为0,说明更新失败
-- 需要提示用户"数据已被修改,请刷新后重试"
悲观锁
- 锁定机制:使用
SELECT ... FOR UPDATE
语句在查询时就对记录加锁,其他事务想要修改或锁定该记录必须等待。作用范围是整个事务,如果不在事务中执行,锁会在执行语句后立即释放,无法达到锁定资源的目的。 - 锁的范围:
- 会锁定满足条件的行记录
- 如果查询条件使用了索引,会只锁定符合条件的行
- 如果没有使用索引,可能会锁定整张表
- 使用注意事项:
- 必须在事务中使用(BEGIN/COMMIT 之间)
- 锁会在事务提交或回滚后自动释放
- 长时间持有锁会导致并发性能下降,应尽量缩短事务时间
- InnoDB 引擎默认是行级锁,效率相对较高
-- 场景:模拟用户支付订单,使用悲观锁防止并发问题
-- 会话1:用户开始支付订单
BEGIN; -- 开启事务
-- 使用FOR UPDATE进行悲观锁定,锁定ID为1的订单
SELECT * FROM orders
WHERE id = 1 AND status = 0 -- 0表示待付款状态
FOR UPDATE;
-- 此时其他事务将无法修改该订单,直到本事务提交或回滚
-- 执行业务逻辑:更新订单状态为已支付,记录支付时间等
UPDATE orders
SET status = 1, -- 1表示待发货
pay_type = 2, -- 2表示微信支付
pay_amount = total_amount - discount_amount + freight_amount,
payment_time = NOW()
WHERE id = 1;
-- 提交事务,释放锁
COMMIT;
-- 会话2:同时尝试操作同一个订单(会被阻塞直到会话1提交)
BEGIN;
-- 尝试查询并锁定同一个订单,此时会被阻塞
SELECT * FROM orders
WHERE id = 1
FOR UPDATE;
-- 只有当会话1提交后,会话2才能获得锁并执行以下操作
UPDATE orders
SET status = 4, -- 4表示已取消
note = '用户取消订单'
WHERE id = 1;
COMMIT;
锁粒度
表级锁 → 页级锁 → 行级锁
事务执行异常导致的问题(原子性、持久性需解决的问题)
事务执行过程中可能因系统故障(如断电、崩溃)、网络中断或业务错误(如违反约束)中断,需解决以下问题:
1. 部分执行问题(原子性需解决)
- 定义:事务中的操作未全部完成,仅执行了部分步骤,导致数据处于 “中间状态”。
- 危害:破坏业务规则的完整性,如转账时 “扣款成功但收款失败”,导致资金丢失。
- 解决:通过原子性保证,事务要么全部成功提交,要么通过 undo log 回滚到初始状态,避免中间状态。
2. 数据丢失问题(持久性需解决)
- 定义:事务已提交,但因系统崩溃(如内存数据未写入磁盘)导致修改丢失。
- 危害:已确认的业务操作(如订单支付成功)被撤销,引发用户纠纷。
- 解决:通过 redo log 机制,事务提交时先将修改记录写入磁盘日志,即使内存数据丢失,重启后可通过日志恢复已提交的修改。
3. 约束违反问题(一致性需解决)
- 定义:事务执行后的数据违反数据库约束(如主键重复、外键关联失效、CHECK 条件不满足)。
- 危害:破坏数据的逻辑合法性,如插入一个不存在的外键值,导致关联查询出错。
- 解决:通过一致性校验,事务执行过程中若违反约束,会立即终止并回滚,确保数据始终符合预设规则。
如何选择合适的存储引擎
MySQL 支持的存储引擎,详情查看官方文档

可以看到只有 InnoDB 支持事务、XA 分布式事务、Savepoints(存储引擎支持保存点,事务内部分段回滚的能力)

MySQL 使用 InnoDB 作为默认的存储引擎,因其全面支持 ACID 事务、行级锁与 MVCC 机制、外键约束及崩溃恢复能力,能同时满足数据可靠性、高并发性能和业务完整性需求,是大多数场景下的最优选择。
MySQL Log

- Redo Log 与 Undo Log 是 InnoDB 引擎专属日志,直接参与事务特性的实现(持久性和原子性)。
- Binlog 是 MySQL 服务器层日志,所有存储引擎均可使用,主要用于复制和恢复,与 InnoDB 的事务特性无直接关联,但在分布式事务或主从架构中需与 Redo Log 配合。
- 其他日志(错误日志、慢查询日志等)属于 MySQL 全局日志,用于监控和排查 InnoDB 相关问题,但不参与核心事务机制。
资料来源:
《高性能 MySQL(第3版)》