1、回滚点的使用
2、并发访问下事务产生的问题
3、掌握什么是脏读、不可重复读、幻读的概念,以及如何产生
4、通过设置不同的隔离级别来解决不同的问题。
5、串行化锁表原理
事务的基本概念请参考:如何跟女朋友解释清楚什么是事务?
事务很大程度上保障了我们业务操作的安全性。因此在很多安全性较高的场合我们都需要借助事务来帮我们完成。
MySQL中可以有两种方式进行事务的操作:
查看当前MySQL是否是自动提交事务:
show variables like 'autocommit';
No(1):开启自动提交事务(默认值)
OFF(0):关闭自动提交事务
set autocommit=0;
set autocommit
只是本次会话有用,下次会话又将变为自动提交事务。
如果需要永久生效,请到MySQL配置文件中增加:autocommit=0
功能 | SQL语句 |
---|---|
开启事务 | start transaction |
提交事务 | commit |
回滚事务 | rollback |
模拟小刚给小美转账520元
create table `user`(
id int primary key auto_increment,
name varchar(30),
money double
);
insert into `user` values(1,'xiaogang',600);
insert into `user` values(2,'xiaomei',0);
小刚初始余额:600
小美初始余额:0
-- 查询两个人的余额
select * from user;
-- 开启事务
start transaction;
-- 执行SQL操作(小刚扣钱),此时并没有真正写入到数据库(因为后续操作很有可能会有错误)
update user set money=money-520 where id=1;
-- 执行SQL操作(小美加钱),此时并没有真正写入到数据库(因为后续操作很有可能会有错误)
update user set money=money+520 where id=2;
-- 提交事务(确认无误,提交事务)
commit;
事务提交后查询最新数据,发现小刚的余额-520,剩余80,小美的余额+520,剩余520。
首先把小刚和小美的余额恢复到原始状态,小刚600元,小美0元。
-- 查询小刚和小美的余额
select * from user;
-- 开启一个事务
start transaction;
-- 小刚扣钱,此时并没有真正写入到数据库(因为后续操作很有可能会有错误)
update user set money=money-520 where id=1;
-- 小美加钱,此时并没有真正写入到数据库(因为后续操作很有可能会有错误)
update user set money=money+520 where id=2;
-- 发现中途操作其他业务时,出现了错误,那么此次事务作废(之前的所有操作全部不会记录到数据库)
rollback;
-- 再次查询数据库,发现余额还是原来的
select * from user;
在某些成功的操作完成之后,后续的操作有可能成功有可能失败,但是不管成功还是失败,前面操作都已经成功,可以在当前成功的位置设置一个回滚点。可以供后续失败操作返回到该位置,而不是回滚所有操作,这个点称之为回滚点。
回滚点的操作语句 | 语句 |
---|---|
设置回滚点 | savepoint 名字 |
回到回滚点 | rollback to 名字 |
我们来操作一段SQL,如下:
1)让小刚账号-10块钱
2) 让小美账号+10块钱
3)设置回滚点
4)让小刚账号-20块钱
5)让小美账号+20块钱
6)回滚到指定保存点
7)提交本次事务
-- 查询原始数据
select * from user;
-- 开启事务
start transaction;
-- 执行减钱操作
update user set money=money-10 where id=1;
-- 执行加钱操作
update user set money=money+10 where id=2;
-- 设置一个保存点,防止后续出现意外要回滚本次所有的操作
savepoint one;
-- 执行减钱操作
update user set money=money-10 where id=1;
-- 执行加钱操作
update user set money=money+10 where id=2;
-- 回滚到one保存点之前的状态
rollback to one;
-- 提交本次事务
commit;
-- 再次查询数据
select * from user;
设置回滚点可以让我们在失败的时候回到回滚点,而不是回到事务开启的时候。
当同时有多个用户在访问同一张表中的记录,每个用户在访问的时候都是一个单独的事务。
事务在操作时的理想状态是:事务之间不应该相互影响,实际应用的时候会引发下面三种问题。
应该尽量避免这些问题的发生。通过数据库本身的功能去避免,设置不同的隔离级别。
四种隔离级别:
级别 | 名字 | 隔离级别 | 脏读 | 不可重复读 | 幻读 | 数据库默认隔离级别 |
---|---|---|---|---|---|---|
1 | 读未提交 | read uncommitted | 是 | 是 | 是 | |
2 | 读已提交 | read committed | 否 | 是 | 是 | Oracle和SQL Server |
3 | 可重复读 | repeatable read | 否 | 否 | 是 | MySQL |
4 | 串行化 | serializable | 否 | 否 | 否 |
四种隔离级别起的作用:
Serializable
(串行化): 可以避免所有事务产生的并发访问的问题 效率及其低下Repeatable read
(可重复读):会引发幻读的问题Read committed
(读已提交):会引发不可重复读和幻读的问题Read uncommitted
(读未提交): 所有事务中的并发访问问题都会发生mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql>
MySQL默认的事务隔离级别为:
REPEATABLE-READ
(可重复读),因此MySQL会引发幻读的问题。
在并发情况下,一个事务读取到另一个事务没有提交的数据,这个数据称之为脏数据,此次读取也称为脏读。
我们知道,只有read uncommitted
(读未提交)的隔离级别才会引发脏读。
read uncommitted
(读未提交)。mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
记得需要退出当前客户端,重新打开
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)
mysql>
隔离级别已经更改为
READ-UNCOMMITTED
(读未提交)。
在窗口一执行转账操作,此时还未提交,窗口二执行查询,查询到了窗口一还未提交的脏数据。
脏读由事务隔离级别READ-UNCOMMITTED
(读未提交)所引发,一个事务中读取到了其他事务还未提交的数据,因为数据还没提交,其他事务随时可能回滚。我们不能拿还未提交到数据库中的脏数据做数据运算处理,这样是非常不安全的。
我们可以通过调高MySQL的事务隔离级别,来解决脏读问题。
read committed
(读已提交):mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
mysql>
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set, 1 warning (0.00 sec)
mysql>
修改完事务隔离级别后记得要重启客户端(重新连接)
操作一段SQL,观察变化:
在同一个事务中的多次查询应该出现相同的结果,两次读取不能出现不同的结果。
隔离级别为read committed
(读已提交)会引发不可重复读,我们在上面的演示中能够看出,小美窗口一次事务中多次查询到的数据不一致,这就是不可重复读。在一次事务中,我们应该保证多次查询的数据是一致的。
在窗口一执行转账操作,此时还未提交,窗口二执行查询,发现数据是正常的(还未被修改),此时窗口提交数据,窗口二再次执行查询,发现数据被修改了。在一次事务中多次查询的结果不一致。
不可重复读问题由read committed
(读已提交)所引发,一个事务中,多次读取到的数据不一致(读取到了其他事务提交的数据)。Oracle和SQL Server数据库的默认隔离级别为读已提交,因此会引发不可重复读问题。
我们再次提高事务的隔离级别,来解决不可重复读问题。
repeatable read
(可重复读):mysql> set global transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
注意重新启动MySQL客户端
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql>
操作如下SQL,观察变化:
MySQL的默认隔离级别为
repeatable read
(可重复读)。
幻读由事务隔离级别可重复读引发,在一个事务内读取到了别的事务插入的数据,由于"可重复读"的原因,查询不到那条数据,想插入那条数据发现提示"数据已存在",但是又查询不到,就好像出现幻觉一样。
在窗口一和窗口二分别开启事务,并查询当前user表信息,都发现id为3的用户,窗口一率先插入id为3的用户,**并且提交。**此时窗口二再次查询数据表信息,发现依旧没有id为3的用户,于是想插入id为3的数据,发现报错(主键冲突),此时窗口二感觉很诡异,于是再次查询数据表信息,发现依旧没有id为3的用户,就好像出现幻觉一样,明明查询不到,但是又不让插入。
我们再次将事务隔离级别调高,来解决幻读问题。
repeatable read
(串行化):mysql> set global transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
重启MySQL客户端
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
1 row in set, 1 warning (0.00 sec)
mysql>
通过上面的演示,我们知道隔离级别为serializable
(串行化)可以解决幻读问题,但是随之而然给我们带来的弊端非常明显,那就是并发性能极差。
在窗口一的事务中,一旦有更新操作(insert、update、delete)等, 那么会将整表都锁起来,不让其他客户端(事务)操作,而且是读写都不让!如果是在高并发情况下,串行化毋庸置疑会导致大量的锁等待,性能极低,因此实际生产中没有人会使用串行化的隔离级别。
其实我们通过名词就能知道,“串行化"明显和并行(并发)是冲突的,串行化要求的是"串行”,而我们我们实际开发则要求的是"并行"。
串行化的隔离级别在一个客户端(事务)执行任何的更新操作(insert、update、delete)等,其他客户端(事务),均不可对该表进行读写操作(记住,是读写均不能操作!)。
1)MySQL的默认隔离级别为:repeatable read
(可重复读)
2)Oracle和SQL Server的默认隔离级别为:read committed
(读已提交)
3)回滚点的概念:在一次事务中,操作的N多次数据库,但由于某次操作失误,需要rollback,但是一旦执行rollback,回滚此次事务的所有操作,因此我们可以在一次事务中,执行过若干次代码后,设置一个保存点,如果后续出现上面失误操作,可以回滚到指定的位置。
4)回滚点的指令:savepoint name
5)事务在并发访问下的安全问题(隔离级别)
6)脏读、不可重复读、幻读的基本概念
7)通过隔离级别来解决不同问题
8)牢记串行化给我们带来的问题(锁表)