Using SETNX locks to prevent cached haemorrhage

SETNX is an instruction in Redis, called "Set If Not Exist", which sets value to key only when it does not exist, otherwise no operation is performed. SETNX can also be used to implement locks in Redis.

Problem introduction

Before introducing how to use SETNX to implement locks, let's consider the following question:

Suppose we now have a hot data, which is stored in mysql. We use redis to make a layer of cache. At a certain point in time, the cache fails. At this time, a large number of requests arrive at the same time. Because the cache miss may hit the request at MySQL at the same time, and blood avalanche may occur. So how to avoid this problem?

Solutions

The answer is to use locks. We can use a lock when we cache miss to mysql to fetch data. We can ensure that the cache invalidation only goes to mysql once.

Reference to official documents SETNX - Redis Explain that we can use

SETNX lock.foo <current Unix time + lock timeout + 1>

If we return 1, we succeed in getting the lock, and the lock will invalid seconds after the lock timeout, then we can go to mysql to get the data and cache it.

If you return 0, it means that the lock has been occupied, you can try again or do something else.

Deadlock handling

Below is the original description of deadlock:

In the above locking algorithm there is a problem: what happens if a client fails crashes, or is otherwise not able to release the lock? It's possible to detect this condition because the lock key contains a UNIX timestamp. If such a timestamp is equal to the current Unix time the lock is no longer valid. When this happens we can't just call DEL against the key to remove the lock and then try to issue a SETNX, as there is a race condition here, when multiple clients detected an expired lock and are trying to release it.

Would it be a problem if the client waited several times after SETNX returned to 0, then DEL lock.foo, and then re-SETNX?

C1 and C2 read lock.foo to check the timestamp, because they both received
0 after executing SETNX, as the lock is still held by C3 that crashed after holding the lock.
C1 sends DEL lock.foo
C1 sends SETNX lock.foo and it succeeds
C2 sends DEL lock.foo
C2 sends SETNX lock.foo and it succeeds

both C1 and C2 acquired the lock because of the race condition.

It is true that C1 and C2 arrive at the same time and may acquire locks at the same time. What is the solution?

Fortunately, it's possible to avoid this issue using the following algorithm. Let's
see how C4, our sane client, uses the good algorithm:
C4 sends SETNX lock.foo in order to acquire the lock
The crashed client C3 still holds it, so Redis will reply with 0 to C4.
C4 sends GET lock.foo to check if the lock expired. If it is not, it will sleep for
some time and retry from the start.
Instead, if the lock is expired because the Unix time at lock.foo is older than
the current Unix time, C4 tries to perform:

GETSET lock.foo <current Unix timestamp + lock timeout + 1>
Because of the GETSET semantic, C4 can check if the old value stored atkey
is still an expired timestamp. If it is, the lock was acquired.
If another client, for instance C5, was faster than C4 and acquired the lock
with the GETSET operation, the C4 GETSET operation will return a non
expired timestamp. C4 will simply restart from the first step. Note that even if
C4 set the key a bit a few seconds in the future this is not a problem.

The key is the GETSET instruction, which can determine whether the timestamp has been modified when setting the timestamp. If it has been modified, it returns 0, ensuring that no two clients set a new timestamp at the same time.

Keywords: Unix Redis MySQL

Added by Snorkel on Mon, 10 Jun 2019 04:47:19 +0300