事务处理几乎在每一个信息系统中都会涉及,它存在的意义是为了保证系统中所有数据都是符合期望的,且相互关联的数据之间不会产生矛盾,即数据状态的一致性 (Consistency)
需要达成数据状态一致性的目标,需要从三个方面来保障:
原子性 (Atomic):对于一个事务中的多个操作来说,要么都成功,要么回到都没有执行的状态
持久性 (Durability):一个成功执行的事务应该对数据的修改应该可以持久化,不会丢失数据
隔离性 (Isolation):不同事务之间正在读写的数据相互独立,不会彼此影响
以上四种属性就是事务的 ACID 特性。A D I 是手段,C 是目的,前者是因,后者是果。在 MySQL 中,把需要保证原子性、持久性、隔离性、一致性的一个或多个数据库操作称为事务
MySQL 通过 undo log (回滚日志) 来保证事务原子性,关于 undo log 的详细介绍可见 undo log
MySQL 通过 redo log (重做日志) 来保证事务持久性,关于 redo log 的详细介绍可见 redo log
MySQL 通过 MVCC 和锁来保证事务隔离性,关于 MVCC 的详细介绍可见 MVCC、关于锁的详细介绍可见 锁
在 Redis 事务中,一个事务中的所有命令在事务提交前都不会执行,只会添加到事务队列中,当事务提交后再一起执行
而 MySQL 事务中,一个事务中的所有操作在事务提交前其实已经执行,但仅仅只是对内存中缓冲页修改,并未刷新回磁盘,事务提价后也不会立刻刷回磁盘,脏页刷盘时机可见 脏页刷新回磁盘
PS:一个事务提交后不立刻刷盘,不怕数据丢失吗?这个由 redo log 保障持久性!!
根据事务中操作所执行的不同阶段把事务大致划分成了下面几个状态:
活动的:事务对应的数据库操作正在执行的过程中,称之为活动的状态
部分提交的:当事务最后一个操作执行完,但未刷新回磁盘之前,称之为部分提交的状态
提交的:当事务提交后,对数据库的修改刷新回磁盘后,称之为提交的状态
失败的:当事务处于活动的或者部分提交的状态时,遇到了某些错误或者人为停止了事务的执行,称之为失败的状态
中止的:当事务处于失败的状态,然后执行了回滚操作恢复到了执行事务之前的状态后,称之为中止的状态
在 Java 并发中,如果存在多个线程访问共享资源,但不对其进行同步控制,那么一定会出现问题;MySQL 的事务也是如此,一个事务包含一个或多个数据库操作,而且事务中都是对数据库的操作,相当于全都是共享资源,所有事务访问的是同一个数据库
下面模拟两个事务在没有任何约束的条件下执行的情况:假设事务 T1 和 T2 分别对 A - 5,对 B + 5,A 和 B 的初始值分别为 11 和 2,正确的情况下 A 和 B 最终值应该为 1 和 12
可以看到如果不对事务的执行顺序加以控制的话,结果就会出问题,实际上是没有保证事务的隔离性。如果让多个事务都串行执行,即同一时刻只有一个事务在执行,类似于单线程,这样会大大降低执行的效率
可以像 Java 并发中一样对共享资源加锁,当多个事务都需要访问同一个共享变量时,使用锁保证同一时刻只有一个事务可以访问,只有当事务提交后其它事务才可以继续访问,这样就实现了可串行化执行
注意:如果两个事务同时读同一个共享资源不会引发并发问题,只有存在写操作时才会出现问题,即:读写、写读、写写。有点类似于 Java 中的读写锁:共享读、独占写
下面来分析在没有任何约束的情况下,并发事务会出现哪些问题!!!
如果一个事务修改了另一个未提交事务修改过的数据,就意味着发生了脏写现象
假设一致性需求为 x 和 y 始终要保持一致,如果按照下面的执行顺序,最终数据库的数据:x = 2,y = 1
假设 x 和 y 的初始值都为 0,如果按照下面的执行顺序,最终数据库的数据:x = 0,y = 2
如果一个事务读到了另一个未提交事务修改过的数据,就意味着发生了脏读现象
假设一致性需求为 x 和 y 始终要保持一致,如果按照下面的两种执行顺序,将会出现一致性问题
上图的左边只是读到了一个不一致的状态,虽然数据库最终还是一致性的 (广义上的脏读);上图的右边直接读到了一个根本不存在的值 (严格意义上的脏读)
如果一个事务修改了另一个未提交事务读取的数据,就意味着发生了不可重复读
假设一致性需求为 x 和 y 始终要保持一致,如果按照下面的执行顺序,最终读取到的结果:x = 0,y = 1 (广义上的不可重复读)
假设在一个事务中两次读 x,如果按照下面的执行顺序,将出现两次读取结果不一致的情况 (严格意义上的不可重复读)
如果一个事务先根据某些搜索条件查询出一些记录,在该事务未提交时,另一个事务写入了一些符合那些搜索条件的记录 (这里的写入可以是 insert、update、delete),就意味着发生了幻读现象
注意:幻读针对于一个结果集,而不可重复读针对于一个记录 (值)
上部分介绍了并发事务中可能会出现的四种不一致问题,这四种问题的严重程度:脏写 > 脏读 > 不可重复读 > 幻读
由于脏写会直接影响了数据库的不一致性,也就是影响了最终一致性;而后面三种只影响了读取过程中的不一致性,并未导致最终不一致。所以 SQL 标准中规定的四种隔离级别都不允许脏写的发生
下面给出四种不同的隔离级别,以及并发事务执行过程中可以发生不同的现象
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED (读未提交) | 可能 | 可能 | 可能 |
READ COMMITTED (读已提交) | 不可能 | 可能 | 可能 |
REPEATABLE READ (可重复读) | 不可能 | 不可能 | 可能 |
SERIALIZABLE (串行化) | 不可能 | 不可能 | 不可能 |
隔离级别从上到下依次递增,但性能依次递减:
READ UNCOMMITTED (读未提交):可以读未提交事务修改的数据
READ COMMITTED (读已提交):只能读已提交事务修改的数据,可以避免脏读
REPEATABLE READ (可重复读):一个事务执行过程中读到的数据都是一致的,可以避免脏读和不可重复读。MySQL InnoDB 存储引擎默认隔离级别
SERIALIZABLE (串行化):对记录上锁,如果两个事务都需要访问 (读写) 一个记录时,必须等前一个事务提交后才能访问