Redission锁的设计原理和应用
一:基本使用方法
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.2</version>
</dependency>
@Test
public void tt() {
Config config = new Config();
config.useSingleServer().setAddress("127.0.0.1:6399").setDatabase(0);
// 构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
// 设置锁定资源名称
RLock disLock = redissonClient.getLock("helloRedissionLock");
boolean isLock;
try {
//尝试获取分布式锁
isLock = disLock.tryLock(500, 15000, TimeUnit.MILLISECONDS);
if (isLock) {
//TODO if get lock success, do something;
Thread.sleep(15000);
}
} catch (Exception e) {
} finally {
// 无论如何, 最后都要解锁
disLock.unlock();
}
}
二:通过源码解读设计原理
2.1获取锁(关键代码如下)
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call(‘exists‘, KEYS[1]) == 0) then " +
"redis.call(‘hset‘, KEYS[1], ARGV[2], 1); " +
"redis.call(‘pexpire‘, KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call(‘hexists‘, KEYS[1], ARGV[2]) == 1) then " +
"redis.call(‘hincrby‘, KEYS[1], ARGV[2], 1); " +
"redis.call(‘pexpire‘, KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call(‘pttl‘, KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
解读:这是一段lua的脚本
第一个if:判断key = helloRedissionLock是否存在?若不存(==0)在则通过hset命令设置 key = helloRedissionLock的hash对象(key=当前线程ID:1 value=1),并且设置过期时间
第二个if:判断key = helloRedissionLock和hash对象(key=当前线程ID:1 value=1)?若存在(==1)则:1.设置hash对象的value+1,2.重新设置过期时间
return:key = helloRedissionLock的过期时间
备注:Hincrby 命令用于为哈希表中的字段值加上指定增量值,该点是reentrant lock的关键
2.2 解锁
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call(‘exists‘, KEYS[1]) == 0) then " +
"redis.call(‘publish‘, KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
"if (redis.call(‘hexists‘, KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call(‘hincrby‘, KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call(‘pexpire‘, KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call(‘del‘, KEYS[1]); " +
"redis.call(‘publish‘, KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));
解读:
第一个if:判断key=入参是否存在?若不存在(==0),广播机制,然后返回1。
第二个if:判断key和value是否存在?若不存在(==0 返回ni l。
脚本(存在):local counter = redis.call(‘hincrby‘, KEYS[1], ARGV[3], -1);获取value
第三个if:判断counter是否大于0?true-->value-1:
else:删除key并广播
2.3 等待
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
......
}
解读:
通过while(true)实现线程等待
三:应用实例(Springboot)
3.1 简介:在分布式或者多线程场景下,为了让一个数据或方法(数据来源数据库)在同一时刻只能让一个人处理(即串性),我们可以利用Redission锁让只有拿到锁的人才能编辑该数据。
实现原理:利用切面编程+注解(RedissionLock)编程的方式实现对方法上加入了RedissionLock注解的方法进行全局加锁
3.2 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedissionLock {
/**
* 锁住方法中的第几个参数(-1全部参数)
*
* @return
*/
int lockIndexParam() default -1;
/**
* 锁的时间
*
* @return
*/
int leaseTime() default 10;
/**
* 等待时间
*/
int waitTime() default 5;
}
3.3 注册切面
@Aspect
@Component
public class RedissionLockAspect {
@Resource
private RedissonClient redissonClient;
/**
* 环绕通知:灵活*的在目标方法中切入代码
*/
@Around("@annotation(redissionLock)")
public Object around(ProceedingJoinPoint joinPoint, RedissionLock redissionLock) throws Throwable {
// 获取目标方法的名称
String methodName = joinPoint.getSignature().getName();
// 获取方法传入参数
Object[] params = joinPoint.getArgs();
System.out.println("==@Around== lingyejun blog logger --》 method name " + methodName + " args " + params[0]);
RLock lock = redissonClient.getLock(params[redissionLock.lockIndexParam()-1].toString());
boolean b = lock.tryLock(redissionLock.waitTime(), redissionLock.leaseTime(), TimeUnit.MINUTES);
if (b) {
lock.unlock();
return joinPoint.proceed();
} else {
return null;
}
}
}
3.4 使用方式
@RedissionLock(lockIndexParam = 1)
public void tt(String id){
System.out.println("进入了方法::" + id);
}