事务由一组SQL指令组成,这组指令要么全部执行成功,要么全部执行失败。事务能够保证一个业务的完整性。
其实,我们平时对数据库中数据进行添加或删除,一条添加或删除的SQL指令,就是一个事务,而不过这个事务只有一条SQL指令而已。
事务能够解决什么问题?
例如,A给B转账,A账上有1000元,B也有1000元,A给B转500,A账户里扣500,但还没等B账上加500,数据库炸掉了,这时候就会出问题。事务能够解决这种问题。
事务的属性:ACID
- 原子性 Atomicity:一个事务不可再分割,事务中的操作都执行成功,要么都执行失败。
- 一致性 Consistency:事务的执行,会使数据从一个一致状态切换到另一个一致状态。例如转账之前A和B账上金额总和为2000,启用事务进行转账之后,二者账上的金额总和仍要为2000。
- 隔离性 Isolation:一个事务的执行不受其他事务的干扰。
- 持久性 Durability:一个事务一旦提交,就会永久性改变数据库中的数据。
事务的创建
隐性事务:没有显式开启和提交的事务。即平时在cmd中输入的sql添加或删除的指令,就属于隐形事务。
显式事务:有显示开启和提交的事务。
事务并发问题
问题:对于同时执行的多个事务,当这些事务访问数据库中的相同数据时,若没有采取相应的隔离机制,就会导致各种问题。
脏读:对于两个事务 T1 和 T2 ,T1 读取了已被 T2 更新但没有提交的数据,这时候 T2 进行回滚,T1 读到的数据就是临时的无效数据。
不可重复读:对于两个事务 T1 和 T2 ,T1 读取了一个字段,之后 T2 更新(该操作是提交的?)了该字段,T1 再次读取该字段,T1 读到的值就不同了。即T1和T2同时操作一张表,T1读取了其中的某个字段,而T2修改了该字段,导致T1第二次读时,读到了与第一次不同的值。例如,张三去ATM里看自己账户余额,看到了自己女友给自己转的500块余额,但之后他女友撤销了这笔转账,张三第二天去看自己银行卡余额时,发现自己卡里余额为 0 ,很懵逼,这个就是不可重复读。
幻读:对于两个事务 T1 和 T2 ,T1 读取了一张表,之后 T2 在表中插入(该更新操作是提交的?)一些新的行,T1 再次读取时,会多出几行。
数据库事务的隔离性:数据库系统必须具有隔离并发运行的各个事务的能力,使它们不会相互影响,避免各种并发问题。
一个事务与其它事务隔离程度称为隔离级别。数据库规定了几种不同的事务隔离级别,隔离级别越高,数据的一致性越好,但并发性越差。
事务的隔离级别
事务的隔离性可分为四种 ( 性能从低到高 ) :
- READ UNCOMMITTED ( 读未提交 ):任意事务都可以看见其他事务的未提交数据。此时,脏读、不可重复读和幻读的问题都会出现。
- READ COMMITTED ( 读取已提交 ):只能读取到其他事务已经提交的数据。可避免脏读问题,不能避免不可重复读和幻读。
- REPEATABLE READ ( 可重复读 ):确保事务可以多次读取到相同的值,在这个事务执行期间,禁止其他事务对其在读取的数据字段进行更新。可避免脏读和不可重复读的问题,不能避免幻读。
- SERIALIZABLE ( 串行化 ):所有的事务都会按照固定顺序执行,执行完一个事务后再继续执行下一个事务的写入操作。脏读、不可重复读和幻读都可避免。
MySQL 的默认隔离级别是 REPEATABLE READ 。
Oracle中默认隔离级别时 READ COMMITTED 。
查看隔离级别:
select @@tx_isolation;
设置隔离级别:
-- 语法
set session|global transaction isolation level 隔离级别;
-- read uncommitted
set session transaction isolation level read uncommitted;
-- read committed
set session transaction isolation level read committed;
-- repeatable read
set session transaction isolation level repeatable read;
-- serializable
SERIALIZABLE
事务隔离级别演示
演示环境
演示的数据表如下:
create table account (
id int primary key auto_increment,
username varchar(20),
balance double
);
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
开启两个操作数据库的 cmd 窗口,相当于两个事务。将左侧的 cmd 窗口命名为 A ,右侧的命名为 B :
READ UNCOMMITTED
将 B 中的事务隔离级别改为 READ UNCOMMITTED
:
MariaDB [myemployees]> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
- 脏读
在 A 中,开启事务,将 id=1 的记录的 username 字段更新为 john,修改后在A中读到的数据表如下:
MariaDB [myemployees]> start transaction;
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> update account set username='john' where id=1;
Query OK, 1 row affected (0.001 sec)
Rows matched: 1 Changed: 1 Warnings: 0
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | john | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
此时,在 B 中读取 account 表,结果如下:
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | john | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
此时,A 可以进行回滚,回滚后 account 表如下:
MariaDB [myemployees]> rollback;
Query OK, 0 rows affected (0.004 sec)
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
此时就 B 中就发生了脏读,B 中读到了脏数据。B 和 A 同时访问这张数据表,但是 B 不知道 A 在操作这张表,这导致 B 读到错误的数据而不自知。
- 不可重复读
B 读取 account 表的如下字段:
MariaDB [myemployees]> select balance from account where id=1;
+---------+
| balance |
+---------+
| 1000 |
+---------+
1 row in set (0.000 sec)
A 更新该字段:
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
MariaDB [myemployees]> start transaction;
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> update account set balance=500 where id=1;
Query OK, 1 row affected (0.001 sec)
Rows matched: 1 Changed: 1 Warnings: 0
MariaDB [myemployees]> commit;
Query OK, 0 rows affected (0.004 sec)
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 500 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
B 再次读取:
MariaDB [myemployees]> select balance from account where id=1;
+---------+
| balance |
+---------+
| 500 |
+---------+
1 row in set (0.000 sec)
此时 B 就遇到了不可重复读。
无论是脏读、不可重复读还是幻读,都是 A 中事务对数据进行了修改,在 B 的视角下会出现很多问题。
- 幻读
在 B 中读取 account 表:
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 500 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
在 A 中,在 account 表中插入一些数据:
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 500 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
MariaDB [myemployees]> start transaction;
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> insert into account values (null, '杨过',1000);
Query OK, 1 row affected (0.001 sec)
MariaDB [myemployees]> commit;
Query OK, 0 rows affected (0.004 sec)
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 500 |
| 2 | 赵敏 | 1000 |
| 4 | 杨过 | 1000 |
+----+----------+---------+
3 rows in set (0.001 sec)
在 B 中读取 account 表:
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 500 |
| 2 | 赵敏 | 1000 |
| 4 | 杨过 | 1000 |
+----+----------+---------+
3 rows in set (0.000 sec)
此时 B 发生了幻读。
READ COMMITTED
MariaDB [myemployees]> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set (0.000 sec)
数据表如下:
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
- 脏读
在 A 中开启事务,修改表中数据:
MariaDB [myemployees]> start transaction;
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> update account set username='john' where id=1;
Query OK, 1 row affected (0.002 sec)
Rows matched: 1 Changed: 1 Warnings: 0
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | john | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
在 B 中读取表格数据:
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
可以看到 B 中没有出现脏读问题。
- 不可重复读
B 读取数据:
MariaDB [myemployees]> select balance from account where id=1;
+---------+
| balance |
+---------+
| 1000 |
+---------+
1 row in set (0.000 sec)
A 修改数据:
MariaDB [myemployees]> start transaction;
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> update account set balance=500 where id=1;
Query OK, 1 row affected (0.001 sec)
Rows matched: 1 Changed: 1 Warnings: 0
MariaDB [myemployees]> commit;
Query OK, 0 rows affected (0.004 sec)
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 500 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
B 中读取数据:
MariaDB [myemployees]> select balance from account where id=1;
+---------+
| balance |
+---------+
| 500 |
+---------+
1 row in set (0.000 sec)
可见 B 中存在不可重复读的问题。
- 幻读
B 中读取数据表:
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
A 中修改数据表:
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
MariaDB [myemployees]> start transaction;
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> insert into account values (null, '杨过', 1000);
Query OK, 1 row affected (0.000 sec)
MariaDB [myemployees]> commit;
Query OK, 0 rows affected (0.003 sec)
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1000 |
| 2 | 赵敏 | 1000 |
| 6 | 杨过 | 1000 |
+----+----------+---------+
3 rows in set (0.000 sec)
B 中读取数据表:
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1000 |
| 2 | 赵敏 | 1000 |
| 5 | 杨过 | 1000 |
+----+----------+---------+
3 rows in set (0.000 sec)
可见 B 中存在幻读问题。
REPEATABLE READ
修改 B 中的事务隔离级别:
MariaDB [myemployees]> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.000 sec)
- 脏读
本示例与 READ COMMITTED 小节的示例相同,同样可以看到 B 中不会出现脏读问题。
- 不可重复读
设置回滚点(保存点)
使用关键字 savepoint
可以设置回滚操作的回滚点。
savepoint
只能和 rollback
搭配使用。
保存点是事务中的一点,用于取消部分事务,即使用 rollback to 保存点
时,并没有结束整个事务。只有用 rollback ,整个事务才会结束,并删除事务中定义的所有保存点。
例子
数据表如下:
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
savepoint 演示:
MariaDB [myemployees]> start transaction;
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> update account set balance=1100 where id=1;
Query OK, 1 row affected (0.004 sec)
Rows matched: 1 Changed: 1 Warnings: 0
MariaDB [myemployees]> savepoint a; <- 语法:savepoint 保存点名
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> update account set balance=1200 where id=1;
Query OK, 1 row affected (0.000 sec)
Rows matched: 1 Changed: 1 Warnings: 0
MariaDB [myemployees]> rollback to a; <- 取消保存点a之后的事务,语法:rollback to 保存点名
Query OK, 0 rows affected (0.000 sec)
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1100 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)
MariaDB [myemployees]> rollback; <- 取消整个事务
Query OK, 0 rows affected (0.003 sec)
MariaDB [myemployees]> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | 张无忌 | 1000 |
| 2 | 赵敏 | 1000 |
+----+----------+---------+
2 rows in set (0.000 sec)