引言
如果学习过操作系统,那么肯定就知道锁的概念;锁的出现就是为了解决在有限的资源下,同一时间段内的多个线程同时访问该资源而产生的冲突问题;通过锁就可以控制多个并发执行的线程对资源的访问;
在java中可以通过synchronized 关键字或者并发包的类实现锁;但是这种方法适用于单体项目(单个JVM),而现很多大公司都是微服务项目,多个服务器 (不同JVM) 中跑着相同的项目;这样就无法通过以上方法实现锁了;那么资源分配的问题该如何解决?分布式锁就是为了解决这种问题的;
分布式锁实现关键
在同一时间多个服务器存在的情况下怎样保证只有一台服务器抢到锁呢?
核心思想就是:先来的人先把数据改成自己的标识(服务器 ip),后来的人发现标识已存在,就抢锁失败,继续等待。
等先来的人执行方法结束,把标识清空,其他的人继续抢锁。
看着很简单,但是需要注意几点:
-
1使用完锁要释放
-
2锁要加上过期时间(如过没有过期时间,那么假如上锁的服务器宕机了,就无法释放锁了,就造成死锁了)
-
3但是锁一旦加上过期时间又会有新的问题:如果上锁的服务执行时间过长,锁过期了还没执行完,那怎么办?
此时可能会产生以下两种问题:
-
比如此时A服务拥有锁,当A执行了一段时间后锁过期了,此时A服务还没有执行完,因为每个锁解锁前会判断解锁的人是不是上锁的人,而假设此时A服务刚好判断了解锁的服务是A,进入了解锁逻辑,还没有开始解锁,但是锁就已经不存在了(但是解锁逻辑还要继续执行);与此同时B服务看到A服务的锁过期了,那么它就来抢占锁,当它抢到锁后开始执行,此时AB服务都在执行,而A服务开始了解锁逻辑,B服务刚开始;那么就会出现A服务把B服务的锁给释放掉了的问题;这就是连锁效应
-
同样这样也是会存在多个方法同时执行的问题;
伪代码再解释一下:
// 服务A上的锁解锁判断 if (ip == A) { // 上面那种请求就发生在刚判断完当前的锁是A的锁后,进入了该逻辑中,还没开始解锁,锁就自己过期了; // 那么下面继续执行解锁逻辑时就会把刚进来执行的B的锁给释放; 开始执行解锁逻辑 }
-
举个例子理解一下:
比如此时同时有A、B、C三个人想上厕所,而厕所只有一个;
A速度比较快,他先抢到了厕所的使用权,那么他防止其他人在他使用时干扰,就把厕所门锁住了;
-
1当A上完厕所后就需要打开锁开门,这就是 使用完锁要释放;
-
2但是假如A上厕所时不小心掉坑里了,那么他上的锁就无法释放,外面的人怎么也进不来;所以A上锁也要有个时间限制,防止自己出事后别人不知道还在等待;这就是锁要加上过期时间
-
3现在A上厕所给锁加上了过期时间,当超过了这个时间锁就会自动释放,B和C就会开始抢着来上厕所;假设此时A上的锁已经过期了,但是A还没上完厕所,但是而B看到锁过期了,于是抢到了厕所使用权,并上锁;那么就会出现一种情况:A和B一起上厕所;过了一会A上完了厕所,但是B没有上完,A要出去,于是他就把B上厕所前上的锁给释放了,那么外面的C一看没锁了,就会去抢厕所和B一起上…(大致是这个意思,但是解锁时因为需要判断解锁的人是谁,这里忽略了这一点,只是为了方便理解)
这就是A上厕所时间超过了上的锁的过期时间而产生的一连锁的问题;其中总是存在两人同时上厕所的情况发生;
那么该如何解决最后第三种情况产生的问题呢?
很简单的方法:当一个服务还没有执行完而锁快过期了,那么让这个锁再 续期(看门狗机制) 就好了;
伪代码:
boolean end = false; // 当前线程执行状态
new Thread(() -> {
if (!end)}{ // 如果当前线程没有执行结束,则续期
续期
})
end = true;
解决’‘释放锁的时候,有可能先判断出是自己的锁,但这时锁过期了,最后还是释放了别人的锁’'的方法:
那么就需要保证判断解锁对象和解锁整体是一个原子操作(可以理解为是一个事务的意思);
出现上面问题的原因无非就是当判断完A之后(get lock == A),B就set lock B设置了它的锁,然后就被A的解锁逻辑del lock释放了;如果保证了原子操作,那么当A开始判断时就不允许其他服务设置锁,必须让A解锁逻辑执行完成之后B才可以上锁;
// 原子操作
if(get lock == A) {
// set lock B 如果保证原子操作,那么B就不能在这里执行该操作了;也就避免了B刚上锁就被解锁的问题;
del lock
}
// 当以上代码执行完成后其他服务才可以上锁
可以把这个原子操作看成一个整体,该操纵执行时不能收到外部干扰;
实现方法可以通过redis的lua脚本实现;
补充:释放锁的逻辑一定要写到finally中,不要写到try中,因为try中的代码一旦抛异常就会跳过后面的代码,可能就会造成锁无法释放;
思考:如果redis是集群,分布式锁数据不同步那该怎么办?
思路很简单:再给reids集群加上分布式锁就行了;(给上锁的工具再加上锁)
Redisson 实现分布式锁
Redisson 是一个 java 操作 Redis 的客户端,提供了大量的分布式数据集来简化对 Redis 的操作和使用,可以让开发者像使用本地集合一样使用 Redis,完全感知不到 Redis 的存在。
两种引入方式:
- spring boot starter 引入(不推荐,版本迭代太快,容易冲突)https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter
- 直接引入:https://github.com/redisson/redisson#quick-start
Redisson配置完成后操作很简单,调用方法名和集合几乎一模一样;可以通过查阅文档学习;
redisson操作和集合操作对比:
// 集合操作:list,数据存在本地 JVM 内存中
List<String> list = new ArrayList<>();
list.add("yupi");
System.out.println("list:" + list.get(0));
list.remove(0);
// redisson操作:数据存在 redis 的内存中
RList<String> rList = redissonClient.getList("test-list");
rList.add("xxxx"); // reids中新增数据
System.out.println("rlist:" + rList.get(0));
rList.remove(0); // redis中删除数据
操作list集合和redisson是不是一模一样;map操作也是同样的,可以自行尝试;
不要自己写分布式锁,借用工具会更高效;
分布式锁应用场景举例
最典型的分布式锁应用场景就是定时任务;
假设此时有三台服务器,都存在同一个定时任务逻辑;那么只要一到定时任务设定的时刻,三台服务器会同时执行相同的代码,可能就会造成冲突或者浪费资源;
所以我们的目的就是想要在同一时间保证三台服务器只有一台可以执行该定时任务;
使用分布式锁的解决方案,我们可以设置一个锁lock,当定时任务要执行时,让三台服务器同时来抢这个锁,谁抢到这个锁谁来执行定时任务逻辑代码;
示例代码:
void testWatchDog() {
RLock lock = redissonClient.getLock("xxxx:lock"); // 通过redisson在reids中设置分布式锁
try {
// 只有一个线程能获取到锁
// (第一个参数是多长时间抢一次锁,这里0是只允许抢一次)
// (第二个参数是锁的过期时间,如果使用了看门狗机制,那么过期时间设置为-1)
if (lock.tryLock(0, -1, TimeUnit.MILLISECONDS)) {
// 实际要执行的方法
doSomeThings();
System.out.println("getLock: " + Thread.currentThread().getId());
}
} catch (InterruptedException e) {
System.out.println(e.getMessage());
} finally {
// 只能释放自己的锁
if (lock.isHeldByCurrentThread()) { // 判断当前释放锁的线程是不是上锁的线程
System.out.println("unLock: " + Thread.currentThread().getId());
lock.unlock();
}
}
}
再次强调:释放锁要写在 finally 中!!!
看门狗机制
这是redisson 中提供的续期机制,当锁过期而服务未执行结束时会自动续期;
看门狗机制会开一个监听线程,如果方法还没执行完,就帮你重置 redis 锁的过期时间。
原理:
- 监听当前线程,默认过期时间是 30 秒,每 10 秒续期一次(补到 30 秒)
- 如果线程挂掉(注意 debug 模式也会被它当成服务器宕机),则不会续期
可以参考文章:https://blog.csdn.net/qq_26222859/article/details/79645203
总结
分布式锁在很多地方都有应用;作者也是第一次接触到;简单做个笔记总结(仅供参考!!!),如果理解上有偏颇,欢迎各位的指正!