zoukankan      html  css  js  c++  java
  • 乐观锁和悲观锁的问题

    悲观锁与乐观锁是两种常见的资源并发锁设计思路,也是并发编程中一个非常基础的概念。本文将对这两种常见的锁机制在数据库数据上的实现进行比较系统的介绍。

    悲观锁(Pessimistic Lock)


    悲观锁的特点是先获取锁,再进行业务操作,即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的select … for update操作来实现悲观锁。当数据库执行select for update时会获取被select中的数据行的行锁,因此其他并发执行的select for update如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。

    这里需要注意的一点是不同的数据库对select for update的实现和支持都是有所区别的,例如oracle支持select for update no wait,表示如果拿不到锁立刻报错,而不是等待,mysql就没有no wait这个选项。另外mysql还有个问题是select for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在mysql中用悲观锁务必要确定走了索引,而不是全表扫描。

    乐观锁(Optimistic Lock)


    乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。

    乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。一般的做法是在需要锁的数据上增加一个版本号,或者时间戳,然后按照如下方式实现:

    复制代码
    1. SELECT data AS old_data, version AS old_version FROM …;
    2. 根据获取的数据进行业务操作,得到new_data和new_version
    3. UPDATE SET data = new_data, version = new_version WHERE version = old_version
    if (updated row > 0) {
        // 乐观锁获取成功,操作完成
    } else {
        // 乐观锁获取失败,回滚并重试
    }
    复制代码

    乐观锁是否在事务中其实都是无所谓的,其底层机制是这样:在数据库内部update同一行的时候是不允许并发的,即数据库每次执行一条update语句时会获取被update行的写锁,直到这一行被成功更新后才释放。因此在业务操作进行前获取需要锁的数据的当前版本号,然后实际更新数据时再次对比版本号确认与之前获取的相同,并更新版本号,即可确认这之间没有发生并发的修改。如果更新失败即可认为老版本的数据已经被并发修改掉而不存在了,此时认为获取锁失败,需要回滚整个业务操作并可根据需要重试整个过程。

    总结

    • 乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能

    • 乐观锁还适用于一些比较特殊的场景,例如在业务操作过程中无法和数据库保持连接等悲观锁无法适用的地方

    数据库高并发下乐观锁的原理

    在高并发下,经常需要处理SELECT之后,在业务层处理逻辑,再执行UPDATE的情况。

    若两个连接并发查询同一条数据,然后在执行一些逻辑判断或业务操作后,执行UPDATE,可能出现与预期不相符的结果。

    在不使用悲观锁与复杂SQL的前提下,可以使用乐观锁处理该问题,同时兼顾性能。

    场景模拟:

    假设一张表两个字段,一个id,一个use_count。
    表里存了100个id,每个id对应自己的use_count。

    当id每使用一次,use_count要加1。当use_count大于1000时,这个id就不能在被使用了(换句话说 无法从数据库中查出)。

    在高并发情况下,会遇到一种问题:假设数据表中有一条记录为:id=123456, use_count=999
    A与B两个连接并发查询这个id=123456,都执行下列SQL:

    1
    SELECT * FROM table WHERE id=123456 and use_count < 1000;

    A先执行,得到id=123456的use_count是999,之后在程序里做了一些逻辑判断或业务操作后执行SQL:

    1
    UPDATE table SET use_count + 1 WHERE id=123456;

    在A做判断且没有update之前,B也执行了查询SQL,发现use_count是999,之后它也会执行SQL:

    1
    UPDATE table SET use_count + 1 WHERE id=123456;

    但是,事实上B不应该取得这个id,因为A已经是第1000个使用者。

    处理步骤如下:

    1、添加第3个字段version,int类型,default值为0。version值每次update时作加1处理。

    1
    ALTER TABLE table ADD COLUMN version INT DEFAULT '0' NOT NULL AFTER use_count; 

    2、SELECT时同时获取version值(例如为3)。

    1
    SELECT use_count, version FROM table WHERE id=123456 AND use_count < 1000;

    3、UPDATE时检查version值是否为第2步获取到的值。

    1
    UPDATE table SET version=4, use_count=use_count+1 WHERE id=123456 AND version=3;

    如果UPDATE的记录数为1,则表示成功。
    如果UPDATE的记录数为0,则表示已经被其他连接UPDATE过了,需作异常处理。

  • 相关阅读:
    机械学习--5
    机械学习--4
    机械学习--3
    机械学习--2
    机器学习--1
    编译原理 作业十五
    编译原理 作业十四
    编译原理 作业十二
    编译原理 作业十一
    编译原理 作业十
  • 原文地址:https://www.cnblogs.com/ghc666/p/8657083.html
Copyright © 2011-2022 走看看