
事务(Transactions)
事务可以将一组 SQL
语句打包成一个整体,使这些 SQL
语句成为原子的。
这些 SQL
语句要么全部执行成功,要么全部执行失败。只要有一个 SQL
语句失败,整个事务就都失败。只有所有的 SQL
语句都执行成功,整个事务才算成功。
通过事务就可以保证数据库中数据的一致性。
比如:在银行转账过程中,势必会出现一方钱增多,另一方钱减少的情况。如果此时发生一些未知的错误后,导致出现一方钱增多,但另一方的钱没有减少的情况;或者出现一方钱减少,但另一方的钱没有增多的情况。
通过事务,减这一系列操作打包到一起,如果某个环节出现了错误,那么就会回滚到事务开始之前的状态,从而保证了数据的一致性。
事务的四大特性
原子性(Atomicity)
事务是一个不可分割的工作单位,事务中包括的诸多操作要么都做,要么都不做。事务的原子性确保了事务中诸多操作的原子性,要么全部成功,要么全部失败。
一致性(Consistency)
事务执行完后,确保数据正确并且符合预期,使得数据保持一致。比如转账业务中,转完帐后,两个账户的余额之和应该是不变的。
隔离性(Isolation)
事务之间彼此隔离,一个事务的执行不能影响其他事务的执行。保证各并发事务的数据之间是独立的。事务的隔离级别有四个级别:读未提交(Read Uncommitted)、读提交(Read Committed)、可重复读(Repeatable Read)、串行化(Serializable)。
持久性(Durability)
事务一旦提交,对数据库中的数据修改就是永久性的,即使系统崩溃也不会丢失。
只有确保了事务的原子性、隔离性和持久性,才能确保数据库的一致性。
事务的使用
在 MySQL 中的存储引擎中,InnoDB 存储引擎支持事务。可以通过 show engines;
命令查看 MySQL 各个存储引擎的一些说明和支持情况。
语法
sql
# 开启是一个事务
START TRANSACTION;
# 或
BEGIN;
# 事务内的 SQL 语句
SELECT * FROM table_name;
...
# 提交事务,并对其改为持久化保存
COMMIT;
# 回滚当前事务,取消其更改
ROLLBACK;
# 设置事务的隔离级别
SET TRANSACTION ISOLATION LEVEL <level>;
# 查询当前事务的隔离级别(MySQL 8.0+)
SELECT @@transaction_isolation;
保存点
在事务执行时,可以在指定位置设置保存点,回滚时指定保存点即可把数据恢复到保存点的状态。
sql
# 设置保存点
SAVEPOINT save_point_name;
# 回滚到保存点
ROLLBACK TO save_point_name;
自动/手动提交事务
默认情况下,MySQL 的事务是自动提交的,一个事务中只包含一条 DML
语句。
通过 show variables like 'autocommit';
命令可以查看当前的自动提交设置。
通过 SET autocommit = 0;
命令可以禁止自动提交。
注意:
- 只要使用
START TRANSACTION
或者BEGIN
语句开启了一个事务,必须要通过COMMIT
来提交事务,与是否设置SET autocommit
无关。 - 手动提交模式下,不用显示开启事务,执行修改操作后,提交或回滚事务时直接使用
COMMIT
或ROLLBACK
命令。 - 已提交的事务不可进行回滚。
事务的隔离级别
现有 事务A
和 事务B
,事务A
对某个数据的值进行了修改操作,此时 事务B
读取到先前 事务A
修改后的值。
读未提交(Read Uncommitted)
这是最低的隔离级别,允许事务读取其他事务未提交的数据。
注意,此时 事务A
还未 COMMIT
,若 事务A
执行 ROLLBACK
,则先前 事务B
读到的数据就是脏数据 。这种行为被称为 脏读(Dirty Read)。
读已提交(Read Committed)
读已提交只允许事务读取其他事务已提交的数据,避免了脏读。
事务A
还未 COMMIT
时,事务B
读取到的数据还是 事务A
修改前的老数据。
不过,在同一个事务中,多次读取同一数据时,可能会得到不一样的数据,这便是不可重复读(Non-Repeatable Read)。
如果 事务B
有两次读取的操作,第一次执行读取的时机是 事务A
提交前,由于读已提交 的隔离级别,事务B
读取到的还是老数据;第二次执行读取的时机是 事务A
提交后,此时 事务B
读取到的就是新数据,与之前的老数据不同(注意,事务B
的执行始终没有停止)。这便是不可重复读。
可重复读(Repeatable Read)
这是 MySQL 默认的隔离级别,确保同一事务在读取同一数据时,得到的结果始终是一致的,避免了不可重复读。
也就是说,即使其他事务修改并提交了数据,当前事务再次读取时看到的还是原来的数据。这通过多版本并发控制(MVCC) 来实现的。
虽然解决了不可重复读的问题,但还是可能出现幻读(Phantom Read)。
幻读
幻读是指在一个事务中,同样的查询条件,第二次查询时返回了更多的行,因为其他事务插入并提交了符合条件的数据。
现有 test
表,数据为:
id | salary | |
---|---|---|
1 | 1 | 8000 |
2 | 6 | 500 |
以及 查询1
和 查询2
两个事务:
查询1
事务:
sql
START TRANSACTION;
SELECT * FROM test WHERE salary = 500; # 快照读
SELECT * FROM test WHERE salary = 500 FOR UPDATE # 当前读;
COMMIT;
查询2
事务:
sql
START TRANSACTION;
INSERT INTO test salary VALUES 500;
COMMIT;
查询1
和 查询2
同时开启事务,查询2
先执行插入操作,此时 test
表数据为:
id | salary | |
---|---|---|
1 | 1 | 8000 |
2 | 6 | 500 |
3 | 7 | 500 |
查询1
事务执行快照读时,得到的结果:
id | salary | |
---|---|---|
2 | 6 | 500 |
此时并没有出现幻读。
查询1
事务执行当前读时,得到的结果:
id | salary | |
---|---|---|
2 | 6 | 500 |
3 | 7 | 500 |
此时出现了两条记录,这便发生了幻读。
解决幻读的方式有很多,但是它们的核心思想就是一个事务在操作某张表数据的时候,另外一个事务不允许新增或者删除这张表中的数据了。解决幻读的方式主要有以下几种:
- 将事务隔离级别调整为
SERIALIZABLE
。 - 在可重复读的事务级别下,给事务操作的这张表添加表锁。
- 在可重复读的事务级别下,给事务操作的这张表添加
Next-key Lock(Record Lock+Gap Lock)
串行化(Serializable)
串行化是最高的隔离级别,确保事务的执行按照顺序串行化执行。
但是这样会严重影响性能,因为并发度大大降低。
总结
隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 | 隔离力度 |
---|---|---|---|---|---|
READ UNCOMMITTED | ❌ | ❌ | ❌ | 高 | 最低 |
READ COMMITTED | ✅ | ❌ | ❌ | 中 | 低 |
REPEATABLE READ | ✅ | ✅ | ❌ | 低 | 中 |
SERIALIZABLE | ✅ | ✅ | ✅ | 最低 | 高 |