使用redisson提供分布式锁

Redisson

Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson官方文档

一、引入Redisson

1.导包

<!--引入redisson作为所有分布式锁,分布式对象等功能-->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.12.0</version>
        </dependency>

2.配置-单节点方式

@Configuration
public class MyRedissonConfig {
    @Bean
    public RedissonClient getRedisson() {
        // 默认连接地址 127.0.0.1:6379
//    RedissonClient redisson = Redisson.create();
        //1.创建配置
        //Redis url should start with redis:// or rediss:// (for SSL connection)
        Config config = new Config();
        config.useSingleServer().setAddress("redis://1.12.244.105:6379");
        //2.根据config创建处RedissonClient实例
        RedissonClient redisson = Redisson.create(config);
        return redisson;
    }
}

具体的配置信息可以参考官方文档
配置方法

3.之后就可以注入RedissonClient对象进行操作了

二、Redisson提供的常用分布式锁

Redisson的加锁机制
使用redisson提供分布式锁_第1张图片

1.概念:可重入锁

Redisson提供的分布式锁都是基于可重入锁
一个方法在已经获取该锁的情况下,再次获取该锁,不会出现死锁的情况,这种锁就是可重入锁
如:

public void func(){
	lock.lock();
	func();//再次调用
	lock.unlock();
}

为了方便理解,我们称外层的func()方法为func1,内层的方法为func2
此时func1已经锁住了,再func1解锁之前,func2调用了。此时func2想要锁住lock这把锁,但是此时lock这把锁还被func1锁着。func1想要解锁,就必须等func2结束,但是func2无法结束。此时就死锁了
可重入锁不会出现这种情况
Redisson提供的分布式锁还有看门狗机制,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。

=========可重入锁的原理解析

我们使用redis自建的分布式锁通常是使用的setNX实现的,并且使用的是String数据结构类型,这样的分布式锁是无法支持重入的。
而Redisson提供的分布式锁是可重入的,它使用的是Hset命令和Hash数据结构。
使用redisson提供分布式锁_第2张图片
调用redisson.lock()方法会在redis中存储一个hash数据结构,key为锁的名称,value中的field为当前操作的线程id,value为锁重入的次数。

例如:在上面的func函数中,当外层func函数加锁之后,会获取当前的线程标识存入field字段,并将value+1;当内层函数再次加这个锁,会先判断当前线程与field中存的线程是否是一样的,如果是一样的,value+1;
此时value为2。如果要解锁,先要判断锁是否是自己的(比对key和field字段),如果是,则value-1;

具体流程图:

使用redisson提供分布式锁_第3张图片
上面的流程步骤较多,为了保证操作的原子性,我们需要使用lua脚本,在redisson源码中我们可以看到:
加锁:

    <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  "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; " +
                  "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));
    }

解锁:

   protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                "end; " +
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -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;",
                Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));

    }

这两段lua脚本的流程和上面的流程图基本一致。

=========分布式锁可重试原理解析

=========分布式锁超时续期原理解析

=========分布式锁MultiLock原理解析

Redisson MultiLock旨在解决分布式环境下的多个锁并发管理问题。在分布式系统中,为保证数据正确性,在对共享资源进行访问或修改时需要使用锁机制,以避免多个客户端同时修改同一份数据而造成数据不一致的情况。但是,如果有多个锁需要同时获取或释放,就需要进行协调和管理。

例如,假设我们有3个节点A、B、C,它们都需要使用Redisson锁来保护各自的数据。如果这些节点上的锁之间存在依赖关系,比如B节点需要同时获取A节点和C节点的锁才能进行操作,那么单独使用Redisson的RLock就无法满足这种需求。这个时候就可以使用Redisson MultiLock来将这些锁视为一个整体来进行协调和管理。

通过使用Redisson MultiLock,我们可以将多个Redisson锁作为一个组进行处理,从而实现多锁的原子性管理,避免死锁等问题。MultiLock会确保所有的锁都以原子方式获取或释放,从而保证了数据的一致性。因此,Redisson MultiLock主要用于解决分布式系统中多个锁的并发管理问题。

redis分布式锁中的看门狗机制也是设计的非常巧妙,可以自行了解
看门狗机制
使用redisson提供分布式锁_第4张图片

简单分布式锁操作

    //测试redisson最简单的分布式锁
    @ResponseBody
    @GetMapping("/hello")
    public String hello() {
        //1.获取一把锁,只要锁的名字相同,就是同一把锁  其余锁也满足这个条件
        RLock lock = redissonClient.getLock("my-lock");
        //2.加锁
        //redisson lock()存在看门狗机制(自动续期)详情见文档 默认30秒后解锁,但如果30秒结束后业务没结束,则会自动续期
        lock.lock();//阻塞等待式锁
        //lock(leaseTime,TimeUnit.SECONDS) 该方法不会自动续期
        //1、如果我们传递了锁的超时时间,就发送给redis执行lua脚本,进行占锁,默认超时就是我们指定的时间
        //2、如果我们未指定锁的超时时间,就使用30 * 1000 [LockWatchdogTimeout看i门狗的默认时间] ;
        //只要占锁成功,就会启动一个定时任务[重新给锁设置过期时间,新的过期时间就是看门狗的默认时间] , 每隔10s都会自动续期
        //internalLockLeaseTime [看门狗时间] / 3, 10s

        //最佳实践
        //1)、lock.lock(30,TimeUnit.SECONDS);省掉了整个续期操作。手动解锁

        try {
            System.out.println("加锁成功,执行业务" + Thread.currentThread().getId());
            Thread.sleep(3000);
        } catch (Exception e) {

        } finally {
            //3.解锁 就算程序在解锁之前崩溃了,redisson也会自动解锁
            lock.unlock();
        }
        return "hello";
    }

打开两个网页同时访问该接口,后访问的要等待先访问的释放锁,才能进入执行业务
使用redisson提供分布式锁_第5张图片

读写锁

/**
     * 测试redisson读写锁  
     * 修改期间,写锁是一个排他锁(互斥锁),读锁是一个共享锁
     * 写锁没释放,读锁就必须等待
     * 读 + 读 :相当于无锁,并发读,只会在redis中记录好所有当前的读锁。他们都会同时加锁成功
     * 写 + 读 :等待写锁释放
     * 写 + 写 :阻塞方式
     * 读 + 写 :有读锁,写也需要等待
     * 

* 只要有写操作,都必须等待, * * @return */ //写数据加写锁 @GetMapping("/write") @ResponseBody public String writeValue() { RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock"); RLock rLock = readWriteLock.writeLock();//获取写锁 String s = ""; try { rLock.lock(); System.out.println("写锁加锁成功" + Thread.currentThread().getId()); s = UUID.randomUUID().toString(); Thread.sleep(30000); stringRedisTemplate.opsForValue().set("write", s); } catch (Exception e) { } finally { rLock.unlock(); System.out.println("写锁解锁成功" + Thread.currentThread().getId()); } return s; } //读数据加读锁 @GetMapping("/read") @ResponseBody public String readValue() { RReadWriteLock readWriteLock = redissonClient.getReadWriteLock("rw-lock"); RLock rLock = readWriteLock.readLock();//获取读锁 String s = ""; try { rLock.lock(); System.out.println("读锁加锁成功" + Thread.currentThread().getId()); s = stringRedisTemplate.opsForValue().get("writeValue"); } catch (Exception e) { } finally { rLock.unlock(); System.out.println("读锁释放" + Thread.currentThread().getId()); } return s; }

独写锁有四种情况:

读 + 读

如果所有请求都是读,没有写操作,则相当于无锁状态,直接执行业务

读 + 写

有读正在操作,此时写操作进入,写操作也需要等待读操作执行完成

写 + 读

有写正在操作,此时读操作进入,读操作也需要等待写操作执行完成

写 + 写

依次完成

总结

写锁是互斥锁,排他锁,读锁是共享锁
只要存在写操作,就必须等待
读 + 读 :相当于无锁,并发读,只会在redis中记录好所有当前的读锁。他们都会同时加锁成功
写 + 读 :等待写锁释放
写 + 写 :阻塞方式
读 + 写 :有读锁,写也需要等待

信号量(Semaphore)

信号量为存储在redis中的一个数字,当这个数字大于0时,即可以调用acquire ()方法增加数量,也可以调用release ()方法减少数量,但是当调用release ()之后小于0的话方法就会阻塞,直到数字大于0

利用停车场案例来理解信号量
提供两个方法,停车方法park()车离开停车场方法go()。信号量值就抽象为停车场空余车位

/**
     * 使用Semaphore信号量模拟停车
     * 

* 信号量也可以用于分布式限流 * * @return * @throws InterruptedException */ //停车 @GetMapping("/park") @ResponseBody public String park() throws InterruptedException { RSemaphore park = redissonClient.getSemaphore("park"); // park.acquire(); boolean b = park.tryAcquire(); if (b) { //执行业务 return "ok"; } else {//如果满了就直接走了 return "error"; } } //模拟停车场离开一辆车 @GetMapping("/go") @ResponseBody public String go() { RSemaphore park = redissonClient.getSemaphore("park"); park.release(); return "ok"; }

假设停车场有五个空位
使用redisson提供分布式锁_第6张图片
访问停车接口
使用redisson提供分布式锁_第7张图片
此时停车场空位就变成了4
使用redisson提供分布式锁_第8张图片
我们连续停5俩车,直到没有空位
使用redisson提供分布式锁_第9张图片
再次访问停车接口就会返回error
使用redisson提供分布式锁_第10张图片
访问一次汽车离开接口
使用redisson提供分布式锁_第11张图片
访问成功后再观察信号量
使用redisson提供分布式锁_第12张图片
此时信号量变成了1,又可以进行停车了。

Redisson信号量官网描述

使用redisson提供分布式锁_第13张图片

闭锁(CountDownLatch)

CountDownLatch直译为向下计数闩锁
会向redis中存储一个标志,类似于信号量,但只有当信号量值变为0时,才能解锁
同样,我们使用一个门卫关学校大门的例子来理解闭锁。
学校有5个班级,门卫管理大门,只有当5个班级全部放学,门卫才能关闭学校大门。

/**
     * CountDownLatch 向下计数闩锁
     * 使用学校大门门卫锁大门演示闭锁
     * 如:学校5个班,门卫要等5个班都放学了才能锁门
     */
    //模拟门卫锁门
    @GetMapping("/lockDoor")
    @ResponseBody
    public String lockDoor() throws InterruptedException {
        RCountDownLatch door = redissonClient.getCountDownLatch("door");
        door.trySetCount(5);//设置5个班
        door.await();//门卫等待5个班全部放学完成
        return "门卫下班了";
    }

    //模拟班级放学
    @GetMapping("/gogogo/{id}")
    @ResponseBody
    public String go(@PathVariable Long id) {
        RCountDownLatch door = redissonClient.getCountDownLatch("door");
        door.countDown();//班级放学,计数减一
        return id + "班放学了";
    }

1.首先访问关门接口,会一直转圈,代表该接口处于阻塞状态

使用redisson提供分布式锁_第14张图片

2.连续访问五次放学接口

使用redisson提供分布式锁_第15张图片

使用redisson提供分布式锁_第16张图片

使用redisson提供分布式锁_第17张图片

使用redisson提供分布式锁_第18张图片

使用redisson提供分布式锁_第19张图片

3.当这5次放学接口访问完成之后,关门接口也不阻塞了,执行完成

使用redisson提供分布式锁_第20张图片

闭锁官网描述

使用redisson提供分布式锁_第21张图片
线程的闭锁使用了await方法,会一直阻塞等待,直到该锁的值被其它线程操作变为0。

你可能感兴趣的:(分布式,java,spring)