最近的自己,一直都在做些老年的技术,没有啥升级,自己也快麻木了,自己该怎么说,那必须行动起来啊!~来来,我们一起增长自己的内功
分布式锁的最强实现: Redisson
在介绍之前,我们要知道这个
Redisson
是啥? 难道就是Redis
的son
?(我第一次就这么认为的哈哈!) 事实也的确如此–>看下面解释首先来点专业点解释:
Redisson
是In-momery data Grid
(建立在Redis
基础上的java
驻内存网格)(一种针对分布式的缓冲技术挺棒的!)通讯基于
Netty
,面向企业级开发提供一系列的分布式
java
常用对象,以及分布式服务等大白话:
a.就是特么封装许多分布式常用功能的分布式工具,
b.简化开发者开发,更专注于业务开发 ,而不是话大多时间的
Redis
上面 c.避免分布式造轮子
这玩意儿我也说下吧?就是并发业务的刚需;俗话说: 并发好不好,就看这把锁管的好不好!
有啥example
呢? (有啊!骚等~ ) 我们就以Redis
来写一个简单的分布式锁:~
编写一个最简单的
Redis
分布式锁: 采用SpringDataRedis
的RedisTemplate
setIfAbsent
:setNx
+expire
// 加锁
public Boolean tryLock(String key, String value, long timeout, TimeUnit unit) {
return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
}
// 解锁,防止删错别人的锁,以uuid为value校验是否自己的锁
public void unlock(String lockName, String uuid) {
if(uuid.equals(redisTemplate.opsForValue().get(lockName)){
redisTemplate.opsForValue().del(lockName);
}
}
// 结构
if(tryLock){
// todo
}finally{
unlock;
}
上面的代码完成锁操作,但是我们会发现无法保证原子性,一旦并发量高了,就要好家伙了!~
咋解决呢?咋保证原子性啊?我咋知道啊!(哈哈!我们往下读,自会柳暗花明)我们引入Redis
内置的语言:lua
,
lua
脚本我们使用
lua
的 ,将操作封装成一个lua
脚本,通过Redis
的eval
/evalsha
lua
脚本代码:
脚本名称:
lockDel.lua
if redis.call('get', KEYS[1]) == ARGV[1]
then
-- 执行删除操作
return redis.call('del', KEYS[1])
else
-- 不成功,返回0
return 0
end
删除java
代码:
// 解锁脚本
DefaultRedisScript<Object> unlockScript = new DefaultRedisScript();
unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lockDel.lua")));
// 执行lua脚本解锁
redisTemplate.execute(unlockScript, Collections.singletonList(keyName), value);
写到这儿,我们也添加了lua
,也能保证原子性了吧!但是你看看是不是少了啥?(哈哈,你心里是不是在说,我看出来个锤子啊!没毛病啊)
嗯嗯,那我提个醒—按照上面的思路:如果一个线程多次拿锁呢?好家伙是不是来问题了?怎么解决呢?(想没想到?都往下看吧!哈哈)
怎么保证可重入?
首先: 我们应该明白可重入的核心:同一个线程多次获取同一把锁是许可的,不会产生死锁
嗯~道理没怎么懂,我们用
Synchronized
的偏向锁来理解其实现思路
synchronized
实现重入是在JVM
层面,java
对象头MARK word
中含有线程ID和计数器对线程做重入判断,从而避免每次的CAS
- 当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储偏向的线程
ID
- 这样后面此线程ID再进入和退出同步块时,就不用
CAS
来进行加锁和解锁啦- 只需要检测MARK WORD对象头里面是否存储指向当前线程的偏向锁
- 测试成功:线程获得锁
- 测试失败:再测试下
MARK WORD
偏向锁标志是否设置为1
- 0: 没有
CAS
竞争- 1:
CAS
将对象头偏向锁指向当前线程- 还有一个计数器:同个线程进入则自增1,离开再减1,直到为0释放
根据上面的思路:(我们对要实现的lua
脚本进行改造)
需要存储锁名称lockName
该锁线程ID
对应线程进入的次数: count
不存在:
设置hash的Key为线程ID,value=1
设置expireTime(过期时间)
返回获取锁,成功为true
存在:
继续判断是否存在当前线程ID的hash key
存在:
线程key的value+1,重入次数加1(count++),设置expireTime
不存在:
返回加锁失败
存在:
是否有该线程的ID的hash key,有则减1,没有返回解锁失败
减1后:
判断剩余count为不为0
=0: 不再需要这把锁,执行del命令删除
Redis采用HASH结构
锁名称: lock_name1`
key: thread_id (唯一键,线程ID)
value
:
count` (计数器)
hset lock_name1 thread_id 1
hget lock_name1
当同一个线程获取同一把锁时,我们需要对应线程的计数器count做加减
怎么判断一个Redis的key是否存在,
我们可以用exists/hexists判断一个hash的key是否存在,
hset lock_name1 thread_id 1
exists/exists lock_name1
hash的自增命令:
hincrby lock_name1 thread_id 1
当一把锁不再被需要了,每解锁一次,count减1,直到为0,执行删除
LUA
代码加锁
lock.lua
local key = KEYS[1];
local threadId = ARGV[1];
local releaseTime = ARGV[2];
-- lockname不存在
if(redis.call('exists', key) == 0) then
redis.call('hset', key, threadId, '1');
redis.call('expire', key, releaseTime);
return 1;
end;
-- 当前线程已id存在
if(redis.call('hexists', key, threadId) == 1) then
redis.call('hincrby', key, threadId, '1');
redis.call('expire', key, releaseTime);
return 1;
end;
return 0;
解锁
unlock.lua
local key = KEYS[1];
local threadId = ARGV[1];
-- lockname、threadId不存在
if (redis.call('hexists', key, threadId) == 0) then
return nil;
end;
-- 计数器-1
local count = redis.call('hincrby', key, threadId, -1);
-- 删除lock
if (count == 0) then
redis.call('del', key);
return nil;
end;
RedisLock.java
: 实现分布式锁实现功能: 互斥,可重入,防死锁
/**
* @description 原生redis实现分布式锁
**/
@Getter
@Setter
public class RedisLock {
private RedisTemplate redisTemplate;
private DefaultRedisScript<Long> lockScript;
private DefaultRedisScript<Object> unlockScript;
public RedisLock(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
// 加载加锁的脚本
lockScript = new DefaultRedisScript<>();
this.lockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lock.lua")));
this.lockScript.setResultType(Long.class);
// 加载释放锁的脚本
unlockScript = new DefaultRedisScript<>();
this.unlockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("unlock.lua")));
}
/**
* 获取锁
*/
public String tryLock(String lockName, long releaseTime) {
// 存入的线程信息的前缀
String key = UUID.randomUUID().toString();
// 执行脚本
Long result = (Long) redisTemplate.execute(
lockScript,
Collections.singletonList(lockName),
key + Thread.currentThread().getId(),
releaseTime);
if (result != null && result.intValue() == 1) {
return key;
} else {
return null;
}
}
/**
* 解锁
* @param lockName
* @param key
*/
public void unlock(String lockName, String key) {
redisTemplate.execute(unlockScript,
Collections.singletonList(lockName),
key + Thread.currentThread().getId()
);
}
}
上面代码,已经实现了大部分的功能,对于一般的场景都是可以从容应对了,但是(哈哈哈,最怕但是了是不?)
针对特殊场景:
1.若A进程在获取到锁的时候,因为业务操作时间太长,锁释放了,但是业务还是在执行,此刻B 进程有可以正常拿到锁进行业务操作,这时候就会出现A,B进程共享资源的问题
2.若负责存储这个分布式锁的
Redis
宕机,儿此时这个锁正好处于锁住状态,这时就会造成锁死的状态
那对于这种情况时,我们需要采用锁续约
锁续约:延长锁的
ReleaseTime
直到完成业务期望结果,本质就是不断延长锁过期时间
但是,对于当中的处理续约,性能比如锁的最大等待时间,无效的锁申请,与以及失败重试机制等等我们写到猴年马月啊!~立即推=—>放弃哈哈哈!!
别别别~来这时候我们就要介绍我们今天的主角:Reidsson
!!(解决上面所有问题)
Redisson
分布式锁兵马未动,粮草先行
我们看看咋使用啊~是不是
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.13.6version>
dependency>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-boot-starterartifactId>
<version>3.13.6version>
dependency>
@Configuration
public class RedissionConfig {
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.password}")
private String password;
private int port = 6379;
@Bean
public RedissonClient getRedisson() {
Config config = new Config();
config.useSingleServer().
setAddress("redis://" + redisHost + ":" + port).
setPassword(password);
config.setCodec(new JsonJacksonCodec());
return Redisson.create(config);
}
}
简洁明了,只需要一个
RLock
,如下面的代码所示
@Resource
private RedissonClient redissonClient;
RLock rLock = redissonClient.getLock(lockName);
try {
boolean isLocked = rLock.tryLock(expireTime, TimeUnit.MILLISECONDS);
if (isLocked) {
// TODO
}
} catch (Exception e) {
rLock.unlock();
}
RLock
还在更…