目录

mysql ACID实现原理

事务

典型的MySQL事务是如下操作的:

start transaction;
# 一条或多条sql语句
commit;

其中start transaction标识事务开始,commit提交事务,将执行结果写入到数据库。如果sql语句执行出现问题,会调用rollback,回滚所有已经执行成功的sql语句。当然,也可以在事务中直接使用rollback语句进行回滚。

MySQL中默认采用的是自动提交(autocommit)模式。在自动提交模式下,如果没有start transaction显式地开始一个事务,那么每个sql语句都会被当做一个事务执行提交操作。

针对某次连接也可以临时指定不自动提交,set autocommit =0即可。

特殊操作: 在MySQL中,存在一些特殊的命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务;如DDL语句(create table/drop table/alter/table)、lock tables语句等等。 不过,常用的select、insert、update和delete命令,都不会强制提交事务。

ACID特性

ACID是衡量事务的四个特性:

原子性(Atomicity,或称不可分割性) 一致性(Consistency) 隔离性(Isolation) 持久性(Durability)

按照严格的标准,只有同时满足ACID特性才是事务;但是在各大数据库厂商的实现中,真正满足ACID的事务少之又少。因此与其说ACID是事务必须满足的条件,不如说它们是衡量事务的四个维度。

原子性

实现原理:undo log。

MySQL的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种事务日志:redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。undo log属于逻辑日志,它记录的是sql执行相关的信息。

实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

持久性

持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。

实现原理:redo log。

redo log和undo log都属于InnoDB特有的事务日志。redo log是物理日志。

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宕机而丢失,从而满足了持久性要求。

为什么binlog不能做到crash-safe

简单来说一句话:binlog可能和实际的库不一致。

假如只有binlog,有可能先提交事务再写binlog,有可能事务提交数据更新之后数据库崩了,还没来得及写binlog。我们都知道binlog一般用来做数据库的主从复制或恢复数据库,这样就导致主从数据库不一致或者无法恢复数据库了。同样即使先写binlog再提交事务更新数据库,还是有可能写binlog成功之后数据库崩掉而导致数据库更新失败,这样也会导致主从数据库不一致或者无法恢复数据库。所以只有binlog做不到crash-safe。为了支持crash-safe,需要redolog,而且为了保证逻辑一致,事务提交需要两个阶段:prepare阶段和commit阶段。写redolog并落入磁盘(prepare状态)–>写binlog–>commit。commit的时候是不会落盘的。

如果binlog写成功之后,将redolog置成commit的时候数据库崩了,如果在commit的时候redolog才落盘,由于事务是否成功以binlog为依据,上面的情况下事务是成功的,但是redolog没有写到磁盘,丢了。恢复之后数据库与binlog就不一致了。如果在prepare阶段落盘,上面的情况下redolog已经写入到文件了(在prepare阶段已经写盘了),恢复的时候不会丢数据。

同样的,如果不分两个阶段。假如redolog和binlog独立,那么还是会出现“为什么binlog不能做到crash-safe”里面描述的问题:数据库与binlog不一致。

为什么有了redo log可以做到crash-safe

先写redo log日志在改库,再加上二阶段。

redo log与binlog

我们知道,在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的:

(1)作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。

(2)层次不同:redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层(可以参考文章前面对MySQL逻辑架构的介绍)实现的,同时支持InnoDB和其他存储引擎。

(3)内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。

(4)写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元:

当事务提交时会调用fsync对redo log进行刷盘;这是默认情况下的策略,修改innodb_flush_log_at_trx_commit参数可以改变该策略,但事务的持久性将无法保证。 除了事务提交时,还有其他刷盘时机:如master thread每秒刷盘一次redo log等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快。 innodb_flush_log_at_trx_commit=0:每秒一次将Log Buffer中数据写入到Log File中,并且Flush到磁盘。事务提交不会主动触发写磁盘操作。 innodb_flush_log_at_trx_commit=1:每次事务提交时将Log Buffer数据写入到Log File中,并且Flush到磁盘。 innodb_flush_log_at_trx_commit=2:每次事务提交时将Log Buffer数据写入到Log File中,但不立即Flush到磁盘,MySQL会每秒一次刷新到磁盘。 由于进程调度问题,每条一次操作不能保证每一秒都执行一次。

sync_binlog=0:每次事务提交后,将Binlog Cache中的数据写入到Binlog文件,但不立即刷新到磁盘。由文件系统(file system)决定何时刷新到磁盘中。 sync_binlog=N:每N次事务提交后,将Binlog Cache中的数据写入到Binlog文件,调用fdatasync()函数将数据刷新到磁盘中。

隔离性

隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。

隔离性追求的是并发情形下事务之间互不干扰。简单起见,我们主要考虑最简单的读操作和写操作(加锁读等特殊读操作会特殊说明),那么隔离性的探讨,主要可以分为两个方面:

(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性 (一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性

不过需要说明的是,RR虽然避免了幻读问题,但是毕竟不是Serializable,不能保证完全的隔离: 例子,如果在事务中第一次读取采用非加锁读,第二次读取采用加锁读,则如果在两次读取之间数据发生了变化,两次读取到的结果不一样,因为加锁读时不会采用MVCC

一致性

一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。

可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。

总结

原子性A实现原理:undo log。 持久性D实现原理:redo log。 隔离性I实现原理:mvcc + 锁。mvcc是解决读的隔离性,可以不用锁,更快。但是写写之间还是要靠排他锁。