如何实现过期的非分布式锁?

时间:2014-12-01 16:48:03

标签: concurrency redis locking memcached concurrent-programming

意思是,他们不必分发。我正在考虑使用memcachedredis。可能是后者。我关心的是"我们必须释放一些内存,所以我们会在它过期之前删除这个键/值。"事情。但是,我也可以接受其他建议。

1 个答案:

答案 0 :(得分:0)

tl; dr 使用开发人员建议的ready-made solution

所以,我决定不将memcached用于此目的。因为它是一个缓存服务器。我没有看到一种方法来确保它不会删除我的密钥,因为它是内存不足。使用redis只要maxmemory-policy = noeviction就不会出现问题。

There are 3 links我想与您分享。它们基本上是我现在所知道的三种解决问题的方法。只要你有redis >= 2.6.0即可。

redis> = 2.6.12

如果您有redis >= 2.6.12,那么您很幸运,只需使用setnx命令及其新选项exnx

$redis->set($name, <whatever>, array('nx', 'ex' => $ttl));

但是,如果我们要允许关键部分花费的时间比我们预期的要长(>= ttl),我们最终不能只删除锁定。请考虑以下情况:

C1 acquires the lock
lock expires
C2 acquires the lock
C1 deletes C2's lock

为了不发生这种情况,我们将当前时间戳存储为锁的值。然后,知道Lua脚本是原子的(参见Atomicity of scripts):

$redis->eval('
    if redis.call("get", KEYS[1]) == KEYS[2] then
        redis.call("del", KEYS[1])
    end
', array($name, $now));

但是,两个客户端是否可能具有相等的now值?为此,上述所有操作都应在一秒钟内完成,ttl必须等于0

结果代码:

function get_redis() {
    static $redis;
    if ( ! $redis) {
        $redis = new Redis;
        $redis->connect('127.0.0.1');
    }
    return $redis;
}

function acquire_lock($name, $ttl) {
    if ( ! $ttl)
        return FALSE;
    $redis = get_redis();
    $now = time();
    $r = $redis->set($name, $now, array('nx', 'ex' => $ttl));
    if ( ! $r)
        return FALSE;
    $lock = new RedisLock($redis, $name, $now);
    register_shutdown_function(function() use ($lock) {
        $r = $lock->release();
        # if ( ! $r) {
            # Here we can log the fact that lock has expired too early
        # }
    });
    return $lock;
}

class RedisLock {
    var $redis;
    var $name;
    var $now;
    var $released;

    function __construct($redis, $name, $now) {
        $this->redis = get_redis();
        $this->name = $name;
        $this->now = $now;
    }

    function release() {
        if ($this->released)
            return TRUE;
        $r = $this->redis->eval('
            if redis.call("get", KEYS[1]) == KEYS[2] then
                redis.call("del", KEYS[1])
                return 1
            else
                return 0
            end
        ', array($this->name, $this->now));
        if ($r)
            $this->released = TRUE;
        return $r;
    }
}

$l1 = acquire_lock('l1', 4);
var_dump($l1 ? date('H:i:s', $l1->expires_at) : FALSE);

sleep(2);

$l2 = acquire_lock('l1', 4);
var_dump($l2 ? date('H:i:s', $l2->expires_at) : FALSE);   # FALSE

sleep(4);

$l3 = acquire_lock('l1', 4);
var_dump($l3 ? date('H:i:s', $l3->expires_at) : FALSE);

到期

我找到的另一个解决方案here。您只需使用expire命令使值过期:

$redis->eval('
    local r = redis.call("setnx", ARGV[1], ARGV[2])
    if r == 1 then
        redis.call("expire", ARGV[1], ARGV[3])
    end
', array($name, $now, $ttl));

因此,只有acquire_lock函数更改:

function acquire_lock($name, $ttl) {
    if ( ! $ttl)
        return FALSE;
    $redis = get_redis();
    $now = time();
    $r = $redis->eval('
        local r = redis.call("setnx", ARGV[1], ARGV[2])
        if r == 1 then
            redis.call("expire", ARGV[1], ARGV[3])
        end
        return r
    ', array($name, $now, $ttl));
    if ( ! $r)
        return FALSE;
    $lock = new RedisLock($redis, $name, $now);
    register_shutdown_function(function() use ($lock) {
        $r = $lock->release();
        # if ( ! $r) {
            # Here we can log that lock as expired too early
        # }
    });
    return $lock;
}

GETSET

最后一个在documentation中再次描述。标有“留下历史原因”的说明。

这次我们存储锁定到期时刻的时间戳。我们用setnx命令存储它。如果成功,我们就获得了锁定。否则,其他人持有锁,或锁已过期。不管是后者,我们使用getset来设置新值,如果旧值没有改变,我们就获得了锁:

$r = $redis->setnx($name, $expires_at);
if ( ! $r) {
    $cur_expires_at = $redis->get($name);
    if ($cur_expires_at > time())
        return FALSE;
    $cur_expires_at_2 = $redis->getset($name, $expires_at);
    if ($cur_expires_at_2 != $cur_expires_at) 
        return FALSE;
}

让我感到不舒服的是,我们似乎改变了别人的expires_at价值,不是吗?

在旁注中,您可以查看以这种方式使用的redis

function get_redis_version() {
    static $redis_version;
    if ( ! $redis_version) {
        $redis = get_redis();
        $info = $redis->info();
        $redis_version = $info['redis_version'];
    }
    return $redis_version;
}

if (version_compare(get_redis_version(), '2.6.12') >= 0) {
    ...
}

一些调试功能:

function redis_var_dump($keys) {
    foreach (get_redis()->keys($keys) as $key) {
        $ttl = get_redis()->ttl($key);
        printf("%s: %s%s%s", $key, get_redis()->get($key),
            $ttl >= 0 ? sprintf(" (ttl: %s)", $ttl) : '',
            nl());
    }
}

function nl() {
    return PHP_SAPI == 'cli' ? "\n" : '<br>';
}