Innodb引擎支持以事务的方式执行SQL,事务包含ACID四个特性,分别是原子性、一致性、隔离性和持久化。
原子性
原子性是指开启事务后,使用commit提交事务或rollback回滚事务,使事务内的多条修改语句同时成功或失败。
原子性是通过redo log和undo log的方式实现。一起成功是通过在执行变更语句时,先把执行的sql先记录到redo log。记录成功后,代表变更结果已经持久化。一起都失败是事务开始后,对每条记录的变更都会记录undo log,在回滚事务的时候,通过undo log里记录的之前版本的数据进行回滚数据。
Redo Log为同个数据页下的多条数据变更记录一条日志
sql
TransactionID: 127
PageID: 462
Operation: MULTIPLE_UPDATES
Updates:
- UpdateType: UPDATE
Before Image: (1, '小明')
After Image: (1, '小李')
- UpdateType: INSERT
After Image: (2, '小红')
- UpdateType: DELETE
Before Image: (3, '小张')
Undo Log为每条变更的数据独立记录一条日志
sql
TransactionID: 126
PageID: 459
Operation: UPDATE
Before Image: (1, '小明')
一致性
一致性是指事务结束后,各个表变更的记录状态是一样的。要么是成功状态,要么是失败状态,也就是开启事务之前的状态。在数据库发生意外奔溃时,会通过redo log和undo log恢复到崩溃之前的状态,确保数据库前后的一致性。
一致性是通过原子性、隔离性和持久性保证的。
隔离性
事务有不同的隔离级别,数据变更后,在不同事务之间,可见性不一样。
读未提交
是最弱的隔离级别,当前事务可以看见其他未提交事务的数据变更结果,这种现象称为脏读。
读已提交
当前事务可以看见其他已提交事务的数据变更结果。例如,第一次查询到的列a值等于1,第二次查到列a值等于2。这种现象称为不可重复读,也就是事务内没做修改,但是两次select查询到的结果不一样。
可重复读
当前事务无法看到其他事务的数据变更结果。也就是事务内没做修改,两次select查询到的结果基本是一样的。除了一种情况,第一次查询了范围数据,然后其他事务在同一个到范围内插入了数据,这时原来的事务再次查询同个范围的数据,就会读到另一个事务插入的新数据。这种现象称为幻读。但是Innodb的这个级别不会有幻读,因为它使用了临键锁(记录锁+间隙锁),会把查询范围内,存在的数据加上记录锁,不存在的数据范围加了间隙锁。这样其他事务想插入数据,就需要先获取锁。
但是另一种情况也会出现不可重复度。例如,有一条记录a=1。事务A先开启查询到a=1,事务B先执行a=a+1,并提交事务。这时事务A也执行a=a+1,再查询a,得到的结果是3而不是2。原因是update操作是先读取再更新记录,读取方式是当前读,即会读到最新的数据状态。所以事务A在更新a的时候,a已经等于2,然后被当前事务改成3。这时,事务再取读a值就是3。
序列化
序列化就是通过加锁的方式一个个执行事务,不存在可见性问题。
MVCC
对于读已提交和可重复读这两个隔离级别,Innodb使用了无锁读的技术MVCC和读视图,来提升查询性能。MVCC会在记录被修改时记录多个版本的数据,其实是一条最新版本的数据,然后跟着一串undo log。其他事务在开启一致性读后,根据事务ID在读视图里查询可见的数据版本。
这两个隔离级别的区别在于,读已提交是每次执行查询语句之前,都会重新创建读视图,这就会每次读到最近的已提交记录。可重复读是在第一次执行查询语句的时候创建读视图,这样能保证后面读到的数据是一样的。
读视图
MVCC里不同版本数据对事务的可见性由读视图来控制,读视图主要是一组事务ID,分为已提交事务、活跃事务、不可见事务三部分。
- 已提交事务是指在当前事务开启的时刻,这些事务已经提交。这部分事务的变更对当前事务都是可见的。比当前活跃事务里最小的事务ID还小的事务,必然是已提交事务。在最小的事务ID和已分配的最大事务之间的非活跃事务,也算已提交事务。
- 活跃事务是指当前事务开启的时刻,还未提交的事务。这部分事务的变更对当前事务都是不可见的。当前还活跃的事务ID集合。
- 不可见事务是指当前事务开启的时刻,还不存在的事务。这部分事务的变更对当前事务都是不可见的。比当前系统已分配的最大事务ID还大的事务,必然是不可见事务。
持久性
持久性是指把数据变更写入磁盘,实现持久化的目的。Innodb使用AWL(ahead write log)方式,在事务提交的时候先把变更写入redo log,然后再同步到磁盘。
redo log写入磁盘的过程中分为三个状态,写入内存的redo buffer、写入文件的page cache、最后同步到硬盘。只有同步到硬盘了,数据才算最终持久化成功。
根据redo log的不同状态,提交事务时又分为三种同步策略,由参数innodb_flush_log_at_trx_commit控制:
0:redo log先写入redo buffer,由后台线程每秒写入page cache并同步到磁盘。这个过程数据库crash会导致数据丢失。
1:redo log会马上同步到磁盘(这个是默认值)
2:redo log写入到page cache,由后台线程每秒将日志写入磁盘。这个过程数据库crash不会影响page cache的内容,但是服务器宕机会导致数据丢失。