preface

Nowadays, business scenarios are more and more complex, and the architectures used are also more and more complex. Distributed and high concurrency are the normal business requirements. Like Tencent’s many services, there are CDN optimization, remote backup and other processing. Speaking of distributed, it inevitably involves the concept of distributed lock. How to ensure the synchronization of distributed lock of different threads on different machines?

Realize the point

  1. Mutually exclusive, at the same time, intelligence has a client holding the lock.
  2. To prevent deadlocks, if the client holding the lock crashes and does not release the lock actively, ensure that the lock can be released and other clients can lock properly.
  3. Lock and lock release must be the same client.
  4. Fault tolerance, only redis nodes survive, can be normal lock unlock operation.

Correct redis distributed lock implementation

Wrong way to lock

Wrong way 1

Mutual exclusion and deadlock prevention, the first thought of redis setnx command to ensure mutual exclusion, in order to prevent deadlocks, locks need to set a timeout period.

    public static void wrongLock(Jedis jedis, String key, String uniqueId, int expireTime) {
        Long result = jedis.setnx(key, uniqueId);
        if(1 == result) {// If the redis instance crashes, the expiration time cannot be set. Jedis.expire (key, expireTime); }}Copy the code

In a multi-threaded concurrent environment, any non-atomic operation can cause problems. In this code, if the redis instance crashes while setting the expiration time, the expiration time cannot be set. If the client does not release the lock correctly, the lock (which never expires) will never be released.

Error # 2

It’s easy to think that setting values and timeouts to atoms would solve the problem. Use the setnx command to set value to expire.

public static boolean wrongLock(Jedis jedis, String key, int expireTime) { long expireTs = System.currentTimeMillis() + expireTime; // Lock does not exist, current thread lock resultif (jedis.setnx(key, String.valueOf(expireTs)) == 1) {
            return true; } String value = jedis.get(key); // If the current lock exists and the lock has expiredif(value ! = null && numberUtils.tolong (value) < system.currentTimemillis ()) {// Lock expired, String oldValue = jedis.getSet(key, string.Valueof (expireTs));if(oldValue ! = null && oldValue.equals(value)) {// Only one thread succeeds in setting the keyreturn true; }} // In other cases, the lock failsreturn true;
    }
Copy the code

At first glance, there seems to be no problem. However, after careful analysis, there are the following problems:

  1. If value is set to the expiration time, all clients must synchronize their clocks strictly. In this case, a synchronous clock is required. Even with a synchronous clock, distributed servers are generally bound to have a small amount of time error.
  2. When a lock expires, using jedis. GetSet ensures that only one thread is set successfully, but it does not guarantee that the lock is set and unlocked by the same client, because there is no indication which client set the lock.

Wrong unlocking mode

Incorrect unlocking method 1

Delete key directly

Public static void wrongReleaseLock(Jedis Jedis, String key) {// Jedis.del (key); }Copy the code

Simple and crude, direct unlock, but not their own lock, will be deleted, this seems a bit arbitrary!

Incorrect unlocking method 2

Determine if you are the owner of the lock. If so, only the owner can release the lock.

    public static void wrongReleaseLock(Jedis jedis, String key, String uniqueId) {
        ifJedis.del (key); (uniqueid.equals (jedis.get(key)); }}Copy the code

It looks perfect, but if you decide that the lock is in your possession, it will automatically be released when the lock expires. Jedis.del (key) is executed by the current thread. This thread removes the lock from the other thread.

Correct lock release mode

Basically avoid above several kinds of wrong way, is the right way. The following conditions should be met:

  1. Commands must be mutually exclusive
  2. The key must have an expiration date to prevent locks from being released in the event of a crash
  3. Value identifies each client with a unique ID, ensuring that only the holder of the lock can release the lock

Set the unique ID and expiration time by using the set command. Unlocking is a bit more complicated. After locking, a unique ID can be returned to indicate that the lock is owned by the client lock. To release the lock, determine whether the owner is the owner and then delete the lock. This requires the Lua script of Redis to ensure the atomic execution of the two commands. Here is the specific lock and lock release code:

@Slf4j
public class RedisDistributedLock {
    private static final String LOCK_SUCCESS = "OK";
    private static final Long RELEASE_SUCCESS = 1L;
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX"; Private static int EXPIRE_TIME = 5 * 1000; private static int EXPIRE_TIME = 5 * 1000; Private static int WAIT_TIME = 1 * 1000; private static int WAIT_TIME = 1 * 1000; private Jedis jedis; private String key; public RedisDistributedLock(Jedis jedis, String key) { this.jedis = jedis; this.key = key; } // Keep trying to lock public Stringlock() {try {// Wait time, lock failed longwaitEnd = System.currentTimeMillis() + WAIT_TIME;
            String value = UUID.randomUUID().toString();
            while (System.currentTimeMillis() < waitEnd) {
                String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
                if (LOCK_SUCCESS.equals(result)) {
                    return value;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        } catch (Exception ex) {
            log.error("lock error", ex);
        }
        return null;
    }

    public boolean release(String value) {
        if (value == null) {
            return false; } // Determine that the key exists and deleting the key must be an atomic operation // and who owns the lock, who releases String script ="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = new Object();
        try {
            result = jedis.eval(script, Collections.singletonList(key),
                    Collections.singletonList(value));
            if (RELEASE_SUCCESS.equals(result)) {
                log.info("release lock success, value:{}", value);
                return true;
            }
        } catch (Exception e) {
            log.error("release lock error", e);
        } finally {
            if(jedis ! = null) { jedis.close(); } } log.info("release lock failed, value:{}, result:{}", value, result);
        return false; }}Copy the code

A single Redis distributed lock has so many lines, I don’t know if you understand? Leave a comment and discuss!

Programmers’ partners, feel lonely, then join the public number [programmer’s way], communicate together, out of our programmer’s way!