MySQL-事务

在 MySQL 中,只有InnoDB 是支持事务的

1.什么是事务

事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。这里的一组操作就是在开启事务后进行了一些增删改操作,即事务对应着一个或多个数据库操作。

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

2.事务的状态

MySQL根据这些增删改操作所执行的不同阶段把 事务 大致划分成几个状态:

  • 活动的(active)

    事务对应的数据库操作正在执行过程中时,我们就说该事务处在 活动的 状态。

  • 部分提交的(partially committed)

    当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并 没有刷新到磁盘时,我们就说该事务处在 部分提交的 状态。

  • 失败的(failed)

    当事务处在 活动的 或者 部分提交的 状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在 失败的 状态。

  • 中止的(aborted)

    如果事务执行了一部分而变为 失败的状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称之为回滚 。当 回滚 操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了 中止的 状态。

  • 提交的(committed)

    当一个处在 部分提交的 状态的事务将修改过的数据都 同步到磁盘 上之后,我们就可以说该事务处在了 提交的 状态。

3.事务的ACID特性

①原子性(atomicity)

原子性是指事务是 一个不可分割的工作单位,要么全部提交,要么全部失败回滚。

②一致性(consistency)

根据定义,一致性是指事务执行前后,数据从一个 合法性状态 变换到另外一个 合法性状态 。这种状态是 语义上 的而不是语法上的,跟具体的业务有关,符合现实世界的约束。

比如有一个表,其中一个属性为金额,在语义上金额应该大于等于0,账户A有100元,账户B有0元,账户A要转账200元,这个操作就是不合法的。

③隔离型(isolation)

事务的隔离性是指一个事务的执行 不能被其他事务干扰 ,即一个事务内部的操作及使用的数据对 并发 的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

④持久性(durability)

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

持久性是通过 事务日志 来保证的。日志包括了 redo日志 和 undo日志 。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到 redo 日志中,然后再对数据库中对应的行进行修改。

这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的redo日志,重新执行,从而使事务具有持久性。

4.使用事务

4.1 显式开启事务

步骤1: 显式开启一个事务。

START TRANSACTION 或者 BEGIN

1
2
3
mysql> BEGIN;
#或者
mysql> START TRANSACTION;

START TRANSACTION后边能跟随几个修饰符 :READ ONLY(当前开启的是只读事务)、READ WRITE(当前开启的事务可以进行增删改操作)、WITH CONSISTENT SNAPSHOT(一致性读),如果后面啥也不写,默认是READ WRITE

步骤2:一系列事务中的操作(主要是数据操纵语言DML,不含数据定义语言DDL)

DML(Data Manipulation Language):数据操纵语言,对数据库中的数据进行一些简单操作,如insert、delete、update、select等。DML操作是可以手动控制事务的开启、提交和回滚的。
DDL(Data Definition Language):数据定义语言,对数据库中的某些对象(例如database、table)进行管理,如create、alter和drop。DDL操作是隐性提交的(默认语句执行完就提交事务了),不能rollback。

步骤3:提交事务 或 中止事务(即回滚事务)

1
2
3
4
5
6
7
8
# 1.提交事务。当提交事务后,对数据库的修改是永久性的。
mysql> COMMIT;

# 2.回滚事务。即撤销正在进行的所有没有提交的修改,回到begin处最初的状态
mysql> ROLLBACK;

# 3.将事务回滚到某个保存点。因为rollback是回到最初的状态,但是我们只想要回到后面的某个位置,就需要那个位置执行完之后设置保存点【savepoint s1】,然后就回滚到这个位置,取消保存点【release savepoint s1】
mysql> ROLLBACK TO [SAVEPOINT]

4.2 隐式开启事务

MySQL中有一个系统变量 autocommit:默认是开启的状态,作用是使每一个DML语句都是一个独立的事务,执行完就自动提交了。这个系统变量只对DML语句有效。

如何关闭DML语句的隐式自动提交:1.当显式的的使用START TRANSACTION或者 BEGIN语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉隐式的自动提交的功能,显式的优先级比隐式的高。2.将这个系统变量设置为off

关闭之后,执行过DML语句不会自动提交,需要手动COMMIT


但是DDL语句不受这个变量的影响,DDL语句始终都是执行完这一句默认提交事务。


如果在事务开始前设置了SET @@completion_type=1;在它后面,如果一个事务完成,它会再帮我们开启一个事务,这个事务结束,会再帮我们开启一个相同隔离级别的事务,就是一个链式事务。

这个变量默认值是0,就是commit提交事务之后,如果还想开启事务,需要重新begin

5.事务隔离级别

5.1 为什么会有事务隔离?

MySQL是一个 客户端/服务器 架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话( Session )。

每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,对于服务器来说可能同时处理多个事务。

这样就会产生并发访问和修改数据的问题。具体来说,有这四个问题:

假设有两个会话,他们分别有一个事务,SessionA和SessionB

  1. 脏写

    事务A修改了另一个未提交的事务B修改过的数据,并且还提交了,之后如果事务B发生了回滚,数据就变成事务B修改之前的状态了,但是事务A已经提交了,按道理说数据不应该变,这就是脏写。

  2. 脏读

    事务A读取了事务B更新但还没有被提交的字段,之后如果事务B回滚,事务A读取的内容就是临时无效的。

  3. 不可重复读

    事务A读取了一个字段,然后事务B更新了这个字段,之后事务A再次读取同一个字段,值不同了,这就是发生了不可重复读

  4. 幻读

    事务A从表中读取了一个字段,然后事务B在该表中插入了一些新的行,之后如果事务A再次读取同一个表,就会多出几行,那就意味着发生了幻读,好像出现了幻觉一样,觉得是凭空出现的数据。

5.2 SQL中的四种隔离级别

5.3 MySQL支持的隔离级别

MySQL默认支持的是REPEATABLE READ

1
2
3
4
5
6
7
mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.00 sec)

修改MySQL的事务隔离级别

1
2
3
4
5
6
SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔离级别'
#其中,隔离级别格式:
> READ-UNCOMMITTED
> READ-COMMITTED
> REPEATABLE-READ
> SERIALIZABLE
  • GLOBAL

    对当前已经存在的会话无效,只对执行完该语句之后产生的会话起作用

  • SESSION

    对当前会话的所有后续的事务有效,如果在事务之间执行,则对后续事务有效,如果在已经开启的事务中间执行,这个事务的隔离级别还是原来的,不会被影响。而且只会影响当前会话

但是如果重启MySQL服务器,会恢复到默认的设置


**①脏读的现象 ** —> read-uncommitted

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#事务A,事务B的隔离级别都修改为READ-UNCOMMITTED
mysql> set session transaction_isolation = 'READ-UNCOMMITTED';
#初始表状态
mysql> select * from account;
+--------+---------+
| name | balance |
+--------+---------+
| 张三 | 100 |
| 李四 | 0 |
+--------+---------+
#事务A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set balance = balance + 100 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0 #注意,还未提交

#事务B
mysql> select * from account;#查询到的是临时有效的数据
+--------+---------+
| name | balance |
+--------+---------+
| 张三 | 200 |
| 李四 | 0 |
+--------+---------+
2 rows in set (0.00 sec)

#事务A
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

#事务B
mysql> select * from account;
+--------+---------+
| name | balance |
+--------+---------+
| 张三 | 100 |
| 李四 | 0 |
+--------+---------+
2 rows in set (0.00 sec)
#事务B前后读取的数据不一致,脏读

②如何避免脏读? —> read-committed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
mysql> set session transaction_isolation='read-committed';
Query OK, 0 rows affected (0.00 sec)
#只要未提交的修改,其他事务都不会读取出这个数据

#事务A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance = balance - 10 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from account;
+--------+---------+
| name | balance |
+--------+---------+
| 张三 | 90 |
| 李四 | 0 |
+--------+---------+
2 rows in set (0.00 sec)

#事务B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+--------+---------+
| name | balance |
+--------+---------+
| 张三 | 100 |
| 李四 | 0 |
+--------+---------+

#事务A
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
#事务B
mysql> select * from account;
+--------+---------+
| name | balance |
+--------+---------+
| 张三 | 90 |
| 李四 | 0 |
+--------+---------+
2 rows in set (0.00 sec)

③如何避免脏读和不可重复读? —> repeatable-read

在一次事务中读取的某个列的值始终是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
mysql> set session transaction_isolation='repeatable-read';
Query OK, 0 rows affected (0.00 sec)
#事务A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
#事务B
mysql> select * from account;
+--------+---------+
| name | balance |
+--------+---------+
| 张三 | 110 |
| 李四 | 0 |
+--------+---------+
2 rows in set (0.00 sec)
#事务A
mysql> update account set balance = balance + 10 where name = '张三';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
#事务B
mysql> select * from account;
+--------+---------+
| name | balance |
+--------+---------+
| 张三 | 100 |
| 李四 | 0 |
+--------+---------+
2 rows in set (0.00 sec)
#事务A
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
#事务B
mysql> select * from account;
+--------+---------+
| name | balance |
+--------+---------+
| 张三 | 100 |
| 李四 | 0 |
+--------+---------+
2 rows in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+--------+---------+
| name | balance |
+--------+---------+
| 张三 | 110 |
| 李四 | 0 |
+--------+---------+
2 rows in set (0.00 sec)

6.MySQL事务日志

事务有4种特性:原子性一致性隔离性持久性。那么事务的四种特性到底是基于什么机制实现呢?

事务的隔离性由 锁机制 实现。

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

UNDO LOG称为 回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性

6.1 redo日志

当开启事务后,进行一些列增删改操作,正常commit之后,这些数据是先保存在内存中,在一定的时间内将其刷盘到磁盘上,如果在这个时间或者还未刷盘的时间内,系统宕机了,这些数据就失效了,因此为了避免这种错误,设置了redo日志,它是保存在磁盘上的,每次进行一系列增删改操作,都先保存到redo日志中**(事务执行的过程中一直不断的往redo日志中顺序记录)**,然后在以一定频率刷盘到磁盘中,如果这个时候系统宕机了,redo日志进行数据的恢复。

InnoDB的更新操作采用的是Write-Ahead Log(预先日志持久化):在持久化一个数据页之前,先将内存中相应的日志页持久化。先写日志,再写入磁盘。

6.1.1 重做日志的缓冲 (redo log buffer)

保存在内存中,易失

innodb_log_buffer_size:redo log buffer 大小,默认 16M,最大值是4096M,最小值为1M。

1
2
3
4
5
6
7
mysql> show variables like '%innodb_log_buffer_size%';
+------------------------+----------+
| Variable_name | Value |
+------------------------+----------+
| innodb_log_buffer_size | 16777216 |
+------------------------+----------+
1 row in set (0.01 sec)

一个redo log block占用512字节


redo log的刷盘策略:这里的刷盘是指将redo log buffer(在内存中)中的内容以 一定的频率 刷入到真正的redo log file(在磁盘中) 。

redo log buffer刷盘到redo log file的过程并不是真正的刷到磁盘中去,只是刷入到 文件系统缓存(page cache)中去(这是现代操作系统为了提高文件写入效率做的一个优化),真正的写入会交给系统自己来决定

InnoDB给出 innodb_flush_log_at_trx_commit参数,该参数控制 commit提交事务时,如何将 redo log buffer 中的日志刷新到 redo log file 中。它支持三种策略:

  1. 设置为0 :
    表示每次事务提交时不进行写入文件系统缓存和刷盘操作。(通过系统默认master thread每隔1s进行一次重做日志的同步)

  2. 设置为1 :
    表示每次事务提交时都将进行同步,刷盘操作( 默认值 )

    在事务中进行更新数据时,实时将这些操作写入redo log buffer中,当事务提交后,立刻将redo log buffer中的内容写入文件系统缓存,然后os立刻将文件系统缓存刷盘到redo file中

  3. 设置为2 :
    表示每次事务提交时都只把 redo log buffer 内容写入 page cache(操作系统的缓存),不进行同步。由os自己决定什么时候同步到磁盘文件。

6.1.2 重做日志文件 (redo log file)

innodb_log_group_home_dir :指定 redo log 文件组所在的路径,默认值为./,表示在数据库的数据目录下。MySQL的默认数据目录( var/lib/mysql )下默认有两个名为 ib_logfile0 和ib_logfile1 的文件,log buffer中的日志默认情况下就是刷新到这两个磁盘文件中。此redo日志文件位置还可以修改。
innodb_log_files_in_group(log_files文件个数):指明redo log file的个数,命名方式如:ib_logfile0,iblogfile1…

采用循环使用的方式向redo日志文件组里写数据

write pos是当前记录的位置,一边写一边往后移动

checkout是当前要擦除的位置,一边删除一边往后移,删除就是指将数据加载到磁盘中后,就可以情删除日志文件中的记录了。

6.2 undo日志

在事务中 更新数据 的 前置操作 其实是要先写入一个 undo log

undo log并不是真正的恢复到原来事务开启前的状态,而是反向操作,即如果事务中进行的是insert,在undo log 中记录的就是delete,如果是update,undo日志中记录的就是反向的update,即逻辑上回到原来的效果

InnoDB对undo log的管理采用段的方式,也就是 回滚段(rollback segment) 。每个回滚段记录了1024 个 undo log segment,而在每个undo log segment段中进行 undo页 的申请。

每个事务只会使用一个回滚段,当数据被修改时,院士的数据会被赋值到回滚段中,但是由于undo log 页可以重用,所以一个回滚段在同一时刻可以服务多个事务。


回滚段中的数据分类

  1. 事务未提交的回滚数据:该数据是保存的被修改数据对应的原始数据,它关联的事务没有提交,用于实现事务一致性特性
  2. 事务已经提交但未过期的回滚数据
  3. 事务已经提交并且过期的回滚数据


先找是否有加载对应的BufferPool有就使用没有就加载读取 —-> 写入UndoLog —>操作执行数据 —> 写入RedoLogBuffer内存 —-> 写入RedoLog到磁盘文件


MySQL-事务
https://vickkkyz.fun/2022/04/07/计算机/mysql/事务/
作者
Vickkkyz
发布于
2022年4月7日
许可协议