18. 谷粒商城分布式锁Redisson

本文详细介绍了Redisson作为Java Redis客户端的特性,包括其可重入锁的概念与避免死锁的设计,以及读写锁的使用场景。通过示例展示了Redisson如何实现分布式锁的自动续期和可配置过期时间,以及信号量在分布式限流中的应用。此外,还探讨了Redisson的闭锁(CountDownLatch)在同步控制中的作用。
摘要由CSDN通过智能技术生成

1 、简介

Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid)。充分的利用了 Redis 键值数据库提供的一系列优势, 基于 Java 实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。 使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。

官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

2、简单测试

https://gitee.com/UnityAlvin/gulimall/commit/9238af02e6bfcc8b6478d71493cff9f931bceebc

3、可重入锁与不可重入锁

可重入锁

假设现在有A、B两个方法,A方法调用了B方法,A方法加了一把1号锁,B方法也想加这个1号锁,

如果是可重入的,那整个流程就应该是这样,

A方法执行之后,把1号锁加上了,里面调用了B方法,B方法一看,A方法已经加上1号锁了,直接就拿过来用了,B方法内部就可以直接执行,执行完之后,A释放锁

不可重入锁

还是A方法先执行,然后把1号锁持有了,里面调用了B方法,而B则需要等待A方法释放1号锁之后,它才能抢到1号锁,这就是不可重入锁。

这种锁是有问题的,A在调用B之前,压根就没释放过锁,所以B根本就拿不到这个锁,

A会等待B执行完之后才会释放锁,B连锁都拿不到,又怎么会执行呢?所以A也释放不了锁,最终会导致死锁。

结论

所以的锁,都应该设计成可重入锁,避免死锁问题

4、Redisson锁测试

对标ReentrantLock

    @ResponseBody
    @GetMapping("/hello")
    public String hello() {
        RLock lock = redissonClient.getLock("anyLock");
        try {
            lock.lock();
            try {
                System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
                TimeUnit.SECONDS.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
            System.out.println("释放锁..." + Thread.currentThread().getId());
        }
        return "hello";
    }

同时开启10000、10001两个商品服务,

假设10000先抢到锁,它先执行业务,此时10001则是在外面等待,直到10000释放锁之后,10001才抢到了锁,等10001执行完,然后才释放锁

假设还是10000先抢到锁,它在执行业务期间宕机了,没有释放锁,我们发现10001会一直在外面等待,最终抢到锁,然后执行业务,再释放锁,并没有出现死锁的现象。

我们发现Redisson内部的lock()实现,里面有一个死循环,会一直去获取锁。

  • lock()是阻塞式等待,默认加的锁都是30s时间
  • 如果执行业务时间过长,运行期间Redisson会给锁自动续期,每次都会续上30s,不会因为业务时间过长,导致锁自动删掉
  • 等业务执行完,就不会给当前锁续期,即使不手动释放锁,锁也会在30s以后自动删除

5、Redisson看门狗原理

未指定锁的过期时间

lock.lock();

如果我们未指定锁的过期时间,那Redisson内部会调用this.lock(-1L, (TimeUnit)null, false);

再调用Long ttl = tryAcquire(leaseTime, unit, threadId);

再调用return get(tryAcquireAsync(leaseTime, unit, threadId));

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

指定锁的过期时间

lock.lock(10, TimeUnit.SECONDS);

如果我们指定了锁的过期时间,那Redisson内部会调用this.lock(leaseTime, unit, false);

然后会调用以下方法

在这里插入图片描述

在这里插入图片描述

结论

推荐使用lock.lock(10, TimeUnit.SECONDS);这种方式

  1. 省掉了自动续期

    指定一个长过期时间+手动解锁解决业务超时,比如说过期时间设置为30s,如果每个业务都能超过30s,那说明肯定是业务内部有问题。

6、Redisson读写锁测试

对标ReentrantReadWriteLock

  • 读写锁通常都是成对出现的
  • 写锁控制了读锁
  • 只要写锁存在,读锁就得等待
  • 并发写,肯定得一个一个执行
  • 如果写锁不存在,读锁一直在那加锁,那跟没加是一样的

不加锁用例

    /**
     * 什么锁也不加,单纯的往redis中写入一个值
     *
     * @return
     */
    @ResponseBody
    @GetMapping("/write-unlock")
    public String writeUnlock() {
        String uuid = "";
        try {
            uuid = UUID.randomUUID().toString();
            TimeUnit.SECONDS.sleep(30);
            stringRedisTemplate.opsForValue().set("writeValue", uuid);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return uuid;
    }

    /**
     * 什么锁也不加,单纯的从redis中读取一个值
     *
     * @return
     */
    @ResponseBody
    @GetMapping("/read-unlock")
    public String readUnlock() {
        String name = "";
        name = stringRedisTemplate.opsForValue().get("writeValue");
        return name;
    }

加锁用例一

    @ResponseBody
    @GetMapping("/write")
    public String write(){
        RReadWriteLock lock = redissonClient.getReadWriteLock("rwAnyLock");
        String uuid = "";
        RLock rLock = lock.writeLock();
        try{
            rLock.lock();
            uuid = UUID.randomUUID().toString();
            TimeUnit.SECONDS.sleep(30);
            stringRedisTemplate.opsForValue().set("writeValue",uuid);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }
        return uuid;
    }

    @ResponseBody
    @GetMapping("/read")
    public String read(){
        RReadWriteLock lock = redissonClient.getReadWriteLock("rwAnyLock");
        String uuid=  "";
        RLock rLock = lock.readLock();
        try{
            rLock.lock();
            uuid = stringRedisTemplate.opsForValue().get("writeValue");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }
        return uuid;
    }

测试一

  • 先给redis中,添加一个writeValue的值,查看是否能读取到
  • 结果:可以

测试二

  • 先调用写的方法,再调用读的方法
  • 结果:读请求会一直加载,等写请求执行完业务之后,读请求瞬间加载到数据

加读写锁的作用就是,保证一定能读到最新数据。

修改期间,写锁是一个互斥锁,读锁则是一个共享锁

读 + 读:相当于无锁,并发读,只会在redis中记录好当前的读锁,它们都会同时加锁成功

写 + 读:读必须等待写锁释放

加锁用例二

    @ResponseBody
    @GetMapping("/write")
    public String write() {
        RReadWriteLock lock = redissonClient.getReadWriteLock("rwAnyLock");
        String uuid = "";
        RLock rLock = lock.writeLock();
        try {
            rLock.lock();
            // 打印log
            System.out.println("写锁加锁成功" + Thread.currentThread().getId());
            uuid = UUID.randomUUID().toString();
            TimeUnit.SECONDS.sleep(30);
            stringRedisTemplate.opsForValue().set("writeValue", uuid);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            // 打印log
            System.out.println("写锁释放" + Thread.currentThread().getId());
        }
        return uuid;
    }

    @ResponseBody
    @GetMapping("/read")
    public String read() {
        RReadWriteLock lock = redissonClient.getReadWriteLock("rwAnyLock");
        String uuid = "";
        RLock rLock = lock.readLock();
        try {
            rLock.lock();
            // 打印log
            System.out.println("读锁加锁成功" + Thread.currentThread().getId());
            uuid = stringRedisTemplate.opsForValue().get("writeValue");
            // 让读锁也等待30秒
            TimeUnit.SECONDS.sleep(30);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
            // 打印log
            System.out.println("读锁释放" + Thread.currentThread().getId());
        }
        return uuid;
    }

测试三

  • 先发送一个读请求,再发送一个写请求
  • 结果:读加上锁了,读释放锁之后,写才加上锁

测试四

  • 发送一个写请求,再发送四个读请求
  • 结果:写请求释放的瞬间,四个读请求都加上锁了

写 + 写:阻塞方式

读 + 写:有读锁,写也需要等待

总结:只要有一个写存在,其它的读/写就必须等待

7、Redisson信号量测试

对标Semaphore

用例一

    @ResponseBody
    @GetMapping("/park")
    public String park() throws InterruptedException {
        RSemaphore park = redissonClient.getSemaphore("park");
        park.acquire();
        return "空闲车位-1";
    }

    @ResponseBody
    @GetMapping("/go")
    public String go() {
        RSemaphore park = redissonClient.getSemaphore("park");
        park.release();
        return "空闲车位+1";
    }

acquire()是一个阻塞方法,必须要获取成功,否则就一直阻塞

先发送3次go请求,添加3个空闲车位,在redis中,发现park的值为3,再发送park请求,每发送1次,redis的park就会-1,

执行第4次的时候,界面不动了,一直在加载

此时执行1次go请求,发现go请求刚执行完,空闲车位加了1个,park请求也执行完了,使空闲车位减了1个,最终park的值为0

用例二

    @ResponseBody
    @GetMapping("/try-park")
    public String tryPark() {
        RSemaphore park = redissonClient.getSemaphore("park");
        boolean result = park.tryAcquire();
        if (result) {
            return "空闲车位-1";
        } else {
            return "空闲车位已满";
        }
    }

tryAcquire()尝试获取一下,不行就算了

还是用例一的那种情况,当try-park请求,执行到4次的时候,直接提示了空闲车位已满

结论

信号量也可以用作分布式限流,在做分布式限流的时候,可以判断信号量是否为true,为true则执行业务,否则直接返回错误,告诉它当前流量过大

8、Redisson闭锁测试

对标CountDownLatch

    @ResponseBody
    @GetMapping("/go-home/{id}")
    public String goHome(@PathVariable Integer id) {
        RCountDownLatch home = redissonClient.getCountDownLatch("home");
        home.countDown();
        return id + "班已走";
    }

    @ResponseBody
    @GetMapping("/lock-door")
    public String lockDoor() throws InterruptedException {
        RCountDownLatch home = redissonClient.getCountDownLatch("home");
        home.await();
        return "锁门";
    }

先为redis的home设置个3,先发送lock-door请求锁门,发现界面一直是加载中,此时发送3次go-home请求,让3个班回家,执行完第3次的时候,发现lock-door的界面刷新了,提示锁门。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值