Redis-分布式锁(PHP实现)

Redis实现分布式锁

运用场景(例如解决库存超卖问题)

库存超卖现象是怎么产生的?

先来看看如果不用分布式锁,所谓的电商库存超卖是啥意思?大家看看下面的图:
在这里插入图片描述
这个图,其实很清晰了,假设订单系统部署两台机器上,不同的用户都要同时买10台iphone,分别发了一个请求给订单系统。

接着每个订单系统实例都去数据库里查了一下,当前iphone库存是12台。

俩大兄弟一看,乐了,12台库存大于了要买的10台数量啊!

于是乎,每个订单系统实例都发送SQL到数据库里下单,然后扣减了10个库存,其中一个将库存从12台扣减为2台,另外一个将库存从2台扣减为-8台。

现在完了,库存出现了负数!泪奔啊,没有20台iphone发给两个用户啊!这可如何是好。

用分布式锁如何解决库存超卖问题?

一个锁key,同一时间只能有一个客户端拿到锁,其他客户端会陷入无限的等待来尝试获取那个锁,只有获取到锁的客户端才能执行下面的业务逻辑。
在这里插入图片描述
大家可以顺着上面的那个步骤序号看一遍,马上就明白了。

从上图可以看到,只有一个订单系统实例可以成功加分布式锁,然后只有他一个实例可以查库存、判断库存是否充足、下单扣减库存,接着释放锁。

释放锁之后,另外一个订单系统实例才能加锁,接着查库存,一下发现库存只有2台了,库存不足,无法购买,下单失败。不会将库存扣减为-8的。

有没有其他方案可以解决库存超卖问题?

当然有啊!比如悲观锁,分布式锁,乐观锁,队列串行化,异步队列分散,Redis原子操作,等等,很多方案,我们对库存超卖有自己的一整套优化机制。

但是前面说过了,这篇文章就聊一个分布式锁的并发优化,不是聊库存超卖的解决方案,所以库存超卖只是一个业务场景而已。

以后有机会笔者会写一篇文章,讲讲电商库存超卖问题的解决方案,这篇文章先focus在一个分布式锁并发优化上,希望大家明白这个用意和背景,避免有的兄弟没看清楚又吐槽。

而且建议大家即使对文章里的内容有异议,公众号后台给我留言跟我讨论一下,技术,就是要多交流,打开思路,碰撞思维。

分布式锁应该具备哪些条件:

1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。

PHP实现Redis分布式锁

class RedisMutexLock
{
    
    
    /**
     * 缓存 Redis 连接。
     *
     * @return void
     */
    public static function getRedis()
    {
    
    
        // 这行代码请根据自己项目替换为自己的获取 Redis 连接。
        return YCache::getRedisClient();
    }

    /**
     * 获得锁,如果锁被占用,阻塞,直到获得锁或者超时。
     * -- 1、如果 $timeout 参数为 0,则立即返回锁。
     * -- 2、建议 timeout 设置为 0,避免 redis 因为阻塞导致性能下降。请根据实际需求进行设置。
     *
     * @param  string  $key         缓存KEY。
     * @param  int     $timeout     取锁超时时间。单位(秒)。等于0,如果当前锁被占用,则立即返回失败。如果大于0,则反复尝试获取锁直到达到该超时时间。
     * @param  int     $lockSecond  锁定时间。单位(秒)。
     * @param  int     $sleep       取锁间隔时间。单位(微秒)。当锁为占用状态时。每隔多久尝试去取锁。默认 0.1 秒一次取锁。
     * @return bool 成功:true、失败:false
     */
    public static function lock($key, $timeout = 0, $lockSecond = 20, $sleep = 100000)
    {
    
    
        if (strlen($key) === 0) {
    
    
            // 请更换为自己项目抛异常的方法。
            YCore::exception(500, '缓存KEY没有设置');
        }
        if (!is_int($timeout) || $timeout < 0) {
    
    
            YCore::exception(500, "timeout 参数设置有误");
        }
        $start = self::getMicroTime();
        $redis = self::getRedis();
        do {
    
    
            // [1] 锁的 KEY 不存在时设置其值并把过期时间设置为指定的时间。锁的值并不重要。重要的是利用 Redis 的特性。
            $acquired = $redis->set("Lock:{
      
      $key}", 1, ['NX', 'EX' => $lockSecond]);
            if ($acquired) {
    
    
                break;
            }
            if ($timeout === 0) {
    
    
                break;
            }
            usleep($sleep);
        } while ((self::getMicroTime()) < ($start + ($timeout * 1000000)));
        return $acquired ? true : false;
    }

    /**
     * 释放锁
     *
     * @param  mixed  $key  被加锁的KEY。
     * @return void
     */
    public static function release($key)
    {
    
    
        if (strlen($key) === 0) {
    
    
            // 请更换为自己项目抛异常的方法。
            YCore::exception(500, '缓存KEY没有设置');
        }
        $redis = self::getRedis();
        $redis->del("Lock:{
      
      $key}");
    }

    /**
     * 获取当前微秒。
     *
     * @return bigint
     */
    protected static function getMicroTime()
    {
    
    
        return bcmul(microtime(true), 1000000);
    }
}

内容参考网上文章整理,仅限个人学习使用:

  1. https://zhuanlan.zhihu.com/p/95217001
  2. https://www.cnblogs.com/liuqingzheng/p/11080501.html
  3. https://blog.csdn.net/C18298182575/article/details/92786579

猜你喜欢

转载自blog.csdn.net/magentodaddy/article/details/108538672
今日推荐