1、事务的特性
| 特性 | 描述 |
|---|---|
| 原子性(Atomicity) | 事务的操作要么做要么不做 |
| 一致性(Consistency) | 数据在事务操作前后必须满足业务规则 |
| 隔离性(Isolation) | 允许多个并发事务同时对数据进行读写和修改的能力,隔离性可以防止多个事务交叉执行导致的数据的不一致 |
| 持久性(Durability) | 事务结束后,对数据的修改是永久的 |
2、事务的隔离级别
事务并发执行时遇到的一致性问题:
脏写:一个事务修改另一个未提交事务修改过的数据
脏读:事务B修改数据,但未提交,事务A读取数据,B回滚,则读到脏数据。
不可重复读:事务A第一次读数据,事务B修改数据提交,事务A第二次读数据,发现两次数据不一致(不可重复读侧重于修改,需要锁住满足条件的行)
幻读:事务A update所有的行,事务B插入一行,事务A会发现表中有未修改的行(幻读侧重于增加和删除,需要锁表)
事务的隔离级别
| 隔离级别 | 描述 |
|---|---|
| 读未提交(read uncommitted)RU | 在一个事务中,可以读取到其他事务未提交的数据变化 |
| 读已提交(read committed)RC | 在一个事务中,可以读取到其他事务已经提交的数据变化 |
| 可重复读(repetable read)RR | 在一个事务中,直到事务结束前,都可以反复读取到事务刚开始看到的数据 |
| 串行化(serializable) | 对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行 |
不同隔离级别可能发生的一致性问题
| 事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 是 | 是 | 是 |
| 读已提交 | 否 | 是 | 是 |
| 可重复读 | 否 | 否 | 是(InnoDB除外) |
| 串行化 | 否 | 否 | 否 |
3、MVCC原理
3.1、版本链
数据记录都必须包含trx_id和roll_pointer两个隐藏列
| 列名 | 是否必须 | 占用空间 | 描述 |
|---|---|---|---|
| row_id | 否 | 6字节 | 优先使用用户自定义主键,如果没有自定义主键,则选择一个不允许存储null值的唯一索引列作为键,如果表中没有不允许存储null值的唯一索引列,则InnoDB会为表默认添加一个名为row_id的隐藏列做主键。 |
| trx_id | 是 | 6字节 | 事务id |
| roll_pointer | 是 | 7字节 | 回滚指针,每次对数据记录进行修改时,都会把旧的版本写到undo 日志中,该回滚指针指向了该数据行的undo日志 |
roll_pointer组成如图所示:
点击查看【processon】
版本链
每次更新记录后,都会将旧值放入到undo log中,每条 UPDATE 类型 undo log都有一个roll_pinter属性,指向了一个更早的版本记录,随着更新次数的增多,所有的版本会被roll_pointer属性连接成一个链表,这个链表称为版本链。
3.2、ReadView
对于读未提交隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了。
对于串行化隔离级别的事务来说,InnoDB使用加锁的方法来访问记录。
对于读已提交和可重复读隔离级别的事务来说,都必须保证读到已经提交事务修改过的记录,核心问题是,需要判断版本链中的哪个版本是当前事务可见的。
于是InnoDB提出了Read View(一致性视图)的概念,ReadView中主要包含4个比较重要的内容:
- m_ids:在生成ReadView时,当前系统中活跃的读写事务的事务id列表
- min_trx_id:在生成ReadView时,当前系统中活跃的读写事务中最小的事务id,也就是m_ids的最小值
- max_trx_id:在生成ReadView时,系统应该分配给下一个事务的事务id
- creator_trx_id:生成该ReadView的事务的事务id
只有在对表记录进行修改时才会为事务分配唯一的事务id,否则一个事务的事务id值默认为0 对于只读事务来说,只有在它第一次对某个用户创建的临时表执行增删改操作时,才会为这个事务分配一个事务id,否则是不分配事务id的。 对于读写事务来说。只有在它第一次对某个表(包括用户创建的临时表)执行增删改操作时,才会为这个事务分配一个事务id,否则是不分配事务id的。
访问某条记录时,判断记录的某个版本是否可见的步骤:
- 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id相同,意味着当前事务在访问他自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id,表明生成该版本的事务在当前事务生成ReadView之前已经提交,所以该版本可以被当前事务访问
- 如果被访问版本的trx_id属性值大于或者等于ReadView的max_trx_id,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问
- 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,则需要判断trx_id是否在m_ids列表中。如果在,则说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以访问;如果不在,说明创建ReadView时生成该版本事务已经提交,该版本可以被访问。
读已提交和可重复读级别生成ReadView的时机
读已提交:每次读取数据前生成一个ReadView
可重复读:在第一次读取数据时生成一个ReadView
3.3、二级索引与MVCC
只有在聚簇索引记录中才有trx_id和roll_pointer的隐藏列,那么如果某个查询语句使用二级索引查询,该如何判断可见性呢?
- 二级索引页面的Page Header部分有一个名为PAGE_MAX_TRX_ID的属性,每当对页面中的记录执行增删改操作时,如果执行该操作的事务的事id大于PAGE_MAX_TRX_ID的属性值,就会把PAGE_MAX_TRX_ID的属性值设置为该操作事务的事务id,这就意味着PAGE_MAX_TRX_ID属性表示修改二级索引页面的最大事务id是什么。当SELECT 语句访问某个二级索引记录时,首先会看一下ReadView的min_trx_id是否大于该页面的PAGE_MAX_TRX_ID属性。如果是,说明该页面的所有记录对该ReadView可见,否则就得回表后判断可见性。
- 利用二级索引记录的主键进行回表操作,得到聚簇索引记录后根据不同隔离级别的规则找到对该ReadView可见的第一个版本,然后再判断版本中的相应二级索引列的值是否与利用该二级索引查询时的值相同。如果是就将本条记录发送给客户端,否则就跳过该记录。
3、关于purge
前面说到过,如果对应的Undo页面链表为update undo链表且不符合被重用的条件(即Undo页面链表只使用了一个页面,且已使用空间小于该页面的3/4)则该Undo页面链表的TRX_UNDO_STATE属性会被设置为TRX_UNDO_TO_PURGE,并将该undo slot的值设置为FIL_NULL,然后将本次事务写入的一组undo日志放到History链表中。
之前说过每个回滚段对应着一个名为Rollback Segment Header的页面,该页面有两个属性
- TRX_RSEG_HISTORY : 表示history链表的基节点
- TRX_RESG_HISTORY_SIZE:表示History链表占用的页面数量
即每个回滚段都有一个History链表,一个事务在某个回滚段中写入的一组 update undo日志在该事务提交后,如果对应Undo页面链表不符合重用原则,就会加入到回滚段的History链表中。
delete mark: 1、执行delete操作时,在事务提交前,做了一次delete mark操作,将待删除记录的deleted_flag标识设置为1,在事务提交后由专门的线程进行purge操 作 2、更新主键时,先要将旧记录进行delete mark操作,然后根据更新后各列的值创建一条新记录,并将其插入到聚簇索引中。在update语句所在事务提 交前,对旧记录只执行一个delete mark操作,将待删除记录的deleted_flag标识设置为1,在事务提交后由专门的线程进行purge操作
但是加入到History链表的update undo日志所占用的存储空间没有释放;为了支持MVCC,delete mark仅仅只是在记录打上了一个删除标记,并没有真正删除(Undo Log Header中的TRX_UNDO_DEL_MARKS属性就是用来标记本组undo日志是否包含delete mark操作而产生的undo日志),所以InnoDB应该在合适的时候把update undo日志以及仅仅被标记为山删除的记录彻底删除掉,这个删除操作就称为purge。
purge相关规定:
- 一个事务在提交时会为这个事务生成一个名为事务no的值,该值用来表示事务提交的顺序,先提交的事务的事务no值小,后提交事务的事务no值大
一组undo日志中对应的Undo Log Header部分有一个名为TRX_UNDO_TEX_NO的属性。当事务提交时,就会把该事务对应的事务no值填到这个属性中,因为事务no值代表事务提交的顺序,而History链表又是按照事务提交的顺序来排列各组undo日志的。
- 一个ReadView中包含一个事务no的属性,在生成一个ReadView时,会把当前系统中最大的事务no值+1赋给该属性
purge操作过程:
InnoDB将当前系统中所有的ReadView按照创建时间连成了一个链表,当专门的后台线程执行purge操作时,就把系统中最早生成的ReadView取出来。如果系统中不存在ReadView则立即创建一个(新建的ReadView的事务no值肯定比当前已经提交的事务no值大)。然后从各个回滚段的History链表中取出事务no值较小的各组undo日志,如果一组undo日志的事务no值小于当前系统最早生成的ReadView的事务no属性值,就意味着该组undo日志没有用了,就会从History链表移除,并释放它们占用的存储空间。如果该组undo日志包含因delete mark操作而产生的undo日志(Undo Log Header中的TRX_UNDO_DEL_MARKS属性为1),那么也需要将对应标记为删除的记录彻底删除。
