深入MySQL事务与MVCC机制
MySQL事务
我们的数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。
这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制,用一整套机制来解决多事务并发问题。本章节先深入讲解事务隔离与MVCC机制
事务及其ACID属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
- 原子性(Atomicity) :事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。(内部由undolog支持回滚)
- 一致性(Consistent) :在事务开始和完成时,数据都必须保持一致状态。使用事务最终目的就是要保证数据一致性。由其它3个特性和业务代码正确逻辑来实现。
- 隔离性(Isolation) :在事务并发执行时,他们内部逻辑操作不能相互干扰。隔离性由MySQL各种锁和MVCC机制实现。
- 持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。由redo log日志来实现。
并发事务处理带来的问题
脏读: 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态(可能回滚); 这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。
更新丢失(Lost Update)或脏写 当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新。
不可重复读 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性
幻读 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。事务A读取到了事务B提交的新增数据,不符合隔离性
注意:不可重复读针对的是:update、delete操作而言、幻读针对insert操作而言。之所以要分成两个问题主要原因是MySQL提供的解决方案不同。不可重复读是repeatable-read隔离级别基于一致性读视图(read-view)来解决的;而幻读是基于next-key lock解决的。
事务隔离级别
“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
这里可重复读隔离级别并未解决幻读问题哈,具体会在锁这一章节介绍
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。
同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读"和“幻读”并不敏感,可能更关心数据并发访问的能力。
常看当前数据库的事务隔离级别: show variables like 'tx_isolation'; 设置事务隔离级别:set tx_isolation='REPEATABLE-READ';
Mysql默认的事务隔离级别是可重复读,用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔离级别,如果Spring设置了就用已经设置的隔离级别
MVCC多版本并发控制机制
Mysql在可重复读隔离级别下就算其它事务对数据有修改也不会影响当前事务sql语句的查询结果。这个隔离性就是靠MVCC(Multi-Version Concurrency Control)机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。
Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。
MVCC快照机制
在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。这个快照是基于整库的。InnoDB里面每个事务有一个唯一的事务ID,叫作 transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。
而每行数据也都是有多个版本的。每次事务更新数据的时候,都会生成一个新的数据版本,并且把transaction id赋值给这个数据版本的事务ID,记为row trx_id。同时,旧的数据版本要保留,并且在新的数据版本中,能够有信息可以直接拿到它。这其实是依赖undo log版本链来实现的。
undo log版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链。
示例执行如下图所示,其中的每一行都表示同一个时刻。
一共有三个事务id(100 200 300),并且这三个事务commit时间都是不同的。为此会生成这样一个undo版本链:
在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前永远都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成read-view),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
版本链比对规则(可见性算法):
- 如果 row 的 trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
- 如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
- 如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况 a. 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的事务是可见的); b. 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。
read view机制详解
readview和可见性算法其实就是记录了sql查询那个时刻数据库里提交和未提交所有事务的状态。
要实现RR隔离级别,事务里每次执行查询操作readview都是使用第一次查询时生成的readview,也就是都是以第一次查询时当时数据库里所有事务提交状态来比对数据是否可见,当然可以实现每次查询的可重复读的效果了。
要实现RC隔离级别,事务里每次执行查询操作readview都会按照数据库当前状态重新生成readview,也就是每次查询都是跟数据库里当前所有事务提交状态来比对数据是否可见,当然实现的就是每次都能查到已提交的最新数据效果了。
注意:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作或加排它锁操作(比如select...for update)的语句,事务才真正启动。才会向mysql申请真正的事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。
总结
本文深入探讨了MySQL事务与MVCC机制的核心概念和工作原理:
-
事务的ACID特性:
- 原子性:事务是不可分割的操作单元,由undolog支持回滚
- 一致性:保证数据从一个一致状态转变为另一个一致状态
- 隔离性:通过锁机制和MVCC实现事务间的隔离
- 持久性:通过redolog确保提交的事务永久生效
-
并发事务问题:
- 脏读:读取到其他事务未提交的数据
- 脏写/更新丢失:多事务更新同一数据导致更新覆盖
- 不可重复读:同一事务内多次读取同一数据得到不同结果(针对update/delete)
- 幻读:同一事务内多次查询,后续查询发现前面不存在的记录(针对insert)
-
事务隔离级别:
- 读未提交(Read Uncommitted):可能出现脏读、不可重复读、幻读
- 读已提交(Read Committed):避免脏读,可能出现不可重复读、幻读
- 可重复读(Repeatable Read):MySQL默认级别,避免脏读和不可重复读
- 串行化(Serializable):最高隔离级别,通过加锁避免所有并发问题
-
MVCC机制:
- 实现在读已提交和可重复读隔离级别
- 通过undo log版本链实现数据的多版本并发访问
- 每个事务有唯一递增的事务ID(transaction id)
- 每行数据修改会生成新版本,旧版本保留在undo log中
-
一致性读视图(Read View):
- 由未提交事务ID数组和最大事务ID组成
- 可重复读隔离级别:事务开始时创建read-view并在整个事务期间保持不变,可使用start transaction with consistent snapshot构建视图。
- 读已提交隔离级别:每次查询都创建新的read-view
- 通过版本链比对规则(可见性算法)确定数据可见性
-
事务启动时机:
- begin/start transaction命令不是事务真正起点
- 第一个修改操作或排他锁操作才会真正启动事务
MySQL通过MVCC机制巧妙地平衡了数据一致性和并发性能,在不加锁的情况下实现了事务隔离,大大提高了数据库的并发处理能力。