深入浅出 超详细 从 线程锁 到 redis 实现分布式锁(篇节 2)「建议收藏」

2022-08-31 16:38:51 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

redis 实现 分布式锁

上节 我们讲了 线程锁 在单体项目中的作用,和 放在 分布式 项目里产生的问题,那接下来我们就来解决 分布式 架构上怎么 保证 数据的一直性

使用 redisTemplate 实现

代码语言:javascript复制
// 设置锁
setIfAbsent("lock", "1213")---> SETNX lock "1213"
// 释放锁
redisTemplate.delete("lock");
代码语言:javascript复制
@GetMapping("/cut")
    public Object kc() { 
   
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        /** * 设置锁,如果 lock 不存在的话,设置 lock=1213 并返回 true * 如果存在的话:就不操作 直接返回 false */
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1213");
        if(!lock){ 
   
            // 锁存在
            return "服务繁忙,请稍后再试";
        }

        int num = Integer.parseInt(redisTemplate.opsForValue().get("num").toString());
        if (num > 0) { 
   
            int lastNum = num - 1;
            redisTemplate.opsForValue().set("num", lastNum   "");
            System.out.println("扣减库存成功,剩余库存为:"   lastNum);
        } else { 
   
            System.out.println("扣减库存失败,库存不足");
        }

        // 释放锁
        redisTemplate.delete("lock");

        return "ok";
    }

简述一下逻辑: 第一个请求 进来,设置一把锁 进行锁住,然后 往下执行减库存的操作,此时 第二个线程进来获取锁,但是第一请求没有释放锁,所以第二个请求获取锁就会失败 得到返回值为 false,进入if 方法体 直接返回了。其他线程也是如此,直到 第一个线程释放锁后 其他线程才有获取锁的机会,每次只有一个线程能够成功获取锁,其他线程获取不到直接返回。

大家 觉得 这把锁怎么样,是不是解决问题了呢?还有什么问题吗?有没有那个小可爱想到了呢?

肯定有小伙伴想到了。

情况1:一个请求进来,获取锁,执行扣减库存的操作,在它 释放锁之前 服务突然抛异常了呢?
  • 那就 使用 try{}finally{}嘛,这下不怕抛异常 无法释放锁了吧
代码语言:javascript复制
try{ 
   
       int num = Integer.parseInt(redisTemplate.opsForValue().get("num").toString());
       if (num > 0) { 
   
           int lastNum = num - 1;
           redisTemplate.opsForValue().set("num", lastNum   "");
           System.out.println("扣减库存成功,剩余库存为:"   lastNum);
       } else { 
   
           System.out.println("扣减库存失败,库存不足");
       }

   }finally { 
   
       // 释放锁
       redisTemplate.delete("lock");
   }
情况2:要是不抛异常,在释放锁之前服务重启了呢?那就来不及执行finally了吧,这下 锁也没有释放吧。
那该怎么解决呢?
有小伙伴 就会想:给锁加一个 定时器嘛,它要挂就挂,到了时间 锁自动释放,其他人又可以获取到 锁 服务又可用了
感觉是可行哦,那我们来实际操作一下。源码如下:
OK 定时有了,要是 线程挂在里面,时间一到 锁就会自动释放
事实真的会 是这个样子的吗?
情况3:锁设置成功,接下来对 锁进行定时,此时准备定时呢,还没定时成功突然程序挂了,又会导致死锁,像前那情况一样。
那这时 我们就要保存设置锁 和 定时 两个指令的原子性,要么全部成功 要么全部失败,那该怎么实现呢?
这个问题 大牛已经都想过了,往下看
代码语言:javascript复制
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "1213",10, TimeUnit.SECONDS);
一条更比两条强。这个底层使用 Lua 实现,保证其原子性。
再看一下 我们优化后的代码
代码语言:javascript复制
@GetMapping("/cut")
    public Object kc2() { 
   
        redisTemplate.setKeySerializer(new StringRedisSerializer());

        /** * 设置锁,如果 lock 不存在的话,设置 lock=1213 并返回 true * 如果存在的话:就不操作 直接返回 false */
        String lockKey = "lock";
        String redisClientId = UUID.randomUUID().toString();
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, redisClientId,10, TimeUnit.SECONDS);

        if(!lock){ 
   
            // 锁存在
            return "服务繁忙,请稍后再试";
        }
        try{ 
   
            int num = Integer.parseInt(redisTemplate.opsForValue().get("num").toString());
            if (num > 0) { 
   
                int lastNum = num - 1;
                redisTemplate.opsForValue().set("num", lastNum   "");
                System.out.println("扣减库存成功,剩余库存为:"   lastNum);
            } else { 
   
                System.out.println("扣减库存失败,库存不足");
            }
        }finally { 
   
            // 释放锁
            if (redisClientId.equals(redisTemplate.opsForValue().get(lockKey))){ 
   
                redisTemplate.delete(lockKey);
            }
        }
        return "ok";
    }
小伙伴 会说,这下没问题啦 哈哈。。。。
你确定 没问题?
那就我来给你分析分析

正常逻辑:一个请求进来 说去锁,设置有效时间为 10 秒,然后 执行下面 业务逻辑,最后释放锁,然后第二个线程进来。。。。

lock 有效时间为 10 秒,保不定 那个线程执行任务的时候 执行完要 15 秒,此时,lock 10秒就失效,那下一个线程就会进来,假如第二个线程要执行8 秒,第一个线程5秒后就执行完了 然后释放lock 锁,线程1的锁早就失效了,它释放的锁确实线程2的锁,而第二个线程还有3秒才执行完,此时线程3获取到锁,又进来了,3秒后线程2又释放线程3 的锁,这样下去线程3释放线程4.。。。 就会导致 锁永久失效。

所以 这个超时时间该设置多少呢? 就需要根据项目来考量了。

那有什么 好的解决方案吗,解决这个锁失效的问题呢?

有的,那肯定是有的,听我徐徐道来。。。

假设 设置锁有效时间为 30 秒,那当线程获取锁后,开一个子线程 做一个定时器,每隔一段时间去检查该对象的锁是否存在,存在的话 就重新给 锁续命。 那如何保证 自家的锁不会被别人释放呢? 这下那个 锁的 value 就派上用场了,给每个线程的锁配置上唯一标识(这个唯一标识就使用UUID) 每次释放锁的时候就判断是否是自己的锁,保证只释放自己的锁。

理论有了,那该怎么实现呢?
使用Redisson 实现分布式锁
1、引入 Redisson 依赖
代码语言:javascript复制
<dependency>
   <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.0</version>
</dependency>
2、配置Redisson
代码语言:javascript复制
@Bean
public Redisson redisson(){ 
   
    // 此为单机模式
    Config config = new Config();
    config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0).setPassword("root");
    return (Redisson) Redisson.create(config);
}
3、代码实现
代码语言:javascript复制
@Autowired
private Redisson redisson;

@GetMapping("/cut")
public Object kc() { 
   
    String lockKey = "lock";
    RLock rLock = redisson.getLock(lockKey);
    try { 
   
        // 加锁
        rLock.lock();
        int num = Integer.parseInt(redisTemplate.opsForValue().get("num").toString());
        if (num > 0) { 
   
            int lastNum = num - 1;
            redisTemplate.opsForValue().set("num", lastNum   "");
            System.out.println("扣减库存成功,剩余库存为:"   lastNum);
        } else { 
   
            System.out.println("扣减库存失败,库存不足");
        }
    } finally { 
   
        // 释放锁
        rLock.unlock();
    }
    return "ok";
}
但是 主从架构 也是可能出现问题,如 redis 挂了
后期优化 可以 用 redis 集群,增加高可用。
OK redis 锁分布式就讲完了,到这里 锁就差不多了
要是还想更加完善 可以选择使用ZK来实现,但是性能是没有 redis 好。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/152027.html原文链接:https://javaforall.cn

0 人点赞