分布式锁实现原理三两谈

本文探讨了在分布式系统中实现秒杀功能时遇到的线程安全问题,从原始的JVM锁到Redis的分布式锁,再到Redisson的高级锁机制。通过分析代码示例,阐述了如何利用Redis的setnx命令和expire命令防止超卖,并讨论了在异常处理和服务器挂机情况下的锁管理策略,最后推荐使用redisson框架以确保更健壮的锁机制。
摘要由CSDN通过智能技术生成

抛砖引玉

假设有个秒杀活动,秒杀手机50台。现在把手机台数放在redis缓存里,然后秒杀成功一次,库存减一,库存没了就告诉用户秒杀失败
看下面这段伪代码

        String productName = "iphone11";
        Integer num = Integer.parseInt( stringRedisTemplate.opsForValue().get(productName));
        if(num > 0){
           int realNum =  num - 1;
           stringRedisTemplate.opsForValue().set(productName,String.valueOf(realNum));
            System.out.println("秒杀成功,库存为" + realNum);
        }else {
            System.out.println("库存不足,秒杀失败");
        }

这段代码有个很严重的问题,就是线程不安全。

线程不安全

当多个用户同时访问,同时获取到当前库存,同时然后将库存减1,再写回缓存里。这样必定会发生超卖现象。
线程不安全,有的同学会说了,这个我熟啊,只需加个锁呗。

        synchronized (this){
            String productName = "iphone11";
            Integer num = Integer.parseInt( stringRedisTemplate.opsForValue().get(productName));
            if(num > 0){
                int realNum =  num - 1;
                stringRedisTemplate.opsForValue().set(productName,String.valueOf(realNum));
                System.out.println("秒杀成功,库存为" + realNum);
            }else {
                System.out.println("库存不足,秒杀失败");
            }
        }

如果这个系统只是单体架构,就是一台服务器在跑,只有一个JVM,那其实是可以的啦。但是如果这个一个分布式系统呢,synchronized是JVM层面的锁,同个JVM下对多个线程有效果。但是如果是不同JVM下的两个线程在竞争呢,那就没有屌用了…分布式的系统呢,当然用的是分布式锁嘛

分布式锁

我听过redis有个操作,就setnx。什么意思呢。只有缓存里没有相同的Key,才会set进去,然后返回true。否则返回false。咦~~,这个可以有耶。我们可以让线程进来前,都setnx相同的key值,只有返回true的那个线程才能继续往下走。等它做完它想做的事,再删掉这个key值。

但是会不会有多个线程就在那么一瞬间同时进行setNx,同时设置成功呢?这个不会的。原因是redis是单线程的,不过再怎么同时,你进入redis这边,一定一定是有个先后顺序的。所以这个放心嘛~~。

先来看看代码怎么改

 		String lockKey = "lockKey";
        String lockValue = "lockValue";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue);
        if(!result){
            return "It is busy now";
        }


        String productName = "iphone11";
        Integer num = Integer.parseInt( stringRedisTemplate.opsForValue().get(productName));
        if(num > 0){
           int realNum =  num - 1;
           stringRedisTemplate.opsForValue().set(productName,String.valueOf(realNum));
            System.out.println("秒杀成功,库存为" + realNum);
        }else {
            System.out.println("库存不足,秒杀失败");
        }

        stringRedisTemplate.delete(lockKey);

但是这个会有很多问题,假如我加上锁,但是在后面的业务逻辑上抛异常了怎么办?

业务逻辑上抛了异常怎么办

因为抛了异常,程序结束,执行不了删除锁那行代码,造成死锁,后面的线程都进不来了。

有同学会说这个我也熟啊,只要抛了异常还能够执行删除锁那句话,可以用try-finally。代码如下

	   String lockKey = "lockKey";
       String lockValue = "lockValue";
       try {
           Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue);
           if(!result){
               return "It is busy now";
           }
           String productName = "iphone11";
           Integer num = Integer.parseInt( stringRedisTemplate.opsForValue().get(productName));
           if(num > 0){
               int realNum =  num - 1;
               stringRedisTemplate.opsForValue().set(productName,String.valueOf(realNum));
               System.out.println("秒杀成功,库存为" + realNum);
           }else {
               System.out.println("库存不足,秒杀失败");
           }
       }finally {
           stringRedisTemplate.delete(lockKey);
       }

嗯~~。但是如果不是抛异常,而是程序跑到中途服务器直接挂机呢。这个用finally也无济于事。

服务器中途挂机了

反正目标是不要造成死锁,我在看看redis还有什么好用的命令。嗯~,我发现有个命令,叫expire,设置过期时间。对,就是这个,可以这样,将这把锁设置为一个过期时间,假如是10秒。那么就算服务挂了,过期时间一到,锁也会失效了。代码如下:

       String lockKey = "lockKey";
       String lockValue = "lockValue";
       try {
           Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue);
           stringRedisTemplate.expire(lockKey,10, TimeUnit.SECONDS);
           if(!result){
               return "It is busy now";
           }
           String productName = "iphone11";
           Integer num = Integer.parseInt( stringRedisTemplate.opsForValue().get(productName));
           if(num > 0){
               int realNum =  num - 1;
               stringRedisTemplate.opsForValue().set(productName,String.valueOf(realNum));
               System.out.println("秒杀成功,库存为" + realNum);
           }else {
               System.out.println("库存不足,秒杀失败");
           }
       }finally {
           stringRedisTemplate.delete(lockKey);
       }

嗯~。但是如果好巧不巧,加上锁后,还没到设过期时间之前,服务就挂了呢。
没事,咱还有另外一个API,在set的时候,直接就设置好过期时间了,代码如下:

      String lockKey = "lockKey";
      String lockValue = "lockValue";
      try {
          Boolean result = 
          	stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue,10,TimeUnit.SECONDS);
          if(!result){
              return "It is busy now";
          }
          String productName = "iphone11";
          Integer num = Integer.parseInt( stringRedisTemplate.opsForValue().get(productName));
          if(num > 0){
              int realNum =  num - 1;
              stringRedisTemplate.opsForValue().set(productName,String.valueOf(realNum));
              System.out.println("秒杀成功,库存为" + realNum);
          }else {
              System.out.println("库存不足,秒杀失败");
          }
      }finally {
          stringRedisTemplate.delete(lockKey);
      }

过期时间太短了怎么办

万一过期时间太短了呢。我设置过期时间为10秒,但是我这个线程在10秒还没走完,锁就过期失效了。那肯定不行的。那设个20秒?30秒?咋不设个一年呢。那还不是一样,问题没解决。咋办呢。可以当锁被加上去以后,再开一个子线程去做一个定时器任务。每隔一段时间,一般是过期时间的1/3,去判断下锁是不是过期了,没过期的话,再续回来,直到主线程结束任务。
那这代码咋写呢,要再开一个子线程,还要考虑其他的细节。
这里已经有个很好的解决方案了,可以用框架redisson,原理是都一样的。这个问题,包括上面的那些问题,redisson已经都考虑到了(当然了)

代码如下

        String lockKey = "lockKey";
        RLock lock = redisson.getLock(lockKey);
        try {
           lock.lock();
            String productName = "iphone11";
            Integer num = Integer.parseInt( stringRedisTemplate.opsForValue().get(productName));
            if(num > 0){
                int realNum =  num - 1;
                stringRedisTemplate.opsForValue().set(productName,String.valueOf(realNum));
                System.out.println("秒杀成功,库存为" + realNum);
            }else {
                System.out.println("库存不足,秒杀失败");
            }
        }finally {
           lock.unlock();
        }

redisson的底层原理

在这里插入图片描述

原理

用到是lua脚本,那么lua脚本能够保证脚本里的业务逻辑的原子性
代码如下

if (redis.call('exists', KEYS[1]) == 0) then 
	redis.call('hset', 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]);

加锁机制

先判断加锁的key存不存在,不存在就加锁,用的是hset命令
后面进来的线程进来会获取到这个锁的剩余生存时间。然后它会进入循环,不断的尝试加锁

可重入锁

可重入用到的是hincrby命令,也是在上面那段lua脚本,锁的次数加上1。

释放锁

逻辑很简单,就是加锁次减1,到0后用del命令删除锁的key值

redisson问题

当然,这个还是有个问题,就是当把key写入redis Master时,此时会异步复制到redis Slave。但是这时要是master挂了的话,还没来得及同步到slave。然后主备切换,slave变成master。但是这时候这台新的master并没有这个key值的数据。然后另外一个线程也过来,咦~,发现没有锁,自己也加上锁。这样就导致两个线程同时都认为自己有锁。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值