vlambda博客
学习文章列表

MySQL 浅谈事务的隔离性和隔离级别

有这样一个例子:

比如:A同学银行卡里有100块钱,要到银行往B同学的卡里转100,银行后台的数据操作流程可能是先给B同学卡里加100块钱,再从A同学卡里减100块钱。如果前半部分完成后,后半部分还没来得及执行,这个时候A同学给C同学卡里转100块钱,这时因为A同学卡里的钱还没减掉,所以是可以给C同学转账的。那这样就有问题了,A同学明明只有100块钱,却给两个同学B,C,各转了100块钱并且还成功了,这样银行就没法玩了。这时就要用到“事务”这个概念了。

简单地说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在MySQL中,事务是在引擎层实现的。我们知道MySQL不止有一个引擎,但是不是所有的引擎都支持事务,比如MySQL原生的MyISAM引擎就不支持事务,InnoDB是支持事务的。

事务的隔离性与隔离级别

事务的四大特性(ACID):

  1. 原子性(Atomicity)

    原子性是指事务中包含的所有操作,要么全部成功,要么全部失败。

  2. 一致性(Consistency)

    一致性是指事务必须使数据库从一个一致性的状态变成另一个一致性的状态。也就是一个事务执行之前和执行之后都必须处于一致性状态。

  3. 隔离性(Isolation)

    隔离性是指一个事务的操作不能被其他事务干扰,多个事务之间互相隔离。

  4. 持久性(Durability)持久性是指一个事务一旦被提交,那么对数据库中的数据的改变就是永久性的,接下来的操作或者故障不会对其产生任何影响。

当数据库上有多个事务同时执行时,可能会出现脏读(dirty read),不可重复读(non-repeatable read),幻读(phantom read)的问题。

为了解决上面这几个问题,所以就有了“隔离级别”的概念。

事务的隔离级别:

  • 读未提交(read uncommitted):一个事务还没提交时,它做的改变就能被别的事务看到,造成的结果就是脏读。

  • 读提交(read committed):一个事务提交之后,它做的改变才会被其它事务看到,造成的结果是不可重复读。

  • 可重复读(repeatable read):一个事务执行过程中,提交事务之前看到的数据总是和事务开启时看到的数据是一致的。造成的结果是幻读。

  • 串行化(serializable):对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候。后访问的事务必须等待前一个事务执行完成才能继续执行。

注意:事务的隔离级别越严,效率就会越低。MySQL数据库默认隔离级别是可重复读。

举例说明这几个隔离级别:

创建一张user表,插入一条数据:

mysql> create table user(age int) engine=InnoDB;insert into user(age) values(12);

对于这条数据的操作顺序如下图:



不同事务隔离级别下V1,V2,V3,的值分别如下:

  • 读未提交:则 V1 的值就是 13。这时候事务 B 虽然还没有提交,但是结果已经被 A 看到了。因此,V2、V3 也都是 13。

  • 读提交:则 V1 是 12,V2 的值是 13。事务 B 的更新在提交后才能被 A 看到。所以, V3 的值也是 13。

  • 可重复读:则 V1、V2 是 12,V3 是 13。之所以 V2 还是 12,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的。

  • 串行化:则在事务 B 执行“将 12 改成 13”的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。所以从 A 的角度看, V1、V2 值是 12,V3 的值是 13。

实现方式:

数据库里会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动的时候创建的,整个事务存在期间都会用这个视图。在“读提交”隔离级别下,这个视图是每个SQL执行语句开始执行的时候创建的。在“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念。在“串行化”隔离级别下直接用加锁的方式来避免并行访问。

我们可以查看MySQL的事务隔离级别:

show variables like 'transaction_isolation';

事务隔离的实现

以下是以“可重复读”隔离级别为例说明。

在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新的值,通过回滚操作都可以得到前一个状态的值。

假设一个值从1被依次改成了2,3,4,在回滚日志里面就会有类似下图的记录。

当前值为4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的视图,上图中可以看到,在视图A,B,C里面,这条记录的值分别是1,2,4,同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC),对于视图A来说,要得到1,就必须将当前的值依次执行视图中所有的回滚操作才能得到。

但是回滚日志不能一直保留着。在不需要的时候才会删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志就会被删除。

什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的视图的时候,也就是这个数据不会再有谁驱使它回滚了。

所以,为什么建议尽量不要使用长事务,其中一个原因就是,长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

你可以在information_schema库的innodb_trx这个表查询长事务,比如下面这个语句就是用来查找持续时间超过60s的事务。

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

结束!