使用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的加锁机制
在这里插入图片描述

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.lock()方法会在redis中存储一个hash数据结构,key为锁的名称,value中的field为当前操作的线程id,value为锁重入的次数。

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

具体流程图:

在这里插入图片描述
上面的流程步骤较多,为了保证操作的原子性,我们需要使用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最简单的分布式锁
    @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读写锁  
     * 修改期间,写锁是一个排他锁(互斥锁),读锁是一个共享锁
     * 写锁没释放,读锁就必须等待
     * 读 + 读 :相当于无锁,并发读,只会在redis中记录好所有当前的读锁。他们都会同时加锁成功
     * 写 + 读 :等待写锁释放
     * 写 + 写 :阻塞方式
     * 读 + 写 :有读锁,写也需要等待
     * <p>
     * 只要有写操作,都必须等待,
     *
     * @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信号量模拟停车
     * <p>
     * 信号量也可以用于分布式限流
     *
     * @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";
    }

假设停车场有五个空位
在这里插入图片描述
访问停车接口
在这里插入图片描述
此时停车场空位就变成了4
在这里插入图片描述
我们连续停5俩车,直到没有空位
在这里插入图片描述
再次访问停车接口就会返回error
在这里插入图片描述
访问一次汽车离开接口
在这里插入图片描述
访问成功后再观察信号量
在这里插入图片描述
此时信号量变成了1,又可以进行停车了。

Redisson信号量官网描述

在这里插入图片描述

闭锁(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.首先访问关门接口,会一直转圈,代表该接口处于阻塞状态

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

闭锁官网描述

在这里插入图片描述
线程的闭锁使用了await方法,会一直阻塞等待,直到该锁的值被其它线程操作变为0。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值