Redisson实现分布式锁从入门到应用
Posted 结构化思维wz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redisson实现分布式锁从入门到应用相关的知识,希望对你有一定的参考价值。
分布式锁
随着技术快速发展,数据规模增大,分布式系统越来越普及,一个应用往往会部署在多台机器上(多节点),在有些场景中,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。
- 在单机环境中,应用是在同一进程下的,通过Java并发包提供的API即可保证线程的安全性。
- 在集群多机部署的环境中,应用在不同的进程中,也就引出了分布式锁的问题。
白话讲分布式锁:所有请求的线程都去同一个地方占坑
,如果有坑位,就执行业务逻辑,没有坑位,就需要其他线程释放坑位
。这个坑位是所有线程可见的,可以把这个坑位放到 Redis 缓存或者数据库
分布式锁的基本原理:
Redis实现分布式锁
Redisson
用Redisson实现分布式锁,是目前最流行的分布式锁解决方案
**官方概念:**Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson的基础概念和使用
Netty 框架:Redisson采用了基于NIO的Netty框架,不仅能作为Redis底层驱动客户端,具备提供对Redis各种组态形式的连接功能,对Redis命令能以同步发送、异步形式发送、异步流形式发送或管道形式发送的功能,LUA脚本执行处理,以及处理返回结果的功能
基础数据结构:将原生的Redis Hash,List,Set,String,Geo,HyperLogLog等数据结构封装为Java里大家最熟悉的映射(Map),列表(List),集(Set),通用对象桶(Object Bucket),地理空间对象桶(Geospatial Bucket),基数估计算法(HyperLogLog)等结构。
分布式数据结构:这基础上还提供了分布式的多值映射(Multimap),本地缓存映射(LocalCachedMap),有序集(SortedSet),计分排序集(ScoredSortedSet),字典排序集(LexSortedSet),列队(Queue),阻塞队列(Blocking Queue),有界阻塞列队(Bounded Blocking Queue),双端队列(Deque),阻塞双端列队(Blocking Deque),阻塞公平列队(Blocking Fair Queue),延迟列队(Delayed Queue),布隆过滤器(Bloom Filter),原子整长形(AtomicLong),原子双精度浮点数(AtomicDouble),BitSet等Redis原本没有的分布式数据结构。
分布式锁:Redisson还实现了Redis文档中提到像分布式锁Lock这样的更高阶应用场景。事实上Redisson并没有不止步于此,在分布式锁的基础上还提供了联锁(MultiLock),读写锁(ReadWriteLock),公平锁(Fair Lock),红锁(RedLock),信号量(Semaphore),可过期性信号量(PermitExpirableSemaphore)和闭锁(CountDownLatch)这些实际当中对多线程高并发应用至关重要的基本部件。正是通过实现基于Redis的高阶应用方案,使Redisson成为构建分布式系统的重要工具。
节点:Redisson作为独立节点可以用于独立执行其他节点发布到分布式执行服务
和分布式调度服务
里的远程任务。
-
引入Maven
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.6.0</version> </dependency>
-
定义配置类
@Configuration public class RedissonConfig @Value("$spring.redis.host") private String host; @Value("$spring.redis.port") private String port; @Value("$spring.redis.password") private String password; /** * RedissonClient,单机模式 */ @Bean(destroyMethod = "shutdown") public RedissonClient redisson() throws IOException Config config = new Config(); config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password); return Redisson.create(config);
-
测试配置类
@Autowired RedissonClient redissonClient; @Test void contextLoads() System.out.println(redissonClient);
输出信息: org.redisson.Redisson@25bc65ab
Redisson分布式锁
Redisson分布式锁都与JUC采用了相似的接口和用法。
可重入锁(Reentrant Lock)
基于Redis的Redisson分布式可重入锁
RLock
Java 对象实现了java.util.concurrent.locks.Lock
接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
案例:
// 1.设置分布式锁
RLock lock = redisson.getLock("WZ_LOCK");
//2.获取锁
lock.lock();
//3.执行业务逻辑
....
//4.释放锁
lock.unlock();
//另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
//还可以通过 trylock()方法自定义获取锁,类似juc中的trylock()方法。
可重入锁的两点思考:
-
多个线程抢占锁,后面的锁是否需要等待?
# Redisson 的可重入锁(lock)是阻塞其他线程的,需要等待其他线程释放的。
-
如果抢占到锁的服务停了,锁会不会自动释放?
# 会释放,默认看门狗检查锁的超时时间是30s。 Redisson看门狗原理
看门狗原理
一个线程获取到Redis锁后,锁的有效时间是多少??
如果这个服务挂掉了,锁会自动释放吗??
模拟业务执行时间为60s,观察redis中WZ_LOCK键的超时时间:
// 1.获取锁,只要锁的名字一样,获取到的锁就是同一把锁。
RLock lock = redisson.getLock("WZ_LOCK");
try
// 2.加锁
lock.lock();
System.out.println("加锁成功,执行后续代码。线程 ID:" + Thread.currentThread().getId());
Thread.sleep(60000);
catch (Exception e)
//TODO
finally
// 3.解锁
lock.unlock();
System.out.println("Finally,释放锁成功。线程 ID:" + Thread.currentThread().getId());
现象:
超时时间默认为30s,当时间降为20s的时候超时时间又被设置到了30s。
如果中途服务挂掉了,那么超时时间到20s后也不会重新设置,而是继续变小,知道过期删除key
看门狗原理:
如果负责储存这个分布式锁的 Redisson 节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗
,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。
默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
如果我们未指定 lock 的超时时间,就使用 30 秒作为看门狗的默认时间。只要占锁成功,就会启动一个定时任务
:每隔 10 秒重新给锁设置过期的时间,过期时间为 30 秒。
当服务器宕机后,因为锁的有效期是 30 秒,所以会在 30 秒内自动解锁。(30秒等于宕机之前的锁占用时间+后续锁占用的时间)。
图片引自:王者方案 - 分布式锁 Redisson - 掘金 (juejin.cn)
公平锁(Fair Lock)
它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。
RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();
有看门狗、支持指定加锁时间。
分布式读写锁(ReadWriteLock)
类似于JUC中的读写锁,写写互斥,其中读锁和写锁都继承了RLock接口。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
有看门狗、支持指定加锁时间。
信号量(Semaphore)
基于Redis的Redisson的分布式信号量(Semaphore)Java对象
RSemaphore
采用了与java.util.concurrent.Semaphore
相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
关于信号量的使用大家可以想象一下这个场景,有三个停车位,当三个停车位满了后,其他车就不停了。可以把车位比作信号,现在有三个信号,停一次车,用掉一个信号,车离开就是释放一个信号。
我们用 Redisson 来演示上述停车位的场景。
先定义一个占用停车位的方法:
/**
* 停车,占用停车位
* 总共 3 个车位
*/
@ResponseBody
@RequestMapping("park")
public String park() throws InterruptedException
// 获取信号量(停车场)
RSemaphore park = redisson.getSemaphore("park");
// 获取一个信号(停车位)
park.acquire();
return "OK";
复制代码
再定义一个离开车位的方法:
/**
* 释放车位
* 总共 3 个车位
*/
@ResponseBody
@RequestMapping("leave")
public String leave() throws InterruptedException
// 获取信号量(停车场)
RSemaphore park = redisson.getSemaphore("park");
// 释放一个信号(停车位)
park.release();
return "OK";
在redis中创建 key park ,value 设置为3,发起请求观察,发现每次获取信号量,值减一,当value为0时,再发起请求获取停车位,那么接口就会阻塞。
当发起释放停车位接口时:
刚才阻塞的请求可以获取到车位。如果想要不阻塞,可以用 tryAcquire 或 tryAcquireAsync。
注意:多次执行释放信号量操作,剩余信号量会一直增加,而不是到 3 后就封顶了。
联锁(MultiLock)
基于Redis的Redisson分布式联锁
RedissonMultiLock
对象可以将多个RLock
对象关联为一个联锁,每个RLock
对象实例可以来自于不同的Redisson实例。
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 所有的锁都上锁成功才算成功。
lock.lock();
...
lock.unlock();
有看门狗、支持指定加锁时间。
适用场景:某个业务与多个业务互斥,并且都需要锁。
红锁(RedLock)
与联锁类似,红锁是如果从大于等于n/2+1个实例获取锁成功,则获取分布式所就成功。
RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同时加锁:lock1 lock2 lock3
// 红锁在大部分节点上加锁成功就算成功。
lock.lock();
...
lock.unlock();
有看门狗、支持指定加锁时间。
项目应用方案
AOP+Redisson
项目中许多需要用到分布式锁的地方,防止并发修改对数据一致性的影响。获取分布式锁的过程有可以抽象为几个步骤,发现步骤基本相似,所以可以采用AOP的方式,自定义注解来设置某个方法是否需要获取分布式锁。
根据业务场景复杂度,以及注解的功能复杂度,实现方案也不同。
开源项目方案:
- lock4j: 基于Spring AOP 的声明式和编程式分布式锁,支持RedisTemplate、Redisson、Zookeeper (gitee.com)
- limbo-world/limbo-locker: 基于Redisson的分布式锁封装。封装了锁模板,支持单锁、联锁;实现了基于注解的AOP,能够简单在方法上添加注解实现加锁;支持SpringBoot一键开启自动装配,无需额外配置。 (github.com)
这里举例用AOP实现一个最简单的单机分布式锁。
-
定义注解
/** * RedissonLock * @author ZeWang2 */ @Target(ElementType.METHOD,ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface RedissonLock //key值,锁名称 String lockName();
-
定义AOP切面
/** * @ClassName: RedissonAop * @Author: Ze WANG * @Date: 2022/8/22 **/ @Order(0) @Aspect @Component @Slf4j public class RedissonAop @Resource private RedissonClient redisson; @Around("@annotation(redissonLock)") public Object setRedissonLock(ProceedingJoinPoint joinPoint,RedissonLock redissonLock) throws Throwable //获取锁 String lockName = redissonLock.lockName(); RLock rLock = redisson.getLock(lockName); log.info(Thread.currentThread().getName()+"===尝试获取分布式锁===",lockName); //加锁 boolean tryLock = rLock.tryLock(); //执行业务 if(!tryLock) throw new IllegalAccessError("当前线程获取分布式锁失败"); log.info(Thread.currentThread().getName()+"===获取分布式锁成功==="); Object ret = joinPoint.proceed(); //释放锁 rLock.unlock(); log.info(Thread.currentThread().getName()+"===释放分布式锁===",lockName); return ret;
-
使用注解
@Service public class TestAop @RedissonLock(lockName = "WZ_LOCK") public void testA() System.out.println(Thread.currentThread().getName()+"业务代码执行中....."); try Thread.sleep(30000); catch (InterruptedException e) throw new RuntimeException(e); System.out.println(Thread.currentThread().getName()+"业务代码执行结束....");
@Autowired private TestAop testAop; @GetMapping("trylock") public void testAop() testAop.testA();
-
测试
首先发起一个Http请求:
可以发现,当前exex-3获取到了分布式锁,正在执行业务…
再发起一个http请求:
exex-2获取分布式锁失败,抛出异常。达到了预期效果。
用不用RedLock?
参考文章:
Redisson 分布式锁源码 09:RedLock 红锁的故事 - 掘金 (juejin.cn)
官方文档中,RedLock已经被弃用:
以上是关于Redisson实现分布式锁从入门到应用的主要内容,如果未能解决你的问题,请参考以下文章