详解事务的 ACID 特性及其实现原理

2023年 9月 3日 28.1k 0

1. 事务的 ACID 特性

ACID 特性指原子性、一致性、隔离性、持久性。

1.1原子性 Atomicity

原子性是指事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。

拿转账为例,A 有 500 元,B 有 100 元,如果在一个事务里 A 转给 B100 元。如果事务成功,那么 A 一定减少 100 元,B 增加 100 元。不可能 A 账户扣减成功,B 账户增加失败。

1.2隔离性 Isolation

隔离性,是指一个事务的执行不能被其他事务干扰,即并发执行的各个事务之间不能互相干扰。

在 InnoDB 中隔离级别分为 4 种,我们在文章 xxx 中对其有详细的介绍。

1.3持久性 Durability

事务应当保证所有被成功提交的数据修改都能够正确地被持久化,不丢失数据。

1.4 一致性 Consitency

一致性是指事物处理前后,系统中的数据始终是正确的。拿转账为例,A 有 500 元,B 有 100 元,如果在一个事务里 A 成功转给 B 100 元,那么只要事务执行成功了,最后 A 账户一定是 400 元,B 账户一定是 200 元。

这里说句题外话:事实上 ACID 这四种特性并不正交。A、I、D 是手段,C 是目的。ACID 完全是为了拼凑个单词缩写才弄到一块去。

2. ACID 的实现原理

2.1 原子性

原子性使用 undo log 来实现。操作数据之前先将执行事务之前的数据写入 redo log。如果事务执行过程中出错或者用户执行了 rollback,系统通过 undo log 日志返回事务开始的状态。

image.png
这里的撤销操作指的是业务方主动rollback。

2.2隔离性

MySQL 提供了四种隔离级别的实现。分别为:

  • 读未提交
  • 读已提交(考虑到 RC 隔离级别性能更好,大厂中更多使用 RC 隔离级别)
  • 可重复读(MySQL 默认隔离级别)
  • 串行化

这四种隔离级别按照顺序隔离性越来越好,但是读写性能越来越差。

隔离级别 存在的问题 底层原理
读未提交 脏读 底层没有任何的机制,这种隔离级别没有任何场景会使用
读已提交 不可重复读、幻读 - MVCC:通过 undo log + readview 解决了脏读的问题,其中 readview 在事务中每次 select 都会重新生成,因此会存在不可重复读的问题。
可重复读 问题解决:解决了脏读、不可重复读的问题,但是存在幻读的问题。不过 InnoDB 通过行锁和间隙锁解决了幻读的问题。 - MVCC:通过 undo log + readview 解决了脏读的问题。其中 readview 是在事务中第一次 select 时生成,因此解决了不可重复读的问题。
  • Innondb 的可重复读隔离级别,通过间隙锁解决了幻读的问题。 |
    | 串行化 | 问题解决:解决了脏读、不可重复读、幻读的问题。 | 查询、修改操作全部串行执行,因此事务之间的隔离性最好。但是读写性能时四个隔离级别中最差的。因此一般不会使用串行化这个隔离级别。 |

下面具体解释下这四种隔离级别:

2.2.1读未提交 RU

RU 级别,实际上就是完全不隔离。每个进行中事务的中间状态,对其他事务都是可见的,所以有可能会出现“脏读”。所谓脏读,就是说一个事务读到了另外一个事务中的 "脏数据",脏数据就是指事务未提交的数据。

举个例子:zhangsan 给 lisi 转账 100 元,转账前 lisi 只有 500 元。T3 时候事务 A 读取到了事务 B T2 时刻操作的结果,如果 T3 时候 lisi 执行提现 600 元,然后事务 B 在执行回滚操作,就会数据的不一致。

image.png
暂时无法在飞书文档外展示此内容

总结:这种级别虽然性能好,但是隔离性太差,所以基本不用。

2.2.2 读已提交 RC

这种隔离级别可以避免脏读,但是无法解决不能重复读的问题。

不可重复读的概念如下:在一个事务中,前后执行两次相同的查询操作,但是得到的结果不一致。

解决脏读:unod log + read view 机制。

如下图所示:假设表 account 中 id = 100 的 money 值为 200,事务 A 首先将其更改为 200,但是未提交事务。然后事务 B 执行 select 操作。

事务 B 执行 select 操作时,虽然事务 A 执行 UPDATE 操作将 money 字段更改为了 200,但是结合 readview 机制,trx=200 的操作对事务 B 不可见,而 trx = 100 对事务 B 可见。此时事务 B 会沿着 undo log 向下寻找,直到找到第一个可见的版本,即 trx = 100, money =100。因此事务 B 的查询操作查询结果为 money = 100 而非 money = 200。

暂时无法在飞书文档外展示此内容

image.png
不可重复读

RC 隔离级别下,每次执行 select 操作都会重新生成 readview,我们还是以上面的例子举例,事务 B 在事务 A 未提交事务时读取到的 money 值为 100,此时事务 A 提交事务。事务 B 重新执行查询,此时重新生成 readview 视图,并且此时事务 A 的操作对事务 B 可见,因此第二次查询结果为 money =200。这种在同一个事务中相同查询 SQL 得到不同结果的现象就被称为不可重复读。

image.png

2.2.3 可重复读

可重复读隔离级别下,事务第一次执行 SELECT 操作生成 readview,后面一直到事务结束都会一直使用这个隔离级别。从而解决了不可重复读的问题。

隔离级别 生成 readview 时机
读已提交 事务中每次执行 select 都重新生成 readview
可重复读 事务中第一次执行 select 操作生成,后续不会再生成

此外,InnoDB 在可重复读隔离级别下,引入了 Next-Key 锁(行锁+间隙锁)的概念。Next-key 锁通过锁定记录本身和记录之间的间隙,解决了幻读的问题。

2.2.4 串行化

串行化相当于给整张表加了锁,所有的查询、更新操作必须全部串行执行。这种隔离级别虽然隔离性最好,但是性能却是最差的,因此基本上不会被使用。

2.3 持久性

持久性通过 redo log 保证。mysql 为了提升性能,采取写内存 buffer pool 而非直接写磁盘(磁盘随机 IO 太慢),如果数据库宕机会造成数据丢失。mysql 引入 redo log(顺序写,磁盘顺序 IO 很快)来存储数据库变更之后的一个值。mysql 提交事务的时候,除了写 buffer pool 还会将数据修改后的值持久化到 redo log(顺序写磁盘)。

当系统崩溃恢复时,内存 Buffer Pool 中的数据全部丢失。这时会根据 redo log 来恢复之前已经完成的事务。

对于某个事务,如果 redo log 状态为 PREPARED,并且根据 XID 能够找到完整的 binlog 文件,则代表该事务已经提交完成,此时会根据 redo log 文件的内容更新磁盘上的文件。

这里的崩溃恢复机制详细过程可参考 MySQL 的二阶段提交协议:xxx

小贴士:所谓的 redo log 日志,就是记录下来你对数据做了什么修改。比如对"id = 100" 这行记录修改了 age 字段的值为 100。

Redo log 用于在 MySQL 宕机恢复更新过的数据。

image.png

2.4 一致性

正如上面提到的:原子性、隔离性、持久性是手段,一致性是目的。一致性是通过原子性、隔离性、持久性实现的。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论