资讯专栏INFORMATION COLUMN

用分布式锁解决并发问题

Brenner / 2922人阅读

摘要:用的方法做分布式锁这个方案的背景主要是在和的方案上针对可能存在的死锁问题,做了一些优化。下面是用代码实现的分布式锁,关于部分使用的是伪代码,请根据自己的情况用连接对象替代其中的伪代码。

在系统中,当存在多个进程和线程可以改变某个共享数据时,就容易出现并发问题导致共享数据的不一致性。即多个进程同时获取到了对数据的操作权限并对数据进行了更新,很典型的场景就是在线销售系统在售卖热销商品时遇到多个并发请求在同一时间提交订单的情况则极有可能造成商品超卖的现象。只要访问流量不错的系统都有可能遭遇并发请求造成数据库中数据重复写入的情况。

针对程序块被多个进程并发执行问题的解决方案是确保同一个时刻同一个程序块只能有一个进程可执行,其他进程等待当前进程执行完成才能获取程序块的执行权对数据进行更新,以此类推将并发执行变为串行顺序执行。为了让获取执行权的进程不被其他干扰,就需要设置一个所有进程都能读取到的标记,当标记不存在时可以设置该标记,其余后续进程发现已经有标记了则等待拥有标记的进程结束执行程序块取消标记后再去尝试设置标记。这个标记可以理解为锁,设置标记的过程就是我们通常说的加锁。

用redis 的 setnx、expire 方法做分布式锁

setnx()

setnx 的含义就是 SET if Not Exists,其主要有两个参数 setnx(key, value)。该方法是原子的,如果 key 不存在,则设置当前 key 成功,返回 1;如果当前 key 已经存在,则设置当前 key 失败,返回 0。

expire()

expire 设置过期时间,要注意的是 setnx 命令不能设置 key 的超时时间,只能通过 expire() 来对 key 设置。

具体步骤

1、setnx(lockKey, 1) 如果返回 0,则说明占位失败;如果返回 1,则说明占位成功

2、expire() 命令对 lockKey 设置超时时间,为的是避免死锁问题。

3、执行完业务代码后,可以通过 delete 命令删除 key。

这个方案其实是可以解决日常工作中的需求的,但从技术方案的探讨上来说,可能还有一些可以完善的地方。比如,如果在第一步 setnx 执行成功后,在 expire() 命令执行成功前,发生了宕机的现象,那么就依然会出现死锁的问题,所以如果要对其进行完善的话,可以使用 redis 的 setnx()、get() 和 getset() 方法来实现分布式锁。

用 redis 的 setnx()、get()、getset()方法做分布式锁

这个方案的背景主要是在 setnx() 和 expire() 的方案上针对可能存在的死锁问题,做了一些优化。

getset()

这个命令主要有两个参数 getset(key,newValue)。该方法是原子的,对 key 设置 newValue 这个值,并且返回 key 原来的旧值。假设 key 原来是不存在的,那么多次执行这个命令,会出现下边的效果:

getset(key, "value1") 返回 null 此时 key 的值会被设置为 value1

getset(key, "value2") 返回 value1 此时 key 的值会被设置为 value2

依次类推!

使用步骤

setnx(lockKey, 当前时间+过期超时时间),如果返回 1,则获取锁成功;如果返回 0 则没有获取到锁,转到步骤 2。

get(lockKey) 获取值,值是当前lockKey的过期时间用oldExpireTime代表 ,并将这个 oldExpireTime与当前的系统时间进行比较,如果早于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向 步骤3,否则等待指定时间后返回步骤2重新开始判定。

计算 newExpireTime = 当前时间+过期超时时间,然后 getset(lockKey, newExpireTime) 会返回当前 lockKey 之前设置的旧值currentExpireTime。

判断 currentExpireTime 与 oldExpireTime 是否相等,如果相等,说明当前进程getset 设置锁成功,获取到了锁。如果不相等,说明这个锁已经被别的进程获取走了,那么当前请求可以根据具体需求逻辑直接返回失败,或者返回步骤2继续重试。

在获取到锁之后,当前进程可以开始自己的业务处理,当处理完毕后,比较当前理时间和对锁设置的超时时间,如果小于锁设置的超时时间,则直接执行 delete 释放锁;如果大于锁设置的超时时间,锁可能已由其他进程获得,这时执行 delete释放锁的操作会导致把其他进程已获得的锁释放掉。

下面是用PHP代码实现的Redis分布式锁,关于Redis部分使用的是伪代码,请根据自己的情况用Redis连接对象替代其中的伪代码。

/**
 * 获取Redis分布式锁
 *
 * @param $lockKey
 * @return bool
 */
function getRedisDistributedLock(string $lockKey) : bool
{
    $lockTimeout = 2000;// 锁的超时时间2000毫秒
    $now = intval(microtime(true) * 1000);
    $lockExpireTime = $now + $lockTimeout;
    $lockResult = Redis::setnx($lockKey, $lockExpireTime);

    if ($lockResult) {
        // 当前进程设置锁成功
        return true;
    } else {
        $oldLockExpireTime = Redis::get($lockKey);
        if ($now > $oldLockExpireTime && $oldLockExpireTime == Redis::getset($lockKey, $lockExpireTime)) {
            return true;
        }
    }

    return false;
}

/**
 * 串行执行程序
 *
 * @param string $lockKey Key for lock
 * @param Closure $closure 获得锁后进程要执行的闭包
 * @return mixed
 */
function serialProcessing(string $lockKey, Closure $closure)
{
    if (getRedisDistributedLock($lockKey)) {
        $result = $closure();
        $now = intval(microtime(true) * 1000);
        if ($now < Redis::get($lockKey)) {
            Redis::del($lockKey);   
        }
    } else {
        // 延迟200毫秒再执行
        usleep(200 * 1000);
        return serialProcessing($lockKey, $closure);
    }

    return $result;
}

上面serialProcessing方法里当前进程设置锁成功,获取了代码块的执行权后就会执行闭包参数$closure里的代码块,通过传递闭包给方法,让我们可以在项目任何需要确保程序串行执行的地方使用serialProcessing方法给程序加分布式锁解决并发请求的问题。

上面代码实现用面向过程的方式是为了能简单明了的描述怎么设置分布式锁,读者可以针对自己的情况执行设计实现代码。针对于大型系统使用集群Redis的情况,设置分布式锁的步骤更复杂,有兴趣的可以查看Redlock 算法和redissonredis分布式锁组件。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/31005.html

相关文章

  • 布式幂等问题解决方案三部曲

    摘要:解决幂等问题的三部曲,也是作者的思考框架。这是解决幂等问题的第二部曲列出并减少副作用的分析维度。所以在并发执行的维度,将并发重复执行变成串行重复执行是最好的幂等解决方案。 纲要 文章目的:本文旨在提炼一套分布式幂等问题的思考框架,而非解决某个具体的分布式幂等问题。在这个框架体系内,会有一些方案举例说明。文章目标:希望读者能通过这套思考框架设计出符合自己业务的完备的幂等解决方案。文章内容...

    mumumu 评论0 收藏0
  • Redis布式解决抢购问题

    摘要:废话不多说,首先分享一个业务场景抢购。下面就是分布式锁的解决方法。首先要加入的依赖,该类只有两个功能,加锁和解锁,解锁比较简单,就是删除当前的键值对。这时继续执行,由于所以该线程获取到锁。 废话不多说,首先分享一个业务场景-抢购。一个典型的高并发问题,所需的最关键字段就是库存,在高并发的情况下每次都去数据库查询显然是不合适的,因此把库存信息存入Redis中,利用redis的锁机制来控制...

    taoszu 评论0 收藏0
  • Akka系列(六):Actor解决了什么问题

    摘要:原文链接解决了什么问题使用模型来克服传统面向对象编程模型的局限性,并应对高并发分布式系统所带来的挑战。在某些情况,这个问题可能会变得更糟糕,工作线程发生了错误但是其自身却无法恢复。 这段时间由于忙毕业前前后后的事情,拖更了很久,表示非常抱歉,回归后的第一篇文章主要是看到了Akka最新文档中写的What problems does the actor model solve?,阅读完后觉...

    Carson 评论0 收藏0
  • 浅谈Java并发编程系列(一)—— 如何保证线程安全

    摘要:比如需要用多线程或分布式集群统计一堆用户的相关统计值,由于用户的统计值是共享数据,因此需要保证线程安全。如果类是无状态的,那它永远是线程安全的。参考探索并发编程二写线程安全的代码 线程安全类 保证类线程安全的措施: 不共享线程间的变量; 设置属性变量为不可变变量; 每个共享的可变变量都使用一个确定的锁保护; 保证线程安全的思路: 1. 通过架构设计 通过上层的架构设计和业务分析来避...

    mylxsw 评论0 收藏0
  • 为Java程序员金三银四精心挑选的300余道Java面试题与答案

    摘要:为程序员金三银四精心挑选的余道面试题与答案,欢迎大家向我推荐你在面试过程中遇到的问题我会把大家推荐的问题添加到下面的常用面试题清单中供大家参考。 为Java程序员金三银四精心挑选的300余道Java面试题与答案,欢迎大家向我推荐你在面试过程中遇到的问题,我会把大家推荐的问题添加到下面的常用面试题清单中供大家参考。 前两天写的以下博客,大家比较认可,热度不错,希望可以帮到准备或者正在参加...

    tomorrowwu 评论0 收藏0

发表评论

0条评论

Brenner

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<