分布式锁

当我们在设计分布式锁的时候,我们应该考虑分布式锁至少要满足的一些条件,同时考虑如何高效的设计分布式锁

1、互斥

在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。

2、防止死锁

在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。

所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。

3、性能

对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。

所以在锁的设计时,需要考虑两点。

  • 1、锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。

  • 2、锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。

4、重入

我们知道ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。关于这点之后会做演示。

普通的 Redis 分布式锁的缺陷

我们在网上看到的redis分布式锁的工具方法,大都满足互斥、防止死锁的特性,有些工具方法会满足可重入特性。

如果只满足上述3种特性会有哪些隐患呢?redis分布式锁无法自动续期,比如,一个锁设置了1分钟超时释放,如果拿到这个锁的线程在一分钟内没有执行完毕,那么这个锁就会被其他线程拿到,可能会导致严重的线上问题,我已经在秒杀系统故障排查文章中,看到好多因为这个缺陷导致的超卖了。

针对以上Redisson都能很好的满足,下面就来分析下它。

Redisson

据Redisson官网的介绍,Redisson是一个Java Redis客户端,与Spring 提供给我们的 RedisTemplate 工具没有本质的区别,可以把它看做是一个功能更强大的客户端(虽然官网上声称Redisson不只是一个Java Redis客户端)

Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类,支持 Redis 单实例、Redis 哨兵、Redis Cluster、Redis master-slave 等各种部署架构。Redisson使用Lua脚本方式将多个非原子命令封装在一起,一起发送给服务端,保证操作的原子性

redisson是目前redis分布式锁相对完美的实现,更多详情可以通过Redisson了解

Redisson简单使用

RLock lock = redisson.getLock("lockName");
try{
    //可以设置超时时间
    lock.lock();
    //业务逻辑
} finally {
    lock.unlock();
}

Redisson分布式锁工具类

/**
 * @Author: huangyibo
 * @Date: 2023/7/12 11:42
 * @Description: Redisson 分布式锁
 *
 * 总结
 * 1.要使 watchLog机制生效 ,lock时 不要设置 过期时间
 * 2.watchlog的延时时间 可以由 lockWatchdogTimeout指定默认延时时间,但是不要设置太小。如100
 * 3.watchdog 会每 lockWatchdogTimeout/3时间,去延时。
 * 4.watchdog 通过 类似netty的 Future功能来实现异步延时
 * 5.watchdog 最终还是通过 lua脚本来进行延时
 */

@Component
@Slf4j
public class RedissonLockUtil {

    @Resource
    private RedissonClient redissonClient;


    /**
     * 加锁 lock(), 拿不到lock就不罢休,不然线程就一直block
     * 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
     * @param lockKey
     * @return
     */
    public RLock lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        log.info("加锁成功,Redis Lock key :{}", lockKey);
        return lock;
    }

    /**
     * 带超时的锁
     * timeout = -1时, 开启WatchDog自动延期机制给锁延期
     * @param lockKey
     * @param timeout 超时时间 单位:秒
     */
    public RLock lock(String lockKey, int timeout) {
        return lock(lockKey, timeout, TimeUnit.SECONDS);
    }

    /**
     * 带超时的锁
     * timeout = -1时, 开启WatchDog自动延期机制给锁延期
     * @param lockKey
     * @param timeout 超时时间
     * @param unit 时间单位
     */
    public RLock lock(String lockKey, int timeout, TimeUnit unit ) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
        log.info("加锁成功,Redis Lock key :{}", lockKey);
        return lock;
    }


    /**
     * 尝试获取锁
     * 设置了leaseTime之后,如果waitTime大于leaseTime,那么锁自动失效后,其他线程会等待waitTime之后,才会获取锁而不是立即获取锁。
     * leaseTime = -1时, 开启WatchDog自动延期机制给锁延期
     * @param lockKey
     * @param waitTime 最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * 默认时间单位:秒
     * @return
     */
    public  boolean tryLock(String lockKey, int waitTime, int leaseTime) {
        return tryLock(lockKey, waitTime, leaseTime, TimeUnit.SECONDS);
    }

    /**
     * 尝试获取锁
     * 设置了leaseTime之后,如果waitTime大于leaseTime,那么锁自动失效后,其他线程会等待waitTime之后,才会获取锁而不是立即获取锁。
     * leaseTime = -1时, 开启WatchDog自动延期机制给锁延期
     * @param lockKey
     * @param waitTime 最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * @param unit 时间单位
     * @return
     */
    public boolean tryLock(String lockKey, int waitTime, int leaseTime, TimeUnit unit) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            return false;
        }
    }

    /**
     * 释放锁
     * @param lockKey
     */
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        // 释放锁,判断要解锁的key是否已被锁定并且是否被当前线程保持
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            lock.unlock();
            log.info("释放锁成功,Redis Lock key :{}", lockKey);
        }
    }

    /**
     * 释放锁
     * @param lock
     */
    public void unlock(RLock lock) {
        // 释放锁,判断要解锁的key是否已被锁定并且是否被当前线程保持
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            lock.unlock();
            log.info("释放锁成功,Redis Lock key :{}", lock.getName());
        }
    }
}

Redisson分布式锁-公平锁工具类

/**
 * @Author: huangyibo
 * @Date: 2023/7/12 11:42
 * @Description: Redisson 分布式锁 公平锁
 */

@Component
@Slf4j
public class RedissonFairLockUtil {

    @Resource
    private RedissonClient redissonClient;


    /**
     * 加锁 lock(), 拿不到lock就不罢休,不然线程就一直block
     * 具有Watch Dog 自动延期机制 默认续30s 每隔30/3=10 秒续到30s
     * @param lockKey
     * @return
     */
    public RLock getFairLock(String lockKey) {
        RLock lock = redissonClient.getFairLock(lockKey);
        lock.lock();
        log.info("公平锁加锁成功,Redis Lock key :{}", lockKey);
        return lock;
    }

    /**
     * 带超时的锁
     * timeout = -1时, 开启WatchDog自动延期机制给锁延期
     * @param lockKey
     * @param timeout 超时时间 单位:秒
     */
    public RLock lock(String lockKey, int timeout) {
        return lock(lockKey, timeout, TimeUnit.SECONDS);
    }

    /**
     * 带超时的锁
     * timeout = -1时, 开启WatchDog自动延期机制给锁延期
     * @param lockKey
     * @param timeout 超时时间
     * @param unit 时间单位
     */
    public RLock lock(String lockKey, int timeout, TimeUnit unit ) {
        RLock lock = redissonClient.getFairLock(lockKey);
        lock.lock(timeout, unit);
        log.info("公平锁加锁成功,Redis Lock key :{}", lockKey);
        return lock;
    }


    /**
     * 尝试获取锁
     * 设置了leaseTime之后,如果waitTime大于leaseTime,那么锁自动失效后,其他线程会等待waitTime之后,才会获取锁而不是立即获取锁。
     * leaseTime = -1时, 开启WatchDog自动延期机制给锁延期
     * @param lockKey
     * @param waitTime 最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * 默认时间单位:秒
     * @return
     */
    public  boolean tryLock(String lockKey, int waitTime, int leaseTime) {
        return tryLock(lockKey, waitTime, leaseTime, TimeUnit.SECONDS);
    }

    /**
     * 尝试获取锁
     * 设置了leaseTime之后,如果waitTime大于leaseTime,那么锁自动失效后,其他线程会等待waitTime之后,才会获取锁而不是立即获取锁。
     * leaseTime = -1时, 开启WatchDog自动延期机制给锁延期
     * @param lockKey
     * @param waitTime 最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * @param unit 时间单位
     * @return
     */
    public boolean tryLock(String lockKey, int waitTime, int leaseTime, TimeUnit unit) {
        RLock lock = redissonClient.getFairLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            return false;
        }
    }

    /**
     * 释放锁
     * @param lockKey
     */
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getFairLock(lockKey);
        // 释放锁,判断要解锁的key是否已被锁定并且是否被当前线程保持
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            lock.unlock();
            log.info("公平锁释放锁成功,Redis Lock key :{}", lockKey);
        }
    }

    /**
     * 释放锁
     * @param lock
     */
    public void unlock(RLock lock) {
        // 释放锁,判断要解锁的key是否已被锁定并且是否被当前线程保持
        if (lock.isLocked() && lock.isHeldByCurrentThread()) {
            lock.unlock();
            log.info("公平锁释放锁成功,Redis Lock key :{}", lock.getName());
        }
    }
}

Redisson加解锁过程分析

通过一张图来看一看Redisson内部的锁操作流程,其内部实现主要用到3大技术栈(Lua脚本+Semaphore+异步线程),注:笔者使用的Redisson版本为3.12.1

image.png

image.png

image.png

image.png

加锁操作

  • 线程去获取锁,获取成功: 执行lua脚本,保存数据到redis数据库。
  • 线程去获取锁,获取失败: 一直通过while循环尝试获取锁,获取成功后,执行lua脚本,保存数据到redis数据库。

加锁源代码

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
	long threadId = Thread.currentThread().getId();
	Long ttl = tryAcquire(leaseTime, unit, threadId);
	// ttl为空,则说明获锁成功
	if (ttl == null) {
		return;
	}
	// 使用redis->subscribe订阅channel,用于监听回调处理
	RFuture<RedissonLockEntry> future = subscribe(threadId);
	
	......
	
	// 获锁失败,则使用while循环不断获取锁的剩余过期时间ttl,然后指定Park的时间为ttl,不断循环判断
	try {
		while (true) {
			// 如果锁还存在,则返回的是当前锁的剩余过期时间ttl
			ttl = tryAcquire(leaseTime, unit, threadId);
			// 如果其它客户端释放了锁,当前客户端竞争锁成功
			if (ttl == null) {
				break;
			}
			// 当前客户端线程阻塞,时间为指定的ttl
			if (ttl >= 0) {
				//使用Semaphore->LockSupport.park()方法
				future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
				}
			} 
		}
	} finally {
		unsubscribe(future, threadId);
	}
}

加锁lua脚本

解释一下Lua脚本中的几个参数

  • KEYS[1]加锁的key的名称,比如RLock lock = redisson.getLock(“lockName”);则KEYS[1]就是lockName
  • ARGV[1]表示锁的过期时间,如果未设置默认为30秒
  • ARGV[2]表示加锁的客户端ID,格式为:uuid + “:” + threadid,例如: 11bb52bc-a764-4649-8b46-a61513d7fe44:1
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                 //锁不存在,进行加锁操作,将锁资源保存在hash中
              "if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              //锁存在且为同一线程重复加锁,将该线程的锁重入次数加1
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              //其它客户端已经竞争到锁,返回当前锁的剩余过期时间
              "return redis.call('pttl', KEYS[1]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

获锁过程

分支一:锁不存在

使用"exists lockName"命令判断锁是否存在,不存在则使用"hset KEYS[1] ARGV[2] 1"命令进行加锁操作,假设客户端1加锁成功,使用hash数据结构存储客户端1的锁资源,结构为:

"lockName":{
    //客户端ID:重入次数
    "11bb52bc-a764-4649-8b46-a61513d7fe44:1":1
}

接着使用"pexpire KEYS[1] ARGV[1]",即"pexpire lockName 30000"设置lockName锁的过期时间,然后返回null表示加锁成功!

分支二:锁存在且为同一客户端重复加锁

客户端在同一线程操作中是可以重复获得锁的,使用命令"hincrby KEYS[1] ARGV[2] 1"将同一客户端的可重入次数加1,并重新设置过期时间,返回null表示加锁成功!

客户端1重复加锁成功,此时hash结构如下:

"lockName":{
    //客户端ID:重入次数加了1
    "11bb52bc-a764-4649-8b46-a61513d7fe44:1":2
}

分支三:客户端锁竞争

在客户端获锁失败后,当前客户端会订阅(subscribe)名称为"redisson_lock__channel: {lockName}"的channel,用于监听回调处理,客户端释放锁时会在redisson_lock__channel:{lockName}的channel上发布(publish)UNLOCK_MESSAGE的解锁消息

如果此时另一个客户端2也尝试在lockName上加锁,exists判断lockName已存在且hash中lockName键已经存在客户端1的锁"11bb52bc-a764-4649-8b46-a61513d7fe44:1",所以客户端2不能加锁了,怎么办?

客户端线程会使用"pttl KEYS[1]"命令返回当前锁的剩余过期时间ttl,然后使用J.U.C框架中的Semaphore根据返回的ttl时间调用LockSupport.parkNanos(ttl)来阻塞自己,在指定的等待时间结束后,则继续尝试加锁,不断循环,直到成功为止

RedissonLock类中的lock()方法代码片段如下:

while (true) {
    //尝试加锁
    ttl = tryAcquire(leaseTime, unit, threadId);
    //获取成功
    if (ttl == null) {
        break;
    }
    
    if (ttl >= 0) {
        // 调用Semaphore的tryAcquire()方法
        future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
    }
}

Semaphore类中的tryACquire代码片段如下:

//nanosTimeout时间为客户端需要等待的时间ttl
LockSupport.parkNanos(this, nanosTimeout);

可重入加锁机制

Redisson可以实现可重入加锁机制的原因,跟两点有关:

  • 1、Redis存储锁的数据类型是 Hash类型
  • 2、Hash数据类型的key值包含了当前线程信息。

下面是redis存储的数据 image.png

这里表面数据类型是Hash类型,Hash类型相当于我们java的 <key,<key1,value>> 类型,这里key是指 'redisson'

它的有效期还有9秒,我们再来看里们的key1值为078e44a3-5f95-4e24-b6aa-80684655a15a:45它的组成是:

guid + 当前线程的ID。后面的value是就和可重入加锁有关。

举图说明

image.png

上面这图的意思就是可重入锁的机制,它最大的优点就是相同线程不需要在等待锁,而是可以直接进行相应操作。

WatchDog延期机制

为什么要使用WatchDog?

Redisson提供的获锁api中有一个leaseTime选项,该值为-1时表明获锁成功的客户端可以一直持有该锁,释放锁之前,其他客户端线程将一直等待下去。我们知道当在Redis中设置一个key时,往往需要指定expireTime,防止其长期占用内存空间。在种场景下,锁最终还是会过期,所以在key过期之前,必须提供一种机制(WatchDog)来保证key继续有效。

Redisson分布式锁中WatchDog实现机制

  • 1、可自定义设置过期时间,只有在没有设置过期时间(过期时间为默认值0)的情况下,才会启动自动延长。
  • 2、没有设置过期时间,直接申请锁时,会默认设置一个延长过期时间30s,定时每隔延长过期时间的三分之一时间10s,就重新设置过期时间30s(时期时间值为延长过期时间)。
  • 3、为了防止某次业务由于异常而出现任务持续很久,从而长时间占有了锁,添加最大延期次数参数限制,比如延期超过三次就不再延期。

客户端加锁(lock)成功后,会启用一个watch dog后台线程,使用netty时间轮HashedWheelTimer算法,每隔delay=10秒检查如果客户端还持有锁,则重新设置锁的过期时间为lockWatchdogTimeout=30秒(默认),其中delay = lockWatchdogTimeout/3

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    //当leaseTime为-1时启用watchdog
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }
        // 客户端获锁成功,延期操作
        if (ttlRemaining == null) {
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

对于同一客户端重复获锁且成功时,Redisson是怎么保证WatchDog的延期操作只执行一次?答案是:本地缓存

private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    //第2次以后再获取锁,不用再使用时间轮算法延期了
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        //第1次获取成功时,进行延期操作
        entry.addThreadId(threadId);
        renewExpiration();
    }
}

RedissonLock类中的renewExpiration()方法代码片段如下:

//延期操作
private void renewExpiration() {
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            //Lua脚本延期锁的过期时间
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                //延期成功
                if (res) {
                    // 继续循环延期操作
                    renewExpiration();
                }
            });
        }
    //每隔10秒检查一次
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}

调用renewExpirationAsync()方法设置锁的过期时间,Lua脚本如下:

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return 1; " +
            "end; " +
            "return 0;",
        Collections.<Object>singletonList(getName()), 
        internalLockLeaseTime, getLockName(threadId));
}

哪些获锁方法会使用WatchDog?

大家应该注意,不是所有的获锁成功操作都会开启WatchDog功能,还需要leaseTime为-1的条件成立时,才会启用WatchDog。

下面将罗列出Redisson提供的部分获锁操作: 定义RLock rlock = client.getLock(“lockName”);

获锁方法 是否开启WatchDog
rlock.lock() 启用
rlock.tryLock() 启用
tryLock(long waitTime, TimeUnit unit) 启用
tryLock(long waitTime, long leaseTime != -1, TImeUnit unit) 关闭
...... ......

注:leaseTime不为-1,则不会开启watchDao功能

结论

  • 想要触发Redisson看门狗机制,不能自定义 leaseTime(或者传参 -1)
  • Redisson默认加锁30秒,每隔10秒刷新加锁时间
  • watchdog的延时时间 可以由 lockWatchdogTimeout指定默认延时时间,但是不要设置太小
  • Redisson是通过Future和Timeout功能来实现异步延时

释放锁操作

释放锁的操作相对简单,也比较容易理解,大概就四步:

删除key -> 设置过期时间 ->删除本地缓存 -> 发布解锁消息

解锁操作lua脚本

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            //不存在就直接返回null
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                "return nil;" +
            "end; " +
            //将当前客户端的锁重入次数-1
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            //如果当前客户端还持了锁,则重新设置过期时间
            "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                "return 0; " +
             //客户端已经释放了锁,删除key,发布解锁消息
            "else " +
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

删除本地缓存代码

void cancelExpirationRenewal(Long threadId) {
    ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (task == null) {
        return;
    }
    if (threadId != null) {
        task.removeThreadId(threadId);
    }
    if (threadId == null || task.hasNoThreads()) {
        Timeout timeout = task.getTimeout();
        if (timeout != null) {
            timeout.cancel();
        }
        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
    }
}

其中删除本地缓存map是在异步线程中执行的,WatchDog对客户端的锁进行缓期操作后,将该客户端线程信息保存在本地缓存map中,保证同一客户端重复获锁成功时,锁延期操作只执行一次。

Redis分布式锁的缺点

Redis分布式锁会有个缺陷,就是在Redis哨兵模式下:

  • 客户端1 对某个master节点写入了redisson锁,此时会异步复制给对应的 slave节点。但是这个过程中一旦发生 master节点宕机,主备切换,slave节点从变为了 master节点。
  • 这时客户端2 来尝试加锁的时候,在新的master节点上也能加锁,此时就会导致多个客户端对同一个分布式锁完成了加锁。
  • 这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。
  • 缺陷在哨兵模式或者主从模式下,如果 master实例宕机的时候,可能导致多个客户端同时完成加锁。

总结

至此,Redisson加解锁的详细过程分析完毕!回到开篇,我们说Redisson还有些小缺陷,比如在Mast-Slave架构下,主从同步通常是异步的。

在这种场景(主从结构)中存在明显的竞态:

  • 1、客户端A从master获取到锁
  • 2、在master将锁同步到slave之前,master宕掉了
  • 3、slave节点被晋级为master节点
  • 4、客户端B取得了同一个资源被客户端A已经获取到的另外一个,锁安全失效

参考: https://codeleading.com/article/10595976645/

https://cloud.tencent.com/developer/article/1953234

https://blog.csdn.net/qq_26222859/article/details/79645203

https://blog.csdn.net/qq_41807885/article/details/128407627

https://blog.csdn.net/weixin_51297617/article/details/121184342