MySQL数据库——事务介绍

大家好,这里是编程Cookbook。本文详细介绍MYSQL的事务,包括ACID、隔离级别、以及如何解决并发带来的问题。
MySQL事务是对数据库操作的一组处理逻辑单元,通常包括一系列的SQL语句,事务保证了这些语句要么全部成功执行,要么全部失败后回滚。
事务的特性:ACID
MySQL中的事务管理机制符合ACID特性,确保数据的一致性、可靠性和完整性。分别是:
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔离性(Isolation)
- 持久性(Durability)
1. 原子性(Atomicity)
定义:
- 事务中的所有操作要么全部完成,要么全部不做,不能只执行部分操作。换句话说,事务是“原子”级的,事务的操作不可分割。
- 如果事务在执行过程中发生错误,所有已执行的操作都会回滚,数据会恢复到事务开始之前的状态。
实现原理:
原子性通过 undo log 来实现。事务执行过程中,如果发生错误或用户执行了回滚(rollback),系统会通过 undo log 将数据恢复到事务开始前的状态,确保事务不会对数据库造成不一致。
示例:
假设有一个事务 A 需要从账户 A 扣款,并将款项转账到账户 B。事务 A 的两条语句:
- 从账户 A 扣款
- 向账户 B 添加款项
如果第二条语句失败,第一条语句所做的扣款操作会通过 undo log 回滚,保证两者操作要么都成功,要么都失败。
2. 一致性(Consistency)
定义:
- 事务开始前和结束后,数据库的状态必须是一致的。在事务执行前后,数据库的约束、触发器等规则必须保持不变。
- 数据库的任何操作都不会导致数据的不一致性。比如在事务执行完毕后,所有的约束(如外键、主键等)仍然有效。
实现原理:
一致性 的实现不仅仅依赖于 undo log,它还需要数据库的 数据约束(如主键、外键约束)来确保数据正确。事务的 隔离性 也很重要,保证并发操作不会破坏数据一致性。另外,redo log(重做日志) 确保已提交事务的数据不会丢失。原子性、隔离性、持久性是一致性依赖的基石
。
示例:
假设一个转账操作的事务(从账户 A 转账到账户 B),事务包含以下步骤:
- 从账户 A 扣款 100 元。
- 将 100 元存入账户 B。
如果事务的第二步(存入账户 B)由于某种原因失败(例如,账户 B 的余额为负,违反了数据库的约束),那么:
- 数据一致性保证:即使在扣款时修改了账户 A 的余额,事务也不会提交。通过 undo log,账户 A 的余额会被恢复到原始状态。
- 由于发生了错误,事务会被 回滚,从而确保数据库回到一个一致的状态,即 A 和 B 账户的余额都没有被错误地修改。
3. 隔离性(Isolation)
定义:
- 隔离性要求多个并发事务之间应该相互隔离,一个事务不应该被其他事务的操作干扰。
- MySQL提供不同的事务隔离级别,用以控制并发事务之间的可见性。
实现原理:
隔离性通过 锁机制 和 MVCC(多版本并发控制)来实现。锁机制通过加锁操作来确保数据的安全性,而 MVCC 通过为每个事务生成一个快照来避免事务间的数据干扰。
隔离性主要通过以下两种机制来实现:
-
锁机制:
- 锁是最常见的隔离手段。通过对数据的加锁,可以确保在一个事务操作数据时,其他事务无法同时对同一数据进行修改或读取。锁机制常见的类型包括 共享锁(S锁) 和 排他锁(X锁),它们分别用于读操作和写操作。
- 锁分为不同的粒度,常见的锁包括:行锁、表锁、页锁等。行锁是最细粒度的锁,可以在行级别加锁,允许更多的并发操作。
-
MVCC(多版本并发控制):
- MVCC 是一种通过保存数据的多个版本来实现事务隔离性的方法。在使用 MVCC 的数据库系统中,每次事务对数据的修改都会产生一个新的数据版本,事务只能看到其开始时的快照版本(称为 一致性视图)。
- MVCC 通过时间戳或版本号来区分不同的事务版本,保证一个事务只能看到在它开始时已经提交的数据,而不能看到其他事务正在修改的数据。
- 常见的 MVCC 实现方式是:在每条数据记录上添加事务标识符或版本号,标明该记录是由哪个事务修改的,哪些事务已经提交。
示例:
假设我们有两个事务 A 和 B,它们都需要操作数据库中的同一行数据,事务 A 执行了更新操作,而事务 B 正试图读取该行数据。若没有隔离性,事务 B 可能会读取到事务 A 尚未提交的修改(即 脏读,除此之外还存在避免不可重复读和幻读等并发问题)。这种情况下,事务 B 将会基于不一致的数据做出决策,最终可能导致数据的不一致性。
4. 持久性(Durability)
定义:
- 一旦事务被提交,它对数据库的更改是持久的,即使数据库崩溃,数据也不会丢失。
- 一旦事务提交,所有的变更都会被永久写入磁盘。
实现原理:
持久性通常通过 redo log(重做日志)来实现。事务提交时,数据库系统会将事务的所有操作记录到 redo log 中。即使系统崩溃,redo log 会提供足够的信息来将事务的数据恢复到提交时的状态。只要 redo log 被成功持久化到磁盘,系统重启后就可以利用该日志将未持久化的数据恢复。
示例:
当事务 A 完成转账操作并提交时,数据库将操作记录到 redo log 中。如果系统崩溃,只要 redo log 已经持久化,数据库就能在重启后恢复事务提交时的数据状态。
事务并发带来的问题
在数据库的并发事务处理中,多个事务同时操作相同的数据,可能会导致以下问题:
1. 更新丢失 (Lost Update)
定义:
当事务 A 和事务 B 并发更新同一行数据时,两个事务可能互相不知道对方的存在,从而导致一个事务的修改覆盖另一个事务的修改,丢失其中一个事务的更新。
示例:
- 事务 A 修改了
account_balance
为 1000 元,事务 B 也修改了account_balance
为 1200 元。假设事务 A 在事务 B 提交前提交,事务 B 的更新就会被覆盖,导致丢失事务 A 的修改。 - 解决方案:
使用行锁(如行级锁或乐观锁)来确保并发操作的数据不被覆盖。
2. 脏读 (Dirty Read)
定义:
脏读发生在事务 A 读取了事务 B 未提交的数据,而事务 B 后续回滚(rollback)时,这些数据并没有被持久化到数据库中,因此事务 A 读取到的数据是“脏”的,不一定是最终有效的数据。
示例:
- 事务 A 在事务 B 提交之前读取了事务 B 修改的数据。如果事务 B 回滚,事务 A 就读取到了一个已经不存在的数据,可能导致错误的后续操作。
- 解决方案:
使用较高的隔离级别(如 读已提交 或 可重复读)来防止读取未提交的数据。
3. 不可重复读 (Non-Repeatable Read)
定义:
不可重复读发生在同一事务内
,执行相同的查询
时,查询记录中的值发生变化
。事务 A 在读取某数据时,该数据可能会被事务 B 修改(由UPDATE引起
)。当事务 A 在后续查询该数据时,读到的数据已经发生变化,导致事务 A 读取到的数据不可重复。
示例:
- 事务 A 在开始时读取了某个账户的余额(例如 1000 元)。然后,事务 B 修改了该账户余额(如变为 1200 元),并提交。再回到事务 A 中读取该账户余额时,发现余额已经变了(1200 元),与第一次读取时的值不一致。
- 解决方案:
使用 可重复读 隔离级别来避免数据在同一事务
中的多次读取结果发生变化,或者使用锁机制保证在事务执行期间数据的一致性。
4. 幻读 (Phantom Read)
定义:
幻读是指在同一个事务
内,执行相同的查询
时,查询结果集的条数发生变化
。这通常发生在其他事务对数据的插入(INSERT)或删除(DELETE)操作导致查询的结果集条数变动。
示例:
- 事务 A 在查询账户余额大于 1000 元的账户时,得到一组结果(如 3 条记录)。随后,事务 B 插入了一条余额大于 1000 元的账户记录,并提交。再回到事务 A 查询时,发现查询结果集条数已经变成 4 条,这就是幻读。
- 解决方案:
使用 串行化 隔离级别来完全避免幻读问题,或者结合 Gap 锁 和 Next-Key Lock 机制来防止其他事务对查询数据范围的插入操作。
总结
事务并发时出现的问题会导致数据不一致或错误操作,理解这些并发问题的原因和解决方案是数据库管理和优化的重要部分。通过合理的隔离级别和锁机制,可以有效地避免这些问题,提高数据库的并发处理能力和数据一致性。
问题类型 | 定义 | 解决方案 |
---|---|---|
更新丢失 | 事务 A 和 B 更新同一数据时,丢失某一事务的更新 | 使用行锁(行级锁或乐观锁) |
脏读 | 事务 A 读取了事务 B 未提交的数据 | 使用 读已提交 或 可重复读 隔离级别 |
不可重复读 | 事务 A 内多次读取同一数据,读到的数据发生变化 | 使用 可重复读 隔离级别或锁机制 |
幻读 | 事务 A 在同一查询条件下,查询结果集条数发生变化 | 使用 串行化 隔离级别,或结合 间隙锁 和 Next-Key Lock |
通过合理选择隔离级别和使用锁,数据库能够有效避免并发事务带来的这些问题。
事务的隔离级别
回顾上面的并发问题:
- 更新丢失:通常使用锁解决,常见的锁解决方案包括:
- 行级锁:确保同一行数据只有一个事务能修改,避免了并发更新的冲突。
- 悲观锁:通过加锁确保数据在事务执行期间不被其他事务修改,从而避免更新丢失。
- 乐观锁:通过版本号或时间戳机制,检查数据是否被其他事务修改,避免丢失更新。
- 串行化隔离级别:强制事务顺序执行,避免并发更新。
剩下的三个并发问题,通常通过事务的隔离级别解决。回顾问题:
- 脏读(Dirty Reads):由于其他事务可以读取到未提交的修改数据,可能会读取到不一致或临时的数据。如果原事务回滚,这些读取的数据将是无效的,导致脏读。
- 不可重复读(Non-repeatable Reads):如果一个事务读取了某条数据,另一个事务修改并提交了这条数据,那么第一次读取的数据在第二次读取时将不同。
- 幻读(Phantom Reads):由于事务不加锁,可能会导致查询结果
条数
的变化,新增、删除的数据会影响查询的结果。
事务的隔离性(Isolation)是数据库系统在处理并发事务时所采用的机制,确保不同事务之间不会相互干扰,避免数据的冲突和不一致。为了解决事务并发操作时可能出现的问题(如脏读、不可重复读和幻读),数据库系统提供了四个隔离级别。每个级别都有不同的影响和性能 trade-off。
以下是四个隔离级别的详细介绍,以及它们如何解决脏读、不可重复读和幻读的问题:
1. 读未提交(Read Uncommitted)
读未提交是最弱的隔离级别,事务对数据的修改在未提交前就可以被其他事务读取。即使事务正在执行未提交的数据更新,其他事务也能读取到这些数据。
- 没有加锁,数据是“裸奔”的,事务可以读取未提交的数据。性能最好,但没有隔离。
适用场景:
- 读未提交通常不推荐使用,因为它可能导致严重的数据一致性问题,特别是在并发环境下。
2. 读已提交(Read Committed)
读已提交的隔离级别保证一个事务只能读取到其他事务已提交的数据。也就是说,事务只能看到已提交的修改,不允许读取到未提交的数据。
- 事务提交后才允许读取数据,避免脏读。性能适中,解决了脏读问题,但不能保证可重复读和幻读。
适用场景:
- 适用于要求较高并发、并且对数据一致性要求不是特别严格的场景。
3. 可重复读(Repeatable Read)
可重复读是MySQL 的默认隔离级别
。确保在同一个事务中,多次读取同一数据时,结果是相同的。即,在事务执行期间,数据不会被其他事务修改或删除,保证查询结果的一致性。
理论上,只解决了脏读和不可重复读,仍然无法完全避免幻读。
实际上,可重复读(Repeatable Read)隔离级别在MySQL中默认是能够解决幻读的。在MySQL的实现中,通过行锁、MVCC(多版本并发控制)以及Next-Key Lock机制,MySQL的可重复读隔离级别实际上能够有效地防止幻读。
适用场景:
- MySQL 的默认级别,适用于大多数场景,能够提供较好的并发性能和数据一致性保障。适用于中等一致性需求的场景,如订单处理、银行事务等。
4. 串行化(Serializable)
串行化是最高级别的隔离级别,它通过强制事务排队执行,完全消除了并发访问的数据的干扰。所有事务按顺序执行,相当于将事务串行化,没有并发操作。
-
最高隔离级别,强制串行执行事务,完全避免脏读、不可重复读和幻读,但性能最低,几乎没有并发。
-
所有事务串行执行,事务在读取时加共享锁,在写入时加排他锁,保证完全的隔离性,性能最差,基本不会使用。
- 所有的 SELECT 查询都会隐式地转化为 SELECT ... FOR SHARE,即加共享锁。
- 写操作加排他锁,会阻塞其他事务的读写。
适用场景:
- 多用于需要严格一致性和防止任何并发问题的场景,如财务系统、关键数据的处理。
隔离级别总结
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 可能(可以通过加锁避免) |
串行化 | 不可能 | 不可能 | 不可能 |
- 读未提交:事务没有加锁,因此没有隔离效果。事务可以读取未提交的数据,也不会阻塞其他事务的读写操作。
- 读已提交:事务读取的是其他事务已经提交的数据,通过MVCC,解决脏读问题,但仍然可能发生不可重复读和幻读。事务写入时会加排他锁,读时不加锁。
- 可重复读:通过MVCC,避免脏读和不可重复读,通过间隙锁(Gap Lock)和Next-Key 锁(行锁 + 间隙锁)避免幻读。MySQL 默认使用此隔离级别。
- 串行化:所有事务串行执行,事务在读取时加共享锁,在写入时加排他锁,保证完全的隔离性,性能最差,基本不会使用。
选择合适的隔离级别
在实际应用中,根据系统的需求进行权衡选择适当的隔离级别:
- 如果系统要求高并发,性能为首要考虑,可以选择 读已提交 或 可重复读。
- 如果数据一致性要求非常严格,并且并发性能不是重点,可以选择 串行化。
各隔离级别中具体解决并发问题的方案
- 读已提交:通过MVCC机制来避免脏读问题。
- 可重复读:
- 通过 MVCC 机制 来避免脏读问题。
- 使用 MVCC 机制 来解决不可重复读问题。
- 使用了Next-Key Lock(行锁和间隙锁的组合) 解决幻读问题。
- 串行化:通过强制事务顺序执行,完全避免了脏读、不可重复读和幻读,是最强的隔离级别。
如何解决脏读
在可重复读和读已提交隔离级别下,MySQL使用了MVCC(多版本并发控制)。通过MVCC,每个事务会看到一个一致的数据库快照,而不会直接读取到其他事务未提交的更改。具体来说:
- 当事务A读取某行数据时,事务A会看到该行数据在事务A开始时的版本,即使其他事务正在修改这行数据。
- 如果事务B对某行数据进行了修改并且尚未提交,事务A也无法看到这些未提交的更改。因此,事务A无法读取到事务B的脏数据。
- 只有在事务B提交之后,事务A才能看到事务B所做的更改。
这种方式有效避免了脏读,因为事务A永远只能看到已经提交的数据。
如何解决不可重复读
在可重复读隔离级别下,MySQL使用行锁(Record Lock) 来解决不可重复读问题。具体来说:
- 当事务A读取某行数据时,MySQL会为该行数据加上共享锁(S锁)。这意味着,其他事务不能修改这行数据,直到事务A结束。
- 其他事务可以对该数据行进行读取操作,但不能修改该数据行,直到事务A提交或回滚。
- 如果事务B想要修改这行数据,它必须等到事务A提交后才能执行修改。
这样,在事务A的整个执行过程中,它对同一行数据的读取始终是相同的,避免了不可重复读的问题。
如何避免幻读
虽然可重复读隔离级别能够防止脏读和不可重复读,但它本身不能完全避免幻读。为了避免幻读,MySQL使用了Next-Key Lock(行锁和间隙锁的结合)的机制来加强对数据的保护。具体来说:
- 间隙锁:假设事务A执行查询
SELECT * FROM accounts WHERE balance > 1000
,查询返回了三行数据。MySQL会锁住这三行数据,并且锁住它们之间的“间隙”。即使事务B插入了一条满足查询条件的新数据(如balance = 1500
),它也不能被插入到事务A的查询范围内。 - Next-Key Lock:是行锁和间隙锁的结合。MySQL通过锁住符合查询条件的数据行以及数据行之间的间隙区域,防止事务B插入或删除数据行,从而确保事务A在查询期间的结果集不会发生变化。
举个例子,如果事务A查询出balance > 1000
的所有行并且这些行都被锁住,事务B就不能插入balance = 1500
的新记录,因为插入的间隙会被锁住。这样,事务A的查询结果集在整个事务期间保持一致,避免了幻读。
事务的基本结构
事务的四大核心操作
START TRANSACTION; -- 事务开始(或 BEGIN)
[SQL 操作1] -- 增删改查(DML)
[SQL 操作2]
SAVEPOINT sp1; -- 可选:设置保存点(部分回滚用)
ROLLBACK [TO sp1]; -- 回滚(或回滚到保存点)
COMMIT; -- 提交
例子:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;
SAVEPOINT before_update_order;
UPDATE orders SET status = 'paid' WHERE order_id = 100;
-- 若订单更新失败:
ROLLBACK TO before_update_order; -- 仅回滚订单操作,保留账户扣款
COMMIT; -- 最终提交账户扣款
关键概念解析
-
BEGIN;
- 显式声明一个事务的开始
- 其他写法(数据库依赖):
START TRANSACTION;
(MySQL/PostgreSQL)- 隐式事务(如SQL Server默认每条语句都是独立事务)
-
COMMIT;
- 提交事务,使所有修改永久生效
- 只有执行COMMIT后,其他事务才能看到本事务的修改
-
ROLLBACK;
- 撤销所有未提交修改
- 释放所有锁
- 可回滚到指定保存点(如ROLLBACK TO sp1)
- 详细介绍参考后续
-
事务生命周期
graph LR A[开始] --> B[BEGIN] B --> C[SQL操作] C --> D{成功?} D -->|是| E[COMMIT] D -->|否| F[ROLLBACK]
ROLLBACK
的作用和必要性
ROLLBACK
是事务安全的重要保障机制,但它的使用方式分为两种:
显式回滚(手动控制)
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 业务逻辑检查
IF some_condition_failed THEN
ROLLBACK; -- 手动回滚
ELSE
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
END IF;
隐式回滚(自动触发)
当出现错误时,数据库会自动回滚:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 假设这句出错(如违反约束)
UPDATE accounts SET balance = balance + 100 WHERE id = 999; -- 不存在的ID
-- 数据库会自动回滚整个事务,无需显式ROLLBACK
何时需要显式使用 ROLLBACK
?
场景 | 是否需要显式ROLLBACK | 示例 |
---|---|---|
业务逻辑验证失败 | ✅ 需要 | 余额不足时手动回滚 |
数据库错误(如约束违反) | ❌ 自动回滚 | 主键冲突等 |
连接中断 | ❌ 自动回滚 | 网络断开 |
程序异常退出 | ❌ 自动回滚 | 应用崩溃 |