缓存与分布式锁——Redisson

Redisson

简介

redisson 官方文档

整合

Maven 依赖

<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.0</version>
</dependency>

配置方法,使用该配置类创建 RedissonClient 实体

@Configuration
public class RedissonConfig {

    @Bean(destroyMethod="shutdown")
    RedissonClient redisson() throws IOException {
        Config config = new Config();

        config.useSingleServer().setAddress("redis://121.5.47.104:6379");
        // 通过 config 创建 RedissonClient 对象
        return Redisson.create(config);
    }

}

测试 lock

@ResponseBody
@GetMapping("/hello")
public String hello(){
    DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
    // 获取锁
    RLock lock = redissonClient.getLock("lock");
    // 加锁 阻塞式等待
    lock.lock();
    try {
        System.out.println("加锁成功-执行业务-线程id:"
                +Thread.currentThread().getId()+"-当前时间:"
                +dateFormat.format(new Date()));
        Thread.sleep(15000);
    } catch (InterruptedException e) {

    }finally {
        // 解锁
        System.out.println("释放锁-线程id:"
                +Thread.currentThread().getId()+"-当前时间:"
                +dateFormat.format(new Date()));
        lock.unlock();
    }
    return "hello";
}

分布式情况下,打开两个相同的服务,端口不一样,一个 10001,一个 10002。

同时访问 各自端口下的 /hello 服务

线程1
线程2
可以看到,先访问的 10001 端口下的线程先占到锁,执行业务后释放,10002 端口下的线程才能占到锁,开始执行业务。

一个网页执行业务的时候,另一个网页转圈圈。

注意,上述代码并没有设置锁的过期时间,考虑一个场景,如果先获得锁的进程在执行业务的时候停电(这里用 stop 服务模拟),无法执行下面 finally 块里的释放锁。会不会造成死锁,另一个进程无法获得锁呢?

线程1
线程2
可以看到,端口 10001 下的线程获得锁没来得及释放就被停掉了,10002 下的线程在 30秒后获得了锁。

锁的自动续期 WatchDog

上述问题是因为在创建的时候默认添加了 30秒的过期时间。

当把模拟执行业务的时间加长到 60 s,发现还是可以正常加锁解锁

加锁成功-执行业务-线程id:392-当前时间:2021/08/04 11:01:51
释放锁-线程id:392-当前时间:2021/08/04 11:02:51

加锁成功-执行业务-线程id:390-当前时间:2021/08/04 11:02:51
释放锁-线程id:390-当前时间:2021/08/04 11:03:51

说明正在运行的业务可以自动续锁

可以使用下面操作来指定过期时间

void lock(long time, TimeUnit timeUnit);

这种方式指定了过期时间,redisson 将过期时间写入 lua 脚本让 redis 执行,一旦到达过期时间,直接释放锁,业务方法出现冲突自行解决。

void lock()

这种方式未指定过期时间,使用 LockWatchdogTimeout 看门狗的默认时间 30*1000 ms。

一旦占锁成功,启动一个定时任务,每隔 看门狗的默认时间/3 即 10s 执行去更新过期时间,直到业务方法完成,锁被释放。

读写锁 ReadWriteLock

类似 JUC 包下的读写锁,写锁是一个排它锁,读锁是一个共享锁。当拿到写锁修改数据时,所有读锁被阻塞。这样可以保证能够读到最新的数据。

读场景

@ResponseBody
@GetMapping("/readValue")
public String readValue(){
    DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
    String res = "";
    // 获取读锁
    RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
    RLock readLock = lock.readLock();
    readLock.lock();
    try {
        // 读数据
        System.out.println("获取了读锁-线程id:"
                +Thread.currentThread().getId()
                +"-"
                +dateFormat.format(new Date()));
        res = redisTemplate.opsForValue().get("value");
        Thread.sleep(20000);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        System.out.println("释放了读锁-线程id:"
                +Thread.currentThread().getId()
                +"-"
                +dateFormat.format(new Date()));
        // 释放锁
        readLock.unlock();
    }
    return res;
}

写场景

@ResponseBody
@GetMapping("/writeValue")
public String writeValue(){
    DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
    String res = "";
    // 获取写锁
    RReadWriteLock lock = redissonClient.getReadWriteLock("rw-lock");
    RLock writeLock = lock.writeLock();
    writeLock.lock();

    try {
        System.out.println("获取了写锁-线程id:"
                +Thread.currentThread().getId()
                +"-"
                +dateFormat.format(new Date()));
        res = UUID.randomUUID().toString();
        // 写数据
        redisTemplate.opsForValue().set("value", res);
        Thread.sleep(20000);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        System.out.println("释放了写锁-线程id:"
                +Thread.currentThread().getId()
                +"-"
                +dateFormat.format(new Date()));
        // 释放锁
        writeLock.unlock();
    }
    return res;
}

打开多个网页,分别多个请求 /readValue 和 /writeValue

例:一个先请求 /writeValue 三个后请求 /readValue

结果

获取了写锁-线程id:384-2021/08/04 17:45:48
释放了写锁-线程id:384-2021/08/04 17:46:09
获取了读锁-线程id:385-2021/08/04 17:46:09
获取了读锁-线程id:386-2021/08/04 17:46:11
获取了读锁-线程id:389-2021/08/04 17:46:13
释放了读锁-线程id:385-2021/08/04 17:46:29
释放了读锁-线程id:386-2021/08/04 17:46:31
释放了读锁-线程id:389-2021/08/04 17:46:33

可以看到写锁为互斥锁,读锁为共享锁。

  • 读 + 读:共享,并发
  • 读 + 写:互斥,先读后写
  • 写 + 读:互斥,先写后读
  • 写 + 写:互斥

信号量 Semaphore

以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。

在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。

模拟一下上述场景

停车场景

@ResponseBody
@GetMapping("/park")
public String park() throws InterruptedException {
    // 获取信号量(停车场) parking lot
    RSemaphore parkingLot = redissonClient.getSemaphore("parkingLot");
    // 获取一个车位,没有就在门口等
    parkingLot.acquire();

    return "ok";
}

离开场景

@ResponseBody
@GetMapping("/go")
public String go(){
    // 获取信号量(停车场) parking lot
    RSemaphore parkingLot = redissonClient.getSemaphore("parkingLot");
    // 跟看门大爷说一声 车开走了
    parkingLot.release();

    return "ok";
}

向 redis 中插入一个 key 为 parkingLot(停车场) value 为 3(共3个车位)。

打开一个网页连续访问 /park 三次,第四次一直转圈圈。每次访问 redis 中的 parkingLot 值减 1 。

再打开一个网页访问 /go,打开之后之前转圈圈的 /park 访问成功,之后再访问 /go,redis 中的 parkingLot 值加 1。

可以看出使用 acquire() 的获取信号量是阻塞性的,没有就一直等,可以使用 tryAcquire(),没有就走了。

闭锁 CountDownLatch

CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信(而不是用作互斥的作用)。

CountDownLatch 能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在 CountDownLatch 上等待的线程就可以恢复执行接下来的任务。

模拟一个场景,停车场里停了 3 辆车。现在停车场倒闭了,需要关门,关门之前需要 3 辆车都离开才能关门。

关门场景

@ResponseBody
@GetMapping("/lockDoor")
public void lockDoor() throws InterruptedException {
    // 获取闭锁(停车场)
    RCountDownLatch parkingLot = redissonClient.getCountDownLatch("parkingLot");
    // 清点了下 里面有三辆车
    parkingLot.trySetCount(3);
    System.out.println("有三辆车 ~ 等待离开");
    // 等待它们离开
    parkingLot.await();

    System.out.println("车都走了 ~ 准备关门");
}

离开场景

@ResponseBody
@GetMapping("/leave/{car}")
public void leave(@PathVariable("car") String car){
    // 获取闭锁(停车场)
    RCountDownLatch parkingLot = redissonClient.getCountDownLatch("parkingLot");
    // 离开一辆
    parkingLot.countDown();

    System.out.println(car+" 离开了停车场");
}

结果,打开一个网页访问 /lockDoor,一直转圈圈,另一个网页访问三次 /leave。关门结束。

有三辆车 ~ 等待离开
宝马 离开了停车场
红旗 离开了停车场
五菱宏光 离开了停车场
车都走了 ~ 准备关门
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值