Mysql-事务

什么是事务

事务

事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。

事务处理的原则

保证事务作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。
当在一个事务中执行多个操作时,要么所有的事务都被提交( commit ),那么这些修改就永久地保存下来;要么数据库管理系统将放弃所作的所有修改 ,整个数据库回滚( rollback )到最初状态。

一、事务的ACID特性

原子性(atomicity)
原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。
一致性(consistency)
根据定义,一致性是指事务执行前后,数据从一个 合法性状态 变换到另外一个 合法性状态 。这种状态是语义上的而不是语法上的,跟具体的业务有关。

那什么是合法的数据状态呢?满足预定的约束 的状态就叫做合法的状态。通俗一点,这状态是由你自己来定义的(比如满足现实世界中的约束)。满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。

隔离型(isolation)
事务的隔离性是指一个事务的执行不能被其他事务干扰 ,即一个事务内部的操作及使用的数据对并发 的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。如果无法保证隔离性会怎么样?假设A账户有200元,B账户0元。A账户往B账户转账两次,每次金额为50元,分别在两个事务中执行。如果无法保证隔离性,会出现下面的情形:

UPDATE accounts SET money = money - 50 WHERE NAME = 'AA';
UPDATE accounts SET money = money + 50 WHERE NAME = 'BB';

在这里插入图片描述

两个事务在执行上存在冲突,事务二在执行过程中读取到了事务一所操作的中间数据,导致最后的结果不准确。

持久性(durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的 ,接下来的其他操作和数据库故障不应该对其有任何影响。

持久性是通过事务日志来保证的。日志包括了 重做日志回滚日志 。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。

二、事务的状态

我们现在知道 事务 是一个抽象的概念,它其实对应着一个或多个数据库操作,MySQL根据这些操作所执行的不同阶段把 事务 大致划分成几个状态:

  1. 活动的(active)
    事务对应的数据库操作正在执行过程中时,我们就说该事务处在 活动的 状态。
  2. 部分提交的(partially committed)
    当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处在 部分提交的 状态。
  3. 失败的(failed)
    当事务处在 活动的 或者 部分提交的 状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在 失
    败的 状态。
  4. 中止的(aborted)
    如果事务执行了一部分而变为 失败的 状态,那么就需要把已经修改的事务中的操作还原到事务执
    行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称之为 回滚 。当 回滚 操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了 中止的 状态。
  5. 提交的(committed)
    当一个处在 部分提交的 状态的事务将修改过的数据都 同步到磁盘 上之后,我们就可以说该事务处
    在了 提交的 状态。
    在这里插入图片描述

三、事务的使用

使用事务有两种方式,分别为 显式事务隐式事务

显式事务

步骤1: START TRANSACTION 或者 BEGIN ,作用是显式开启一个事务。

mysql> BEGIN;
#或者
mysql> START TRANSACTION;

START TRANSACTION 语句相较于 BEGIN 特别之处在于,后边能跟随几个 修饰符 :
① READ ONLY :标识当前事务是一个 只读事务 ,也就是属于该事务的数据库操作只能读取数据,而不能修改数据。
② READ WRITE :标识当前事务是一个 读写事务 ,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
③ WITH CONSISTENT SNAPSHOT :启动一致性读。

步骤2:一系列事务中的操作(主要是DML,不含DDL)
步骤3:提交事务 或 中止事务(即回滚事务)

# 提交事务。当提交事务后,对数据库的修改是永久性的。
mysql> COMMIT;
# 回滚事务。即撤销正在进行的所有没有提交的修改
mysql> ROLLBACK;
# 将事务回滚到某个保存点。
mysql> ROLLBACK TO [SAVEPOINT]

隐式事务

MySQL中有一个系统变量 autocommit :

mysql> SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)

当然,如果我们想关闭这种 自动提交 的功能,可以使用下边两种方法之一:

  1. 显式的的使用 START TRANSACTION 或者 BEGIN 语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。
  2. 把系统变量 autocommit 的值设置为 OFF ,就像这样:
SET autocommit = OFF;
#或
SET autocommit = 0;

隐式提交数据的情况

  1. 数据定义语言(Data definition language,缩写为:DDL)
  2. 隐式使用或修改mysql数据库中的表
  3. 事务控制或关于锁定的语句:
    ① 当我们在一个事务还没提交或者回滚时就又使用 START TRANSACTION 或者 BEGIN 语句开启了另一个事务时,会 隐式的提交 上一个事务。
    ② 当前的 autocommit 系统变量的值为 OFF ,我们手动把它调为 ON 时,也会 隐式的提交 前边语句所属的事务
    ③ 使用 LOCK TABLES 、 UNLOCK TABLES 等关于锁定的语句也会 隐式的提交 前边语句所属的事务。
  4. 加载数据的语句
  5. 关于MySQL复制的一些语句

例子:

CREATE TABLE user (name varchar(20), PRIMARY KEY (name)) ENGINE=InnoDB;
BEGIN;
INSERT INTO user SELECT '张三';
COMMIT;(该事务结束)
INSERT INTO user SELECT '李四';
INSERT INTO user SELECT '李四';(隐式提交,该语句的执行让上一句进行了commit)
ROLLBACK;
mysql> SELECT * FROM user;
+--------+
| name |
+--------+
| 张三 |
| 李四 |
+--------+

CREATE TABLE user(name varchar(255), PRIMARY KEY (name)) ENGINE=InnoDB;
SET @@completion_type = 1;
BEGIN;
INSERT INTO user SELECT '张三';
COMMIT;
INSERT INTO user SELECT '李四';
INSERT INTO user SELECT '李四';
ROLLBACK;
mysql> SELECT * FROM user;
+--------+
| name |
+--------+
| 张三 |
+--------+

当我们设置 autocommit=0 时,不论是否采用 START TRANSACTION 或者 BEGIN 的方式来开启事 务,都需要用 COMMIT 进行提交,让事务生效,使用 ROLLBACK 对事务进行回滚。 当我们设置 autocommit=1 时,每条 SQL语句都会自动进行提交。 不过这时,如果你采用 START TRANSACTION 或者 BEGIN的方式来显式地开启事务,那么这个事务只有在 COMMIT 时才会生效, 在 ROLLBACK 时才会回滚。

四、事务的隔离级别

MySQL是一个 客户端/服务器 架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话( Session )。每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。事务有 隔离性 的特性,理论上在某个事务 对某个数据进行访问 时,其他事务应该进行 排队 ,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对 性能影响太大 ,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时 性能尽量高些 ,那就看二者如何权衡取舍了。

数据并发问题

针对事务的隔离性和并发性,我们怎么做取舍呢?先看一下访问相同数据的事务在 不保证串行执行 (也就是执行完一个再执行另一个)的情况下可能会出现哪些问题:

  1. 脏写( Dirty Write )
    对于两个事务 Session A、Session B,如果事务Session A 修改了 另一个 未提交 事务Session B 修改过的数据,那就意味着发生了 脏写,也就是两个事务都同时对同一数据进行了修改。
  2. 脏读( Dirty Read )
    对于两个事务 Session A、Session B,Session A 读取了已经被 Session B 更新但还没有被提交的字段。之后若 Session B 回滚 ,Session A 读取 的内容就是 临时且无效 的。

例如:
Session A和Session B各开启了一个事务,Session B中的事务先将studentno列为1的记录的name列更新为’张三’,然后SessionA中的事务再去查询这条studentno为1的记录,如果读到列name的值为’张三’,而Session B中的事务稍后进行了回滚,那么Session A中的事务相当于读到了一个不存在的数据,这种现象就称之为脏读

  1. 不可重复读( Non-Repeatable Read )
    对于两个事务Session A、Session B,Session A 读取了一个字段,然后 Session B 更新了该字段。 之后Session A 再次读取同一个字段,值就不同了。那就意味着发生了不可重复读。也就是事务A在执行期间读取的两次数据不一致的情况。

例如:
我们在Session B中提交了几个隐式事务(注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了studentno列为1的记录的列name的值,每次事务提交之后,如果Session A中的事务都可以查看到最新的值,这种现象也被称之为 不可重复读 。

  1. 幻读( Phantom )
    对于两个事务Session A、Session B, Session A 从一个表中读取 了一个字段, 然后 Session B 在该表中插入了一些新的行。 之后, 如果 Session A 再次读取同一个表, 就会多出几行。那就意味着发生了幻读。

例如:
Session A中的事务先根据条件 studentno > 0这个条件查询表student,得到了name列值为’张三’的记录;之后Session B中提交了一个 隐式事务 ,该事务向表student中插入了一条新记录;之后Session A中的事务 再根据相同的条件studentno > 0查询表student,得到的结果集中包含Session B中的事务新插入的那条记 录,这种现象也被称之为 幻读 。我们把新插入的那些记录称之为 幻影记录

四种隔离级别

上面介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题有轻重缓急之分,我们给这些问题按照严重性来排一下序:

脏写 > 脏读 > 不可重复读 > 幻读

我们愿意舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,并发问题发生的就越多。 SQL标准 中设立了4个 隔离级别 :

  1. READ UNCOMMITTED :读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读。(能够读取未提交事务的数据)
  2. READ COMMITTED :读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在。(只能读取到已经提交事务的数据)
  3. REPEATABLE READ :可重复读,事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL的默认隔离级别。
  4. SERIALIZABLE :串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。(让事务串行化执行)

SQL标准 中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:
在这里插入图片描述

脏写 怎么没涉及到?因为脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生。
不同的隔离级别有不同的现象,并有不同的锁和并发机制,隔离级别越高,数据库的并发性能就越差

如何设置隔离级别:

SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别;
#其中,隔离级别格式:

> READ UNCOMMITTED
> READ COMMITTED
> REPEATABLE READ
> SERIALIZABLE
使用 GLOBAL 关键字(在全局范围影响)但当前已经存在的会话无效只对执行完该语句之后产生的会话起作用
使用 SESSION 关键字(在会话范围影响)
对当前会话的所有后续的事务有效
如果在事务之间执行,则对后续的事务有效
该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务

数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性
就越好,但并发性越弱。

五、事务日志

事务的隔离性由 锁机制 实现。而事务的原子性、一致性和持久性由事务的 redo 日志和undo 日志来保证。

REDO LOG 称为 重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持
久性。

UNDO LOG 称为 回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性。
有的DBA或许会认为 UNDO 是 REDO 的逆过程,其实不然

redo日志

为什么需要REDO日志
一方面,缓冲池可以帮助我们消除CPU和磁盘之间的鸿沟,checkpoint机制可以保证数据的最终落盘,然而由于checkpoint 并不是每次变更的时候就触发 的,而是master线程隔一段时间去处理的。所以最坏的情况就是事务提交后,刚写完缓冲池,数据库宕机了,那么这段数据就是丢失的,无法恢复。另一方面,事务包含 持久性 的特性,就是说对于一个已经提交的事务,在事务提交后即使系统发生了崩溃,这个事务对数据库中所做的更改也不能丢失。那么如何保证这个持久性呢? 一个简单的做法 :
在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘,但是这个简单粗暴的做法有些问题
另一个解决的思路 :我们只是想让已经提交了的事务对数据库中数据所做的修改永久生效,即使后来系统崩溃,在重启后也能把这种修改恢复出来。所以我们其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘,只需要把 修改了哪些东西 记录一下 就好。比如,某个事务将系统表空间中 第10号 页面中偏移量为 100 处的那个字节的值 1 改成 2 。我们只需要记录一下:将第0号表空间的10号页面的偏移量为100处的值更新为 2 。
在这里插入图片描述
REDO日志的好处、特点

  1. 好处:
    redo日志降低了刷盘频率
    redo日志占用的空间非常小
  2. 特点
    redo日志是顺序写入磁盘的
    事务执行过程中,redolog不断记录
  3. 组成
    重做日志的缓冲(redologbuffer),保存在内存中,是易失的。
    重做日志文件(redologfile),保存在硬盘中,是持久的。

REDO日志整体流程:
在这里插入图片描述
第1步:先将原始数据从磁盘中读入内存中来,修改数据的内存拷贝
第2步:生成一条重做日志并写入redologbuffer,记录的是数据被修改后的值
第3步:事务commit时,将redologbuffer中的内容刷新到redologfile,对redologfile采用追加写的方式
第4步:定期将内存中修改的数据刷新到磁盘中

redo log日志的刷盘策略
redolog的写入并不是直接写入磁盘的,InnoDB引擎会在写redolog的时候先写redologbuffer,之后以一定的频率刷入到真正的redologfile中。这里的一定频率怎么看待呢?这就是我们要说的刷盘策略。
在这里插入图片描述
注意,redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到文件系统缓存针对这种情况,InnoDB(page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定(比如page cache足够大了)。那么对于InnoDB来说就存在一个问题,如果交给系统来同步,同样如果系统宕机,那么数据也丢失了(虽然整个系统宕机的概率还是比较小的)。给出 innodb_flush_log_at_trx_commit 参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:
设置为0 :表示每次事务提交时不进行刷盘操作。(系统默认master thread每隔1s进行一次重做日志的同步)
设置为1 :表示每次事务提交时都将进行同步,刷盘操作( 默认值 )
设置为2 :表示每次事务提交时都只把 redo log buffer 内容写入 page cache,不进行同步。由os自
己决定什么时候同步到磁盘文件。

下面是设置为: 2时的流程图:
在这里插入图片描述

CheckPoin机制
在这里插入图片描述
如果 write pos 追上 checkpoint ,表示日志文件组满了,这时候不能再写入新的 redo log记录,MySQL 得停下来,清空一些记录,把 checkpoint 推进一下。

undo日志

redo log是事务持久性的保证,undo log是事务原子性的保证。在事务中 更新数据 的 前置操作 其实是要先写入一个 undo log

事务需要保证 原子性 ,也就是事务中的操作要么全部完成,要么什么也不做。但有时候事务执行到一半会出现一些情况,比如:

情况一:事务执行过程中可能遇到各种错误,比如 服务器本身的错误 , 操作系统错误 ,甚至是突
然 断电 导致的错误。
情况二:程序员可以在事务执行过程中手动输入 ROLLBACK 语句结束当前事务的执行。

以上情况出现,我们需要把数据改回原先的样子,这个过程称之为 回滚 ,这样就可以造成一个假象:这个事务看起来什么都没做,所以符合 原子性 要求

undo日志的存储结构:

  1. 回滚段与undo页
    InnoDB对undo log的管理采用段的方式,也就是 回滚段(rollback segment) 。每个回滚段记录了1024 个 undo log segment ,而在每个undo log segment段中进行 undo页 的申请。
    从1.1版本开始InnoDB支持最大 128个rollback segment ,故其支持同时在线的事务限制提高到
    了 128*1024 。
  2. 回滚段与事务
    1. 每个事务只会使用一个回滚段,一个回滚段在同一时刻可能会服务于多个事务。
    2. 当一个事务开始的时候,会制定一个回滚段,在事务进行的过程中,当数据被修改时,原始的数据会被复制到回滚段。
    3. 在回滚段中,事务会不断填充盘区,直到事务结束或所有的空间被用完。如果当前的盘区不够用,事务会在段中请求扩展下一个盘区,如果所有已分配的盘区都被用完,事务会覆盖最初的盘区或者在回滚段允许的情况下扩展新的盘区来使用。
    4. 回滚段存在于undo表空间中,在数据库中可以存在多个undo表空间,但同一时刻只能使用一个undo表空间。
    5. 当事务提交时,InnoDB存储引擎会做以下两件事情:
      将undo log放入列表中,以供之后的purge操作
      判断undo log所在的页是否可以重用,若可以分配给下个事务使用
  3. 回滚段中的数据分类
    1. 未提交的回滚数据(uncommitted undo information)
    2. 已经提交但未过期的回滚数据(committed undo information)
    3. 事务已经提交并过期的数据(expired undo information)

事务执行流程
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Aimyon_36

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

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

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

打赏作者

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

抵扣说明:

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

余额充值