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;

手动提交事务使用过程:

  1. 执行成功的情况: 开启事务 ==>执行多条 SQL 语句 ==> 成功提交事务
  2. 执行失败的情况: 开启事务 ==>执行多条 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,断开连接) 。

原理图如下:

image-20201119110127938

由上图可知事务的实现步骤:

  1. 客户端连接数据库服务器,创建连接时创建此用户临时日志文件
  2. 开启事务以后,所有的操作都会先写入到临时日志文件中
  3. 所有的查询操作从表中查询,但会经过日志文件加工后才返回
  4. 如果事务提交则将日志文件中的数据写到表中,否则清空日志文件。

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):

image-20201119111157199

5.2 事务的隔离级别

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

image-20201119111448100

为了解决并发访问时引发的问题,各大数据库都对其进行了相应的处理:

上面的级别最低,下面的级别最高。“是”表示会出现这种问题,“否”表示不会出现这种问题。

image-20201119111838605

注:隔离级别越高,性能越差,安全性越高。

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两个查询窗口,并且都开启事务

image-20201119114118114

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

image-20201119114337186

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

image-20201119114503316

我们发现B窗口读到了A窗口未提交的数据

(4)A窗口回滚后,B窗口再查询account表数据

image-20201119114645356

A窗口回滚后,B窗口再查询,发现钱又回到了原来的样子。

脏读是非常危险的,比如张三向李四购买商品,张三开启事务,向李四账号转入 500 块,然后打电话给李四说钱 已经转了。李四一查询钱到账了,发货给张三。张三收到货后回滚事务,李四的再查看钱没了。

我们将隔离级别提升至read committed,可以解决脏读这个问题:

set global transaction isolation level read committed;

5.5 不可重复读演示

将隔离级别提升至read committed后,脏读问题解决了。但是还有一个问题我们来看下:

(1)A、B两窗口都开启事务后,B窗口先查询数据

image-20201119135630257

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

image-20201119140025789

我们发现,在同一事务中,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表记录的数量

image-20201119143156690

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

image-20201119143224719

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

image-20201119143324784

我们发现,B窗口在同一事务中,两次读取到表记录的数量不一致,此即为幻读。

要解决幻读问题,就要将隔离级别进行提升到最高等级:serializable

set global transaction isolation level serializable;

注意:隔离级别越高,数据库效率也就越低,我们要根据实际需求来选择合理的隔离级别。

posted @ 2020-11-19 14:37  渺渺孤烟起  阅读(150)  评论(0)    收藏  举报