68-MySQL-事务
一、数据库事务概述
1.1、查看当前 MySQL 支持的存储引擎都有哪些,以及这些存储引擎是否支持事务
- 命令
SHOW ENGINES ;
只有 InnoDB 引擎支持事务.png
1.2、事务特性 ACID
1.2.1、原子性(atomicity)
原子性是指把使用了
事务的工作内容包装成是一个不可分割的工作单位,要么全部提交,要么全部失败回滚
1.2.2、一致性(consistency)
根据定义,一致性是指事务执行前后,数据从一个
合法性状态变换到另外一个合法性状态。这种状态是语义上的而不是语法上的,跟具体的业务有关
1.2.3、隔离型(isolation)
事务的隔离性是指一个事务的执行
不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰
1.2.4、持久性(durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是
永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。
持久性是通过事务日志来保证的。日志包括了重做日志和回滚日志。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性
1.2.5、小结
数据库事务,其实就是数据库设计者为了方便起见,把需要保证
原子性、隔离性、一致性和持久性的一个或多个数据库操作称为一个事务
- 原子性是基础
- 隔离性是手段
- 一致性是约束
- 持久性是目的
二、事务的状态
事务是一个抽象的概念,它其实对应着一个或多个数据库操作,MySQL根据这些操作所执行的不同阶段把事务大致划分成几个状态
2.1、活动的(active)
事务对应的数据库操作正在执行过程中时,我们就说该事务处在
活动的状态
2.2、部分提交的(partially committed)
当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并
没有刷新到磁盘时,我们就说该事务处在部分提交的状态
2.3、失败的(failed)
当事务处在
活动的或者部分提交的状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在失败的状态
2.4、中止的(aborted)
如果事务执行了一部分而变为
失败的状态,那么就需要把已经修改的事务中的操作还原到事务执 行前的状态。换句话说,就是要撤销失败事务对当前数据库造成的影响。我们把这个撤销的过程称之为回滚。当回滚操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了中止的状态
2.5、提交的(committed)
当一个处在
部分提交的状态的事务将修改过的数据都同步到磁盘上之后,我们就可以说该事务处在了提交的状态。
2.6、小结
只有当
事务处于提交的或者中止的状态时,一个事务的生命周期才算是结束了。对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态
事务状态.png
三、使用事务方式
使用事务有两种方式,分别为
显式事务和隐式事务
3.1、显示使用事务
3.1.1、步骤1: START TRANSACTION 或者 BEGIN ,作用是显式开启一个事务
- SQL
BEGIN ;
或
START TRANSACTION;
-
START TRANSACTION语句相较于BEGIN特别之处在于,后边能跟随几个修饰符- ①
READ ONLY:标识当前事务是一个只读事务,也就是属于该事务的数据库操作只能读取数据,而不能修改数据 - ②
READ WRITE:标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据 - ③
WITH CONSISTENT SNAPSHOT:启动一致性读
- ①
3.1.2、步骤2:一系列事务中的操作(主要是DML,不含DDL)
3.1.3、步骤3:提交事务 或 中止事务(即回滚事务)
-
提交事务。当提交事务后,对数据库的修改是永久性的
COMMIT;
-
回滚事务。即撤销正在进行的所有没有提交的修改
ROLLBACK;
- 将事务回滚到某个保存点。
ROLLBACK TO [SAVEPOINT]
- 在事务中创建保存点,方便后续针对保存点进行回滚。一个事务中可以存在多个保存点
SAVEPOINT 保存点名称;
- 删除保存点
RELEASE SAVEPOINT 保存点名称;
3.2、隐式使用事务
MySQL中有一个系统变量
autocommit,默认为ON
- 查询命令
SHOW VARIABLES LIKE 'autocommit';
image.png
3.2.1、关闭自动提交
-
方式一:显式的的使用
START TRANSACTION或者BEGIN语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能 -
方式二:把系统变量
autocommit的值设置为OFF
SET autocommit = OFF;
或
SET autocommit = 0;
四、并发问题
MySQL是一个
客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话(Session)。每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。事务有隔离性的特性,理论上在某个事务对某个数据进行访问时,其他事务应该进行排 队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对性能影响太大,我们既想保持事务的隔离性,又想让服务器在处理访问同一数据的多个事务时性能尽量高些,那就看二者如何权衡取舍了
4.1、数据准备
- 创建表
CREATE TABLE student
(
id INT,
name VARCHAR(20),
class VARCHAR(20),
PRIMARY KEY (id)
);
- 插入数据
INSERT INTO student
VALUES (1, 'Raven', '1班');
- 查看数据
SELECT *
FROM student;
image.png
4.2、并发问题- 脏写( Dirty Write )
对于两个事务
Session A、Session B,如果事务Session A修改了 另一个未提交事务Session B修改过的数据,那就意味着发生了脏写
4.2.1、操作步骤
- 步骤1:
Session-A开启一个事务 - 步骤2:
Session-B开启一个事务,并将id=1的name改为 李四 - 步骤3:
Session-A将id=1的name改为 张三,并且提交 - 步骤4:
Session-B事务进行回滚
4.2.2、最终结果
由于
Session-B的回滚,那么Session-A中的更新也将不复存在,这种现象就称为脏写。这时Session-A中的事务就没有效果了,明明把数据更新了,最后也提交了事务了,最后看到的数据什么变化也没有。默认隔离级别下,上面Session-A中的更新语句会处于等待状态。
脏写示意图.png
4.3、并发问题- 脏读( Dirty Read )
对于两个事务
Session A、Session B,Session A读取 了已经被Session B更新但还没有被提交的字段。之后若Session B回滚,Session A读取 的内容就是临时且无效的。
4.3.1、操作步骤
- 步骤1:
Session-A开启一个事务 - 步骤2:
Session-B开启一个事务,并将id=1的name改为 张三 - 步骤3:
Session-A查询id=1的信息(name 为 张三),并提交事务 - 步骤4:
Session-B事务进行回滚
4.3.2、最终结果
Session-A读到列name的值为'张三',而Session B中的事务稍后进行了回滚,那么Session A中的事务相当于读到了一个不存在的数据,这种现象就称之为脏读
脏读示意图.png
4.4、并发问题- 不可重复读( Non-Repeatable Read )
对于两个事务
Session A、Session B,Session A读取 了一个字段,然后Session B更新 了该字段。 之后Session A再次读取 同一个字段, 值就不同 了。那就意味着发生了不可重复读
4.4.1、操作步骤
- 步骤1:
Session-A开启一个事务,查询id=1的信息name=Raven - 步骤2:
Session-B更新id=1的name=张三 - 步骤3:
Session-A查询id=1的信息name=张三 - 步骤4:
Session-B更新id=1的name=李四 - 步骤5:
Session-A查询id=1的信息name=李四
4.4.2、最终结果
Session B中提交了几个隐式事务(注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了id列为1的记录的列name的值,每次事务提交之后,如果Session A中的事务都可以查看到最新的值,这种现象也被称之为不可重复读
不可重复读示意图.png
4.5、并发问题- 幻读( Phantom )
对于两个事务
Session A、Session B,Session A从一个表中 读取 了一个字段, 然后Session B在该表中插 入了一些新的行。 之后, 如果Session A再次读取同一个表, 就会多出几行。那就意味着发生了幻读
4.5.1、操作步骤
- 步骤1:
Session-A开启一个事务,查询id>o的信息name=Raven - 步骤2:
Session-B插入一条数据name=赵六 - 步骤3:
Session-A查询id>0的信息name=张三和name=赵六两条信息
4.5.2、最终结果
Session A中的事务先根据条件 id > 0这个条件查询表student,得到了name列值为'张三'的记录;之后Session B中提交了一个隐式事务,该事务向表student中插入了一条新记录;之后Session A中的事务再根据相同的条件 id > 0查询表student,得到的结果集中包含Session B中的事务新插入的那条记录,这种现象也被称之为幻读。我们把新插入的那些记录称之为幻影记录
幻读示意图.png
五、事务隔离级别
为了解决并发带来的问题,产生了对于的隔离级别来解决上述并发问题。在上诉并发问题中严重程度依次为
脏写 > 脏读 > 不可重复读 > 幻读,并发问题解决的越彻底并发能力就越弱,所以要有所取舍,舍弃一部分隔离性来换取一部分性能。在这样的主旨下:设立了一些隔离级别,隔离级别越低,并发问题发生的就越多。
因为脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生
5.1、SQL标准的4个隔离级别
5.1.1、READ UNCOMMITTED 隔离级别
READ UNCOMMITTED:读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读、幻读
5.1.2、READ COMMITTED 隔离级别
READ COMMITTED:读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在
5.1.3、REPEATABLE READ 隔离级别
REPEATABLE READ :可重复读,事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL的默认隔离级别
5.1.4、SERIALIZABLE 隔离级别
SERIALIZABLE:可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止
其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读
隔离级别.png
隔离级别与并发能力.png
5.2、设置事务的隔离级别
MySQL的默认隔离级别为REPEATABLE READ,我们可以手动修改一下事务的隔离级别
5.2.1、查看MySQL事务的隔离级别
- MySQL 5.7.20的版本之前
SHOW VARIABLES LIKE 'tx_isolation';
- MySQL 5.7.20的版本及之后
SHOW VARIABLES LIKE 'transaction_isolation';
或
SELECT @@transaction_isolation;
image.png
5.2.2、修改MySQL事务的隔离级别
- 方式一
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL 隔离级别;
-
隔离级别格式:
- READ UNCOMMITTED
- READ COMMITTED
- REPEATABLE READ
- SERIALIZABLE
-
方式二
SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = '隔离级别'
- 隔离级别格式
- READ-UNCOMMITTED
- READ-COMMITTED
- REPEATABLE-READ
- SERIALIZABLE
5.2.3、修改MySQL事务的隔离级别使用GLOBAL或SESSION的影响
- 使用 GLOBAL 关键字(在全局范围影响)
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 或
SET GLOBAL TRANSACTION_ISOLATION = 'SERIALIZABLE';
-
效果
- 当前已经存在的会话无效
- 只对执行完该语句之后产生的会话起作用
-
使用 SESSION 关键字(在会话范围影响)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 或
SET SESSION TRANSACTION_ISOLATION = 'SERIALIZABLE';
- 效果
- 对当前会话的所有后续的事务有效
- 如果在事务之间执行,则对后续的事务有效
- 该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务
六、实战
隔离级别.png
6.1、数据准备
- 建表
CREATE TABLE `account`
(
`id` INT NOT NULL,
`balance` INT NOT NULL,
PRIMARY KEY (`id`)
);
- 插入数据
INSERT INTO account (id, balance)
VALUES (1, 100);
6.2、选择 READ UNCONMITED(读未提交)
- 步骤1:修改MySQL事务为
READ UNCONMITED 隔离级别
SET SESSION TRANSACTION_ISOLATION = 'READ-UNCOMMITTED';
- 步骤2:
Session A 和 Session B开启事务
BEGIN ;
- 步骤3:
Session A更新id=1的balance
UPDATE account
SET balance = balance + 100
WHERE id = 1
- 步骤4:
Session A查询
SELECT *
FROM account;
image.png
- 步骤5:
Session B查询
SELECT *
FROM account;
image.png
-
Session A未提交,而Session B已经读到Session A修改的数据,造成了脏读 -
步骤6:
Session A回滚
ROLLBACK ;
- 步骤7:
Session A提交
COMMIT ;
- 步骤8:
Session A和Session B最终查询到id=1的balance=100
小结
只解决了
脏写问题
6.3、选择 READ CONMITED(读已提交)
- 步骤1:修改MySQL事务为
READ CONMITED 隔离级别
SET SESSION TRANSACTION_ISOLATION = 'READ-COMMITTED';
- 步骤2:
Session A和Session B开启事务
BEGIN ;
- 步骤3:
Session A和Session B查询id=1的balance=100
SELECT *
FROM account;
- 步骤4:
Session B更新id=1的balance+=100
UPDATE account
SET balance = balance + 100
WHERE id = 1;
-
步骤5:
Session B查询id=1的balance=200
image.png
-
步骤6:
Session A查询id=1的balance=100,未发生脏读
image.png
-
步骤7:
Session A和Session B提交,最终id=1的balance=200
COMMIT ;
小结:
解决
脏读
6.4、选择 REPEATABLE READ(可重复读)
- 步骤1:修改MySQL事务为
REPEATABLE READ 隔离级别
SET SESSION TRANSACTION_ISOLATION = 'REPEATABLE-READ';
- 步骤2:
Session A和Session B开启事务
BEGIN ;
- 步骤3:
Session B插入一条数据
INSERT INTO account (id, balance)
VALUES (2, 200);
- 步骤4:提交
COMMIT ;
- 步骤5:
Session B可以查询到
SELECT *
FROM account;
- 步骤6:
Session A插入id=2一条数据
INSERT INTO account (id, balance)
VALUES (2, 200);
报主键冲突.png
- 查询时明明只有一条数据,没有id=2的数据
SELECT *
FROM account;
- 步骤7:
Session A回滚,回滚后再次查询时,有2条数据
ROLLBACK ;
小结
无法解决
幻读问题
6.4、选择 SERIALIZABLE(串行)
- 步骤1:修改MySQL事务为
SERIALIZABLE 隔离级别
SET SESSION TRANSACTION_ISOLATION = 'SERIALIZABLE';
- 步骤2:重复
6.4、选择 REPEATABLE READ(可重复读)中步骤
image.png
小结
当其他会话不提交时(COMMIT),别的会话必须要等待
image.png