ACID 简介
- A (Atomicity) 原子性:一个事务是一个不可分割的最小单位。只有事务中所有的数据库操作都执行成功,整个事务才算成功。事务中任何一个 SQL 语句执行失败,已经执行成功的 SQL 语句也必须撤销,数据库状态应该回退到执行事务前的状态。
- C (Consistency) 一致性: 事务将数据库从一种正确状态转变为另一种正确的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
- I (Isolation) 隔离性: 一个事务所做的修改在最终提交之前,对其它事务是不可见的。这个和数据库的隔离级别有关。
- D (Durability) 持久性: 事务一旦提交,其所做的修改会被永久保存到数据库中。
ACID 里的 AID 都是数据库的特征,也就是依赖数据库的具体实现。而唯独这个 C,实际上它依赖于应用层,也就是依赖于开发者。这里的一致性是指系统从一个正确的状态, 迁移到另一个正确的状态。什么叫正确的状态呢?就是当前的状态满足预定的约束就叫做正确的状态。而事务具备 ACID 里 C 的特性是说通过事务 AID 来保证我们的一致性。
关于如何理解数据库事务中的一致性概念的讨论看这里。
多个事务并发执行遇到的问题
关于不可重复读和幻读的区别网上相关的讨论太多了,这里以维基百科为准。
更新丢失 (Lost Update): 最后提交的事务的更新覆盖了其它事务之前的更新。
脏读 (Dirty Read): 一个事务读到了另一个未提交事务修改过的数据。
不可重复读 (Non-Repeatable Read): 在一次事务中,两次读取同一行数据,得到了不同的结果。
幻读 (Phantom Read): 在一次事务中,两次按照相同的条件查询,返回不同的结果集。
事务的隔离级别
数据库事务的隔离级别有 4 中,由低到高分别为:
读未提交(READ-UNCOMMITED): 一个事务还未提交,它做的变更就能被别的事务看到。可能发生脏读、不可重复读和幻读问题。
读已提交(READ-COMMITTED): 一个事务提交以后,它做的变更才能被别的事务看到。可能发生不可重复性读和幻读问题,不会发生脏读问题。
可重复读(REPEATABLE-READ): 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据时一致的。可能发生幻读问题,不会发生脏读和不可重复读问题。
串行化(SERIALIZABLE): 对于同一行记录,“写” 会加 “写锁”,“读” 会加 “读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。各种问题都不会发生。
MVCC 是什么
Multi-Version Concurrency Control,多版本并发控制,一般在数据库管理系统中,解决数据库的多事务并发访问问题。
数据库通常使用锁来实现隔离性,最原生的锁,锁住一个资源后会禁止其它任何线程访问同一个资源。但是很多应用都是读多写少,很多数据的读取次数远大于修改的次数,而读取数据间互相排斥显得不是很必要。所以就使用了一种读写锁的方法,读锁和读锁之间不互斥,读锁和写锁、写锁和写锁之间都互斥,这样很大提升了系统的并发能力。
后来人们发现并发度还是不够,又提出了让读写之间也不冲突的方法,读取数据时通过一种类似快照的方式将数据保存下来,这样读锁和写锁之间就不冲突了,不同的事务会看到自己特定版本的数据,这就是 MVCC。但是 MVCC 并没有一个统一的实现标准,所以不同的数据库,不同的存储引擎的实现都不相同。
InnoDB 中 MVCC 的实现原理
InnoDB 实现 MVCC 是通过在每行记录后面保存两个隐藏的列来实现的。一个保存了行的事务 ID(DB_TRX_ID),一个保存了行的回滚指针(DB_ROLL_PTR)。
- 每开始一个新的事务,都会自动递增产生一个新的
事务id,事务开始时会把事务id放到受到当前事务影响的行记录的事务id中。 - 每次对行记录进行改动,都会记录一条
undo日志,每个undo日志也都有一个roll_pointer,可以将这些undo日志都连起来,形成一个链表,称为版本链。版本链的头结点就是当前记录的最新值,另外,每个版本中还包含生成该版本时对应的事务id。
ReadView
对于使用 READ COMMITTED 隔离级别的事务来说,所有事务直接读取数据库的最新值即可。
对于使用 SERIALIZABLE 隔离级别的事务来说,InnoDB 规定使用加锁的方式来访问记录。
对于 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说,必须保证读到的是已经提交了的事务修改过的记录,也就说假如另一个事务已经修改了记录但未提交,是不能直接读取最新版本的记录的。核心问题就是:需要判断一下版本链中的哪个版本是对当前事务可见的。为此,InnoDB 提交了 ReadView 的概念,这个 ReadView 中主要包含 4 个比较重要的内容:
-
m_ids:表示生成ReadView时当前系统中活跃的读写事务的事务id列表。 -
min_trx_id:表示生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。 -
max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。注意
max_trx_id并不是m_ids中的最大值,事务id是递增分配的。比如说现在有 id 为 1, 2, 3 这三个事务,之后 id 为 3 的事务提交了。那么一个新的读事务在生成ReadView时,m_ids就包括了 1 和 2,min_trx_id的值就是 1,max_trx_id的值就是 4。 -
creator_trx_id:表示生成该ReadView的事务的事务id。只要对表中的记录做改动时(执行
insert、delete、update)才会为事务分配事务id,否则在一个只读事务中的事务id都默认为 0。
有了这个 ReadView,在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
- 如果被访问行的
trx_id与ReadView中的creator_trx_id相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。 - 如果被访问行的
trx_id小于ReadView中的min_trx_id,意味着生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。 - 如果被访问行的
trx_id大于等于ReadView中的max_trx_id,意味着生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。 - 如果被访问行的
trx_id在ReadView中的min_trx_id和max_trx_id之间,那需要先判断一下trx_id是不是在m_ids列表中。如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性。依次类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意外着该条记录对该事务完全不可见,查询结果就不包含该记录。
在 MySQL 中,READ COMMITTED 和 REPEATABLE READ 隔离级别最大的区别就是它们生成 ReadView 的时机不同:
READ COMMITTED:每次读取数据前都生成一个 ReadView。
REPEATABLE READ:在第一个读取数据时生成一个 ReadView。