In Redis, the so-called Setnx, is the "set if not exists" abbreviation, that is, only when it does not exist to set, you can use it to achieve the effect of the lock, but many people do not realize that setnx have traps!
For example: A query database interface, because of the large number of calls, so add a cache, and set the cache after the expiration of the refresh, the problem is when the amount of concurrency is larger, if there is no lock mechanism, then the cache expiration of the instant, a large number of concurrent requests will penetrate the cache directly query the database, resulting in avalanche effect, Then you can control that there is only one request to update the cache, and the other requests are either waiting or using an expired cache.
Here's the most popular Phpredis extension in the current PHP community, for 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 by SETNX, and if successful, the cache is updated and the lock is removed. The logic seems very simple, unfortunately there is a problem: if the request execution for some reason unexpectedly quit, resulting in the creation of a lock but did not delete the lock, then the lock will always exist, so that the cache can no longer be updated. So we need to add an expiration time to the lock safekeeping:
<?php
$redis->multi ();
$redis->setnx ($key, $value);
$redis->expire ($key, $ttl);
$redis->exec ();
?>
Because SETNX does not have the ability to set the expiration time, we need to use Expire to set up, and we need to wrap both multi/exec to ensure the atomic nature of the request, lest Setnx succeed Expire failed. Unfortunately, there are still questions: When multiple requests arrive, although only one of the requested SETNX can succeed, the Expire of any one request can succeed, which means that even if the lock is not acquired, the expiration time can be refreshed, and if the request is more intensive, the expiration time is always refreshed. Causes the lock to remain in effect. So we need to have the conditional execution 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 think it would be a hassle to implement a seemingly simple feature that uses Lua scripts. In fact, Redis has taken into account the sufferings of everyone, from 2.6.12, set covers the function of Setex, and set itself has included the ability to set the expiration time, that is to say, the functionality we need to use only set can be implemented.
<?php
$ok = $redis->set ($key, $value, Array (' NX ', ' ex ' => $ttl));
if ($ok) {
$cache->update ();
$redis->del ($key);
}
?>
Is the code perfect? The answer is still almost! Imagine if a request to update the cache for a long time, even longer than the expiration of the lock, resulting in the cache update process, the lock is invalid, at this time the other request will acquire the lock, but the previous request in the cache after the update is complete, if not judged directly delete the lock, There will be a case of accidentally deleting a lock created by another request, 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);
}
}
?>
So basic to achieve a single lock, if you want to achieve distribution lock, please refer to: Distributed locks with Redis, here is not in-depth discussion, Summary: The best way to avoid falling into the setnx trap is never to use it