Example code for Redis Template implementing distributed locks

  • 2020-06-12 10:57:41
  • OfStack

preface

Distributed lock 1 generally has three implementation methods: 1. Database optimistic lock; 2. 2. Distributed locking based on Redis; 3. Distributed locking based on ZooKeeper. This blog will introduce a second approach to implementing distributed locks based on Redis. Although there are various blogs on the web that introduce the implementation of Redis distributed locks, their implementations have a variety of problems. To avoid confusion, this blog will detail how to properly implement Redis distributed locks.

reliability

First, in order to ensure that distributed locks are available, we need to ensure that lock implementations meet at least four conditions:
1. Mutual exclusion. Only 1 client can hold the lock at any given time.
2. Deadlocks will not occur. Even if one client crashes while holding the lock without actively unlocking it, subsequent others are guaranteed to lock.
3. Be fault-tolerant. As long as most of the Redis nodes are running, the client can be locked and unlocked.
4. The bell must be rung. The lock and unlock must be on the same client. The client itself cannot unlock the lock added by others.

Steps to obtain distributed locks using Redis's SETNX command:

The & # 8226; The C1 and C2 threads simultaneously check the timestamp to get the lock, execute the SETNX command and both return 0, at which point the lock is still held by C3 and C3 has crashed
The & # 8226; C1 DEL lock
The & # 8226; C1 USES the SETNX command to obtain the lock and succeeds
The & # 8226; C2 DEL lock
The & # 8226; C2 USES the SETNX command to obtain the lock and succeeds
The & # 8226; ERROR: Both C1 and C2 acquired locks due to race conditions

Fortunately, the following steps completely prevent this from happening. How does the C4 thread work

The & # 8226; C4 obtains the lock using the SETNX command
The & # 8226; C3 has crashed but still holds the lock, so Redis returns 0 to C4
The & # 8226; C4 USES the GET command to get the lock and check that it has expired. If it has not, wait for another period and try again
The & # 8226; If the lock has expired, C4 tries GETSET ES62en.foo < current Unix timestamp + lock timeout + 1 >
The & # 8226; Using the GETSET syntax, C4 can check whether the old time is still expired and, if so, get the lock
The & # 8226; If another client, C5, gets the lock first, C4 executes the GETSET command and returns a non-expiration time, then C4 continues to try to get the lock again from scratch. This operation C4 will extend the expiration time of locks acquired by C5 at 1 point, but this is not a big deal.

Next we present it in the form of code:


package com.shuige.components.cache.redis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
 * Description:  general Redis Helper classes 
 * User: zhouzhou
 * Date: 2018-09-05
 * Time: 15:39
 */
@Component
public class CommonRedisHelper {
  public static final String LOCK_PREFIX = "redis_lock";
  public static final int LOCK_EXPIRE = 300; // ms
  @Autowired
  RedisTemplate redisTemplate;
  /**
   *  Ultimately, distributed locking is strengthened 
   *
   * @param key key value 
   * @return  Whether or not to get 
   */
  public boolean lock(String key){
    String lock = LOCK_PREFIX + key;
    //  using lambda expression 
    return (Boolean) redisTemplate.execute((RedisCallback) connection -> {
      long expireAt = System.currentTimeMillis() + LOCK_EXPIRE + 1;
      Boolean acquire = connection.setNX(lock.getBytes(), String.valueOf(expireAt).getBytes());
      if (acquire) {
        return true;
      } else {
        byte[] value = connection.get(lock.getBytes());
        if (Objects.nonNull(value) && value.length > 0) {
          long expireTime = Long.parseLong(new String(value));
          if (expireTime < System.currentTimeMillis()) {
            //  If the lock has expired 
            byte[] oldValue = connection.getSet(lock.getBytes(), String.valueOf(System.currentTimeMillis() + LOCK_EXPIRE + 1).getBytes());
            //  Prevent deadlocks 
            return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();
          }
        }
      }
      return false;
    });
  }
  /**
   *  Remove the lock 
   *
   * @param key
   */
  public void delete(String key) {
    redisTemplate.delete(key);
  }
}

How to use it? After importing the utility class:


boolean lock = redisHelper.lock(key);
    if (lock) {
      //  Perform logical operations 
      redisHelper.delete(key);
    } else {
      //  Set the number of failures counter ,  When they arrive in 5 When the time ,  Returns the failure 
      int failCount = 1;
      while(failCount <= 5){
        //  Waiting for the 100ms retry 
        try {
          Thread.sleep(100l);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        if (redisHelper.lock(key)){
          //  Perform logical operations 
          redisHelper.delete(key);
        }else{
          failCount ++;
        }
      }
      throw new RuntimeException(" There are too many people now ,  Please try again later ");
    }

After successfully executing the logic, the lock must be unlocked, otherwise it is not recommended to use the lock mechanism

conclusion


Related articles: