A brief analysis of Redis distributed locks

  • 2020-05-27 07:33:51
  • OfStack

In the recent work, I met with the following business scenarios. I needed to push a batch of data to the other 1 system regularly every day. However, since the system is deployed in a cluster, there will be task contention under the condition of system 1, so I need to add a distributed lock to ensure that there is one Job within a fixed time range to complete the scheduled task. The solution considered in the early stage is to adopt ZooKeeper distributed task and Quartz distributed task scheduling. However, since Zookeeper needs to add additional components and Quartz needs to add tables, and Redis 1 component already exists in the project, Redis distributed lock is considered to complete the function of distributed task preemption

Record the detour you took.

Version 1:


@Override
	public <T> Long set(String key,T value, Long cacheSeconds) {
		if (value instanceof HashMap) {
			BoundHashOperations valueOperations = redisTemplate.boundHashOps(key);
			valueOperations.putAll((Map) value);
			valueOperations.expire(cacheSeconds, TimeUnit.SECONDS);
		}
		else{
		// use map storage 
		BoundHashOperations valueOperations = redisTemplate.boundHashOps(key);
		valueOperations.put(key, value);
		// seconds 
		valueOperations.expire(cacheSeconds, TimeUnit.SECONDS);
		}
		return null;
	}


	@Override
	public void del(String key) {
		redisTemplate.delete(key);
	}

The use of set and del to complete the lock occupation and release, after testing, it is known that set is not thread safe, in the case of concurrency will often lead to data not 1.

Version 2:


/**
   *  A distributed lock 
   * @param range  The length of the lock   How many requests are allowed to preempt the resource 
   * @param key
   * @return
   */
  public boolean getLock(int range, String key) {
    ValueOperations<String, Integer> valueOper1 = template.opsForValue();
    return valueOper1.increment(key, 1) <= range;
  }

  /**
   *  Initialize the lock ,  Set equal to the 0
   * @param key
   * @param expireSeconds
   * @return
   */
  public void initLock(String key, Long expireSeconds) {
    ValueOperations<String, Integer> operations = template.opsForValue();
    template.setKeySerializer(new GenericJackson2JsonRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    operations.set(key, 0, expireSeconds * 1000);
  }

  /**
   *  Release the lock 
   * @param key
   */
  public void releaseLock(String key) {
    ValueOperations<String, Integer> operations = template.opsForValue();
    template.setKeySerializer(new GenericJackson2JsonRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    template.delete(key);
  }

The increament operation of redis is used to preempt the lock. However, when the lock is released, each thread can delete the key value in redis, and initLock will be overwritten once, so this method is also discarded

Final version:


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnection;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;
import redis.clients.jedis.Jedis;
import java.lang.reflect.Field;
import java.util.Collections;
@Service
public class RedisLock {
  private static final String LOCK_SUCCESS = "OK";
  private static final String SET_IF_NOT_EXIST = "NX";
  private static final String SET_WITH_EXPIRE_TIME = "PX";
  private static final Long RELEASE_SUCCESS = 1L;
  @Autowired
  private RedisConnectionFactory connectionFactory;
  /**
   *  Attempt to acquire a distributed lock 
   * @param lockKey  The lock 
   * @param requestId  The request id 
   * @param expireTime  Beyond the time 
   * @return  Whether you are successful or not 
   */
  public boolean lock(String lockKey, String requestId, int expireTime) {
    Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis");
    ReflectionUtils.makeAccessible(jedisField);
    Jedis jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection());

    String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

    if (LOCK_SUCCESS.equals(result)) {
      return true;
    }
    return false;

  }
  /**
   *  Release distributed locks 
   * @param lockKey  The lock 
   * @param requestId  The request id 
   * @return  Successful release 
   */
  public boolean releaseLock(String lockKey, String requestId) {

    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Object result = getJedis().eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

    if (RELEASE_SUCCESS.equals(result)) {
      return true;
    }
    return false;
  }
  public Jedis getJedis() {
    Field jedisField = ReflectionUtils.findField(JedisConnection.class, "jedis");
    ReflectionUtils.makeAccessible(jedisField);
    Jedis jedis = (Jedis) ReflectionUtils.getField(jedisField, connectionFactory.getConnection());
    return jedis;
  }
}


Related articles: