** | 作者 肖泽凡,腾讯TEG研发管理部小小后台攻城狮一枚,负责腾讯敏捷产品研发平台TAPD的基础功能的开发和维护,热爱技术,喜欢分享,文章首次发表于SegmentFault,博客名“X先生”,欢迎与我交流**
在数据库的实际使用过程中,我们常常会遇到不希望数据被同时写或者读的情景,例如秒杀场景下,两个请求同时读到系统还有库存1个,然后又先后把库存更新为0,这时候就会出现超卖的情况,这时候货物的实际库存和我们的记录就会对应不上了。
为了解决这种资源竞争导致的数据不一致等问题,我们需要有一种机制来进行保证数据的正确访问和修改,而在数据库中,这种机制就是数据库的并发控制。其中乐观并发控制,悲观并发控制和多版本并发控制是数据库并发控制主要采用的技术手段。
悲观并发控制
一、本质
维基百科:在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,PessimisticConcurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作读某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。
事实上我们常说的悲观锁并不是一种实际的锁,而是一种并发控制的思想,悲观并发控制对于数据被修改持悲观的态度,认为数据被外界访问时,必然会产生冲突,所以在数据处理的过程中都采用加锁的方式来保证对资源的独占。
数据库的锁机制其实都是基于悲观并发控制的观点进行实现的,而且按照实际使用情况,数据库的锁又可以分为许多种类,具体可以见我后面的文章。
二、实现方式
数据库悲观锁的加锁流程大致如下:
开始事务后,按照操作类型给需要加锁的数据申请加某一类锁:例如共享行锁等
加锁成功则继续后面的操作,如果数据已经被加了其他的锁,而且和现在要加的锁冲突,则会加锁失败(例如已经加了排他锁),此时需等待其他的锁释放(可能出现死锁)
完成事务后释放所加的锁
三、优缺点
优点:
悲观并发控制采取的是保守策略:“先取锁,成功了才访问数据”,这保证了数据获取和修改都是有序进行的,因此适合在写多读少的环境中使用。当然使用悲观锁无法维持非常高的性能,但是在乐观锁也无法提供更好的性能前提下,悲观锁却可以做到保证数据的安全性。
缺点:
由于需要加锁,而且可能面临锁冲突甚至死锁的问题,悲观并发控制增加了系统的额外开销,降低了系统的效率,同时也会降低了系统的并行性。
乐观并发控制
一、本质
维基百科:在关系数据库管理系统里,乐观并发控制(又名“乐观锁”,OptimisticConcurrency Control,缩写“OCC”)是一种并发控制的方法。它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。
乐观并发控制对数据修改持乐观态度,认为即使在并发环境中,外界对数据的操作一般是不会造成冲突,所以并不会去加锁,而是在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,则让返回冲突信息,让用户决定如何去做下一步,比如说重试或者回滚。
可以看出,乐观锁其实也不是实际的锁,甚至没有用到锁来实现并发控制,而是采取其他方式来判断能否修改数据。乐观锁一般是用户自己实现的一种锁机制,虽然没有用到实际的锁,但是能产生加锁的效果。
二、实现方式
CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。实现非阻塞同步的方案称为“无锁编程算法”( Non-blocking algorithm)。
乐观锁基本都是基于 CAS(Compare and swap)算法来实现的。我们先来看下CAS过程,一个CAS操作的过程可以用以下c代码表示:
intcas(long*addr,longold,longnew)
{
/* Executes atomically. */
if(*addr!= old)
return0;
*addr= new;
return1;
}
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。整个CAS操作是一个原子操作,是不可分割的。
乐观锁的实现就类似于上面的过程,主要有以下几种方式:
版本号标记:在表中新增一个字段:version,用于保存版本号。获取数据的时候同时获取版本号,然后更新数据的时候用以下命令:updatexxx set version=version+1,… where … version="old version" and ....。这时候通过判断返回结果的影响行数是否为0来判断是否更新成功,更新失败则说明有其他请求已经更新了数据了。
时间戳标记:和版本号一样,只是通过时间戳来判断。一般来说很多数据表都会有更新时间这一个字段,通过这个字段来判断就不用再新增一个字段了。
待更新字段:如果没有时间戳字段,而且不想新增字段,那可以考虑用待更新字段来判断,因为更新数据一般都会发生变化,那更新前可以拿要更新的字段的旧值和数据库的现值进行比对,没有变化则更新。
所有字段标记:数据表所有字段都用来判断。这种相当于就、不仅仅对某几个字段做加锁了,而是对整个数据行加锁,只要本行数据发生变化,就不进行更新。
三、优缺点
优点:
乐观并发控制没有实际加锁,所以没有额外开销,也不错出现死锁问题,适用于读多写少的并发场景,因为没有额外开销,所以能极大提高数据库的性能。
缺点:
乐观并发控制不适合于写多读少的并发场景下,因为会出现很多的写冲突,导致数据写入要多次等待重试,在这种情况下,其开销实际上是比悲观锁更高的。而且乐观锁的业务逻辑比悲观锁要更为复杂,业务逻辑上要考虑到失败,等待重试的情况,而且也无法避免其他第三方系统对数据库的直接修改的情况。
多版本并发控制
一、本质
维基百科: 多版本并发控制(Multiversion concurrency control, MCC 或 MVCC),是数据库管理系统常用的一种并发控制,也用于程序设计语言实现事务内存。
乐观并发控制和悲观并发控制都是通过延迟或者终止相应的事务来解决事务之间的竞争条件来保证事务的可串行化;虽然前面的两种并发控制机制确实能够从根本上解决并发事务的可串行化的问题,但是其实都是在解决写冲突的问题,两者区别在于对写冲突的乐观程度不同(悲观锁也能解决读写冲突问题,但是性能就一般了)。而在实际使用过程中,数据库读请求是写请求的很多倍,我们如果能解决读写并发的问题的话,就能更大地提高数据库的读性能,而这就是多版本并发控制所能做到的事情。
与悲观并发控制和乐观并发控制不同的是,MVCC是为了解决读写锁造成的多个、长时间的读操作饿死写操作问题,也就是解决读写冲突的问题。MVCC 可以与前两者中的任意一种机制结合使用,以提高数据库的读性能。
数据库的悲观锁基于提升并发性能的考虑,一般都同时实现了多版本并发控制。不仅是MySQL,包括Oracle、PostgreSQL等其他数据库系统也都实现了MVCC,但各自的实现机制不尽相同,因为MVCC没有一个统一的实现标准。
总的来说,MVCC的出现就是数据库不满用悲观锁去解决读-写冲突问题,因性能不高而提出的解决方案。
二、实现方式
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。每个事务读到的数据项都是一个历史快照,被称为快照读,不同于当前读的是快照读读到的数据可能不是最新的,但是快照隔离能使得在整个事务看到的数据都是它启动时的数据状态。而写操作不覆盖已有数据项,而是创建一个新的版本,直至所在事务提交时才变为可见。
当前读和快照读
什么是MySQL InnoDB下的当前读和快照读?
当前读
像select lock in share mode(共享锁),select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
快照读
像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是未提交读和串行化级别,因为未提交读总是读取最新的数据行,而不是符合当前事务版本的数据行。而串行化则会对所有读取的行都加锁
三、优缺点
MVCC 使大多数读操作都可以不用加锁,这样设计使得读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。
适用场景
一、悲观锁
用来解决读-写冲突和写-写冲突的的加锁并发控制
适用于写多读少,写冲突严重的情况,因为悲观锁是在读取数据的时候就加锁的,读多的场景会需要频繁的加锁和很多的的等待时间,而在写冲突严重的情况下使用悲观锁可以保证数据的一致性
数据一致性要求高
可以解决脏读,幻读,不可重复读,第一类更新丢失,第二类更新丢失的问题
二、乐观锁
解决写-写冲突的无锁并发控制
适用于读多写少,因为如果出现大量的写操作,写冲突的可能性就会增大,业务层需要不断重试,这会大大降低系统性能
数据一致性要求不高,但要求非常高的响应速度
无法解决脏读,幻读,不可重复读,但是可以解决更新丢失问题
三、MVCC
解决读-写冲突的无锁并发控制
与上面两者结合,提升它们的读性能
可以解决脏读,幻读,不可重复读等事务问题,更新丢失问题除外
本文由博客一文多发平台 OpenWrite 发布!