事务是指满足ACID特性的一组操作。
1.原子性(Atomicity)
事务被视为不可分割的最小单元,事务包含的所有操作要么全部提交成功,要么全部失败回滚。
回滚可以用回滚日志(Undo Log)来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
2.一致性(Consistency)
数据库在事务执行前后保持一致性状态,在一致性状态下,所有事务对同一个数据的读取结果都是相同的。
3.隔离性(Isolation)
事务所做的修改在最终提交之前,对其它事务是不可见的。
4.持久性(Durability)
事务一旦被提交,其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
系统发生崩溃可以用重做日志(Redo Log)进行恢复,从而实现持久性。与回滚日志记录数据的逻辑修改不同,重做日志记录的是数据页的物理修改!
在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题
一个事务的更新操作被另外一个事务的更新操作替换,如下图所示:
指在一个事务内多次读取同一数据集合,在这一事务还未结束前,另一事务也访问了该同一数据集合并做了修改,导致同一事务中两次读取的数据可能不一致,如下图:
幻影读本质上也属于不可重复读的情况,其强调的是事务多次读取某个范围的数据,而另一事务在该范围内插入/删除了的数据,使得同一事务两次读取结果不同,如下图:
注:不可重复读和幻影读的区别在于,不可重复读强调的是数据被修改了,而幻影读强调的是范围内数据新增或减少了。
产生并发不一致性问题的主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。
- 封锁粒度:行级锁、表级锁
- 封锁类型:读/写锁(S/X)、意向锁(IS/IX)
- 封锁协议:三级封锁协议(描述的是什么时候加什么样的锁以实现什么样的隔离级别)
指事务中的修改,即使没有提交,对其他事务也是可见的。
事务在最终提交之前,对其他事务是不可见的,也就是说一个事务只能读取到已经提交的事务所做的修改。
同一事务中多次读取同一数据的结果是一样的。
所有事务串行执行,事务间互不干扰,因此不会出现并发一致性问题。
多版本并发控制(MVCC)是MySQL的InnoDB存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读两种隔离级别。未提交读隔离级别总是读取最新的数据行,要求很低,无需使用MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用MVCC无法实现。
加锁能解决多个事务同时执行时出现的并发一致性问题,又由于实际场景中读操作往往多于写操作,因此我们引入了读写锁来避免不必要的加锁操作(读/读没有互斥关系)。但加锁方式中,读和写操作仍然是互斥的,MVCC利用多版本的思想,使得读和写操作没有了互斥关系:写操作更新最新的版本快照,而读操作去读旧版本的快照。
在MVCC中事务的修改操作(如delete、insert、update)会为数据行新增一个版本快照。
MVCC 的多版本指的是多个版本的快照,快照存储在 Undo 日志中,该日志通过回滚指针 ROLL_PTR 把一个数据行的所有快照连接起来。
例如在 MySQL 创建一个表 t,包含主键 id 和一个字段 x。我们先插入一个数据行,然后对该数据行执行两次更新操作。
INSERT INTO t(id, x) VALUES(1, "a");
UPDATE t SET x="b" WHERE id=1;
UPDATE t SET x="c" WHERE id=1;
因为没有使用 START TRANSACTION 将上面的操作当成一个事务来执行,根据 MySQL 的 AUTOCOMMIT 机制,每个操作都会被当成一个事务来执行,所以上面的操作总共涉及到三个事务。快照中除了记录事务版本号 TRX_ID 和操作之外,还记录了一个 bit 的 DEL 字段,用于标记是否被删除。
INSERT、UPDATE、DELETE 操作会创建一个日志,并将事务版本号 TRX_ID 写入。DELETE 可以看成是一个特殊的 UPDATE,还会额外将 DEL 字段设置为 1。
MVCC 维护了一个 ReadView 结构,主要包含了当前系统未提交的事务列表 TRX_IDs {TRX_ID_1, TRX_ID_2, …},还有该列表的最小值 TRX_ID_MIN 和 TRX_ID_MAX。
在进行 SELECT 操作时,根据数据行快照的 TRX_ID 与 TRX_ID_MIN 和 TRX_ID_MAX 之间的关系,从而判断数据行快照是否可以使用:
TRX_ID < TRX_ID_MIN,表示该数据行快照时在当前所有未提交事务之前进行更改的,因此可以使用。
TRX_ID > TRX_ID_MAX,表示该数据行快照是在事务启动之后被更改的,因此不可使用。
TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根据隔离级别再进行判断:
(1)提交读:如果 TRX_ID 在 TRX_IDs 列表中,表示该数据行快照对应的事务还未提交,则该快照不可使用。否则表示已经提交,可以使用。
(2)可重复读:不管 TRX_ID 在不在 TRX_IDs 列表中都不可以使用。因为如果可以使用的话,那么其它事务也可以读到这个数据行快照并进行修改,那么当前事务再去读这个数据行得到的值就会发生改变,也就是出现了不可重复读问题。
在数据行快照不可使用的情况下,需要沿着 Undo Log 的回滚指针 ROLL_PTR 找到下一个快照,再进行上面的判断。