MySQL事务

科技资讯 投稿 5200 0 评论

MySQL事务

1. 事务

中途失败需要回滚到指定状态,全部执行成功需要确保持久保存在数据库中。

 

2. ACID特性

假设A有10元,B有15元,C有8元

T1: read(A, A=A-5, write(A, read(B, B=B+5, write(B。

读取B的账户余额,将B的账户余额增加5元后再写入到数据库中。

T2: read(C, C=C-4, write(C, read(B, B=B+4, write(B

读取B的账户余额,将B的账户余额增加4元后再写入到数据库中。

2.1 原子性(Atomicity

事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行,是一个最小执行单元,不可分割。

假设由于断电等意外事件,导致T1只执行了部分操作,如T1:read(A, A=A-5, write(A

因此事务需要保证保证在事务执行过程中出现错误时,将已经执行的操作“撤销”,恢复到原始状态。

2.2 一致性(Consistency

事务应确保数据库的状态从一个一致状态转变为另一个一致状态。

 

2.3 隔离性(Isolation

A给B转账5元,同时C给B转账4元,这个两个事务应该是互相隔离的,互不影响。

假设T1, T2可以交叉执行,如下图所示。最终结果看起来B只收到了A的5元转账,余额为20元。

2.4 持久性(Durability

已被提交的事务对数据库的修改应该永久保存在数据库中。

磁盘写操作相当耗时,而且同一个事务可能修改多个数据页面,而且可能执行页面中一个字节的数据。

MySQL设计人员通过redo日志来持久化最小量的数据来达到相同的效果。

3. redo日志

为了保证事务的持久性,在事务提交动作完成之前,需要把该事务修改所有页面都刷新到磁盘,但是存在以下问题

    随机I/O比较慢,一个事务可能修改到多个页面,这些页面在磁盘中可能不相邻,可能需要多次长距离移动磁盘读写磁头。
  • 刷新完整的数据页面较慢,一个事务也可能值修改页面中一个字节,却要同步整个页面到磁盘上。

即使在系统崩溃之后,按照redo日志重新更改数据页进行数据恢复即可。

3.1 redo日志格式

redo日志通用格式如下图所示

    type,redo日志类型,通过定义不同类型可以达到节省空间的目的
  • space id,表空间id
  • page number,页号
  • data,redo日志具体内容

可能需要性申请数据页,金额可能要修改各种段、区的统计信息等,

在崩溃恢复时,也是将这一组日志作为不可分割的整体来处理,

 

3.2 redo日志缓冲区

而是先将redo日志放到缓冲区,在特定时机刷新到磁盘。

InnoDB引擎使用lsn(log sequence number来记录系统当前有多少redo日志写入到缓冲区

3.3 redo日志文件

3.3.1 flush链表中的lsn

InnoDB会将lsn相关信息写入到flush链表中,进而方便判断哪些redo日志文件可以被重复使用,

Buffer Pool中页面会在控制块中记录页面的修改信息

    oldest_modification: 第一次修改Buffer Pool中某个页面时,将MTR开始时的lsn写入该变量
  • newest_modification: 每次修改Buffer Pool中某个页面时,将MTR结束时的lsn写入该变量

当页面被多次更新时,会更新对应页面的newest_modification变量的值。

InnoDB将检查flush链表最小的oldest_modification的lsn值称为checkpoint操作。

3.3.2 redo日志文件格式

磁盘上存在多个redo日志文件,会被循环使用,这一组redo日志文件称为redo日志文件组。

和redo日志缓冲区一样,redo日志文件也是由若干个512B构成的block组成

崩溃恢复会从checkpoint_lsn在日志文件组中对应的偏移量开始。

    redo日志缓存不足时
  • 事务提交时
  • 后台线程周期性刷盘
  • 正常关闭服务器

 

3.3.3 奔溃恢复

InnoDB从redo日志文件组的第一个文件的checkpoint信息,然后从checkpoint_lsn在日志文件组中对应的偏移量开始,

 

4. undo日志

但是为了保证事务的原子性,需要将数据恢复到事务开启前,这个恢复过程就称为回滚。

保存事务执行过程中修改内容的东西称为undo日志。

4.1 undo日志格式

4.1.1 聚簇索引行结构

InnoDB会将聚簇索引行结构如下图所示

    trx_id: 一个事务某次对某条聚簇索引记录进行改动时,都会把该事务的事务ID赋值给trx_id,事务ID是单调递增的
  • roll_pointer: 每次对某条聚簇索引进行改动时,都会把旧版本写入到undo日志中,并以聚簇索引为起点构成一个从最新到最旧的单向链表结构,这个链表就称为版本链

roll_pointer结构如下图所示

    is_insert, 表示该指针指向的undo日志是否是TRX_UNDO_INSERT大类的undo日志
  • resg id, 表示该指针指向的undo日志的回滚段编号
  • page number,  表示该指针指向的undo日志所在页面的页号
  • offset, 表示该指针指向的undo日志在页面中的偏移量
4.1.2 插入操作

如果需要回滚插入操作,只需要将插入的记录删除即可,

插入操作的对应的undo日志类型为TRX_UNDO_INSERT_REC,结构如下图所示

    undo type, 即TRX_UNDO_INSERT_REC
  • undo no, 事务执行过程中,每生成一条undo日志,undo no就增加1,且从0开始
  • table id, 该undo日志对应的记录所在表的table id
4.1.3 删除操作

在事务中执行删除操作,会将记录的deleted_flag标识为值为1,但该记录依然在正常记录链表,并没有移动到垃圾记录链表,这个过程称为delete mark。

删除操作产生TRX_UNDO_DEL_MARK_REC类型的日志,结构如下图所示,

假设一个事务对某条记录先更新再删除,这样就能通过TRX_UNDO_DEL_MARK_REC找到更新的undo日志。

 
4.2.4 更新操作
    不更新主键
    • 更新的每个列在更新前后占用的存储空间不变,则可以进行就地更新
    • 更新前后占用存储空间有变,需要将旧记录从聚簇索引删除,再创建一条新的记录
  • 更新主键,旧记录执行delete mark操作,创建新纪录插入聚簇索引

针对以上各种情况,InnnoDB设计了对应的undo日志格式,限于篇幅这里就不展开说明。

4.2 undo日志页面

和InnoDB普通页面结构类型,undo日志页面结构及页面链表如下图所示。

    TRX_UNDO_INSERT,由insert语句产生undo日志,或者update语句更新主键也会产生该类型的undo日志
  • TRX_UNDO_UPDATE,除了TRX_UNDO_INSERT类型的undo日志,其它类型的undo日志都属于这个大类

InnoDB对临时表和普通表产生的undo日志分开记录,因此一个事务最多可能需要4个undo页面链表。

4.3 回滚

同一个时刻,可能存在多个事务在执行,为了更好的管理undo页面链表,

在奔溃恢复时,需要将未提交事务的修改回滚掉,通过undo slot找到undo页面链表,

如果为TRX_UNDO_ACTIVE,则进一步通过undo链表最后一个页面的Undo Log Header中找到该事务对应的事务ID,

 

5. 事务隔离级别和MVCC

5.1 常见一致性问题

一个事务修改了另外一个未提交事务修改过的数据。

脏读(Dirty Read

 

一个事务修改了另一个未提交事务读取的数据。

幻读(Phantom

再次以相同条件查询,前后两次结果不一致。

5.2 事务隔离级别

SQL标准中定义了四种隔离级别

不同隔离级别对应的可能和不可能发生的一致性问题如下图所示。

 

5.3 MVCC

5.3.1 版本链

假设在某个时刻,事务321、315、301对某条记录进行Updata操作后,形成的版本链如下图所示。其中事务301对这条记录更新了两次

5.3.2 MVCC和ReadView

Multi-Version Concurrency Control, 即多版本并发控制,

ReadView,即一致性视图,通过这个视图可以判断版本链的某个版本是否可被当前事务访问,中ReadView包含以下4个比较重要的数据

    creator_trx_id,生成该ReadView的事务对应的事务ID
  • m_ids,在生成ReadView时当前系统中活跃的事务ID列表
  • min_trx_id,m_ids的最小值
  • max_trx_id,生成ReadView时,系统的下一个事务ID值。
  1. 如果被访问版本的trx_id值与ReadView中的creator_trx_id值相同,说明是当前事务在访问自己修改过的记录,该版本可以被当前事务访问
  2. 如果被访问版本的trx_id值小于ReadView中min_trx_id值,说明该版本在当前事务生成ReadView前已经提交,因此该版本可以被访问
  3. 如果被访问版本的trx_id值不小于ReadView中的max_trx_id值,说明该版本的事务在当前事务生成ReadView后启动,因此该版本不可访问
  4. 如果被访问版本的trx_id值在ReadView的min_trx_id和max_trx_id之间
    1. 如果trx_id在m_ids列表中,说明ReadView生成时该版本的事务依然活跃,因此该版本不可访问
    2. 如果trx_id不在在m_ids列表中,说明ReadView生成时该版本的事务已经提交,因此该版本可以被访问
  5. 顺着版本链重复上述操作,直到找到可以访问的版本,或者到达版本链末尾。如果版本链最后一个版本依然不可见,则查询结果为记录不存在

在读已提交和可重复读隔离级别下,ReadView生成的时机有所不同,

    读已提交在每一次进行普通Select操作前都会生成一个ReadView,确保了读取到都是已提交事务的数据
  • 可重复读只在第一次进行普通Select操作前生产一个ReadView, 之后查询操作都重复使用这个ReadView,保证了同一个事务内不同时间读到相同数据

 

编程笔记 » MySQL事务

赞同 (25) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽