事务的概念与意义
一个行为可能涉及到多个操作,如果进行到中途系统忽然崩溃,那么可能会造成巨大的损失。
我们想要保证一系列的数据库操作,要么全部成功,要么全部失败。这就是事务。
MySQL的InnoDB支持事务,而MyISAM不支持,所以我们这里谈的MySQL事务都基于InnoDB引擎。
MySQL事务的使用
MySQL的事务可以保证数据一致性,可以自己创建事务:
|
|
如果事务有一条出错了,后面的还会继续执行,所以需要用到回滚机制。如果某一条执行错误就回滚。
ACID
事务的ACID
Atomicity 原子性:事务的操作要么都执行,要么都不执行 Consistency 一致性:数据的完整性不会因为事务而破坏 Isolation 隔离性:多个事务同时执行的时候互不干扰 Durability 持久性:事务对数据的更改是永久的
隔离级别
隔离的越少,越容易出现问题,隔离的越严密,效率越低。
这就有点像分布式系统对一致性的追求,我们需要根据实际场景来选择隔离等级。
不同的隔离级别会有不同的结果。SQL的隔离等级有读未提交、读提交、可重复读和串行化,它们的并行效率依次降低,安全性依次提高。下面通过一个具体的例子来了解它们。
假设数据库中有一个值,目前是1,有两个事务A、B:
读未提交 RU
一个事务还没提交就能被另一个事务看到。
也就是B还没提交的时候,A就能读到修改的值,此时V1=2,V2=2,V3=2
读未提交就是每次直接返回记录中最新的值。
读提交 RC
一个事务提交之后就能被另一个事务看到。
如果是读提交,需要在提交之后才能读到修改,此时V1=1,V2=2,V3=2
对于读提交,数据库会创建一个视图,访问时以视图的逻辑为准,在每次SQL语句开始执行的时候创建。
可重复读 RR
RR是InnoDB的默认事务隔离级别。
事务执行过程中看到的数据和启动时看到的一致
如果是可重复读,事务的整个过程读到的数据都一样,V1=1,V2=1,执行完之后就不需要一样,V3=2
对于读提交,数据库会创建一个视图,在每次事务启动的时候创建。
串行化 Serializable
同一行记录读和写都会加锁,不允许任何并行读写
如果是串行化,B在执行修改操作时被锁住,等到A事务执行完,B才继续执行,此时V1=1,V2=1,V3=2
串行化是通过加锁的方式来避免一切并行访问。
配置隔离级别
事实上每一种隔离级别都要对应的使用场景,如果要配置的话,可以设置启动参数 transaction-isolation 的值
比如改为READ-COMMITTED:读提交
事务隔离的实现:MVCC
前面我们说了undo log,记录回滚的日志。
MVCC(Multi-Version Concurrency Control)即多版本并发控制,可以避免同一个数据在不同事务之间的竞争。通过MVCC机制,只有写写才会阻塞,读读、读写、写读都可以并行,从而大大提高效率,同时允许多个版本同时存在。
前面提到的读提交和可重复度用到了MVCC。
在介绍MVCC之前,先引出ReadView的概念。
ReadView
是数据库某一时刻所有未提交事务的快照。
读未提交和串行都不会用到MVCC
如果是读已提交RC,每次开始查询时都会生成一个ReadView。
如果是可重复读RR,事务在第一次读取数据的时候就生成一个ReadView,之后再也不会生成。
包括几个参数:
参数 | 说明 |
---|---|
creator_trx_id | 当前事务Id |
m_ids | 生成ReadView时,系统中活跃的读写事务的事务Id列表 |
min_trx_id | 生成ReadView时,系统中活跃的读写事务的最小事务Id |
max_trx_id | 生成ReadView时,将在下一次分配的事务Id |
注意,这里的min_trx_id就是m_ids列表的最小值,而max_trx_id并不是m_ids的最大值,而是下次要分配的事务id
当一个事务创建的瞬间,已提交的事务、正在活跃但还未提交的事务、未开始的事务构成了这个事务的ReadView。
隐藏列
InnoDB存储引擎对于每一个聚类索引都包含了两个隐藏列:
trx_id:事务Id,把改变某条聚簇索引的事务Id记录到这个列
roll_pointer:回滚指针,也就是指向对应undo log的指针
通过roll_pointer可以串成一个undo log链表,称为事务链
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
判断事务使用的版本
如果被访问的trx_id小于ReadView的低水位Id,即落在上图的绿色区域。那么说明该版本的事务在生成ReadView之前就提交了,所以可以被当前事务访问。
如果被访问的trx_id大于ReadView的高水位Id,即落在上图的红色区域。那么说明该版本事务在生成ReadView之后才生成,不能被当前事务访问。
如果介于最小Id和最大Id值之间,即落在上图的蓝色区域。需要判断trx_id是否在m_ids列表中,如果在的话说明创建ReadView时该版本事务还活跃,不能访问。如果不在的话说明创建ReadView时该版本已经提交,可以访问。
举个例子,当前事务id=10,目前正在活跃的事务
m_ids = [5,7,8]
,那么min_trx_id = 5
,也就是最小值,而max_trx_id
是11。 对于每一行数据,都有一串事务版本链,会顺着去往前看,如果当前版本的事务id小于min_trx_id,也就是这一行的事务id如果<5,那么就说明这条记录的更新一定在创建事务之前就提交了,所以可以访问。 如果当前版本的事务id大于max_trx_id,也就是这条记录的当前事务版本>=11,那也就是说这个版本一定是当前事务不可见的。会往前寻找更旧的版本。 如果当前事务的版本介于二者之间,也就是处于[5:9]之间(自己的当前版本10肯定是可见的),然后就会看是否处于m_ids
里面,如果在里面就说明是还没提交的,就不可见,如果不在里面就说明事务创建的时候已经提交了,就可见。
长事务的问题
所谓长事务,也就是运行时间比较长,长时间没有提交的事务。
长事务会导致阻塞、占用锁资源、影响性能,应该尽量避免。
可以通过以下命令查看时间超过一分钟的长事务:
|
|