Java中的锁(3):悲观锁与乐观锁 - Go语言中文社区

Java中的锁(3):悲观锁与乐观锁


主线:数据库的读现象(脏读、不可重复读、幻读)--->并发控制--->数据库的锁机制(悲观锁与乐观锁)--->事物隔离

提示:以下内容均来自网络学习总结。

1、并发控制

       在并发访问情况下,可能会出现脏读、不可重复读和幻读等读现象。为了应对这样的问题,目前主流数据库都提供了锁机制,和事务隔离级别的概念。

(1)并发控制是什么?
    并发控制是确保及时纠正由并发操作导致的错误的一种机制。

(2)并发控制能干什么?
       在数据库管系统理(DBMS)中,并发控制的任务是确保多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。
    
(3)并发控制主要采用的技术手段有哪些?
    锁、时间戳、乐观并发控制(乐观锁)和悲观并发控制(悲观锁)。

2、MySql中的锁

 (1)MySQL中的行级锁,表级锁,页级锁:http://www.hollischuang.com/archives/914
 (2)MySQL中的共享锁与排他锁:http://www.hollischuang.com/archives/923
 (3)深入理解乐观锁与悲观锁:http://www.hollischuang.com/archives/934

3、悲观锁 

       悲观锁与乐观锁是并发控制主要采用的技术手段。
       针对于不同的业务场景,应该选用不同的并发控制方式。所以,不要把乐观并发控制和悲观并发控制狭义的理解为DBMS中的概念,更不要把他们和数据中提供的锁机制(行锁、表锁、排他锁、共享锁)混为一谈。其实,在DBMS中,悲观锁正是利用数据库本身提供的锁机制来实现的。
    
(1)什么是悲观锁(Pessimistic Concurrency Control,“PCC”)?
      悲观锁,是指对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改保持保守态度,即悲观。因此,在整个数据处理过程中,将数据处于锁状态。
      总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁、行锁、表锁),当其他线程想要访问数据时,都需要阻塞挂起。可以依靠数据库实现,如行锁、读锁和写锁等,都是在操作之前加锁。在Java中,synchronized的思想也是悲观锁。
      悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排它性;否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

(2)如何在数据中实现悲观锁?

    在数据库中,悲观锁的流程:
    a、在对任意记录进行修改前,先尝试对该记录加上排它锁。
    b、如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者跑出异常。具体响应方式由开发者根据实际需求决定。
    c、如果加锁成功,那么就可以对记录进行修改,事务提交后就会解锁了。
    d、期间,如果有其他对该记录做修改或加排它锁的操作,都会等待解锁或直接跑出异常。
    
    注:要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,即要设置“autocommit=0;”,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。
    
    设置完autocommit后,就可以执行我们的正常业务了。具体如下:    

//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;


    注:上面的begin/commit为事务的开始和结束,因为在前一步关闭了mysql的autocommit,所以需要手动控制事务的提交。
    
      上面的查询语句中,使用了select…for update的方式,这样就通过开启排他锁的方式实现了悲观锁。此时在t_goods表中,id为1的那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样就可以保证当前的数据不会被其它事务修改。
    
      补充:MySQL select…for update的Row Lock与Table Lock:
     上面提到,使用select…for update会把数据给锁住,不过需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据),否则MySQL将会执行Table Lock (将整个数据表单给锁住)。
      这一点,可参考:mysql悲观锁总结和实践:http://chenzhou123520.iteye.com/blog/1860954

       注意:在事务中,只有执行SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE 同一笔数据时会等待其它事务结束后才执行,一般SELECT ... 则不受此影响。
      拿上面的实例来说,当执行了select status from t_goods where id=1 for update;后。在另外的事务中如果再次执行select status from t_goods where id=1 for update;则第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,但是如果在第二个事务中执行select status from t_goods where id=1;则能正常查询出数据,不会受第一个事务的影响。
        关于SELECT ... LOCK IN SHARE MODE和SELECT ... FOR UPDATE链接地址

(3)select ... for update用法
    (a)、它有什么作用?
    select ... for update 是为了在查询时,避免其他用户以该表进行插入,修改或删除等操作,造成表的不一致性。 
    
    (b)、举几个例子: 

    select * from t for update 会等待行锁释放之后,返回查询结果。 
    select * from t for update nowait 不等待行锁释放,提示锁冲突,不返回结果 
    select * from t for update wait 5 等待5秒,若行锁仍未释放,则提示锁冲突,不返回结果 
    select * from t for update skip locked 查询返回查询结果,但忽略有行锁的记录

    【使用格式】 
    SELECT…FOR UPDATE 语句的语法如下: 
      SELECT … FOR UPDATE [OF column_list][WAIT n|NOWAIT][SKIP LOCKED]; 
    其中: 
      OF 子句用于指定即将更新的列,即锁定行上的特定列。 
      WAIT 子句指定等待其他用户释放锁的秒数,防止无限期的等待。

    (c)、使用FOR UPDATE WAIT子句的优点如下: 
      1、防止无限期地等待被锁定的行。 
      2、允许应用程序中对锁的等待时间进行更多的控制。 
      3、对于交互式应用程序非常有用,因为这些用户不能等待不确定。 
      4 、若使用了skip locked,则可以越过锁定的行,不会报告由wait n 引发的‘资源忙’异常报告。

4、乐观锁    

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

       因此,可以把一个乐观锁控制的事务分为三个阶段:数据读取、写入校验、数据写入。其中,写入校验是整个乐观锁控制的关键所在。在写入校验阶段,事务会检查数据在读取阶段是否有其他事务对数据进行过更新,以确保数据更新的一致性。

      总是认为不会产生并发问题,当读取数据的时候总是认为不会有其他线程对该数据进行修改,因此不会上锁,但是在更新这条数据的时候就会判断其他线程在此期间是否对该数据修改了。乐观锁,一般使用数据版本号CAS操作实现。

     version方式:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

    核心SQL代码:

update table set x=x+1, version=version+1 where id=#{id} and version=#{version};

     CAS操作方式:即compare and swap 或者 compare and set,涉及到三个操作数,数据所在的内存值,预期值,新值。当需要更新时,判断当前内存值与之前取到的值是否相等,若相等,则用新值更新,若失败则重试,一般情况下是一个自旋操作,即不断的重试。

      version方式一:使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。用下面的一张图来说明:


       如上图所示,如果更新操作顺序执行,则数据的版本(version)依次递增,不会产生冲突。但是如果发生有不同的业务操作对同一版本的数据进行修改,那么,先提交的操作(图中B)会把数据version更新为2,当A在B之后提交更新时发现数据的version已经被修改了,那么A的更新操作会失败。

    version方式二:乐观锁定的第二种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

(2)如何在数据中实现乐观锁?
    
     相对于悲观锁,在对数据库进行处理的时候,乐观锁并不会使用数据库提供的锁机制。一般的实现乐观锁的方式就是记录数据版本。  实现数据版本有两种方式,第一种是使用版本号,第二种是使用时间戳。

      数据版本,为数据增加的一个版本标识。当读取数据时,将版本标识的值一同读出,数据每更新一次,同时对版本标识进行更新。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的版本标识进行比对, 如果数据库表当前版本号与第一次取出来的版本标识值相等,则予以更新,否则认为是过期数据。
    
    以MySQL InnoDB为例:
    商品goods表中有一个字段status,status为1代表商品未被下单,status为2代表商品已经被下单,那么我们对某个商品下单时必须确保该商品status为1。假设商品的id为1。

    下单操作包括3步骤:

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};

更详细解释:mysql乐观锁总结和实践:http://chenzhou123520.iteye.com/blog/1863407

5、乐观锁与悲观锁使用场景

   悲观锁:比较适合写入操作比较频繁的场景,如果出现大量的读取操作,每次读取的时候都会进行加锁,这样会增加大量的锁的开销,降低了系统的吞吐量。

   乐观锁:比较适合读取操作比较频繁的场景,如果出现大量的写入操作,数据发生冲突的可能性就会增大,为了保证数据的一致性,应用层需要不断的重新获取数据,这样会增加大量的查询操作,降低了系统的吞吐量。

    总结:两种所各有优缺点,读取频繁使用乐观锁,写入频繁使用悲观锁。

参考:

select for update用法: https://blog.csdn.net/sinat_27933301/article/details/79186837

悲观锁与乐观锁:http://www.digpage.com/lock.html#lock

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/jingzi123456789/article/details/81511956
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-13 12:02:51
  • 阅读 ( 1568 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢