目录
引言
在数据库管理中,事务是一个不可或缺的核心概念。它确保了一系列数据库操作要么全部成功,要么全部失败,从而维护数据的完整性和一致性。尤其在处理银行转账、订单支付等业务场景时,事务的重要性不言而喻。
一、什么是事务?
事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。这些操作本身是独立的,但作为一个整体,它们必须满足 ACID 特性。
一个经典的事务例子是银行转账:
-
从账户 A 扣除 100 元。
-
向账户 B 增加 100 元。
这两个步骤必须作为一个不可分割的整体。如果第一步成功而第二步失败,将会导致严重的数据不一致。事务就是保证不会发生这种情况的机制。
二、事务的四大特性(ACID)
ACID 是衡量事务的四个核心准则:
-
原子性 (Atomicity)
-
核心:事务中的所有操作是一个不可分割的原子单位。要么全部执行成功(
COMMIT
),要么全部失败回滚(ROLLBACK
)。 -
MySQL 实现:通过 Undo Log(回滚日志) 实现。如果事务失败或执行
ROLLBACK
,InnoDB 引擎会利用 Undo Log 将数据恢复到事务开始前的状态。
-
-
一致性 (Consistency)
-
核心:事务执行前后,数据库必须从一个一致性状态转变到另一个一致性状态。这意味着数据必须满足所有预定义的规则,包括约束、级联、触发器等。
-
例如:转账前后,两个账户的总金额必须保持不变。这是原子性、隔离性和持久性共同追求的结果。
-
-
隔离性 (Isolation)
-
核心:数据库允许多个事务并发执行,但一个事务的执行不应影响其他事务。它规定了事务之间的可见性规则。
-
MySQL 实现:通过 锁机制 和 MVCC (多版本并发控制) 来实现。InnoDB 提供了不同的事务隔离级别供用户权衡“并发性能”和“数据一致性”。
-
-
持久性 (Durability)
-
核心:一旦事务提交(
COMMIT
),它对数据的修改就是永久性的,即使发生系统故障(如断电)也不会丢失。 -
MySQL 实现:通过 Redo Log(重做日志) 实现。修改数据时,会先写 Redo Log,再在合适的时间点将数据更新到磁盘。即使宕机,重启后也能根据 Redo Log 重新恢复已提交的事务数据。
-
三、MySQL 中的事务操作
MySQL 默认采用自动提交模式(AUTOCOMMIT=1
),即每条 SQL 语句都会被当作一个独立的事务自动提交。
要显式地控制事务,需要使用以下命令:
-- 1. 关闭自动提交,并开启一个事务(也可直接用 START TRANSACTION;)
SET AUTOCOMMIT = 0;
-- 2. 执行一系列SQL操作
UPDATE accounts SET balance = balance - 100 WHERE user_id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE user_id = 'B';
-- 3. 如果所有操作成功,提交事务,使修改永久生效
COMMIT;
-- 4. 如果中途发生错误,回滚事务,撤销所有修改
ROLLBACK;
-- 5. 恢复自动提交模式
SET AUTOCOMMIT = 1;
也可以使用 BEGIN 或 START TRANSACTION; 来显式开启一个事务,此时会自动暂停自动提交,直到遇到 COMMIT 或 ROLLBACK。
四、并发事务出现的问题
MySQL允许多个客户端同时连接。所以,当多个事务同时处理,就会产生脏读、不可重复读、幻读等并发事务问题。
- 脏读:读取到其它事务尚未提交的数据;
- 不可重复读:前后读取到的数据不一致;
- 幻读:前后读取到的记录数量不一致;
当多个事务并发执行时,会引发一些数据一致性问题。SQL 标准定义了四种隔离级别,级别越高,一致性越强,但并发性能越低。
1. 脏读(Dirty read)
脏读是指一个事务正在访问数据,并且对数据进行了修改,但是这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
2. 不可重复读(Unrepeatable read)
不可重复读是指在一个事务内,多次读取同一个数据。
在这个事务还没有结束时,另外一个事务也访问了该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
3. 幻读(Phantom read)
事务 A 按条件查询数据,期间事务 B 插入符合条件的新数据并提交,A 再次查询时出现 “新数据”。
例如:第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
五、 事务隔离级别
为了解决以上这些问题,标准 SQL 定义了 4 类事务隔离级别,用来指定事务中的哪些数据改变是可见的,哪些数据改变是不可见的。
MySQL 包括的事务隔离级别如下:
- 读未提交(READ UNCOMITTED):当一个事务还没有提交时,它修改的数据就可以被其它事务读取
- 读已提交(READ COMMITTED):当一个事务提交后,它修改的数据可以被其它事务读取
- 可重复读(REPEATABLE READ):一个事务的执行过程中,多次读取同一数据结果一致
- 串行化(SERIALIZABLE):串行化是事务隔离级别的最高等级。它的核心思想非常简单粗暴:强制所有事务按顺序一个接一个地执行,完全杜绝并行执行。
MySQL 的事务的隔离级别由高到低,分别为串行化,可重复读,读已提交,读未提交,低级别的隔离级别可以支持更高的并发处理,同时占用的系统资源更少。
隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
---|---|---|---|---|
读未提交 | √ | √ | √ | 性能最高,但可能读取到其他事务未提交的数据,几乎不用。 |
读已提交 | × | √ | √ | 只能读取已提交的数据。 |
可重复读 | × | × | √ | 同一事务内多次读取同一数据结果一致。 |
串行化 | × | × | × | 强制事务串行执行,性能最低,但完全隔离。 |
1. 读未提交 (READ UNCOMMITTED)
读未提交就是可以读到未提交的内容。如果一个事务读取到了另一个未提交事务修改过的数据,那么这种隔离级别就称之为读未提交。 在该隔离级别下,所有事务都可以看到其它未提交事务的执行结果。因为它的性能与其他隔离级别相比没有高多少,所以一般情况下,该隔离级别在实际应用中很少使用。
核心:事务可以读取其他事务尚未提交的修改。这是最低的隔离级别。
问题:可能发生脏读、不可重复读和幻读。
实现:几乎不加锁,直接读取内存中最新的数据页。
事务可读取其他未提交事务的修改。可能导致脏读、不可重复读、幻读。
2. 读已提交(READ COMMITTED,RC)
读提交就是只能读到已经提交了的内容。如果一个事务只能读取到另一个已提交事务修改过的数据,并且其它事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那么这种隔离级别就称之为读提交。
该隔离级别满足了隔离的简单定义:一个事务从开始到提交前所做的任何改变都是不可见的,事务只能读取到已经提交的事务所做的改变。 这是大多数数据库系统的默认事务隔离级别,但不是 MySQL 默认的。
核心:事务只能读取其他事务已经提交的修改。
问题:解决了脏读,但可能发生不可重复读和幻读。
MySQL 实现:通过 MVCC 实现。每次执行查询时都会生成一个独立的快照(Read View),确保每次读取到的都是已提交的数据。
事务只能读取其他已提交事务的修改。可避免脏读,但仍可能出现不可重复读、幻读。
3. 可重复读(REPEATABLE READ,RR)
可重复读是专门针对不可重复读这种情况而制定的隔离级别,可以有效的避免不可重复读。
在一些场景中,一个事务只能读取到另一个已提交事务修改过的数据,但是第一次读过某条记录后,即使其它事务修改了该记录的值并且提交,之后该事务再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据。那么这种隔离级别就称之为可重复读。
可重复读是 MySQL 的默认事务隔离级别,它能确保多个事务并发时,同一事务在读取数据过程中,看到同样的数据行。在该隔离级别下,如果有事务正在读取数据,就不允许有其它事务进行修改操作,这样就解决了可重复读问题。
核心:在一个事务内,多次读取同一数据的结果是一致的。
问题:解决了脏读和不可重复读,但可能发生幻读。
MySQL 实现:通过 MVCC 实现。在一个事务中,只在第一次执行查询时生成一个快照,后续所有读取都基于这个同一快照,从而保证结果可重复。InnoDB 通过 Next-Key Lock 算法在很大程度上避免了幻读。
MySQL InnoDB 默认隔离级别。事务期间多次读取同一数据结果一致,可避免脏读、不可重复读,通过 Next-Key Lock 机制避免幻读。
4. 串行化(SERIALIZABLE)
如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来。那么这种隔离级别就称之为串行化。
串行化是最高的事务隔离级别,主要通过强制事务排序来解决幻读问题。简单来说,就是在每个读取的数据行上加上共享锁实现,这样就避免了脏读、不可重复读和幻读等问题。但是该事务隔离级别执行效率低下,且性能开销也最大,所以一般情况下不推荐使用。
核心:强制所有事务串行执行,这是最高的隔离级别。
问题:解决了所有并发问题(脏读、不可重复读、幻读)。
实现:通过严格的锁机制(如读加共享锁,写加排他锁)来实现,导致事务只能一个一个依次执行,并发性能最低。
当多个事务对同一条数据进行读写操作时,通过加锁的方式,强制事务串行执行。可避免所有并发问题,但并发性能极差。