Redisson实现分布式锁

640 阅读7分钟

本文内容主要来自黑马程序员Redis课程相关章节总结

一、基于Redission实现分布式锁

基于setnx实现的分布式锁存在的问题:

  • 不可重入,同一线程无法多次获取同一把锁
  • 不可重试 ,获取锁只尝试一次就返回false,没有重试机制
  • 超时释放,锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
  • 主从一致性,如果redis提供了主从集群,主从同步存在延迟,当主宕机时,从机可能没有同步到锁数据

1、导入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.16.3</version>
</dependency>

2、配置Redisson客户端

@Bean
public RedissonClient redissonClient() {
    //配置
    Config config = new Config(); 
    config.useSingleServer().setAddress("redis://192.168.85.150:6379").setPassword("123456");
    //创建redisson客户端
    return Redisson.create(config);
}

3、使用Redisson分布式锁

@Resource
private RedissionClient redissonClient;

@Test
void testRedisson() throws Exception{
    //获取锁(可重入),指定锁的名称
    RLock lock = redissonClient.getLock("anyLock");
    //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
    boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
    //判断获取锁成功
    if(isLock){
        try{
            System.out.println("执行业务");          
        }finally{
            //释放锁
            lock.unlock();
        } 
    }
}

二、redission可重入锁原理

redission采用hash结构用来存储锁,其中key表示这把锁是否存在,用field标识当前这把锁被哪个线程所持有。当线程初次获取锁时,创建一个hash结构,并将值设为1,以后每次重入,将该值+1,每次释放,则将值-1,直到值为0时删除。

1653548087334.png

获取锁的Lua脚本

参数说明:

KEYS[1] :锁名称

ARGV[1]: 锁失效时间

ARGV[2]: 锁的小key

-- 判断锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
    -- 不存在, 则获取锁
    redis.call('hincrby', 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
    -- 已经持有,则重入次数+1
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    -- 重置有效期
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;-- 返回结果nil,表示再次获取锁
end;
-- 未获取锁,返回锁的剩余有效时间
return redis.call('pttl', KEYS[1]);

释放锁的Lua脚本

参数说明:

KEYS[1]: 锁名称

KEYS[2]: 锁的通道名

ARGV[1]: 锁释放的发布消息

ARGV[2]: 锁失效时间

ARGV[3]: 锁的小key

-- 判断当前锁是否还被自己持有
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
    return nil; -- 如果已经不是自己,表示已释放,直接返回
end;
-- 是自己的锁,则重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
-- 判断是否重入次数是否已经为0
if (counter > 0) then
    -- 大于0说明不能释放锁,重置有效期然后返回
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 0;
else -- 等于0说明可以释放锁,直接删除
    redis.call('del', KEYS[1]);
    -- 发布锁释放消息
    redis.call('publish', KEYS[2], ARGV[1]);
    return 1;
end;
return nil;

三、redisson锁重试和WatchDog机制

tryLock方法获取锁有以下几种方式:

boolean tryLock();

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

其中,第一种无参数的方式为一次性获取锁,失败则不再重试,而另外两个则需要设置锁等待时间,在等待时间内,如果没有获取锁,会不断进行尝试。

由以下代码可知,方法二调用了方法三,将锁释放时间设置为-1。

public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
    return tryLock(waitTime, -1, unit);
}

有等待时间的获取锁代码讲解:

@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    //将等待时间转化为毫秒
    long time = unit.toMillis(waitTime);
    //获取系统当前毫秒时间戳
    long current = System.currentTimeMillis();
    //获取当前线程ID
    long threadId = Thread.currentThread().getId();
    //调用获取锁lua脚本的返回值
    Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    // 返回值为空,表示获取锁,直接返回true
    if (ttl == null) {
        return true;
    }

    // 返回值不为空,表示当前线程未获取锁,计算锁等待剩余时间
    // 锁等待剩余时间 = 锁等待时间 - 本次获取锁过程的消耗时间
    time -= System.currentTimeMillis() - current;
    //如果剩余时间已经用完,则当前线程未能获取锁
    if (time <= 0) {
        acquireFailed(waitTime, unit, threadId);
        return false;
    }

    current = System.currentTimeMillis();
    //如果剩余时间未用完,则订阅锁释放消息
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    // 如果在剩余时间内,没有等到其他线程释放锁,则取消订阅,返回失败
    if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
        if (!subscribeFuture.cancel(false)) {
            subscribeFuture.onComplete((res, e) -> {
                if (e == null) {
                    unsubscribe(subscribeFuture, threadId);
                }
            });
        }
        acquireFailed(waitTime, unit, threadId);
        return false;
    }

    try {
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }

        // 如果其他线程释放了锁,则再次不断尝试获取
        while (true) {
            long currentTime = System.currentTimeMillis();
            ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                return true;
            }

            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }

            // waiting for message
            currentTime = System.currentTimeMillis();
            if (ttl >= 0 && ttl < time) {
                subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
            }

            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        }
    } finally {
        unsubscribe(subscribeFuture, threadId);
    }
//        return get(tryLockAsync(waitTime, leaseTime, unit));
}

以上代码解释了redisson的锁重试原理。

下面分析当调用方法tryLock(long time, TimeUnit unit),琐释放时间设置为-1时,Redisson是如何处理的。

从上述获取锁的部分"Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId)"开始往下跟踪源码

private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Long> ttlRemainingFuture;
    if (leaseTime != -1) {
        //当锁释放时间不是-1时,执行获取锁lua脚本
        ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        //当锁释放时间为-1时,则设置为30秒释放
        ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    }
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }

        // lock acquired
        if (ttlRemaining == null) {
            if (leaseTime != -1) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                //定时重置锁失效时间
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

internalLockLeaseTime的值为代码设置的看门狗时间,默认为30秒

private long lockWatchdogTimeout = 30 * 1000;

继续跟踪scheduleExpirationRenewal方法,进入到renewExpiration方法;

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    
    //10秒钟后执行任务
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            
            //使用lua脚本重置锁失效时间
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getRawName() + " expiration", e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }
                
                //重置成功,递归调用renewExpiration方法
                if (res) {
                    // reschedule itself
                    renewExpiration();
                } else {
                    // 取消看门狗
                    cancelExpirationRenewal(null);
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

因为锁的失效时间是30s,当10s之后,此时这个timeTask 就触发了,他就去进行续约,把当前这把锁续约成30s,如果操作成功,那么此时就会递归调用自己,再重新设置一个timeTask(),于是再过10s后又再设置一个timerTask,完成不停的续约。

为什么使用看门狗不断续约,重置超时时间?

当服务意外宕机,没有执行unlock释放锁时。这时没有人再去调用renewExpiration方法,所以等到时间之后自然就释放了。

image.png

四、redission锁的MutiLock原理

解决主从一致问题

为了提高redis的可用性,一般会搭建集群或者主从

当主机宕机,哨兵会发现主机宕机,并且选举一个slave变成master,而此时新的master中并没有同步到锁信息,则锁信息就丢掉了。

为了解决这个问题,redission提出了MutiLock锁(联锁)。MutiLock加锁的逻辑需要写入到每一个主节点上,只有所有的服务器都写入成功,才是加锁成功,假设现在某个节点挂了,那么他去获得锁的时候,只要有一个节点拿不到,都不能算是加锁成功,就保证了加锁的可靠性。

使用案例:

@Autowired
private RedissonClient redissonClient1;
private RedissonClient redissonClient2;
private RedissonClient redissonClient3;

private RLock lock;

@BeforeEach
public void setUp() {
    RLock lock1 = redissonClient1.getLock("order");
    RLock lock2 = redissonClient2.getLock("order");
    RLock lock3 = redissonClient3.getLock("order");
    //创建联锁
    lock = new RedissonMultiLock(lock1, lock2, lock3);
}

@Test
public void testRedissonMultiLock() throws InterruptedException {
    //尝试获取锁
    boolean isLock = lock.tryLock(10L, TimeUnit.SECONDS);
    if(!isLock){
        log.error("获取锁失败");
        return;
    }
    try{
        log.info("收取锁成功,开始执行业务");
        //...
    }finally {
        log.warn("准备释放锁");
        lock.unlock();
    }
}

源码分析:

查看RedissonMultiLock的构造方法,在该方法中,redission会将多个RLock锁对象添加到一个locks集合中;

final List<RLock> locks = new ArrayList<>();

/**
 * Creates instance with multiple {@link RLock} objects.
 * Each RLock object could be created by own Redisson instance.
 *
 * @param locks - array of locks
 */
public RedissonMultiLock(RLock... locks) {
    if (locks.length == 0) {
        throw new IllegalArgumentException("Lock objects are not defined");
    }
    this.locks.addAll(Arrays.asList(locks));
}

再进入tryLock方法,

@Override
public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
    return tryLock(waitTime, -1, unit);
}

@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
//        try {
//            return tryLockAsync(waitTime, leaseTime, unit).get();
//        } catch (ExecutionException e) {
//            throw new IllegalStateException(e);
//        }
    long newLeaseTime = -1;
    if (leaseTime != -1) {
        if (waitTime == -1) {
            newLeaseTime = unit.toMillis(leaseTime);
        } else {
            newLeaseTime = unit.toMillis(waitTime)*2;
        }
    }

    long time = System.currentTimeMillis();
    long remainTime = -1;
    if (waitTime != -1) {
        remainTime = unit.toMillis(waitTime);
    }
    long lockWaitTime = calcLockWaitTime(remainTime);

    //最多允许获取锁失败的限制个数,默认为0
    int failedLocksLimit = failedLocksLimit();
    //成功获取锁的集合
    List<RLock> acquiredLocks = new ArrayList<>(locks.size());
    //遍历locks集合,对每个lock实例都尝试获取锁
    for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
        RLock lock = iterator.next();
        boolean lockAcquired;
        try {
            if (waitTime == -1 && leaseTime == -1) {
                lockAcquired = lock.tryLock();
            } else {
                long awaitTime = Math.min(lockWaitTime, remainTime);
                lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
            }
        } catch (RedisResponseTimeoutException e) {
            unlockInner(Arrays.asList(lock));
            lockAcquired = false;
        } catch (Exception e) {
            lockAcquired = false;
        }

        if (lockAcquired) {
            acquiredLocks.add(lock);
        } else {
            //如果成功获取锁的个数满足要求,则跳出循环,已经失败的锁不再重试
            if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
                break;
            }

            if (failedLocksLimit == 0) {
                //如果有一个锁获取失败,则释放所有已经成功获取的锁,确保锁的一致性
                unlockInner(acquiredLocks);
                if (waitTime == -1) {
                    return false;
                }
                failedLocksLimit = failedLocksLimit();
                acquiredLocks.clear();
                // 重置迭代器游标,再次遍历获取锁
                while (iterator.hasPrevious()) {
                    iterator.previous();
                }
            } else {
                failedLocksLimit--;
            }
        }

        if (remainTime != -1) {
            remainTime -= System.currentTimeMillis() - time;
            time = System.currentTimeMillis();
            //如果剩余时间小于0,则释放所有锁,返回获取锁失败
            if (remainTime <= 0) {
                unlockInner(acquiredLocks);
                return false;
            }
        }
    }

    if (leaseTime != -1) {
        acquiredLocks.stream()
                .map(l -> (RedissonLock) l)
                .map(l -> l.expireAsync(unit.toMillis(leaseTime), TimeUnit.MILLISECONDS))
                .forEach(f -> f.syncUninterruptibly());
    }

    return true;
}

五、redission锁的RedLock原理

RedissonReadLock红锁是基于RedissonMultiLock实现的。它们的区别是,红锁在获取锁的节点超过半数时,即认为成功获取锁。

RedissonRedLock源码,重写了failedLocksLimit方法,不再返回0,而是locks.size()/2 - 1

public class RedissonRedLock extends RedissonMultiLock {

    /**
     * Creates instance with multiple {@link RLock} objects.
     * Each RLock object could be created by own Redisson instance.
     *
     * @param locks - array of locks
     */
    public RedissonRedLock(RLock... locks) {
        super(locks);
    }

    @Override
    protected int failedLocksLimit() {
        return locks.size() - minLocksAmount(locks);
    }
    
    protected int minLocksAmount(final List<RLock> locks) {
        return locks.size()/2 + 1;
    }

    @Override
    protected long calcLockWaitTime(long remainTime) {
        return Math.max(remainTime / locks.size(), 1);
    }
    
    @Override
    public void unlock() {
        unlockInner(locks);
    }

}