Redis 分布式锁

分布式锁的定义:

保证同一时间只能有一个客户端对共享资源进行操作。
另外有几点要求也是必须要满足的:
1、不会发生死锁。 即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
2、具有容错性。 只要大部分的Redis节点正常运行,客户端就可以加锁和解锁
3、解铃还须系铃人。 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了

分布式锁演进史

第一版 setnx

setnx,是set if not exists 的缩写,也就是只有不存在的时候才设置, 设置成功时返回 1 , 设置失败时返回 0 。可以利用它来实现锁的效果,但是很多人在使用的过程中都有一些问题没有考虑到。

setIfAbsent(key, val) -> setnx(key, val)

PS: 当执行流程异常,线程被销毁, 锁无法释放, 就会产生死锁.

第二版 expire

基于 setnx 命令实现分布式锁的缺陷也是很明显的, 那就是一定情况下可能发生死锁.
对 Redis 的锁标志位加上过期时间就能很好的防止死锁问题
PS: 对分布式锁添加了过期时间, 但依然无法避免极端情况下的死锁问题。那就是如果在客户端加锁成功后, 还没有设置过期时间时宕机, 所以加锁标志位与添加过期时间命令保证一个原子性, 要么一起成功, 要么一起失败.

第三版 set

添加锁原子命令就要登场了, 从 Redis 2.6.12 版本起, 提供了可选的 字符串 set 复合命令。

 SET key value [expiration EX seconds|PX milliseconds] [NX|XX]
 cache.set(lockKey, "lock", "nx", "ex", 5)

PS: 加锁以及设置过期时间确实保证了原子性, 但存在业务执行时长比过期时间要长的情况, 可能存在线程一将线程二的锁释放掉之后, 线程三获取到锁, 然后线程二执行完将线程三的锁释放.

第四版 verify value

创建辨别客户端身份的唯一值了, 将加锁及解锁归一化

String lockKey = "lockKey";
String lockValue= UUID.randomUUID.toString();
cache.set(lockKey, lockValue, "nx", "ex", 5);    //加锁
if(Object.equals(cache.get(lockKey),lockValue)) cache.del(lockKey);

同时为每个客户端设置了 uuid 作为锁标志位的 val, 解锁时需要判断锁的 val 是否和自己客户端的相同, 辨别成功才会释放锁.

PS: 解锁时, 由于判断锁和删除标志位并不是原子性的, 所以可能还是会存在误删

第五版 lua

很不友好的是, del 删除操作并没有提供原子命令, 所以我们需要想点办法
Redis在 2.6 推出了脚本功能, 允许开发者使用 Lua 语言编写脚本传到 Redis 中执行

使用 Lua 脚本有什么好处呢?
1、减少网络开销: 原本我们需要向 Redis 服务请求多次命令, 可以将命令写在 Lua 脚本中, 这样执行只会发起一次网络请求
2、原子操作: Redis 会将 Lua 脚本中的命令当作一个整体执行, 中间不会插入其它命令
3、复用: 客户端发送的脚步会存储 Redis 中, 其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑

# 获取 KEYS[1] 对应的 Val
local cliVal = redis.call('get', KEYS[1])
# 判断 KEYS[1] 与 ARGV[1] 是否保持一致
if(cliVal == ARGV[1]) then
  # 删除 KEYS[1]
  redis.call('del', KEYS[1])
  return 'OK'
else
  return nil
end
String script =  "local cliVal = redis.call('get', KEYS[1]) " +
                    "if(cliVal == ARGV[1]) then " +
                    "redis.call('del', KEYS[1]) " +
                    "return 'OK' " +
                    "else " +
                    "return nil " +
                    "end ";
cache.eval(script, Lists.newArrayList(lockKey),Lists.newArrayList(lockValue));

//KEYS[1]: lockKey
//ARGV[1]: lockValue

锁过期了怎么办?
例如 Redisson引入了一个Watch Dog机制,这个机制是针对分布式锁来实现锁的自动续约, 如果当前获得锁的线程没有执行完,那么Redisson会自动给Redis中目标key延长超时时间

watch dog 的自动延期机制 , 如果释放锁操作本身异常了,watch dog 还会不停的续期 造成死锁吗?
不会,因为无论释放锁操作是否成功,EXPIRATION_RENEWAL_MAP中的目标 ExpirationEntry 对象已经被移除了,watch dog 通过判断后就不会继续给锁续期了。
释放锁的源码:

// 锁释放
public void unlock() {
    try {
        get(unlockAsync(Thread.currentThread().getId()));
    } catch (RedisException e) {
        if (e.getCause() instanceof IllegalMonitorStateException) {
            throw (IllegalMonitorStateException) e.getCause();
        } else {
            throw e;
        }
    }
}

// 进入 unlockAsync(Thread.currentThread().getId()) 方法 入参是当前线程的id
public RFuture<Void> unlockAsync(long threadId) {
    RPromise<Void> result = new RedissonPromise<Void>();
    //执行lua脚本 删除key
    RFuture<Boolean> future = unlockInnerAsync(threadId);

    future.onComplete((opStatus, e) -> {
        // 无论执行lua脚本是否成功 执行cancelExpirationRenewal(threadId) 方法来删除EXPIRATION_RENEWAL_MAP中的缓存
        cancelExpirationRenewal(threadId);

        if (e != null) {
            result.tryFailure(e);
            return;
        }

        if (opStatus == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            result.tryFailure(cause);
            return;
        }

        result.trySuccess(null);
    });

    return result;
}

// 此方法会停止 watch dog 机制
void cancelExpirationRenewal(Long threadId) {
    ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (task == null) {
        return;
    }
    
    if (threadId != null) {
        task.removeThreadId(threadId);
    }

    if (threadId == null || task.hasNoThreads()) {
        Timeout timeout = task.getTimeout();
        if (timeout != null) {
            timeout.cancel();
        }
        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
    }
}

原链接: https://www.cnblogs.com/MrLiuZF/p/15110559.html
原文有Redisson实现Redis分布式锁的底层原理及加锁机制讲解

posted @ 2022-05-18 11:08  栋_RevoL  阅读(84)  评论(0编辑  收藏  举报