数据库事务 ACID 特性与隔离级别
数据库事务ACID特性与隔离级别
数据库事务ACID特性
数据库事务正确执行的四个基础要素是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
- 原子性:是指事务包含的所有操作要么全部成功,要么全部失败回滚,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有被执行过一样。
- 一致性:是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
- 隔离性:两个事务之间的隔离程度
- 持久性:持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下(如断电、崩溃)也不会丢失提交事务的操作。
丢失更新
在互联网中存在着抢购、秒杀等高并发环境,使得数据库在一个多事务的环境中运行,多个事务的并发会产生一系列的问题,主要的问题之一就是丢失更新,一般而言存在两类丢失更新。
假设一个场景,一个账户存在互联网消费和刷卡消费两种形式,而一对夫妻共用这个账户。男喜欢刷卡消费,女喜欢互联网消费,那么可能产生如下表所示场景
表1 第一类丢失更新
| 时间 | 事务一(男) | 事务二(女) |
|---|---|---|
| T1 | 查询账户余额为10000元 | |
| T2 | 查询账户余额为10000元 | |
| T3 | 网购1000元 | |
| T4 | 请客吃饭消费1000元 | |
| T5 | 提交事务成功,余额9000元 | |
| T6 | 取消购买,回滚事务到T2时刻,余额10000元 |
整个过程只有男消费1000元,而在最后的T6时刻,女回滚事务,却恢复了原来的初始值10000元,这显然不符合事实。这样的两个事务并发,一个回滚、一个提交成功导致不一致,称之为第一类丢失更新。大部分数据库(包括Mysql和Oracle)基本都已经消灭了这类丢失更新。第二类丢失更新是我们真正需要关注的内容。
表2 第二类丢失更新
| 时间 | 事务一(男) | 事务二(女) |
|---|---|---|
| T1 | 查询账户余额为10000元 | |
| T2 | 查询账户余额为10000元 | |
| T3 | 网购1000元 | |
| T4 | 请客吃饭消费1000元 | |
| T5 | 提交事务成功,余额9000元 | |
| T6 | 提交事务,根据之前余额10000元,扣减1000元后,余额为9000元 |
整个过程存在两笔交易,一笔是男的请客吃饭,一笔是女的网购,两者都提交了事务,由于在不同的事务中,无法探知其它事务的操作,导致两者提交后,余额都为9000元,而实际正确的应为8000元,这就是第二类丢失更新。为了克服事务之间协助的一致性,数据库标准规范中定义了事务之间的隔离级别,来在不同程度上减少出现丢失更新的可能性---->数据库隔离级别。
隔离级别
隔离级别可以在不同程度上减少丢失更新,按照SQL的标准规范,把隔离级别定义为4层,分别是:脏读(dirty read)、读/写提交(read commit)、可重复读(repeatable read)和序列化(serializable)。
各类隔离级别和产生的现象
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 脏读(Read Uncommitted) | √ | √ | √ |
| 读/写提交(Read Committed) | × | √ | √ |
| 可重复读(Repeatable Read) | × | × | √ |
| 序列化(Serializable) | × | × | × |
√表示该隔离级别下会出现对应问题,× 表示不会出现。
脏读是最低的隔离级别,允许一个事务去读取另一个事务中未提交的数据。
脏读
| 时间 | 事务一(男) | 事务二(女) | 备注 |
|---|---|---|---|
| T1 | 查询余额10000元 | ||
| T2 | 查询余额10000元 | ||
| T3 | 网购1000元,余额9000元 | ||
| T4 | 请客吃饭消费1000元,余额8000元 | 读取到事务二,未提交余额为9000元,所以余额为8000元 | |
| T5 | 提交事务 | 余额为8000元 | |
| T6 | 回滚事务 | 由于第一类丢失更新已经克服,所以余额为错误的8000元 |
由于在T3时刻女启动了消费,导致余额为9000元,男在T4时刻消费,因为用了脏读,所以能够读取女消费的余额(事务二未提交的)为9000元,这样余额就为8000元了,于是T5时刻男提价事务,余额变为了8000元,女在T6时刻回滚事务,由于第一类丢失更新已经克服,所以余额为错误的8000元,显然这是一个错误的余额,产生这个错误的根源来自于T4时刻,也就是事务一读取到事务二未提交的事务,这样的场景称之为脏读。
为了克服脏读,SQL标准提出了第二个隔离级别----读/写提交。所谓读写提交,就是说一个事务只能读取另一个事务已经提交的数据。
读/写提交
| 时间 | 事务一(男) | 事务二(女) | 备注 |
|---|---|---|---|
| T1 | 查询余额10000元 | ||
| T2 | 查询余额10000元 | ||
| T3 | 网购1000元,余额9000元 | ||
| T4 | 请客吃饭消费1000元,余额9000元 | 由于事务二的余额未提交,采取读/写提交时不能读出,所以余额为9000元 | |
| T5 | 提交事务 | 余额为9000元 | |
| T6 | 回滚事务 | 由于第一类丢失更新已经克服,所以余额依旧为正确的9000元 |
在T3时刻由于事务采取读/写提交的隔离级别,所以男无法读取女未提交的9000元余额,他只能读取到10000元,所以在消费后余额依旧为9000元。T5时刻提交事务,而T6时刻女回滚事务,所以结果为正确的9000元,这样就消除了脏读带来的问题,但是也会引发其它问题,如下表所示。
不可重复读
| 时间 | 事务一(男) | 事务二(女) | 备注 |
|---|---|---|---|
| T1 | 查询余额10000元 | ||
| T2 | 查询余额10000元 | ||
| T3 | 网购1000元,余额9000元 | ||
| T4 | 请客吃饭消费2000元,余额8000元 | 由于采取读/写提交,不能读取事务二中未提交的余额9000元 | |
| T5 | 继续购物8000元,余额1000元 | 由于采取读/写提交,不能读取事务一中未提交的余额8000元 | |
| T6 | 提交事务,余额1000元 | 女提交事务,余额更新为1000元 | |
| T7 | 提交事务发现余额为1000元,不足以买单 | 由于采取读/写提交,因此此时事务一可以知道余额不足 |
由于T7时刻事务一知道事务二提交的结果----余额为1000元,导致男无钱买单的尴尬。对于男而言,他并不知道女做了什么事情,但是账户余额却莫名其妙地从10000元变为了1000元,对他来说,账户余额是不能重复读取的,而是一个会变化的值,这样的场景称之为不可重复读(unrepeatable read),这是读/写提交存在的问题。
为了克服不可重复读带来的错误,SQL标准又提出了一个可重复读的隔离级别来解决问题。注意,可重复读针对的是数据库同一条记录而言的,换句话说,可重复读会使得同一条记录的读/写按照一个序列化进行操作,不会产生交叉情况,这样就能保证同一条数据的一致性,进而保证上述场景的正确性。但是由于数据库并不是只能针对一条数据进行读/写操作,在很多场景,数据库需要同时对多条记录进行读/写,这个时候会产生下面的情况。如下表所示
幻读
| 时间 | 事务一(男) | 事务二(女) | 备注 |
|---|---|---|---|
| T1 | 查询消费记录为10条,准备打印 | 初始状态 | |
| T2 | 启用消费一笔 | ||
| T3 | 提价事务 | ||
| T4 | 打印消费记录得到11条 | 女发现打印了11条消费记录,比查询的10条多了一条。她会认为这条是多余不存在的,这样的场景称之为幻读。 |
女在T1查询得到10条记录,到T4打印记录时,并不知道男在T2和T3时刻进行了消费,导致多一条(可重复读针对的是同一条记录,而这里不是同一条记录)消费记录的产生,她会质疑这条多出来的记录是不是幻读出来的,这样的场景称之为幻读。
为了克服幻读,SQL标准又提出了序列化的隔离级别。它是一种让SQL按照顺序读/写的方式,能够消除数据库事务之间并发产生数据不一致的问题。
MySQL的事务和MVCC
MySQL事务支持情况
MySQL中并非所有存储引擎都支持事务:
- 支持事务:InnoDB、NDB
- 不支持事务:MyISAM(常用但无事务特性)
事务ACID的实现机制
- 原子性:通过 undo_log日志 实现(事务失败时回滚到初始状态)。
- 持久性:通过 redo_log重做日志 实现(事务提交后,数据修改持久化到磁盘)。
- 隔离性:通过 锁机制 + MVCC 共同实现(保证并发事务的隔离性)。
- 一致性:事务的最终目标,由原子性、隔离性、持久性共同保障。
InnoDB与锁配合,同时采用另一种事务隔离性的实现机制MVCC,即 Multi-Versioned Concurrency Control 多版本并发控制,用来解决脏读、不可重复读等事务之间读写问题,MVCC在某些场景中替代了低效的锁,在保证了隔离性的基础上,提升了读取效率和并发性。
MVCC 多版本并发控制
定义
MVCC(Multi-Versioned Concurrency Control)即多版本并发控制,是InnoDB存储引擎的核心特性之一。
作用:在 READ COMMITTED 和 REPEATABLE READ 隔离级别下,事务执行普通SELECT操作时,通过访问记录的“版本链”实现读写并发,替代部分低效锁机制,提升读取效率和并发性能。
版本链的构成
InnoDB的聚簇索引记录包含两个必要隐藏列和一个非必要隐藏列:
- trx_id:事务id。每次事务修改该记录时,会将当前事务id赋值给此列。
- roll_pointer:回滚指针。每次修改记录时,会将旧版本记录写入undo日志,此列作为指针指向该旧版本记录。
- row_id(非必要):当创建的表中有主键或者非NULL的UNIQUE键时都不会包含row_id列。
版本链形成过程
每次对记录进行INSERT/UPDATE/DELETE操作时,都会生成一条undo日志,undo日志通过roll_pointer属性串联成链表,形成“版本链”(最新版本在链表头部,旧版本依次向后)。

ReadView的作用与构成
核心问题
当事务使用READ COMMITTED或REPEATABLE READ隔离级别时,普通SELECT查询需判断版本链中的哪个版本对当前事务“可见”(即是否能读取)。
ReadView的定义
对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另一个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的。因此,核心问题就是需要判断一下版本链中的哪个版本是当前事务可见的。包含4个关键属性:
- m_ids:生成ReadView时,当前系统中活跃的读写事务id列表。
- min_trx_id:m_ids中的最小事务id(当前活跃事务的最小id)。
- max_trx_id:生成ReadView时,系统即将分配给下一个事务的id(活跃事务最大id + 1)。
- creator_trx_id:创建当前ReadView的事务id(当前事务自身的id)。

ReadView的可见性判断规则
访问记录的某个版本时,通过以下步骤判断是否可见:
- 若版本的trx_id == creator_trx_id:当前事务访问自己修改的记录,可见。
- 若版本的trx_id < min_trx_id:生成该版本的事务在当前ReadView生成前已提交,可见。
- 若版本的trx_id >= max_trx_id:生成该版本的事务在当前ReadView生成后才开启,不可见。
- 若min_trx_id ≤ trx_id < max_trx_id:
- 若trx_id在m_ids列表中:生成该版本的事务仍活跃,不可见。
- 若trx_id不在m_ids列表中:生成该版本的事务已提交,可见。

ReadView的生成时机(隔离级别核心区别)
READ COMMITTED和REPEATABLE READ的核心差异在于生成ReadView的时机:
- READ COMMITTED:每次执行SELECT查询前,都会重新生成一个ReadView。
- 后果:同一事务内多次查询可能读取到不同版本的数据(其他事务提交后的修改),因此会出现不可重复读。
- REPEATABLE READ:仅在事务第一次执行SELECT查询时生成一个ReadView,后续查询复用该ReadView。
- 后果:同一事务内多次查询读取到的是同一版本的数据,避免了不可重复读,但仍可能出现幻读。
MySQL与Oracle的默认隔离级别
- MySQL默认隔离级别:REPEATABLE READ(可避免脏读、不可重复读,部分防止幻读,但无法完全消除)。
- Oracle默认隔离级别:READ COMMITTED(可避免脏读,允许不可重复读和幻读)。

浙公网安备 33010602011771号