基于Redis实现分布式锁

package com.xxxx.is.xxxx.base.redis;

import static com.google.common.base.Preconditions.checkArgument;

import java.util.Objects;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.math.RandomUtils;
import redis.clients.jedis.Jedis;
import redis.clients.util.Pool;

/**
 * 基于Redis实现的分布式锁.
 * Note: 基于同一个redis key实现分布式锁时,请注意lock的配置相同.(超时时间等)
 *
 * @author Andy
 * @date 2017/8/25
 */
@Slf4j
public class RedisLock {

  private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

  /**
   * 基于redis key实现Lock.
   */
  private String lockKey;

  /**
   * 锁超时时间,防止线程在入锁以后,无限的执行等待.
   */
  private int expireMills = 5 * 1000;

  /**
   * 锁等待时间,防止线程饥饿.
   */
  private int timeoutMills = 30 * 1000;

  // Redis连接池
  private Pool pool;

  private volatile boolean locked = false;

  /**
   * 锁的下一次超时时间.
   */
  private volatile long lockTimeOut;

  private final Object mutex = new Object();

  /**
   * 初始化锁.
   *
   * @param pool - jedis客户端.
   * @param lockKey - 锁的key
   */
  public RedisLock(Pool pool, String lockKey) {
    Objects.requireNonNull(pool);
    checkArgument(lockKey != null && lockKey.length() > 0, "lockKey must be not empty!");

    this.pool = pool;
    this.lockKey = lockKey + "_lock";
  }

  /**
   * 初始化锁.
   *
   * @param pool - jedis客户端.
   * @param lockKey - 锁的key
   * @param timeoutMills - 获取锁的超时时间
   */
  public RedisLock(Pool pool, String lockKey, int timeoutMills) {
    this(pool, lockKey);

    checkArgument(timeoutMills > 0, "timeoutMills must be greater than zero!");
    this.timeoutMills = timeoutMills;
  }

  /**
   * 初始化锁.
   *
   * @param pool - jedis客户端.
   * @param lockKey - 锁的key
   * @param timeoutMills - 获取锁的超时时间
   * @param expireMills - 锁的过期时间
   */
  public RedisLock(Pool pool, String lockKey, int timeoutMills, int expireMills) {
    this(pool, lockKey, timeoutMills);

    checkArgument(expireMills > 0, "expireMills must be greater than zero!");
    this.expireMills = expireMills;
  }

  /**
   * 获取redis中lock的key.
   */
  public String getLockKey() {
    return lockKey;
  }

  /**
   * 获取锁.
   */
  public boolean lock() throws InterruptedException {
    int timeout = timeoutMills;
    synchronized (mutex) {
      try (Jedis jedis = pool.getResource()) {
        while (timeout >= 0) {
          // 失效的时间戳
          long expires = System.currentTimeMillis() + expireMills + 1;
          lockTimeOut = expires;

          // ===========================
          // case1: 直接获取lock成功
          // ===========================

          long ret = jedis.setnx(lockKey, String.valueOf(expires));
          if (ret == 1) {
            if (log.isDebugEnabled()) {
              log.debug("lock setnx, got locked!");
            }
            locked = true;
            return locked;
          }

          // ===========================
          // case2: 判断超时时间.
          // ===========================

          // redis中的超时时间
          String timeoutInRedis = jedis.get(lockKey);

          /**
           * 1. key可能已经被别的线程删除了,所以必须判断!=null.
           * 2. redis中的锁已经超时了
           */
          if (timeoutInRedis != null && Long.parseLong(timeoutInRedis) < System
              .currentTimeMillis()) {

            // 返回redis中的的过期时间,并设置新的过期时间,key不存在时返回为空.
            String oldTimeOut = jedis.getSet(lockKey, String.valueOf(expires));
            if (oldTimeOut != null && oldTimeOut.equals(timeoutInRedis)) {
              if (log.isDebugEnabled()) {
                log.debug("lock timeout, get locked!");
              }
              locked = true;
              return true;
            }
          }

          timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;

          /**
           * 延迟0-100毫秒,可以防止饥饿进程的出现。
           即,当同时到达多个进程,只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面又来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
           使用随机的等待时间可以一定程度上保证公平性
           *
           */
          TimeUnit.MILLISECONDS.sleep(RandomUtils.nextInt(100));
        }
      }
    }

    if (log.isDebugEnabled()) {
      log.debug("got failed!");
    }
    return false;
  }

  /**
   * 解锁. Note: 持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,
   * 操作完的时候,锁因为超时已经被别人获得,这时就不必解锁了.
   */

  public synchronized void unlock() {
    if (locked) {
      if (System.currentTimeMillis() < lockTimeOut) {
        if (log.isDebugEnabled()) {
          log.debug("unlocked!");
        }
        try (Jedis jedis = pool.getResource()) {
          jedis.del(lockKey);
        }
      } else {
        if (log.isDebugEnabled()) {
          log.debug("not need unlocked!");
        }
      }
      locked = false;
    }
  }


}

你可能感兴趣的:(基于Redis实现分布式锁)