该篇主要总结一下MySQL事务的相关知识点,包括事务的特性、并发事务的问题、事务的隔离级别等。
一、事务的基本概念
在MySQL中,事务是一个不可分割的工作单元,要么全部执行成功 ,要么全部失败,不允许出现中间状态的数据。
例如在常见的转账业务中,为了保证转账业务中的所有数据库操作不可分割,则可以使用数据库中的 事务 达到这种效果。
在转账前先开启事务,等所有数据库操作执行完毕后,才提交事务,对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,如果中途发生发生中断或错误,那么该事务期间对数据库所做的修改将会被回滚到没执行该事务之前的状态。
事务对应着一个或多个数据库操作。这些操作所执行的不同阶段把事务大致划分成了下面几个状态:
活跃的(active)
:事务对应的数据库操作正在执行过程中,则该事务处于活跃状态。部分提交的(partially committed)
:当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,则该事务处于部分提交的状态。失败的(failed)
:当事务处于活动的状态或者部分提交的状态时,可能遇到了某些错误而无法执行,或人为停止了当前事务的执行,则该事务处于失败的状态。中止的(aborted)
:如果事务执行了半截而变为失败的状态,那么就需要撤销失败事务对当前数据库造成的影响,即回滚。当回滚操作执行完毕后,也就是数据库恢复到了执行事务之前的状态,则该事务处于终止的状态。提交的(committed)
:当一个处于部分提交的状态的事务将修改过的数据都刷新到磁盘汇中之后,则该事务处于提交的状态。
二、事务的特性
在MySQL事务中,主要有四个特性:
原子性(Atomicity)
:一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,如果事务在执行过程中发生错误,会被回滚到事务开始前的状态。一致性(Consistency)
:指事务操作前和操作后,数据满足完整性约束,数据库状态保持一致性。例如在转账前后,双方的总金额保持不变。隔离性(Isolation)
:数据库允许多个并发事务同时对数据进行读写,隔离性可以防止多个事务并发执行时导致的数据不一致情况。因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。持久性(Durability)
:事务提交后,对数据的修改是永久有效的,即使数据库故障也不会丢失。
事务的这些特性在InnoDB是如何实现的?
- 持久性是通过
redo log(重做日志)
来保证的; - 原子性是通过
undo log(回滚日志
)来保证的; - 隔离性是通过
MVCC(多版本并发控制)
或锁机制
来保证的; - 一致性是通过
持久性 + 原子性 + 隔离性
来保证的;
三、并发事务带来的问题
MySQL是支持事务进行并发执行的,这种情况下MySQL可能会出现同时处理多个事务时导致的一些问题,例如:脏读、不可重复读、幻读。
脏读
如果一个事务读到了另一个未提交事务修改过的数据,就意味着发生了脏读现象。
例如:
当事务A和事务B同时处理任务,事务A先从数据库中读取数据,再执行更新操作,如果此时事务A并未提交事务,而此时事务B也从数据库红读取相同的护具,那么事务B读取到的数据就是刚才事务A更新后的数据,即使事务A还没有提交事务。
因为事务 A 是还没提交事务的,也就是它随时可能发生回滚操作,如果在上面这种情况事务 A 发生了回滚,那么事务 B 刚才得到的数据就是过期的数据,这种现象就被称为脏读。
不可重复读
在一个事务内多次读取同一个数据,如果出现前后两次读取到的数据不一样的情况,就意味着发生了不可重复读现象。
例如:
当事务A和事务B同时处理任务,事务A先从数据库中读取数据,然后执行后续的逻辑处理,在这个过程中如果事务B更新这这条数据,并提交了事务,那么当事务A再次读取该数据时,就会发现前后两次读取到的数据不一致,这种现象被称为不氪重复读。
可以看到,事务A在前后两次读取到的数据是不一致的,发生了不可重复读现象。
幻读
在一个事务内多次查询某个符合查询条件的记录数据,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了幻读现象。
例如:
当事务A和事务B同时处理任务,事务 A 先开始从数据库查询账户余额大于 100 万的记录,发现共有 5 条,然后事务 B 也按相同的搜索条件也是查询出了 5 条记录。
接下来,事务 A 插入了一条余额超过 100 万的账号,并提交了事务,此时数据库超过 100 万余额的账号个数就变为 6。
然后事务 B 再次查询账户余额大于 100 万的记录,此时查询到的记录数量有 6 条,发现和前一次读到的记录数量不一样了,就感觉发生了幻觉一样,这种现象就被称为幻读。
四、事务的隔离级别
在并发事务执行过程中可能会遇到的一些现象,这些现象可能会对事务的一致性产生不同程度的影响。
三种现象的严重性排序:
脏读 > 不可重复读 > 幻读
为了解决上述的现象,提出了四种隔离级别来避免这些现象的发生,隔离级别越高,性能效率就越低,这四种隔离级别如下:
读未提交(read uncommitted)
:指一个事务还没提交时,它做的变更就能被其他事务看到;读已提交(read committed)
:指一个事务提交之后,它做的变更才能被其他事务看到;可重复读(repeatable read)
:指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;串行化(serializable )
:会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
针对不同的隔离级别,并发事务执行过程中可以发生不同的现象:
即:
- 在「读未提交」隔离级别下,可能发生脏读、不可重复读和幻读现象;
- 在「读已提交」隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象;
- 在「可重复读」隔离级别下,可能发生幻读现象,但是不可能脏读和不可重复读现象;
- 在「串行化」隔离级别下,脏读、不可重复读和幻读现象都不可能会发生。
因此,在解决并发事务产生的现象,可以从以下入手:
- 解决幻读,可以将隔离级别升级为「读已提交」以上的隔离级别;
- 解决不可重复读,可以将隔离级别升级为「可重复读」的隔离级别;
- 解决幻读,不建议将隔离级别升级为「串行化」;
MySQL在「可重复读」隔离级别下,可以很大程度上解决幻读的问题,但并不是完全解决,因此MySQL不会使用「串行化」隔离级别来避免幻读现象的发生,因为该隔离级别对数据上锁,影响性能。
在「可重复读」隔离级别在解决幻读上,主要分为以下两种情况:
- 针对快照读(普通 select 语句),是通过 MVCC 方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,即使中途有其他事务插入了一条数据,是查询不出来这条数据的,所以就很好了避免幻读问题。
- 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
四种隔离级别的具体实现方式如下:
- 「读未提交」隔离级别下,可以读到未提交事务修改的数据,因此直接读取最新的数据;
- 「串行化」隔离级别下,通过加读写锁的方式来避免并行事务的访问;
- 「读提交」和「可重复读」隔离级别下,通过
Read View
实现,区别在于生成Read View
的时机不同。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个Read View
,而「可重复读」隔离级别是「启动事务时」生成一个Read View
,然后整个事务期间都在用这个Read View
。
假设事务A和事务B在执行的过程中,具体在各个隔离级别下事务读取数据的情况:
在不同的隔离级别下,事务A查询得到的余额值可能会不同:
- 在「读未提交」隔离级别下,事务B修改余额后,虽然事务B还未提交,但此时余额已经可以被事务A看见并查询,因此事务A中
余额V1
查询的值为200万,余额V2
、V3
也是200万; - 在「读提交」隔离级别下,事务B修改余额后,因为还未提交事务,因此事务A中
余额V1
的值仍然为100万,等事务B提交完后,最新的余额数据才能被事务A看见,因此余额V2
、余额V3
都是200万; - 在「可重复读」隔离级别下,事务A只能看见事务启动时的数据,因此
余额V1
、余额V2
的值都是100万,当事务A提交事务后,就可以查询到最新的余额数据,所以余额V3
的值为200万。 - 在「串行化」隔离级别下,事务B在执行将余额100万修改为200万时,由于此前事务A执行了读操作,这样会发生读写冲突,于是会被锁住,直到事务A提交后,事务B才可以继续执行,所以从事务A的角度看,
余额V1
、V2
的值是100万,余额V3
的值是200万。
以上就是MySQL事务的一些知识总结,其中也参考了许多的资料进行总结。