MySQL(一)事务概述

事务就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。本文涵盖了事务特性并发事务问题事务隔离级别三部分的简要总结。

事务特性

原子性

一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。

对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。

一致性

数据库总是从一个一致性的状态转换到另外一个一致性的状态。

银行账户中,A账户的余额为1000元美元,B账户的余额为500美元。A账户向B账户转账200美元的伪代码为

1
2
3
4
begin;
update user_amount set balance = balance - 200 where account = A;
update user_amount set balance = balance + 200 where account = B;
commit

在前面的例子中,一致性确保了,即使在执行第二、三条语句之间时系统崩溃,账户中也不会损失200美元,因为事务最终没有提交,所以事务中所做的修改也不会保存到数据库中。

隔离性

通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。

在前面的例子中,当执行完第三条语句、第四条语句还未开始时,此时有另外一个账户汇总程序开始运行,则其看到的账户A的余额并没有被减去200美元。关于隔离级别的内容,见下方。

持久性

一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。

持久性是个有点模糊的概念,因为实际上持久性也分很多不同的级别。有些持久性策略能够提供非常强的安全保障,而有些则未必。而且不可能有能做到100%的持久性保证的策略(如果数据库本身就能做到真正的持久性,那么备份又怎么能增加持久性呢?)。

并发事务带来的问题

第一类丢失更新

第一类丢失更新,又称为回滚丢失。 事务撤销时,把已经提交的B事务的更新数据覆盖了

时间 事务A 事务B
T1 begin
T2 查询用户年龄为10岁
T3 begin
T4 查询用户年龄为10岁
T5 修改用户A的年龄为20岁
T6 修改用户A的年龄为5岁
T7 commit
T8 rollback

上面的执行流程中,事务B在T6时间内将用户A的年龄修改为5岁,并提交修改。
随即事务A在T8时间点对自己事务内的修改进行了回滚,即这条数据被恢复成了10岁。这不但导致事务A的修改回滚,也导致事务B的修改丢失

第二类丢失更新

第二类丢失更新,又称为覆盖丢失/两次更新问题。A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失

时间 事务A 事务B
T1 begin
T2 begin
T3 查询账户A余额为1000元 查询账户A余额为1000元
T4 取出100元,把余额改为900元
T5 存入200元,把余额改为1200元 commit
T6 commit

上面的执行流程中,事务B将账户余额扣减100元并提交,事务A随后增加200元并提交。此时账户A的余额变为1200元,即事务B的修改被事务A的修改覆盖

脏读

脏读又称无效数据的读出
是指在数据库访问中,事务T1将某一值修改,然后事务T2读取该值,此后T1因为某种原因撤销对该值的修改,这就导致了T2所读取到的数据是无效的,这种现象称之为脏读。

事务A读取数据前,事务B修改了数据但是没提交事务,此时事务A读取到了B修改未提交的数据。之后事务B因某些原因再次修改数据或回滚数据,导致事务A获取到的是被事务B丢弃掉的数据,称之为脏读。

时间 事务A 事务B
T1 begin
T2 begin
T3 查询账户A余额为1000元
T4 取出100元,把余额为900元
T5 查询账户A余额900元
T6 rollback
T7 取出100元,把余额改为800元
T6 commit

不可重复读

在一个事务里面执行两次以上读操作,得到的结果不一致。

事务A在两次读操作的时间区间内,事务B修改了该数据并提交事务,导致A两次读取结果不一致。

时间 事务A 事务B
T1 begin
T2 begin
T3 查询账户A的余额为900元
T4 查询账户A的余额为900元
T5 取出100元,修改余额为800元
T6 查询账户A的余额为800元
T7 commit
T8 commit

幻读

在一个事务里面的操作中,发现了未被操作的数据

换句话说,当某个事务在读取某个范围的数据时,其他事务在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻象行,对产生幻象行的这种现象,称之为幻读。
事务A按条件修改了一批数据,提交后发现有几条数据没有被修改。原因是在A事务处理的过程中,B向条件区间内插入了几条数据。

事务隔离级别

隔离性是比较复杂的,在SQL标准中定义了四种隔离级别,按照隔离级别的由低到高分别是读未提交读已提交可重复读序列化。每一种隔离级别都规定了一个事务中所做的修改,哪些在事务内可见,哪些在事务间可见。
通常,较低的隔离级别可以执行较高的并发,系统开销也较低,性能较高。不同的存储引擎实现的隔离级别不尽相同。

读未提交

英文:RU,Read Uncommitted
事务中的修改,即使没有提交,对其他事务也都是可见的。

事务可以读取其他事务为提交的数据,这也称之为脏读。在这个隔离级别下会导致很多问题,从性能上来说,该级别也不必其他级别好太多。所以,除非真的非常有必要,否则不要使用该隔离级别。
SQL标准中规定:在该事务隔离级别下,不允许出现第一类丢失更新问题,但允许出现脏读不可重复读幻读第二类丢失更新的问题。

读已提交

英文:RC,Read Committed
一个事务开始时,只能“看见”自己或已经提交的事务所做的修改。

换句话说,一个事务从开始直到提交的这段时间,所做的修改对其他事务都是不可见的。又因为在这个隔离级别下,两次相同的查询有可能得到不同的结果,所以该隔离级别又可以被称为不可重复读
SQL标准中规定:在该事务隔离级别下,不允许出现第一类丢失更新脏读问题,但允许出现不可重复读幻读第二类丢失更新的问题

可重复读

英文:RR,Repeattable Read
同一个事务中,多次读取相同的数据,返回的结果是一致的。

可重复读是MySQL的默认隔离级别。理论上该隔离级别还是无法解决幻读的问题,但MySQL的InnoDB存储引擎在该隔离级别下,通过多MVCC和Next-key Lock解决了幻读问题。
SQL标准中规定:在该事务隔离级别下,不允许出现第一类丢失更新脏读不可重复读问题,但允许出现幻读第二类丢失更新的问题。

可串行化

英文:Serializable
强制所有事务串行执行,解决了所有因并发事务产生的问题。

可串行化是最高的隔离级别,简单来说,在该级别下,会给读取的每一行数据都加锁,所以可能导致大量的超时和锁争用问题。所以一般很少使用该隔离级别,除非需要保证数据的绝对一致性,否则不建议使用。

隔离级别vs并发问题

注:以 yes 标注存在的问题,以 no 标注解决的问题

# 脏读 不可重复读 幻读 第一类丢失更新 第二类丢失更新
读未提交 yes yes yes no yes
读已提交 no yes yes no yes
可重复读 no no yes no no
序列化 no no no no no