MySQL事务详解(事务隔离级别、实现、MVCC、幻读问题)
基本认识
事务(Transaction)在数据库中一般是指包含了一组原子性操作的步骤组合,这些操作要么都成功,要么都失败,事务一般包含了ACID四大特性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。这也是数据库区别于一般文件系统的重要特性之一。
分类
事务按照类别可以划分为以下几种类型:
- 扁平事务(Flat Transactions)
事务类型中最为简单的一种,也是使用最频繁的一种,通常伴随着BEGIN...COMMIT/ROLLBACK语句,所有操作都是原子性的。
- 带有保存点的扁平事务(Flat Transactions with Savepoints)
相比于扁平事务,带有保存点的扁平事务再事务内部维护了一个可以递增的保存点,在事务进行回滚时可以指定回滚到某个保存点,如果需要回滚到最初状态,需要再次执行ROLLBACK语句。
说明:执行过程类似于START TRANSACTION...SAVEPOINT a...ROLLBACK TO SAVEPOINT a...ROLLBACK /COMMIT
- 链事务(Chained Transactions)
链事务可以看作为带有保存点的扁平事务的一种变种。链事务将带有保存点的扁平事务中一个较长的操作链路转化为较短链路的事务链,这样一定程度保障了数据丢失的可能性。链事务同带有保存点的扁平事务类型相比,前者只能回滚到最近一个操作事务内的保存点,而后者可以回滚到任意保存点,并且链事务在COMMIT后就释放了当前事务所拥有的锁。
- 嵌套事务(Nested Transactions)
嵌套事务是一个多层次架构的事务树,由顶层事务控制子事务,子事务既可以是嵌套事务也可以是扁平事务,且叶子节点只能是扁平事务,每个子事务从根到叶节点的距离可以是不同的。
子事务既可以提交也可以回滚。但是它的提交操作并不马上生效,除非其父事务已经提交。因此可以推论出,任何子事物都在顶层事务提交后才真正的提交。树中的任意一个事务的回滚会引起它的所有子事务一同回滚,故子事务仅保留A、C、I特性,不具有D的特性。
- 分布式事务(Distributed Transactions)
分布式事务通常指在分布式环境中运行的扁平事务,一般出现在跨数据库实例的访问链路中。分布式事务同样要求满足ACID特性。单依靠数据库特性并不一定可以完美解决,一般需要引入第三方工具进行中间协调,如Seata。
MySQL中对于InnoDB存储引擎来说,其支持扁平事务、带有保存点的事务、链事务、分布式事务。对于嵌套事务,其并不原生支持,但用户仍可以通过带有保存点的事务来模拟串行的嵌套事务。
特性
事务所谓四大特性特点如下:
- 原子性:
原子指的是不可拆分的特性,例如Java中AtomicInteger原子性操作。同样在数据库中事务是操作的最小单元,整个事务所包含的操作中全部成功或失败,不存在中间状态。在MySQL中与原子性相关的设置或者操作包含 autocommit设置、 COMMIT 语句、ROLLBACK语句。
- 一致性
数据库中的状态总是会从一种一致性的状态转换到另外一种一致性状态。而事务的原子性操作特点,也保证了一致性的必然结果。与一致性相关的特点InnoDB Crash Recovery、 Doublewrite Buffer。
- 隔离性
事务隔离性通常指的是一个事务对于数据的操作未提交前,对于另外一个事务产生的结果是不可见的。这主要取决于事务的隔离级别,在主流数据库中默认的隔离级别都是READ COMITTED(或称不可重复读),这种设置主要是考虑到性能影响,因为事务的实现是通过锁来实现的,过度的安全性并不会影响结果,但会导致数据库性能下降。与隔离性相关的特点Transaction Isolation Levels。
- 持久性
事务的持久性对事务的操作结果负责,一旦事务提交,操作结果会永久保存到本地磁盘中。即使由于某种原因导致系统崩溃,但是存储的数据文件依然可以对数据进行找回,当然根据crash-safe的设置机制,系统崩溃也有可能导致很小部分的数据丢失。持久性通过sync_binlog等实现。
不同存储引擎中对于事务的支持程度也不一样,如在MySQL中InnoDB支持事务特性,而MyISAM、Memory不支持事务。MySQL中默认的存储引擎为InnoDB。
隔离级别
基本概念
InnoDB中实现了四种隔离级别,这四种级别的隔离属性逐渐增强。
READ UNCOMMITTED
读未提交(脏读)
在该隔离级别中,一个未提交的事务,对数据的操作,其他事务也可以读取到。事务可以读取到未提交事务操作的数据称为脏读(Dirty Read)。从性能角度而言,READ UNCOMMITTED并没有比其他隔离级别好多少,但是脏读带来的问题会相当严重。
READ COMMITTED
读已提交(不可重复读)
在该隔离级别下,某种程度上已经解决了读隔离性的问题,这也是大多数数据库所采用的隔离级别。相比于READ UNCOMMITTED并不会带来脏读的结果,但是会出现不可重复读现象,同一事务中,两次读取的结果会不相同。
REPEATABLE READ
可重复读(默认值,幻读)
相比于READ COMMITTED,可重复读解决了READ COMMITTED不可重复读的问题,这也是MySQL中默认的隔离级别,但是单纯依赖于事务的隔离级别无法解决幻读(Phantom Read)问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另一个事务又在该范围内插入了新的记录,当前一个事务再次进行读取时会出现幻影行(Phantom Row)。关于如何解决幻读,后面会进行说明。
SERIALIZABLE
串行化
串行化是事务隔离级别中最高的隔离级别,通过更细粒度的锁,强制事务串行执行,该隔离级别解决了事务的幻读问题。在读取的每一行数据都进行加锁,会导致大量的时间都在等待或者争抢锁,对数据库的性能影响相当大,实际生产中很少使用该隔离级别。对数据强一致性且对性能要求较低场景才有可能考虑此隔离级别。
总而言之事务的隔离级别解决的是数据在操作过程中一致性的问题,关于事务的隔离级别以及出现的数据一直性问题如下表所示:
隔离级别 / 存在的问题 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | 存在 | 存在 | 存在 |
READ COMMITTED | 存在 | 存在 | |
REPEATABLE READ | 存在 | ||
SERIALIZABLE |
示例
关于事务隔离级别通过测试数据进行演示,关闭事务自动提交功能,创建测试数据:
set autocommit = 0;
create table u(id int primary key,name varchar(10)) engine=innodb;
insert into u values(1,'zs');
insert into u values(2,'ls');
insert into u values(3,'ww');
commit;
- READ UNCOMMITTED
- 客户端1
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update u set name='张三' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> rollback;
Query OK, 0 rows affected (0.01 sec)
- 客户端2
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from u where id=1;
+----+------+
| id | name |
+----+------+
| 1 | zs |
+----+------+
1 row in set (0.00 sec)
mysql> select * from u where id=1;
+----+--------+
| id | name |
+----+--------+
| 1 | 张三 |
+----+--------+
1 row in set (0.00 sec)
mysql> select * from u where id=1;
+----+------+
| id | name |
+----+------+
| 1 | zs |
+----+------+
1 row in set (0.00 sec)
流程图如下:
- READ COMMITTED
- 客户端1
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update u set name='张三' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
- 客户端2
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from u where id=1;
+----+------+
| id | name |
+----+------+
| 1 | zs |
+----+------+
1 row in set (0.00 sec)
mysql> select * from u where id=1;
+----+------+
| id | name |
+----+------+
| 1 | zs |
+----+------+
1 row in set (0.00 sec)
mysql> select * from u where id=1;
+----+--------+
| id | name |
+----+--------+
| 1 | 张三 |
+----+--------+
1 row in set (0.00 sec)
流程图如下:
- REPEATABLE READ
- 不可重复读
- 客户端1
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update u set name='zs' where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
- 客户端2
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from u where id=1;
+----+--------+
| id | name |
+----+--------+
| 1 | 张三 |
+----+--------+
1 row in set (0.00 sec)
mysql> select * from u where id=1;
+----+--------+
| id | name |
+----+--------+
| 1 | 张三 |
+----+--------+
1 row in set (0.00 sec)
mysql> select * from u where id=1;
+----+--------+
| id | name |
+----+--------+
| 1 | 张三 |
+----+--------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from u where id=1;
+----+------+
| id | name |
+----+------+
| 1 | zs |
+----+------+
1 row in set (0.00 sec)
- 幻读
- 客户端1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into u values(4,'zl');
Query OK, 1 row affected (0.01 sec)
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from u where id=4 for update;
+----+------+
| id | name |
+----+------+
| 4 | zl |
+----+------+
- 客户端2
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from u where id=4;
Empty set (0.00 sec)
mysql> insert into u values(4,'zl');
ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY'
mysql> select * from u where id=4;
Empty set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from u where id='4';
+----+------+
| id | name |
+----+------+
| 4 | zl |
+----+------+
1 row in set (0.01 sec)
流程图如下:
- SERALIZABLE
- 客户端1
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from u where id='5';
Empty set (0.00 sec)
- 客户端2
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into u values(5,'zb');
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
如图所示,当使用SERIALIZABLE隔离级别时,数据库会对读写进行强制重排序,保证事务有序进行。
流程图如下:
事务实现
数据库中事务包含了ACID四大特性,其中的隔离性通过读写锁+MVCC实现,原子性通过undo log实现,持久性则通过redo log实现,一致性通过AID三者共同实现,事务的最终目的也就是保证数据的一致性。
需要注意的是redo和undo并不是互逆操作,两种都是一种恢复操作,其中redo恢复的是提交事务修改的页操作,undo是回滚行记录到某个特定版本;此外两者记录内容不同redo通常是物理日志,记录的页物理操作,undo是逻辑日志,记录每行操作。
MySQL中的日志文件主要包含binlog、redo log、Undo Log等,其中binlog是MySQL Server层面的日志,关于服务端日志更多内容。而redo log、undo log是存储引擎层面的日志,并且只存在于存储引擎为InnoDB时。
binlog
binlog全称为binary log,也就是说该日志的存储格式为二进制。存储了MySQL中所有数据,binlog主要用来进行数据恢复以及主从复制时进行数据同步。binlog是逻辑日志,记录了MySQL中的操作信息,采用顺序写入,追加文件的形式。
MySQL 8.x版本中binlog是默认开始的,可以使用命令查看开启状态。
select @@log_bin
从表面看binlog与redo log有相似之处,二者本质上存在较大差别,前者的产生与存储引擎无关,任何存储引擎都会产生binlog,其次binlog是逻辑日志,且只在事务提交后记录SQL操作的语句,而后者只存在于InnoDB存储引擎中,且为物理格式的日志,记录的为实际页修改,并且需要记录事务进行中操作的内容。
redo log
redo log又称为重做日志,主要用来实现数据库事务中的持久性。重做日志主要由两部组成,第一部分是存储在内存中的重做日志缓冲(redo log buffer),由于存在内存中因此存在丢失风险,另外一部分是重做日志文件(redo log file),存储于磁盘中,是持久化的不易丢失。
redo log的存在另一方面还提高了数据库的更新效率,数据库中的数据持久化在磁盘文件中,如果每次对于数据的操作都要进行磁盘IO读写,会大大消耗性能。因此采用了一种称为WAL(Write Ahead Logging)的策略,这种策略关键点在于先写日志再写磁盘。这里的先写日志指的是会现在内存的Log Buffer中完成redo log及Undo Log的写入,然后在合适的时机调用fsync操作把内存中的Log Buffer更新到磁盘文件中。
Log Buffer如何从内存写入到磁盘日志文件中如下图所示:
日志内容会从MySQL的Buffer Pool汇总到Log Buffer中,并会根据不同的策略写入OS Buffer中,最终调用fsync操作写入磁盘文件中,这种缓冲的操作减少了IO次数提高了数据库的性能。
InnoDB存储引擎在事务COMMIT时,会通过Force Log at Commit机制要求先对日志文件进行写入磁盘。关于何时将Log Buffer内容写到磁盘,可通过innodb_flush_log_at_trx_commit配置,该参数值包含以下三种情况:
- 0:速度最快,安全性最差。在Commit阶段,会将数据写到Log Buffer里,不同步执行刷到磁盘操作,然后MySQL会每秒执行一次fsync操作将数据刷到磁盘。该模式下,当发生掉电时最多会丢失1秒内的全部操作。
- 1:最安全但速度最慢。每次事务提交时都会把数据刷到磁盘上,这是8.0版本的默认值。
- 2:每次事务提交时都会把数据写入到OS Buffer中,不同步执行刷到磁盘操作。然后MySQL会每秒执行一次OS Buffer的fsync操作。该模式速度较快,比0安全,如果有掉电保护组件的话,可以开启。
redo log文件大小是固定的,是一个物理日志(记录的是在某个数据页上做了什么样的修改),使用的是循环写入的过程。如图所示:
在redo log中包含了两个指针,write pos表示下一次写入数据的位置,可以一直往后写直到check point(已经刷新到磁盘的日志序列号,即LSN)位置,此外,每个页上也会记录一个LSN,用于表示已经刷新的数据。当写完一个事务时,write pos会往前移动,在redo log中的记录被更新到数据库中时,checkpoint会往前移动。
check point和write pos之间表示当前已经写入的数据,write pos和 checkpoint 之间的记录,可以被擦除。当write pos和check point重合时,write pos会回到最开头的文件,重新开始写。
2pc
2pc指的是两阶段提交,是为了保证MySQL Server层和存储引擎InnoDB层两部分日志所记录数据的一致性。当恢复数据时,无论根据redo log还是binlog,得到的数据是一致的。两阶段提交的过程如下图所示:
如果redo log和binlog分别提交,可能会造成用日志恢复出来的数据和原来数据不一致的情况:
- 假设先写redo log再写binlog,即redo log没有prepare阶段,写完直接置为commit状态,然后再写binlog。那么如果写完redo log后MySQL宕机了,重启后系统自动用redo log 恢复出来的数据就会比binlog记录的数据多出一些数据,这就会造成磁盘上数据库数据页和binlog的不一致,下次需要用到binlog恢复误删的数据时,就会发现恢复后的数据和原来的数据不一致。
- 假设先写binlog再写redo log。如果写完redo log后Mysql宕机了,那么binlog上的记录就会比磁盘上数据页的记录多出一些数据出来,下次用binlog恢复数据,就会发现恢复后的数据和原来的数据不一致。
由此可见,redo log和binlog的两阶段提交是非常必要的。
crash safe
MySQL Server层的binlog主要用于数据恢复,在数据误删时,可以将数据根据上次的全量备份恢复到某个时间点。而存储引擎InnoDB是以插件的形式引入,redo log的功能更主要是为了实现crash safe能力。
redo log和binlog的存在使得MySQL可以保证即使数据库发生异常重启,根据日志文件中记录的每个事务的状态,实现已提交的事务数据不丢失、未提交的事务数据进行回滚,这种机制叫做crash-safe,保证了事务的持久性特点。
为了实现无差错的crash-safe,在写入redo log和binlog文件时必须进行一次fsync操作将日志刷入磁盘,这样保证掉电时两个日志数据都写入了磁盘且数据一致,因此redolog必需设置innodb_flush_log_at_trx_commit=1。binlog也有决定何时进行fsync的配置sync_binlog,当值为0时代表刷新binlog_cache中的信息到磁盘由os决定。当值大于0为N时,代表每N次事务提交后就刷新binlog cache中的信息到磁盘,因此还要设置sync_binlog=1。两个参数值都得为1,俗称"双1",是保证crash-safe的根本,官方建议在需要保证事务持久性和一致性场景下设置成双1。
关于使用crash-safe进行数据恢复的场景如下所示:
- 场景一:开启binlog
- binlog有记录,redolog状态commit:正常完成的事务,无需操作
- binlog有记录,redolog状态prepare:在binlog写完提交事务之后的crash,提交事务
- binlog无记录,redolog状态prepare:在binlog写完之前的crash,回滚事务
- binlog无记录,redolog无记录:在redolog prepare之前crash,回滚事务
- 场景二:未开启binlog
- redolog状态是commit,正常完成的事务,无需操作
- redolog状态是prepare,回滚事务
undo log
undo log用来保证事务的原子性,InnoDB在写日志的时,在Log Buffer中不仅包含redo log buffer,还存在undo log buffer,也就意味着同时还会写入undo log file,与redo log file不同的是,undo log数据,位于一个特殊的段中(rollback segment),位于共享表空间中。undo log是一份逻辑日志,存储了与实际操作相反的记录,如下面几种场景:
- 当delete一条记录时,undo log会记录一条insert日志,反之依然
- 当update一条记录时,undo log会记录update之前的数据,作为回退源
当事务执行出现了错误或者用户手动调用rollback时,就可以进行数据恢复。undo log主要包含两种:
- insert undo log:代表事务在insert新记录时产生的undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃
- update undo log:事务在进行update或delete时产生的undo log,不仅在事务回滚时需要,在快照读时也需要,所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志会被 purge线程统一清除
说明:purge线程
- 为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除
- 为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view);如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
MVCC
MVCC全称为多版本并发控制,一种并发控制的方法,实现对数据库的并发访问。简单的说,MVCC就是为了实现读-写冲突不加锁,这里的读指的就是快照读,而非当前读(实际上是一种加锁的操作,是悲观锁的实现)。
当前读与快照读
MySQL中对于数据读取的操作包含两种形式:
- 当前读:如select lock in share mode(共享锁),select for update ; update, insert ,delete(排他锁)这些操作都称为当前读,读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
- 快照读:不加锁的非阻塞读(隔离级别不是串行级别)就是快照读。快照读的实现是基于MVCC,也可以认为是行锁的一个变种。快照读是基于提高并发性能的考虑,在很多情况下,避免了加锁操作,降低了开销。既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
实现原理
MVCC的实现原理主要是依赖三部分内容:隐式字段、undo log、Read View。
隐式字段
MySQL每行记录除了自定义字段外,还有数据库隐式定义的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段,具体作用如下:
- DB_TRX_ID:操作事务ID,长度为6字节,用来记录一条记录最近一次操作的(修改、插入)事务ID
- DB_ROLL_PTR:回滚指针,长度为7字节,用来指向这条记录的上一个版本(存储于rollback segment里),配合undo log使用
- DB_ROW_ID:隐含自增主键,长度为6字节,如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引
此外实际还有一个删除flag隐藏字段,当记录被更新或删除并不代表真的删除,而是删除flag变了。
undo log
对MVCC有帮助的实质是update undo log,undo log实际上就是存在rollback segment中旧记录链。不同事务或者相同事务对同一记录的修改,会导致该记录的undo log成为一条记录版本链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。
当执行update或者delete时,这条数据并不会被直接处理,例如在执行删除时,只是将隐式字段标志标记为删除,真正删除动作的执行是通过purge来延时操作完成的,也正基于此种设计使得通过undo log实现了MVCC多版本共存的能力。
Read View
Read View是事务在进行快照读操作时产生的读视图,也就意味着这个数据是在读的时刻产生的快照副本,记录并维护了一个当前活跃事务的自增ID。
Read View主要是用来做可见性判断的,当某个事务执行快照读的时候,对该记录创建一个Read View,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据。
Read View需要确保处于这样一个状态:当前视图里的数据是一部分或者全部事务提交后的结果,不存在未提交事务(即当前活跃事务)的部分数据。如何做到?
Read View遵循一个可见性算法,主要是将要被修改的数据的最新记录中的DB_TRX_ID(即当前事务ID)取出来,与系统当前其他活跃事务的ID去对比(由Read View维护),如果DB_TRX_ID跟Read View的属性做了某些比较,不符合可见性,那就通过DB_ROLL_PTR回滚指针去取出Undo Log中的DB_TRX_ID再比较,即遍历链表的DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),直到找到满足特定条件的DB_TRX_ID, 那么这个DB_TRX_ID所在的旧记录就是当前事务能看见的最新老版本
如何判断DB_TRX_ID对Read View是否符合可见性?简化一下Read View结构,假设有这三个全局属性
-
trx_list:一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID
-
up_limit_id:记录trx_list列表中事务ID最小的ID
-
low_limit_id:ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
判断逻辑如下:
- 比较DB_TRX_ID < up_limit_id,如果小于,则代表当前View能看到DB_TRX_ID所在的记录,符合可见性。否则进入下个判断
- 判断 DB_TRX_ID 大于等于 low_limit_id,如果大于等于则代表DB_TRX_ID所在的记录在Read View生成后才出现的,那对当前View肯定不可见,如果小于则进入下一个判断
- 判断DB_TRX_ID 是否在活跃事务之中,trx_list.contains(DB_TRX_ID),如果在,则代表View生成时刻,这个事务还在活跃,还没有Commit,你修改的数据,我当前View也是看不见的,不符合可见性;如果不在,则说明这个事务在View生成之前就已经Commit了,你修改的结果,我当前事务是能看见的,即符合可见性
常见问题
MVCC相关问题
RR是如何在RC级的基础上解决不可重复读的?
- 在RR级别下的某个事务对某条记录的第一次读会创建一个快照及Read View,将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;
- 即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见
- 而在RC级别下的事务中,每次快照读都会新生成一个快照和Read View,这就是在RC级别下的事务中可以看到别的事务提交的更新的原因
总结,在RC隔离级别下,每个快照读都会生成最新的Read View;在RR下,同一个事务中只会在第一次快照读时创建Read View,并且之后的快照读也是读的先前的Read VIew。
MySQL如何解决幻读
什么是幻读?
MySQL官方给出的幻读解释是:当某个事务在读取某个范围内的记录时,另一个事务又在该范围内插入了新的记录,当前一个事务再次进行读取时会出现幻影行(Phantom Row)
简单来说就是只要在一个事务中,第二次SELECT多出了Row就算幻读。举例说明:
A事务先SELECT,B事务此时进行INSERT,数据库会加Gap Lock,当B事务执行COMMIT,锁就会释放(释放后A事务也可以进行DML操作),A事务再进行SELECT出来的结果在MVCC下与第一次SELECT一样,如果A事务不使用for update进行读,当前读会作用在所有行上(包括B事务COMMIT的数据),这时A事务就会SELECT出B事务中操作的新行。如图所示:
在这个案例中,A事务使用快照读查询结果没有读出新行,使用当前读,可以读出B事务操作的新行。
MySQL如何解决幻读?
关于如何解决幻读,这里需要再次明确快照读和当前读两种形式下的区别。
- 在快照读读情况下,MySQL通过MVCC避免幻读(即当前读和快照读结果不一致不当做是幻读)
- 在当前读读情况下,MySQL通过Next-Key Lock避免幻读
不能把快照读和当前读得到的结果不一样这种情况认为是幻读,这是两种不同的使用方式。
说明:关于间隙锁简单概念
- 间隙锁(Gap Lock):锁住两行纪录之间的空隙,以避免数据新增、删除和更新操作。只在RR级别下有用,解决幻读问题。
- Next-Key Lock:单行锁(Record Lock)+ 间隙锁(Gap Lock)合起来称为Next-Key Lock。
通过参数innodb_locks_unsafe_for_binlog配置为0,代表开启间隙锁;值为1代表关闭间隙锁,在MySQL8.0之前默认值为0,该参数在 8.0 版本被移除,说明一直是打开的。
总结
- 事务
- 事务最终的本质是解决数据操作的安全性与最终状态的一致性,因此引申出了事务的ACID四大特性,其中AID共同保障了C,事务的并发安全程度主要依赖于隔离性的高低。
- 事务按照类型可以分为扁平事务(是否包含保存点)、链事务、嵌套事务(一般依赖于数据库之外的第三方工具实现)、分布式事务(依赖于数据库2pc或者分布式事务组件实现)。
- 事务的原子性依赖于undo log、隔离性依赖于锁、持久性依赖于redo log实现,此外在server还包含了二进制形式保存的binlog(主要用户数据恢复和数据备份)。MySQL在保障数据安全及保存效率的过程中采用了先写日志再写磁盘的形式,无论根据redo log还是binlog都可进行一致的数据恢复。
- 关于事务还要注意一些不好的事务习惯例如在循环中提交事务,使用自动提交或者回滚事务,在遇到长事务场景下尽可能去拆分事务。
- 隔离级别
- 数据库一般默认的隔离级别都是RC,在这种情况下可以尽可能的保障数据安全与并发性能,MySQL的默认隔离级别为RR(解决了不可重复读)。
- MySQL中可以使用SERIALIZABLE解决幻读,但生产中并不推荐,串行化导致性能过低。此外在快照读中通过MVCC解决幻读,在当前读中通过Next-Key Lock解决幻读。
- 快照读只会在select不加锁时存在,RR级别下,同一事务里的只会生成一个快照读,RC级别下,每次快照读都生成新的view,因此存在不可重复读问题
- MVCC
- MVCC作用是不加锁的方式解决快照读与写冲突问题,MVCC是一种乐观锁体现,既可以保证数据相对的安全又提高了并发下的性能。
- MVCC的实现基于undo log、数据库隐式字段、和Read View实现,提供了多版本并发控制,通过undo log记录了数据操作的多个版本,隐式字段标记及purge线程实现了数据的延时操作,通过Read View读取的数据不一定是当前数据的最新版本,可能为某个undo log链中记录的数据,如果在快照读中,读取了数据的多个版本不认为是幻读(即快照读和当前读得到的结果不一样这种情况不认为是幻读),那即可认为MySQL在RR级别快照读场景中解决了幻读。
- 当发生快照读时(快照读本身就在一个事务里),通过MVCC会组装一个view,这个view只包含当前事务发生之前已提交的最新数据,和当前事务在快照读前修改的最新数据,而不存在其他未提交事务的脏数据或者老的数据,或者在当前事务之后新创建事务提交的数据。
参考资料:
- https://dev.mysql.com/doc/refman/5.7/en/
- 博客园《MySQL如何解决幻读》
- MySQL技术内幕:InnoDB存储引擎 第2版
- 高性能MySQL 第3版
- 高可用MySQL 第3版
本文来自博客园,作者:星光Starsray,转载请注明原文链接:https://www.cnblogs.com/starsray/p/16456569.html