MySQL的ACID实现原理

·2442·6 分钟
AI摘要: 本文介绍了MySQL的ACID实现原理,包括原子性、一致性、隔离性和持久性。Innodb作为MySQL最常用的存储引擎,其内部构造包括buffer pool、redo log和undo log。Atomic实现原理主要通过undo log保证事务的原子性,Consistency由业务逻辑或约束实现,Isolation采用MVCC提高事务并发性能。Durability通过redo log防止数据库崩溃造成数据丢失。

ACID

  • Atomic(原子性): 要么全部完成, 要么全部失败(比如转账中,一个人减少余额,另外一个人增加余额,要么全部成功要么全部失败)

  • Consisitency(一致性):操作前后,数据的状态不能发生变化,需要符合规范,是有效状态(比如资金不能出现负值)

  • Isolation(隔离性):各个事务并发不能相互影响,需要表现得像是在串行(比如两个人向同一个人转账,收款应该是 2x2x ¥,而不是 xx ¥)

  • Durability(持久性):数据不能丢失,需要持久化到磁盘中(数据库宕机也不能丢失数据)

Innodb

Innodb作为MySQL最常用的存储引擎,其内部构造如下图:

image-20241125135122808

其中最为主要的几个组件为:buffer pool, redo log, undo log

Atomic实现原理:undo log

Undo Log是逻辑日志,记录的是数据的增量变化,利用Undo Log可以进行事务回滚,从而保证事务的原子性,同时也实现了多版本并发控制(MVCC),提供了隔离性的基础,解决读写冲突和一致性读问题。这里主要介绍undo Log实现原子性的过程:

image-20241125142225884

如图所示,当执行SQL对数据库表增加内容和修改内容,undo log中就会对应着删除和逆修改的内容,一旦执行失败,将会执行undo log中的SQL进行回退。

Consistency实现原理

一致性一般是由业务逻辑或者约束实现的,比如余额不能为负值,在业务代码中校验。

Isolation实现原理:MVCC

当多个事务并发执行时候,会遇到如下三种情况

  • 读读:简单的解决方式是使用共享锁

  • 写写:简单的解决方式是排它锁

  • 读写:简单的解决方式是加锁

加锁是最直接的方法,也是最慢的方法,而MySQL采用MVCC来提供事务隔离级别无锁实现方式,用于提高事务的并发性能

数据库中事务并发和Java中的并发不太一样,数据库中的事务在失败的时候是可以回滚的(Atomic特性),但是Java中是没有,因此不能完全类比

数据库中事务可能出现的隔离级别:

  • 脏读:A事务修改了数据,B事务读取到修改的数据,结果A事务因为错误而回滚,B这个时候拿到的数据就是错误的

    • MVCC解决方案:读已提交

    • 行锁解决方案:A事务修改数据但是未提交时候,B事务只能阻塞,虽然可行,但不是最佳的,行锁带来的性能太低,所以MySQL还是使用的是MVCC的无锁机制

  • 不可重复读:A事务第一次读取到某个数据,B事务紧接着修改了这个数据,A事务第二次读取这个数据,结果发现前后不一致

    • MVCC解决方案:可重复读
  • 幻读:A事务统计整个表中行做统计操作,B事务在这期间增减行数,导致A事务前后统计结果不一致

    • MVCC解决方案:串行化(表锁)

Undo Log维护的数据结构如下图,每个记录行都存在一个指针指向其上一个历史版本。

image-20241125142815854

Undo LogMVCC中具体实现隔离性的步骤:

  • 每个事务都有一个递增的事务ID

  • 数据页的行记录中包含了三个隐藏列:DB_ROW_ID, DB_TRX_ID(当前操作的事务ID), DB_ROLL_PTR(回滚指针,记录当前行的上一个版本);

  • DB_ROLL_PTR将数据行的所有快照记录都通过链表的结构串联起来(也就是上图Undo Log的结构)

MVVC就是通过Read ViewUndo Log找到对应版本执行CAS ,不同隔离界别采用的Read View是不同的,这里就懒得写了,内容有点多。

总结,MVCC的意义:

  • 读写互不阻塞

  • 降低死锁的概率

  • 实现一致性读

Durability实现原理:redo log

为了防止数据库崩溃而造成数据丢失,一种直接的方法是可以在修改数据时候同时写入磁盘,也就是事务提交前页面写入磁盘,但是这种思路会遇到两个问题:

  1. 随机IO慢:由于修改的数据大概率是不满足局部一致性原理,所以随机IO非常耗时,将会极大降低数据库性能;

  2. 写放大问题:由于数据库写磁盘不是一条一条写入,而是写入一整个page,所以当修改只发生在很少的数据上,却需要写一整个page,也会降低性能;

综上分析,事务提交前写入磁盘是实现持久化唯一解决办法,但是随机IO又太慢了,那么为什么一定要随机IO呢?为什么一定要把物理数据刷入磁盘呢?直接将操作写入磁盘不好吗?直接顺序IO不好吗?

所以,成熟的解决方案是WAL(Write-ahead logging):

MySQL的每次更新操作都是先写入redo log中,然后在写入buffer pool中。redo log是物理日志,记录的是页面的变化。由于redo log是顺序IO,因此写入速度非常快。如果写入磁盘前发生故障,重启MySQL后根据redo log重做即可恢复。

Kaggle学习赛初探