如果一个数据库只有一个用户的话,很多我们现在考虑的问题都不会存在,而数据库的设计也会变得比较简单。但是现实中,数据库一般是一个大型的共享资源,也就意味着很多用户都要进行事物操作,这也就带来一个问题就是,如果一个数据被一个以上的用户同时访问怎么办?这个就是数据库中的并发控制问题。举一个估计被说烂的例子:

抢火车票,尤其是春运的时间。现在一趟列车有票300张,大家都开始抢,电脑,手机,平板电脑。大概一秒内会有几十甚至上百的请求要求购买车票,放到数据库中,就是一秒内几十甚至上百的事务操作,这个操作就是对火车票剩余数据进行数值减一的修改[因为对买个人而言假定就买一张车票],如果没有并发控制,那么这时虽然已经买了几十张票,但是数据库显示当前还有票299张。等人们真去坐车的时候肯定要打架。

这时候并发控制就会发挥作用,他要求无论在任何情况下,都要保证事务的隔离性,和整个系统的一致性。就是说虽然是同时发起的请求也要保证,每个人都可以买到一张票,也就是依次对数据进行修改。那么如何才能做到并发控制?

经典的并发控制机制有:乐观并发控制[Optimistic Concurrency Control],悲观并发控制[Pessimistic Concurrency Control],多版本并法控制[Multiversion Concurrency Control]

Time Stamp

时间戳是一个解决数据库[乃至其他领域]的并发问题。对于每个Txn,数据库系统会在他们执行之前分配一个唯一固定的时间戳。时间戳越大说明该Txn进入数据库系统执行的越晚。

而且时间戳的排序机制通过事先在每对事务之间选择一个顺序来保证可串行性[serialization],因此,时间戳的值产生了一个精确的顺序,事务按照该顺序提交给DBMS。时间戳是在Txn执行之前由数据库系统分配的唯一

其实时间戳的最根本作用就是对事物进行有逻辑顺序的编号,使其在任何时候都可以进行并发控制或者事务调度的时候遵循一个既定的逻辑。

时间戳必须有两个特性:

  • 惟一性,保证不存在相等的时间戳值。

  • 单调性,保证时间戳的值是一直增长的。

同一事务中所有的数据库操作(读和写)都必须有相同的时间戳。

DBMS按照时间戳顺序执行冲突的事务,因此保证了事务的可串行化。如果两个事务冲突,通常终止其中一个,将其回滚并重新调度,赋予新的时间戳。存储在数据库中的每个值都要求两个附加的时间戳域:一个是该域最后一次读的时间,另一个是最后一次更新的时间。因此时间戳增加了内存需求和数据库的处理开销。因为有可能导致许多事务被终止,重新调度和重新赋予时间戳,时间戳方法一般需要大量的系统资源。

Optimistic Concurrency Control

Optimistic Concurrency Control[OCC],乐观并发控制机制,又名乐观锁。不同于悲观锁的机制,乐观锁的设计哲学是乐观,积极,它默认其他的事务操作不会对自己的操作造成影响,等到最终事物提交的时候再进行检查。所以,即便是两个事务同时操作一个数据,他们依然可以进行操作,这个时候,系统会为他们生成不同的版本号(版本号可以由时间戳方式生成)。

这里摘抄维基百科的解释:

在关系数据库管理系统里,乐观并发控制[又名“乐观锁”,Optimistic Concurrency Control,缩写OCC]是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。乐观事务控制最早是由孔祥重[H.T.Kung]教授提出。

乐观锁[Optimistic Locking]认为数据一般情况下不会造成冲突,即假定所有操作都是友善的。所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。

数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对,如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。

特点:

乐观并发控制相信事务之间的数据竞争[data race]的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。比如单纯的读取工作,如果使用悲观锁就会造成开销,但是使用乐观锁就不会产生任何这种开销。

但如果直接简单这么做,还是有可能会遇到不可预期的结果,例如多个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题,需要合理机制处理这种情况。

Pessimistic Concurrency Control

Pessimistic Concurrency Control,悲观并发控制,又称悲观锁。

其主要用于解决系统中的并发问题。首先要说明的是,无论是悲观并发控制还是后面介绍的乐观并发控制,他们主要阐述的是一种思想,一种解决问题的设计哲学,而并不是单纯的机制,事实上,在实现悲观并发控制和乐观并发控制的时候也需要考虑很多环境因素才能达到最好的效果。

悲观并发控制的设计哲学是保守的,悲观的,说消极的。它默认除了自己本身的操作外,其他所有操作都是有敌意的,都有可能会对自己操作造成影响。所以再进行数据操作的时候,会先给数据加上一个排它锁来防止其他事务操作此项数据。

下面借用维基百科的解释:

悲观并发控制可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作都某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。 悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本的环境中。

具体流程是:

  1. 在对任意记录进行修改前,先尝试为该记录加上排他锁[exclusive locking]。

  2. 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际需要决定。

  3. 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。

  4. 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。

特点

  • 悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。

  • 在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;

  • 另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数

Multiversion Concurrency Control

Multi-Version Concurrent Control,MVCC,多版本并发控制,也是解决在分布式系统中并发问题的一种思想。

之前介绍过悲观锁和乐观锁,他们的理念都是锁,就是一次只能一个事务操作一个数据。比如一个人正在读数据,一个人正在写输入,那么读取数据的那个人获得的数据可能就是不完整的,甚至是错误的。所以可以先把数据锁住,让那个写数据的人先完成操作,再让其他人读。无论是悲观锁还是乐观锁都有其适合的场景换句话说都有缺陷,比如系统操作比较慢,锁住的数据其他人不能读取。

多版本并发控制提供了另一种思路,snapshot[快照]

当一个用户进入数据库,读取或者写入数据时,他看到或者操作的数据是当前时段,这个数据的快照(snapshot),也就是说并不是真正的那个数据而是他的一个副本,这样每个用户都会获得一个快照,所以并不会出现等待的问题。

数据的一致性也会被保证,因为其他人的修改,这个用户并不会看到,直到其他人的修改进行了事务提交,这个用户才会被告知有更新的版本。但是,其他人修改的新数据并不会覆盖以前的,而是添加一个新的版本号,然后存起来,这样,对于同一个数据,可能会在数据库中存在很多个版本,但是他们的版本号是唯一且线性的,所以永远会有一个最新的版本,然后其他的版本会被标为obsolete。当然这么做会占用大量的存储资源,所以一般DBMS会周期性清理过期的数据版本释放资源。

所以多版本并发控制一般流程是:

  1. 如果是单纯读取工作,MVCC不会在意,因为它本身就是非阻塞式的。当产生并发控制问题,MVCC会为每个事务创建一个快照,snapshot,即原数据的副本。

  2. 当一个新的修改commit之后,MVCC不会重写这个数据,而是创建一个这个数据的新的版本(较高的版本号),然后将之前的数据标记为obsolete。

  3. 对于同一个数据,DBMS中永远会有一个最新的版本,和其他的过时版本。

  4. DBMS定期清理过期版本来释放存储资源。

当然像悲观锁和乐观锁一样,多版本并发控制也是一种方法或者说理论,具体实现的时候还有很多种方式,比如如何生成版本号,周期清理过期版本方式,snapshot创建级别是row-level还是table-level,等等。