今天这篇文章主要介绍一下MySQL的事务。事务指的是一组要进行的操作。
事务特性
事务的特性是ACID,分别指的是原子性、一致性、隔离性、持久性。
| 特性 | 含义 |
|---|---|
| 原子性 | 整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。 |
| 一致性 | 一个事务可以封装状态改变(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。也就是说:如果事务是并发多个,系统也必须如同串行事务一样操作。 |
| 隔离性 | 隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。 |
| 持久性 | 在事务完成以后,该事务对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。 |
ACID实现方式
主要有两种方式可以实现ACID:Write ahead logging(WAL)以及Shadow paging。
MySQL使用的是Write ahead logging日志,核心思想是对数据文件的修改,必须只能发生在这些修改已经记录了日志之后。
事务隔离级别
事务存在四种隔离级别,按照隔离程度从低到高分别是:未提交读 > 已提交读 > 可重复读 > 可串行化。不同隔离级别存在不同的问题。
未提交读
未提交读指的是事务可以读取到其他事务还没有提交的内容。
未提交读的问题是“脏读”,即两个事务可能同时更新相同的数据,或一个事务在修改了一条数据后,另一个事务对该行数据进行了读取,第一个事务由于某些原因,回滚了事务,导致第二个事务读取的数据是无效的,产生“脏读”现象。
已提交读
已提交读指的是事务的只有在提交后才可以被其他事务所读取到。
已提交读的问题是不可重复读:当一个事务在开始时select了一些数据,随后另一个事务对数据进行了更改,第一个事务再次进行读取时,数据被改变,而无法读到相同的数据,产生“不可重复读”问题。
可重复读
可重复读的问题是幻读:幻读指的是,当一个事务在查询了数据后,另一个事务在数据中插入或删除了部分数据,导致第一个事务再次查询时发现数据数量变化,产生”幻读“现象。
可串行化
可串行化是隔离级别最高的,其保证事务之间是串行执行的,这样就不会有异常,但并发程度较低,实践中很少使用。
基本的概念介绍完了,下面从实现层面介绍一下事务隔离级别。
隔离级别实现
对于未提交读而言,实现上就是不对事务的操作进行控制,按照理想情况进行,假设系统中不会同时存在两个事务,这样对事务的读写就立即完成,不考虑对其他事务的影响,因此数据的不安全性最高。
对于已提交读而言,已提交读是通过加锁来实现的,当事务要读数据时,会添加读锁,如果存在写锁,则读锁被阻塞。而当事务要写数据时,会添加写锁,如果已经存在写锁或读锁,则写锁会被阻塞。
需要注意的是,对数据进行加锁是有可能产生死锁的。MySQL中InnoDB可以配置死锁检测,当发生死锁时,事务会超时返回。
可串行化的执行,则控制事务是单线程执行即可。
可重复读
可重复读在实现上是最复杂的。首先对于查询操作,要求事务读取的数据,与事务开始时的系统中的数据是完全一致的。
而对于更新操作,则要求事务更新的数据,是有效的,即必须根据事务执行到此时的数据来进行更新,否则会覆盖其他事务执行的结果,产生数据不一致问题。那么,要如何实现呢?
MVVC多版本并发控制
在MySQL中,每个事务开启时,会向系统申请一个唯一且不断增长的事务号,后续所有对数据的操作,都会带上这个事务号。MySQL中每个数据都会保留多个版本,为最后一个对该数据进行操作的事务的事务号所标记。
因此,当事务进行select操作时,如果数据的事务号小于当前事务的事务号时,则可以查询到,否则对当前事务是不可见的。这称为快照读。
而对于更新操作呢?我们知道,更新操作是不可以无视其他事务的更改的,因此更新需要先查询到数据库中最新的数据,再进行更新,这称为当前读。同理,对于删除操作也是相同的。正是由于可重复读隔离级别下存在的当前读,导致幻读问题的存在。
幻读更严重的问题——数据异常
假设不同颜色是不同的事务,执行顺序如图所示,其中data字段不为主键。这样会存在什么问题呢,从MySQL事务执行的角度而言,是正确的,但是从binlog的角度而言,却是错误的。
为什么呢?因为蓝色事务的提交是在绿色事务之后的!这会导致,binlog的最后一行是更新data字段为5的数据,这导致binlog无法用于数据恢复!因为数据是错误的。
解决幻读问题——间隙锁和next-key锁
对于上面的数据异常问题,根本原因在于,插入数据导致了数据错误。那么为什么不添加行锁呢?最主要的原因在于,当蓝色事务开启时,绿色事务插入的数据还不存在,无法加锁。
幸运的是,MySQL中通过添加GAP间隙锁解决了这个问题,间隙锁与写锁是不同的,间隙锁是对数据行之间的间隙进行加锁的,而写锁是对数据本身进行加锁的。间隙锁是一个开区间,而间隙锁和行锁的结合,称为next-key锁,是一个前开后闭区间。
间隙锁与写锁是没有影响的,间隙锁可以防止插入数据。因此,间隙锁能防止幻读问题的发生,尽管可能牺牲部分的并发度,但为了数据安全,也是一种可行的解决方案。