基于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<Jedis> 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<Jedis> 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<Jedis> 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<Jedis> 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;
    }
  }


}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容