分布式锁原理分析

Zookeeper系列——6Zookeeper的分布式锁及Leader选举原理分析 分布式锁的实现原理;Zookeeper实现分布式锁的核心流程解析;Zookeeper实现Leader选举的核心思想 阅读详情

原理分析

最近看到好多博主都在推分布式锁,实现方式很多,基于db、redis、zookeeper。zookeeper方式实现起来比较繁琐,这里我们就谈谈基于redis实现分布式锁的正确实现方式。

背景

在很多互联网产品应用中,有些场景需要加锁处理,比如:秒杀,全局递增ID,楼层生成等等。大部分的解决方案是基于DB实现的,Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系。 其次Redis提供一些命令SETNX,GETSET,可以方便实现分布式锁机制。

Redis命令介绍

使用Redis实现分布式锁,有两个重要函数需要介绍。

SETNX命令(SET if Not Exists)

  • 语法:

SETNX key value
  • 功能:
    当且仅当 key 不存在,将 key 的值设为 value ,并返回1; 若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。

GETSET命令

  • 语法:

GETSET key value
  • 功能:
    将给定 key 的值设为 value ,并返回 key 的旧值 (old value), 当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。

GET命令

  • 语法:

GET key
  • 功能:
    返回 key 所关联的字符串值,如果 key 不存在那么返回特殊值 nil 。

DEL命令

  • 语法:

DEL key [KEY …]
  • 功能:
    删除给定的一个或多个 key ,不存在的 key 会被忽略。

兵贵精,不在多。分布式锁,我们就依靠这四个命令。但在具体实现,还有很多细节,需要仔细斟酌,因为在分布式并发多进程中,任何一点出现差错,都会导致死锁,hold住所有进程。

加锁实现

SETNX 可以直接加锁操作,比如说对某个关键词foo加锁,客户端可以尝试 SETNX foo.lock <current unix time>。

  • 如果返回1,表示客户端已经获取锁,可以往下操作,操作完成后,通过 DEL foo.lock 命令来释放锁。

  • 如果返回0,说明foo已经被其他客户端上锁,如果锁是非堵塞的,可以选择返回调用。如果是堵塞调用,就需要进入下一个重试循环,直至成功获得锁或者重试超时。

理想是美好的,现实是残酷的。仅仅使用SETNX加锁带有竞争条件的,在某些特定的情况会造成死锁错误。

处理死锁

在上面的处理方式中,如果获取锁的客户端执行时间过长,进程被kill掉,或者因为其他异常崩溃,导致无法释放锁,就会造成死锁。所以,需要对加锁要做时效性检测。

因此,我们在加锁时,把当前时间戳作为value存入此锁中,通过当前时间戳和redis中的时间戳进行对比,如果超过一定差值,认为锁已经时效,防止锁无限期的锁下去。

但是,在大并发情况,如果同时检测锁失效,并简单粗暴的删除死锁,再通过SETNX上锁,可能会导致竞争条件的产生,即多个客户端同时获取锁。

情景描述如下:

  1. C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,获得foo.lock的时间戳,通过比对时间戳,发现锁超时。

  2. C2 向foo.lock发送DEL命令。

  3. C2 向foo.lock发送SETNX获取锁。

  4. C3 向foo.lock发送DEL命令,此时C3发送DEL时,其实DEL掉的是C2的锁。

  5. C3 向foo.lock发送SETNX获取锁。

此时C2和C3都获取了锁,产生竞争条件,如果在更高并发的情况,可能会有更多客户端获取锁。

所以,DEL锁的操作,不能直接使用在锁超时的情况下,幸好我们有GETSET方法,假设我们现在有另外一个客户端C4,看看如何使用GETSET方式,避免这种情况产生。

  1. C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,调用GET命令获得foo.lock的时间戳T1,通过比对时间戳,发现锁超时。

  2. C4(调用SETNX上锁返回0后,调用GET命令获得foo.lock的时间戳T1,通过比对时间戳,发现锁超时)向foo.lock发送GESET命令,GETSET foo.lock 并得到foo.lock中老的时间戳T2。

    • 如果T1=T2,说明C4获得锁。

    • 如果T1!=T2,说明C4之前有另外一个客户端C5通过调用GETSET方式获取并更改了时间戳,C4未获得锁。只能进入下次循环中。

时间戳问题

我们看到foo.lock的value值为时间戳,所以要在多客户端情况下,保证锁有效,一定要同步各服务器的时间。如果各服务器间,时间有差异,时间不一致的客户端,在判断锁超时,就会出现偏差,从而产生竞争条件。锁的超时与否,严格依赖时间戳。

锁覆盖问题

现在唯一的问题是,C4设置foo.lock的新时间戳,是否会对C5获取得锁产生影响?

其实我们可以看到C4和C5只有在调用GET命令获得foo.lock的时间戳,通过比对时间戳,发现锁超时后,几乎同时调用GETSET方式获取锁,执行的时间差值极小,并且写入foo.lock中的都是有效时间戳,所以对锁并没有影响。

为了让这个锁更加强壮,获取锁的客户端,应该在调用关键业务时,再次调用GET方法获取T1,和写入的T0时间戳进行对比,以免锁因其他情况被执行DEL意外解开而不知。但是如果遇到上面描述得问题,则T0则会与T1不一致,当然差别一般会很小。这就是锁覆盖问题。

锁覆盖会导致什么问题呢?

当客户端的锁过期时间被覆盖,会造成锁不具有标识性,会造成客户端无法释放锁(客户端只能释放明确自己持有的锁)。

nil 问题

GET返回nil时应该走哪种逻辑?

一、第一种走循环走setnx逻辑

  1. C1客户端获取锁,并且处理完后,DEL掉锁。

  2. 在DEL锁之前,C2通过SETNX向foo.lock设置时间戳T0失败,发现有客户端获取锁,进入GET操作。C2 向foo.lock发送GET命令,获取返回值T1(nil)(因为此时C1执行DEL删除锁)。

  3. C2 循环,进入下一次SETNX逻辑。

二、第二种走超时逻辑

  1. C1客户端获取锁,并且处理完后,DEL掉锁。

  2. 在DEL锁之前,C2通过SETNX向foo.lock设置时间戳T0发现有客户端获取锁,进入GET操作。C2 向foo.lock发送GET命令,获取返回值T1(nil)(因为此时C1执行DEL删除锁)。

  3. C2 通过 `T0 > T1 + expire` 对比,进入GETSET流程。

  4. C2调用GETSET向foo.lock发送T0时间戳,返回foo.lock的原值T2,C2判断如果T2=T1相等,获得锁,如果T2!=T1,未获得锁。

分析

两种逻辑貌似都是OK,但是从逻辑处理上来说,当GET返回nil,表示锁是被删除的,而不是超时,应该走SETNX逻辑加锁。

对于"第二种走超时逻辑"是否会造成死锁,尚不清楚,不过推荐采用第一种方式。

GETSET返回nil时应该怎么处理?

前提: 假设C4客户端获取锁后由于异常退出等原因未正常释放锁,导致锁超时。此时,C1、C2和C3客户端同时请求获取锁。C1、C2和C3客户端调用GET接 口,C1返回T1,此时C3网络情况更好,快速进入获取锁,并执行DEL删除锁,C2返回T2(nil)。C1进入超时处理逻辑。C2面临上面提到 「GET返回nil时应该走哪种逻辑?」的两种选择:1. 也进入超时处理逻辑;2. 继续循环走setnx逻辑(推荐);

  1. C1向foo.lock发送GETSET命令,获取返回值T11(nil)。C1比对C1和C11发现两者不同,处理逻辑认为未获取锁,然后继续循环走setnx逻辑。

  2. C2有两种选择:

    • 进入超时处理逻辑;
      C2 向foo.lock发送GETSET命令,获取返回值T22(C1写入的时间戳)。C2比对T2和T22发现两者不同,处理逻辑认为未获取锁,然后继续循环走setnx逻辑。

    • 继续循环走setnx逻辑;

  3. 很明显,C1和C2最终都会继续循环走setnx逻辑,然后通过SETNX向foo.lock设置时间戳T0会失败,这其实是因为在步骤1中C1执行GETSET命令导致的。此时C1和C2都认为未获取锁,其实C1是已经获取锁了,但是他的处理逻辑没有考虑GETSET返回nil的情况,只是单纯的用GET和GETSET值进行对比。

分析

至 于为什么会出现这种情况?就如上面设想的场景那样,多客户端时,每个客户端连接redis后,发出的命令并不是连续的,导致从单客户端看到的好像连续的命 令,到redis server后,这两条命令之间可能已经插入大量的其他客户端发出的命令,比如DEL,SETNX等。

正确的处理方式就是GETSET返回nil时,获取锁成功。

总结

  1. 必要的超时机制:获取锁的客户端一旦崩溃,一定要有过期机制,否则其他客户端都降无法获取锁,造成死锁问题。

  2. 分布式锁,多客户端的时间戳不能保证严格意义的一致性,所以在某些特定因素下,有可能存在问题。要适度的机制,可以承受小概率的事件产生。

  3. 只对关键处理节点加锁,良好的习惯是,把相关的资源准备好,比如连接数据库后,调用加锁机制获取锁,直接进行操作,然后释放,尽量减少持有锁的时间。

  4. 在持有锁期间要不要CHECK锁,如果需要严格依赖锁的状态,最好在关键步骤中做锁的CHECK检查机制,但是根据我们的测试发现,在大并发时,每一次CHECK锁操作,都要消耗掉几个毫秒,而我们的整个持锁处理逻辑才不到10毫秒,玩客没有选择做锁的检查。

  5. sleep 学问,为了减少对redis的压力,获取锁尝试时,循环之间一定要做sleep操作。但是sleep时间是多少是门学问。需要根据自己的redis的 QPS,加上持锁处理时间等进行合理计算。如果redis的QPS足够高,也可以考虑循环之间不sleep,循环一定次数/时间执行yeild,提高响应 速度。

  6. 至于为什么不使用Redis的muti,expire,watch等机制,可以查下参考资料,找下原因。


代码实现

代码库

https://github.com/HuTu92/distributed-lock

源码

package com.github.hutu92.concurrent.locks;

import com.alibaba.fastjson.JSON;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

/**
 * Created by liuchunlong on 2018/8/31.
 * <p>
 * 基于redis的分布式锁 v1
 *
 * 需要客户端时间同步
 */
public class DistributedLock {

    private static final long RETRY_BARRIER = 3 * 1000; // 请求锁重试屏障,单位毫秒

    private final JedisPool jedisPool; // redis连接池
    private final String lockKey; // lock Key
    private final long lockExpiryInNanos; // 锁的过期时长,单位纳秒

    private static final ThreadLocal<Lock> lockThreadLocal = new ThreadLocal<Lock>();

    /**
     * 构造方法
     *
     * @param jedisPool          redis连接池
     * @param lockKey            锁的Key
     * @param lockExpiryInMillis 锁的过期时长,单位毫秒
     */
    public DistributedLock(JedisPool jedisPool, String lockKey, long lockExpiryInMillis) {
        this.jedisPool = jedisPool;
        this.lockKey = lockKey;
        this.lockExpiryInNanos = lockExpiryInMillis * 1000;
    }

    /**
     * 构造方法
     * <p>
     * 使用锁默认的过期时长Integer.MAX_VALUE,即锁永远不会过期
     *
     * @param jedisPool redis连接池
     * @param lockKey   锁的Key
     */
    public DistributedLock(JedisPool jedisPool, String lockKey) {
        this(jedisPool, lockKey, Integer.MAX_VALUE);
    }

    /**
     * 获取锁在redis中的Key标记
     *
     * @return locks key
     */
    public String getLockKey() {
        return this.lockKey;
    }

    /**
     * 锁的过期时长
     *
     * @return
     */
    public long getLockExpiryInNanos() {
        return lockExpiryInNanos;
    }

    /**
     * 请求分布式锁,不会阻塞,直接返回
     *
     * @param jedis redis 连接
     * @return 成功获取锁返回true, 否则返回false
     */
    private boolean tryAcquire(Jedis jedis) {

        final Lock newLock = new Lock(System.nanoTime() + this.lockExpiryInNanos);

        /**
         * 将新锁(newLock)写入redis中。如果成功写入,redis中不存在锁,获取锁成功;否则,redis中已存在锁,获取锁失败;
         */
        if (jedis.setnx(this.lockKey, newLock.toString()) == 1) {
            lockThreadLocal.set(newLock);
            return true;
        }

        /**
         * 至此,说明redis中已存在锁,获取锁失败,则需要进行如下操作:
         * 1. 判断redis中已存在的锁是否过期,如果过期则直接获取锁;
         * 2. 否则,获取锁失败;
         */

        final String currentLockValue = jedis.get(lockKey);
        // 特别的,当jedis.get()获取已存在的锁currentLockValue为空时,应该重新SETNX
        if (currentLockValue == null || currentLockValue.length() == 0) {
            tryAcquire(jedis);
        }
        final Lock currentLock = Lock.fromJson(currentLockValue); // redis中已存在的锁

        // 如果redis中已存在的锁已超时,则重新获取锁
        if (isExpired(currentLock)) {
            String originLockValue = jedis.getSet(lockKey, newLock.toString());

            /**
             * 这里还有个前置条件:
             *      会对已存在的锁进行校验,jedis.get()和jedis.getSet()获取的锁必须是同一锁,重新获取锁才成功
             */

            // 特别的,当jedis.getSet()获取已存在的锁originLockValue为空时,则认定获取锁成功
            if (originLockValue == null || originLockValue.length() == 0) {
                lockThreadLocal.set(newLock);
                return true;
            }

            if (originLockValue.equals(currentLockValue)) {
                lockThreadLocal.set(newLock);
                return true;
            }
        }

        return false;
    }

    /**
     * 请求分布式锁,不会阻塞,直接返回
     *
     * @return 成功获取锁返回true, 否则返回false
     */
    public boolean tryAcquire() {

        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            return tryAcquire(jedis);
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 超时请求分布式锁,会阻塞
     *
     * 采用"自旋获取锁"的方式,直至获取锁成功或者请求锁超时
     *
     * @param acquireTimeoutInMillis 锁的请求超时时长
     * @return
     */
    public boolean acquire(long acquireTimeoutInMillis) {

        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();

            long acquireTime = System.currentTimeMillis();

            // 锁的请求到期时间
            long expiryTime = System.currentTimeMillis() + acquireTimeoutInMillis;

            while (expiryTime >= System.currentTimeMillis()) {
                boolean result = tryAcquire(jedis);
                if (result) { // 获取锁成功直接返回,否则循环重试
                    return true;
                }

                if ((System.currentTimeMillis() - acquireTime) > RETRY_BARRIER) {
                    Thread.yield();
                }
            }

        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
        return false;
    }

    /**
     * 释放锁
     */
    public void release() {

        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            release(jedis);
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }

    /**
     * 释放锁
     *
     * @param jedis
     */
    private void release(Jedis jedis) {
        Lock currlock = lockThreadLocal.get();
        if (currlock != null) {
            final String currentLockValue = jedis.get(lockKey);
            if (currentLockValue != null && currentLockValue.length() != 0) {
                final Lock currentLock = Lock.fromJson(currentLockValue); // redis中已存在的锁
                if (currlock.equals(currentLock)) {
                    lockThreadLocal.remove();
                    jedis.del(lockKey);
                }
            }
        }
    }

    /**
     * 判断当前线程是否持有锁
     *
     * 未持有锁或者锁超时,返回false
     *
     * @return
     */
    public boolean isLocked() {
        Lock currlock = lockThreadLocal.get();
        // 如果当前线程保存的lock不为null,并且未超时,则当前线程必然持有锁,锁未被意外释放
        return currlock != null && !currlock.isExpired();
    }

    /**
     * 判断指定的lock是否是当前线程持有的锁
     *
     * @return
     */
    boolean isMine(final Lock lock) {
        Lock currlock = lockThreadLocal.get();
        return currlock != null && currlock.equals(lock);
    }

    /**
     * 判断锁是否超时
     *
     * @param lock
     * @return
     */
    boolean isExpired(final Lock lock) {
        return lock.isExpired();
    }

    /**
     * 锁
     */
    protected static class Lock {

        private long expiryTime; // 锁的过期时间,注意,不是过期时长,单位纳秒

        Lock(long expiryTime) {
            this.expiryTime = expiryTime;
        }

        /**
         * 解析字符串,根据解析出的过期时间构造Lock
         *
         * @param json
         * @return
         */
        static Lock fromJson(String json) {
            return JSON.parseObject(json, Lock.class);
        }

        @Override
        public String toString() {
            return JSON.toJSONString(this, false);
        }

        public long getExpiryTime() {
            return expiryTime;
        }

        /**
         * 判断锁是否超时,如果锁的过期时间小于当前系统时间,则判定锁超时
         *
         * @return
         */
        boolean isExpired() {
            return this.expiryTime < System.nanoTime();
        }

        @Override
        public boolean equals(Object obj) {
            return obj != null
                    && obj instanceof Lock
                    && this.expiryTime == ((Lock) obj).getExpiryTime();
        }
    }
}

优化

上面存在的锁覆盖问题是不可避免的,还有就是要求客户端时间同步。下面我们进一步优化这一问题。

Redis命令介绍

SET

  • 语法:

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 :只在键已经存在时,才对键进行设置操作。

因为 SET 命令可以通过参数来实现和 SETNX 、 SETEX 和 PSETEX 三个命令的效果,所以将来的 Redis 版本可能会废弃并最终移除 SETNX 、 SETEX 和 PSETEX 这三个命令。

  • 返回值:
    在 Redis 2.6.12 版本以前, SET 命令总是返回 OK 。
    从 Redis 2.6.12 版本开始, SET 在设置操作成功完成时,才返回 OK 。
    如果设置了 NX 或者 XX ,但因为条件没达到而造成设置操作未执行,那么命令返回空批量回复(NULL Bulk Reply)。

使用模式

命令 SET resource-name anystring NX EX max-lock-time 是一种在 Redis 中实现锁的简单方法。

客户端执行以上的命令:

  • 如果服务器返回 OK ,那么这个客户端获得锁。

  • 如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。

设置的过期时间到达之后,锁将自动释放。

可以通过以下修改,让这个锁实现更健壮:

  • 不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)。

  • 不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。

这两个改动可以防止持有过期锁的客户端误删现有锁的情况出现。

以下是一个简单的解锁脚本示例:

if redis.call("get",KEYS[1]) == ARGV[1]
then
    return redis.call("del",KEYS[1])
else
    return 0
end

源码

package com.github.hutu92;

import com.alibaba.fastjson.JSON;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by liuchunlong on 2018/9/4.
 * <p>
 * 基于redis的分布式锁 v2
 * <p>
 * 不需要客户端时间同步
 */
public class DistributedLock {

    private static final long RETRY_BARRIER = 600; // 重试屏障,单位毫秒
    private static final long INTERVAL_TIMES = 200; // 下一次重试等待,单位毫秒

    private final JedisPool jedisPool; // redis连接池
    private final String lockKey; // lock Key
    private final long lockExpiryInMillis; // 锁的过期时长,单位纳秒

    private final ThreadLocal<Lock> lockThreadLocal = new ThreadLocal<Lock>();

    /**
     * 构造方法
     *
     * @param jedisPool          redis连接池
     * @param lockKey            锁的Key
     * @param lockExpiryInMillis 锁的过期时长,单位毫秒
     */
    public DistributedLock(JedisPool jedisPool, String lockKey, long lockExpiryInMillis) {
        this.jedisPool = jedisPool;
        this.lockKey = lockKey;
        this.lockExpiryInMillis = lockExpiryInMillis;
    }

    /**
     * 构造方法
     * <p>
     * 使用锁默认的过期时长Integer.MAX_VALUE,即锁永远不会过期
     *
     * @param jedisPool redis连接池
     * @param lockKey   锁的Key
     */
    public DistributedLock(JedisPool jedisPool, String lockKey) {
        this(jedisPool, lockKey, Integer.MAX_VALUE);
    }

    /**
     * 获取锁在redis中的Key标记
     *
     * @return locks key
     */
    public String getLockKey() {
        return this.lockKey;
    }

    /**
     * 锁的过期时长
     *
     * @return
     */
    public long getLockExpiryInMillis() {
        return lockExpiryInMillis;
    }

    /**
     * can override
     *
     * @param jedis
     * @return
     */
    private String nextUid(Jedis jedis) {
        // 可以考虑雪花算法..
        return UUID.randomUUID().toString();
    }

    private synchronized Jedis getClient() {
        return jedisPool.getResource();
    }

    private synchronized void closeClient(Jedis jedis) {
        jedis.close();
    }

    /**
     * 请求分布式锁,不会阻塞,直接返回
     *
     * @param jedis redis 连接
     * @return 成功获取锁返回true, 否则返回false
     */
    private boolean tryAcquire(Jedis jedis) {

        final Lock nLock = new Lock(nextUid(jedis));
        String result = jedis.set(this.lockKey, nLock.toString(), "NX", "PX", this.lockExpiryInMillis);
        if ("OK".equals(result)) {
            lockThreadLocal.set(nLock);
            return true;
        }
        return false;
    }

    /**
     * 请求分布式锁,不会阻塞,直接返回
     *
     * @return 成功获取锁返回true, 否则返回false
     */
    public boolean tryAcquire() {

        Jedis jedis = null;
        try {
            jedis = getClient();
            return tryAcquire(jedis);
        } finally {
            if (jedis != null) {
                closeClient(jedis);
            }
        }
    }

    /**
     * 超时请求分布式锁,会阻塞
     *
     * 采用"自旋获取锁"的方式,直至获取锁成功或者请求锁超时
     *
     * @param acquireTimeoutInMillis 锁的请求超时时长
     * @return
     */
    public boolean acquire(long acquireTimeoutInMillis) throws InterruptedException {

        Jedis jedis = null;
        try {

            jedis = getClient();

            long acquireTime = System.currentTimeMillis();
            long expiryTime = System.currentTimeMillis() + acquireTimeoutInMillis; // 锁的请求到期时间

            while (expiryTime >= System.currentTimeMillis()) {
                boolean result = tryAcquire(jedis);
                if (result) { // 获取锁成功直接返回,否则循环重试
                    return true;
                }

                Thread.sleep(INTERVAL_TIMES);
            }

        } finally {
            if (jedis != null) {
                closeClient(jedis);
            }
        }
        return false;
    }

    /**
     * 释放锁
     *
     * @return
     */
    public boolean release() throws InterruptedException {
        return release(Integer.MAX_VALUE);
    }

    /**
     * 释放锁
     *
     * @return
     */
    public boolean release(long releaseTimeoutInMillis) throws InterruptedException {

        Jedis jedis = null;
        try {
            jedis = getClient();
            return release(jedis, releaseTimeoutInMillis);
        } finally {
            if (jedis != null) {
                closeClient(jedis);
            }
        }
    }

    /**
     * 释放锁
     *
     * @param jedis
     * @param releaseTimeoutInMillis
     * @return
     */
    private boolean release(Jedis jedis, long releaseTimeoutInMillis) throws InterruptedException {
        Lock cLock = lockThreadLocal.get();
        if (cLock == null) {
            System.out.println("lock is null!");
        }
        if (cLock != null) {
            String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

            long releaseTime = System.currentTimeMillis();
            long expiryTime = System.currentTimeMillis() + releaseTimeoutInMillis; // 锁的释放到期时间

            while (expiryTime >= System.currentTimeMillis()) {
                Object result = jedis.eval(luaScript, Collections.singletonList(this.lockKey),
                        Collections.singletonList(cLock.toString()));
                if (((Long) result) == 1L) {
                    lockThreadLocal.remove();
                    return true;
                }

                Thread.sleep(INTERVAL_TIMES);
            }
        }
        return false;
    }


    /**
     * 锁
     */
    protected static class Lock {

        private String uid; // lock 唯一标识

        Lock(String uid) {
            this.uid = uid;
        }

        public String getUid() {
            return uid;
        }

        @Override
        public String toString() {
            return JSON.toJSONString(this, false);
        }
    }

}

性能调优

这里我们使用ab性能测试工具来模拟测试。

由于没有使用队列,对高并发请求进行削峰,所以所有的压力都会被打到redis上。为了测试方便我这里只是本地启动了单机redis,没有做其它的调优配置。

我们并发测试场景是1000个并发请求,总共2000个请求。

ab -n 2000 -c 1000 "localhost:8080/lock/v2/seckill"

上述的地址是一个接口,接口代码如下:

@RestController
@RequestMapping("/lock")
public class LockController {

    private static LongAdder longAdder = new LongAdder();
    private static Long ACQUIRE_TIMEOUT_IN_MILLIS = (long) Integer.MAX_VALUE;
    private static Long stock = 100000L;
    private static DistributedLock lock;

    static {
        longAdder.add(stock);
    }

    private final JedisPool jedisPool;

    @Autowired
    public LockController(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
        lock = new DistributedLock(jedisPool, "seckillV2_" + UUID.randomUUID().toString());
    }

    @GetMapping("/v2/seckill")
    public String seckillV2() throws InterruptedException {

        boolean acquireResult = false;
        try {
            acquireResult = lock.acquire(ACQUIRE_TIMEOUT_IN_MILLIS);

            if (!acquireResult) {
                return "人太多了,换个姿势操作一下!";
            }

            if (longAdder.longValue() == 0L) {
                return "已抢光!";
            }

            doSomeThing(jedisPool);

            longAdder.decrement();

            System.out.println("已抢: " + (stock - longAdder.longValue()) + ", 还剩下: " + longAdder.longValue());

        } finally {
            if (acquireResult) {
                boolean releaseResult = lock.release();
                if (!releaseResult) {
                    System.out.println("释放锁失败!");
                }
            }
        }

        return "OK";
    }

    private void doSomeThing(JedisPool jedisPool) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();

            jedis.incr("already_bought");
        } finally {
            if (jedis != null) {
                jedis.close();
            }
        }
    }
}

那么我们这里说的性能调优指的是什么呢?

仔 细分析上面的源码你会发现,获取锁的逻辑是循环获取的,再每次循环之间,应该怎么去处理?如果不做任何处理,直接继续下一个循环,表面上看能够及时的获取 锁,但这会给redis更大的压力,如果redis扛不住,到最后只会适得其反;而如果sleep等待,那么等待多久呢?等待久了,锁的获取和释放就会不 及时;使用yield如何?等等

No1

if ((System.currentTimeMillis() - acquireTime) > RETRY_BARRIER) {
    Thread.yield();
}

请求获取锁的前600毫秒内直接循环重试,如果超过600毫秒还未获取到锁则每次循环都将线程推迟到下一个时间片执行。

 

主要参数说明:

  • Failed requests:失败的请求

  • Time per request:每个请求的平均耗时

No2

if ((System.currentTimeMillis() - acquireTime) > RETRY_BARRIER) {
    Thread.sleep(INTERVAL_TIMES);
} else {
    Thread.yield();
}

请求获取锁的前600毫秒内每次循环重试都先将线程推迟到下一个时间片,如果超过600毫秒还未获取到锁则每次循环都将线程休眠200毫秒。

 

很明显,出错率降低了很多,每个请求的耗时也减少了一半,这是因为,No1中在600毫秒内的直接循环重试,会产生很多意义的请求,给redis造成了巨大的压力,无法响应请求。

No3

Thread.sleep(INTERVAL_TIMES);

请求获取锁的每次循环重试都将线程休眠200毫秒。

 

No4

Thread.sleep(INTERVAL_TIMES * 10);

请求获取锁的每次循环重试都将线程休眠2秒。

 

很明显,休眠时间过长,会使部分线程请求锁的时间变长,不能够及时获取到锁。

No5

Thread.yield();

请求获取锁的每次循环重试都将线程推迟到下一个时间片执行。

总结

总的来说,No2与No3表现的都还可以。但是No2使用了Thread.yield();也会给redis造成压力,我可以对比下两者的 Percentage of the requests served within a certain time (ms) 数据。可以看到No3的90%以下请求的用户平均时间要明显低于No2的。所以最终我们选择No3策略。

当然你也可以根据你的redis的QPS自行调整策略。

c# redis分布式锁 学习资料: 90分钟完全吃透Redis分布式锁!(C#/.NETCore/.NET Core)B0562_哔哩哔哩_bilibili 0-2是概念 3-4是代码演示 1、什么是分布式锁 普通锁机制: 悲观锁和乐观锁 性能极低 分布式锁: 为了解决多个进程操作共享资源出现竞争,需要保证数据库的安全性 阻塞锁就是一直等到拿到锁 非阻塞锁就是拿不到锁就不拿了 2、分布式锁能解决什么问题? 为了解决多个进程操作共享资源出现竞争,需要保证数据库的安全性 ... 阅读详情

相关推荐

C#中基于StackExchange.Redis的分布式锁

C#中基于StackExchange.Redis的分布式锁实现,拷贝即用,使用简单,无需配置

初夏de遐想的博客 3252

C#实操控制并发之Lock和分布式锁

其实就是一把锁,在分布式环境下,多个客户端并发访问的情况下,保证共享资源的串行有序访问,控制并发。Redis分布式锁底层采用setnx+expire命令组合来实现加锁,释放锁时,根据身份标识去释放各自创建的锁,各删各的锁。基于上面的问题,根据Redis来实现的Redis分布式锁就应允而生。因为Redis够快、支持事务、单命令支持原子性等这些特性,保证了Redis分布式锁的出色性能。但是个人很容易混淆,不易深刻理解这个锁对于并发的控制。本文会以秒杀为案例,简单记录下不同场景下的并发。

wzl644的专栏 2274

redisson 实现分布式锁的原理,源码分析

先上图,加锁场景和解锁场景。 加锁逻辑: redisson使用lua脚本操作redis来进行加锁,因为lua脚本中的代码逻辑在redis中执行能够保证原子性。 这个lua脚本就是加锁的核心代码,分析一下内容。 KEYS[1]就是要加锁的key,ARGV[1]是加锁超时时间,ARGV[2]是加锁线程唯一标识。 首先判断key是否存在; 若不存在则代表无锁状态,接下来设置锁信息,field为线程唯一标识,value是加锁次数,然后设置key的超时时间,最后返回nil,意味着获得了锁。 .

qq_40987088的博客 821

c# 分布式锁

环境: net4.5 vs2022 模拟商品抢购,单个服务器,可以通过本地服务器缓存或数据库锁就可以处理, 分布式服务器的话,想要实现进程与进程之间实现同步锁,可以使用redis、数据库锁、ZooKeeper来实现。 单体服务器: 可以通过HttpRuntime.Cache缓存来简单实现 public class RuntimeCacheLock { private static System.Web.Caching.Cache cache = HttpRun

czjnoe的博客 2610

DistributedLocking:分布式锁定C#库

分布式锁定 分布式锁的简单C#实现,可以在专注于可扩展性的多客户端(流程)环境中使用。 快速开始 该库旨在通过抽象使用,因此,无论选择哪种基础存储库,基本操作均保持不变。 您可以在下面找到基本用法示例。 获取锁 为了获取锁,您需要提供: 资源ID-可以识别您的资源的东西, 所需的锁的TTL(生存时间)-如果您的进程无法正常释放锁,则它必须自动将自身变为非活动状态,以避免无限锁。 为此,您有一个简单的界面: // IDistributedLockProvider provider = ... var resourceId = " Resource ID " ; var ttl = TimeSpan . FromMinutes ( 5 ); var distributedLock = await provider . TryAcquireAsync ( resourceI

Sparse4Dv3

instance_feature也设置为nn.Parameter。修正分支2*(linear->relu->linear->relu->layernorm)->linear->scale,分类分枝2*(linear->relu->layernorm)->linear(256,9),质量评估分枝2*(linear->relu->layernorm)->linear(256,2)。每组两类instance,1是加上范围(-x,x)内均匀分布噪声,2是加上(-2x,-x)U(x,2x)范围内均匀分布噪声。

yuanl15的博客 1351

Redission 分布式锁原理分析

此时,如果客户端 2 来尝试加锁,会如何呢?接着第二个 if 判断,判断一下,myLock 锁 key 的 hash 数据结构中,是否包含客户端 2 的 ID,这里明显不是,因为那里包含的是客户端 1 的 ID。鱼和熊掌不可兼得,如果你想强一致性的话可以选择 ZK 的分布式锁,但 ZK 的话性能就会有一定的下降,如果项目没有用到 ZK 的话,那就选择 Redis 的分布式锁吧,比较你为了那极小的概率而丢去性能以及引入一个组件很不划算,如果无法忍受 Redis 的红锁缺陷,那自己在业务中自己保证吧。

2401_87878486的博客 753

C语言实现TCP聊天室:Socket编程与并发模型实战解析

网络编程是构建分布式系统的核心技术,其核心在于理解进程间通信的底层机制。TCP/IP协议栈提供了可靠的字节流传输服务,通过Socket API这一抽象接口,开发者可以在应用层建立连接、收发数据。为了高效处理多个并发连接,服务器需要采用多线程或I/O复用模型,前者为每个连接创建独立线程,逻辑清晰但资源消耗大;后者通过select、poll或epoll等系统调用监控多个文件描述符,显著提升并发能力。这些技术是构建高性能网络服务(如即时通讯、在线游戏服务器)的基础。本文以C语言实现一个简洁的TCP聊天室为例,详细

weixin_32642109的博客 292

Zookeeper 分布式锁原理深入分析及实战

今天我们分析Zookeeper 分布式锁原理及实战,分布式锁在分布式服务中一定会用到的,且ZK的分布式锁相对比较稳定,可用性比较好,因此我们深入分析一下。 1、ZK分布式锁原理图: 1.1,争抢锁,只有一个人能获得锁 1.2,获得锁的客户端出问题,临时节点(session)就会自动删除,不需要设置 过期时间,也不会造成死锁问题。 1.3,获得锁的人成功了,使用后自动释放锁。 2、锁被释放、删除,其他客户端怎么知道,如图ZK的监听机制: 2.1:(可以想到的方案1)主动轮询,心跳检测等

nandao158的博客 1798

ESP8266 进行HDLC解码,AX.25,APRS

/************************************************************** 函数名:aprs_wait_begin() 参 数:无 返回值:bool true=得到合法脉冲,false=未得到 说 明:等待第1个833us高脉冲,判断范围 730-930,其他忽略 ***************************************************************/ bool aprs_wait_begin(){ stati

蓝蓝的博客 1630

HyperMesh二次开发实战:自动化Comp与材料属性批量绑定

本文详细介绍了HyperMesh二次开发中自动化Comp与材料属性批量绑定的实战技巧。通过TCL脚本实现批量创建属性卡、智能材料分配和异常处理,大幅提升前处理效率,适用于汽车、航空航天等领域的复杂模型处理。

weixin_29214691的博客 691

Redis分布式锁实现原理与实战案例分析

在分布式系统的世界里,多个服务实例同时运行时,如何协调它们对共享资源的访问成为一个棘手的问题。想象一下,如果多个快递员同时试图将包裹放入你家的信箱,没有协调机制的话,可能会导致包裹丢失或放错位置—这就是分布式系统面临的并发挑战。分布式系统并发挑战:为解决这些问题,分布式锁应运而生。而Redis作为分布式锁的实现方案,就像一个轻巧而高效的交通信号灯,以其独特的优势成为许多开发者的首选。为什么选择Redis实现分布式锁?与其他分布式锁方案相比,Redis展现出明显的优势和特点

sinat_27016095的博客 1974

PD3.0详解 协议的时序timer和counter计数

前面介绍了关于PD协议的交互,还有协议层数据各个字段的含义,但是一直没有系统的说关于协议的时序,还有部分counter,就是协议允许发送的次数,或者一些其他计数。 大师匈今天说一下关于这方面的东西,这些东西都是非常关键的东西,但是相对来说PD标准内对时序的要求还是比较宽泛,非常容易能够实现这样的要求。 Timer 1、CRCReceiveTimer 0.9~1.1ms 表示每次通信收到goodcrc的时间,必须在这个时间内收到goodcrc, 我们要注意不是在0.9~1.1ms内,而是第三方可以在这个范

大师匈的博客 5007

[Redis] Redisson分布式锁原理及源码分析

使用 Redis 的 SETNX 命令(Set if Not Exists)来确保只有一个客户端可以成功创建一个锁键。使用 EXPIRE 命令设置键的过期时间,防止死锁。 如果客户端成功设置了锁键,它就获得了锁;如果失败,则表示锁已被其他客户端占用。 为了解决锁失效或客户端故障问题,可以使用 Redisson 或者实现 Redlock 算法来增强锁的可靠性。

qq_45061342的博客 1455

【制作100个unity游戏之22】3DRPG游戏开发07——其他详细的设计

我们会有有很多单例,如果每个单例都去写会很麻烦,所以我们定义泛型单例模式 Singleton,我们将所有的单例都继承这个泛型单例,就省略很多重复的工作定义一个GameManager类,继承Singleton,控制整个游戏的进度,比如如果主角死了,广播,游戏结束调用。

向宇的博客,专注php/web全栈 unity游戏开发,欢迎大家评论纠错 2577

AI大模型:(三)3.2 Spring AI实现Agent

SpringAI实现了流式对话功能,通过与Ollama模型集成完成前后端交互,当前版本存在thinking信息丢失问题。系统支持两种工具注册方式(方法注解和函数式),并内置时间工具类展示外部工具调用能力。记忆功能提供多种存储方案(内存、JDBC、NoSQL等),实现跨对话轮次的上下文保持。MCP协议集成方面,服务端实现计算和业务查询工具,客户端支持12306及自定义业务系统接入。完整演示了流式对话、工具调用与记忆功能的综合应用。该实践构建了具备复杂交互能力的AI系统,同时指出生产环境需关注的安全与优化问题。

haylee的专栏 1009

Redisson分布式锁原理分析

公平锁就是指线程成功加锁的顺序,跟线程请求加锁的顺序一样,实现了先来先成功加锁的特点,不插队才叫公平前面所说的RedissonLock的实现都是非公平锁,但里面有些机制如watchdog机制是公平的。

Yu_Mariam的博客 1652

redhawk:Low Power Analysis

1.rush current与switch cell 在standby状态下为了控制leakage power我们选择power gating的设计方式,使用power switch cell关闭block/power domain的电源。 power switch的基本介绍可见: 低功耗设计-Power Switch power switch的table中有四种状态,对应block工作、待机和上电,下电。low power analysis主要分析上电的过程。 上电过程可以看做mos电容

m0_61544122的博客 3206

Redlock(redis分布式锁)原理分析

Redlock(redis分布式锁)原理分析 Redlock:全名叫做 Redis Distributed Lock;即使用redis实现的分布式锁; 使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击); 官网文档地址如下:https://redis.io/topics/distlock 这个锁的算法实现了多redis实例的情况,相对于单redis节点来说,优点在于 防止了 单节点故障造成整个服务停止运行的情况;并且在多节点中锁的设计,及多节点同时崩.

qq_28022397的博客 1344

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

基于Curator对zookeeper分布式锁、Leader选举的实践和原理分析

文章目录zookeeper在分布式锁和master选举实践一、包含的知识点二、基于Curator的分布式锁实现2.1 引如pom文件2.2 实现逻辑代码示例三、基于Curator的master选举实现3.1 引入pom文件3.2 实现逻辑代码示例四、Curator分布式锁实现原理4.1 分布式锁入口InterProcessMutex4.2 curator获取锁acquire4.3 curator释放锁release五、Curator选举Leader实现原理5.1 选举入口start()5.2 进行入队操作r

yewandemty的博客 546

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

分布式锁遇锁等待_Redisson 实现分布式锁原理分析

写在前面 在了解分布式锁具体实现方案之前,我们应该先思考一下使用分布式锁必须要考虑的一些问题。​互斥性:在任意时刻,只能有一个进程持有锁。防死锁:即使有一个进程在持有锁的期间崩溃而未能主动释放锁,要有其他方式去释放锁从而保证其他进程能获取到锁。加锁和解锁的必须是同一个进程。锁的续期问题。常见的分布式锁实现方案 基于 Redis 实现分布式锁基于 Zookeeper 实现分布式锁本文采用第一种方案,...

weixin_39853155的博客 3489

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

Redisson实现Redis分布式锁的原理分析

目录 一、写在前面 二、Redisson实现Redis分布式锁的底层原理 1、加锁机制 2、锁互斥机制 3、watch dog自动延期机制 4、可重入加锁机制 一、写在前面 现在面试,一般都会聊聊分布式系统这块的东西。通常面试官都会从服务框架(Spring Cloud、Dubbo)聊起,一路聊到分布式事务、分布式锁、ZooKeeper等知识。 所以咱们这篇文章就来聊聊分布式锁这块知识,具体的来看看Redis分布式锁的实现原理。 说实话,如果在公司里落地生产环境用分布式锁的时候,一定是会用

骏马逸动,心随你动的博客 9079

Redisson分布式锁——加锁原理基本介绍以及源码分析

Redis分布式锁相关基础可查阅——redis实现分布式锁(手写redis分布式锁) Redisson的基本使用查阅我的另一篇博文——Redisson——分布式锁的基本应用 其实之前我在写Redis锁的使用过程中,已经是查阅了很多相关的资料,算是对redis分布式锁有了一个新的认识;在使用Riddson的过程中,Riddson已经帮开发者做了很好的封装,几乎是无侵入的帮助开发者勿略掉了分布式锁的实现问题,简单有效。由于最近比较忙,现在重新捡起来Riddson,来简单的聊一聊Riddson是怎么实现分布式

保暖透气大裤衩LeoLee的博客 2594

Redisson 分布式锁原理分析

Redisson 分布式锁原理分析

Charge8的博客 3898

分布式锁的原理分析

在多级部署的情况下,很容易出现多机竞争问题,我最近做的项目中就有遇到过,我详细的研究了一下市场上常用的一些分布式锁,在这里分享一下每种分布式锁的原理其实在项目开发中,我们经常涉及到锁的概念,就是当不同线程或进程对同一个资源进行访问时,就需要使用锁来保证并发安全问题,保护我们的临界资源。在 Java 的各个线程之间,我们使用来保证并发安全,只能应用于单个进程里边的锁,不同进程中的线程无法使用单机锁老保证并发安全。随着网络服务的进一步增加,很多场景都需要我们从单机环境提升到分布式环境,所以我们就需要使用。

2401_89221445的博客 807
上一篇: Redis 分布式锁实现(2)
下一篇: IDEA重构快捷键
低调的杉菜
博客等级 码龄15年 12粉丝 34原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值