下载帮

您现在的位置是:首页 > 数据库 > MySQL

MySQL

MySQL 是如何实现 ACID 的?

2022-02-12 18:12MySQL

我们都知道,事务具有 ACID 四个特性——原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。但你知道 MySQL 是通过什么技术手段来实现的吗?

ACID 简介

先来简单回顾一下 ACID 的定义:

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

一致性:事务开始前和事务结束后,数据库的完整性没有被破坏。即写入的数据必须完全符合所有的预设约束、触发器、级联回滚等。

隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。

持久性:已被提交的事务对数据库的修改应该永久保存在数据库中。即使系统挂了,数据也不会丢。

我们按照:持久性 -> 原子性 -> 隔离性 -> 一致性 的顺序来讨论。

PS:本文基于 InnoDB

 

持久性

我们知道程序修改数据的时候,是先将数据从磁盘加载到内存,然后修改完再由内存写回磁盘。持久化其实就是将内存里的数据写入磁盘。因此,持久性的关键就在于如何保证数据可以由内存顺利写入磁盘。

我们有以下几个方案:

方案一:

  1. 加载数据到内存
  2. 修改内存
  3. 然后写回磁盘
  4. 提交事务

方案二:

  1. 加载数据到内存
  2. 修改内存
  3. 提交事务
  4. 后台写回磁盘

 

第一种方案,靠谱是靠谱,但频繁 I/O 性能太低,会严重拖累 MySQL 的吞吐量。

第二种方案虽然性能上来了,但如果在第四步时宕机了,而系统认为事务已提交,这时候就会丢失数据了。

那怎么办呢?MySQL 给出的方案是 WAL(Write Ahead Log)机制。WAL 翻译过来就是先写日志的意思。这个日志就是 redo log。具体做法是:

  1. 加载数据到内存
  2. 修改内存
  3. 写入 redo log
  4. 提交事务
  5. 后台写回磁盘

如果第五步时系统宕机,也可以通过 redo log 来恢复。

你可能有疑问:写入 redo log 不也有磁盘 I/O 吗?这不是脱了那啥再那啥,多此一举吗?写 redo log 和写表的区别就在于随机写和顺序写。MySQL 的表数据是随机存储在磁盘中的,而 redo log 是一块固定大小的连续空间。而磁盘顺序写入要比随机写入快几个数量级。

因此,这种方案即保证了数据的安全,性能上也能够接受。

 

原子性

假如一个事务做了如下操作:

  1. 插入一条数据 insert into user values('1','小刘','18')
  2. 更新一条数据 update user set name = '小水' where id = 2
  3. 删除一条数据 delete from user where id = 3

根据原子性的规定,这三个操作要么都成功,要么都失败。那么问题就来了,如何保证 3 失败的情况下,让 1,2 也回退呢?

答案就是 undo log。

每个事务操作(增删改)都会记录一条与之对应的 undo log:

  1. insert 记录插入的主键,回滚则根据该主键删除记录
  2. update 记录记录主键和被修改列的当前值,回滚则根据主键和之前的值覆盖
  3. delete 为记录添加删除标志,即 MySQL 内部的逻辑删除,回滚根据主键恢复

 

隔离性

数据库事务有四种隔离级别,不同的级别可能会出现各种各样的问题(脏读、幻读、不可重复读),关系如下:

隔离级别

脏读

不可重复读

幻读

读未提交(Read uncommitted)

可能

可能

可能

读已提交(Read committed)

不可能

可能

可能

可重复读(Repeatable read)

不可能

不可能

可能

可串行化(Serializable)

不可能

不可能

不可能

 

 

MySQL 中 RR 级别已经解决了幻读问题。

 

并发的情况才需要隔离,而并发有三种组合:

  1. 读读
  2. 读写
  3. 写写

「读读」的情况,不需要隔离;「读写」通过 MVCC 隔离;「写写」只能通过锁来隔离。

MVCC(Multi Version Concurrency Control,多版本并发控制)作用于 RC 和 RR 级别。可以为事务中的读操作创建一个快照(Readview),从而来避免被其他事务干扰。

RC 级别下,一个事务中的每次(同参数)读都会创建一个 Readview。

RR 级别下,一个事务中只在第一次读时创建 Readview,后面再次读,仍然读取该 Readview。

「写写」的情况通过三种锁来实现隔离:Record Lock、Gap Lock 和 Next Key Lock(前两者的组合)。

Record Lock 锁住一条数据,从而使其他事务无法修改和删除;Gap Lock 锁住一个范围,从而使其他事务不能在该区间插入数据;Next Key Lock 锁住具体数据和区间,从而使其他事务无法更新、删除和在该区间插入数据。

MVCC + 锁 使得 MySQL 在 RR 级别避免了幻读问题。

 

一致性

很多人聊到一致性,很喜欢拿转账的业务举例,但这明显是原子性的范畴——A 账户扣钱,B 账户加钱,两个 Update 操作,要么都成功,要么都失败。

一致性更侧重是,数据的完整性:主外键约束、唯一索引、列完整等。MySQL 中保证一致性主要靠 CR(Crash Recovery)和 DWB(Doublewrite Buffer)来保证的。

这两个特性比较复杂,一篇文章根本讲不完,如果你感兴趣可以去看官方文档,或者留言告诉我,我来安排。

 

最后

一致性是一个比较特殊的存在,它和原子性、隔离性有一层「你中有我,我中有你」的暧昧关系。比如转账的业务场景,如果说它属于一致性的范畴,也能够说得通,可以叫「用户自定义一致性」;另外,隔离性使得事务之间互不影响的最终效果也是保证了数据的一致。

文章评论