随笔 - 171  文章 - 0  评论 - 0  阅读 - 62466

分布式锁

redis做分布式锁采用set命令实现。

set指令扩展参数:SET key value[EX seconds][PX milliseconds][NX|XX]

- NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁, 而其他客户端请求只能等其释放锁,才能获取。

- EX seconds :设定key的过期时间,时间单位是秒。

- PX milliseconds: 设定key的过期时间,单位为毫秒

- XX: 仅当key存在时设置值

释放别人的锁

try{
    if(redis.set(lockKey, requestId, "NX", "PX", expireTime) == 1){//加锁
       //加锁成功,业务逻辑处理
    }
    //加锁失败
} finally {
     unlock(lockKey); //释放锁
}  

假设A、B两个线程来尝试加锁,A线程先拿到锁(假如锁超时时间是1秒后过期)。如果线程A执行的业务逻辑很耗时,超过了1秒还是没有执行完。这时候,Redis会自动释放锁。刚好这时,线程B过来了,它就能抢到锁了,开始执行它的业务逻辑,恰好这时,线程A执行完逻辑,去释放锁的时候,它就把B的锁给释放掉了。

非原子性操作释放锁,导致释放别人的锁

记录获取锁的唯一凭证requestId,释放锁时进行判断。

requestId:UUID拼接线程ID。

复制代码
try{
    if(redis.set(lockKey, requestId, "NX", "PX", expireTime) == 1){//加锁
       //加锁成功,业务逻辑处理
    }
    //加锁失败
} finally {
    if (requestId.equals(redis.get(lockKey))) {//校验是不是自己获取的requestId
       unlock(lockKey);//释放锁
    }  
}      
复制代码

假设A、B两个线程来尝试加锁,A线程先拿到锁(假如锁超时时间是1秒后过期)。如果A线程执行完requestId.equals(redis.get(lockKey))刚好1秒,Redis自动释放锁,此时B线程刚好抢到了锁。A线程就把B现场的锁释放了。

Lua脚本保证原子性

通过lua脚本校验requestId,释放锁。

if redis.call('get', KEYS[1]) == ARGV[1] then 
       return redis.call('del', KEYS[1]) 
else 
       return 0 
end

如何实现过期时间自动续期

  1. 起一个定时任务去定时检查锁是否过期,未过期,进行续期
  2. 使用redisson

Redisson加锁过程

tryAcquire的确是一个熟悉的方法,试图获取锁,我们可以作为入口跟踪到这段代码:

复制代码
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                "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; " +
                        "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.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
    }
复制代码

通过执行Lua脚本来加锁,核心内容就是这段脚本。所以,理解这段脚本的含义有助于我们理解加锁的过程,即使我们不熟悉Lua脚本的语法,Lua脚本中熟悉的Redis命令就是我们的着手点。

KEYS[1]代表Redis的key,ARGV[1]代表过期时间,ARGV[2]是生成的一个唯一标识。通过Lua脚本提交给Redis,使命令本身具有原子性,所以这里我们把精力集中在每一行的含义:

Redis HINCRBY 命令为哈希表 key 中的 field 的值加上增量 increment 。增量也可以为负数,相当于对给定字段的值进行减法操作。 如果 key 不存在,将自动创建一个新的哈希表并执行 HINCRBY 命令;如果域 field 不存在,那么在执行命令前,

字段的值被初始化为 0。

//使用exits命令判断key是否已经存在
redis.call('exists', KEYS[1]) == 0
//使用hincrby命令,在对应key下设置了map,而map的key为唯一标识,值自增,第一次值为1。
redis.call('hincrby', KEYS[1], ARGV[2], 1);

为什么采用map的接口而不是只存储一个单值?

Redission需要增加计数器,而实现重入锁。

//判断是否为同一个线程获取锁
redis.call('hexists', KEYS[1], ARGV[2]) == 1

未获取到锁返回时间毫秒值

1
return redis.call('pttl', KEYS[1]);

WatchDog实现锁续期

WatchDog是在线程获取到锁后的一个定时任务线程,该线程保证获取锁后业务代码没有执行完进行锁续期。

成功获取锁后会执行scheduleExpirationRenewal——定时过期续期。

Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                //...略
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                //...略
            }
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

定时任务执行线程,每隔一段时间(过期时间/3)执行一次续期。renewExpirationAsync里面的核心又是一段Lua脚本,如下:

if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
   redis.call('pexpire', KEYS[1], ARGV[1]); 
   return 1; 
end; 
return 0;

Redisson锁释放过程

如果该线程的key已经被释放,那就直接返回。否则,将重入计数器-1,如果计数器没有释放到0,设置过期时间。如果该线程已经是最后一个释放锁的,删除对应key,并发布释放锁通知到频道,完成锁释放的过程。

KEYS[1]代表Redis的key,KEYS[2]代表频道名称,ARGV[1]代表消息的类别,0为解锁,ARGV[2]代表过期时间,ARGV[3]是生成的一个唯一标识。

复制代码
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;
复制代码

锁释放也就是触发终止看门狗线程。

cancelExpirationRenewal——取消续期方法:

复制代码
protected 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()); } }
复制代码

 流程图

 

posted on   zhengbiyu  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示