1.1.1. 02.分布式锁


原子性:是运行过程中的最小单位。 原子操作:指在不会被线程调度机制打断的操作。这种操作一旦开始,会一直运行到结束,不会被打断。

分布式锁:很常见的例子是,通过某个接口查询数据库,由于访问量大,一般都会加一层缓存,并且加上过期时间。但是这里有个问题是,当缓存过期的瞬间,会有大量的请求穿透去数据库查询,导致宕机。而分布式锁就可以解决这个问题。

分布式锁至少需要满足几个条件:

  1. 互斥,在任何时刻,同一个锁只能由一个客户端用户锁定。
  2. 不会死锁,就算持有锁的客户端在持有期间崩溃了,也不影响其他客户端用户加锁。
  3. 谁加锁谁解锁。就是解锁要验证客户端身份,不能被其他客户端解锁。

错误的方式

1.通常会以这一的方式来加锁
>setnx lock-key true
OK

... do something ...

>del lock-key
OK

这里会有个问题,如果在加锁后,执行 del 之前服务器挂了,那么锁就不会被删除,会导致死锁。

好,那么加一个过期时间怎么样。看下面这个例子

2.加过期时间

在上个例子中,加上过期时间。

>setnx lock-key true
OK
# 加上过期时间
expire lock-key 5 
... do something ...

>del lock-key
OK

加上过期时间后应该就不会死锁了吧。但是,如果在 setnx 和 expire 之间,服务器挂了呢,同样也会造成死锁这个问题。 发现问题了吧,原因是 setnx 和 expire 这两个操作是两个命令而不是一个原子操作。不过好在 Redis 官方给了个解决方案,使用 set 命令。

3.使用 set 命令

从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改: set 命令:SET key value [EX|PX] [NX|XX]

  • EX|PX :过期时间,单位秒/毫秒。
  • NX:只有键不存在,才对键进行操作。 SET key value NX 效果等同于 SETNX key value 。
  • XX:只有键存在,才对键进行操作。

那么我们使用 set 来修改上面的列子。

>set lock-key true EX 5 NX
OK

... do something ...

>del lock-key
OK
4.超时问题

在上面例子中,我们解决了加锁问题,但是还有一些问题没有解决。如果客户端 A 持有锁过期了,但是它的临界区的逻辑没有执行完,客户端 B 提前持有了锁,导致代码代码无法严格串行执行下去。 这里产生了两个问题:

  1. 验证锁的所有者
  2. 删除锁

很遗憾,Redis 中没有「验证锁」同时「删除锁」的原子性操作。不过可以使用 lua 脚本来实现。

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
PHP 实现
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;


class DemoController extends Controller
{
    public function demo()
    {
        $redis = new \Predis\Client();

        $key = 'lock1';
        $requireId = 1;
        $ret = self::tryGetLock($redis, $key, $requireId, 1000);
        $ret2 = self::releaseLock($redis, $key, $requireId);
        dd($ret2);
    }

    const LOCK_SUCCESS = 'OK'; // 结果
    const IF_NOT_EXIST = 'NX'; // 如果不存在则创建
    const MILLISECONDS_EXPIRE_TIME = 'PX'; // 以毫秒为单位
    const RELEASE_SUCCESS = 1; // 释放成功

    /**
     * 尝试获取锁
     * @param \Predis\Client $redis redis客户端
     * @param String $key 锁
     * @param String $requestId 请求id
     * @param int $expireTime 过期时间
     * @return bool                     是否获取成功
     */
    public static function tryGetLock(\Predis\Client $redis, String $key, String $requireId, int $expireTime)
    {
        $result = $redis->set($key, $requireId, self::MILLISECONDS_EXPIRE_TIME, $expireTime, self::IF_NOT_EXIST);
        return (String)$result === self::LOCK_SUCCESS;
    }

    /**
     * @param \Predis\Client $redis redis客户端
     * @param String $key 锁
     * @param String $requireId 请求 id
     * @return bool
     */
    public static function releaseLock(\Predis\Client $redis, String $key, String $requireId)
    {
        // lua 脚本
        $lua = <<<'LUA'
        if redis.call('get', KEYS[1]) == ARGV[1] then 
            return redis.call('del', KEYS[1]) 
        else 
            return 0 
        end
LUA;
        $result = $redis->eval($lua, 1, $key, $requireId);
        return self::RELEASE_SUCCESS === $result;
    }
}

可重入性

可重入性:是指线程在持有锁的情况下再次请求加锁, 如果一个锁支持同一个线程的多次加锁,那么这个锁就是可重入的。

暂时没体会到有什么作用,等以后了解用法后再更新。

应用

  • 在 VRM 中,修改仓库设置,用到了 redis 分布式锁。防止多人同时修改仓库设置。
Copyright © Kagami丶 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-12-10 21:45:40

results matching ""

    No results matching ""