乐观锁与悲观锁

在数据库的锁机制中介绍过,数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。

乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是数据库系统中有乐观锁和悲观锁的概念,像memcache、hibernate、tair等都有类似的概念。

针对于不同的业务场景,应该选用不同的并发控制方式。所以,不要把乐观并发控制和悲观并发控制狭义的理解为DBMS中的概念,更不要把他们和数据中提供的锁机制(行锁、表锁、排他锁、共享锁)混为一谈。其实,在DBMS中,悲观锁正是利用数据库本身提供的锁机制来实现的。

下面来分别学习一下悲观锁和乐观锁。

悲观锁

在关系数据库管理系统里,悲观并发控制(又名“悲观锁”,Pessimistic Concurrency Control,缩写“PCC”)是一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作都某行数据应用了 锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。悲观并发控制主要用于数据争用激烈的环境,以及发生并发冲突时使用锁保护数据的成本 要低于回滚事务的成本的环境中。

悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度(悲观),因此,在整个数据处理 过程中,将数据处于锁定状态。 悲观锁的实现,往往依靠数据库提供的锁机制 (也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)

在数据库中,悲观锁的流程如下:

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

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

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

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

MySQL InnoDB中使用悲观锁

要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。 set autocommit=0;

//0.开始事务
begin;/begin work;/start transaction; (三者选一就可以)
//1.查询出商品信息
select status from t_goods where id=1 for update;
//2.根据商品信息生成订单
insert into t_orders (id,goods_id) values (null,1);
//3.修改商品status为2
update t_goods set status=2;
//4.提交事务
commit;/commit work;

上面的查询语句中,我们使用了 select…for update 的方式,这样就通过开启排他锁的方式实现了悲观锁。此时在t_goods表中,id为1的 那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

上面我们提到,使用 select…for update 会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认行级锁。行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁把整张表锁住,这点需要注意。

优点与不足

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增 加产生死锁的机会;另外,在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数 据,其他事务就必须等待该事务处理完才可以处理那行数

乐观锁

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

乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。

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

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

实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳。

使用版本号实现乐观锁

使用版本号时,可以在数据初始化时指定一个版本号,每次对数据的更新操作都对版本号执行+1操作。并判断当前版本号是不是该数据的最新的版本号。

1.查询出商品信息
select (status,status,version) from t_goods where id=#{id}
2.根据商品信息生成订单
3.修改商品status为2
update t_goods 
set status=2,version=version+1
where id=#{id} and version=#{version};

优点与不足

乐观并发控制相信事务之间的数据竞争(data race)的概率是比较小的,因此尽可能直接做下去,直到提交的时候才去锁定,所以不会产生任何锁和死锁。但如果直接简单这么做,还是有可能会遇到不可预 期的结果,例如两个事务都读取了数据库的某一行,经过修改以后写回数据库,这时就遇到了问题。

在数据库中,并发控制有乐观锁和悲观锁之间,什么时候用乐观锁比较好什么时候用悲观锁比较好?

实际生产环境里边,如果并发量不大,完全可以使用悲观锁定的方法,这种方法使用起来非常方便和简单。
但是如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以就要选择乐观锁定的方法。

悲观锁假定其他用户企图访问或者改变你正在访问、更改的对象的概率是很高的,因此在悲观锁的环境中,在你开始改变此对象之前就将该对象锁住,并且直 到你提交了所作的更改之后才释放锁。悲观的缺陷是不论是页锁还是行锁,加锁的时间可能会很长,这样可能会长时间的限制其他用户的访问,也就是说悲观锁的并 发访问性不好。

乐观锁则认为其他用户企图改变你正在更改的对象的概率是很小的,因此乐观锁直到你准备提交所作的更改时才将对象锁住,当你 读取以及改变该对象时并不加锁。可见乐观锁加锁的时间要比悲观锁短,乐观锁可以用较大的锁粒度获得较好的并发访问性能。但是如果第二个用户恰好在第一个用 户提交更改之前读取了该对象,那么当他完成了自己的更改进行提交时,数据库就会发现该对象已经变化了,这样,第二个用户不得不重新读取该对象并作出更改。 这说明在乐观锁环境中,会增加并发用户读取对象的次数。

以版本控制系统为例,来说说两种最基本的并发性问题。 

  【丢失更新】 
  小张想修改源代码里面的a方法,正在她修改的同时,小李打开了这个文件,修改了b方法并且保存了文件,等小张修改完成后,保存文件,小李所做的修改就被覆盖了。 

  【不一致的读】 
  小张想要知道包里面一共有多少个类,包分了a,b两个子包。小张打开a包,看到了7个类。突然小张接到老婆打来的电话,在小张接电话的时候,小李往a包中加了2个类,b包中加了3个类(原先b包中是5个类)。 

  小张接完电话后再打开b包,看到了8个类,很自然得出结论:包中一共有15个类。 

  很遗憾,15个永远不是正确的答案。在小李修改前,正确答案是12(7+5),修改后是17(9+8)。这两个答案都是正确的,虽然有一个不是当前的。但15不对,因为小张读取的数据是不一致的。 

  小结:不一致读指你要读取两种数据,这两种数据都是正确的,但是在同一时刻两者并非都正确。 

  【隔离 和 不可变】 
  在企业应用中,解决并发冲突的两种常用手段是隔离和不可变。 

  只有当多个活动(进程或者线程)同时访问同一数据时才会引发并发问题。一种很自然的思路就是同一时刻只允许一个活动访问数据。如果小张打开了文件,就不允许其他人打开,或者其他人只能通过只读的方式打开副本,就可以解决这个问题。 

  隔离能够有效减少发生错误的可能。我们经常见到程序员陷入到并发问题的泥潭里,每一段代码写完都要考虑并发问题,这样太累了。我们可以利用隔离技术创建出 隔离区域,当程序进入隔离区域时不用关心并发问题。好的并发性设计就是创造这样的一些隔离区域,并保证代码尽可能的运行在其中。 

  另一种思路:只有当你需要修改共享的数据时才可能引发并发性问题,所以我们可以将要共享的数据制作为“不可变”的,以避免并发性问题。当然我们不可能将所有的数据都做成不可变的,但如果一些数据是不可变的,对它们进行并发操作时我们就可以放松自己的神经了。 

  【乐观并发控制、悲观并发控制】 
  如果数据是可变的,并且无法隔离呢?这种情况下最常用的两种控制就是乐观并发控制和悲观并发控制。 

  

  假设小张和小李想要同时修改同一个文件。如果使用乐观锁,俩人都能打开文件进行修改,如果小张先提交了内容,没有问题,他所做的改变会保存到服务器上。但 小李提交时就会遇到麻烦,版本控制服务器会检测出两种修改的冲突,小李的提交会被具体,并由小李决定该如何处理这种情况(对于绝大部分版本控制软件来说, 会读取并标识出小张做的改变,然后由小李决定是否合并)。 

  

  如果使用的是悲观锁,小张先检出(check out)文件,那么小李就无法再次检出同一文件,直到小张提交了他的改变。 

  

  建议你将乐观锁想成一种检测冲突的手段,而悲观锁是一种避免冲突的手段(严格来说,乐观锁其实不能称之为“锁”,但是这个名字已经流传开了,那就继续使用 吧)。一些老的版本控制系统,比如VSS 6.0使用的是悲观锁的机制。而现代的版本控制系统一般两种都支持,默认使用乐观锁。 
   
  两种锁各有优缺点。。。这段懒的翻译了,很明显看出,乐观锁可以提高并发访问的效率,但是如果出现了冲突只能向上抛出,然后重来一遍;悲观锁可以避免冲突的发生,但是会降低效率。 

  

  选择使用那一种锁取决于访问频率和一旦产生冲突的严重性。如果系统被并发访问的概率很低,或者冲突发生后的后果不太严重(所谓后果应该指被检测到冲突的提交会失败,必须重来一次),可以使用乐观锁,否则使用悲观锁。 

  

  【我再补充两句】 
  我们经常会在访问数据库的时候用到锁,怎么实现乐观锁和悲观锁呢?以Hibernate为例,可以通过为记录添加版本或时间戳字段来实现乐观锁。可以用 session.Lock()锁定对象来实现悲观锁(本质上就是执行了SELECT * FROM t FOR UPDATE语句)

原文地址:https://www.cnblogs.com/zhangtan/p/5803194.html