0%

事务、事务隔离级别

事务

事务是数据库很常见且必须的特性,具体是指将多个普通的数据库操视为一个原子操作。最好的解释事务的例子是银行转账,账户A要给账户B转200元,具体步骤:

  1. 判断账户A是否有200元,若有,则扣除200元
  2. 在账户B上加200元

原子性的必要,若步骤1成功,步骤2失败,则用户账户金额丢失,用户肯定不答应。 若步骤1执行失败,却继续执行步骤2,成功了,则银行亏损200元,老板肯定不答应。
通常若步骤1执行失败,那么我们不继续向下执行了步骤2,这个很好理解。若步骤1执行成功,步骤2执行失败,此时则需要回滚步骤1到之前的状态,保证数据的一致性。

事务的 ACID 特性

  • Atomicity 原子性
  • Consistency 一致性
  • Ioslation 隔离性
  • Durability 持久性

具体每个特性的定义在这就不解释了。之前有一个歧义的地方,如果能保证原子性,一致性不自然就保证了吗?此处的一致性是指事务操作前后,数据库要保证在两个一致性的状态,比如“外键约束一致性”。

事务隔离级别

在介绍事务隔离级别前,先介绍不同事务间相互影响导致数据方面可能存在的问题。因为事务的隔离级别恰恰是为了解决这几个问题而产生的,有因才有果。

1. 脏读(dirty read)

事务1修改记录a,但还未提交,事务2就能读到记录a的修改,此时若事务2回滚修改,则事务1读到的数据就是错误的。

时间点 事务1 事务2
1 start transaction start transaction
2 select a (返回 123)
3 update a = 234
4 select a (返回234)
5 rollback
6

2. 不可重复读

在事务1两次select期间,事务2将数据修改并提交,那么导致事务1两个select看到的数据不一致,所以叫“不可重复读”。

时间点 事务1 事务2
1 start transaction start transaction
2 select a (返回 123)
3 update a = 234
4 commit
5 select a (返回234)
6

3. 幻读(phantom)

和具体的一条记录无关,事务1对一个范围内记录的修改,会影响事务2。

时间点 事务1 事务2
1 start transaction start transaction
2 select count(*) from table where id > 0 (返回 2)
3 insert into table values(id = 11)(插入一条新记录,id = 11)
4 commit
5 select count(*) from table where id > 0 (返回 3)
6

事务隔离级别:

  1. READ UNCOMMITTED
    是最低的隔离级别,以上三个问题都有,事务间完全没有隔离性,基本这种隔离级别不会用到。
  • READ COMMITTED(nonrepeatable read() (大多数数据库默认事务隔离级别)
    解决了脏读的问题,但是“不可重复读”,“幻读”的问题依旧存在
  • REPEATABLE READ (MySQL默认事务隔离级别)
    解决了“不可重复读”的问题,但是并未解决幻读问题
  • SERIALIZABLE
    将不同事务完全串行化,解决了幻读的问题,但是这种级别效率太低

可以看到自顶向下,事务的问题的严重性越来越高,事务相应有四个隔离级别,依次解决上述三个问题。

MySQL

MySQL的事务是存储引擎级别的支持,和Server层无关。有事务型存储引擎,例如InnoDB;有非事务型存储引擎 MyISAM。因为每张表的存储引擎不同,切勿在同一个事务里既操作事务型存储引擎表又操作非事务型存储引擎表。

  • MySQl的InnoDB引擎默认事务隔离级别是REPEATABLE READ,但InnoDB引擎通过MVCC解决了幻读的问题。
  • MVCC: multi-version concercney controller

InnoDB引擎实现MVCC的方式:数据库全局维护一个事务ID,每新开始一个事务,获取事务ID,并将其作为自己的事务ID,并将全局事务ID加1。后续事务会拿到新事务ID,事务ID依次递增,新事务的ID一定比旧事务ID大。
事务操作数据时(增删改查),将涉及数据默认加上两个字段,“创建时间”,“删除时间”,再创建备份。此处的“两个时间”,并不是真正的时间,而是操作此条记录的事务的事务ID(因为事务ID依次递增,可以用事务ID大致表示”先后次序“,即”时间“)。

  • 增,创建一条新记录,创建时间为全局事务ID
  • 删,将删除时间设置为全局事务ID
  • 改,备份一条新记录,创建时间为全局事务ID,删除时间未定义
  • 查,记录的创建时间 <= 当前事务ID小,记录的删除时间一定是未定义,或 >= 当前事务大

NOTE:此处只是一个大致思路,并未详尽研究,可能有不正确的地方。

Reference & Thanks

  • 《Hight Performance MySQL》3rd Edition Chapter 1