Implementing distributed locks using Redis

Background:
In spring projects, we often use timed annotation @ Scheduled combined with cron expression to complete our Scheduled task requirements. This method is no problem in the single point state. However, in actual use, we often deploy projects with multiple nodes for high availability. At this time, the problem of Scheduled tasks being executed multiple times appears.
Of course, we can specify specific instances of performing scheduled tasks through unified configuration items in the configuration center, but this will lose the effect of high availability and is not conducive to later project handover and maintenance. Therefore, distributed locks are needed to solve the current problem. Of course, there are also some formed distributed timing task schemes, such as XXL. Here, for convenience, we implement a simple distributed lock based on Redis.

need:
redis (of course)
redisTemplate/jedis

1. Implementation ideas

Before each task is executed, first obtain the lock in redis. If it is obtained, the scheduled task can be executed normally. The so-called obtaining the lock is to store a pair of kv in redis. If it is placed successfully, it is considered that the lock has been obtained. Because redis is single threaded, it is thread safe.

1. Lock

There is not much to pay attention to in locking. It mainly uses redis
SET key value NX EX max-lock-time

That is, when storing kv, if the key does not exist, it will be placed successfully. If the key exists, it will fail to be updated. A timeout time will be set during storage to prevent deadlock. This command is mapped to the redisTemplate, which is the api setIfAbsent. When setting, the key is designed according to its own business requirements, and the value needs a unique value. The purpose is to recognize that the lock is indeed its own lock when releasing the lock, so as to prevent the lock of other instances from being released by mistake. Here I use UUID. code:

    /**
     * Lock with timeout
     * @param lockKey key to lock
     * @param uniqueValue Unique value (UUID, etc.)
     * @param timeout Timeout
     * @return Boolean true:Lock success false: Lock failure, lock has been obtained by another instance
     */
    public Boolean lock(String lockKey, String uniqueValue, Long timeout) {
        return redisUtil.setIfAbsent(lockKey, uniqueValue, timeout);
    }

It should be noted that you must not set a pair of kv first and then set the timeout time, because we cannot guarantee atomicity by calling it step by step through the api. Once the kv is set, the program crashes, and the lock will always exist in redis and become a deadlock.

2. Release the lock

When releasing the lock, first verify the identity through value, and then del the pair of kv from redis. In short:

	if (get(key) == value) {
	    del(key)
	}

Like locking, we can't complete this process step by step, because we first get the key and then del, which can't guarantee atomicity. Therefore, we use LUA script to complete this process. LUA can ensure atomicity during execution. Code:

    /**
     * Release lock
     * @param unlockKey key to unlock
     * @param verifyValue The unique value used to verify the identity (consistent with the value when locking, used to verify the identity and prevent the lock of other instances from being released by mistake)
     * @return Boolean true:Release succeeded false: release failed, it has been released by another instance, or it is not its own lock
     */
    public Boolean unLock(String unlockKey, String verifyValue) {
        Long SUCCESS = 1L;
        //Use LUA script to ensure the atomicity of operation
        String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return   redis.call('del',KEYS[1])  else return 0 end";
        Object obj = redisUtil.eval(luaScript, Collections.singletonList(unlockKey), Collections.singletonList(verifyValue));
        return SUCCESS.equals(obj);
    }
3. Complete code
@Component
public class DistributedLockUtil {
    @Autowired
    RedisUtil redisUtil;

    /**
     * Lock with timeout
     * @param lockKey key to lock
     * @param uniqueValue Unique value (UUID, etc.)
     * @param timeout Timeout
     * @return Boolean true:Lock success false: Lock failure, lock has been obtained by another instance
     */
    public Boolean lock(String lockKey, String uniqueValue, Long timeout) {
        return redisUtil.setIfAbsent(lockKey, uniqueValue, timeout);
    }

    /**
     * Lock (it is recommended to use lock with timeout to prevent deadlock)
     * @param lockKey key to lock
     * @param uniqueValue Unique value (UUID, etc.)
     * @return Boolean true:Lock success false: Lock failure, lock has been obtained by another instance
     */
    public Boolean lock(String lockKey, String uniqueValue) {
        return redisUtil.setIfAbsent(lockKey, uniqueValue);
    }

    /**
     * Release lock
     * @param unlockKey key to unlock
     * @param verifyValue The unique value used to verify the identity (consistent with the value when locking, used to verify the identity and prevent the lock of other instances from being released by mistake)
     * @return Boolean true:Release succeeded false: release failed, it has been released by another instance, or it is not its own lock
     */
    public Boolean unLock(String unlockKey, String verifyValue) {
        Long SUCCESS = 1L;
        //Use LUA script to ensure the atomicity of operation
        String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return   redis.call('del',KEYS[1])  else return 0 end";
        Object obj = redisUtil.eval(luaScript, Collections.singletonList(unlockKey), Collections.singletonList(verifyValue));
        return SUCCESS.equals(obj);
    }
}
4. Summary

When using, we only need to create a random value first, and then use this value to lock and unlock, for example:

	String eqValue = UUID.randomUUID().toString().replace("-", "").toLowerCase();
	distributedLockUtil.lock(RedisKey.key, eqValue, TimeConstant.THREE_HOUR);
	//Business code
	...
	distributedLockUtil.unLock(RedisKey.key, eqValue);

In fact, if your business is the same as mine, it is to solve the problem of distributed timing, rather than grabbing the lock inside the same instance, the unique value does not have to be really unique. For example, you can use the method of ip + port to meet the requirements, which can also make the api easier to use. Moreover, after locking, as long as a reasonable timeout is specified, there is no need to release the lock.

Tags: Java

Posted by riffy on Sat, 16 Apr 2022 12:21:23 +0930