ACID和实现原理

date
Nov 17, 2022
slug
acid
status
Published
tags
数据库
summary
事务的 ACID 特性是如何实现的
type
Post

简介

本文主要解释事务的ACID特性和相关实现原理,背景是Mysql5.6,数据引擎是Inno db

基础概念

事务是指一组操作,这组操作要么全都执行成功,要么全都不执行。
MySQL的架构
首先我们了解一下MySQL的架构
notion image
MySQL的结构一共可以分为三层
  1. 链接层,处理链接,授权认证等
  1. 服务器层,负责语句的解析、优化
  1. 存储引擎层,其中使用最广泛的存储引起是InnoDB。MySQL本身是不支持事务的,事务是由存储引擎实现的。
事务的提交
Mysql中默认是autocommit(自动提交)模式,如果没有显示的开启一个事务,那么每个SQL语句都会当作一个事务执行。
notion image
我们也可以通过 set autocommit = 0,关闭自动提交;需要注意的是,autocommit参数是针对连接的,在一个连接中修改了参数,不会对其他连接产生影响。
notion image
如果关闭了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定义了四种隔离级别,每种隔离级别均解决了一定的问题
notion image
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
  • 一致性
    • 事务执行前后,数据的状态是合法的,数据库的完整性约束没有被破坏
    • 要求我们实现好原子性、持久性、隔离性
 

Ref


© hhmy 2019 - 2024