Redisson 专题

  • **Redisson 简介**
  • 配置类
  • 测试redisson连接
  • **Redisson 看门狗守护机制,实现分布式锁自动续期**
  • 可重入锁
  • 获取Redisson的分布式锁
  • 当还没执行解锁代码突然断电,redisson会不会出现死锁?
  • 加锁时指定时间自动解锁
  • 加锁时指定时间深入源码
  • **读写锁**
  • **信号量**
  • **闭锁**
  • **分布式缓存一致性**
  • 双写模式:写入数据库后再去写入缓存
  • 失效模式:写入数据库后删除缓存
  • 缓存一致性解决方案


Redisson 简介

上一章我们讲到,本地锁和分布式锁的原理,分布式锁通过保证加锁和解锁操作的原子性,来解决本地锁在分布式锁下的弊端。但是在我们的分布式场景下,这种加锁解锁的操作还是挺多的,如果在每个需要用到分布式锁的地方都去加锁解锁都有点麻烦了,更何况分布式锁的究极实现“加锁自动续期”,秉持着 java 轮子比基础多的特点,redis官方提供了一套针对分布式锁进行封装实现的API Redisson。

redis自减超卖 redisson自增_redis

配置类

配置Redisson,返回一个 RedissonClient对象,后面通过redisson的所有操作都是通过这个歌RedissonClient对象进行。

redis自减超卖 redisson自增_java_02

测试redisson连接

redis自减超卖 redisson自增_redis自减超卖_03

Redisson 看门狗守护机制,实现分布式锁自动续期

可重入锁

可重复锁简介:如果现在我们有两个方法,方法A和方法B,在方法A中调用了方法B,并且这两个方法的都要加锁,如果这时A和B使用的是同一把锁,方法A先拿到锁进行执行,这时执行到方法B,方法B张眼一看,方法A已经拿到了自己需要用的锁,就直接可以直接拿来用就行了,业务逻辑处理完方法A把锁进行释放就行了。

如果设计为不可重入锁,方法B也需要拿到锁才能够执行,但是现在这把锁在方法A手里面,B方法没办法拿到锁进行执行,这时两个线程在互相等待就会形成死锁。

所以一句话,我们都应该使用可重入锁,来避免死锁的问题。

redis自减超卖 redisson自增_数据库_04

获取Redisson的分布式锁

注入RedissonClient

redis自减超卖 redisson自增_缓存_05

使用redisson加锁解锁变得格外简单,通过redisson.getLock获取的Rlock是继承了JUC包下的Lock类,所以JUC怎么用Rlock锁还怎么用。

下面的 lock.lock(); 进行加锁时,如果没有拿到锁,会进行阻塞式等待,只要能执行到try方法块的业务肯定是拿到锁了。关于自旋等待还有阻塞式等待会放在文章的最后

redis自减超卖 redisson自增_数据库_06


我们自定义的锁在redis中占的坑位就是,加锁时设置的名称,key是一大串随机的值,可以理解成UUID加上,最后的82表示拿到锁的线程,哪个线程来redis占坑,就把线程号打印到随机值的末尾。

可见底层还是,原子加锁解锁随机key加lua脚本。

redis自减超卖 redisson自增_redis自减超卖_07


当分布式锁一释放,redis里面就没有锁了。

redis自减超卖 redisson自增_redis_08

当还没执行解锁代码突然断电,redisson会不会出现死锁?

redis自减超卖 redisson自增_数据库_09


上图同时开了两个一样的服务发送 hello 请求,在服务 1 抢到锁后,把它停掉,到最后服务 2 的代码还是会执行,这说明就算没有执行到解锁代码,redisson还是会进行自动解锁的,不会出现死锁问题。

redis自减超卖 redisson自增_redis自减超卖_10


来到redis会发现,占位值是有自动过期时间的,redisson官方给的文档有这块的详细说明。

redis自减超卖 redisson自增_redis自减超卖_11


当我们的业务逻辑没有执行完,看门狗会一直为锁续期,当我们的业务执行完后就不会给锁进行自动续期,即使不手动解锁,默认30s后自动解锁。

redis自减超卖 redisson自增_redis_12

加锁时指定时间自动解锁

redis自减超卖 redisson自增_java_13

虽然有加锁指定时间自动解锁,但是如果我们的业务逻辑时间大于自定义的解锁时间,看门狗还会为我们进行自动续期吗?

例如:现在我们加锁时指定了拿到锁10s后自动解锁,但是我们的业务执行了12秒,锁在10秒的时候已经删除了,别人都已经拿到锁进行执行了,导致我们的分布式锁没有锁住,那么看门狗会在10s的时候为我们的锁自动续期吗?

答案是:如果设置了锁的自动过期时间,这表示我们需要有自己的业务处理方式,看门狗是不会为我们自动续期的。那么这样会导致redisson自动删锁线程A后,线程B抢到了锁进行业务处理,这时线程A处理完了业务进行手动删锁会把线程B的锁删掉吗?

当然不会!redisson会提示我们不能使用当前线程进行解锁操作,会抛出异常,因为上面我们看到,redisson往redis中存入的占位锁的最后会加上当前线程号的,如果加锁和解锁的线程不一致,是不能进行解锁的,这也就避免了当前线程自动释放锁后,别的线程拿到锁,当前线程又进行手动删锁而把别人的锁给删了的问题,只能说我们能想到的大佬们早就想到了。

redis自减超卖 redisson自增_缓存_14


所以如果我们自己设置了锁的过期时间,一定要让锁的过期时间大于我们的业务执行时间,以避免抛出上图的异常。

加锁时指定时间深入源码

我们传入过期时间,使用我们的过期时间,否则使用 -1。

redis自减超卖 redisson自增_redis_15


redis自减超卖 redisson自增_缓存_16


尽管有看门狗守护机制,还是建议大家使用自定义的过期时间,在锁过期之前手动解锁就ok了,只要把过期时间设置的长一些就好了,这样就省去了看门狗自动续期的过程,如果我们的业务逻辑执行了很长时间都没有进行手动解锁,肯定是我们的程序出问题了。

redis自减超卖 redisson自增_redis自减超卖_17


redis自减超卖 redisson自增_数据库_18

读写锁

读写锁可以保证一定能读到最新数据,修改期间,写锁是一个排他锁(互斥锁)。读锁也是一个共享锁(读锁和写锁公用一把锁),写锁没释放读就必须等待

读写锁一般都是成对出现的,当线程拿到写锁,读锁就必须进行等待写锁的释放,当所有服务都是读请求等于没有加锁,都并发进来读数据。

redis自减超卖 redisson自增_数据库_19

redis自减超卖 redisson自增_缓存_20

redis自减超卖 redisson自增_redis自减超卖_21


redis自减超卖 redisson自增_java_22

信号量

可以通过现实世界的车库停车来模拟信号量,比如现在有三个车位,都停有汽车,必须有一个车位的汽车开走,新来的汽车才能停进去。如果没有汽车驶出停车位,新来的汽车就需要阻塞式等待。

信号量可以在分布式的系统中做限流操作,如果现在所有服务都来请求要读数据,但是太多的话会使我们的系统卡顿,通过测试出我们系统的峰值来确定一个信号量,比如信号量为1万,当所有服务的请求过来时让每一个请求都去获取一个信号量,如果获取到了就执行自身的业务逻辑,如果没有获取到信号量就表示现在已经达到我们的系统峰值了,不能再让更多的请求过来了,必须等待其他请求处理完自身的业务释放信号量,后续的请求才能获取信号量来执行业务逻辑,这样就达成了一个限流的效果。

redis自减超卖 redisson自增_java_23


redis自减超卖 redisson自增_缓存_24

闭锁

当我们需要所有线程都完成自己的任务时,才能算业务逻辑处理完成,就可以使用闭锁,让所有的线程都加上同一把闭锁,然后当有一个线程完成就将闭锁的计数减一,当所有的线程完成后也闭锁的计数器也就等于零了,当计数器等于零就表示所有的线程都完成了自己的业务,然后再执行其他的业务逻辑处理。

redis自减超卖 redisson自增_数据库_25


学生放假来模拟闭锁,现在有 5 个班,放假的时候只要这 5 个班的学生都走完了,保安才能去锁大门。

redis自减超卖 redisson自增_java_26

redis自减超卖 redisson自增_缓存_27

分布式缓存一致性

redis自减超卖 redisson自增_redis自减超卖_28


当我们把通过分布式锁把数据存入缓存后又有请求进数据库更改了缓存对应的数据,这时就会造成我们的缓存不是最新的数据,那么这时再去缓存中拿数据就不行了,所以我们不得不考虑分布式缓存一致性问题。

当然我们能想到的大佬们早都想到了,也已经为我们铺好了路,解决缓存一致性有两种非常经典的方式 双写模式失效模式,但两种模式再在大数据的情况下都会有所缺陷。

双写模式:写入数据库后再去写入缓存

双写模式的问题:当用户 1 写入数据库之后接着要写入缓存,这时由于网络原因还没有将缓存写入,用户 2 也来写数据库,也去写缓存并且写入成功,这时用户 1 的网络突然又活过来了,继续去写缓存,这就导致会把用户 2 写入的缓存覆盖掉,就会产生脏数据。

解决方案一

所以我们还是一贯作风,加锁保证业务的原子性,让用户 1 拿到锁之后执行完自己的业务逻辑后将锁释放掉给用户 2 使用去处理用户 2 的业务逻辑。

解决方案二

看我们的业务允不允许脏数据的存在,比如我们更改了数据库,但是这些数据需要审核什么的,需要1天或3天,才会给用户进行展示,这时就可以忽略掉这些脏数据,因为我们往缓存中放数据都是设置又过期时间的,当时间到了就自动删除了这些脏数据,等到1天或者3天后,用户再来查询数据,这时缓存的脏数据已经自动删除,就会从数据库查出一份新的数据放入缓存,来保持数据的最终一致性

redis自减超卖 redisson自增_java_29

失效模式:写入数据库后删除缓存

失效模式的问题:例如:现在有三个用户按照下图的方式对数据库和缓存进行操作,用户 1 写入数据库数据然后删除缓存,这时用户 2 进来写数据到数据库,还没有进行删除缓存,这时用户 3 进来读缓存,缓存中没数据然后去读数据库,这时用户 2 还在写数据库,用户 3 读到了用户 1 写的数据库,读完数据库准备去更新缓存,这时用户 2 写完了数据库进行了删除缓存的操作,用户 3 将读到用户 1 写入的数据写入了缓存,用户 2 已经执行了删缓存的操作,将没有人能去制约用户 3 写入缓存, 这就导致了缓存与数据库的不一致性。

解决方案一

所有的业务时序问题我们都可以通过加锁来解决,来保证原子性,只要当前服务完成了自身业务才去释放锁,让其他服务执行业务。

解决方案二

我们需要考虑这些经常修改的数据到底要不要放入缓存,如果读多写少,肯定是要放缓存的,可是如果数据经常修改,就没有必要再将这些数据放入缓存了。

redis自减超卖 redisson自增_redis自减超卖_30

缓存一致性解决方案

redis自减超卖 redisson自增_java_31

redis自减超卖 redisson自增_redis_32