Talk about SETNX for Redis

In Redis, the so-called SETNX is the abbreviation of " SET if Not e Xists ", that is, it is only set when it does not exist, and it can be used to achieve the effect of locking, but many people do not realize that SETNX has traps!

 

For example, an interface for querying a database has a large amount of calls, so a cache is added, and the cache is set to be refreshed after it expires. The problem is that when the amount of concurrency is large, if there is no lock mechanism, the moment the cache expires, A large number of concurrent requests will directly query the database through the cache, causing an avalanche effect. If there is a lock mechanism, then only one request can be controlled to update the cache, and other requests will either wait or use the expired cache depending on the situation.

Let's take the most popular PHPRedis extension in the PHP community as an example to implement a demo code:

<?php

$ok = $redis->setNX($key, $value);

if ($ok) {
    $cache->update();
    $redis->del($key);
}

?>

When the cache expires, the lock is acquired through SetNX, if successful, the cache is updated, and then the lock is deleted. It seems that the logic is very simple, but unfortunately there is a problem: if the request execution exits unexpectedly for some reason, resulting in a lock created but not deleted, the lock will always exist, so that the cache will never be updated in the future. So we need to add an expiration time to the lock to prevent accidents:

<?php

$redis->multi();
$redis->setNX($key, $value);
$redis->expire($key, $ttl);
$redis->exec();

?>

Because SetNX does not have the function of setting the expiration time, we need to use Expire to set it. At the same time, we need to wrap the two with Multi / Exec to ensure the atomicity of the request, so that SetNX succeeds Expire but fails. Unfortunately, there is still a problem: when multiple requests arrive, although only one of the requested SetNX can succeed, the Expire of any request can succeed, which means that even if the lock cannot be obtained, the expiration time can be refreshed. If the request If it is denser, the expiration time will always be refreshed, resulting in the lock being valid all the time. So we need to conditionally execute Expire while ensuring atomicity, and then we have the following Lua code:

local key   = KEYS[1]
local value = KEYS[2]
local ttl   = KEYS[3]

local ok = redis.call('setnx', key, value)
 
if ok == 1 then
  redis.call('expire', key, ttl)
end
 
return ok

I didn't expect to use Lua scripts to implement a seemingly simple function, which is really troublesome. In fact, Redis has taken everyone's suffering into account. Since 2.6.12, SET has  covered the function of SETEX, and SET itself has already included the function of setting the expiration time. That is to say, the functions we need in front of us can only be achieved with SET.

<?php

$ok = $redis->set($key, $value, array('nx', 'ex' => $ttl));

if ($ok) {
    $cache->update();
    $redis->del($key);
}

?>

Is the above code perfect? The answer is a little bit! Imagine that if a request takes a long time to update the cache, even longer than the validity period of the lock, the lock becomes invalid during the cache update process. At this time, another request will acquire the lock, but the previous request is in the cache update. At the end, if you delete the lock directly without making a judgment, you will accidentally delete the lock created by other requests, so we need to introduce a random value when creating the lock:

<?php

$ok = $redis->set($key, $random, array('nx', 'ex' => $ttl));

if ($ok) {
    $cache->update();

    if ($redis->get($key) == $random) {
        $redis->del($key);
    }
}

?>

Supplement: In this article, there is actually a problem when deleting the lock, and the impact of problems such as GC pause is not considered. For example, the A request is stuck before DEL, and then the lock expires. At this time, the B request is successful again. When the lock is acquired, when A's request is slowed down, the lock created by the B's request will be DEL dropped. This problem is far more complicated than expected. For the specific solution, see several reference links on locks at the end of this article.

This basically implements single-machine locks. If you want to implement distributed locks, please refer to: Distributed locks with Redis , but distributed locks need to pay more attention: How to do distributed lockingIs Redlock safeIn addition, there is also a Chinese version: Is Redis-based distributed lock safe ( up / down ).

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326442288&siteId=291194637