Redis分布式锁

普通锁与分布式锁

1.普通锁

普通锁是用于单体未集群的架构,用于保证我们程序的正确性。在多线程的环境下如果没有锁会出现意料之外、且不确定的错误,会给我们造成不可估量的损失(商品超卖等)。在单体架构时,我们只需要一个普通的锁即可解决问题,但如果单体架构无法支撑业务量时我们就需要将架构转为集群架构,利用负载均衡等算法来解决单台机器无法支撑的问题。现在问题来了,多个用户如果在这样的环境下使用普通锁会发生什么情况?

可以看到在三个user并行执行该函数后结果是99,结果明显不正确,当架构改变时锁也应该改变,一个时间只能有一个user访问此函数。所以我们需要分布式锁来解决这种问题。 

2.分布式锁

如果使用分布式锁,则是如下场景,使其3个user无法并行执行业务,保证了程序的正确性。

 所以分布式锁与普通锁的区别是分布式锁全局唯一(这个全局是指整个系统),而普通锁只是进程唯一。

Redis实现分布式锁

分布式锁实现的方法有许多,例如Redis、Zookeeper、数据库等。今天我们聊聊Redis实现分布式锁。

1.setnx实现分布式锁:

setnx 语义为 set if not exsits,使用该指令去创建一个kv键值对,如果创建成功返回true,创建失败则返回false(redis是单线程操作,不存在线程安全问题),我们使用该bool值判断是否拿到分布式锁;在完成逻辑代码后使用del来释放锁。

  boolean lock = redisTemplate.opsForValue().setIfAbsent("ulock","");
       try{
          if(lock){
              //do something
          }
       }catch (Exception e){
          e.printStackTrace();
       }finally {
           redisTemplate.delete("ulock");
       }

我们来思考一个问题,如果上锁之后服务故障了,锁得不到释放怎么办?应对这种问题我们应该再给此锁加上自动过期时间(注意:setnx与设置过期时间两者必须是一个原子操作,如果不是原子操作将会出现上锁成功了过期时间设置不了)。

boolean lock = redisTemplate.opsForValue().setIfAbsent("ulock","",5, TimeUnit.SECONDS);
       try{
          if(lock){
              //do something
          }
       }catch (Exception e){
          e.printStackTrace();
       }finally {
           redisTemplate.delete("ulock");
       }

上图中使用原子操作解决了超时锁不释放问题,那么是否还有别的问题?答案是肯定的,有一种情况是,A服务器成功获取到锁,并设置了10秒过期时间,此时A线程由于网络通讯原因导致此操作超过10秒钟,但是此锁已过期,B线程成功拿到锁,B线程拿到锁之后,A线程所在服务器网络通讯恢复正常,完成具体操作之后手动释放锁(此时的锁是B线程的锁,A线程恢复过来后将B线程的锁释放了),于是又有别的线程可以拿到锁(B线程的锁操作还没完成),具体步骤请看图。

 可以看到此时完全乱套了,这是一个恶性循环,因为删除别人的锁导致线程无法保证安全了。

如何解决这种问题?

使用服务器ID+ThreadName来判断此锁是否是自己加的(不限于此种方案,只要确保value唯一都可以使用),如果是、那就释放锁,如果不是、那就不再进行释放锁操作

  boolean lock = redisTemplate.opsForValue().setIfAbsent("ulock",serverId+Thread.currentThread().getName(),5, TimeUnit.SECONDS);
       try{
          if(lock){
              //do something
          }
       }catch (Exception e){
          e.printStackTrace();
       }finally {
           if(redisTemplate.opsForValue().get("ulock").equals(serverId+Thread.currentThread().getName())) {
               redisTemplate.delete("ulock");
           }
       }

此时大家认为是否还有问题?没错,此时还有问题,下图来描述此问题所在。

 造成此问题的原因是下面代码段不属于原子操作,我们要使判断与delete两个操作具有原子性。

if(redisTemplate.opsForValue().get("ulock").equals(serverId+Thread.currentThread().getName())) {
               redisTemplate.delete("ulock");
           }

如何解决这种问题?我们可以使用lua脚本来解决此问题,使其两个操作具有"原语"的特性。

  String value = serverId+Thread.currentThread().getName();
        String key = "ulock";
        boolean lock = redisTemplate.opsForValue().setIfAbsent(key,value,5, TimeUnit.SECONDS);
       try{
          if(lock){
              //do something
          }
       }catch (Exception e){
          e.printStackTrace();
       }finally {
           String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
           DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<Long>();
           defaultRedisScript.setScriptText(luaScript);
           defaultRedisScript.setResultType(Long.class);
           redisTemplate.execute(defaultRedisScript,Arrays.asList(key),value);
       }


2.Redisson实现分布式锁

上面的基于setnx实现分布式锁你是否觉得太麻烦?是的,Redis也觉得很麻烦,所以redis写了Redisson,帮助我们更好的使用Redis分布式锁,下面我们先来感受一下Redisson的便利性

RLock lock = Reddison.getLock('ulock');

try{
 lock.lock();
 //do something

}catch(Exception e){

}finally{
 lock.unlock();
}

Redisson也是基于lua脚本实现分布式锁,原理我们不再细说,大致第一部分已经讲完啦~

可重入

Redisson的lock是基于ReentrantLock实现的

这意味着Redisson的lock是可重入锁(当然我们自己也可以做判断实现重入锁) 。

Watch Dog

同样为了防止上述超时锁不释放问题,Redisson提供了Watch Dog(看门狗)机制,如果此时拿到锁的线程所在节点故障迟迟无法完成业务,为了防止锁被其他线程删除,Watch Dog会帮助此线程"自动续期"。默认续期30s,可以通过修改Config.lockWatchdogTimeout来指定续期时间。Redisson 还可以指定leaseTime参数的加锁方法来指定加锁的时间。

 lock.lock(50,TimeUnit.SECONDS);

超过这个时间后锁会自动解开,不再延长锁的有效期。

使用Reddison能使我们更方便的使用基于Redis实现的分布式锁。


此Redis分布式锁是基于笔者的理解,如有错误欢迎各位指正,非常感谢!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值