MySQL事务的隔离级别

隔离级别用来限定事务内的可见性,SQL标准定义了4种隔离级别,MySQL默认使用RR

  1. READ UNCOMMITTED(读取未提交,以下简称RU)

    事务内可以读取到事务外未提交的内容

  2. READ COMMITTED(读取已提交,以下简称RC)

    事务内可以读取到事务外已提交的内容

  3. REPEATABLE READ(可重读,以下简称RR)

    事务内的同一条查询多次执行返回相同的数据

  4. SERIALIZABLE(串行)

    对应一个记录会加锁,出现冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行

事务隔离的实现

MySQL的默认隔离级别为RR,那么怎么去实现呢?怎么确保我每次查询的结果都是一样的呢,如果数据有多个版本,然后可以根据事务的版本找到相应版本的数据就好了。

MVCC

InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。

当这个事务更新行记录的时候,会写入三个隐藏字段:

通过undolog,可以找到数据的历史版本,这就是MVCC(多版本并发控制)。

MVCC指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性。

一致性视图

那么怎么通过当前事务 ID 去找到对应的数据版本呢?每个事务在启动瞬间会用一个数组记录下当前正在活跃的事务 ID,数组里面事务 ID 的最小值记为低水位,当前系统里面已经创建过的事务 ID 的最大值加 1 记为高水位。这个视图数组和高水位,就组成了当前事务的一致性视图。

snapshot

这样,对于当前事务的启动瞬间来说,一个数据版本的 row trx_id,有以下几种可能:

  1. 如果落在绿色部分,表示这个版本是已提交的事务或者是当前事务自己生成的,这个数据是可见的;
  2. 如果落在红色部分,表示这个版本是由将来启动的事务生成的,是肯定不可见的;
  3. 如果落在黄色部分,那就包括两种情况: 3.1 若 row trx_id 在数组中,表示这个版本是由还没提交的事务生成的,不可见; 3.2 若 row trx_id 不在数组中,表示这个版本是已经提交了的事务生成的,可见。

一致性视图只会在 RR 与 RC 下才会生成,对于 RR 来说,一致性视图会在第一个查询语句的时候生成(或者在启动事务的时候指定:start transaction with consistent snapshot )。而对于 RC 来说,每个查询语句都会重新生成视图。

当前读和快照读

MySQL 使用 MVCC 机制,可以读取之前版本数据。这些旧版本记录不会且也无法再去修改,就像快照一样,所以我们将这种查询称为快照读。

为什么尽量不要使用长事务?

长事务意味着系统里面会存在很老的事务视图,在这个事务提交之前,回滚记录都要保留,这会导致大量占用存储空间。除此之外,长事务还占用锁资源,可能会拖垮库。

在开发过程中,少用长事务,如果无法避免,保证逻辑日志空间足够用,并且支持动态日志空间增长。可以在 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务:

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

为什么MySQL默认的隔离级别是可重复读?

在MySQL5.0以前,如果采用读已提交,主从复制会存在bug。因为当时的binlog只支持statement格式,会导致master和slave的SQL执行顺序不一致,而最终导致主从数据不一致。

所以当时的解决办法就是把隔离级别设置为可重复读,在这个级别下,会通过间隙锁来保证执行的顺序。

后来binlog有了row格式,它不会出现这种执行顺序不一致的问题,InnoDB作者也提倡采用这种格式,这时候,采用读已提交的隔离级别,会有较好的并发性能。

查看和设置隔离级别

查看全局和当前会话的隔离级别:

mysql> SELECT @@global.tx_isolation, @@tx_isolation;
+-----------------------+----------------+
| @@global.tx_isolation | @@tx_isolation |
+-----------------------+----------------+
| REPEATABLE-READ       | READ-COMMITTED |
+-----------------------+----------------+

要为当前会话设置隔离级别,使用SET SESSION TRANSACTION语句:

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

要修改全局配置,修改my.ini配置文件的transaction-isolation选项

各个隔离级别下的实例演示

RU

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

mysql read uncommitted

RC

mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

mysql read committed

RR

mysql> SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

mysql repeatable read

SERIALIZABLE

mysql> SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

mysql serializable

各个隔离级别下的锁策略

隔离级别不同,锁策略也不一样,锁定范围取决于查询条件使用到什么索引,下面就针对不同的查询条件来展开

RC

RR

参考资料