InnoDB事务的实现

事务

数据库的事务有四大特性,即:原子性,隔离性,一致性和持久性.要保证这四个特性,InnoDB存储引擎做了一系列的操作,下面来看一下InnoDB是怎么保证数据库事务的这四个特性的.

隔离性

隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。

隔离性就很好说了,在InnoDB中,如果想要访问某一行记录的话,首先需要获取到它的锁,之后才能继续访问.通过锁,就能很好的实现了事务的隔离性. InnoDB默认的访问级别是Read Repeatable级别,避免了脏读,不可重复读等特性,并且在行锁的基础上又加上了gap Lock(间隙锁)来解决幻读的问题.InnoDB锁可以参见我之前的文章InnoDB存储引擎之锁的实现

虽然锁可以很好的解决数据库事务的隔离性问题,但是性能也得到了一定的瓶颈,就是读写冲突.InnoDB又实现了MVCC机制(多版本并发控制).主要是通过每个行记录后面保存两个隐藏的列来实现的.两个列保存了行的创建时间和过期时间(或删除时间). 这里的事件就是事务的Id,每个事务都有一个Id,Id是自增的. 下面来看一下主要是如何操作的:

  • select 会查找行记录的创建时间早于当前事务Id的行,或者说删除时间那一列Id大于自己的Id的行记录
  • insert 插入一条新的数据,并保存当前事务Id作为记录的创建时间.
  • delete 为删除的每一行保存当前事务Id为行删除标识
  • update 插入一条新的记录,并且把当前事务Id保存到新的行记录中,并设置旧的行记录的删除标识为此事务Id.

MVCC只在Repeatable Read和Read Commited两个隔离级别下工作,其他情况下都不兼容.因为Read UnCommited总是读取最新的行,而Serializable总是会对所有读取的行都加锁.

一致性

一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态.

了解了隔离性,一致性也就好说了,每个事务都是相互隔离的,那么最终,数据肯定也是一致性的.因为每个事务都互相不影响,并且事务具备原子性,那么一致性也就是自然而然了.

原子性

原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚.

原子性的实现就需要用到undo日志了.
undo日志到底是个什么呢.undo就是重做,主要记录了事务的行为,可以很好的通过其对页进行”重做”,如果数据库事务发生了错误需要进行回滚,就会使用undo日志将数据库回滚到修改之前的样子.

undo日志通过存储数据库修改的逻辑日志来回复数据库.比如,在进行回滚的时候,对于insert,InnoDB会完成一个delete操作;对于delete,InnoDB执行一个insert;对于update,InnoDB执行一个相反的update,来将修改前的行放回去.

undo日志的另一个而作用是MVCC,就是在InnoDB存储引擎中MVCC的实现是通过undo来完成的.当用户读取一条记录的时候,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,通过此来实现非锁定读取.

持久性

持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作

InnoDB作为事务引擎,数据都是存储在磁盘上的,由于磁盘太慢,InnoDB引擎提供了缓存(Buffer Pool)实现,Buffer Pool中包含了磁盘中部分页的映射.在访问数据的时候会先去缓存池中访问,如果没有,就把数据页读取到缓冲池中.在写入数据的时候,也是会先写入Buffer Pool,并且Buffer Pool中的数据页会定时的刷新到磁盘上. 这样做虽然效率变高了,但是当某一时刻数据库忽然宕机,而此时Buffer Pool中的数据还没来得及刷新回磁盘,那么就会导致数据的丢失.

为了保证就算遇到故障的时候数据库系统也不会出现数据丢失的情况,redo日志就出现了.redo日志保存了对数据库的修改.并且,redo日志是物理格式的,即记录的是对于每个页的修改,而不是具体的sql语句.并且redo日志是预写式日志,也就是说,所有的修改会先写入redo日志中,再更新到Buffer Pool.

通过这个redo文件的写入,就能够保证数据库的持久性了.如果数据库在某一时刻宕机了,当数据库重启的时候就会通过redo文件来进行数据恢复操作.

当然了,如果redo日志是直接向磁盘进行写入的话,不免操作也会变慢很多,因此InnoDB也引入了redo log buffer,redo日志是会先写入redo log buffer中,之后再在一定的条件下刷新会磁盘.

  • Master Thread每秒执行一次刷新操作,刷新回磁盘
  • 每个事务提交的时候,会刷新回磁盘
  • 当缓冲区的可用空间少于一半的时候,重做日志被刷新会磁盘.

到这里就有人要问了,为什么把redo日志写入磁盘就会比把真正的数据写入磁盘快呢,这是因为

  • 刷新脏页的IO是随机IO,因为每次修改的数据位置随机,但是redo是顺序IO,是顺序写入的.因此会快一点
  • 刷新脏页是以数据页为单位的,mysql默认大小是16k,在页上的每一次小的改动都要将整个页写入磁盘,而redo log只是写入真正写入的数据,无效IO大大减少.