事务由一组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 再次读取时,会多出几行。

数据库事务的隔离性:数据库系统必须具有隔离并发运行的各个事务的能力,使它们不会相互影响,避免各种并发问题。

一个事务与其它事务隔离程度称为隔离级别。数据库规定了几种不同的事务隔离级别,隔离级别越高,数据的一致性越好,但并发性越差。

事务的隔离级别

事务的隔离性可分为四种 ( 性能从低到高 )

  1. READ UNCOMMITTED ( 读未提交 ):任意事务都可以看见其他事务的未提交数据。此时,脏读、不可重复读和幻读的问题都会出现。
  2. READ COMMITTED ( 读取已提交 ):只能读取到其他事务已经提交的数据。可避免脏读问题,不能避免不可重复读和幻读。
  3. REPEATABLE READ ( 可重复读 ):确保事务可以多次读取到相同的值,在这个事务执行期间,禁止其他事务对其在读取的数据字段进行更新。可避免脏读和不可重复读的问题,不能避免幻读。
  4. SERIALIZABLE ( 串行化 ):所有的事务都会按照固定顺序执行,执行完一个事务后再继续执行下一个事务的写入操作。脏读、不可重复读和幻读都可避免。

MySQL 的默认隔离级别是 REPEATABLE READ
Oracle中默认隔离级别时 READ COMMITTED

查看隔离级别:

  1. select @@tx_isolation;

设置隔离级别:

  1. -- 语法
  2. set session|global transaction isolation level 隔离级别;
  1. -- read uncommitted
  2. set session transaction isolation level read uncommitted;
  3. -- read committed
  4. set session transaction isolation level read committed;
  5. -- repeatable read
  6. set session transaction isolation level repeatable read;
  7. -- serializable
  8. SERIALIZABLE

事务隔离级别演示

演示环境

演示的数据表如下:

  1. create table account (
  2. id int primary key auto_increment,
  3. username varchar(20),
  4. balance double
  5. );
  1. +----+----------+---------+
  2. | id | username | balance |
  3. +----+----------+---------+
  4. | 1 | 张无忌 | 1000 |
  5. | 2 | 赵敏 | 1000 |
  6. +----+----------+---------+

开启两个操作数据库的 cmd 窗口,相当于两个事务。将左侧的 cmd 窗口命名为 A ,右侧的命名为 B :
image.png

READ UNCOMMITTED

将 B 中的事务隔离级别改为 READ UNCOMMITTED

  1. MariaDB [myemployees]> set session transaction isolation level read uncommitted;
  2. Query OK, 0 rows affected (0.000 sec)
  3. MariaDB [myemployees]> select @@tx_isolation;
  4. +------------------+
  5. | @@tx_isolation |
  6. +------------------+
  7. | READ-UNCOMMITTED |
  8. +------------------+
  1. 脏读

在 A 中,开启事务,将 id=1 的记录的 username 字段更新为 john,修改后在A中读到的数据表如下:

  1. MariaDB [myemployees]> start transaction;
  2. Query OK, 0 rows affected (0.000 sec)
  3. MariaDB [myemployees]> update account set username='john' where id=1;
  4. Query OK, 1 row affected (0.001 sec)
  5. Rows matched: 1 Changed: 1 Warnings: 0
  6. MariaDB [myemployees]> select * from account;
  7. +----+----------+---------+
  8. | id | username | balance |
  9. +----+----------+---------+
  10. | 1 | john | 1000 |
  11. | 2 | 赵敏 | 1000 |
  12. +----+----------+---------+
  13. 2 rows in set (0.000 sec)

此时,在 B 中读取 account 表,结果如下:

  1. MariaDB [myemployees]> select * from account;
  2. +----+----------+---------+
  3. | id | username | balance |
  4. +----+----------+---------+
  5. | 1 | john | 1000 |
  6. | 2 | 赵敏 | 1000 |
  7. +----+----------+---------+

此时,A 可以进行回滚,回滚后 account 表如下:

  1. MariaDB [myemployees]> rollback;
  2. Query OK, 0 rows affected (0.004 sec)
  3. MariaDB [myemployees]> select * from account;
  4. +----+----------+---------+
  5. | id | username | balance |
  6. +----+----------+---------+
  7. | 1 | 张无忌 | 1000 |
  8. | 2 | 赵敏 | 1000 |
  9. +----+----------+---------+
  10. 2 rows in set (0.000 sec)

此时就 B 中就发生了脏读,B 中读到了脏数据。B 和 A 同时访问这张数据表,但是 B 不知道 A 在操作这张表,这导致 B 读到错误的数据而不自知。

  1. 不可重复读

B 读取 account 表的如下字段:

  1. MariaDB [myemployees]> select balance from account where id=1;
  2. +---------+
  3. | balance |
  4. +---------+
  5. | 1000 |
  6. +---------+
  7. 1 row in set (0.000 sec)

A 更新该字段:

  1. MariaDB [myemployees]> select * from account;
  2. +----+----------+---------+
  3. | id | username | balance |
  4. +----+----------+---------+
  5. | 1 | 张无忌 | 1000 |
  6. | 2 | 赵敏 | 1000 |
  7. +----+----------+---------+
  8. 2 rows in set (0.000 sec)
  9. MariaDB [myemployees]> start transaction;
  10. Query OK, 0 rows affected (0.000 sec)
  11. MariaDB [myemployees]> update account set balance=500 where id=1;
  12. Query OK, 1 row affected (0.001 sec)
  13. Rows matched: 1 Changed: 1 Warnings: 0
  14. MariaDB [myemployees]> commit;
  15. Query OK, 0 rows affected (0.004 sec)
  16. MariaDB [myemployees]> select * from account;
  17. +----+----------+---------+
  18. | id | username | balance |
  19. +----+----------+---------+
  20. | 1 | 张无忌 | 500 |
  21. | 2 | 赵敏 | 1000 |
  22. +----+----------+---------+
  23. 2 rows in set (0.000 sec)

B 再次读取:

  1. MariaDB [myemployees]> select balance from account where id=1;
  2. +---------+
  3. | balance |
  4. +---------+
  5. | 500 |
  6. +---------+
  7. 1 row in set (0.000 sec)

此时 B 就遇到了不可重复读。

无论是脏读、不可重复读还是幻读,都是 A 中事务对数据进行了修改,在 B 的视角下会出现很多问题。

  1. 幻读

在 B 中读取 account 表:

  1. MariaDB [myemployees]> select * from account;
  2. +----+----------+---------+
  3. | id | username | balance |
  4. +----+----------+---------+
  5. | 1 | 张无忌 | 500 |
  6. | 2 | 赵敏 | 1000 |
  7. +----+----------+---------+

在 A 中,在 account 表中插入一些数据:

  1. MariaDB [myemployees]> select * from account;
  2. +----+----------+---------+
  3. | id | username | balance |
  4. +----+----------+---------+
  5. | 1 | 张无忌 | 500 |
  6. | 2 | 赵敏 | 1000 |
  7. +----+----------+---------+
  8. 2 rows in set (0.000 sec)
  9. MariaDB [myemployees]> start transaction;
  10. Query OK, 0 rows affected (0.000 sec)
  11. MariaDB [myemployees]> insert into account values (null, '杨过',1000);
  12. Query OK, 1 row affected (0.001 sec)
  13. MariaDB [myemployees]> commit;
  14. Query OK, 0 rows affected (0.004 sec)
  15. MariaDB [myemployees]> select * from account;
  16. +----+----------+---------+
  17. | id | username | balance |
  18. +----+----------+---------+
  19. | 1 | 张无忌 | 500 |
  20. | 2 | 赵敏 | 1000 |
  21. | 4 | 杨过 | 1000 |
  22. +----+----------+---------+
  23. 3 rows in set (0.001 sec)

在 B 中读取 account 表:

  1. MariaDB [myemployees]> select * from account;
  2. +----+----------+---------+
  3. | id | username | balance |
  4. +----+----------+---------+
  5. | 1 | 张无忌 | 500 |
  6. | 2 | 赵敏 | 1000 |
  7. | 4 | 杨过 | 1000 |
  8. +----+----------+---------+
  9. 3 rows in set (0.000 sec)

此时 B 发生了幻读。

READ COMMITTED
  1. MariaDB [myemployees]> set session transaction isolation level read committed;
  2. Query OK, 0 rows affected (0.000 sec)
  3. MariaDB [myemployees]> select @@tx_isolation;
  4. +----------------+
  5. | @@tx_isolation |
  6. +----------------+
  7. | READ-COMMITTED |
  8. +----------------+
  9. 1 row in set (0.000 sec)

数据表如下:

  1. +----+----------+---------+
  2. | id | username | balance |
  3. +----+----------+---------+
  4. | 1 | 张无忌 | 1000 |
  5. | 2 | 赵敏 | 1000 |
  6. +----+----------+---------+
  1. 脏读

在 A 中开启事务,修改表中数据:

  1. MariaDB [myemployees]> start transaction;
  2. Query OK, 0 rows affected (0.000 sec)
  3. MariaDB [myemployees]> update account set username='john' where id=1;
  4. Query OK, 1 row affected (0.002 sec)
  5. Rows matched: 1 Changed: 1 Warnings: 0
  6. MariaDB [myemployees]> select * from account;
  7. +----+----------+---------+
  8. | id | username | balance |
  9. +----+----------+---------+
  10. | 1 | john | 1000 |
  11. | 2 | 赵敏 | 1000 |
  12. +----+----------+---------+
  13. 2 rows in set (0.000 sec)

在 B 中读取表格数据:

  1. MariaDB [myemployees]> select * from account;
  2. +----+----------+---------+
  3. | id | username | balance |
  4. +----+----------+---------+
  5. | 1 | 张无忌 | 1000 |
  6. | 2 | 赵敏 | 1000 |
  7. +----+----------+---------+
  8. 2 rows in set (0.000 sec)

可以看到 B 中没有出现脏读问题。

  1. 不可重复读

B 读取数据:

  1. MariaDB [myemployees]> select balance from account where id=1;
  2. +---------+
  3. | balance |
  4. +---------+
  5. | 1000 |
  6. +---------+
  7. 1 row in set (0.000 sec)

A 修改数据:

  1. MariaDB [myemployees]> start transaction;
  2. Query OK, 0 rows affected (0.000 sec)
  3. MariaDB [myemployees]> update account set balance=500 where id=1;
  4. Query OK, 1 row affected (0.001 sec)
  5. Rows matched: 1 Changed: 1 Warnings: 0
  6. MariaDB [myemployees]> commit;
  7. Query OK, 0 rows affected (0.004 sec)
  8. MariaDB [myemployees]> select * from account;
  9. +----+----------+---------+
  10. | id | username | balance |
  11. +----+----------+---------+
  12. | 1 | 张无忌 | 500 |
  13. | 2 | 赵敏 | 1000 |
  14. +----+----------+---------+
  15. 2 rows in set (0.000 sec)

B 中读取数据:

  1. MariaDB [myemployees]> select balance from account where id=1;
  2. +---------+
  3. | balance |
  4. +---------+
  5. | 500 |
  6. +---------+
  7. 1 row in set (0.000 sec)

可见 B 中存在不可重复读的问题。

  1. 幻读

B 中读取数据表:

  1. MariaDB [myemployees]> select * from account;
  2. +----+----------+---------+
  3. | id | username | balance |
  4. +----+----------+---------+
  5. | 1 | 张无忌 | 1000 |
  6. | 2 | 赵敏 | 1000 |
  7. +----+----------+---------+
  8. 2 rows in set (0.000 sec)

A 中修改数据表:

  1. MariaDB [myemployees]> select * from account;
  2. +----+----------+---------+
  3. | id | username | balance |
  4. +----+----------+---------+
  5. | 1 | 张无忌 | 1000 |
  6. | 2 | 赵敏 | 1000 |
  7. +----+----------+---------+
  8. 2 rows in set (0.000 sec)
  9. MariaDB [myemployees]> start transaction;
  10. Query OK, 0 rows affected (0.000 sec)
  11. MariaDB [myemployees]> insert into account values (null, '杨过', 1000);
  12. Query OK, 1 row affected (0.000 sec)
  13. MariaDB [myemployees]> commit;
  14. Query OK, 0 rows affected (0.003 sec)
  15. MariaDB [myemployees]> select * from account;
  16. +----+----------+---------+
  17. | id | username | balance |
  18. +----+----------+---------+
  19. | 1 | 张无忌 | 1000 |
  20. | 2 | 赵敏 | 1000 |
  21. | 6 | 杨过 | 1000 |
  22. +----+----------+---------+
  23. 3 rows in set (0.000 sec)

B 中读取数据表:

  1. MariaDB [myemployees]> select * from account;
  2. +----+----------+---------+
  3. | id | username | balance |
  4. +----+----------+---------+
  5. | 1 | 张无忌 | 1000 |
  6. | 2 | 赵敏 | 1000 |
  7. | 5 | 杨过 | 1000 |
  8. +----+----------+---------+
  9. 3 rows in set (0.000 sec)

可见 B 中存在幻读问题。

REPEATABLE READ

修改 B 中的事务隔离级别:

  1. MariaDB [myemployees]> set session transaction isolation level repeatable read;
  2. Query OK, 0 rows affected (0.000 sec)
  3. MariaDB [myemployees]> select @@tx_isolation;
  4. +-----------------+
  5. | @@tx_isolation |
  6. +-----------------+
  7. | REPEATABLE-READ |
  8. +-----------------+
  9. 1 row in set (0.000 sec)
  1. 脏读

本示例与 READ COMMITTED 小节的示例相同,同样可以看到 B 中不会出现脏读问题。

  1. 不可重复读

设置回滚点(保存点)

使用关键字 savepoint 可以设置回滚操作的回滚点。

savepoint 只能和 rollback 搭配使用。

保存点是事务中的一点,用于取消部分事务,即使用 rollback to 保存点 时,并没有结束整个事务。只有用 rollback ,整个事务才会结束,并删除事务中定义的所有保存点。

例子

数据表如下:

  1. MariaDB [myemployees]> select * from account;
  2. +----+----------+---------+
  3. | id | username | balance |
  4. +----+----------+---------+
  5. | 1 | 张无忌 | 1000 |
  6. | 2 | 赵敏 | 1000 |
  7. +----+----------+---------+
  8. 2 rows in set (0.000 sec)

savepoint 演示:

  1. MariaDB [myemployees]> start transaction;
  2. Query OK, 0 rows affected (0.000 sec)
  3. MariaDB [myemployees]> update account set balance=1100 where id=1;
  4. Query OK, 1 row affected (0.004 sec)
  5. Rows matched: 1 Changed: 1 Warnings: 0
  6. MariaDB [myemployees]> savepoint a; <- 语法:savepoint 保存点名
  7. Query OK, 0 rows affected (0.000 sec)
  8. MariaDB [myemployees]> update account set balance=1200 where id=1;
  9. Query OK, 1 row affected (0.000 sec)
  10. Rows matched: 1 Changed: 1 Warnings: 0
  11. MariaDB [myemployees]> rollback to a; <- 取消保存点a之后的事务,语法:rollback to 保存点名
  12. Query OK, 0 rows affected (0.000 sec)
  13. MariaDB [myemployees]> select * from account;
  14. +----+----------+---------+
  15. | id | username | balance |
  16. +----+----------+---------+
  17. | 1 | 张无忌 | 1100 |
  18. | 2 | 赵敏 | 1000 |
  19. +----+----------+---------+
  20. 2 rows in set (0.000 sec)
  21. MariaDB [myemployees]> rollback; <- 取消整个事务
  22. Query OK, 0 rows affected (0.003 sec)
  23. MariaDB [myemployees]> select * from account;
  24. +----+----------+---------+
  25. | id | username | balance |
  26. +----+----------+---------+
  27. | 1 | 张无忌 | 1000 |
  28. | 2 | 赵敏 | 1000 |
  29. +----+----------+---------+
  30. 2 rows in set (0.000 sec)