分布式锁Redisson

什么是Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

为什么要用Redisson

基于SETNX实现的分布式锁存在诸多问题

  • 不可重入问题:是指获取锁的线程,不可以再次获取相同的锁。例:方法A在获取锁A后调用了方法B,方法B也需要获取A,但是由于不可重入锁的原因,方法B无法获取锁A,此情况会导致死锁的发生,由此暴露出了不可重入锁的弊端。因此可重入锁的意义在于防止死锁(synchronized和Lock锁都是可重入的)
  • 超时释放:在加锁的时候增加了TTL,可以防止死锁,但是如果线程发生卡顿或阻塞的时间太长,会导致锁超时释放,释放后其他线程可以获取锁从而引发安全问题。
  • 主从一致性:在Redis是主从集群时,在向集群写数据时,主节点需要异步将数据同步给从节点,特殊情况下,在数据同步之前主节点发生了宕机(主从同步存在时间延迟),未把数据成功同步到从节点,从而出现主从一致性的问题
  • 不可重试:通过SETNX命令获取分布式锁只能尝试一次,失败后返回false,不能进行锁重试机制

Redisson提供了不同的分布式锁:

  1. 可重入锁(Reentrant Lock)
  2. 公平锁(Fair Lock)
  3. 联锁(MultiLock)
  4. 红锁(RedLock)
  5. 读写锁(ReadWriteLock)
  6. 信号量(Semaphore)
  7. 可过期性信号量(PermitExpirableSemaphore)
  8. 闭锁(CountDownLatch)

Redisson入门

1.引入依赖:

XML 复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

2.配置Redisson客户端

java 复制代码
@Configuration
public class RedisConfig {
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer.setAddress("redis://192.168.0.1:6379").setPassword("123456");    
        return Redisson.create(config);
    }
}

3.使用Redisson的分布式锁

java 复制代码
@Resource
private RedissonClient redissonClient;

private RLock rLock;

@Test
void testRedisson throws Exception {
    lock = redissonClient.getLock("lock");// 获取锁,并指定锁名称
    boolean isLock = lock.tryLock();// 尝试获取锁,此处方法根据业务类型传参
    if (isLock) {// 如果为true,表示获取成功获取锁
        try {
            // 执行业务...
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

Redisson之重入锁------tryLock

java 复制代码
// 尝试获取锁,不可重入锁
// 如果失败则直接返回false,如果成功,则自动更新锁的过期时间(watchDog机制)
// leaseTime 默认是-1,当leaseTime为-1时,执行watchDog机制
boolean lock = lock.tryLock();

// 尝试获取锁,可重入锁,并自动更新锁的过期时间(watchDog机制)
// waitTime 最大等待时常(在获取锁失败后的一段时间内不断尝试获取锁)
// leaseTime 此处不传leaseTime默认值是-1,成功获取锁后,通过watchDog机制自动更新锁的过期时间
// unit 时间单位
boolean lock = lock.tryLock(waitTime, unit);
    
// 尝试获取锁,可重入锁
// waitTime 最大等待时常(在获取锁失败后的一段时间内不断尝试获取锁)
// leaseTime 锁的有效期(不会自动更新锁的过期时间,当leaseTime为-1时,会执行watchDog机制自动更新时间)
// unit 时间单位
boolean lock = lock.tryLock(waitTime, leaseTime, unit);

Java中的Lock重入锁,是借助于一个voaltile的一个state变量来记录重入的状态。

  • 如果当前没有人持有这把锁,那么state = 0
  • 如果有人持有这把锁,那么state = 1
  • 如果持有者把锁的人再次持有这把锁,那么state+1

Java中的synchronize重入锁

  • 对于synchronize而言,在c语言代码中会有一个count
  • 原理与state类似,也是重入一次就+1,释放一次就-1,直至减到0,表示这把锁没有被人持有

在Redisson中,采用了Redis中的hash结构来实现可重入锁。通过hash结构来实现重入锁加1减1的操作

逻辑图:

Lua入门:https://www.runoob.com/lua/lua-tutorial.html

为了保证原子性,Redisson内部采用了Lua脚本的方式执行相关的命令

Lua脚本实现重入锁:

java 复制代码
if (redis.call('exists', KEYS[1]) == 0) // 判断锁是否存在,等于0代表不存在,否则存在
then 
    redis.call('hset', KEYS[1], ARGV[2], 1); 
// 不存在,则创建锁,重入次数加1
    redis.call('pexpire', KEYS[1], ARGV[1]); // 设置锁的过期时间
    return nil; 
end; 
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) // 判断锁是否是自己,等于1则是自己
then 
    redis.call('hincrby', KEYS[1], ARGV[2], 1); 
// 自增1
    redis.call('pexpire', KEYS[1], ARGV[1]); // 设置锁的过期时间
    return nil; 
end; 
return redis.call('pttl', KEYS[1]);// 获取锁失败,并返回旧锁的剩余时间

注:该Lua脚本在 RedissonLock 类的 tryLockInnerAsync 方法

Lua脚本释放重入锁:

java 复制代码
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) // 判断锁是否存在
then 
    return nil;// 不存在,结束
end; 
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); // 获取锁,重入次数减1
if (counter > 0) 
then 
    redis.call('pexpire', KEYS[1], ARGV[2]); // 重置锁的有效期
    return 0; 
else 
    redis.call('del', KEYS[1]); // 删除锁
    redis.call('publish', KEYS[2], ARGV[1]); // 通知其他线程,锁已释放
    return 1; 
end; 
return nil;

注:该Lua脚本在 RedissonLock 类的 unlockInnerAsync 方法

Redisson重入锁的执行原理

Redisson的WatchDog机制

源码:

图一:通过判断 leaseTime 判断是否等于-1,如果等于-1,则调用 tryLockInnerAsync 方法尝试获取锁,如果锁成功获取了,则调用 scheduleExpirationRenewal方法

图二:该 scheduleExpirationRenewal 方法分为两步,判断是否该锁已存在,如果存在则进行重入次数加1(如图三),如果不存在,在重入次数加1后调用 renewExpiration 方法

图四:renewExpiration 方法的本质是一个定时任务,通过递归的方式定时自动刷新锁的过期时间,刷新时间的方法是 renewExpirationAsync

Redisson的锁重试机制

锁重试,必然是由于当前线程没有成功获取到锁,Redisson内部通过不断判断是否有剩余重试时间来操作重试机制,先使用 subscribe方法订阅其他锁是否存在已经释放的信号,如果在剩余重试时间内没有等到其他线程释放锁的信号,则取消订阅,并返回false获取锁失败。如果在剩余时间内等到了其他线程释放锁的信号,那么会先计算是否还有剩余时间,如果有时间则通过dowhile循环尝试获取锁。

java 复制代码
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        long time = unit.toMillis(waitTime);
        long current = System.currentTimeMillis();
        long threadId = Thread.currentThread().getId();
        // 尝试获取锁
        Long ttl = this.tryAcquire(leaseTime, unit, threadId);
        // 如果ttl为空,代表成功获取锁
        if (ttl == null) {
            return true;
        } else {
            // 计算重试的剩余时间
            time -= System.currentTimeMillis() - current;
            if (time <= 0L) {
                this.acquireFailed(threadId);// 没时间了 返回false
                return false;
            } else {
                current = System.currentTimeMillis();
                // 订阅其他线程是否有释放锁的信号
                RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);
                // 在time这个时间内等待释放锁的信号
                if (!this.await(subscribeFuture, time, TimeUnit.MILLISECONDS)) {
                    if (!subscribeFuture.cancel(false)) {
                        subscribeFuture.onComplete((res, e) -> {
                            if (e == null) {
                                // 取消订阅
                                this.unsubscribe(subscribeFuture, threadId);
                            }

                        });
                    }
                    // 在time时间内没有等到信号,返回false,获取锁失败
                    this.acquireFailed(threadId);
                    return false;
                } else {
                    // 等到了缩放锁的信号
                    try {
                        // 计算是否还有重试时间
                        time -= System.currentTimeMillis() - current;
                        if (time <= 0L) {
                            this.acquireFailed(threadId);
                            boolean var20 = false;
                            return var20;
                        } else {
                            // 有时间,则通过循环尝试获取锁
                            boolean var16;
                            do {
                                long currentTime = System.currentTimeMillis();
                                ttl = this.tryAcquire(leaseTime, unit, threadId);
                                if (ttl == null) {
                                    var16 = true;
                                    return var16;
                                }

                                time -= System.currentTimeMillis() - currentTime;
                                if (time <= 0L) {
                                    this.acquireFailed(threadId);
                                    var16 = false;
                                    return var16;
                                }

                                currentTime = System.currentTimeMillis();
                                if (ttl >= 0L && ttl < time) {
                                    this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                                } else {
                                    this.getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                                }

                                time -= System.currentTimeMillis() - currentTime;
                            } while(time > 0L);

                            this.acquireFailed(threadId);
                            var16 = false;
                            return var16;
                        }
                    } finally {
                        // 取消订阅
                        this.unsubscribe(subscribeFuture, threadId);
                    }
                }
            }
        }
    }

Redisson的MutiLock原理

在Redis的主从模式和哨兵模式中,都需要把主节点的数据发送到从节点,在这个过程中,如果主节点发生了故障宕机,就会导致锁失效从而引发线程安全问题。

由此,Redisson提出来了MutiLock锁,MutiLock锁的思想是每次加锁时,对多个节点同时加锁,把这多个节点都认为是主节点,只有把锁成功的加到每个节点上,才认为是加锁成功。如果其中有一个主节点发生故障宕机,也不会影响其他主机点,锁的信息在其他主节点也存在。保证了加锁的可靠性。

先定义多个节点

java 复制代码
@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.0.1:6379")
                .setPassword("root");
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redissonClient2() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://92.168.0.1:6379")
                .setPassword("root");
        return Redisson.create(config);
    }

    @Bean
    public RedissonClient redissonClient3() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://92.168.0.1:6379")
                .setPassword("root");
        return Redisson.create(config);
    }
}

使用MutiLock锁

java 复制代码
@Resource
private RedissonClient redissonClient;
@Resource
private RedissonClient redissonClient2;
@Resource
private RedissonClient redissonClient3;

private RLock lock;

@BeforeEach
void setUp() {
    RLock lock1 = redissonClient.getLock("lock");
    RLock lock2 = redissonClient2.getLock("lock");
    RLock lock3 = redissonClient3.getLock("lock");
    lock = redissonClient.getMultiLock(lock1, lock2, lock3);
}

@Test
void method1() {
    boolean success = lock.tryLock();
    if (!success) {
        log.error("获取锁失败,1");
        return;
    }
    try {
        log.info("获取锁成功");
        method2();
    } finally {
        log.info("释放锁,1");
        lock.unlock();
    }
}

void method2() {
    RLock lock = redissonClient.getLock("lock");
    boolean success = lock.tryLock();
    if (!success) {
        log.error("获取锁失败,2");
        return;
    }
    try {
        log.info("获取锁成功,2");
    } finally {
        log.info("释放锁,2");
        lock.unlock();
    }
}

MutiLock 锁的tryLock方法源码

java 复制代码
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    long newLeaseTime = -1L;
    //如果传入了释放时间
    if (leaseTime != -1L) {
        //再判断一下是否有等待时间
        if (waitTime == -1L) {
            //如果没传等待时间,不重试,则只获得一次
            newLeaseTime = unit.toMillis(leaseTime);
        } else {
            //想要重试,耗时较久,万一释放时间小于等待时间,则会有问题,所以这里将等待时间乘以二
            newLeaseTime = unit.toMillis(waitTime) * 2L;
        }
    }
    //获取当前时间
    long time = System.currentTimeMillis();
    //剩余等待时间
    long remainTime = -1L;
    if (waitTime != -1L) {
        remainTime = unit.toMillis(waitTime);
    }
    //锁等待时间,与剩余等待时间一样    
    long lockWaitTime = this.calcLockWaitTime(remainTime);
    //锁失败的限制,源码返回是的0
    int failedLocksLimit = this.failedLocksLimit();
    //已经获取成功的锁
    List<RLock> acquiredLocks = new ArrayList(this.locks.size());
    //迭代器,用于遍历
    ListIterator<RLock> iterator = this.locks.listIterator();

    while(iterator.hasNext()) {
        RLock lock = (RLock)iterator.next();

        boolean lockAcquired;
        try {
            //没有等待时间和释放时间,调用空参的tryLock
            if (waitTime == -1L && leaseTime == -1L) {
                lockAcquired = lock.tryLock();
            } else {
                //否则调用带参的tryLock
                long awaitTime = Math.min(lockWaitTime, remainTime);
                lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
            }
        } catch (RedisResponseTimeoutException var21) {
            this.unlockInner(Arrays.asList(lock));
            lockAcquired = false;
        } catch (Exception var22) {
            lockAcquired = false;
        }
        //判断获取锁是否成功
        if (lockAcquired) {
            //成功则将锁放入成功锁的集合
            acquiredLocks.add(lock);
        } else {
            //如果获取锁失败
            //判断当前锁的数量,减去成功获取锁的数量,如果为0,则所有锁都成功获取,跳出循环
            if (this.locks.size() - acquiredLocks.size() == this.failedLocksLimit()) {
                break;
            }
            //否则将拿到的锁都释放掉
            if (failedLocksLimit == 0) {
                this.unlockInner(acquiredLocks);
                //如果等待时间为-1,则不想重试,直接返回false
                if (waitTime == -1L) {
                    return false;
                }

                failedLocksLimit = this.failedLocksLimit();
                //将已经拿到的锁都清空
                acquiredLocks.clear();
                //将迭代器往前迭代,相当于重置指针,放到第一个然后重试获取锁
                while(iterator.hasPrevious()) {
                    iterator.previous();
                }
            } else {
                --failedLocksLimit;
            }
        }
        //如果剩余时间不为-1,很充足
        if (remainTime != -1L) {
            //计算现在剩余时间
            remainTime -= System.currentTimeMillis() - time;
            time = System.currentTimeMillis();
            //如果剩余时间为负数,则获取锁超时了
            if (remainTime <= 0L) {
                //将之前已经获取到的锁释放掉,并返回false
                this.unlockInner(acquiredLocks);
                //联锁成功的条件是:每一把锁都必须成功获取,一把锁失败,则都失败
                return false;
            }
        }
    }
    //如果设置了锁的有效期
    if (leaseTime != -1L) {
        List<RFuture<Boolean>> futures = new ArrayList(acquiredLocks.size());
        //迭代器用于遍历已经获取成功的锁
        Iterator var24 = acquiredLocks.iterator();

        while(var24.hasNext()) {
            RLock rLock = (RLock)var24.next();
            //设置每一把锁的有效期
            RFuture<Boolean> future = ((RedissonLock)rLock).expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS);
            futures.add(future);
        }

        var24 = futures.iterator();

        while(var24.hasNext()) {
            RFuture<Boolean> rFuture = (RFuture)var24.next();
            rFuture.syncUninterruptibly();
        }
    }
    //但如果没设置有效期,则会触发WatchDog机制,自动帮我们设置有效期,所以大多数情况下,我们不需要自己设置有效期
    return true;
}

总结:

利用Hash结构,记录线程标识和重入次数,解决了锁的可重入

利用WatchDog机制,通过内部定时器延续锁的时间,解决了锁的超时释放

利用信号量和重试时间控制锁重试,解决了不可重试问题

利用multiLock机制,解决主从一致性问题

相关推荐
jiayou641 天前
KingbaseES 实战:深度解析数据库对象访问权限管理
数据库
李广坤2 天前
MySQL 大表字段变更实践(改名 + 改类型 + 改长度)
数据库
初次攀爬者3 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
爱可生开源社区3 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1773 天前
《从零搭建NestJS项目》
数据库·typescript
加号34 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏4 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐4 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再4 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip
tryCbest4 天前
数据库SQL学习
数据库·sql