什么是事务?
事务(transaction)是作为一个不可分割的逻辑单元而被执行的一组 SQL 语句。
为什么需要事务?
为什么需要事务,我们都会有这个疑问。
无事务的银行转账系统
那么,下面来看一个典型的财务转账的例子,即把钱从一个账户转到另一个账户。
假设Jack给Lucy开了一张$100的支票,Lucy拿着这张支票去取钱,取钱操作放到程序中会变成这样:
Jack账户减少$100Lucy账户增加$100
将其转换为 SQL 就是这样(不考虑顺序):1
2UPDATE account SET balance = balance - 100 WHERE name = 'Jack';
UPDATE account SET balance = balance + 100 WHERE name = 'Lucy';
在正常情况下,转账操作应该没有问题,但是就怕意外呀!万一银行的转账系统在这两条语句执行时发生了崩溃,整个操作将不再完整,根据先执行的是哪一条语句,会出现以下情形:
Jack的账户少了$100而Lucy的账户金额未增加Lucy的账户多了$100而Jack的账户金额未减少
我们知道,以上两种情况都是不正确的!
事务的提交回滚机制
从前面的例子我们得知:并非所有的语句每次都能执行成功,执行错误时有些语句却会对数据产生永久性的影响,使得新旧数据混杂在一起,导致数据库呈不稳定状态。
如有必要,这组 SQL的执行结果应可以被撤销。
commit)和回滚(rollback)机制来解决这种错误。此机制确保了尚未全部完成的操作不会影响到数据库,即新旧数据不再混杂以使数据库呈不稳定状态。
引入事务后的银行转账系统
现在我们知道:事务机制提供的回滚操作可以正确地处理好前面的问题,把发生错误之前已经执行完的语句产生的结果撤销掉即可。
那么,使用事务来重新执行下转账操作吧:1
2
3
4mysql> START TRANSACTION;
mysql> UPDATE account SET balance = balance - 100 WHERE name = 'Jack';
mysql> UPDATE account SET balance = balance + 100 WHERE name = 'Lucy';
mysql> COMMIT;
事务的其他用途
事务的另一种用途是确保某个操作所涉及的数据行在你正在使用它们时不会被其他客户端修改。
比如,MySQL 在执行每一条 SQL 语句时,都会自动地对该语句所涉及的资源进行锁定,以避免各语句之间相互干扰。
但是,这仍不足以保证每一个数据库操作总是能达到预期的结果。要知道,有些数据库操作需要多条语句才能完成,而在此期间,不同的客户就有可能相互干扰。
只有通过把多条语句定义给一个执行单元(即事务),事务机制才可防止在多客户环境里可能发生的并发问题。
事务特性
数据库事务有严格的定义,它必须同时满足 4 个特性:
- 原子性(
Atomic):事务是一个不可分割的整体,其操作要么全执行,要么全不执行。换而言之,不允许只完成一部分事务,可以避免只执行这些操作的一部分而带来的错误。原子性要求事务必须被完整执行。 - 隔离性(
Isolated):亦称独立性,不同的事务之间不应相互影响。换而言之,当两个或多个事务并发执行时,为了保证数据的安全性,会将一个事务内部的操作与其他事务的操作隔离起来,不被其他正在执行的事务看到,它们互不干扰。 - 持久性(
Durable):亦称永久性,事务成功执行之后,它的影响将被永久的记录到数据库中。这意味着对即使系统或介质发生故障,对数据库操作所修改的数据都应该在磁盘上保留下来。 - 一致性(
Consistent):一个事务执行前后,数据库数据的状态必须要保持一致。数据库的一致性状态应该满足模式锁指定的约束,那么在完整执行该事务后数据库仍然处于一致性的状态。为了维护所有数据的完整性,在关系型数据库中,所有规则必须应用到事务的修改上。数据库的一致性状态由用户来负责,由并发控制机制实现。例如银行转账,转账前后两个账户金额之和应保持不变。由此并发操作带来的数据不一致性包括丢失数据修改、读脏数据、不可重复读、产生‘幽灵’数据。
在这些事务特性中,数据“一致性”是最终目标,其他特性都是为达到这个目标而采取的措施、要求或手段。
日志文件可进一步分为两类:
undo log:undo log记录了数据库变化的每一个动作,若数据库在一个事务中执行一部分操作后发生错误退出,数据库即可根据undo log撤销已经执行的操作。redo log:对于已经提交的事务,即使数据库崩溃,在重启时数据库也能够根据redo log对尚未持久化的数据进行相应的重执行操作。
当多个事务试图对相同的数据进行操作时,只有持有锁的事务才能操作数据,直到前一个事务完成后,后面的事务才有机会对数据进行操作。Oracle 数据库还使用了数据版本的机制,在回滚段为数据的每个变化都保存一个版本,使数据的更改不影响数据的读取。
事务中的并发问题
一个数据库可能拥有多个访问客户端,这些客户端都可用并发的方式访问数据库。因此,数据库中的相同数据可能同时被多个事务访问,若没有采取必要的隔离措施,就会导致各种并发问题,破坏了数据的完整性。
多个事务同时执行时可能存在的这些问题,大致可以归结为五类:
- 丢失更新(
lost update) - 脏读(
dirty read) - 不可重复读(
nonrepeatable read) - 覆盖更新(
cover update) - 幻影数据行(
phantom row)
下面分别通过实例讲解引发问题的场景。
丢失更新
指撤销一个事务同时把另一个事务已提交更新过的数据覆盖掉了。
举个栗子:A 和 B 事务并发执行, B 事务执行更新后提交; A 事务在 B 事务更新后,A 事务对该行数据更新操作后回滚,最后两次更新操作都丢失了。
| 时间 | 取款( A 事务) | 转账( B 事务) |
|---|---|---|
| T1 | 开始事务 | |
| T2 | 开始事务 | |
| T3 | 查询余额为 1000 元 | |
| T4 | 查询余额为 1000 元 | |
| T5 | 汇入 100 元,余额 1100 元 | |
| T6 | 提交事务 | |
| T7 | 取出 100 元,余额 900 元 | |
| T8 | 撤销事务,余额恢复为 1000 元 |
最终结果:客户损失 100 元
脏读
指某个事务所作出的修改在它尚未被提交时就可以被其他事务看到。
此时,其他事务会认为数据行已经被修改了,但对数据行作出修改的那个事务还有可能会被回滚,这将导致数据库的数据发生混乱。
| 时间 | 取款( A 事务) | 转账( B 事务) |
|---|---|---|
| T1 | 开始事务 | |
| T2 | 开始事务 | |
| T3 | 查询余额为 1000 元 | |
| T4 | ||
| T5 | 取出 100 元,余额 900 元 | |
| T6 | 查询余额为 900 元(脏读) | |
| T7 | 撤销 A 事务,余额恢复 1000 元 | |
| T8 | 汇入 100 元 | |
| T9 | 提交事务 |
最终结果:客户损失 100 元
不可重复读
指同一个事务使用同一条SELECT语句每次读取到的结果不一样。
比如说,如果有一个事务执行了两次相同的SELECT语句,但另一个事务在这条SELECT语句的两次执行之间修改了一些数据行,就会发生这种问题。
| 时间 | 取款( A 事务) | 转账( B 事务) |
|---|---|---|
| T1 | 开始事务 | |
| T2 | 开始事务 | |
| T3 | 查询余额为 1000 元 | |
| T4 | 查询余额为 1000 元 | |
| T5 | 取出 100 元,余额 900 元 | |
| T6 | 提交事务 | |
| T7 | 查询余额为 900 元 |
最终结果:客户账户余额到底是多少?该如何转账呢?
覆盖更新(第二类丢失更新)
这是不可重复读中的特例,指一个事务覆盖另一个事务已提交的更新数据。
举个栗子: A 事务更新数据后, B 事务又更新了该数据,最后 A 事务查询后发现自己更新的数据改变了。
| 时间 | 取款( A 事务) | 转账( B 事务) |
|---|---|---|
| T1 | 开始事务 | |
| T2 | 开始事务 | |
| T3 | 查询余额为 1000 元 | |
| T4 | 查询余额为 1000 元 | |
| T5 | 取出 100 元,余额 900 元 | |
| T6 | 提交事务 | |
| T7 | 汇入 100 元,余额 1100 元 | |
| T8 | 提交事务 | |
| T9 | 查询余额 1100 元 |
最终结果:银行损失 100 元
幻影数据行
亦叫幻读、虚读,指某个事务突然看到了一个它以前没有见过的数据行。
比如说,若某个事务刚执行完一条SELECT语句就有另一个事务插入了一个新数据行,前一个事务再次执行同一条SELECT语句时就可能多看到一个新的数据行,这就是幻影数据行。
| 时间 | 注册( A 事务) | 统计( B 事务) |
|---|---|---|
| T1 | 开始事务 | |
| T2 | 开始事务 | |
| T3 | 统计网站注册用户为 1000 人 | |
| T4 | 注册新用户 | |
| T5 | 提交事务 | |
| T6 | 统计网站注册用户为 1001 人 |
最终结果:到底哪个统计数据有效?
隔离级别
为什么需要隔离级别?
为了解决事务运行中存在的并发问题,定义了隔离级别来对不同的事务进行隔离,不同的隔离级别对事务的隔离程度存在差异。
隔离级别的详细说明
部分存储引擎(如 InnoDB)为事务提供了以下 4 种从低到高的隔离级别:
READ UNCOMMITTED:允许某个事务看到其他事务尚未提交的数据行改动(未交卷就可看)READ COMMITTED:只允许某个事务看到其他事务已经提交的数据行改动(已交卷才可看)REPEATABLE READ:如果某个事务两次执行同一个SELECT语句,其结果是相同的。换而言之,即使有其他事务在同时插入或修改数据行,这个事务第二次所看到的结果和第一次是一样的SERIALIZABLE:某个事务正在查看的数据行在该事务完成之前不允许其他事务修改。详而言之,对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
下表列出了 4 种隔离级别以及它们是否允许脏读、不可重读或幻读等问题:
| 隔离级别 | 脏读 | 不可重读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | ✅ | ✅ | ✅ |
| READ COMMITTED | ❌ | ✅ | ✅ |
| REPEATABLE READ | ❌ | ❌ | ❓ |
| SERIALIZABLE | ❌ | ❌ | ❌ |
业务场景
总结来说,存在即合理,哪个隔离级别都有它自己的使用场景,需要根据自己的业务情况来定。我想你可能会问那什么时候需要“可重复读”的场景呢?我们来看一个数据校对逻辑的案例。
假设你在管理一个个人银行账户表。一个表存了每个月月底的余额,一个表存了账单明细。这时候你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。
因此,此时使用“可重复读”隔离级别就很方便,因为事务启动时的视图可以认为是静态的,不受其他事务更新的影响。
数据库差异
对不同的数据库而言,对隔离级别的实现可能不同。
比如说,在 SQL 的标准定义(不同数据库实现不同)中,REPEATABLE READ隔离级别中是可能发生幻读问题的,而对于 MySQL 的 InnoDB 存储引擎,其默认使用的隔离级别是REPEATABLE READ,解决了脏读,不可重读,幻读问题
MySQL 下的隔离级别修改
若想修改隔离级别,可以通过以下 3 种形式的语句来改变:
SET GLOBAL TRANSACTION ISOLATION LEVEL [隔离级别];:全局事务SET SESSION TRANSACTION ISOLATION LEVEL [隔离级别];:局部事务SET TRANSACTION ISOLATION LEVEL [隔离级别];:下一个事务
SUPER权限的用户可以使用SET GLOBAL TRANSACTION语句改变全局隔离级别的设置,该设置将作用于此后连接的服务器的任何客户。
此外,任何用户都可以修改它自己的事务隔离级别(局部事务)。
当然,也可用SET TRANSACTION语句修改为只作用于下一个事务。
事务状态
事务在不同阶段大致上划分成以下几个状态:
- 活动(active):事务对应的数据库操作正在执行过程中时,该事务就处在活动的状态
- 部分提交(partially committed):当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,该事务就处在部分提交的状态 - 失败(failed):当事务处在活动的或者部分提交的状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,该事务就处在失败的状态
- 中止(aborted):如果事务执行了半截而变为失败的状态,比如银行转账 A 账户的钱被扣除,但是 B 账户的钱没有增加时遇到了错误,从而使当前事务处在了失败的状态,那么就需要把已经修改的 A 账户余额调整为未转账之前的金额,换句话说,就是要撤销失败事务对当前数据库造成的影响。书面一点的话,我们把这个撤销的过程称之为回滚。当回滚操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,该事务就处在了中止的状态
- 已提交(committed):当一个处在部分提交的状态的事务将修改过的数据都同步到磁盘上之后,该事务就处在了已提交的状态

从图中可以看出,只有当事务处于提交的或者中止的状态时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态。
事务使用须知
如果你想借助事务来避免数据出错,那么使用事务时必须注意一些事情,具体介绍见 Spring——事务管理一文。
参考
- MySQL 是怎样运行的:从根儿上理解 MySQL
- Paul DuBois. MySQL 技术内幕 [M]. 人民邮电出版社, 2011
文章信息
| 时间 | 说明 |
|---|---|
| 2019-02-12 | 初稿 |
| 2021-09-26 | 重构 |
| 2022-02-18 | 锁独立一文 |
| 2022-11-15 | 增加事务使用须知一节 |
| 2023-06-04 | MVCC 独立一文 |