MySQL-数据库事务及InnoDB的事务、MVCC
数据库事务
ACID特性
原子性(atomicity,或称不可分割性)
一致性(consistency)
隔离性(isolation,又称独立性)
持久性(durability)
标准事务的隔离级别
读未提交 read-uncommitted
允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。事务对当前被读取的数据不加锁;
事务在更新某数据的瞬间,必须先对其加行级共享锁,直到事务结束才释放。读已提交 read-committed
允许读取并发事务已经提交过的数据,可以防止脏读,但是幻读或不可重复读仍有可能发生。事务对当前被读取的数据加行级共享锁(当读到时才加锁),一旦读完该行,立即释放;
事务在更新某数据的瞬间,必须先对其加行级排他锁,直到事务结束才释放。Oracle/SQLserver的默认隔离级别是read committed,是允许幻读和不可重复读存在的。
MySQL InnoDB在RC级别通过快照读解决了不可重复读问题。可重复读 repeatable-read
对同一数据记录的多次读取结果都是一致的,除非是被本身事务自己所修改,可以防止脏读和不可重复读,但幻读仍有可能发生。事务在读取某数据的瞬间,必须先对其加行级共享锁,直到事务结束才释放;
事务在更新某数据的瞬间,必须先对其加行级排他锁,直到事务结束才释放。MySQL InnoDB默认隔离级别是RR,在可重复读级别解决了幻读问题,是通过行锁和间隙锁的组合 Next-Key 锁实现的。
串行化 serializable
最高的隔离级别,完全服从ACID的隔离级别。
所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。事务在读取数据是,必须先对其加表级共享锁,直到事务结束才释放;
事务在更新数据时,必须先对其加表级排他锁,知道事务结束才释放。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ-UNCOMMITTED | √ | √ | √ |
READ-COMMITTED | × | √ | √ |
REPEATABLE-READ | × | × | √ |
SERIALIZABLE | × | × | × |
事务的并发问题
脏读 dirty read
当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”。不可重复读 unrepeatable read
一个事务对同一数据的读取结果前后不一致。
与脏读的区别在于,脏读的是另一事务未提交的数据,不可重复读的是另一事务已经提交的数据,只不过数据被其他事务修改过。事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
幻读 phantom read
事务读取某个范围的数据时,因为其他事务的insert操作导致前后两次读取的结果不一致。
幻读和不可重复读的区别在于,不可重复读是针对确定的某一行数据而言,而幻读是针对不确定的多行数据,因此幻读通常出现在带有查询条件的范围查询中。
一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询不存在的行。系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
更新丢失(Lost Update)或脏写
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新。
事务并发场景
- 读读情况
并发事务相继读取相同记录,允许发生。 - 读-写或写-读情况
读-写或写-读,即一个事务进行读取操作,另一个进行改动操作。
这种情况下可能发生脏读、不可重复读、幻读的问题。 - 写写情况
这种情况会出现脏写的问题,任何一种隔离级别都不允许这种问题的发生,是通过加锁来实现的。
并发问题的解决方案
怎么解决脏读、不可重复读、幻读的问题,有两种解决方案。
- 方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁
- 方案二:读写操作都采用加锁的方法
MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读)
,是通过 next-key锁 + MVCC
一起实现的,正常读的时候不加锁,写的时候加锁。
MySQL InnoDB事务原理
MySQL InnoDB事务特性实现
- A-原子性;原子性通过undo log实现;
- C-一致性;通过原子性、隔离性、持久性实现的;
- I-隔离性;隔离级别,通过MVCC + 锁实现;
- D-持久性;持久性通过redo log实现;
MySQL InnoDB的事务隔离级别的实现原理
标准SQL事务隔离级别的实现是依赖锁的。
InnoDB使用不同的锁策略(Locking Strategy)以及MVCC机制来实现不同的隔离级别。
读未提交 Read UnCommitted
事务对当前被读取的数据不加锁,都是当前读
。
事务在更新某数据的瞬间,对其加行级共享锁,直到事务结束才释放。读已提交 Read Committed
事务对当前被读取的数据不加锁,且是快照读
。
事务在更新某数据的瞬间,对其加行级排他锁(Record Lock),直到事务结束才释放。可重复读 Repeatable Read
事务对当前被读取的数据不加锁,且是快照读
。
事务在更新某数据的瞬间,对其加行级排他锁(Record + gap = next-key lock),直到事务结束才释放。- RR级别是通过MVCC实现的。
- 通过临键锁,MySQL InnoDB在RR级别解决了幻读。
序列化
事务在读取数据(读)时,必须先对其加表级共享锁,直到事务结束才释放,都是当前读
。
事务在更新数据(写)时,必须先对其加表级排他锁,直到事务结束才释放。
MySQL InnoDB的MVCC实现机制
什么是MVCC
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。
MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用不加锁的方式去处理读-写冲突,实现读-写冲突不加锁
。
实现了MySQL在InnoDB引擎中RC级别和RR级别下对数据库的并发访问,每次select操作会访问数据库中的版本链记录,其他事务可以修改这条记录,而select根据当前隔离级别去版本链中找到对应的版本记录,实现了读-写、写-读的并发执行,提升了系统的性能。
什么是MySQL InnoDB下的当前读和快照读
快照读和当前读都是针对于某一行数据记录来说的。
当前读
UPDATE
DELETE
INSERT
SELECT ... LOCK IN SHARE MODE
SELECT ... FOR UPDATE
为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
快照读
读取的是快照版本,也就是历史版本。
像不加锁的SELECT操作就是快照读,即不加锁的非阻塞读。快照读的前提是隔离级别不是未提交读和序列化读级别。
因为未提交读总是读取最新的数据行,而不是符合当前事务版本的数据行;而序列化读则会对表加锁,退化成当前读。
当前读、快照读和MVCC的关系
MVCC实现原理
MVCC的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 4个隐式字段、undo日志、Read View 来实现的。
Read View
什么是Read View,说白了Read View就是事务进行快照读操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。
本质是一个数据结构,包括以下几个部分:
- m_ids
当前活跃的事务ID集合(开启但未提交的事务ID); - min_trx_ids
最小活跃事务ID,小于它说明事务已经提交; - max_trx_ids
预分配的事务ID(下一个事务开启分配的事务ID),即当前最大事务ID+1,大于它说明该事务是ReadView生成之后才开启; - creator_ids
ReadView创建者的事务编号;
4个隐式字段 - 版本链
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID等字段
- DB_ROW_ID 6byte
隐含的自增ID(隐藏主键),如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引 - DB_TRX_ID 6byte
最近修改(修改/插入)事务ID,记录创建这条记录/最后一次修改该记录的事务ID - DB_ROLL_PTR 7byte
回滚指针,用于配合undo日志,指向这条记录的上一个版本(存储于rollback segment里) - DELETED_BIT 1byte
记录被更新或删除并不代表真的删除,而是删除flag变了
undo日志
InnoDB把这些为了回滚而记录的这些东西称之为undo log。
这里需要注意的一点是,由于查询操作(SELECT)并不会修改任何用户记录,所以在查询操作执行时,并不需要记录相应的undo log。
undo log主要分为3种:
- Insert undo log :插入一条记录时,至少要把这条记录的主键值记下来,之后回滚的时候只需要把这个主键值对应的记录删掉就好了。
- Update undo log:修改一条记录时,至少要把修改这条记录前的旧值都记录下来,这样之后回滚时再把这条记录更新为旧值就好了。
- Delete undo log:删除一条记录时,至少要把这条记录中的内容都记下来,这样之后回滚时再把由这些内容组成的记录插入到表中就好了。
- 删除操作都只是设置一下老记录的DELETED_BIT,并不真正将过时的记录删除。
- 为了节省磁盘空间,InnoDB有专门的purge线程来清理DELETED_BIT为true的记录。
为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view)。
如果某个记录的DELETED_BIT为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
整体流程
事务根据生成的ReadView去版本链中寻找自己能够访问的数据,最后得到所读的数据。
而这个寻找可读数据的规则就至关重要了,判断规则如下:
- 当前记录修改操作事务ID 等于 ReadView创建事务ID ? 等于说明是自身事务修改的数据,可以访问;
- 判断 trx_id < min_trx_id(2) ? 如果小于说明事务提交了,可以访问;
- 判断 trx_id >= max_trx_id(5) ? 成立说明该事物是在ReadView生成之后才开启的,不允许访问,不成立继续向下判断;
- 判断 min_trx_id <= trx_id < max_trx_id ?成立则在m_ids中对比,如果不存在说明事务已经提交,可以访问;
- 不满足以上条件,根据回滚指针依次往前一个版本数据查找,直到找到满足条件的数据;
MVCC相关问题
RC、RR级别下的InnoDB快照读有什么不同
正是Read View生成时机的不同,从而造成RC、RR级别下快照读的结果的不同。
- RR快照读
在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见。
即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见。 - RC快照读
在RC级别下的事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。
总之,
在RC隔离级别下,是每个快照读都会生成并获取最新的Read View,因此会出现不可重复读问题;
而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。