Redis实现分布式锁(详解)

🚏基本实现原理:


Redis分布式锁原理如上图所示,当有多个Set命令发送到Redis时,Redis会串行处理,最终只有一个Set命令执行成功,从而只有一个线程加锁成功

🛴SetNx命令加锁

Redis的Setnx命令会在数据库中创建一个<Key,Value>记录,这个命令只有当Redis中没有这个Key的时候才执行成功,当已经有这个Key的时候会返回失败

🚲死锁问题

上述代码存在一个问题,假如在执行业务逻辑的时候抛出异常,或者Redis服务直接崩溃了,就会造成无法释放锁,从而造成死锁问题

解决办法

这时我们就要利用Redis提供的expire命令,引入过期时间的概念,这时只要超过了过期时间,Redis就会自动删除这个Key,这样不论是否抛出异常或Redis崩溃都可以释放锁。

但是依然存在一个原子性问题,我们执行Redis的SetNx命令和Expire命令并不是一个原子性操作,在高并发场景下,假如程序在执行Expire命令前就崩溃了,依然会造成死锁问题
这种情况有两种解决办法:
1.使用RedisTemplete提供的原子命令如下:
2.使用lua脚本

🛺错误删除锁问题

上面直接删除Key来解锁的方式会存在一个问题,考虑下面这几种情况:

(1) 线程1执行业务时间过长导致自己加的锁过期

(2) 此时线程2执行Setnx成功加锁

(3) 线程1业务执行完毕,删除锁,此时删除的是线程2的锁

(4) 此时线程3加锁成功,这时候就会有两个线程同时执行一段业务的并发问题

初步解决办法

对加锁命令进行改造,在value字段里加入当前线程的id,为锁添加一个线程标示,标示不一致的线程无法删除锁,这里可以使用uuid来实现。 如上图所示,删除锁时判断线程id是否一致即可解决误删锁的问题,但这里仍然存在原子命令问题,比较并删除这个操作并不是原子命令,可能会出现以下问题:

(1) 线程1获取uuid并判断锁是自己的

(2)准备解锁时,出现GC或者其他原因导致程序卡顿无法立即释放锁,导致线程1的锁过期释放

(3) 线程2就会加锁成功

(4) 此时线程1卡顿结束,就会删除线程2的锁

引入lua脚本实现原子删除操作

lua是一个非常轻量级的脚本语言,Redis底层天生支持lua脚本的执行,一个lua脚本中可以包含多条Redis命令,Redis会将整个lua脚本当作原子操作来执行,从而实现聚合多条Redis指令的原子操作
所以在解锁时,使用lua脚本将判断和删除变为一个原子命令 //lua脚本如下

//lua脚本如下
    luaScript =  " if redis.call('get',key) == value then
                  return redis.call('del',key) 
               else 
                  return 0 
               end;"

最终结合lua脚本,实现了一个完整的分布式的加锁和解锁过程,伪代码如下:

uuid = getUUID();
//加锁
lockResut = redisClient.setNx(key,uuid,timeOut);
if(!lockResult){
    return;
}
try{
   //执行业务逻辑
}finally{
    //解锁
    redisClient.eval(delLuaScript,keys,values)
}
//解锁的lua脚本
delLuaScript =  " if redis.call('get',key) == value then
                     return redis.call('del',key) 
                  else 
                     return 0 
                  end;"

到此,我们已经实现了一个加锁和解锁功能较为完整的redis分布式锁了,但是作为一个锁来说,还有一些其他的功能需要进一步完善,如锁失效问题,可重入锁问题等

🛤️锁失效问题解决

假如我们锁的过期时间设置太短,或者业务执行时间太长导致锁过期,但为了避免死锁又必须设置过期时间,那这就需要引入自动续期的功能,即在加锁成功时,开启一个定时任务,定时刷新锁的过期时间从而避免上述问题

uuid = getUUID();
//加锁
lockResut = redisClient.setNx(key,uuid,timeOut);
if(!lockResult){
    return;
}
//开启一个定时任务
new Scheduler(key,time,uuid,scheduleTime)
try{
   //执行业务逻辑
}finally{
    //删除锁
    redisClient.eval(delLuaScript,keys,values)
    //取消定时任务
    cancelScheduler(uuid);
}

如上述代码所示,加锁成功后开启一个定时任务来对锁进行自动续期,定时任务的执行逻辑是:
(1) 判断Redis中的锁是否是自己的 (2)如果存在的话就使用expire命令重新设置过期时间
这里由于需要两个Redis的命令,所以也需要使用lua脚本来实现原子操作,代码如下所示:

luaScript = "if redis.call('get',key) == value) then
            return redis.call('expire',key,timeOut);
         else
            return 0;
         end;"

🛣️Redis分布式锁优化

可重入锁

对于一个完整的锁来说,可重入功能是必不可少的特性,所谓的可重入即同一个线程第一次加锁成功后,第二次加锁时,无须排队等待,只要判断是否是自己的锁就行了,可以直接再次获取锁来执行业务逻辑,如下图所示:
在这里插入图片描述
实现可重入锁的原理就是在加锁的时候记录加锁次数,在释放锁的时候减少加锁次数,这个加锁的次数记录可以存在Redis中,如下图所示:
在这里插入图片描述

加入可重入功能后,加锁的步骤就变为如下
(1)判断锁是否存在(2)判断是否是自己的(3)增加锁的次数
由于增加次数以及减少次数是多个操作,这里需要再次使用lua脚本来实现,同时由于这里需要在Redis中存入加锁的次数,所以需要使用到Redis中的Map数据结构*Map(key,uuid,lockCount), 加锁lua脚本如下

//锁不存在
if (redis.call('exists', key) == 0) then
    redis.call('hset', key, uuid, 1); 
    redis.call('expire', key, time); 
    return 1;
end;
//锁存在,判断是否是自己的锁
if (redis.call('hexists', key, uuid) == 1) then
    redis.call('hincrby', key, uuid, 1); 
    redis.call('expire', key, uuid);
    return 1; 
end; 
//锁不是自己的,返回加锁失败
return 0;

到此,我们在实现了基本的加锁与解锁的逻辑基础上,又加入了可重入和自动续期的功能,自此一个分布式锁基本完成,伪代码如下:

uuid = getUUID();
//加锁
lockResut = redisClient.eval(addLockLuaScript,keys,values);
if(!lockResult){
    return;
}
//开启一个定时任务
new Scheduler(key,time,uuid,scheduleTime)
try{
   //执行业务逻辑
}finally{
    //删除锁
    redisClient.eval(delLuaScript,keys,values)
    //取消定时任务
    cancelScheduler(uuid);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值