MySQL(一)事务概述
事务就是一组原子性的SQL查询,或者说一个独立的工作单元。如果数据库引擎能够成功地对数据库应用该组查询的全部语句,那么就执行该组查询。如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。本文涵盖了事务特性、并发事务问题和事务隔离级别三部分的简要总结。
事务特性
原子性
一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。
对于一个事务来说,不可能只执行其中的一部分操作,这就是事务的原子性。
一致性
数据库总是从一个一致性的状态转换到另外一个一致性的状态。
银行账户中,A账户的余额为1000元美元,B账户的余额为500美元。A账户向B账户转账200美元的伪代码为
1 | begin; |
在前面的例子中,一致性确保了,即使在执行第二、三条语句之间时系统崩溃,账户中也不会损失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 |