文章目录
- 事务处理语言 TCL
- 事务简介
- 事务控制
- 并发事务的隔离挑战
- 脏读
- 不可重复读
- 幻读
- 事务的隔离级别
- 未提交读(READ-UNCOMMITED)
- 提交读(READ COMMITED)
- 可重复读(REPEATABLE READ)
- 可串行化(SERIALIZABLE)
事务处理语言 TCL
在进行数据操作的时候,往往会有很多不确定事件发生,这些事情通常都具有破坏性,导致数据不一致或者缺失等等。
有人在银行 ATM 机上取 5000 现金,点钱过程中突然断电了,只取出一部分,而卡中余额提示已经扣掉了 5000,怎么办?这种时候,我们就需要一种机制来保证数据结果的一致性。在数据库中,这个机制就叫做事务。
事务简介
事务(Transaction)是数据库操作的最小工作单元,是作为单个逻辑执行的一系列操作(比如上述中的输入取款金额,得到现金,取回磁卡等操作)的集合;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行。事务是不可再分割的。
事务具有四大特征,即常说的 ACID:
- 原子性(Atomicity):事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做。
- 一致性(Consistency):事务执行的前后数据都处于合法状态,不会违背任何的数据完整性和逻辑的正确性,这就是“一致”的意思。以转账为例,转出账户的钱减少,但转入账户的钱没有增加,就不符合一致性。再以转账为例,无论多少个账户,多少个并行事务,其总数必然是一致的。原子性有助于保证数据的一致性,但不能完全保证。
- 隔离性(Isolation):一个事务的执行不能被其它事务所干扰,即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。换句话说,事务之间感知不到彼此的存在。
- 持续性(Durability):又叫永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。
事务控制
事务的控制是通过一系列控制语句来完成的,代表着事务的各个阶段。常用的控制语句如下表所示:
语句 | 说明 |
---|---|
BEGIN(START TRANSACTION) | 开始一个新的事务 |
SAVEPOINT | 设置事务的保存点 |
COMMIT | 提交事务 |
ROLLBACK | 回滚当前事务到初始状态,撤销提交前的操作 |
ROLLBACK TO SAVEPOINT name | 回滚当前事务到指定保存点 name,并撤销保存点后的操作 |
SET AUTOCOMMIT | 设置当前连接是否自动提交事务,1 表示启用自动提交,0 表示禁用自动提交 |
RELEASE SAVEPOINT | 释放保存点 |
SET TRANSACTION | 设置事务的隔离级别 |
查看当前事务是否设置了自动提交,其 SQL 语句如下:
select @@autocommit;
输出结果:
MariaDB [world]> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 1 |
+--------------+
1 row in set (0.000 sec)
该值为 1,即 MySQL 默认启用事务的自动提交模式,不用再输入提交命令。
为了突出事务的特点,笔者把事务提交方式设置为禁止自动提交。其 SQL 语句如下:
set autocommit = 0;
输出结果:
MariaDB [world]> set autocommit = 0;
Query OK, 0 rows affected (0.000 sec)
查看修改结果:
select @@autocommit;
输出结果:
MariaDB [world]> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 0 |
+--------------+
1 row in set (0.000 sec)
利用 DELETE 子句,把 city 表中 ID 为 1 的记录给删除掉,其 SQL 语句如下:
delete from city where id = 1;
输出结果:
MariaDB [world]> delete from city where id = 1;
Query OK, 1 row affected (0.000 sec)
接着查询 id 为 1 的记录此时是否存在,使用 SQL 语句如下:
select * from city where id = 1;
输出结果:
MariaDB [world]> select * from city where id = 1;
Empty set (0.000 sec)
从结果看,数据已经被删除,但此时事务并没有提交。所以当前对数据的操作还没有完全写入到数据库中,使用 ROLLBACK 来回滚事务,返回到该操作之前,其 SQL 语句如下:
rollback;
输出结果:
MariaDB [world]> rollback;
Query OK, 0 rows affected (0.001 sec)
再次查看 id 为 1 的记录是否存在,结果如下:
select * from city where id = 1;
输出结果:
MariaDB [world]> select * from city where id = 1;
+----+-------+-------------+----------+------------+
| ID | Name | CountryCode | District | Population |
+----+-------+-------------+----------+------------+
| 1 | Kabul | AFG | Kabol | 1780000 |
+----+-------+-------------+----------+------------+
1 row in set (0.000 sec)
通过回滚操作,数据又回来了。只要在正式提交之前,都可以进行数据回滚。提交事务使用的控制语句是 COMMIT ,其 SQL 语句如下:
commit;
输出结果:
MariaDB [world]> commit;
Query OK, 0 rows affected (0.000 sec)
确保数据的修改无误后,使用 COMMIT 来提交数据,我们也把这种方式称为显示提交。 下面,演示一个比较简单的例子,来说明一下事务保存点的设置以及回滚等操作。
- 开始事务
- 然后在 city 表中插入一条记录
- 接着设置一个保存点
- 再插入一条记录
- 查看表内容变化情况
- 然后回滚到保存点
- 再次查看表内容
- 最后回滚到事务开始的地方
- 查看表内容,此时表内容和事务开始前是一样的,没有任何新的记录增加。
上面事务步骤翻译成 SQL 语句如下:
begin;
输出结果:
insert into city values(null,'test1','AFG','test1',101);
输出结果:
savepoint s1;
输出结果:
此处设置了保存点,取名为 s1。
insert into city values(null,'test2','AFG','test2',10001);
输出结果:
select * from city where countrycode = 'AFG';
输出结果:
此时可以看到,新插入的两条记录都显示出来了。
rollback to savepoint s1;
输出结果:
回滚到保存点 s1 位置处。
select * from city where countrycode = 'AFG';
输出结果:
可以看到设置保存点 s1 之后的操作(插入的第二条记录)已经实现回滚,数据没有了。
rollback;
输出结果:
此时回滚到了事务开始的地方,即 begin 位置处。
select * from city where countrycode = 'AFG';
输出结果:
再次查看结果,可以发现 city 表的内容和事务开始之前一模一样,没有任何变化。
并发事务的隔离挑战
事务并发是指多个事务同时对同一个数据进行操作。并发事务未做到隔离性,会带来以下问题:
脏读
一个事务读取到另一事务未提交的更新数据。当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中。这时,另外一个事务也访问这个数据,然后读到了修改后的数据,这个数据就是脏数据,依据脏数据所做的操作也是不正确的。例如 T1 开启了一个长事务,在较早的时间删除了一条记录,此时事务 T2 正好要统计表中记录总数,会少统计一条,随后 T1 事务发现删错了,进行了回滚操作,再随后 T2 事务再次统计,发现前后数据不一致。这显然违背了隔离性原则。
简单来说,脏读是指读到了未持久化的数据。
不可重复读
在同一事务中,多次读取同一数据返回的结果有所不同。换句话说,后续读取读到的是另一事务已提交的更新数据。例如 T1 开启了一个较长的事务,在较早的时间读取了一个数据,在中间另一个事务 T2 更改了数据并提交,随后 T1 再次读取但获得了新版本的数据,这时在同一个事务中多次读取同一数据却得到不同结果,这种现象就是不可重复读。显然,这也不符合隔离性原则。
幻读
在同一个事务中,以同样的条件进行范围查询,两次获得的记录数不一样。事务 T1 先执行了一次查询,然后事务 T2 新插入一行记录,接着 T1 使用相同的查询再次对表进行检索时,会发现这条出来的记录。这突然出现的记录就如一个“幻像”。 与“不可重复读”不同的是,幻读专指新插入的行。
那么如何来预防这些问题呢?
事务的隔离级别
为了防止上述问题,我们要对事务进行隔离。事务隔离有四个级别:
未提交读(READ-UNCOMMITED)
该级别的隔离性最弱,但并发性最好;事务中的修改,即使没有提交,对其他事务也都是可见的。也就是说事务可以读取未提交的数据,即产生脏读现象。
提交读(READ COMMITED)
一个事务提交后,它的变更才能被其他事务看到。大多数据库系统的默认级别,但 MySQL 不是。该级别的隔离可杜绝脏读,但仍会发生不可重复读。
可重复读(REPEATABLE READ)
MySQL 中事务的默认隔离级别。与“不可重复读”相反,“可重复读”是指在同一事务中多次读取同一数据时得到的都是事务开始那个时间点的数据版本,也可以理解为事务开始时对数据库拍了一个快照,后续操作都是在快照基础上进行的,自然不会读到别的事务的提交结果。
该级别直接针对不可重复读,但不能解决幻读。
可串行化(SERIALIZABLE)
当两个事务间存在读写冲突时,数据库通过加锁强制事务串行执行,解决了前面说的所有问题(脏读、不可重复读、幻读)。是最高的隔离级别。
名称 | 含义 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
未提交读 | 一个事务提交前,它的变更就已经能被其他事务看到 | √ | √ | √ |
读提交 | 一个事务提交后,它的变更才能被其他事务看到 | × | √ | √ |
可重复读 | 未提交的事务的变更不能被其他事务看到,同时一次事务过程中多次读取同样记录的结果是一致的。 | × | × | √ |
可串行化 | 当两个事务间存在读写冲突时,数据库通过加锁强制事务串行执行 | × | × | × |