Redission锁的设计原理和应用

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 " +  //判断当前锁是否存 0不存在
              "redis.call('hset', KEYS[1], ARGV[2], 1); " +  //设置锁
              "redis.call('pexpire', KEYS[1], ARGV[1]); " + //设置过期时间
              "return nil; " +                                //返回nil 表示获取锁成功
          "end; " +
          "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " + //判断指定的hash值和属性是否存在  1表示存在
              "redis.call('hincrby', KEYS[1], ARGV[2], 1); " + //新增指定的值+1
              "redis.call('pexpire', KEYS[1], ARGV[1]); " + //从新设置过期时间
              "return nil; " +             //返回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的过期时间
第三步 return:返回该锁的剩余有效时间。

备注: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) { //通过解读上面的代码,当返回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);
}
posted @ 2021-08-13 23:07  爱我-中华  阅读(459)  评论(0编辑  收藏  举报