Post

事务ACID,锁以及MVCC

ACID

大家都熟悉关系型数据库事务的ACID特征。以我的理解,ACID的核心问题是一致性问题,这里的一致性是一个语义上的一致性,也就是保证数据处于一个有意义的状态下或者从一个有意义的状态迁移到另外一个有意义的状态,有意义也可以理解成为广义上的数据是正确的,比如转账的例子,A向B转账作为一个事务,存在两个操作分别是A扣款,和B存款,和两个关联数据A的账户和B的账户,数据一致性要求保证在从数据库中读取A和B账户的金额的时候,必须是符合既定规则的,这里的规则就是金额是实际A和B具有的金额。

首先看一下原子性提供的保证,原子性保证是”all or nothing”,还是以上面的转账为例子,A扣款和B存款,必须全部执行生效或者全部不执行生效。对数据库而言的多个更改数据操作,总是存在先后顺序,操作数据的时候,可能部分成功,但是部分因为系统奔溃而没有执行,这样就会因为原子性保证失效,而产生了不一致,因此将所有对数据库的操作都写入日志中。以便能够在部分操作未完成而奔溃的情况下,进行恢复达到一致性,这个过程称为恢复,这个在大多数关系型数据库中都会有这样的过程,一般是对已经提交,但是尚未执行的操作进行重放(redo),对尚未提交,但是部分已经执行的操作进行撤销(undo),更加详细的做法可以参考不同数据库的描述说明。

单纯的原子性无法保证一致性,例如在事务T1,发起A向B转账的过程中,存在另外一个事务T2,发起C向B转账,并先于T1完成。此时只提供原子性保证的T1会将T2转账的影响覆盖,显然在并发模式下,只保证原子性无法提供一致性的保证,因此引入隔离性概念。

隔离性,从定义上要求提供数据并发读写的能力,也就是每一个事务彼此之间相互独立,或者说多个并发事务执行之后,要能够保证和串行执行一样的结果。隔离性根据隔离级别的不同分成四个级别:

  1. 未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
  2. 提交读(Read Committed):只能读取到已经提交的数据,可以保证每次都读取到最新的数据。Oracle等多数数据库默认都是该级别 (不可重复读)
  3. 可重复读(Repeated Read):可重复读,但是不保证每次都读取到最新的数据。在同一个事务内的查询都是事务开始时刻一致的,MySQL InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,
  4. 串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞

脏读:一个事务读取到了另外一个事务没有提交的数据; 不可重复读:在同一事务中,两次读取同一数据,得到内容不同,典型的例子就是在两次查询之间进行了事务操作提交,导致两次查询的结果不一致 幻读:和不可重复读类似,不可重复读针对的是已有的数据,而幻读针对的是原来没有的数据,也就是insert操作。典型的例子,A事务执行范围查询,B事务插入一行记录,该记录刚好在A事务的查询范围之内,并提交,此时A两次查询的数据行不一致,出现幻读的核心原因就是无法对不存在的数据添加行锁,备注:MySQL利用MVCC实现了一致性读,并且会在查询范围内加Gap锁,保证范围内的数据不被插入,从而避免了幻读,但无法处理插入时候的冲突。

隔离性的实现又可以分成两类,一类是基于锁的并发控制(Lock-Based Concurrency Control),一类是给予多版本的并发控制(Multi-Version Concurrency Control

LBCC

锁是在数据竞争条件下,能够保持数据正确性的一个重要工具,锁按照共享类型划分,可以分为共享锁和排他锁,也成为读锁和写锁,两者的区别主要在于

  1. 共享锁,是为了解决读一致性而产生的锁,允许读读并发,而不允许读写并发,共享锁保证数据在锁范围内不会发生任何的变更操作,因此如果事务T对数据R加上共享锁之后,其他的事务只能对R再添加共享锁,而不能加排他锁,并且添加共享锁的事务只能进行读操作。
  2. 排他锁,是为了解决写读冲突和写写冲突而产生的锁,用于保证数据在锁范围内,只有有锁的持有者才能够访问/修改,在事务T对数据R加上排他锁之后,其他事务无法对R再添加其他类型的锁。

共享锁和排他锁,本质上都是悲观锁。对于读取操作,如果要实现可重复读,则可以在每次读取到的数据上添加共享锁,这样即可保证读取到的数据不被更新,但是无法避免insert操作,也就是幻读的情况,要解决幻读,可以将所有的操作进行排队做序列化的操作,或者针对查询范围添加一个范围锁,对符合范围条件的插入操作做冲突检测。对于更新操作,则添加排它锁,可以保证每次都读取到最新的数据,并且避免覆盖更新。

MVCC

LBCC可以锁的独占,最大限度保证数据的一致性,但是读写无法并发,这在日常的应用系统中显得太过低效了。因此就产生了基于乐观锁原理的多版本并发控制。即不同的事务可以同时看到同一个对象(数据行)的不同历史版本,这样读可以读取到旧版本的数据,同时写可以和读实现并发,这种模式的优势也就在这个地方,实现可重复的读,并且不需要添加悲观锁,同时可以实现读写的并发操作。

在MVCC模式下,读分成两种,快照读和当前读。快照读,读取的是记录的可见版本 (有可能是历史版本),不用加锁。当前读,读取的是记录的最新版本,并且当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。在MySQL的InnoDB中,一般情况下

快照读

select * from table where ?;

当前读

select * from table where ? lock in share mode;

select * from table where ? for update;

insert into table values (…);

update table set ? where ?;

delete from table where ?;

实现和参考

数据库中的锁和并发控制mvcc都是非常复杂而细节的内容,更加具体的实现可以参考

Mysql中的MVCC

MySQL加锁分析

This post is licensed under CC BY 4.0 by the author.