MySQL学习笔记:事务

环境

MacBook Pro

序言

MySQL 技术内幕 InnoDB 存储引擎 第二版

事务

事务的四个特性:
① 原子性
② 隔离性
③ 一致性
④ 持久性

其中,隔离性,是由来保证的。
原子性一致性持久性则是通过redo logundo log来完成。

名称描述记录格式读取方式
redo log重做日志;用来保证事务的原子性和持久性物理日志,记录页的物理修改操作基本上是顺序读
undo log用来保证事务的一致性逻辑日志;根据每行记录进行记录随机读

Q : undoredo的逆操作吗?
A: 其实不是;redo和undo的作用都可以视为一种恢复操作;
redo恢复提交事务修改的页操作;
undo回滚行记录到某个特定版本;

redo log 会产生脏页,这种脏页时间长了,量多了后,就会让MySQL服务器“抖一下”。
刷脏页有四种场景:

  1. redo log 写满了,要刷脏页;这种情况需要尽量避免。因为出现这种情况,整个系统不再接收更新了,所有的更新都会被堵住,从监控上看,就是更新跌为0。
  2. 内存不够用了,要先将脏页写到磁盘中。这种情况其实是常态。InnoDB用缓冲池管理内存,缓冲池内存页有三种状态: ① 还没有使用 ② 使用了并且是干净页。③使用了并且是脏页。对于长时间运行的数据库来说,未被使用的页很少。
    刷脏页虽然是常态,但是出现下面情况,都将影响性能:
  • 一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长;
  • 日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的。

所以,InnoDB 需要有控制脏页比例的机制,来尽量避免上面的这两种情况。

  1. MySQL 认为系统“空闲”的时候。
  2. MySQL 正常关闭的情况。这时候,MySQL 会把内存的脏页都 flush 到磁盘上,这样下次 MySQL 启动的时候,就可以直接从磁盘上读数据,启动速度会很快。

事务的分类

  • 扁平事务
  • 带有保存点的扁平事务
  • 链事务
  • 嵌套事务
  • 分布式事务

扁平事务

在扁平事务中所有的操作都处在同一层次;
begin work开始,commit workrollback work结束;
该事务也是我们最常用的事务

带有保存点的扁平事务

出现的原因:某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销也太大。

保存点:用来通知系统应用记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态。

链事务

可以视为保存点模式的一个变种。

出现的原因:带有保存点的扁平事务,当系统发生崩溃时,所有的保存点都将消失,因为其保存点是意失的,而非持久的。这意味着当进行恢复时,事务需要从开始处重写执行,而不能从最近的一个保存点继续执行。

链事务:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。注意,提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果。

链事务与带保存点的扁平事务区别?
A:后者可以回滚到任意正确的保存点。而链事务中的回滚仅限当前事务,即只能恢复到最近的一个保存点。对于锁的处理也不一样:链事务在执行commit后即释放了当前事务所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。

分布式事务

通常是一个分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中不同节点。

InnoDB存储引擎支持的事务: 扁平事务、带有保存点的事务,链事务,分布式事务

重做日志

redo

重做日志,是ACID中的D,持久化;
其由两部分组成:

  1. 内存中的重做日志缓存
  2. 重做日志文件

InnoDB对事务的提交:必须先将该事务的所有日志写入到重做日志文件进行持久化,待事务的commit操作完成才算完成。
这里的日志是重做日志,其由两部分组成:redo log和undo log

默认情况下,InnoDB 每次将重做日志缓存写入重做日志文件后,都会调用一次fsync操作。
当然,支持参数来控制重做日志刷新到磁盘的策略;

innodb_flush_log_at_arx_commit描述
0事务提交时不进行写入重做日志操作,这个操作仅在master thread中完成;而master thread会每1秒进行一次fsync操作
1默认值;每次将重做日志缓存写入重做日志文件后,都会调用一次fsync操作
2事务提交时,将重做日志写入重做日志文件,但仅写入文件系统缓存中,不进行fsync

二进制日志(binlog)

作用:用来进行point-in-time的恢复和主从复制环境的建立。

binlog重做日志的区别:

  1. 重做日志是InnoDB 存储引擎层产生,而二进制日志是MySQL数据上层产生;
  2. 二进制日志是一种逻辑日志,其记录的是对应的SQL语句。而重做日志是物理格式日志,其记录的是对于每个页的修改。
  3. 二进制日志只在事务提交完成后,进行一次写入。而重做日志在事务进行中不断被写入,这表现为日志并不是随事务提交的顺序进行写入

undo

因为事务有回滚的需求,所以在对数据库进行修改时,InnoDB存储引擎不但会产生redo,还会产生一定量的undo.

作用:
① 用于事务的回滚
② 用于MVCC的控制

undo存储在数据库内部的一个特殊段segment中,这个段称为undo段。位于共享表空间中。

回滚举例说明:

用户执行一个insert 10w条记录的事务。因为undo的缘故,这个事务会导致分配一个新的段,即表空间变大。
现在用户执行rollback进行回滚,会将插入的事务进行回滚,但是表空间的大小并不会因此而收缩。
InnoDB回滚时,它实际上做的是与先前相反的工作:
① 对于每个insert操作,InnoDB存储引擎会完成一个delete操作。
② 对于每个delete操作,其会执行一个insert操作;
③ 对于每个update操作,其会执行一个相反的update,将修改前的行返回去。

MVCC举例:
当用户读取一行记录时,若该记录已经被其他事务占用,当前事务可以通过undo读取之前的行版本信息,以此实现非锁定读取。

undo的结构:

在这里插入图片描述
从上图的结构我们知道:

InnoDB存储引擎有rollback segment,每个回滚段中记录了1024undo log segment,而在每个undo log segment段中进行undo页的申请。

共享表空间偏移量为5,(0,5)记录了所有rollback segment header所在的页。该页的类型FIL_PAGE_TYPE_SYS;
假设只有一个rollback segment,那么支持同时在线的事务为1024

特别注意:

  1. 事务在undo log segment分配页并写入undo log的这个过程同样需要写入重做日志中。当事务提交时,存储引擎会做两件事情:
    ① 将undo log放入链表中,以供purge操作。
    ② 判断undo log 所在的页是否可以重用。
  2. 事务提交后,并不会马上删除undo log及undo log 所有的页。

不马上删除是因为,可能有其他事务需要通过undo来访问行记录之前的版本信息。
故事务提交时,将undo log放入一个链表中,是否可以最终删除undo logundo log所在的页purge线程来判断。

undo log 格式

undo log分为:
① insert undo log
② update undo log

因为insert操作的记录,只对事务本身可见(事务原子性),故该undo log可以在事务提交后直接删除。不需要purge操作。

update undo log记录的是deleteupdate操作产生的undo log,为了支持MVCC机制,提交事务时,不会删除。而是放入undo log链表中等待purge线程进行最后的删除。

最后我们举例来说明

① 插入insert的情况

insert into t select 1, '1'

这种情况会产生一个类型TRX_UNDO_INSERT_RECundo log
提交事务后,该undo页会被放入cache中,以便下次事务需要时直接使用。

② 删除delete的情况

delete form t where a=1;

这种情况会产生一个类型TRX_UNDO_DEL_MARK_RECundo log
提交事务后,undo页会放入cache列表中以供下次使用;

delete操作并不直接删除记录,而只是将记录标记为已删除,记录的最终删除是在purge操作中完成的。

③ 更新update的情况

# 更新非主键的情况
update t set b = '2' where a = 1;

会产生一个TRX_UNDO_UPD_EXIST_RECundo log

# 更新主键的情况
update t set a = 2 where a = 1;

如果更新的是主键,那么操作会分两步完成:
① 将原主键记录标记为已删除,因此会产生已类型为TRX_UNDO_DEL_MARK_RECundo log
② 插入一条新记录,因此会产生一个类型为TRX_UNDO_INSERT_RECundo log

A. 从表中删除一行记录
TRX_UNDO_DEL_MARK_REC (将主键记入日志)
在删除一条记录时,并不是真正的将数据从数据库中删除,只是标记为已删除.这样做的好处是Undo Log中不用记录整行的信息.在undo时操作也变得很简单.
B. 向表中插入一行记录
TRX_UNDO_INSERT_REC (仅将主键记入日志)
TRX_UNDO_UPD_DEL_REC (将主键记入日志) 当表中有一条被标记为删除的记录和要插入的数据主键相同时, 实际的操作是更新这个被标记为删除的记录。
C. 更新表中的一条记录
TRX_UNDO_UPD_EXIST_REC (将主键和被更新了的字段内容记入日志)
TRX_UNDO_DEL_MARK_REC 和 TRX_UNDO_INSERT_REC,当更新主键字段时,实际执行的过程是删除旧的记录然后,再插入一条新的记录。

purge

delete和update操作可能并不直接删除旧数据。

# 表t 列a有聚集索引,列b上有辅助索引
delete from t where a = 1;

上面的SQL,InnoDB仅是将主键列等于1的记录delete flag设置为1,记录并没有删除;

purge用于最终完成deleteupdate操作,这么设计的目的是为了支持MVCC
那么什么时候删除呢?若该行记录已不被任何其他事务引用,那么就可以进行真正的delete操作。

流程:
根据事务提交的顺序,将undo log进行链接,形成一个history列表;

  1. 在执行purge过程中,InnoDB存储引擎首先从history列表中找到第一个需要被清理的记录,这里记为trx1
  2. 清理之后引擎会在trx1的undo log所在的页中继续寻找是否存在可以被清理的记录,这里会找到事务trx3,接着又找到trx5,但是发现trx5被其他事务所引用而不能清理。
  3. 然后再次去history列表中查找,发现这是最尾端的记录trx2,接着找到trx2所在的页,然后依次再把事务trx6trx4的记录进行清理。
  4. 由于undo page2中所有的页都被清理了,因此该undo page可以被重用。

在这里插入图片描述
小结:
1、InnoDB存储引擎这种先找history list中找到undo log,然后再从undo page中找undo log的设计模式是为了避免大量的随机读取操作,从而提高purge的效率。
2、为了节省存储空间,一个页上允许多个事务的undo log的存在。即undo page可以被多个不同事务的undo log重用

group commit

事务的提交过程:
① 修改内存中事务对应的信息,并且将日志写入重做日志缓存中。
② 调用fsync来确保日志都从重做日志缓存中写入了磁盘。

我们都知道fsync这个系统操作时IO操作,非常耗时,如果每次事务提交都执行的话,那么数据库的性能将大大折扣。。。

因此InnoDB引入了group commit
即:当某个事务在执行过程②时,其他事务可以进行过程①,这样就可以在过程②执行时,可以将多个事务日志通过一次fsync刷新到磁盘。

后面到了MySQL5.6版本中,将事务提交分为几个步骤:

首先,在MySQL上层进行提交时会按顺序将其(事务)放入一个队列中,队列中的第一个事务称为leader,其他事务称为followerleader控制着follower的行为;

flush阶段:将每个事务的二进制日志写入内存;
sync阶段:将内存中二进制日志刷新到磁盘,若队列中有多个事务,那么仅一次fsync操作就完成了二进制日志的写入;这就是BLGC
commit阶段:leader根据顺序调用存储引擎层事务的提交,而InnoDB引擎层本来就支持group commit,因此性能非常好;

上面的步骤可以看出,①阶段和②阶段是用来保证binlog提交的顺序;
这也是BLGC的由来:binary log group commit;

特别注意:Flushsync阶段,是先保证队列中的事务都执行完毕后,才会执行commit阶段;

当有一组事务进行commit时,其他事务可以进行flush阶段,从而使group commit不断生效;
在没有BLGC之前,MySQL为了保证二进制日志bin log和InnoDB引擎层事务顺序一致,会使用prepare_commit_mutex这个锁,
但是使用锁之后,那么引擎层面的group commit失效;
Q:为什么要保证MySQL上层二进制日志的写入顺序和InnoDB层的事务提交顺序一致呢?
A:这是因为备份和恢复的需要:
若通过在线备份进行数据库恢复来重新建立replication,并假设有三个事务1,事务2,事务3;
他们bin log的顺序是 1 2 3,InnoDB 层事务提交顺序是2 3 1
那么InnoDB 引擎会检测事务T3在上下层两层(MySQL数据库上层和InnoDB层)都完成了提交,不需要再进行恢复,
此时,事务T1的数据就会丢失;
这也说明,数据恢复,是按照事务的顺序来的;

事务隔离级别

InnoDB存储引擎提供的事务隔离级别有:

事务隔离级别支持级别
read uncommitted脏读、不可重复读、幻读都会出现
read committed解决脏读;使用行锁
repeatable read解决幻读和不可重复度;使用next-key lock 行锁加范围锁
serializable用于分布式

读未提交:别人改数据的事务尚未提交,我在我的事务中也能读到。
读已提交:别人改数据的事务已经提交,我在我的事务中才能读到。
可重复读:别人改数据的事务已经提交,我在我的事务中也不去读。
串行:我的事务尚未提交,别人就别想改数据。

这4种隔离级别,并行性能依次降低,安全性依次提高。

不可重复读和幻读的区别?

其实幻读只是不可重复读的一个特殊情况,而且MySQL官方文档,将它们都归为幻象问题

引起不可重复读发生的操作是:updatedelete
而幻读的操作是:insert

我个人倒是觉得,他两是一类问题;

分布式事务

InnoDB是如何支持分布式事务的呢?

InnoDB存储引擎提供了对XA事务的支持,并通过XA事务来支持分布式事务的实现;

XA事务由一个或多个资源管理器、一个事务管理器,一个应用程序组成;

资源管理器:提供访问事务资源的方法;通常就是一个数据库;
事务管理器:协调参与全局事务中的各个事务。需要和参与全局事务的所有资源管理器进行通讯。
应用程序:定义事务的边界,指定全局事务中的操作;

MySQL分布式事务中,资源管理器就是MySQL数据库,事务管理器为连接MySQL服务器的客户端;

什么是分布式事务?

是指允许多个独立的事务资源参与到一个全局的事务中。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚。

说明:
① 事务资源通常是关系型数据库系统
② 必须设置隔离级别为serializable

分布式事务使用的是两段提交的方式。

第一阶段:

所有参与全局事务的节点都开始准备,告诉事务管理器它们准备好了。

第二阶段:

事务管理器告诉资源管理器执行commitrollback

如果任何一个节点显示不能提交,则所有的节点都被告知需要回滚。
可以看出,和本地事务相比,分布式事务多了一次prepare操作,待收到所有节点的同意信息后,再进行commit或者rollback操作。

内部XA事务

上面介绍的都是外部分布式事务;但是数据库内部还存在存储引擎插件之间或者存储引擎存储引擎之间的事务。

比如:binlog和InnoDB存储引擎之间。由于复制的需要;

在这里插入图片描述

上图中,如果执行完①、②后在步骤③之前,发送宕机,那么主从就会不一致;

为了解决这个问题MySQL数据库在binlogInnoDB存储引擎之间采用了XA事务;
当事务提交时,InnoDB引擎会先做一个prepare的操作,将事务的xid写入,接着写入二进制的日志,
如果在InnoDB存储引擎提交前,MySQL发送宕机了,那么MySQL数据库在重启后会先检查准备的uxid事务是否已经提交,如果没有,则在存储引擎层在进行一次提交。
如下图:

在这里插入图片描述

MySQL提供的事务控制语句

MySQL命令行的默认设置下,事务都是自动提交的,即执行SQL语句后就会马上执行COMMIT操作。因此要显式地开启一个事务需要使用命令BEGIN, START TRANSACTION, 或者执行命令SET AUTOCOMMIT=0, 禁用当前会话的自动提交。

显示开启一个事务

  1. START TRANSACTION | BEGIN 显式地开启一个事务。

  2. COMMIT(COMMIT WORK) 提交事务。

  3. ROLLBACK (ROLLBACK WORK) 回滚事务

  4. SAVEPOINT identity 允许在事务中创建一个保存点,一个事务中可以有多个SAVEPOINT

  5. RELEASE SAVEPOINT identity 删除一个事务的保存点,当没有一个保存点执行这语句时,会抛出一个异常。

  6. ROLLBACK TO [SAVEPOINT] identitySAVEPOINT命令一起使用。可以把事务回滚到标记点,而不回滚在此标记点之前的任何工作。

    6.1 例如,可以发出两个UPDATE语句,后面跟一个SAVEPOINT, 然后又是两条DELETE语句。如果执行DELETE语句期间出现了某种异常情况,并且捕获到这个异常,同时发出了ROLLBACK TO SAVEPOINT命令,事务就会回滚到指定的SAVEPOINT,撤销DELETE完成的所有工作,而UPDATE语句完成的工作不受影响。

  7. SET TRANSACTION 设置事务的隔离级别。

这里需要重点说下一个参数:completion_type
commit workcompletion_type的关系,commit work是用来控制事务结束后的行为,是chain还是release的,可以通过参数completion_type来控制,默认为0(NO_CHAIN),表示没有任何操作 与commit效果一样。

completion_type=1的时候,COMMIT WORK等同于COMMIT AND CHAIN, 表示马上自动开启一个相同隔离级别的事务。

completion_type=2的时候,COMMIT WORK等同于COMMIT AND RELEASE, 在事务提交后会自动断开与服务器的连接。

关于长事务需要注意的地方

开发中,习惯性的会把set autocommit=0,关闭MySQL自动提交,这很容易导致出现长事务,建议使用set autocommit=1,但是这样有些开发人员会纠结:多一次交互,即:又得重写begin开启一个事务。

有这种顾虑的情况下,可以考虑使用commit work and chain语法。
也就是通过参数completion_type=1,再使用commit work 来达到效果:

mysql> set names utf8;
Query OK, 0 rows affected (0.00 sec)

mysql> create table t(a int, primary key (a))engine=innodb;
Query OK, 0 rows affected (0.29 sec)

mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
|            1 |
+--------------+
1 row in set (0.00 sec)

mysql> set @@completion_type=1;
Query OK, 0 rows affected (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t select 1;
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> commit work;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into t select 2;
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> insert into t select 2;
ERROR 1062 (23000): Duplicate entry '2' for key 'PRIMARY'
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

# 回滚之后只有1这个记录,而没有2这个记录
mysql> select * from t;
+---+
| a |
+---+
| 1 |
+---+
1 row in set (0.00 sec)

MySQL事务控制语句(学习笔记)
MySQL提供的事务控制语句

https://time.geekbang.org/column/article/71806

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山鬼谣me

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值