随着技术快速发展,数据规模增大,分布式系统越来越普及,一个应用往往会部署在多台机器上(多节点),在有些场景中,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。
白话讲分布式锁:所有请求的线程都去同一个地方占坑
,如果有坑位,就执行业务逻辑,没有坑位,就需要其他线程释放坑位
。这个坑位是所有线程可见的,可以把这个坑位放到 Redis 缓存或者数据库
分布式锁的基本原理:
青铜->钻石(分布式锁) - 掘金 (juejin.cn)
用Redisson实现分布式锁,是目前最流行的分布式锁解决方案
**官方概念:**Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
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.redissongroupId>
<artifactId>redissonartifactId>
<version>3.6.0version>
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分布式锁都与JUC采用了相似的接口和用法。
基于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)
它保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。
RLock fairLock = redisson.getFairLock("anyLock");
// 最常见的使用方法
fairLock.lock();
有看门狗、支持指定加锁时间。
类似于JUC中的读写锁,写写互斥,其中读锁和写锁都继承了RLock接口。
RReadWriteLock rwlock = redisson.getReadWriteLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
有看门狗、支持指定加锁时间。
基于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 后就封顶了。
基于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();
有看门狗、支持指定加锁时间。
适用场景:某个业务与多个业务互斥,并且都需要锁。
与联锁类似,红锁是如果从大于等于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的方式,自定义注解来设置某个方法是否需要获取分布式锁。
根据业务场景复杂度,以及注解的功能复杂度,实现方案也不同。
开源项目方案:
这里举例用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获取分布式锁失败,抛出异常。达到了预期效果。
参考文章:
Redis Redlock 的争论
Redisson 分布式锁源码 09:RedLock 红锁的故事 - 掘金 (juejin.cn)
官方文档中,RedLock已经被弃用: