In a single-machine environment, thread-safety issues occur when multiple threads can change a variable (mutable shared variable) at the same time. This problem can be avoided by using JAVA’s provision of volatile, ReentrantLock, synchronized, and Concurrent and by packaging thread-safe classes.

However, in a multi-machine deployment environment, thread security needs to be ensured in the multi-process environment. These apis provided by Java can only ensure the thread safety of multi-thread access to shared resources within a single JVM process, which is no longer sufficient. This is where distributed locks are needed to ensure thread safety. Distributed locks ensure that a method can be executed by only one thread on one machine at a time in a distributed application cluster.

Four conditions must be met for a distributed lock:

  1. Mutual exclusivity. Only one client can hold the lock at any time.
  2. No deadlocks. Even if a client crashes while holding the lock and does not actively unlock it, ensure that subsequent clients can lock it.
  3. Lock and unlock must be the same client. Client A cannot unlock the lock of client B, that is, the lock cannot be misunderstood.
  4. Fault tolerance. As long as most Redis nodes are up and running, the client is able to acquire and release locks.

Redis distributed lock

Common distributed lock implementation methods include database, Redis, and Zookeeper. The following focuses on using Redis to implement distributed locks.

Before Redis 2.6.12, setnx + expire was used to implement distributed locks. After Redis 2.6.12, setnx added the expiration time parameter:

SET lockKey value NX PX expire-time
Copy the code

So with Redis 2.6.12, you only need to use setnx to implement distributed locking.

Locking logic:

  1. Setnx grabs the lock for the key. If the key already exists, the system does not perform the operation. After a period of time, the system tries again to ensure that only one client can hold the lock.
  2. Value is set to requestId (you can use the machine IP to concatenate the current thread name), indicating which request added the lock. During unlocking, you need to determine whether the current request holds the lock to prevent misunderstanding of the lock. For example, the lock on client A expires before the unlock is performed. In this case, client B successfully attempts to lock client B and then runs the del() method on client A to unlock client B.
  3. Use expire to add an expiration time to the lock to prevent exceptions from causing the lock not to be released.

Unlock logic:

Obtain the value of the lock and check whether it is equal to requestId. If so, delete the lock. Use Lua scripts to implement atomic operations to ensure thread safety.

Let’s demonstrate the implementation of distributed locks through Jedis, a Java language-based Redis client.

Jedis implements distributed locking

Import the Jedis JAR package and add code to the pom.xml file:

Clients </groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency>Copy the code

lock

Jedis’ set() is called to lock, and the lock code is as follows:

/** * @description: * @author: 2021-08-01 17:13 */ public class RedisTest { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_EXPIRE_TIME = "PX"; @Autowired private JedisPool jedisPool; public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) { Jedis jedis = jedisPool.getResource(); String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_EXPIRE_TIME, expireTime); if (LOCK_SUCCESS.equals(result)) { return true; } return false; }}Copy the code

Parameter description:

  • LockKey: Use a key as a lock. Ensure that the key is unique. You can concatenate custom keys using system numbers.
  • RequestId: indicates which request added the lock and can concatenate the current thread name using the machine IP. During unlocking, check whether the current request holds the lock to prevent misunderstanding of the lock. For example, the lock on client A expires before the unlock is performed. In this case, client B successfully attempts to lock client B and then runs the del() method on client A to unlock client B.
  • NX: SET IF NOT EXIST. IF the key already exists, no operation is performed and retry after a period of time. The NX parameter guarantees that only one client can hold the lock.
  • PX: Set the key to expire at expireTime.
  • ExpireTime: Sets the key expiration time to prevent lock failure due to exceptions.

unlock

Obtain the value of the lock and check whether it is the same as requestId. If so, delete the lock. Lua scripts are used to implement atomic operations to ensure thread-safety.

When a Lua script is executed using the eval command, no other script or Redis command is executed to implement the atomic operation of the combined command. The lua script is as follows:

/ / KEYS [1] is lockKey, ARGV[1] is requestId String script = "if redis. Call ('get', KEYS[1]) == ARGV[1] then return redis. Call ('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));Copy the code

Jedis eval() ¶

public Object eval(String script, List<String> keys, List<String> args) {
    return this.eval(script, keys.size(), getParams(keys, args));
}
Copy the code

Call get to get the value of the lock (KEYS[1]), check if it is equal to requestId (ARGV[1]), and call del to remove the lock if it is equal. Otherwise 0 is returned.

The full unlock code is as follows:

/**
 * @description:
 * @author: 程序员大彬
 * @time: 2021-08-01 17:13
 */
public class RedisTest {
    private static final Long RELEASE_SUCCESS = 1L;
​
    @Autowired
    private JedisPool jedisPool;
​
    public boolean releaseDistributedLock(String lockKey, String requestId) {
        Jedis jedis = jedisPool.getResource();
        ////KEYS[1]是lockKey,ARGV[1]是requestId
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
​
        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}
Copy the code

This is all about using Redis to implement distributed locks. I hope it will help you. This article has been collected by Github/Gitee warehouse, welcome everyone to watch, star

Github Repository: github.com/Tyson0314/J…

If Github is not available, access the Gitee repository.

Gitee Warehouse: gitee.com/tysondai/Ja…

Code word is not easy, if you feel that there is help, you can point to encourage!