ACID和实现原理
date
Nov 17, 2022
slug
acid
status
Published
tags
数据库
summary
事务的 ACID 特性是如何实现的
type
Post
简介
本文主要解释事务的ACID特性和相关实现原理,背景是Mysql5.6,数据引擎是Inno db
基础概念
事务是指一组操作,这组操作要么全都执行成功,要么全都不执行。
MySQL的架构
首先我们了解一下MySQL的架构
MySQL的结构一共可以分为三层
- 链接层,处理链接,授权认证等
- 服务器层,负责语句的解析、优化
- 存储引擎层,其中使用最广泛的存储引起是InnoDB。MySQL本身是不支持事务的,事务是由存储引擎实现的。
事务的提交
Mysql中默认是autocommit(自动提交)模式,如果没有显示的开启一个事务,那么每个SQL语句都会当作一个事务执行。
我们也可以通过
set autocommit = 0
,关闭自动提交;需要注意的是,autocommit参数是针对连接的,在一个连接中修改了参数,不会对其他连接产生影响。如果关闭了autocommit,则所有的sql语句都在一个事务中,直到执行了commit或rollback,该事务结束,同时开始了另外一个事务。
强制提交的命令
在MySQL中,存在一些特殊的命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务;如DDL语句(create table/drop table/alter/table)、lock tables语句等等。
不过,常用的select、insert、update和delete命令,都不会强制提交事务。
ACID
- 原子性 Atomicity
- 一致性 Consistency
- 隔离性 Isolation
- 持久性 Durability
原子性
原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。
实现原理 undo log
可以看到实现原子性的关键是,我们必须要保证sql执行失败的时候进行回滚。在innodb中,回滚的关键依靠的就是undo log。
每当我们对数据库进行修改的时候,就会记录相应操作到undo log中,如果事务需要回滚,那么就会根据undo log里面的记录把数据回滚到初始值。
当发生回滚的时候,innodb会根据undo log里面的操作执行逆操作,例如:
- 记录了insert,就执行delete;记录了delete,就执行insert
- 记录了update,则执行相反的update
持久性
持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
实现原理 redo log
持久性的实现关键在于及时的把操作记录下来,避免丢失。
innodb的实现机制是redo log。
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。
redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
二阶段提交:先写redo log,执行操作,操作成功后再写bin log
隔离性
隔离性要求事务之间互相隔离,不会干扰对方操作。
隔离不足出现的问题
- 脏读
A事务读取到B事务未提交的数据,一旦B事务回滚,那么读取到的数据就是脏数据
- 不可重复读
A事务读取的数据被B事务修改,导致前后读取结果不一致
- 幻读
A事务执行范围查询的期间,B事务在范围内写入了一些数据导致A事务再次执行范围查询的时候两次结果不一致
事务隔离级别
SQL定义了四种隔离级别,每种隔离级别均解决了一定的问题
innodb的默认隔离级别是RR,在SQL标准中RR无法解决幻读问题,但是innodb通过MVCC和加锁解决了幻读问题
RR隔离级别如何解决不可重复读和幻读
一句话总结:
- 当前读场景下使用加锁解决
- 快照读场景下使用MVCC解决
当前读
定义:读取的数据是最新数据就是当前读
当前读的触发语句:
如果我们执行
update , delete , insert
这些写操作的语句,那么Mysql就会加行锁,保证涉及到的数据同一时刻只能被一个事务操作。如果显式的加锁,执行select语句,那么mysql就会加上next-key lock来避免产生幻读问题
next-key lock是行锁和间隙锁的结合体,首先使用行锁来锁住涉及到的数据,然后同时使用间隙锁锁住数据之间的间隙,不让其他事务进行插入。
快照读
快照读指的是读取到的数据是某一时刻的快照
快照读的触发语句:
快照读解决不可重复读和幻读问题的是MVCC。
MVCC
多版本并发控制。
MVCC依赖于几个关键组件
- 隐藏列
innodb中的隐藏列有:
- 本行数据的事务id,指向undo log的指针,自增id
- 基于undo log的版本链
每个指针会指向更早版本的undo log,从而形成一条版本脸
- ReadView
通过版本链和隐藏列,Mysql可以将数据恢复到指定版本。
所谓的ReadView就是事务某一时刻给整个事务系统(trx_sys)打快照,之后在进行读操作的时候,根据事务id和快照相比,从而判断数据是否对当前事务可见。
trx_sys中又包含了几个字段
- low_limit_id:表示生成ReadView时系统中应该分配给下一个事务的id。如果数据的事务id大于等于low_limit_id,则对该ReadView不可见。
- up_limit_id:表示生成ReadView时当前系统中活跃的读写事务中最小的事务id。如果数据的事务id小于up_limit_id,则对该ReadView可见。
- rw_trx_ids:表示生成ReadView时当前系统中活跃的读写事务的事务id列表。如果数据的事务id在low_limit_id和up_limit_id之间,则需要判断事务id是否在rw_trx_ids中:如果在,说明生成ReadView时事务仍在活跃中,因此数据对ReadView不可见;如果不在,说明生成ReadView时事务已经提交了,因此数据对ReadView可见。
总结:
依靠快照和事务id,MVCC决定哪些数据可见哪些数据不可见,从而解决了不可重复读和幻读问题。MVCC和核心是undo log的版本链,通过版本链可以恢复数据。
一致性
一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。
常见的例子就是,A用户给B用户转账1万元,转账前后两个账户钱的和是一致的,不会产生变化。相当于从一个合法的状态转换到了另一个合法的状态。
如何实现一致性
一致性是事务追求的最终目标,它的实现基于原子性、隔离性、持久性的实现基础。在此基础上,要求数据库提供保障,例如不允许向整形列插入字符串值等。
总结
- 原子性
- 要么全部执行,要么全部不执行
- undo log,回滚数据
- 持久性
- 事物的操作是永久的
- redo log,预写操作,保证操作不丢失及时落盘
- 隔离性
- 事务之间互相隔离,不影响对方执行
- 当前读使用锁机制,快照读使用MVCC
- 一致性
- 事务执行前后,数据的状态是合法的,数据库的完整性约束没有被破坏
- 要求我们实现好原子性、持久性、隔离性