mysql隔离级别
介绍隔离级别之前,我们先看看隔离级别需要解决的问题:
- 脏读:所谓脏读,就是指事务A读到了事务B还没有提交的数据。
- 不可重复读:所谓不可重复读,就是指在一个事务里面读取了两次某个数据,读出来的数据不一致。
- 幻读:所谓幻读,就是指在一个事务里面的操作中发现了未被操作的数据。幻读出现的前提是并发的事务中有事务发生了插入、删除操作。
事务隔离级别,就是为了解决上面几种问题而诞生的。事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大。SQL 标准定义了四种隔离级别,这四种隔离级别分别是:
- 读未提交(READ UNCOMMITTED):能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种,因此很少使用。
- 读提交 (READ COMMITTED):能够读到那些已经提交的数据,自然能够防止脏读,但是无法限制不可重复读和幻读。
- 可重复读 (REPEATABLE READ):在数据读出来之后加锁,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,但是幻读的问题还是无法解决
- 串行化 (SERIALIZABLE):最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了。
mvcc
介绍完隔离级别,我们来看MVCC,即Multi Version Concurrency Control的简称,代表多版本并发控制,用于支持RC(读已提交)和RR(可重复读)隔离级别的实现。在一个支持MVCC的并发系统中, 我们需要支持两种读, 一个是快照读, 一个是当前读。
- 快照读:快照读是在事务开始时创建一个快照(snapshot),事务在整个执行过程中都使用这个快照来读取数据。快照读在可重复读(Repeatable Read)和序列化(Serializable)隔离级别下使用。
- 当前读:当前读是指在事务执行过程中直接读取最新的数据,而不使用快照。当前读在读已提交(Read Committed)和可串行化(Serializable)隔离级别下使用。在可重复读隔离级别下,使用select ... for update命令时也会出现当前读的情况,当事务执行select ... for update语句时,它会锁定查询结果集中的数据行,阻止其他事务对这些数据行的修改,直到当前事务提交或回滚。这样可以确保当前事务读取到的数据是最新的。另外,update、insert、delete这些操作也会使用当前读,因为这些语句执行前都会查询最新版本的数据,然后再做进一步的操作。这很好理解,假设你要 update 一个记录,另一个事务已经 delete 这条记录并且提交事务了,这样不是会产生冲突吗,所以 update 的时候肯定要知道最新的数据。
MVCC最大的优势:读不加锁,读写不冲突。在读多写少的OLTP应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。
MYSQL 事务日志
事务日志可以帮助提高事务的效率。使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝(可理解为缓存),再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。事务日志持久以后,内存中被修改的数据在后台可以慢慢地刷回到磁盘。目前大多数存储引擎都是这样实现的,我们通常称之为预写式日志(Write-Ahead Logging),修改数据需要写两次磁盘。
如果数据的修改已经记录到事务日志并持久化,但数据本身还没有写回磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这部分修改的数据。
MySQL Innodb中跟数据持久性、一致性有关的日志,有以下几种:
- Bin Log:是mysql服务层产生的日志,常用来进行数据恢复、数据库复制,常见的mysql主从架构,就是采用slave同步master的binlog实现的。在事务中,binlog是在事务提交时一次性写入的,所以读写分离架构中,如果从库查询主库事务中新增的记录,是查询不到的。
- Redo Log:记录了数据操作在物理层面的修改,mysql中使用了大量缓存,修改操作时会直接修改内存,而不是立刻修改磁盘,事务进行中时会不断的产生redo log,在事务提交时进行一次flush操作,保存到磁盘中。当数据库或主机失效重启时,如果redo log中有事务提交,则进行事务提交修改数据。
- Undo Log: 除了记录redo log外,当进行数据修改时还会记录undo log,undo log用于数据的撤回操作,它记录了修改的反向操作,比如,插入对应删除,修改对应修改为原来的数据,通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC。
MVCC实现
MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建时间,一个保存行的删除时间。当然存储的并不是实际的时间值,而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动递增。事务开始时刻的系统版本号会作为事务的版本号,用来和查询到的每行记录的版本号进行比较。
下面看一下在RR隔离级别下,MVCC具体是如何操作的。
-
SELECT
InnoDB会根据以下两个条件检查每行记录:
1.InnoDB只查找版本早于当前事务版本的数据行(也就是,行的系统版本号小于或等于事务的系统版本号),这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
2.行的删除版本要么未定义,要么大于当前事务版本号。这可以确保事务读取到的行,在事务开始之前未被删除。(如果后面被删除则属于幻读)
只有符合上述两个条件的记录,才能返回作为查询结果 -
INSERT
InnoDB为新插入的每一行保存当前系统版本号作为行版本号。 -
DELETE
InnoDB为删除的每一行保存当前系统版本号作为行删除标识。 -
UPDATE
InnoDB为插入一行新记录,保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为行删除标识。
保存这两个额外系统版本号,使大多数读操作都可以不用加锁。这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。
间隙锁
根据刚才mvvc的介绍,大家发现可重复读级别下,快照读情况下幻读是不会存在的,因为在事务开始时就保存了快照,事务中是不会读到其他事务提交的新数据的。而在当前读情况下,可能会出现幻读,所以Innodb 引擎为了解决可重复读隔离级别使用当前读而造成的幻读问题,就引出了间隙锁,举个例子,我们在事务A执行一条select * from table where id >2 and id < 5 for update语句,执行后会对table表加上 id 范围为 (2, 5) 的行加锁,此时在事务B中,我们执行insert into table values(5, ...)语句,这是判断到插入的位置(id=5)被事务 A 加了锁,于是事务 B 会生成一个插入意向锁,同时进入等待状态,直到事务 A 提交了事务。这样,通过间隙锁,当前读也避免了幻读问题。
当然,mysql在可重复读隔离级别下,虽然通过快照读(mvvc)和间隙锁很大程度避免了幻读,但是在某些极特殊情况下,还是可能会出现幻读问题的(此处不过多介绍了)。
总结
读提交和可重复读的时候有一个快照读的概念,学名叫做一致性视图,这是可重复读与不可重复读的关键;可重复读是在一个事务开始的时候生成一个当前事务的全局性快照,而读提交则是每次执行语句都重新生成一次快照;
对于一个快照来说,它能够读到那些事务版本的数据,需要新遵循以下规则:
-
当前事务内的更新,可以读到;
-
当前事务版本未提交,不能被读到;
-
当前版本事务A已提交,但是事务B的快照是在A提交前创建的,不能读到A的提交;(可重复读,保证读取的数据一致);
-
当前版本事务已提交,是在其它事务快照创建前提交的,可以被读到。
读提交和可重复读的区别就在创建快照的机制上面,读提交每执行一条语句的时候,都要重新创建一次快照,而可重复读是在事务开始时,一次创建,直到事务结束。
ps:串行化隔离级别,是读的时候加共享锁,也就是其他事务可以并发读,但不能写;写的时候加排它锁,其它事务即不能读,也不能写;
网友评论