Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

Redis

C language development based on memory database, read and write speed is very fast, so it is widely used in the cache direction.

A distributed lock

When we modify the existing data in the system, we need to read it first and then save the modification, which is easy to encounter concurrency problems. Because modification and saving are not atomic operations, some operations on data may be lost in concurrent scenarios. In single-server systems, local locks are often used to avoid concurrency problems. However, when services are deployed in a cluster, local locks cannot take effect between multiple servers, so distributed locks are needed to ensure data consistency.

Redis native command implementation

Use Redis to implement distributed locks, mainly using redis SETNX command (set if not exist)

  • Lock command: SETNX key value. If the key does not exist, set the key and return success. If the key does not exist, return failure
  • Unlock command: DEL key, which releases the lock by removing key-value pairs so that other threads can use the lock command to acquire the lock
  • EXPIRE key timeout Specifies the EXPIRE time of the key. If the lock is not explicitly released by the thread, the key will be automatically deleted after the EXPIRE time expires to avoid deadlocks.

Simple lock code implementation is as follows:

public boolean tryLock(String key,String requset,int timeout) { Long result = jedis.setnx(key, requset); If (result == 1L) {return jedis.expire(key, timeout) == 1L; } else { return false; }}Copy the code

There are some problems with the above code:

1. Setnx and EXPIRE operations are non-atomic

If the setnx command is used to set the lock successfully, the server breaks down or restarts when the expire command is used to set the expire time. In this case, the lock does not set the timeout period and deadlock may occur.

Use lua scripts to atomize these two operations:

public boolean tryLock_with_lua(String key, String UniqueId, int seconds) { String lua_scripts = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" + "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end"; List<String> keys = new ArrayList<>(); List<String> values = new ArrayList<>(); keys.add(key); values.add(UniqueId); values.add(String.valueOf(seconds)); Object result = jedis.eval(lua_scripts, keys, values); Return result.equals(1L); }Copy the code

2. Lock misunderstanding

Thread A obtains the lock. After the timeout period is set, thread A’s execution time exceeds the timeout period. When the timeout period is reached, the lock will be automatically released

To solve the problem of lock misunderstanding, we can set the corresponding value when setting the key. Value can be regarded as the unique identifier of the thread or thread that obtains the lock. We can use the UUID as the unique identifier to check whether the value corresponding to the key is the same as the value held by the thread before deleting the lock. This avoids deleting locks that you do not own.

3. Concurrent execution occurs due to timeout unlock

Thread A obtains the lock and starts execution. However, if the execution time exceeds the lock timeout period, the lock is automatically released. Thread B obtains the lock and starts execution.

Thread A and thread B execute concurrently.

  • Set the expiration time long enough to ensure that the code executes within the expiration time
  • Set up a daemon thread for the thread that holds the lock, adding time to the lock that is about to expire but is not released

4. Do not reenter

When a thread holds a lock, it requests the lock again. A lock that can be locked more than once on a thread is reentrant. Conversely, if a non-reentrant lock is locked again by a thread that already holds it, locking again will fail. Redis can count lock reentrant, +1 on lock, -1 on unlock, and lock release when technique goes to 0.

  1. Here’s a simple implementation using a local cache ThreadLocal:
private static ThreadLocal<Map<String, Integer>> LOCKERS = ThreadLocal.withInitial(HashMap::new); Public Boolean lock(String key) {Map<String, Integer> lockers = lockers.get (); if (lockers.containsKey(key)) { lockers.put(key, lockers.get(key) + 1); return true; } else { if (SET key uuid NX EX 30) { lockers.put(key, 1); return true; } } return false; } // Unlock public void unlock(String key) {Map<String, Integer> lockers = lockers.get (); if (lockers.getOrDefault(key, 0) <= 1) { lockers.remove(key); DEL key } else { lockers.put(key, lockers.get(key) - 1); }}Copy the code
  1. The Map data structure of Redis is used to count the reentrant count while setting the key

5. Cannot wait for the lock to be released

All of the above methods return failure or success directly, and cannot be used if the client can wait for the lock to be released

  • This problem can be solved by client polling. If the lock is not obtained, wait for a period of time to obtain the result again until the lock is obtained or the wait times out. This method consumes more server resources and is less efficient when there is a large amount of concurrency.
  • Use Redis’s publish and subscribe function to subscribe to release the lock after the lock fails to be acquired, and send the release of the lock when the lock is released

Redisson implementation

  1. The lock LUa script is executed when the thread obtains the lock, ensuring atomicity

  2. If the thread fails to acquire the lock, it will try to acquire the lock through the while loop until it succeeds, and then execute the Lua script (which also includes the wait time).

  3. Automatic delay of Watch Doc is supported, which is aimed at the situation of concurrent execution caused by timeout unlock mentioned above. Here, Watch Dog starts a thread in the background to continuously extend the survival time of the key. However, the Watch Dog, which is equivalent to monitoring threads, has some impact on performance

  4. Reentrant locking mechanism is implemented

    1. Redis’s own stored data structure supports Map
    2. The key value of the Map can be used to indicate the current thread information, and the value can be used to record the number of reentries