Teach you how to implement a Java based distributed lock service

Keywords: Java Jedis Redis Programming


In modern programming languages, programmers who have been exposed to multithreaded programming have a certain understanding of locks.

In short, the lock in multithreading is a mechanism to ensure the consistency of shared resources when multiple threads modify shared resources in multithreading environment.

We will not expand here.

In the distributed environment, the original multi-threaded lock is not used, and there is a demand for distributed locks.

The so-called distributed lock service is a service that ensures the resource consistency of multiple distributed services under the distributed environment.

It is not easy to implement a distributed lock service in a distributed environment, and many problems that do not need to be considered in a single process lock service need to be considered.

There are many implementations of distributed lock. Here we discuss the implementation of redis in Java.

There is already an open source implementation in the redisson project in GitHub. But that's too complicated.
Now let's implement a simple distributed lock service based on the stand-alone redis.
This service must meet the following requirements

  • It supports immediate lock acquisition. If the acquisition returns true, it returns false if the acquisition fails;
  • It supports the method of waiting for the lock to be acquired. If the lock is acquired, it directly returns true. If the lock cannot be acquired for a short period of time, try again and again. If the lock succeeds, it returns true. If the lock cannot be acquired after the waiting time, it returns false;
  • No deadlock;
  • Can't release the lock not added by oneself;

Let's use an example to demonstrate the implementation of distributed lock service with redis in Java:

Lock up

The locking logic for implementing distributed locks through redis is as follows:

According to this logic, the core code to realize locking is as follows:

jedis.select(dbIndex);
String key = KEY_PRE + key;
String value = fetchLockValue();
if(jedis.exists(key)){
  jedis.set(key,value);
  jedis.expire(key,lockExpirseTime);
  return value;
}

On the surface, there seems to be no problem with this code. In fact, it can't implement the locking operation correctly in the distributed environment. In order to implement the lock operation correctly, the three steps of "judge whether the key exists", "save key value" and "set the expiration time of the key" must be atomic operation. If it is not an atomic operation, there are two possible situations:

  • After "judge whether the key exists" to get the result step that the key does not exist, "save key value" to step, another client executes the same logic, and executes the "judge whether the key exists" step to get the result that the key does not exist. In this way, multiple clients get the same lock;
  • After the client completes the "save key value" step, you need to set the expiration time of a key to prevent the client from deadlock caused by the code quality not being unlocked or the process crash not being unlocked. After the "save key value" step and before the "set key expiration time" step, the process may crash, causing the "set key expiration time" step to fail;

After version 2.6.12, redis has extended the set command to avoid the above two problems. The parameters of the new version of redis set command are as follows

SET key value [EX seconds] [PX milliseconds] [NX|XX]

The new version of the set command adds the EX, PX, NX|XX parameter options. What they mean is as follows

  • EX seconds – set the expiration time of key, in hours and seconds
  • PX milliseconds – sets the expiration time of key, in milliseconds
  • NX - the key value will be set only when the key key does not exist
  • XX - the key value will be set only when the key key exists

In this way, the original three-step operation can be completed in a set atomic operation, avoiding the two problems mentioned above.

The new version of redis lock core code is modified as follows:

jedis = redisConnection.getJedis();
jedis.select(dbIndex);
String key = KEY_PRE + key;
String value = fetchLockValue();
if ("OK".equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
    return value;
}

Unlock

The basic process of unlocking is as follows:

According to this logic, the core code to unlock in Java is as follows:

jedis.select(dbIndex);
String key = KEY_PRE + key;
if(jedis.exists(key) && value.equals(jedis.get(key))){
    jedis.del(key);
    return true;
}
return false;

As in the case of lock adding, the three operations of whether a key exists, whether it holds a lock by itself, and * * delete key value * * need to be atomic operations. Otherwise, when a client finishes the "judge whether it holds a lock by itself" step, it comes to the conclusion that it holds a lock by itself. At this time, the expiration time of the lock is up, and it is automatically released by redis. At the same time, another client is based on this The key is locked successfully. If the first client continues to delete the key value, the lock that does not belong to itself will be released.

This is obviously not working. Here we use the ability of redis to execute Lua script to solve the problem of atomic operation. The modified unlocking core code is as follows:

jedis.select(dbIndex);
String key = KEY_PRE + key;
String command = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
if (1L.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {
    return true;
}

In addition, the mechanism to determine whether a lock is held by oneself is to use the key value when the lock is added to determine whether the current key value is equal to the value obtained when the lock is held by oneself. Therefore, the value of the lock must be a globally unique string.

The complete code is as follows

package com.x9710.common.redis.impl;

import com.x9710.common.redis.LockService;
import com.x9710.common.redis.RedisConnection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import redis.clients.jedis.Jedis;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.UUID;

/**
 * redis implementation of distributed lock
 *
 * @author full name
 * @since specific date
 */
public class LockServiceRedisImpl implements LockService {

private static Log log = LogFactory.getLog(LockServiceRedisImpl.class);

private static String SET_SUCCESS = "OK";

private static String KEY_PRE = "REDIS_LOCK_";

private DateFormat df = new SimpleDateFormat("yyyyMMddHHmmssSSS");

private RedisConnection redisConnection;

private Integer dbIndex;

private Integer lockExpirseTime;

private Integer tryExpirseTime;

public void setRedisConnection(RedisConnection redisConnection) {
    this.redisConnection = redisConnection;
}

public void setDbIndex(Integer dbIndex) {
    this.dbIndex = dbIndex;
}

public void setLockExpirseTime(Integer lockExpirseTime) {
    this.lockExpirseTime = lockExpirseTime;
}

public void setTryExpirseTime(Integer tryExpirseTime) {
    this.tryExpirseTime = tryExpirseTime;
}

public String lock(String key) {
    Jedis jedis = null;
    try {
        jedis = redisConnection.getJedis();
        jedis.select(dbIndex);
        key = KEY_PRE + key;
        String value = fetchLockValue();
        if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
            log.debug("Reids Lock key : " + key + ",value : " + value);
            return value;
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }
    return null;
}

public String tryLock(String key) {

    Jedis jedis = null;
    try {
        jedis = redisConnection.getJedis();
        jedis.select(dbIndex);
        key = KEY_PRE + key;
        String value = fetchLockValue();
        Long firstTryTime = new Date().getTime();
        do {
            if (SET_SUCCESS.equals(jedis.set(key, value, "NX", "EX", lockExpirseTime))) {
                log.debug("Reids Lock key : " + key + ",value : " + value);
                return value;
            }
            log.info("Redis lock failure,waiting try next");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } while ((new Date().getTime() - tryExpirseTime * 1000) < firstTryTime);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }
    return null;
}

public boolean unLock(String key, String value) {
    Long RELEASE_SUCCESS = 1L;
    Jedis jedis = null;
    try {
        jedis = redisConnection.getJedis();
        jedis.select(dbIndex);
        key = KEY_PRE + key;
        String command = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        if (RELEASE_SUCCESS.equals(jedis.eval(command, Collections.singletonList(key), Collections.singletonList(value)))) {
            return true;
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }
    return false;
}

/**
 * Generate a unique lock string
 *
 * @return Unique string
 */
private String fetchLockValue() {
    return UUID.randomUUID().toString() + "_" + df.format(new Date());
}
}

Test code

package com.x9710.common.redis.test;

import com.x9710.common.redis.RedisConnection;
import com.x9710.common.redis.impl.LockServiceRedisImpl;

public class RedisLockTest {

public static void main(String[] args) {
    for (int i = 0; i < 9; i++) {
        new Thread(new Runnable() {
            public void run() {
                RedisConnection redisConnection = RedisConnectionUtil.create();
                LockServiceRedisImpl lockServiceRedis = new LockServiceRedisImpl();
                lockServiceRedis.setRedisConnection(redisConnection);
                lockServiceRedis.setDbIndex(15);
                lockServiceRedis.setLockExpirseTime(20);
                String key = "20171228";
                String value = lockServiceRedis.lock(key);
                try {
                    if (value != null) {
                        System.out.println(Thread.currentThread().getName() + " lock key = " + key + " success! ");
                        Thread.sleep(25 * 1000);
                    }else{
                        System.out.println(Thread.currentThread().getName() + " lock key = " + key + " failure! ");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (value == null) {
                        value = "";
                    }
                    System.out.println(Thread.currentThread().getName() + " unlock key = " + key + " " + lockServiceRedis.unLock(key, value));

                }
            }
        }).start();
    }
}
}

test result

Thread-1 lock key = 20171228 failure! 
Thread-2 lock key = 20171228 failure! 
Thread-4 lock key = 20171228 failure! 
Thread-8 lock key = 20171228 failure! 
Thread-7 lock key = 20171228 failure! 
Thread-3 lock key = 20171228 failure! 
Thread-5 lock key = 20171228 failure! 
Thread-0 lock key = 20171228 failure! 
Thread-6 lock key = 20171228 success! 
Thread-1 unlock key = 20171228 false
Thread-2 unlock key = 20171228 false
Thread-4 unlock key = 20171228 false
Thread-8 unlock key = 20171228 false
Thread-3 unlock key = 20171228 false
Thread-5 unlock key = 20171228 false
Thread-0 unlock key = 20171228 false
Thread-7 unlock key = 20171228 false
Thread-6 unlock key = 20171228 true

From the test results, it can be seen that nine threads lock a key at the same time, only one can successfully acquire the lock, and the rest of the clients can't acquire the lock.

Postscript

This code also implements a tryLock interface. This is mainly because when the client cannot acquire the lock, it will repeatedly try to acquire the lock for a short period of time.

Collected all aspects, the current company's, as well as their own collection and summary. The following pictures are captured with pdf, and if necessary, they are taken by themselves

Interview questions of major companies:

Resume Template:

Link: https://pan.baidu.com/s/1DO6XGkbmak7KIt6Y7JQqyw
Extraction code: fgj6
I don't know if it will fail. If it fails Click (778490892) Or scan the following two-dimensional code, get into the group, link reissue can not come, thank you.

Posted by cuboidgraphix on Mon, 18 May 2020 03:25:12 -0700