本文由 简悦 SimpRead) 转码, 原文地址 cloud.tencent.com)

Redis 分布式锁的作用

在单机环境下,有个秒杀商品的活动,在短时间内,服务器压力和流量会陡然上升。这个就会存在并发的问题。想要解决并发需要解决一下问题

1、提高系统吞吐率也就是 qps 每秒处理的请求书

解决问题一:采用内存型数据库提高系统的 qps

解决问题二:就要用到经常会遇到的锁,例如 MySQL 有读锁、写锁、排他锁、悲观锁、乐观锁。不过这里只讨论 redis 来实现锁

简单版设置锁

  1. $redis = new Redis();
  2. $redis->connect('127.0.0.1', 6379); //连接Redis
  3. $expire = 10;//有效期10秒
  4. $key = 'lock';//key
  5. $value = time() + $expire;//锁的值 = Unix时间戳 + 锁的有效期
  6. $lock = $redis->setnx($key, $value);
  7. //判断是否上锁成功,成功则执行下步操作
  8. if(!empty($lock))
  9. {
  10. //下步操作...
  11. }

如果以这样的简单版设置锁就能解决所有问题,未免也太小看 锁 在程序中应用了。

按正常的操作示例基本上都是这样写的。但是这样写有一些问题

1、假如有 10000 个请求访问了 redis 不存在的键,这样请求就是指接到了 MySQL 数据,造成 CPU 短时间内达到 100% 甚至宕机。这样场景俗称缓存击穿造成的缓存雪崩。

reids setnx 方法的作用是,当设置的 key 不存在时,设置新的值。这样就避免了缓存击穿的问题。检测键的过期时间,避免产生死锁

解决死锁问题

$expire = 10;//有效期10秒
    $key = 'lock';//key
    $value = time() + $expire;//锁的值 = Unix时间戳 + 锁的有效期
    $status = true;
    while($status)
    {
        $lock = $redis->setnx($key, $value);
        if(empty($lock))
        {
            $value = $redis->get($key);
            if($value < time())
            {
                $redis->del($key);
            }
        }else{
            $status = false;
            //下步操作....
        }
    }

2、分布式集群业务业务场景下,每台服务器是独立存在的。多台服务器怎么通过一个标识来相互竞争锁呢。这里就用到了分布式锁

这里简单介绍一下,以 MYSQL 的事务机制来延生。事务四个特性 ACID,有四种隔离级别:为提交读、已提交读、可重复读、串行化。这些特性都只在单台服务器上生效。到了分布式集群了,数据在不同的服务器上,紧靠事务很难保持数据的一致性及隔离性,事务的作用就意义不大了。Redis 也是如此。

正确的分布式锁的打开方式

  /**
     * 实现Redis分布锁
     */
    $key        = 'demo';       //要更新信息的缓存KEY
    $lockKey    = 'lock:'.$key; //设置锁KEY
    $lockExpire = 10;           //设置锁的有效期为10秒

    //获取缓存信息
    $result = $redis->get($key);
    //判断缓存中是否有数据
    if(empty($result))
    {
        $status = TRUE;
        while ($status)
        {
            //设置锁值为当前时间戳 + 有效期
            $lockValue = time() + $lockExpire;
            /**
             * 创建锁
             * 试图以$lockKey为key创建一个缓存,value值为当前时间戳
             * 由于setnx()函数只有在不存在当前key的缓存时才会创建成功
             * 所以,用此函数就可以判断当前执行的操作是否已经有其他进程在执行了
             * @var [type]
             */
            $lock = $redis->setnx($lockKey, $lockValue);
            /**
             * 满足两个条件中的一个即可进行操作
             * 1、上面一步创建锁成功;
             * 2、   1)判断锁的值(时间戳)是否小于当前时间    $redis->get()
             *      2)同时给锁设置新值成功    $redis->getset()
             */
            if(!empty($lock) || ($redis->get($lockKey) < time() && $redis->getSet($lockKey, $lockValue) < time() ))
            {
                //给锁设置生存时间
                $redis->expire($lockKey, $lockExpire);
                //******************************
                //此处执行插入、更新缓存操作...
                //******************************

                //以上程序走完删除锁
                //检测锁是否过期,过期锁没必要删除
                if($redis->ttl($lockKey))
                    $redis->del($lockKey);
                $status = FALSE;
            }else{
                /**
                 * 如果存在有效锁这里做相应处理
                 *      等待当前操作完成再执行此次请求
                 *      直接返回
                 */
                sleep(2);//等待2秒后再尝试执行操作
            }
        }
    }