任何时候当有多个查询想要操作相同的数据的时候便会产生并发问题,而这很有可能会导致数据库陷入一种不一致的状态。如果恰好出现问题的数据是一批关键数据,那这个后果往往可能是致命的,因此如何控制并发是数据库中一个很重要的话题。
数据库控制并发的方式无外乎两种: * 悲观并发控制 * 乐观并发控制
其中悲观控制是最为常见的一种控制方式,我们所熟知的锁就属于悲观并发控制。乐观并发控制又被称为乐观锁,但其实这里并不存在一把真正意义上的锁,乐观锁更多的是一种控制机制,类似于协议一样的东西,只要遵守便能达到并发控制的效果。
另外一个跟数据库并发控制息息相关的概念是 事务 。你可以从 wiki 上获取关于事务的详细信息。数据库在执行事务操作的时候,为了保证事务的正确性可靠性,需要满足四个特性,也就是我们所熟知的 ACID。它们分别是:
* 原子性
(Atomicity)
* 一致性
(Consistency)
* 隔离性
(Isolation)
* 持久性
(Durability)
在不对事务的并发性做任何限制的情况下(舍弃掉隔离性),多个并发的事务可能产生如下一些有趣的结果: * 更新丢失 这很容易理解。针对同一条数据,比如一件商品的库存,原始存量为 100。现在查询 A 读出 100,并对其做+1 操作;同一时刻,查询 B 也读出原始存量 100,并对其做+2 操作;我们期望的是现在最新的库存量应该是 103,然而很不幸,如果没有任何并发控制,商品的库存有可能是 101 也有可能是 102。原因就在于查询 A 的更新动作丢失了,被查询 B 覆盖掉了(反之亦然)。 * 脏读 脏读通常发生在一个查询读取到了另一个还未提交的事务的某个中间状态值。由于事务还未提交,所以读取到的这个中间状态值有可能被进一步更改,也有可能被完全撤销(取决于事务的业务逻辑)。 * 不可重复读 简单说就是一个事务中针对同一份数据如果重复读取多次,则可能会读取到不同值。这通常是因为在多次读取的间隙中,另外一个事务修改了这份数据。 * 幻读 幻读可以解释为在一个事务中多次执行 select 语句,可能会拉取到之前并不存在的数据行(其它事务有新插入),也有可能会拉取不到之前存在过的数据行(其它事务进行了删除动作)
幻读和不可重复读可能会有些混淆,因为二者都表现为在同一个事务中重复执行读动作,会观察到不同的数据值。我们可以这样理解:不可重复读侧重于数据被莫名的更新,而幻读则更侧重于莫名其妙的增加或减少了某些数据
很显然,大多数情况下我们并不希望我们的数据库会发生上述这样一些匪夷所思的情况。之所以会说大多数情况下,是因为在某些场合中对数据的一致性要求并不是特别高,我们可能会通过舍弃部分数据一致性来换取更高的性能(在并发环境下这通常意味着更高的吞吐量),于是脏读、不可重复读以及幻读也都是有可能会允许发生的。
我们通过对事务施加隔离来实现并发控制的目的,SQL 标准定义四种隔离级别,它们分别是: 读未提交
(Read Uncommitted)、 读已提交
(Read Committed)、 可重复读
(Repeatable Read)以及 可串行化
(Serializable)。
数据库锁也就在这个时候正式进入我们的视野,作为实现事务隔离的一种手段添加进来。
接下来我们按隔离性由弱到强依次来看看上面提到的四种隔离级别:
* 读未提交
在这种隔离级别下,上面提到的几种现象中除了 更新丢失
,其它的都有可能会发生。数据库是通过让读取操作不施加任何锁来实现读未提交。因为没有任何锁,所以当其它事务中执行写操作时,该读取操作依然可以进行
锁简单可以分为共享锁和排他锁
数据库为锁定义了兼容性,可以简单的理解为共享锁可以和共享锁相互兼容,这表示如果一个资源上已经存在一个共享锁,那么另一个查询也可以在其上继续申请共享锁;反之,排他锁没办法和任何类型的锁相兼容,如果一个资源上已经存在一个排他锁,那么随后在该资源上任何类型的锁申请都将失败,查询只能是等待该排他锁被主动释放 事务中申请的排他锁会一直保持占用直到事务结束;而共享锁,sql server 中的实现是,当持有共享锁的资源处理完毕时会立即释放掉该共享锁;mysql 中则会像排他锁一样,一直保持占用直到事务结束
键范围锁
(key-range lock)来实现的。How does MVCC (Multi-Version Concurrency Control) work