【MySQL】第十四节—事务:从基础概念到隔离性理论与实践 | 详解

Hello,好久不见,我是云边有个稻草人-个人主页,与你分享MySQL领域专业知识!U·ェ·U

《MySQL》本篇文章所属专栏---持续更新中,欢迎订阅(*^▽^*)

本文详细介绍了MySQL事务的概念与特性。事务是由一组DML语句组成的逻辑单元,具有ACID特性:原子性(全部成功或失败)、一致性(数据完整性)、隔离性(并发控制)和持久性(永久修改)。重点分析了事务的隔离级别:读未提交(可能脏读)、读提交(可能不可重复读),剩下的在事务(下)。通过多个实验演示了事务的基本操作、保存点使用、自动提交设置,以及不同隔离级别下的数据可见性问题。

目录

1、什么是事务?

2、为什么会存在事务?

3、事务的版本支持

4、事务提交方式

5、事务常见操作方式

6、事务的隔离性理论

[6.1 如何理解隔离性1](#6.1 如何理解隔离性1)

[6.2 隔离级别](#6.2 隔离级别)

[6.3 查看与设置隔离性](#6.3 查看与设置隔离性)

[6.4 读未提交【Read Uncommitted】](#6.4 读未提交【Read Uncommitted】)

[6.5 读提交【Read Committed】](#6.5 读提交【Read Committed】)


正文开始------

1、什么是事务?

**事务是由一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。**MySQL提供一种机制,保证我们达到这样的效果。事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据。

一个 MySQL 数据库,不止一个事务在运行,同一时刻,甚至有大量的请求被包装成事务,在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL ,也会有很多 SQL ,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题。甚至,因为事务由多条 SQL 构成,那么,会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?

所以,一个完整的事务,绝对不是简单的 sql 集合,还需要满足如下四个属性:

**原子性:**一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。(easy)

一致性: 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。也就是说写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

**隔离性:**数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务 并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Read uncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化 ( Serializable )

**持久性:**事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。(easy)

只要我们做到了事务的原子性,隔离性,持久性就能在技术上保证数据的一致性

上面四个属性,可以简称为 ACID 。

  • 原子性(Atomicity,或称不可分割性)
  • 一致性(Consistency)
  • 隔离性(Isolation,又称独立性)
  • 持久性(Durability)。

2、为什么会存在事务?

事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?

因此事务本质上是为了应用层服务的,而不是伴随着数据库系统天生就有的


3、事务的版本支持

在 MySQL 中只有使用了 Innodb 数据库存储引擎的数据库或表才支持事务, MyISAM 不支持。

  • 查看数据库引擎
sql 复制代码
mysql> show engines;           -- 表格显示
mysql> show engines \G         -- 行显示
sql 复制代码
*************************** 1. row ***************************
     Engine: InnoDB    -- 引擎名称
     Support: DEFAULT   -- 默认引擎
     Comment: Supports transactions, row-level locking, and foreign keys -- 描述
Transactions: YES       -- 支持事务
         XA: YES
 Savepoints: YES       -- 支持事务保存点
*************************** 2. row ***************************
     Engine: MRG_MYISAM
     Support: YES
     Comment: Collection of identical MyISAM tables
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 3. row ***************************
     Engine: MEMORY    --内存引擎
     Support: YES
     Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 4. row ***************************
     Engine: BLACKHOLE
     Support: YES
     Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 5. row ***************************
     Engine: MyISAM    
     Support: YES
     Comment: MyISAM storage engine
Transactions: NO           -- MyISAM不支持事务
         XA: NO
 Savepoints: NO
*************************** 6. row ***************************
     Engine: CSV
     Support: YES
     Comment: CSV storage engine
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 7. row ***************************
     Engine: ARCHIVE
     Support: YES
     Comment: Archive storage engine
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 8. row ***************************
     Engine: PERFORMANCE_SCHEMA
     Support: YES
     Comment: Performance Schema
Transactions: NO
         XA: NO
 Savepoints: NO
*************************** 9. row ***************************
     Engine: FEDERATED
     Support: NO
     Comment: Federated MySQL storage engine
Transactions: NULL
         XA: NULL
 Savepoints: NULL
9 rows in set (0.00 sec)

4、事务提交方式

事务的提交方式常见的有两种:

  • 自动提交
  • 手动提交

查看事务提交方式

sql 复制代码
show variables like 'autocommit';

用 set 来改变 MySQL 的自动提交模式:

sql 复制代码
set autocommit=0;//禁止自动提交
sql 复制代码
set autocommit=1;//开启自动提交

5、事务常见操作方式

简单银行用户表

  • 提前准备
sql 复制代码
## 为了便于演示,我们将mysql的默认隔离级别设置成读未提交。
## 具体操作我们后面专门会讲,现在以使用为主。
mysql> set global transaction isolation level READ UNCOMMITTED;

mysql> quit
Bye

##需要重启终端,进行查看
mysql> select @@tx_isolation;

通过开启两个mysql的客户端来充当两个并发访问mysql服务的客户端,用这两个客户端来制造并发场景,来研究事务并发运行的情况

  • 创建测试表
sql 复制代码
create table if not exists account(
   id int primary key, 
   name varchar(50) not null default '', 
   blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;
  • 正常演示 - 证明事务的开始与回滚
sql 复制代码
mysql> show variables like 'autocommit';  -- 查看事务是否自动提交。我们故意设置成自动提交,看看该选项是否影响begin
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)

mysql> start transaction;               -- 开始一个事务begin也可以,推荐begin
Query OK, 0 rows affected (0.00 sec)

mysql> savepoint save1;                -- 创建一个保存点save1
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (1, '张三', 100);   -- 插入一条记录
Query OK, 1 row affected (0.05 sec)

mysql> savepoint save2;                 -- 创建一个保存点save2
Query OK, 0 rows affected (0.01 sec)

mysql> insert into account values (2, '李四', 10000);  -- 在插入一条记录
Query OK, 1 row affected (0.00 sec)

mysql> select * from account;             -- 两条记录都在了
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

mysql> rollback to save2;                 -- 回滚到保存点save2
Query OK, 0 rows affected (0.03 sec)

mysql> select * from account;             -- 一条记录没有了
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

mysql> rollback;                          -- 直接rollback,回滚在最开始
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;             -- 所有刚刚的记录没有了
Empty set (0.00 sec)
  • 也可以不用设置保存点,开始事务开始之后直接 rollback,所有的数据直接清空;
  • 回滚只能在事务运行期间操作,一旦事务提交无法回滚;
  • 非正常演示1 - 未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
sql 复制代码
-- 终端A
mysql> select * from account;          -- 当前表内无数据
Empty set (0.00 sec)

mysql> show variables like 'autocommit'; -- 依旧自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)

mysql> begin;                            --开启事务
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (1, '张三', 100);   -- 插入记录
Query OK, 1 row affected (0.00 sec)

mysql> select * from account;           --数据已经存在,但没有commit,此时同时查看

终端B
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

mysql> Aborted                          -- ctrl + \ 异常终止MySQL 

--终端B
mysql> select * from account;           --终端A崩溃前
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

mysql> select * from account;          --数据自动回滚
Empty set (0.00 sec)
  • 非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会再受影响,已经持久化
sql 复制代码
--终端 A
mysql> show variables like 'autocommit'; -- 依旧自动提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)

mysql> select * from account;   -- 当前表内无数据
Empty set (0.00 sec)

mysql> begin;                   -- 开启事务
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (1, '张三', 100); -- 插入记录
Query OK, 1 row affected (0.00 sec)

mysql> commit;                         --提交事务
Query OK, 0 rows affected (0.04 sec)

mysql> Aborted                          -- ctrl + \ 异常终止MySQL 

--终端 B
mysql> select * from account;          --数据存在了,所以commit的作用是将数据持久化到MySQL中
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
  • 非正常演示3 - 对比试验。证明begin操作会自动更改提交方式,不会受MySQL是否自动提交影响
sql 复制代码
-- 终端 A
mysql> select *from account;               --查看历史数据
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

mysql> show variables like 'autocommit';   --查看事务提交方式
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)

mysql> set autocommit=0;                   --关闭自动提交
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'autocommit';   --查看关闭之后结果
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | OFF   |
+---------------+-------+
1 row in set (0.00 sec)

mysql> begin;                             --开启事务
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (2, '李四', 10000);   --插入记录
Query OK, 1 row affected (0.00 sec)

mysql> select *from account;              --查看插入记录,同时查看终端B
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

mysql> Aborted                           --再次异常终止

-- 终端B
mysql> select * from account;       --终端A崩溃前
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

mysql> select * from account;      --终端A崩溃后,自动回滚
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)
  • 非正常演示4 - 证明单条 SQL 与事务的关系
sql 复制代码
--实验一
-- 终端A
mysql> select * from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

mysql> show variables like 'autocommit';  
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)

mysql> set autocommit=0;                  --关闭自动提交
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values (2, '李四', 10000);   --插入记录
Query OK, 1 row affected (0.00 sec)

mysql> select *from account;                   --查看结果,已经插入。此时可以在查看终端B
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

mysql> ^DBye                                    --ctrl + \ or ctrl + d,终止终端

--终端B
mysql> select * from account;                    --终端A崩溃前
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

mysql> select * from account;                   --终端A崩溃后
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec) 

-- 实验二
--终端A
mysql> show variables like 'autocommit'; --开启默认提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit   | ON   |
+---------------+-------+
1 row in set (0.00 sec)

mysql> select * from account;
+----+--------+--------+
| id | name   | blance |
+----+--------+--------+
|  1 | 张三   | 100.00 |
+----+--------+--------+
1 row in set (0.00 sec)

mysql> insert into account values (2, '李四', 10000);
Query OK, 1 row affected (0.01 sec)

mysql> select *from account;   --数据已经插入
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

mysql> Aborted              --异常终止

--终端B
mysql> select * from account;        --终端A崩溃前
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

mysql> select * from account;      --终端A崩溃后,并不影响,已经持久化。autocommit起作用
+----+--------+----------+
| id | name   | blance   |
+----+--------+----------+
|  1 | 张三   |   100.00 |
|  2 | 李四   | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)

结论:

  • 只要输入begin或者start transaction,事务便必须要通过commit提交,才会持久化,与是否设置set autocommit无关。
  • 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚
  • 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select有特殊情况,因为 MySQL 有 MVCC )
  • 从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)
  • 那么隔离性?一致性?

事务操作注意事项

  • 如果没有设置保存点,也可以回滚,只能回滚到事务的开始。
  • 直接使用 rollback(前提是事务 还没有提交) 如果一个事务被提交了(commit),则不可以回退(rollback)
  • 可以选择回退到哪个保存点 InnoDB 支持事务, MyISAM 不支持事务
  • 开始事务可以使 start transaction 或者 begin

6、事务的隔离性理论

6.1 如何理解隔离性1

  • MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行
  • 一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题, 可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。
  • 但,毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。 就如同你妈妈给你说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你妈妈不关心。那么你的学习,对你妈妈来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此时,就需要将你的学习隔离开,保证你的学习环境是健康的。
  • 数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性
  • 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别

6.2 隔离级别

读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交commit的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。

**读提交【Read Committed】 :**该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离 级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。

可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。

**串行化【Serializable】:**这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突, 从而解决了幻读的问题。它在每个读的数据行上面加上共享锁。但是可能会导致超时和锁竞争 (这种隔离级别太极端,实际生产基本不使用)

隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表 锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等。不过,我们目前现有这个认识就行, 先关注上层使用。

6.3 查看与设置隔离性

查看

sql 复制代码
select @@global.tx_isolation --查看全局隔级别
select @@session.tx_siolation --查看会话(当前)全局隔级别
select @@tx_isolation --和上面第二种相同

当前session隔离级别默认取值的是全局global隔离级别,可以修改当前隔离级别,不会影响全局隔离级别

设置

sql 复制代码
-- 设置当前会话 or 全局隔离级别语法
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}

仅修改当前session隔离级别并不会影响全局事务隔离级别,只影响当前会话,当全局和当前隔离级别不同时,会就近原则使用session隔离级别,见下:

当修改全局global隔离级别时,当前session隔离级别并不会立刻跟随全局隔离级别进行同步修改,只有在重新登录时session隔离级别会默认是global隔离级别,见下:

另起一个会话,全局隔离级别也会被影响

6.4 读未提交【Read Uncommitted】

读到了别人还未提交的数据,就叫读未提交。

--几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用

-- 设置隔离级别为 读未提交

一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读 (dirty read)

6.5 读提交【Read Committed】

but,此时还在当前事务中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段 (依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)!! (这个是问题吗??是!)我们不希望这种现象的出现

事务(上)就先到这里吧,后面的明天继续


至此事务还未结束------

我是云边有个稻草人

期待与你的下一次相遇!

相关推荐
干啥啥不行,秃头第一名2 小时前
Python深度学习入门:TensorFlow 2.0/Keras实战
jvm·数据库·python
FL4m3Y4n2 小时前
redis的主从同步与对象模型
数据库·redis·缓存
Mr.45672 小时前
JDK17+Druid+SpringBoot3+ShardingSphere5 多表分库分表完整实践(MySQL+PostgreSQL)【生产优化版】
数据库·spring boot·后端
FL4m3Y4n2 小时前
redis存储原理与数据模型
数据库·redis·缓存
蓝黑20202 小时前
把数据库表里两列的值互换
数据库·sql·mysql
梦想的旅途22 小时前
企业微信引用消息的实现与配置
数据库
是桃萌萌鸭~2 小时前
oracle中的 CDB 和 PDB 详解
数据库·oracle
woniu_buhui_fei2 小时前
MySQL知识整理一
数据库·mysql