事务是什么
事务(Transaction)是一组操作单元,这些操作要么全部执行,要么全部不执行,是数据库管理系统中用于保证数据一致性、完整性和可靠性的机制。
事务具有四个特性,即 ACID:
- 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成,如果有任何一个操作失败,整个事务都将被回滚到之前的状态,即所有修改都将被撤销。
- 一致性(Consistency):事务的执行不会破坏数据库的完整性和约束条件,即事务必须使数据库从一个一致性状态变为另一个一致性状态。
- 隔离性(Isolation):事务的执行过程中,对其他事务是隔离的,即互不干扰。每个事务执行的过程中,对其他事务是透明的,即每个事务都感觉不到其他事务的存在。
- 持久性(Durability):事务完成后,对数据库的修改必须永久保存在数据库中,即使系统崩溃或发生故障,也不能丢失。
通过使用事务,数据库可以保证多个用户并发访问数据库时,数据的完整性和一致性不会被破坏,从而确保了数据库的可靠性和安全性。事务的使用也可以提高数据库的性能,因为将多个操作组成一个事务可以减少对数据库的访问次数,从而减少了系统开销。
在 MySQL 中,只有 InnoDB 支持事务。
事务提交方式
事务提交是指将事务中所做的修改永久保存到数据库中,使其对其他会话可见并产生持久化效果。
事务提交方式指的是事务在完成操作后如何提交到数据库,主要有以下两种方式:
- 显式提交事务:在事务执行完成后,通过执行
COMMIT
语句来将事务提交到数据库。此时,数据库会将事务所做的所有修改永久保存到磁盘上,事务的 ACID 特性也得到了保证。 - 隐式提交事务:在某些情况下,事务会自动提交到数据库。例如,当执行一个 DDL 语句(例如
CREATE TABLE
、ALTER TABLE
)时,系统会自动提交事务;或者在某些数据库中,执行一个DML
语句(例如 INSERT、UPDATE、DELETE)后,系统也会自动提交事务。这种方式的提交虽然方便,但是可能会影响到事务的一致性和可靠性,因此应该谨慎使用。
在实际应用中,一般建议显式提交事务,以保证事务的 ACID 特性。
使用 show variables
显示 MySQL 服务器当前的系统变量值,使用 like 'autocommit'
查询当前会话的事务提交方式是否是自动提交
MariaDB [test_db]> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
使用 set
修改提交方式,将 autocommit
改为 0 关闭自动提交,为 1 开启自动提交
MariaDB [test_db]> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)
隔离级别—初识
数据库并发事务中可能出现的三种数据读取问题:
- 脏读(Dirty Read):脏读指的是一个事务读取了另一个事务还未提交的数据。如果该事务回滚,则读取到的数据就是无效的,造成数据的不一致。
- 不可重复读(Non-repeatable Read):不可重复读指的是在一个事务中,由于并发事务的修改,导致在事务中同一个查询语句返回的结果不同。在某些情况下,这可能会导致数据不一致。
- 幻读(Phantom Read):是不可重复读问题的一种特殊情况,幻读指的是在一个事务中,由于并发事务的插入或删除,导致在事务中同一个查询语句返回了不同的行数。这种情况下,与查询语句匹配的行数可能会发生变化,从而导致数据不一致。
事务隔离级别(Transaction Isolation Level)是指数据库管理系统(DBMS)如何处理并发执行的事务之间的相互影响。不同的隔离级别可以提供不同的并发控制方式,以满足不同应用场景的需求。
通常,数据库管理系统提供四种标准隔离级别,包括:
- 读未提交(Read Uncommitted):一个事务可以读取另一个事务未提交的数据,可能会导致脏读、不可重复读和幻读等问题。
- 读已提交(Read Committed):一个事务只能读取已提交的数据,可以避免脏读问题,但仍可能会出现不可重复读和幻读问题。
- 可重复读(Repeatable Read):一个事务在执行期间多次读取同一行数据时,得到的结果是一致的,可以避免不可重复读问题,但仍可能会出现幻读问题。注:MySQL 在可重复读级别下已经解决幻读问题
- 序列化(Serializable):所有事务串行执行,可以避免所有并发问题,但效率较低,一般用于对数据完整性要求非常高的场景。
不同的隔离级别之间存在的区别主要在于对并发访问数据的限制程度不同,隔离级别越高,数据一致性和可靠性越高,但同时也会影响系统的性能和并发性能。因此,在应用中需要根据具体的业务需求和场景来选择合适的隔离级别。
查看当前的隔离级别,修改隔离级别:
MariaDB [(none)]> set global transaction isolation level READ UNCOMMITTED; -- 修改隔离级别为读未提交
Query OK, 0 rows affected (0.00 sec)
MariaDB [(none)]> quit
Bye
MariaDB [(none)]> select @@tx_isolation; -- 重启终端,查看当前隔离级别
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set (0.00 sec)
启动事务
启动一个事务需要使用 START TRANSACTION
或 BEGIN
语句,它们的作用是开启一个新的事务,并将当前会话的自动提交模式设置为手动提交模式。
START TRANSACTION; -- 或者 BEGIN;
-- 在这里执行一系列 SQL 语句,包括 INSERT、UPDATE、DELETE 等
COMMIT; -- 提交并结束事务
-- 或者 ROLLBACK; -- 回滚事务
SAVEPOINT
语句用于在一个事务中创建一个保存点(Savepoint),保存点可以在事务中的任何时刻进行回滚(Rollback)操作,回滚到保存点所在的状态。保存点可以帮助开发者在一个事务中更精细地控制事务的回滚。
START TRANSACTION;
-- 在此执行一些 SQL 语句
SAVEPOINT sp1;
-- 在此执行更多 SQL 语句
ROLLBACK TO sp1;
-- 回滚到保存点 sp1 所在的状态
-- 在此执行更多 SQL 语句
COMMIT;
在这个例子中,使用 SAVEPOINT
语句创建了一个名为 sp1 的保存点,然后在事务中执行了一些 SQL 语句。如果后面发现了错误,可以使用 ROLLBACK TO
语句回滚到保存点 sp1 所在的状态,然后继续执行其他 SQL 语句。如果没有错误,则可以通过 COMMIT
语句提交事务。
如果单独使用 ROLLBACK
则会撤销当前事务的所有更改,并结束当前事务。
例如,以下是在 MySQL 中启动一个事务的示例:
当前处在读未提交的隔离级别下,利用脏读,方便观察
正常演示:
MariaDB [test_db]> create table emp (
-> id int primary key,
-> name varchar(32) not null,
-> sal decimal(10,2) not null default 0.0
-> )engine=InnoDB;
Query OK, 0 rows affected (0.06 sec)
MariaDB [test_db]> start transaction; -- 启动事务
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> savepoint s1; -- 保存点s1
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> insert into emp values (1, '张飞', 8999.5); -- 插入一条数据,可以在另一个终端看到插入成功
Query OK, 1 row affected (0.00 sec)
MariaDB [test_db]> savepoint s2; -- 保存点s2
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> insert into emp values (2, '关羽' , 9999.8); -- 插入一条数据,可以在另一个终端看到插入成功
Query OK, 1 row affected (0.00 sec)
MariaDB [test_db]> rollback; -- 回滚到最开始并结束事务,在另一个终端可以看到表已经空了
Query OK, 0 rows affected (0.01 sec)
MariaDB [test_db]> begin; -- 启动一个新事务
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> insert into emp values (1, '张飞', 8999.5); -- 插入一条数据,可以在另一个终端看到插入成功
Query OK, 1 row affected (0.00 sec)
MariaDB [test_db]> savepoint s1; -- 保存点s2
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> insert into emp values (2, '关羽', 9999.8); -- 插入一条数据,可以在另一个终端看到插入成功
Query OK, 1 row affected (0.00 sec)
MariaDB [test_db]> savepoint s2; -- 保存点s2
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> rollback to s1; -- 回滚到s1,可以看到表里面只剩张飞
Query OK, 0 rows affected (0.00 sec)
异常演示1:中途异常退出
MariaDB [test_db]> begin; -- 启动事务
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> insert into emp values (1, '张飞', 8999.5); -- 插入数据
Query OK, 1 row affected (0.00 sec)
MariaDB [test_db]> insert into emp values (2, '关羽', 9999.8); -- 插入数据
Query OK, 1 row affected (0.00 sec)
-- 此时可以在另一个终端看到两条数据已经插入成功了
MariaDB [test_db]> Aborted -- ctrl \ 退出
-- 此时可以在另一个终端看到数据清空,事务自动回滚了
异常2:commit 之后客户端崩溃,commit 是提交并结束事务,所以之后崩溃不会回滚,数据已经持久化。
单 SQL 语句与事务
在没有启动事务的情况下,单独的 SQL 语句会自动成为一个事务并执行提交操作,这被称为隐式事务(implicit transaction)。也就是说,我们以前写的 SQL 语句,其实都用到了事务。
之前我们讲过 autocommit
变量,它对于 START TRANSACTION
或 BEGIN
语句启动的事务没有作用,而对当前会话下的单查询语句有作用。
默认 autocommit
变量为 1,也就是自动提交,所以我们以前写的每一条 SQL 都不会因为后续终端崩溃而回滚
例:把 autocommit
设为 0,不启动事务
MariaDB [test_db]> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)
MariaDB [test_db]> insert into emp values (1, '张飞', 8999.5); -- 插入一条记录,可以在另一个终端看到插入成功
Query OK, 1 row affected (0.00 sec)
MariaDB [test_db]> insert into emp values (2, '关羽', 9999.8); -- 插入一条记录,可以在另一个终端看到插入成功
Query OK, 1 row affected (0.00 sec)
MariaDB [test_db]> Aborted -- 异常退出,此时再看另一个终端,发现记录清空,事务回滚了
-- 重新启动 MySQL
MariaDB [test_db]> insert into emp values (1, '张飞', 8999.5); -- 插入一条记录,可以在另一个终端看到插入成功
Query OK, 1 row affected (0.00 sec)
MariaDB [test_db]> insert into emp values (2, '关羽', 9999.8); -- 插入一条记录,可以在另一个终端看到插入成功
Query OK, 1 row affected (0.00 sec)
MariaDB [test_db]> commit; -- 手动提交
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> Aborted -- 异常退出,此时看另一个终端,发现数据还在,说明commit持久化成功
隔离级别—深化
查看隔离级别
-
SELECT @@global.tx_isolation;
获取全局级别的事务隔离级别。全局级别表示数据库系统中所有会话的默认隔离级别。
-
SELECT @@session.tx_isolation;
获取当前会话的事务隔离级别。会话级别的事务隔离级别是当前会话中所有事务的默认隔离级别。
-
SELECT @@tx_isolation;
获取当前会话的事务隔离级别,与
@@session.tx_isolation
等价。
设置隔离级别
-
设置当前会话的隔离级别:
SET SESSION TRANSACTION ISOLATION LEVEL <isolation_level>;
将
<isolation_level>
替换为所需的隔离级别,如READ COMMITTED
、REPEATABLE READ
等。这将仅对当前会话生效,不会影响其他会话。 -
设置全局级别的隔离级别:
SET GLOBAL TRANSACTION ISOLATION LEVEL <isolation_level>;
注意:设置全局级别的隔离级别需要具有适当的权限,通常只有数据库管理员才能执行此操作。全局级别的隔离级别将成为新建会话的默认隔离级别。
认识隔离级别
读未提交我们已经在上面演示过了,在一个会话中启动事务,事务即使未提交,其所执行的改动在另一个会话中也能看到。
读已提交:
只有当提交了事务,其他事务才能看到改动。
-- 终端B
MariaDB [test_db]> set session transaction isolation level read committed; -- 设置隔离级别为读已提交
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> SELECT @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| READ-COMMITTED |
+----------------+
1 row in set (0.00 sec)
-- 终端A
MariaDB [test_db]> begin; -- 启动事务
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> insert into emp values (1, '张飞', 8999.5); -- 插入数据但未提交,此时在另一个终端是看不到的
Query OK, 1 row affected (0.00 sec)
-- 终端B
MariaDB [test_db]> select * from emp;
Empty set (0.00 sec)
-- 终端A
MariaDB [test_db]> commit; -- 提交,现在在终端B可以看到已提交的数据了
Query OK, 0 rows affected (0.00 sec)
-- 终端B
MariaDB [test_db]> select * from emp;
+----+--------+---------+
| id | name | sal |
+----+--------+---------+
| 1 | 张飞 | 8999.50 |
+----+--------+---------+
1 row in set (0.00 sec)
读已提交的不可重复读问题:
-- 终端B
MariaDB [test_db]> begin; -- 启动一个事务
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> select * from emp; -- 查emp表为空
Empty set (0.00 sec)
-- 终端A
MariaDB [test_db]> begin; -- 启动事务
Query OK, 0 rows affected (0.00 sec)
MariaDB [test_db]> insert into emp values (1, '张飞', 8999.5); -- 插入
Query OK, 1 row affected (0.00 sec)
MariaDB [test_db]> commit; -- 提交事务
Query OK, 0 rows affected (0.00 sec)
-- 终端B
MariaDB [test_db]> select * from emp; -- 终端B可以读到已提交的内容
+----+--------+---------+
| id | name | sal |
+----+--------+---------+
| 1 | 张飞 | 8999.50 |
+----+--------+---------+
1 row in set (0.00 sec)
观察终端B前后的 select * from emp;
语句,会发现返回的结果是不同的,因为它受到了其他事务的影响。这显然是违背隔离性的,这一问题叫做不可重复读问题,而且两次结果的行数不同,所以这也是幻读问题。
幻读问题之所以单独拿出来考虑,是因为在普通的不可重复读问题中,可以通过使用锁或快照读等机制来确保事务的一致性视图。
而幻读问题则更强调查询范围内数据集的变化,即在同一查询条件下,新的数据行被插入或删除,导致查询结果的行数发生变化。简单地使用锁或快照读等机制无法完全解决幻读问题,因为仅锁定已存在的数据行或读取已存在的数据快照无法防止新数据行的插入。
为了解决幻读问题,数据库系统采用了额外的机制,如锁定读和间隙锁(Gap Locks)。锁定读和间隙锁的机制可以在查询范围内对数据行和间隙进行锁定,从而防止其他事务在该范围内插入或删除数据,保证查询结果的一致性。
MySQL 中,在可重复读隔离级别下通过间隙锁解决了幻读问题。
序列化
序列化是最高级别的隔离级别。在序列化隔离级别下,事务之间完全串行执行,每个事务都按顺序逐个执行,相当于将并发操作转化为串行操作。当一个事务正在访问某个数据时,其他事务无法对该数据进行任何操作,直到当前事务提交或回滚完成。因此序列化隔离级别的效率很低,很少采用。
序列化隔离级别下最基本的现象就是,当你在一个事务中想修改数据时如果有其他事务正在运行,则你的修改操作会被阻塞,直到其他事务结束。