通过本文档,你将会了解到
- 本地锁不香么?你搞什么分布式锁?
- 分布式锁的产生背景?
- 分布式锁怎么玩?以及玩起来的注意事项
1、 回顾
上次我们讲到了,本地缓存的缺陷,所以喽,直接用一个缓存中间件redis喽。关于怎么使用,就不扯了,大家都会。接下来主要讲的是我们项目中使用到的分布式锁。
2、锁
2.1、本地锁
说到加锁 我们有很多方式,比如synchronized,JUC包下的各种各样的锁,但是这种锁只能锁住当前进程,我们部署10台机器,本地锁就玩不转了。所以分布式锁说那我来吧,这也是我们项目中使用到的。我们所有的机器都先去占坑,如果占坑成功,那么你想怎么样就怎么样。
所以喽要使用同一个锁,我们使用redis分布式锁,当然也有ZK锁。我们讲我们项目中使用到的redis分布式锁。我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。 “占坑”可以去redis,可以去数据库,可以去任何大家都能访问的地方。 等待可以自旋的方式。
上次我们讲到缓存击穿的时候说到过:加锁。大量并发只让一个人去查,其他人等待,查到之后释放锁,其他人获取到锁,先查缓存,就会有数据,不用去查数据库
2.2、分布式锁
2.2.1、介绍
分布式锁的原理就是占坑呗,所有的人都想上厕所,那谁占到坑,那谁上喽,分布式锁大概就是这个原理搞的。
废话不多少,直接上菜呗,Redis里面那个是占坑的?说到学习一门新的知识,那么权威肯定是官网喽。
redis官网
http://www.redis.cn/
http://www.redis.cn/commands/set.html
有个命令比较有特色就是set命令 这个就是占坑的命令。这个坑占起来还有讲究,hahah
SET 里面放个key 一个value 这个后面可是有可选参数哦,有的是过期时间,有一个是NX,所以综上所述
占坑命令就是
set lock haha NX
SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds – Set the specified expire time, in seconds.
PX milliseconds – Set the specified expire time, in milliseconds.
NX – Only set the key if it does not already exist.
XX – Only set the key if it already exist.
EX seconds – 设置键key的过期时间,单位时秒
PX milliseconds – 设置键key的过期时间,单位时毫秒
NX – 只有键key不存在的时候才会设置key的值
XX – 只有键key存在的时候才会设置key的值
注意: 由于SET命令加上选项已经可以完全取代SETNX, SETEX, PSETEX的功能,所以在将来的版本中,redis可能会不推荐使用并且最终抛弃这几个命令。
2.2.2、分布式锁的演进过程
巧了,我懂了,直接用set lock haha NX 分布式锁搞的,分享结束。你觉得会是这么简单么?不要太天真,到时候发现小丑竟是我自己hahaha。
2.2.2.1、青铜玩家
来上菜。我们先看青铜玩家怎么玩?
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "11111");
if (lock) {
try {
//加锁成功...执行业务
XXXXXXX
//删除
stringRedisTemplate.delete(XXXXXX);
}else{
//重试 自旋
1.自己调取自己
2.可以休眠1S
}
你看看青铜玩家,一看就是新手。一眼就看出来的错误就是异常了没人解锁了。
解决:
- 设置锁的自动过期,即使没有删除,会自动删除。
- try 包裹 finally解锁
2.2.2.2、白银玩家
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
try {
//加锁成功...执行业务
}else{
//重试 自旋
1.自己调取自己
2.可以休眠1S
} finally{
//删除
stringRedisTemplate.delete(XXXXXX);
}
你看看白银玩家,有点那么个意思了是吧?那白银玩家有没有问题呢?阶段才是白银了,那肯定是有问题的,关键是问题在哪里?
1、删除锁直接删除???
如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。
2、解决:
- 占锁的时候,值指定为uuid,每个人匹配是自己的锁才删除。。
2.2.2.2、黄金玩家
关键是问题在哪里?
问题:
1、如果正好判断是当前值,正要删除锁的时候,锁已经过期, 别人已经设置到了新的值。那么我们删除的是别人的锁。
2、解决:
- 删除锁必须保证原子性。使用redis+Lua脚本完成
2.2.2.2、钻石玩家
//1、占分布式锁。去redis占坑 设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
Map> dataFromDb = null;
try {
//加锁成功...执行业务
dataFromDb = getDataFromDb();
} finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
stringRedisTemplate.execute(new DefaultRedisScript(script, Long.class), Arrays.asList("lock"), uuid);
}
当然线上你可以这么写,但是毕竟是钻石玩家,我们还有更好的玩法。
2.3、Redisson 作为分布式锁
这个也是我们项目中使用到的。废话不多说,第一步打开官网,开启寻宝之路。
https://www.redis.io/topics/distlock
分布式锁的介绍页面
2.3.1 Redisson 是什么
一种 可重入、持续阻塞、独占式的 分布式锁协调框架。
Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
特点:
- 可重入
拿到锁的线程后续拿锁可跳过获取锁的步骤,只进行value+1的步骤。 - 持续阻塞
获取不到锁的线程,会在一定时间内等待锁。 - 独占式
同一环境下理论上只能有一个线程可以获取到锁
比较爽的一点是什么?
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。
这句话什么意思?如果你已经了解了JUC包下的各种锁,什么ReentrantLock,信号量什么的。恭喜你无缝切换。就像你换手机一样,iPhone12换iPhone13 像流水切换一样自然。以前怎么用,现在还怎么用。
关于并发编程JUC的知识不在本次分享范围内。
//1、获取一把锁,只要锁的名字一样,就是同一把锁
RLock myLock = redisson.getLock("my-lock");
//2、加锁
myLock.lock(); //阻塞式等待。默认加的锁都是30s
//1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
//2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
// myLock.lock(10,TimeUnit.SECONDS); //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
//问题:在锁时间到了以后,不会自动续期
//1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
//2、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
//只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
// internalLockLeaseTime 【看门狗时间】 / 3, 10s
try {
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
} catch (Exception ex) {
ex.printStackTrace();
} finally {
//3、解锁 假设解锁代码没有运行,Redisson会不会出现死锁
System.out.println("释放锁..." + Thread.currentThread().getId());
myLock.unlock();
}
关于看门狗,就是业务执行需要10秒,还没执行完就解锁了?看门狗自动帮你续命时间(这个是一个定时任务,没10秒执行一次看看要不要续命)当前线程活着就续命,线程(而key的组成应该是:{uuid}:{threadid})只有没有指定过去时间才会启动。
最佳实践:我们指定过期时间,性能会有一点增加,指定3秒过期时间,3秒都没执行完,那早就超时了什么的。