MySql是如何实现事务的ACID

事务的四大特性

事务的四大特性ACID分别是,A-原子性(Atomicity)、C-一致性(Consistency),I-隔离性(Isolation)、D-持久性(Durability)。一致性是最终的目的,原子性、隔离性、持久性是为了保证一致性所做的措施。

原子性

原子性就是指一个事务就是一个不可分割的工作单元,要么全部执行成功,要么全部执行失败,没有中间状态或者只执行一部分。mysql的InnoDB引擎是靠ubdo log(回滚日志)来实现的。undo log能够保证在事务回滚时,撤销所有所有已经执行成功的sql。
undo log 属于逻辑日志,它记录是sql执行相关的信息。当事务对数据库进行修改时候,InnoDB会生成与之对应的undo log。如果如果事务执行失败或者调用的rolback,导致事务需要回滚,InnoDB引擎会根据undo log中的记录,将数据回滚到之前的样子。
例如:在执行insert语句时会生成delete语句的undo log。反之执行delete语句也会生成相关的insert语句的undo log。执行update 语句的时候也是如此。不过执行update语句在执行undo log 回滚时候有可能涉及到MVCC,主要是保证在执行undo log的时候select能看到哪个版本的数据。

持久性

持久性是指事务一旦提交,对数据库的操作就是永久性的。接下来其他操作和异常故障不对它有任何影响。
我们都知道mysql的数据最终是存储在磁盘中的,所以才有了磁盘容量大小决定数据容量大小。但是如果对mysql的操作都是通过读写磁盘来进行的话,那么光是磁盘的I/O就够把效率大大降低了。
所以InnoDB为mysql提供了缓冲池(Buffer Poll),Buffer Pool中包含了磁盘部分数据页的映射。
当从数据库读取时候,会先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘中读取后放入到Buffer Pool中。
当数据库写数据时候,会先写到Buffer Pool中,Buffer Pool中更新数据会定期刷新到磁盘中(此过程称之为刷脏)
虽然Buffer Pool为mysql提高了读写效率,但是却也带来了新的问题。那就是刚更新到Buffer Pool中还没来得及刷新到磁盘中时,Mysql突然宕机,这就会导致数据丢失,造成事务的持久性无法保证了。

为了解决这一缓存一执行问题,redo log出现了,在对Buffer Pool中的数据进行修改的时候通过redo log 记录这次操作。当事务提交时会通过fsync接口对redo log进行刷盘。
因为在事务提交时候会把redo log是同步到磁盘中,所以mysql出现宕机时。可以从磁盘中读取redo log进行数据修复,从而保证事务的持久性。
redo log采用的是预写的方式记录日志,即先记录日志,再更新Buffer Pool。这样就强行保证了,数据只要保存在redo log中就一定会存储到磁盘中。

这里要解释一下:read log也是写磁盘,刷脏也是写磁盘,为啥要先记录redo log,而不是直接脏刷呢?
主要原因就是redo log 比脏刷快很多。
第一点:redo log是追加操作日志。是顺序I/O;而脏刷是随机I/O,因为每次更新的数据不一定都是挨着的,也就是随机的。
第二点:脏刷是以数据页(Page)为单位(即每次至少从磁盘读取一页数据到内存中,或者说最少刷一页数据到磁盘中。)mysql默认页大小是16kb,对一个页上的修改,都要整个页都刷到磁盘中;而redo log 只包含真正需要写入磁盘的操作日志。

mysql还有一个记录操作日志的叫:binlog,那么redo log 和binlog是什么区别呢?
第一个区别点:
redo log是用来更新缓存的,为了保证mysql就算宕机也不会影响事务的持久性;binlog是用来记录什么时间操作了什么。主要有时间点,可以保证将数据恢复到某个时间点,也有用于主从同步数据。
第二个区别点:
redo log 是存储引擎InnoDB实现的(MYISAM就没有redo log),而binlog是在mysql服务层面存在的,任何其他引擎也有binlog。存储内容上,redolog是物理日志,基于磁盘的数据页,binlog是逻辑日志,存储一条执行SQL。

第三个区别点:
redo log 默认情况下是事务提交时候刷盘可以通过参数:
innodb_flush_log_at_trx_commit
来改变策略,可以不用等到事务提交时才进行刷盘。
如:可以设置成每秒提交一次。
binlog是在事务提交时写入。

隔离性

原子性和持久性都是基于单个事务内部的措施,而隔离性是多个事务之间的相互隔离,互不影响的特性。
我们都知道事务之间隔离级别最严谨的是串行化(Serializable),但是隔离性越高,性能越低。所以一般不使用串行化隔离级别。,对于隔离性,我们分为两种讨论。
一个事务中写操作对另一个事务中写操作的影响。
一个事务中写操作对另一个事务中读操作的影响。

首先,事务间的写操作其实是靠mysql的锁机制来实现隔离的,而事务间的写和读操作是靠MVCC机制实现的。

锁机制
mysql中的锁住要有
按照功能分:读锁和写锁
按照作用范围分:表级锁和行级锁
还有意向锁和间隙锁等。
读锁:又称“共享锁”,是指多个事务可以共享一把锁,都只能访问数据,而不能修改呢数据。
例如SQL:select * from table where id = 1 lock in share mode;
对于这个sql在执行时未提交之前,再执行update table set name=‘张三’ and where id=1;会直接报错,因为这条记录已经被加上了共享锁。而就算update语句在后面
加上 lock in share mode也不可以,因为update、insert、delete在执行时会默认加上排他锁的。但是我可以在执行查询SQL的时候在后面加上 lock in share mode进行查询。
写锁:又称“排它锁”,是不能和其他事务共享数据的。如果一个事务获取到了一个数据的排它锁。那么其他事务就不能在获取该行的其他锁。包括共享锁和排它锁。
例如:select * from table where id=1 for update;这条sql语句是对查询加上了排他锁(如果查询条件走索引,就锁住索引区间,若没走索引或者使用的MyISAM存储引擎则会锁住整张表)。update 、insert、delete会自动加上排它锁的。

共享锁和排它锁有统称为悲观锁,而乐观锁需要自己手动实现,在表中添加一个version字段,每次查询都查此字段,然后更新时候比较此字段是否为查出时的version值。若相同则直接更新,不相同则先获取最新的值处理后再更新。

表级锁:
是指将整个表进行锁定,性能较差。不同的存储引擎支持锁的颗粒度不同,MyISAM引擎支持表级锁,InnoDB引擎支持表级锁也支持行级锁。
行级锁:
会将需要操作的相应行进行锁定,性能好。
意向锁:意向锁是表级锁,如果在一个事务已经对表中的某个数据加上了排它锁或共享锁,这样当下一个事务来进行锁表的时候发现已经存在意向锁了,就会被阻塞,如果不加意向锁的话,第二个事务来锁表的时候需要一行一行的遍历查看是否有数据已经被锁住了。
间隙锁:间隙锁是为了防止幻读而加的锁,加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间(但是并不包含当前记录)。这样就保证了在间隙锁执行的时候,新增的数据会阻塞,保证了一个事务中的两次查询获得的记录数都是一致的。
Next-Key Lock:Next-Key Lock是行级锁和间隙锁的结合产生的锁,因为间隙锁是不会锁住当前记录的而Next-Key Lock是会将当前记录也锁住的。
例如:如果一个表中有三条数据分别是:

idnameage
1小裴16
1小陈17
1小张17
1小王20

那么执行sql:select * from table where age=17 for update 实际间隙锁会锁住age 区间是(16,17),(17,20)但是next-key Lock锁住的是(16,17),(17,20)区间加间隙锁,同时number=17加记录锁。
锁机制保障了多个事务间的写操作的隔离,而多个事务间的读和写操作的保证是需要通过MVCC机制来保证的。

MVCC机制
MVCC全称是【Multi-Version ConCurrency Control】即多版本控制协议。

MVCC的主要是靠在每行上增加隐藏列和undo log 来实现的,隐藏列主要包括,改行数据创建的版本号(递增的),删除时间,指向undo log 的指针。
那么MVCC是如何保证读写隔离的呢?主要通过快照读和当前读两个操作。
快照读:
MVCC为了保证并发效率,在进行读取数据的时候是不加锁的,在执行select的时候(不带锁的普通select),会读取当前数据的版本号。如果select还没返回结果时,有事务将此行数据进行了修改,那么版本号就会比执行select的时候大,所以为了保证select读取数据一致性,就会读取小于或者等于当前版本的数据。这个历史版本就是从undo log中获取的。

当前读:
当执行insert 、update、delete的时候,是读取当前最新的版本数据,并且会给当前记录加锁,用来保证在操作的时候不会被别的事务将版本号进行修改。
像普通的select就是快照读即读取的有可能就是数据的历史版本。
insert、update、delete、select … lock in share mode 和select … for update 读取的就是当前读,即读取的都是数据的最新版本。
其实将隔离级别设置为Serializable也是可以实现读写隔离的,但是并发效率会比低很多,所以一般用的很少,但是MVCC是读不加锁的,只有在写的时候才会加锁,从而提高的并发的效率。
通过MVCC机制保证了多个事务间的读写隔离,从而实现了事务的隔离性

一致性

一致性是指事务执行前后,数据的一致性,事务前后数据的完整性没有破坏。并且都是合法的数据状态。
其中一致性的指标有:
索引的完整性(唯一索引,不重复等),数据列的完整(字段类型,长度,大小符合要求),外键约束等。
实现一致性的措施:
保证原子性、持久性、隔离性,如果这些特性都无法保证,那么一致性就无法保证。从数据库层面来看,除了前面那几个特性的保证外,对字段的一致性是有保证措施的,例如整型的字符不能传入字符串、时间等格式。字符串的长度不能超过列的限制。但是在应用层面开发者也是要自己来保证的。

例如:从A转账给B一部分金额,那么就要保证,从A从将金额扣除多少就要去给B增加多少金额,如果只扣除A的金额,而没有增加B的金额,是无法保证一致性的。

另外mysql还通过两个阶段提交事务,保证redo log和bin log之间的数据一致性问题。
通过上面的介绍持久性的时候解释了,redo log 和bin log 的区别。在区别第三条有说到,在默认情况下,事务提交时候,既写redo log也有写binlog,那么他们是如何协调一致性的呢?事务提交成功以写入哪个日志为准呢?
mysql通过两个阶段提交来保证两个日志的一致性。
第一阶段的提交:将redo log提交到磁盘,并将状态改为prepare状态,binlog不做任何操作。
第二阶段的提交:1、生成事务操作的binlog,并将binlog写入到磁盘中。2、调用引擎的提交事务接口,将redo log的状态从prepare状态改为commit状态,事务提交完成。

通过上面两个阶段提交保证了事务数据的一致性。
当事务提交redo log处于prepare状态的时候,发生mysql宕机或者奔溃,则会执行事务回滚。
当事务提交redo log 处于commit阶段时候,发生奔溃会执行事务恢复。本机事务会通过redo log恢复。而如果是主从数据库的话,在commit阶段,会根据binlog对数据库进行恢复。

这就是以写入binlog成功为提交事务成功的依据。因为一般在崩溃恢复的时候都是用binlong进行恢复的,如果还未生成binlog,只写入了redo log。在恢复的时候redo log恢复的是一个版本的数据,而通过bin log恢复的从库数据会是之前的一个时间点的binlog版本的数据,这样数据就导致不一致了。

总结

Mysql事务的ACID,一致性是最终目的
保证一致性的措施有
A原子性:靠undo log来保证(异常或者执行失败进行回滚)
D持久性:靠redo log来保证(保证当mysql宕机或者停电后,可以通过redo log最终将数据保存在磁盘中)
I隔离性:事务之间的读写靠mysql的锁机制来保证隔离,事务间的写操作靠MVCC机制(快照读,当前读)来保证隔离性
C一致性:事务的最终目的,即需要数据库层面保证,又需要应用层面进行保证,并且MySQL底层通过两阶段提交事务保证了事务持久化时的一致性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值