主要内容:
- 能够理解事务的概念
- 能够说出事务的特点
- 能够在MySQL中使用事务
- 能够理解脏读、不可重复读、幻读的概念及解决办法
1 事务的基本介绍
1.1 概念
如果一个包含多个步骤的业务操作,被事务管理,那么这些操作要么同时成功,要么同时失败。
事务执行是一个整体,所有的SQL语句都必须执行成功。如果其中有1条SQL语句出现异常,则所有的SQL语句都要回滚,整个业务执行失败。
1.2 特征
- 原子性(Atomicity):每个事务都是一个整体,不可再拆分,事务中所有的SQL语句要么同时成功,要么同时失败。
- 一致性(Consistency):事务操作前后,数据总量不变。如:转账前2个人的总金额是2000,转账后2个人总金额也是2000
- 隔离性(Isolation):多个事务之间,彼此独立,互不影响。
- 持久性(Durability):当事务提交或回滚后,数据库会持久化的保存数据。
1.3 应用场景
在实际的开发过程中,一个业务操作如:转账,往往是要多次访问数据库才能完成的。转账是一个用户扣钱,另一个用户加钱。
-- 创建数据表
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
NAME VARCHAR (10),
balance DOUBLE
);
-- 添加数据
INSERT INTO account (NAME, balance) VALUES ('张三', 1000), ('李四', 1000);
模拟张三给李四转500元钱,一个转账的业务操作最少要执行下面的2条语句:
-- 张三账号-500
UPDATE account SET balance = balance - 500 WHERE NAME = '张三';
-- 李四账号+500
UPDATE account SET balance = balance + 500 WHERE NAME = '李四';
假设当张三账号上-500元,服务器崩溃了。李四的账号并没有+500元,数据就出现问题了。我们需要保证其中一条SQL语句出现问题,整个转账就算失败。只有两条SQL都成功了转账才算成功。这个时候就需要用到事务。
2 提交事务
2.1 手动提交事务
-
SQL语句:
功能 SQL语句 开启事务 start transaction; 提交事务 commit; 回滚事务 rollback; -
过程:
-
案例1 提交事务:
模拟张三给李四转500元钱(成功)目前数据库数据如下:
使用DOS控制台进入MySQL:-- 1) 开启事务 START TRANSACTION; -- 2) 张三账号-500 UPDATE account SET balance = balance - 500 WHERE NAME = '张三'; -- 3) 李四账号+500 UPDATE account SET balance = balance + 500 WHERE NAME = '李四';
此时查看数据库,发现数据并没有改变。在控制台提交事务:
-- 4) 提交事务 COMMIT;
再查看数据库,发现数据改变:
-
案例2 回滚事务:
模拟张三给李四转500元钱(失败)目前数据库数据如下:
使用DOS控制台进入MySQL:-- 1) 开启事务 START TRANSACTION; -- 2) 张三账号-500 UPDATE account SET balance = balance - 500 WHERE NAME = '张三'; -- 3) 李四账号+500 UPDATE account SET balance = balance + 500 WHERE NAME = '李四';
此时查看数据库,发现数据并没有改变。在控制台回滚事务:
-- 4) 回滚事务 ROLLBACK;
再查看数据库,发现数据没有改变:
如果事务中SQL语句没有问题,commit提交事务,会对数据库数据的数据进行改变。 如果事务中SQL语句有问题,rollback回滚事务,会回退到开启事务时的状态。
2.2 自动提交事务
- 概念:MySQL默认每一条DML(增删改)语句都是一个单独的事务,每条语句都会自动开启一个事务,语句执行完毕自动提交事务。
- 取消自动提交:
- 查看MySQL是否开启自动提交事务:
SELECT @@autocommit;
@@
表示全局变量,1
表示开启,0
表示关闭。 - 取消自动提交事务:
SET @@autocommit = 0;
- 执行更新语句,查看数据库,发现数据并没有改变;
- 在控制台执行commit提交任务,数据改变。
- 查看MySQL是否开启自动提交事务:
3 事务的原理
事务开启之后,所有的操作都会临时保存到事务日志中,事务日志只有在得到commit命令才会同步到数据表中,其他任何情况都会清空事务日志(rollback,断开连接)。
- 客户端连接数据库服务器,创建连接时创建此用户临时日志文件;
- 开启事务以后,所有的操作都会先写入到临时日志文件中;
- 所有的查询操作从表中查询,但会经过日志文件加工后才返回;
- 如果事务提交则将日志文件中的数据写到表中,否则清空日志文件。
4 回滚点
在某些成功的操作完成之后,后续的操作有可能成功有可能失败,但是不管成功还是失败,前面操作都已经成功,可以在当前成功的位置设置一个回滚点。可以供后续失败操作返回到该位置,而不是返回所有操作,这个点称之为回滚点。
- 回滚点的操作语句:
回滚点的操作语句 语句 设置回滚点 savepoint 名字 回到回滚点 rollback to 名字 - 案例:
- 开启事务;
- 让张三账号减3次钱,每次10块;
- 设置回滚点:
SAVEPOINT three_times;
- 让张三账号减4次钱,每次10块;
- 回到回滚点:
ROLLBACK TO three_times;
- 总结:设置回滚点可以让我们在失败的时候回到回滚点,而不是回到事务开启的时候。
5 事务的隔离级别【了解】
5.1 概念
事务在操作时的理想状态—— 所有的事务之间保持隔离,互不影响。但是如果多个事务同时操作同一个数据,可能引发并发访问的问题,设置不同的隔离级别就可以解决这些问题。
5.2 并发访问的问题
并发访问的问题 | 含义 |
---|---|
脏读 | 一个事务,读取到另一个事务尚未提交的数据 |
不可重复读(虚读) | 在一个事务中,两次读取到的数据内容不一致。 要求的是一个事务中多次读取时数据的内容是一致的,这是事务 update 时引发的问题 |
幻读 | 一个事务操作数据表时,另一个事务添加了一条数据,则第一个事务查询不到自己的修改。 要求在一个事务多次读取时数据的数量是一致的,这是 insert 或 delete 时引发的问题。 |
5.3 隔离级别
-
分类:✔️表示会出现该问题,❌表示不会出现该问题。
名称 隔离级别 脏读 不可重复读 幻读 数据库默认 read uncommitted
读未提交1 ✔️ ✔️ ✔️ read committed
读已提交2 ❌ ✔️ ✔️ Oracle、SQL Server repeatable read
可重复读3 ❌ ❌ ✔️ MySQL serializable
串行化4 ❌ ❌ ❌ 注意:隔离级别越高,性能越差,安全性越高。
-
MySQL事务隔离级别相关的命令:
-
查询隔离级别:
SELECT @@tx_isolation;
-
设置隔离级别:需要退出MySQL再重新登录才能看到隔离级别的变化。
SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串;
-- 设置隔离级别为“读未提交” SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-
5.4 演示
5.4.1 脏读
-
脏读的演示:脏读是非常危险的,比如张三向李四购买商品,张三开启事务,向李四账号转入500块,然后打电话给李四说钱已经转了。李四一查询钱到账了,发货给张三。张三收到货后回滚事务,李四的再查看钱没了。
- 将数据进行恢复:
UPDATE account SET balance = 1000;
- 打开A窗口登录MySQL,设置全局的隔离级别为最低:
mysql -uroot -proot SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
- 打开B窗口,AB窗口都开启事务:
USE day23; START TRANSACTION;
- A窗口更新2个人的账户数据,未提交:
UPDATE account SET balance=balance-500 WHERE id=1; UPDATE account SET balance=balance+500 WHERE id=2;
- B窗口查询账户:
SELECT * FROM account;
- A窗口回滚事务:
ROLLBACK;
- B窗口查询账户,钱没了:
- 将数据进行恢复:
-
解决脏读的方法:将全局的隔离级别进行提升。
- 将数据进行恢复:
UPDATE account SET balance = 1000;
- 在A窗口设置全局的隔离级别为read committed:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 打开B窗口,AB窗口都开启事务:
USE day23; START TRANSACTION;
- A窗口更新2个人的账户数据,未提交:
UPDATE account SET balance=balance-500 WHERE id=1; UPDATE account SET balance=balance+500 WHERE id=2;
- B窗口查询账户:
SELECT * FROM account;
- A窗口提交事务:
COMMIT;
- B窗口查询账户:
- 将数据进行恢复:
5.4.2 不可重复读(虚读)
- 不可重复读的演示:比如银行程序需要将查询结果分别输出到电脑屏幕和发短信给客户,结果在一个事务中针对不同的输出目的地进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了。
- 将数据进行恢复:
UPDATE account SET balance = 1000;
- 在A窗口设置全局的隔离级别为read committed:
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 打开B窗口,开启事务,查询账户:
START TRANSACTION; SELECT * FROM account;
- 在A窗口开启事务,并更新数据:
START TRANSACTION; UPDATE account SET balance=balance+500 WHERE id=1; COMMIT;
- B窗口查询账户:
SELECT * FROM account;
- 将数据进行恢复:
- 解决不可重复读的方法:将全局的隔离级别进行提升为repeatable read。
- 将数据进行恢复:
UPDATE account SET balance = 1000;
- 在A窗口设置全局的隔离级别为repeatable read:
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
- 打开B窗口,开启事务,查询账户:
START TRANSACTION; SELECT * FROM account;
- 在A窗口开启事务,并更新数据:
START TRANSACTION; UPDATE account SET balance=balance+500 WHERE id=1; COMMIT;
- B窗口查询账户:
SELECT * FROM account;
- 将数据进行恢复:
5.4.3 幻读
解决幻读的方法:使用serializable隔离级别,一个事务没有执行完,其他事务的SQL执行不了,可以挡住幻读。
- 将数据进行恢复:
UPDATE account SET balance = 1000;
- 在A窗口设置隔离级别为最高:
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
- A窗口退出并重新登录MySQL,查询记录数:
START TRANSACTION; SELECT count(*) FROM account;
- 打开B窗口,开启事务,添加一条记录:
INSERT INTO account (NAME, balance) VALUES ('王五', 500);
- 在A窗口中commit提交事务,B窗口中insert语句便可以立马运行:
- 在A窗口中接着查询,发现数据不变:
SELECT count(*) FROM account;
- B窗口中commit提交当前事务:
- A窗口就能看到最新的数据: