最近,五一小长假的放假时间调整了,决定趁着假期出去玩一玩。我和女友商量好,我负责制定行程,她负责购买出行用品。相安无事,我正在各家比价中,不知道发生了什么,女友买买买居然不高兴了。mysql
并发控制sql
在《如何给女友解释什么是并发和并行》一文中咱们介绍过并发和并行。当程序中可能出现并发的状况时,咱们就须要经过必定的手段来保证在并发状况下数据的准确性,经过这种手段保证了当用户和其余用户一块儿操做时,所获得的结果和他单独操做时的祷告的结果是同样的。数据库
这种手段就叫作并发控制。并发控制的目的是保证一个用户的工做不会对另外一个用户的工做产生不合理的影响。安全
没有作好并发控制,就可能致使脏读、幻读和不可重复读等问题。架构
并发
咱们常说的并发控制,通常都和数据库管理系统(DBMS)有关,在DBMS中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。高并发
实现并发控制的主要手段大体能够分为乐观并发控制和悲观并发控制两种。性能
在开始介绍以前要明确一下:不管是悲观锁仍是乐观锁,都是人们定义出来的概念,能够认为是一种思想。其实不只仅是关系型数据库系统中有乐观锁和悲观锁的概念,像memcache、hibernate、tair等都有相似的概念。因此,不该该拿乐观锁、悲观锁和其余的数据库锁等进行对比。网站
悲观锁hibernate
当咱们要对一个数据库中的一条数据进行修改的时候,为了不同时被其余人修改,最好的办法就是直接对该数据进行加锁以防止并发。
这种借助数据库锁机制在修改数据以前先锁定,再修改的方式被称之为悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)。
之因此叫作悲观锁,是由于这是一种对数据的修改抱有悲观态度的并发控制方式。咱们通常认为数据被并发修改的几率比较大,因此须要在修改以前先加锁。
悲观并发控制其实是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
可是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增长产生死锁的机会;另外,还会下降并行性,一个事务若是锁定了某行数据,其余事务就必须等待该事务处理完才能够处理那行数据。
乐观锁
乐观锁( Optimistic Locking ) 是相对悲观锁而言的,乐观锁假设数据通常状况下不会形成冲突,因此在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,若是发现冲突了,则让返回用户错误的信息,让用户决定如何去作。
相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。通常的实现乐观锁的方式就是记录数据版本。
乐观并发控制相信事务之间的数据竞争(data race)的几率是比较小的,所以尽量直接作下去,直到提交的时候才去锁定,因此不会产生任何锁和死锁。
悲观锁实现方式
悲观锁的实现,每每依靠数据库提供的锁机制。在数据库中,悲观锁的流程以下:
在对记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
若是加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。具体响应方式由开发者根据实际须要决定。
若是成功加锁,那么就能够对记录作修改,事务完成后就会解锁了。
其间若是有其余对该记录作修改或加排他锁的操做,都会等待咱们解锁或直接抛出异常。
咱们拿比较经常使用的MySql Innodb引擎举例,来讲明一下在SQL中如何使用悲观锁。
要使用悲观锁,咱们必须关闭mysql数据库的自动提交属性,由于MySQL默认使用autocommit模式,也就是说,当你执行一个更新操做后,MySQL会马上将结果进行提交。set autocommit=0;
咱们举一个简单的例子,如淘宝下单过程当中扣减库存的需求说明一下如何使用悲观锁:
//0.开始事务 begin; //1.查询出商品库存信息 select quantity from items where id=1 for update; //2.修改商品库存为2 update items set quantity=2 where id = 1; //3.提交事务 commit;
以上,在对id = 1的记录修改前,先经过for update的方式进行加锁,而后再进行修改。这就是比较典型的悲观锁策略。
若是以上修改库存的代码发生并发,同一时间只有一个线程能够开启事务并得到id=1的锁,其它的事务必须等本次事务提交以后才能执行。这样咱们能够保证当前的数据不会被其它事务修改。
上面咱们提到,使用select…for update会把数据给锁住,不过咱们须要注意一些锁的级别,MySQL InnoDB默认行级锁。行级锁都是基于索引的,若是一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点须要注意。
乐观锁实现方式
使用乐观锁就不须要借助数据库的锁机制了。
乐观锁的概念中其实已经阐述了他的具体实现细节:主要就是两个步骤:冲突检测和数据更新。其实现方式有一种比较典型的就是Compare and Swap(CAS)。
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知此次竞争中失败,并能够再次尝试。
好比前面的扣减库存问题,经过乐观锁能够实现以下:
//查询出商品库存信息,quantity = 3 select quantity from items where id=1 //修改商品库存为2 update items set quantity=2 where id=1 and quantity = 3;
以上,咱们在更新以前,先查询一下库存表中当前库存数(quantity),而后在作update的时候,以库存数做为一个修改条件。当咱们提交更新的时候,判断数据库表对应记录的当前库存数与第一次取出来的库存数进行比对,若是数据库表当前库存数与第一次取出来的库存数相等,则予以更新,不然认为是过时数据。
以上更新语句存在一个比较重要的问题,即传说中的ABA问题。
好比说一个线程one从数据库中取出库存数3,这时候另外一个线程two也从数据库中库存数3,而且two进行了一些操做变成了2,而后two又将库存数变成3,这时候线程one进行CAS操做发现数据库中仍然是3,而后one操做成功。尽管线程one的CAS操做成功,可是不表明这个过程就是没有问题的。
有一个比较好的办法能够解决ABA问题,那就是经过一个单独的能够顺序递增的version字段。改成如下方式便可:
//查询出商品信息,version = 1 select version from items where id=1 //修改商品库存为2 update items set quantity=2,version = 3 where id=1 and version = 2;
乐观锁每次在执行数据的修改操做时,都会带上一个版本号,一旦版本号和数据的版本号一致就能够执行修改操做并对版本号执行+1操做,不然就执行失败。由于每次操做的版本号都会随之增长,因此不会出现ABA问题,由于版本号只会增长不会减小。
除了version之外,还可使用时间戳,由于时间戳自然具备顺序递增性。
以上SQL其实仍是有必定的问题的,就是一旦发上高并发的时候,就只有一个线程能够修改为功,那么就会存在大量的失败。
对于像淘宝这样的电商网站,高并发是常有的事,总让用户感知到失败显然是不合理的。因此,仍是要想办法减小乐观锁的粒度的。
有一条比较好的建议,能够减少乐观锁力度,最大程度的提高吞吐率,提升并发能力!以下:
//修改商品库存 update item set quantity=quantity - 1 where id = 1 and quantity - 1 > 0
以上SQL语句中,若是用户下单数为1,则经过quantity - 1 > 0
的方式进行乐观锁控制。
以上update语句,在执行过程当中,会在一次原子操做中本身查询一遍quantity的值,并将其扣减掉1。
高并发环境下锁粒度把控是一门重要的学问,选择一个好的锁,在保证数据安全的状况下,能够大大提高吞吐率,进而提高性能。
如何选择
在乐观锁与悲观锁的选择上面,主要看下二者的区别以及适用场景就能够了。
一、乐观锁并未真正加锁,效率高。一旦锁的粒度掌握很差,更新失败的几率就会比较高,容易发生业务失败。
二、悲观锁依赖数据库锁,效率低。更新失败的几率比较低。
随着互联网三高架构(高并发、高性能、高可用)的提出,悲观锁已经愈来愈少的被使用到生产环境中了,尤为是并发量比较大的业务场景。