Skip to content

Instantly share code, notes, and snippets.

@CK110
Forked from sudot/JedisRedisLock.java
Created March 9, 2022 01:00

Revisions

  1. @sudot sudot revised this gist Jul 17, 2021. 1 changed file with 32 additions and 14 deletions.
    46 changes: 32 additions & 14 deletions JedisRedisLock.java
    Original file line number Diff line number Diff line change
    @@ -12,12 +12,15 @@
    *
    * @author tangjialin on 2018-03-18.
    */
    public class JedisRedisLock implements java.util.concurrent.locks.Lock {
    public class JedisRedisLock implements java.util.concurrent.locks.Lock, AutoCloseable {
    /** 锁的默认过期时长:10秒 */
    private static final Duration DEFAULT_EXPIRATION_TIME = Duration.ofSeconds(10L);
    private Jedis jedis;
    private String key;
    private long lock;
    private Duration expirationTime;
    /** 锁实际的过期时长 */
    private final Duration expirationTime;
    /** 锁的KEY */
    private final String key;
    private final Jedis jedis;
    private long lockValue;

    /**
    * 构造函数
    @@ -30,6 +33,17 @@ public JedisRedisLock(Jedis jedis, String name, String key) {
    this(jedis, name, key, DEFAULT_EXPIRATION_TIME);
    }

    /**
    * 构造函数
    *
    * @param jedis Redis客户端连接
    * @param name 锁的命名
    * @param expirationTime 锁的过期时间,若在此时间内未解锁,则自动解锁
    */
    public JedisRedisLock(Jedis jedis, String name, Duration expirationTime) {
    this(jedis, name, null, expirationTime);
    }

    /**
    * 构造函数
    *
    @@ -76,8 +90,7 @@ public JedisRedisLock(Jedis jedis, String name, String key, Duration expirationT
    * redisTemplate.execute((RedisCallback<Boolean>) connection -> {
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * String command = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    * Boolean success = connection.eval(serializer.serialize(command), ReturnType.BOOLEAN, 1, serializer.serialize(key), serializer.serialize(String.valueOf(lock)));
    * return success;
    * return connection.eval(serializer.serialize(command), ReturnType.BOOLEAN, 1, serializer.serialize(key), serializer.serialize(String.valueOf(lock)));
    * });
    * </pre>
    *
    @@ -143,8 +156,8 @@ private void releasableLock(String key, long lock) {
    * // expireTime单位:毫秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expiration.toMillis());
    * // expireTime单位:秒
    * String result = jedis.set(lockKey, lockMark, "NX", "EX", expiration.getSeconds());
    * return "OK".equals(result);
    * String ok = jedis.set(lockKey, lockMark, "NX", "EX", expiration.getSeconds());
    * return "OK".equals(ok);
    *
    * redisTemplate实现方式:
    * return redisTemplate.execute(connection -> {
    @@ -195,8 +208,8 @@ public void lockInterruptibly() throws InterruptedException {

    @Override
    public boolean tryLock() {
    lock = lock(key, expirationTime);
    return lock != 0;
    lockValue = lock(key, expirationTime);
    return lockValue != 0;
    }

    @Override
    @@ -206,12 +219,12 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    while (!tryLock() && time > System.currentTimeMillis()) {
    Thread.sleep(100);
    }
    return lock != 0;
    return lockValue != 0;
    }

    @Override
    public void unlock() {
    releasableLock(key, lock);
    releasableLock(key, lockValue);
    }

    @Override
    @@ -222,4 +235,9 @@ public Condition newCondition() {
    public String getKey() {
    return key;
    }
    }

    @Override
    public void close() throws Exception {
    this.unlock();
    }
    }
  2. @sudot sudot revised this gist Jul 17, 2021. 1 changed file with 17 additions and 1 deletion.
    18 changes: 17 additions & 1 deletion SpringRedisTemplateRedisLock.java
    Original file line number Diff line number Diff line change
    @@ -16,7 +16,7 @@
    *
    * @author tangjialin on 2018-03-18.
    */
    public class SpringRedisTemplateRedisLock implements java.util.concurrent.locks.Lock {
    public class SpringRedisTemplateRedisLock implements java.util.concurrent.locks.Lock, AutoCloseable {
    /** 锁的默认过期时长:10秒 */
    private static final Duration DEFAULT_EXPIRATION_TIME = Duration.ofSeconds(10L);
    /** 锁实际的过期时长 */
    @@ -37,6 +37,17 @@ public SpringRedisTemplateRedisLock(RedisTemplate<?, ?> redisTemplate, String na
    this(redisTemplate, name, key, DEFAULT_EXPIRATION_TIME);
    }

    /**
    * 构造函数
    *
    * @param redisTemplate RedisTemplate
    * @param name 锁的命名
    * @param expirationTime 锁的过期时间,若在此时间内未解锁,则自动解锁
    */
    public SpringRedisTemplateRedisLock(RedisTemplate<?, ?> redisTemplate, String name, Duration expirationTime) {
    this(redisTemplate, name, null, DEFAULT_EXPIRATION_TIME);
    }

    /**
    * 构造函数
    *
    @@ -233,4 +244,9 @@ public Condition newCondition() {
    public String getKey() {
    return key;
    }

    @Override
    public void close() throws Exception {
    this.unlock();
    }
    }
  3. @sudot sudot revised this gist Jul 17, 2021. 1 changed file with 32 additions and 33 deletions.
    65 changes: 32 additions & 33 deletions SpringRedisTemplateRedisLock.java
    Original file line number Diff line number Diff line change
    @@ -17,11 +17,14 @@
    * @author tangjialin on 2018-03-18.
    */
    public class SpringRedisTemplateRedisLock implements java.util.concurrent.locks.Lock {
    /** 锁的默认过期时长:10秒 */
    private static final Duration DEFAULT_EXPIRATION_TIME = Duration.ofSeconds(10L);
    private RedisTemplate<?, ?> redisTemplate;
    private String key;
    private long lock;
    private Duration expirationTime;
    /** 锁实际的过期时长 */
    private final Duration expirationTime;
    /** 锁的KEY */
    private final String key;
    private final RedisTemplate<?, ?> redisTemplate;
    private String lockValue;

    /**
    * 构造函数
    @@ -50,8 +53,7 @@ public SpringRedisTemplateRedisLock(RedisTemplate<?, ?> redisTemplate, String na

    /**
    * 释放锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <h2>方案一(有缺陷,已由方案二代替):</h2>
    * <pre>
    * 方案一的问题在于:
    * 如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。
    @@ -78,29 +80,26 @@ public SpringRedisTemplateRedisLock(RedisTemplate<?, ?> redisTemplate, String na
    *
    * redisTemplate实现方式:
    * redisTemplate.execute((RedisCallback<Boolean>) connection -> {
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * String command = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    * Boolean success = connection.eval(serializer.serialize(command), ReturnType.BOOLEAN, 1, serializer.serialize(key), serializer.serialize(String.valueOf(lock)));
    * return success;
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * String command = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    * return connection.eval(serializer.serialize(command), ReturnType.BOOLEAN, 1, serializer.serialize(key), serializer.serialize(lockValue));
    * });
    * </pre>
    *
    * @param key 锁
    * @param lock 获得的锁
    * @param key
    * @param lockValue 获得的锁
    */
    private void releasableLock(String key, long lock) {
    private void releasableLock(String key, String lockValue) {
    redisTemplate.execute((RedisCallback<Boolean>) connection -> {
    RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    String command = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Boolean success = connection.eval(serializer.serialize(command), ReturnType.BOOLEAN, 1, serializer.serialize(key), serializer.serialize(String.valueOf(lock)));
    return success;
    return connection.eval(serializer.serialize(command), ReturnType.BOOLEAN, 1, serializer.serialize(key), serializer.serialize(lockValue));
    });
    }

    /**
    * 获得执行锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <h2>方案一(有缺陷,已由方案二代替):</h2>
    * <pre>
    * 方案一的问题在于:
    * 1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
    @@ -155,13 +154,13 @@ private void releasableLock(String key, long lock) {
    *
    * redisTemplate实现方式:
    * return redisTemplate.execute(connection -> {
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * long timeMillis = System.currentTimeMillis();
    * byte[] rawKey = serializer.serialize(key);
    * byte[] rawValue = serializer.serialize(String.valueOf(timeMillis));
    * // 系统时间
    * Boolean set = connection.set(rawKey, rawValue, Expiration.seconds(expirationPeriod), RedisStringCommands.SetOption.SET_IF_ABSENT);
    * return Boolean.TRUE.equals(set) ? timeMillis : 0L;
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * String value = String.valueOf(System.currentTimeMillis());
    * byte[] rawKey = serializer.serialize(key);
    * byte[] rawValue = serializer.serialize(value);
    * // 系统时间
    * Boolean set = connection.set(rawKey, rawValue, Expiration.from(expiration), RedisStringCommands.SetOption.SET_IF_ABSENT);
    * return Boolean.TRUE.equals(set) ? value : null;
    * }, true);
    *
    * Redis指令说明
    @@ -181,15 +180,15 @@ private void releasableLock(String key, long lock) {
    * @param expiration 锁的过期时间,若在此时间内未解锁,则自动解锁
    * @return 返回过期的时间.返回0表示未成功加锁
    */
    private long lock(String key, Duration expiration) {
    private String lock(String key, Duration expiration) {
    return redisTemplate.execute(connection -> {
    RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    long timeMillis = System.currentTimeMillis();
    String value = String.valueOf(System.currentTimeMillis());
    byte[] rawKey = serializer.serialize(key);
    byte[] rawValue = serializer.serialize(String.valueOf(timeMillis));
    byte[] rawValue = serializer.serialize(value);
    // 系统时间
    Boolean set = connection.set(rawKey, rawValue, Expiration.from(expiration), RedisStringCommands.SetOption.SET_IF_ABSENT);
    return Boolean.TRUE.equals(set) ? timeMillis : 0L;
    return Boolean.TRUE.equals(set) ? value : null;
    }, true);
    }

    @@ -207,8 +206,8 @@ public void lockInterruptibly() throws InterruptedException {

    @Override
    public boolean tryLock() {
    lock = lock(key, expirationTime);
    return lock != 0;
    lockValue = lock(key, expirationTime);
    return lockValue != null;
    }

    @Override
    @@ -218,12 +217,12 @@ public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    while (!tryLock() && time > System.currentTimeMillis()) {
    Thread.sleep(100);
    }
    return lock != 0;
    return lockValue != null;
    }

    @Override
    public void unlock() {
    releasableLock(key, lock);
    releasableLock(key, lockValue);
    }

    @Override
    @@ -234,4 +233,4 @@ public Condition newCondition() {
    public String getKey() {
    return key;
    }
    }
    }
  4. @sudot sudot revised this gist Dec 1, 2018. 2 changed files with 2 additions and 2 deletions.
    2 changes: 1 addition & 1 deletion JedisRedisLock.java
    Original file line number Diff line number Diff line change
    @@ -91,7 +91,7 @@ private void releasableLock(String key, long lock) {
    }

    /**
    * 判断定时任务是否能执行,并在能执行的时候获得执行锁
    * 获得执行锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <pre>
    2 changes: 1 addition & 1 deletion SpringRedisTemplateRedisLock.java
    Original file line number Diff line number Diff line change
    @@ -98,7 +98,7 @@ private void releasableLock(String key, long lock) {
    }

    /**
    * 判断定时任务是否能执行,并在能执行的时候获得执行锁
    * 获得执行锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <pre>
  5. @sudot sudot revised this gist Nov 27, 2018. 2 changed files with 6 additions and 2 deletions.
    6 changes: 5 additions & 1 deletion JedisRedisLock.java
    Original file line number Diff line number Diff line change
    @@ -8,7 +8,7 @@
    import java.util.concurrent.locks.Condition;

    /**
    * 判断定时任务是否能执行,并在能执行的时候获得执行锁
    * 使用Redis服务实现的分布式锁
    *
    * @author tangjialin on 2018-03-18.
    */
    @@ -218,4 +218,8 @@ public void unlock() {
    public Condition newCondition() {
    throw new UnsupportedOperationException();
    }

    public String getKey() {
    return key;
    }
    }
    2 changes: 1 addition & 1 deletion SpringRedisTemplateRedisLock.java
    Original file line number Diff line number Diff line change
    @@ -12,7 +12,7 @@
    import java.util.concurrent.locks.Condition;

    /**
    * 判断定时任务是否能执行,并在能执行的时候获得执行锁
    * 使用Redis服务实现的分布式锁
    *
    * @author tangjialin on 2018-03-18.
    */
  6. @sudot sudot revised this gist Nov 27, 2018. 2 changed files with 37 additions and 143 deletions.
    97 changes: 19 additions & 78 deletions JedisRedisLock.java
    Original file line number Diff line number Diff line change
    @@ -9,69 +9,6 @@

    /**
    * 判断定时任务是否能执行,并在能执行的时候获得执行锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <pre>
    * 方案一的问题在于:
    * 1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
    * 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
    * 3. 锁不具备拥有者标识,即任何客户端都可以解锁。
    *
    * 以下为方案一实现逻辑:
    * A 向 lock.foo 发送 SETNX 命令。
    * 因为崩溃掉的 B 还锁着 lock.foo ,所以 Redis 向 A 返回 0 。
    * A 向 lock.foo 发送 GET 命令,查看 lock.foo 的锁是否过期。如果不,则休眠(sleep)一段时间,并在之后重试。
    * 另一方面,如果 lock.foo 内的 unix 时间戳比当前时间戳老,A 执行以下命令:
    * GETSET lock.foo <current Unix timestamp + lock timeout + 1>
    *
    * 因为 GETSET 的作用,A 可以检查看 GETSET 的返回值,确定 lock.foo 之前储存的旧值仍是那个过期时间戳,如果是的话,那么 A 获得锁。
    * 如果其他客户端,比如 C,比 A 更快地执行了 GETSET 操作并获得锁,那么 A 的 GETSET 操作返回的就是一个未过期的时间戳(C 设置的时间戳)。A 只好从第一步开始重试。
    * 注意,即便 A 的 GETSET 操作对 key 进行了修改,这对未来也没什么影响。
    *
    * 这里假设锁key对应的value没有实际业务意义,否则会有问题,而且其实其value也确实不应该用在业务中。
    *
    * // 系统时间
    * long timeMillis = System.currentTimeMillis();
    * // 过期时间点(如果获得锁之后,在什么时候未释放算过期)
    * long expirationTime = timeMillis + 1000L * expirationPeriod;
    * ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();
    * // 设置过期时间并返回操作结果,若为true则表示成功获得锁
    * if (opsForValue.setIfAbsent(key, expirationTime)) {
    * // 成功获得锁之后,设置锁本身的过期时间,防止锁一直存在于redis中
    * redisTemplate.expire(key, expirationPeriod, TimeUnit.SECONDS);
    * return expirationTime;
    * }
    * // 未获得锁,获得当前锁的过期时间
    * Long oldExpirationTime = (Long) opsForValue.get(key);
    * oldExpirationTime = oldExpirationTime == null ? 0 : oldExpirationTime;
    * // 若timeMillis < oldExpirationTime,则该锁未过期,获取锁失败
    * if (timeMillis < oldExpirationTime) { return 0; }
    * // 当锁已过期,尝试获得锁,并返回上一次锁的过期时间
    * Long andSet = (Long) opsForValue.getAndSet(key, expirationTime);
    * if (andSet == null) { return expirationTime; }
    * // 若oldExpirationTime == andSet,则表示获得了锁,否则表示锁被其它操作获得
    * return oldExpirationTime.longValue() == andSet.longValue() ? expirationTime : 0;
    * </pre>
    * <h2>方案二:</h2>
    * <pre>
    * Jedis原生API实现方式:
    * // expireTime单位:毫秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * // expireTime单位:秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * return "OK".equals(result);
    *
    * redisTemplate实现方式:
    * return redisTemplate.execute(connection -> {
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * long timeMillis = System.currentTimeMillis();
    * byte[] rawKey = serializer.serialize(key);
    * byte[] rawValue = serializer.serialize(String.valueOf(timeMillis));
    * // 系统时间
    * Boolean set = connection.set(rawKey, rawValue, Expiration.seconds(expirationPeriod), RedisStringCommands.SetOption.SET_IF_ABSENT);
    * return Boolean.TRUE.equals(set) ? timeMillis : 0L;
    * }, true);
    * </pre>
    *
    * @author tangjialin on 2018-03-18.
    */
    @@ -103,7 +40,7 @@ public JedisRedisLock(Jedis jedis, String name, String key) {
    */
    public JedisRedisLock(Jedis jedis, String name, String key, Duration expirationTime) {
    this.jedis = jedis;
    this.key = name + ":" + key;
    this.key = (key == null || key.isEmpty()) ? name : name + ":" + key;
    this.expirationTime = expirationTime;
    }

    @@ -125,6 +62,8 @@ public JedisRedisLock(Jedis jedis, String name, String key, Duration expirationT
    * redisTemplate.delete(key);
    * }
    * </pre>
    *
    *
    * <h2>方案二:</h2>
    * <pre>
    * Jedis调用原生API执行Lua脚本实现方式:
    @@ -196,13 +135,15 @@ private void releasableLock(String key, long lock) {
    * // 若oldExpirationTime == andSet,则表示获得了锁,否则表示锁被其它操作获得
    * return oldExpirationTime.longValue() == andSet.longValue() ? expirationTime : 0;
    * </pre>
    *
    *
    * <h2>方案二:</h2>
    * <pre>
    * Jedis原生API实现方式:
    * // expireTime单位:毫秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expiration.toMillis());
    * // expireTime单位:秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * String result = jedis.set(lockKey, lockMark, "NX", "EX", expiration.getSeconds());
    * return "OK".equals(result);
    *
    * redisTemplate实现方式:
    @@ -215,6 +156,18 @@ private void releasableLock(String key, long lock) {
    * Boolean set = connection.set(rawKey, rawValue, Expiration.seconds(expirationPeriod), RedisStringCommands.SetOption.SET_IF_ABSENT);
    * return Boolean.TRUE.equals(set) ? timeMillis : 0L;
    * }, true);
    *
    * Redis指令说明
    * SET key value [EX seconds] [PX milliseconds] [NX|XX]
    * 将字符串值 value 关联到 key 。
    * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。
    * 对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
    * 可选参数
    * 从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
    * EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
    * PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
    * NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
    * XX :只在键已经存在时,才对键进行设置操作。
    * </pre>
    *
    * @param key 锁
    @@ -224,18 +177,6 @@ private void releasableLock(String key, long lock) {
    private long lock(String key, Duration expiration) {
    if (key == null) { return 0L; }
    long timeMillis = System.currentTimeMillis();
    /**
    * SET key value [EX seconds] [PX milliseconds] [NX|XX]
    * 将字符串值 value 关联到 key 。
    * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。
    * 对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
    * 可选参数
    * 从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
    * EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
    * PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
    * NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
    * XX :只在键已经存在时,才对键进行设置操作。
    */
    String ok = jedis.set(key, String.valueOf(timeMillis), "NX", "EX", expiration.getSeconds());
    return "OK".equals(ok) ? timeMillis : 0L;
    }
    83 changes: 18 additions & 65 deletions SpringRedisTemplateRedisLock.java
    Original file line number Diff line number Diff line change
    @@ -13,69 +13,6 @@

    /**
    * 判断定时任务是否能执行,并在能执行的时候获得执行锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <pre>
    * 方案一的问题在于:
    * 1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
    * 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
    * 3. 锁不具备拥有者标识,即任何客户端都可以解锁。
    *
    * 以下为方案一实现逻辑:
    * A 向 lock.foo 发送 SETNX 命令。
    * 因为崩溃掉的 B 还锁着 lock.foo ,所以 Redis 向 A 返回 0 。
    * A 向 lock.foo 发送 GET 命令,查看 lock.foo 的锁是否过期。如果不,则休眠(sleep)一段时间,并在之后重试。
    * 另一方面,如果 lock.foo 内的 unix 时间戳比当前时间戳老,A 执行以下命令:
    * GETSET lock.foo <current Unix timestamp + lock timeout + 1>
    *
    * 因为 GETSET 的作用,A 可以检查看 GETSET 的返回值,确定 lock.foo 之前储存的旧值仍是那个过期时间戳,如果是的话,那么 A 获得锁。
    * 如果其他客户端,比如 C,比 A 更快地执行了 GETSET 操作并获得锁,那么 A 的 GETSET 操作返回的就是一个未过期的时间戳(C 设置的时间戳)。A 只好从第一步开始重试。
    * 注意,即便 A 的 GETSET 操作对 key 进行了修改,这对未来也没什么影响。
    *
    * 这里假设锁key对应的value没有实际业务意义,否则会有问题,而且其实其value也确实不应该用在业务中。
    *
    * // 系统时间
    * long timeMillis = System.currentTimeMillis();
    * // 过期时间点(如果获得锁之后,在什么时候未释放算过期)
    * long expirationTime = timeMillis + 1000L * expirationPeriod;
    * ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();
    * // 设置过期时间并返回操作结果,若为true则表示成功获得锁
    * if (opsForValue.setIfAbsent(key, expirationTime)) {
    * // 成功获得锁之后,设置锁本身的过期时间,防止锁一直存在于redis中
    * redisTemplate.expire(key, expirationPeriod, TimeUnit.SECONDS);
    * return expirationTime;
    * }
    * // 未获得锁,获得当前锁的过期时间
    * Long oldExpirationTime = (Long) opsForValue.get(key);
    * oldExpirationTime = oldExpirationTime == null ? 0 : oldExpirationTime;
    * // 若timeMillis < oldExpirationTime,则该锁未过期,获取锁失败
    * if (timeMillis < oldExpirationTime) { return 0; }
    * // 当锁已过期,尝试获得锁,并返回上一次锁的过期时间
    * Long andSet = (Long) opsForValue.getAndSet(key, expirationTime);
    * if (andSet == null) { return expirationTime; }
    * // 若oldExpirationTime == andSet,则表示获得了锁,否则表示锁被其它操作获得
    * return oldExpirationTime.longValue() == andSet.longValue() ? expirationTime : 0;
    * </pre>
    * <h2>方案二:</h2>
    * <pre>
    * Jedis原生API实现方式:
    * // expireTime单位:毫秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * // expireTime单位:秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * return "OK".equals(result);
    *
    * redisTemplate实现方式:
    * return redisTemplate.execute(connection -> {
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * long timeMillis = System.currentTimeMillis();
    * byte[] rawKey = serializer.serialize(key);
    * byte[] rawValue = serializer.serialize(String.valueOf(timeMillis));
    * // 系统时间
    * Boolean set = connection.set(rawKey, rawValue, Expiration.seconds(expirationPeriod), RedisStringCommands.SetOption.SET_IF_ABSENT);
    * return Boolean.TRUE.equals(set) ? timeMillis : 0L;
    * }, true);
    * </pre>
    *
    * @author tangjialin on 2018-03-18.
    */
    @@ -129,6 +66,8 @@ public SpringRedisTemplateRedisLock(RedisTemplate<?, ?> redisTemplate, String na
    * redisTemplate.delete(key);
    * }
    * </pre>
    *
    *
    * <h2>方案二:</h2>
    * <pre>
    * Jedis调用原生API执行Lua脚本实现方式:
    @@ -203,13 +142,15 @@ private void releasableLock(String key, long lock) {
    * // 若oldExpirationTime == andSet,则表示获得了锁,否则表示锁被其它操作获得
    * return oldExpirationTime.longValue() == andSet.longValue() ? expirationTime : 0;
    * </pre>
    *
    *
    * <h2>方案二:</h2>
    * <pre>
    * Jedis原生API实现方式:
    * // expireTime单位:毫秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expiration.toMillis());
    * // expireTime单位:秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * String result = jedis.set(lockKey, lockMark, "NX", "EX", expiration.getSeconds());
    * return "OK".equals(result);
    *
    * redisTemplate实现方式:
    @@ -222,6 +163,18 @@ private void releasableLock(String key, long lock) {
    * Boolean set = connection.set(rawKey, rawValue, Expiration.seconds(expirationPeriod), RedisStringCommands.SetOption.SET_IF_ABSENT);
    * return Boolean.TRUE.equals(set) ? timeMillis : 0L;
    * }, true);
    *
    * Redis指令说明
    * SET key value [EX seconds] [PX milliseconds] [NX|XX]
    * 将字符串值 value 关联到 key 。
    * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。
    * 对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
    * 可选参数
    * 从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
    * EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
    * PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
    * NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
    * XX :只在键已经存在时,才对键进行设置操作。
    * </pre>
    *
    * @param key 锁
  7. @sudot sudot created this gist Nov 27, 2018.
    280 changes: 280 additions & 0 deletions JedisRedisLock.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,280 @@
    package net.sudot.commons.lock;

    import redis.clients.jedis.Jedis;

    import java.time.Duration;
    import java.util.Collections;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Condition;

    /**
    * 判断定时任务是否能执行,并在能执行的时候获得执行锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <pre>
    * 方案一的问题在于:
    * 1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
    * 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
    * 3. 锁不具备拥有者标识,即任何客户端都可以解锁。
    *
    * 以下为方案一实现逻辑:
    * A 向 lock.foo 发送 SETNX 命令。
    * 因为崩溃掉的 B 还锁着 lock.foo ,所以 Redis 向 A 返回 0 。
    * A 向 lock.foo 发送 GET 命令,查看 lock.foo 的锁是否过期。如果不,则休眠(sleep)一段时间,并在之后重试。
    * 另一方面,如果 lock.foo 内的 unix 时间戳比当前时间戳老,A 执行以下命令:
    * GETSET lock.foo <current Unix timestamp + lock timeout + 1>
    *
    * 因为 GETSET 的作用,A 可以检查看 GETSET 的返回值,确定 lock.foo 之前储存的旧值仍是那个过期时间戳,如果是的话,那么 A 获得锁。
    * 如果其他客户端,比如 C,比 A 更快地执行了 GETSET 操作并获得锁,那么 A 的 GETSET 操作返回的就是一个未过期的时间戳(C 设置的时间戳)。A 只好从第一步开始重试。
    * 注意,即便 A 的 GETSET 操作对 key 进行了修改,这对未来也没什么影响。
    *
    * 这里假设锁key对应的value没有实际业务意义,否则会有问题,而且其实其value也确实不应该用在业务中。
    *
    * // 系统时间
    * long timeMillis = System.currentTimeMillis();
    * // 过期时间点(如果获得锁之后,在什么时候未释放算过期)
    * long expirationTime = timeMillis + 1000L * expirationPeriod;
    * ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();
    * // 设置过期时间并返回操作结果,若为true则表示成功获得锁
    * if (opsForValue.setIfAbsent(key, expirationTime)) {
    * // 成功获得锁之后,设置锁本身的过期时间,防止锁一直存在于redis中
    * redisTemplate.expire(key, expirationPeriod, TimeUnit.SECONDS);
    * return expirationTime;
    * }
    * // 未获得锁,获得当前锁的过期时间
    * Long oldExpirationTime = (Long) opsForValue.get(key);
    * oldExpirationTime = oldExpirationTime == null ? 0 : oldExpirationTime;
    * // 若timeMillis < oldExpirationTime,则该锁未过期,获取锁失败
    * if (timeMillis < oldExpirationTime) { return 0; }
    * // 当锁已过期,尝试获得锁,并返回上一次锁的过期时间
    * Long andSet = (Long) opsForValue.getAndSet(key, expirationTime);
    * if (andSet == null) { return expirationTime; }
    * // 若oldExpirationTime == andSet,则表示获得了锁,否则表示锁被其它操作获得
    * return oldExpirationTime.longValue() == andSet.longValue() ? expirationTime : 0;
    * </pre>
    * <h2>方案二:</h2>
    * <pre>
    * Jedis原生API实现方式:
    * // expireTime单位:毫秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * // expireTime单位:秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * return "OK".equals(result);
    *
    * redisTemplate实现方式:
    * return redisTemplate.execute(connection -> {
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * long timeMillis = System.currentTimeMillis();
    * byte[] rawKey = serializer.serialize(key);
    * byte[] rawValue = serializer.serialize(String.valueOf(timeMillis));
    * // 系统时间
    * Boolean set = connection.set(rawKey, rawValue, Expiration.seconds(expirationPeriod), RedisStringCommands.SetOption.SET_IF_ABSENT);
    * return Boolean.TRUE.equals(set) ? timeMillis : 0L;
    * }, true);
    * </pre>
    *
    * @author tangjialin on 2018-03-18.
    */
    public class JedisRedisLock implements java.util.concurrent.locks.Lock {
    private static final Duration DEFAULT_EXPIRATION_TIME = Duration.ofSeconds(10L);
    private Jedis jedis;
    private String key;
    private long lock;
    private Duration expirationTime;

    /**
    * 构造函数
    *
    * @param jedis Redis客户端连接
    * @param name 锁的命名
    * @param key 锁的key
    */
    public JedisRedisLock(Jedis jedis, String name, String key) {
    this(jedis, name, key, DEFAULT_EXPIRATION_TIME);
    }

    /**
    * 构造函数
    *
    * @param jedis Redis客户端连接
    * @param name 锁的命名
    * @param key 锁的key
    * @param expirationTime 锁的过期时间,若在此时间内未解锁,则自动解锁
    */
    public JedisRedisLock(Jedis jedis, String name, String key, Duration expirationTime) {
    this.jedis = jedis;
    this.key = name + ":" + key;
    this.expirationTime = expirationTime;
    }

    /**
    * 释放锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <pre>
    * 方案一的问题在于:
    * 如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。
    * 那么是否真的有这种场景?答案是肯定的。
    * 比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了。
    * 此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。
    *
    * // 若获得的锁小于当前时间,则该锁已过期,无须释放锁(加1秒,延迟处理)
    * if (lock + 1000L <= System.currentTimeMillis()) { return; }
    * Long andSet = (Long) redisTemplate.opsForValue().getAndSet(key, lock);
    * if (andSet != null && andSet.longValue() == lock) {
    * redisTemplate.delete(key);
    * }
    * </pre>
    * <h2>方案二:</h2>
    * <pre>
    * Jedis调用原生API执行Lua脚本实现方式:
    * String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    * Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    * Long success = 1L;
    * return success.equals(result);
    *
    * redisTemplate实现方式:
    * redisTemplate.execute((RedisCallback<Boolean>) connection -> {
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * String command = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    * Boolean success = connection.eval(serializer.serialize(command), ReturnType.BOOLEAN, 1, serializer.serialize(key), serializer.serialize(String.valueOf(lock)));
    * return success;
    * });
    * </pre>
    *
    * @param key 锁
    * @param lock 获得的锁
    */
    private void releasableLock(String key, long lock) {
    if (key == null) { return; }
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    jedis.eval(script, Collections.singletonList(key), Collections.singletonList(String.valueOf(lock)));
    }

    /**
    * 判断定时任务是否能执行,并在能执行的时候获得执行锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <pre>
    * 方案一的问题在于:
    * 1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
    * 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
    * 3. 锁不具备拥有者标识,即任何客户端都可以解锁。
    *
    * 以下为方案一实现逻辑:
    * A 向 lock.foo 发送 SETNX 命令。
    * 因为崩溃掉的 B 还锁着 lock.foo ,所以 Redis 向 A 返回 0 。
    * A 向 lock.foo 发送 GET 命令,查看 lock.foo 的锁是否过期。如果不,则休眠(sleep)一段时间,并在之后重试。
    * 另一方面,如果 lock.foo 内的 unix 时间戳比当前时间戳老,A 执行以下命令:
    * GETSET lock.foo <current Unix timestamp + lock timeout + 1>
    *
    * 因为 GETSET 的作用,A 可以检查看 GETSET 的返回值,确定 lock.foo 之前储存的旧值仍是那个过期时间戳,如果是的话,那么 A 获得锁。
    * 如果其他客户端,比如 C,比 A 更快地执行了 GETSET 操作并获得锁,那么 A 的 GETSET 操作返回的就是一个未过期的时间戳(C 设置的时间戳)。A 只好从第一步开始重试。
    * 注意,即便 A 的 GETSET 操作对 key 进行了修改,这对未来也没什么影响。
    *
    * 这里假设锁key对应的value没有实际业务意义,否则会有问题,而且其实其value也确实不应该用在业务中。
    *
    * // 系统时间
    * long timeMillis = System.currentTimeMillis();
    * // 过期时间点(如果获得锁之后,在什么时候未释放算过期)
    * long expirationTime = timeMillis + 1000L * expirationPeriod;
    * ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();
    * // 设置过期时间并返回操作结果,若为true则表示成功获得锁
    * if (opsForValue.setIfAbsent(key, expirationTime)) {
    * // 成功获得锁之后,设置锁本身的过期时间,防止锁一直存在于redis中
    * redisTemplate.expire(key, expirationPeriod, TimeUnit.SECONDS);
    * return expirationTime;
    * }
    * // 未获得锁,获得当前锁的过期时间
    * Long oldExpirationTime = (Long) opsForValue.get(key);
    * oldExpirationTime = oldExpirationTime == null ? 0 : oldExpirationTime;
    * // 若timeMillis < oldExpirationTime,则该锁未过期,获取锁失败
    * if (timeMillis < oldExpirationTime) { return 0; }
    * // 当锁已过期,尝试获得锁,并返回上一次锁的过期时间
    * Long andSet = (Long) opsForValue.getAndSet(key, expirationTime);
    * if (andSet == null) { return expirationTime; }
    * // 若oldExpirationTime == andSet,则表示获得了锁,否则表示锁被其它操作获得
    * return oldExpirationTime.longValue() == andSet.longValue() ? expirationTime : 0;
    * </pre>
    * <h2>方案二:</h2>
    * <pre>
    * Jedis原生API实现方式:
    * // expireTime单位:毫秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * // expireTime单位:秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * return "OK".equals(result);
    *
    * redisTemplate实现方式:
    * return redisTemplate.execute(connection -> {
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * long timeMillis = System.currentTimeMillis();
    * byte[] rawKey = serializer.serialize(key);
    * byte[] rawValue = serializer.serialize(String.valueOf(timeMillis));
    * // 系统时间
    * Boolean set = connection.set(rawKey, rawValue, Expiration.seconds(expirationPeriod), RedisStringCommands.SetOption.SET_IF_ABSENT);
    * return Boolean.TRUE.equals(set) ? timeMillis : 0L;
    * }, true);
    * </pre>
    *
    * @param key 锁
    * @param expiration 锁的过期时间,若在此时间内未解锁,则自动解锁
    * @return 返回过期的时间.返回0表示未成功加锁
    */
    private long lock(String key, Duration expiration) {
    if (key == null) { return 0L; }
    long timeMillis = System.currentTimeMillis();
    /**
    * SET key value [EX seconds] [PX milliseconds] [NX|XX]
    * 将字符串值 value 关联到 key 。
    * 如果 key 已经持有其他值, SET 就覆写旧值,无视类型。
    * 对于某个原本带有生存时间(TTL)的键来说, 当 SET 命令成功在这个键上执行时, 这个键原有的 TTL 将被清除。
    * 可选参数
    * 从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
    * EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
    * PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
    * NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
    * XX :只在键已经存在时,才对键进行设置操作。
    */
    String ok = jedis.set(key, String.valueOf(timeMillis), "NX", "EX", expiration.getSeconds());
    return "OK".equals(ok) ? timeMillis : 0L;
    }

    @Override
    public void lock() {
    while (!tryLock()) {
    try {Thread.sleep(100);} catch (InterruptedException e) {}
    }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
    throw new UnsupportedOperationException();
    }

    @Override
    public boolean tryLock() {
    lock = lock(key, expirationTime);
    return lock != 0;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    if (time <= 0) { return tryLock(); }
    time = System.currentTimeMillis() + unit.toMillis(time);
    while (!tryLock() && time > System.currentTimeMillis()) {
    Thread.sleep(100);
    }
    return lock != 0;
    }

    @Override
    public void unlock() {
    releasableLock(key, lock);
    }

    @Override
    public Condition newCondition() {
    throw new UnsupportedOperationException();
    }
    }
    284 changes: 284 additions & 0 deletions SpringRedisTemplateRedisLock.java
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,284 @@
    package net.sudot.commons.lock;

    import org.springframework.data.redis.connection.RedisStringCommands;
    import org.springframework.data.redis.connection.ReturnType;
    import org.springframework.data.redis.core.RedisCallback;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.core.types.Expiration;
    import org.springframework.data.redis.serializer.RedisSerializer;

    import java.time.Duration;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.locks.Condition;

    /**
    * 判断定时任务是否能执行,并在能执行的时候获得执行锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <pre>
    * 方案一的问题在于:
    * 1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
    * 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
    * 3. 锁不具备拥有者标识,即任何客户端都可以解锁。
    *
    * 以下为方案一实现逻辑:
    * A 向 lock.foo 发送 SETNX 命令。
    * 因为崩溃掉的 B 还锁着 lock.foo ,所以 Redis 向 A 返回 0 。
    * A 向 lock.foo 发送 GET 命令,查看 lock.foo 的锁是否过期。如果不,则休眠(sleep)一段时间,并在之后重试。
    * 另一方面,如果 lock.foo 内的 unix 时间戳比当前时间戳老,A 执行以下命令:
    * GETSET lock.foo <current Unix timestamp + lock timeout + 1>
    *
    * 因为 GETSET 的作用,A 可以检查看 GETSET 的返回值,确定 lock.foo 之前储存的旧值仍是那个过期时间戳,如果是的话,那么 A 获得锁。
    * 如果其他客户端,比如 C,比 A 更快地执行了 GETSET 操作并获得锁,那么 A 的 GETSET 操作返回的就是一个未过期的时间戳(C 设置的时间戳)。A 只好从第一步开始重试。
    * 注意,即便 A 的 GETSET 操作对 key 进行了修改,这对未来也没什么影响。
    *
    * 这里假设锁key对应的value没有实际业务意义,否则会有问题,而且其实其value也确实不应该用在业务中。
    *
    * // 系统时间
    * long timeMillis = System.currentTimeMillis();
    * // 过期时间点(如果获得锁之后,在什么时候未释放算过期)
    * long expirationTime = timeMillis + 1000L * expirationPeriod;
    * ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();
    * // 设置过期时间并返回操作结果,若为true则表示成功获得锁
    * if (opsForValue.setIfAbsent(key, expirationTime)) {
    * // 成功获得锁之后,设置锁本身的过期时间,防止锁一直存在于redis中
    * redisTemplate.expire(key, expirationPeriod, TimeUnit.SECONDS);
    * return expirationTime;
    * }
    * // 未获得锁,获得当前锁的过期时间
    * Long oldExpirationTime = (Long) opsForValue.get(key);
    * oldExpirationTime = oldExpirationTime == null ? 0 : oldExpirationTime;
    * // 若timeMillis < oldExpirationTime,则该锁未过期,获取锁失败
    * if (timeMillis < oldExpirationTime) { return 0; }
    * // 当锁已过期,尝试获得锁,并返回上一次锁的过期时间
    * Long andSet = (Long) opsForValue.getAndSet(key, expirationTime);
    * if (andSet == null) { return expirationTime; }
    * // 若oldExpirationTime == andSet,则表示获得了锁,否则表示锁被其它操作获得
    * return oldExpirationTime.longValue() == andSet.longValue() ? expirationTime : 0;
    * </pre>
    * <h2>方案二:</h2>
    * <pre>
    * Jedis原生API实现方式:
    * // expireTime单位:毫秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * // expireTime单位:秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * return "OK".equals(result);
    *
    * redisTemplate实现方式:
    * return redisTemplate.execute(connection -> {
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * long timeMillis = System.currentTimeMillis();
    * byte[] rawKey = serializer.serialize(key);
    * byte[] rawValue = serializer.serialize(String.valueOf(timeMillis));
    * // 系统时间
    * Boolean set = connection.set(rawKey, rawValue, Expiration.seconds(expirationPeriod), RedisStringCommands.SetOption.SET_IF_ABSENT);
    * return Boolean.TRUE.equals(set) ? timeMillis : 0L;
    * }, true);
    * </pre>
    *
    * @author tangjialin on 2018-03-18.
    */
    public class SpringRedisTemplateRedisLock implements java.util.concurrent.locks.Lock {
    private static final Duration DEFAULT_EXPIRATION_TIME = Duration.ofSeconds(10L);
    private RedisTemplate<?, ?> redisTemplate;
    private String key;
    private long lock;
    private Duration expirationTime;

    /**
    * 构造函数
    *
    * @param redisTemplate RedisTemplate
    * @param name 锁的命名
    * @param key 锁的key
    */
    public SpringRedisTemplateRedisLock(RedisTemplate<?, ?> redisTemplate, String name, String key) {
    this(redisTemplate, name, key, DEFAULT_EXPIRATION_TIME);
    }

    /**
    * 构造函数
    *
    * @param redisTemplate RedisTemplate
    * @param name 锁的命名
    * @param key 锁的key
    * @param expirationTime 锁的过期时间,若在此时间内未解锁,则自动解锁
    */
    public SpringRedisTemplateRedisLock(RedisTemplate<?, ?> redisTemplate, String name, String key, Duration expirationTime) {
    this.redisTemplate = redisTemplate;
    this.key = (key == null || key.isEmpty()) ? name : name + ":" + key;
    this.expirationTime = expirationTime;
    }

    /**
    * 释放锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <pre>
    * 方案一的问题在于:
    * 如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。
    * 那么是否真的有这种场景?答案是肯定的。
    * 比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了。
    * 此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。
    *
    * // 若获得的锁小于当前时间,则该锁已过期,无须释放锁(加1秒,延迟处理)
    * if (lock + 1000L <= System.currentTimeMillis()) { return; }
    * Long andSet = (Long) redisTemplate.opsForValue().getAndSet(key, lock);
    * if (andSet != null && andSet.longValue() == lock) {
    * redisTemplate.delete(key);
    * }
    * </pre>
    * <h2>方案二:</h2>
    * <pre>
    * Jedis调用原生API执行Lua脚本实现方式:
    * String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    * Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    * Long success = 1L;
    * return success.equals(result);
    *
    * redisTemplate实现方式:
    * redisTemplate.execute((RedisCallback<Boolean>) connection -> {
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * String command = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    * Boolean success = connection.eval(serializer.serialize(command), ReturnType.BOOLEAN, 1, serializer.serialize(key), serializer.serialize(String.valueOf(lock)));
    * return success;
    * });
    * </pre>
    *
    * @param key 锁
    * @param lock 获得的锁
    */
    private void releasableLock(String key, long lock) {
    redisTemplate.execute((RedisCallback<Boolean>) connection -> {
    RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    String command = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Boolean success = connection.eval(serializer.serialize(command), ReturnType.BOOLEAN, 1, serializer.serialize(key), serializer.serialize(String.valueOf(lock)));
    return success;
    });
    }

    /**
    * 判断定时任务是否能执行,并在能执行的时候获得执行锁
    * <h2>方案一:</h2>
    * <p>方案一有缺陷,已由方案二代替</p>
    * <pre>
    * 方案一的问题在于:
    * 1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
    * 2. 当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
    * 3. 锁不具备拥有者标识,即任何客户端都可以解锁。
    *
    * 以下为方案一实现逻辑:
    * A 向 lock.foo 发送 SETNX 命令。
    * 因为崩溃掉的 B 还锁着 lock.foo ,所以 Redis 向 A 返回 0 。
    * A 向 lock.foo 发送 GET 命令,查看 lock.foo 的锁是否过期。如果不,则休眠(sleep)一段时间,并在之后重试。
    * 另一方面,如果 lock.foo 内的 unix 时间戳比当前时间戳老,A 执行以下命令:
    * GETSET lock.foo <current Unix timestamp + lock timeout + 1>
    *
    * 因为 GETSET 的作用,A 可以检查看 GETSET 的返回值,确定 lock.foo 之前储存的旧值仍是那个过期时间戳,如果是的话,那么 A 获得锁。
    * 如果其他客户端,比如 C,比 A 更快地执行了 GETSET 操作并获得锁,那么 A 的 GETSET 操作返回的就是一个未过期的时间戳(C 设置的时间戳)。A 只好从第一步开始重试。
    * 注意,即便 A 的 GETSET 操作对 key 进行了修改,这对未来也没什么影响。
    *
    * 这里假设锁key对应的value没有实际业务意义,否则会有问题,而且其实其value也确实不应该用在业务中。
    *
    * // 系统时间
    * long timeMillis = System.currentTimeMillis();
    * // 过期时间点(如果获得锁之后,在什么时候未释放算过期)
    * long expirationTime = timeMillis + 1000L * expirationPeriod;
    * ValueOperations<String, Object> opsForValue = redisTemplate.opsForValue();
    * // 设置过期时间并返回操作结果,若为true则表示成功获得锁
    * if (opsForValue.setIfAbsent(key, expirationTime)) {
    * // 成功获得锁之后,设置锁本身的过期时间,防止锁一直存在于redis中
    * redisTemplate.expire(key, expirationPeriod, TimeUnit.SECONDS);
    * return expirationTime;
    * }
    * // 未获得锁,获得当前锁的过期时间
    * Long oldExpirationTime = (Long) opsForValue.get(key);
    * oldExpirationTime = oldExpirationTime == null ? 0 : oldExpirationTime;
    * // 若timeMillis < oldExpirationTime,则该锁未过期,获取锁失败
    * if (timeMillis < oldExpirationTime) { return 0; }
    * // 当锁已过期,尝试获得锁,并返回上一次锁的过期时间
    * Long andSet = (Long) opsForValue.getAndSet(key, expirationTime);
    * if (andSet == null) { return expirationTime; }
    * // 若oldExpirationTime == andSet,则表示获得了锁,否则表示锁被其它操作获得
    * return oldExpirationTime.longValue() == andSet.longValue() ? expirationTime : 0;
    * </pre>
    * <h2>方案二:</h2>
    * <pre>
    * Jedis原生API实现方式:
    * // expireTime单位:毫秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * // expireTime单位:秒
    * String result = jedis.set(lockKey, lockMark, "NX", "PX", expireTime);
    * return "OK".equals(result);
    *
    * redisTemplate实现方式:
    * return redisTemplate.execute(connection -> {
    * RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    * long timeMillis = System.currentTimeMillis();
    * byte[] rawKey = serializer.serialize(key);
    * byte[] rawValue = serializer.serialize(String.valueOf(timeMillis));
    * // 系统时间
    * Boolean set = connection.set(rawKey, rawValue, Expiration.seconds(expirationPeriod), RedisStringCommands.SetOption.SET_IF_ABSENT);
    * return Boolean.TRUE.equals(set) ? timeMillis : 0L;
    * }, true);
    * </pre>
    *
    * @param key 锁
    * @param expiration 锁的过期时间,若在此时间内未解锁,则自动解锁
    * @return 返回过期的时间.返回0表示未成功加锁
    */
    private long lock(String key, Duration expiration) {
    return redisTemplate.execute(connection -> {
    RedisSerializer<String> serializer = redisTemplate.getStringSerializer();
    long timeMillis = System.currentTimeMillis();
    byte[] rawKey = serializer.serialize(key);
    byte[] rawValue = serializer.serialize(String.valueOf(timeMillis));
    // 系统时间
    Boolean set = connection.set(rawKey, rawValue, Expiration.from(expiration), RedisStringCommands.SetOption.SET_IF_ABSENT);
    return Boolean.TRUE.equals(set) ? timeMillis : 0L;
    }, true);
    }

    @Override
    public void lock() {
    while (!tryLock()) {
    try {Thread.sleep(100);} catch (InterruptedException e) {}
    }
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
    throw new UnsupportedOperationException();
    }

    @Override
    public boolean tryLock() {
    lock = lock(key, expirationTime);
    return lock != 0;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
    if (time <= 0) { return tryLock(); }
    time = System.currentTimeMillis() + unit.toMillis(time);
    while (!tryLock() && time > System.currentTimeMillis()) {
    Thread.sleep(100);
    }
    return lock != 0;
    }

    @Override
    public void unlock() {
    releasableLock(key, lock);
    }

    @Override
    public Condition newCondition() {
    throw new UnsupportedOperationException();
    }

    public String getKey() {
    return key;
    }
    }