MYSQL的事务管理
1. 事务的应用场景
在实际的开发过程中,一个业务操作如:转账,往往是要多次访问数据库才能完成的。转账是一个用户扣钱,另一个用户加钱。如果其中有一条 SQL 语句出现异常,这条 SQL 就可能执行失败。
例如:
-- 创建数据表
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR(10),
balance DOUBLE
);
-- 添加数据
INSERT INTO account (NAME, balance) VALUES ('张三', 1000), ('李四', 1000);
-- 模拟张三给李四转 500 元钱
-- 张三账号-500
update account set balance = balance - 500 where name='张三';
-- 李四账号+500
update account set balance = balance + 500 where name='李四';
假设当张三账号上-500 元,服务器崩溃了。李四的账号并没有+500 元,数据就出现问题了。我们需要保证其中 一条 SQL 语句出现问题,整个转账就算失败。只有两条 SQL 都成功了转账才算成功。这个时候就需要用到事务。
事务执行是一个整体,所有的 SQL 语句都必须执行成功。如果其中有 1 条 SQL 语句出现异常,则所有的 SQL 语句都要回滚,整个业务执行失败。
2. 事务的使用
MYSQL 中可以有两种方式进行事务的操作:
- 手动提交事务
- 自动提交事务
2.1 手动提交事务
手动提交事务的 SQL 语句:
- 开启事务 :start transaction;
- 提交事务 :commit;
- 回滚事务 :rollback;
手动提交事务使用过程:
- 执行成功的情况: 开启事务 ==>执行多条 SQL 语句 ==> 成功提交事务
- 执行失败的情况: 开启事务 ==>执行多条 SQL 语句 ==> 事务的回滚
(1)模拟事务提交成功
-- 第一步:开启事务
start transaction;
-- 第二步:张三账号-500
update account set balance = balance - 500 where name='张三';
-- 第三步:李四账号+500
update account set balance = balance + 500 where name='李四';
-- 第四步:事务提交
commit;
-- 第五步:查询数据库,发现数据发生改变
select * from account;
(2)模拟出现异常事务回滚
-- 第一步:开启事务
start transaction;
-- 第二步:张三账号-500
update account set balance = balance - 500 where name='张三';
-- 第三步:假设此时发生异常,李四账户没有+500
-- update account set balance = balance + 500 where name='李四';
-- 第四步:事务回滚
rollback;
-- 第五步:查询数据库,发现数据没有改变
select * from account;
2.2 自动提交事务
MySQL 默认每一条 DML(增删改)语句都是一个单独的事务,每条语句都会自动开启一个事务,语句执行完毕自动提交事务,MySQL 默认开始自动提交事务。
我们用代码演示一下自动提交事务:
-- 第一步:张三账号-500
update account set balance = balance - 500 where name='张三';
-- 第二步:查询数据库,发现数据发生改变,证明事务已自动提交
select * from account;
(1)取消自动提交事务
-- 第一步:查看MySQL 是否开启自动提交事务
-- @@表示表示全局变量,查询结果为1,1 表示开启,0 表示关闭。
select @@autocommit;
-- 第二步: 取消自动提交事务
set @@autocommit=0;
-- 第三步:张三账号-500
update account set balance = balance - 500 where name='张三';
-- 第四步:查询数据库,发现数据没有改变,证明已取消事务自动提交
select * from account;
3. 事务的原理
事务开启之后, 所有的操作都会临时保存到事务日志中, 事务日志只有在得到 commit 命令才会同步到数据表中,其他任何情况都会清空事务日志(如rollback,断开连接) 。
原理图如下:

由上图可知事务的实现步骤:
- 客户端连接数据库服务器,创建连接时创建此用户临时日志文件
- 开启事务以后,所有的操作都会先写入到临时日志文件中
- 所有的查询操作从表中查询,但会经过日志文件加工后才返回
- 如果事务提交则将日志文件中的数据写到表中,否则清空日志文件。
4. 事务回滚点
在某些成功的操作完成之后,后续的操作有可能成功有可能失败,但是不管成功还是失败,前面操作都已经成 功,可以在当前成功的位置设置一个回滚点。可以供后续失败操作返回到该位置,而不是返回所有操作,这个点称 之为回滚点。
回滚点的SQL语句:
- 设置回滚点 :savepoint 回滚点名称
- 回到回滚点 :rollback to 回滚点名称
使用示例:
-- 1.开启事务
start transaction;
-- 2.让张三账号减 100 块
update account set balance = balance - 100 where name='张三';
-- 3.设置回滚点:savepoint first_time;
savepoint first_time;
-- 4.让张三账号减 2 次钱,每次 200 块
update account set balance = balance - 200 where name='张三';
update account set balance = balance - 200 where name='张三';
-- 5.回到回滚点:rollback to first_time;
rollback to first_time;
注意:设置回滚点可以让我们在失败的时候回到回滚点,而不是回到事务开启的时候。
5. 事务的隔离级别
5.1 事务的四大特性(ACID)
了解事务的隔离级别前,我们先熟悉一下事务的四大特性(ACID):

5.2 事务的隔离级别
由于事务四大特性中的隔离性,所有的事务之间保持隔离,互不影响。但是因为并发操作,多个用户同时访问同一个 数据,可能引发并发访问的问题:

为了解决并发访问时引发的问题,各大数据库都对其进行了相应的处理:
上面的级别最低,下面的级别最高。“是”表示会出现这种问题,“否”表示不会出现这种问题。

注:隔离级别越高,性能越差,安全性越高。
5.3 MySQL 事务隔离级别相关的命令
(1)查询全局事务隔离级别
select @@tx_isolation;
(2)设置全局事务隔离级别
-- 级别字符串:read uncommitted、read committed、repeatable read、serializable
set global transaction isolation level 级别字符串;
5.4 脏读演示
mysql默认的隔离级别为repeatable read,可以避免脏读和幻读。
为了演示脏读效果,我们把mysql隔离级别调到最低:read uncommitted
set global transaction isolation level read uncommitted;
(1)打开A、B两个查询窗口,并且都开启事务

(2)A 窗口更新 2 个人的账户数据,未提交

(3)B窗口查询account表数据

我们发现B窗口读到了A窗口未提交的数据
(4)A窗口回滚后,B窗口再查询account表数据

A窗口回滚后,B窗口再查询,发现钱又回到了原来的样子。
脏读是非常危险的,比如张三向李四购买商品,张三开启事务,向李四账号转入 500 块,然后打电话给李四说钱 已经转了。李四一查询钱到账了,发货给张三。张三收到货后回滚事务,李四的再查看钱没了。
我们将隔离级别提升至read committed,可以解决脏读这个问题:
set global transaction isolation level read committed;
5.5 不可重复读演示
将隔离级别提升至read committed后,脏读问题解决了。但是还有一个问题我们来看下:
(1)A、B两窗口都开启事务后,B窗口先查询数据

(2)在 A 窗口更新数据并提交后,B窗口再查询一次

我们发现,在同一事务中,B窗口两次查询输出的结果不同,到底哪次是对的?不知道以哪次为准。 很多人认为这种情况就对了,无须困惑, 当然是后面的为准。我们可以考虑这样一种情况,比如银行程序需要将查询结果分别输出到电脑屏幕和发短信给客 户,结果在一个事务中针对不同的输出目的地进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了。
解决不可重复读的问题: 将全局的隔离级别进行提升为repeatable read即可
set global transaction isolation level repeatable read;
5.6 幻读演示
幻读出现在可重复读(repeatable read)隔离级别下,普通的SELECT查询就是快照读,是不会看到别的事务插入的数据的。因此,幻读在“当前读”下才会出现。
语法:
-- 在查询语句后面加上 for update,即为当前读,可以看到幻读数据
select count(*) from account for update;
接下来我们来演示一下幻读:
(1)A、B两窗口都开启事务后,B窗口先查询acount表记录的数量

(2)然后A窗口插入一条新纪录并提交事务

(3)B窗口使用当前读,再次查询acount表记录的数量

我们发现,B窗口在同一事务中,两次读取到表记录的数量不一致,此即为幻读。
要解决幻读问题,就要将隔离级别进行提升到最高等级:serializable
set global transaction isolation level serializable;
注意:隔离级别越高,数据库效率也就越低,我们要根据实际需求来选择合理的隔离级别。
浙公网安备 33010602011771号