Redis分布式锁

通过场景来引入:

经典问题-秒杀抢购超卖

为啥经典,因为这个可以引出很多东西

基础版

相信各位大佬,对于这段代码应该一眼就能看出其问题之处了吧

判断 和 写入数据库 不是原子操作

        // 秒杀 -- 超卖
        String s = stringRedisTemplate.opsForValue().get(SHOP_MIAOSHA_PRE + "kucun");
        int kuncun = Integer.parseInt(s);
        if (kuncun > 0) {
            kuncun = kuncun - 1;
            stringRedisTemplate.opsForValue().set(SHOP_MIAOSHA_PRE + "kucun", kuncun + "");
            System.out.println("恭喜,抢到了");
        } else {
            System.out.println("你的手速慢了,已经没了");
        }

先升级一下

       synchronized (this){
            // 秒杀 -- 超卖
            String s = stringRedisTemplate.opsForValue().get(SHOP_MIAOSHA_PRE + "kucun");
            int kucun = Integer.parseInt(s);
            if (kucun > 0) {
                kucun = kucun - 1;
                stringRedisTemplate.opsForValue().set(SHOP_MIAOSHA_PRE + "kucun", kucun + "");
                System.out.println("恭喜,抢到了>>还有"+kucun+"个");
            } else {
                System.out.println("你的手速慢了,已经没了");
            }
        }

JMeter压测一下 (1s 200个 走你)

事实也正如大佬门所见 ,非常完美,没任何超卖现象

上点难度

服务来两个 , 分别 8888 端口 和 8889端口

nginx 将这两个 反向代理到 7777 并且进行负载均衡(轮询,一人一次)

服务启动^-^

配置压测

库存归位

走你

完蛋 ,卖着卖着 多卖了5个,要是卖的是2w的外星人笔记本,直接亏了10w,欧玛噶

怎么办

分析原因

虽然 单独拿出 8888 / 8889 ,都是没有问题的,问题就在于,8888的锁只能锁住自己,保证自己是没有问题的,没有问题的根本是

        单机:同一时间 向redis里面扣减库存的操作,只有一个线程,因为加了锁

        多机:向redis写的操作就并发了

       小demo ,假设 现在库存就剩 1个了 画个图(清晰可见啊)

解决困难

要是 有 一个东西可以把这两个都锁住 就好了,那肯定要一个都可以访问的资源 ,mysql(好像没有必要),redis?,redis太适合了,当然还有一个zk(目前小菜鸡还没有学)

redis 确实是两个都可以访问的资源,那么怎么才能实现锁,

redis(我们都知道,其实就是key-value)

set key value 这个命令我们应该都知道

setnx key value(这个就是 set if not exist) 如果不存在 才会设置这个key,而且这个命令是原子操作,不可被打断,而且 redis 执行这个命令时 也是 单线程的

        那么这个 指令 是不是就可以 当成 获取锁

        业务执行完 delete 这个是不是 可以当成 释放锁

既然有了想法,就要实现它,开时改代码

try finally 的作用: 即使中间的业务代码(抛了异常),也要保证 最中释放掉了这把锁,要不然就死锁了

既然想到了抛异常,出现问题,如果运气为负数,还没删除了,redis宕机了,欧玛噶,这运气真好,百年一遇,那可咋整,可以设置一个比较短的过期时间,这样就可以避免死锁了;感觉30s就差不多了  下面是两种 设置 过期时间的方式

哪一个更好呢 , 第一个 可不是原子操作 第二个 是原子操作 ,哪一个比较好 , 不用多说了吧

因为有可能实在是倒霉了,刚要设置过期时间,redis宕机了,好家伙,又死锁了

        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "kangSir");
        
        stringRedisTemplate.expire("lockKey",10, TimeUnit.SECONDS);
       Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "kangSir",10, TimeUnit.SECONDS);

过期时间设置了

好像还有问题,如果 说 ,我这个中间的业务及其复杂,导致还没执行完呐,锁就自动释放了,那我锁不就白加了,又出现了并发,又超卖了------- 根本原因是 : 就是设置了超时时间,但是我们设置超时时间 就是为了解决 redis 宕机的问题,要不然就会死锁

        现在的问题是,我无法确定业务到底要执行多少时间,可能一个线程执行到一半 ,锁就会释放了,

        导致这个线程后面delete的可能是另一个线程的锁,就相当于全乱了,

        要解决这个问题 , 就要在释放时 做判断(确定当前线程一定是释放当前线程的锁,由于线程的id可能 会 重复 ,so UUID 就可以来帮助我们--》 后面redisson 的 源码 就是这样解决的)

动态的增加过期时间

        上面可能导致 并发的问题 就是 源自于 过期时间的无法判断 ,但是如果在 加锁的同时 开启另一个 线程 来动态的 监视 我主线程 是否已经释放锁了,如果没有释放,那就是还没有执行完,就会自动设置一个新的过期时间--锁续命 (也教 watchDog)

  

  这个是原生的(没有使用 redisson 简单的)
        //这个返回值是代表的是 这个操作是否成功了
        Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "kangSir",10, TimeUnit.SECONDS);
        if (aBoolean) {
            //这个线程 成功拿到 了锁
            try {
                // 秒杀 -- 超卖
                String s = stringRedisTemplate.opsForValue().get(SHOP_MIAOSHA_PRE + "kucun");
                int kucun = Integer.parseInt(s);
                if (kucun > 0) {
                    kucun = kucun - 1;
                    stringRedisTemplate.opsForValue().set(SHOP_MIAOSHA_PRE + "kucun", kucun + "");
                    System.out.println("恭喜,抢到了>>还有" + kucun + "个");
                } else {
                    System.out.println("你的手速慢了,已经没了");
                }
            } finally {
                //最终要释放
                stringRedisTemplate.delete("lockKey");
            }
            return "ok";
        } else {
            return "当前系统繁忙";
        }

所有数据归位,开测

这个结果……

分析 :

哦…… 有些线程没有拿到锁,直接返回了当前系统繁忙,虽然很合理,但是 现在可是抢东 西,先到先得,理应这个200个线程抢到,先到的为啥不让抢(想到了我抢四级名额的时候,都是痛啊);

思考:

怎么解决呐,如果能让他尝试几次,不直接返回,岂不妙哉

如果说 上面的 功能 都需要实现 , 那么代码量就太大了

因此 今天的大佬 redisson 登场 ,这个 几乎 满足 我们的所有需求 (^_^)

入门 redisson

依赖

       <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.6.5</version>
        </dependency>

配置 redisson

    @Bean
    public Redisson redisson() {
        Config config = new Config();
        config.useSingleServer().setAddress("127.0.0.1").setDatabase(0);
        return (Redisson) Redisson.create(config);
    }

使用 (very easy)

//   获取锁对象
        RLock key = redisson.getLock("lockKey");
//   加锁
        key.lock();

        {
//            业务逻辑
        }
        // 释放锁
        key.unlock();

我们使用的越简单 那么 对应的 底层工作就会越多 分析底层

分析之前 要大概 熟悉一下 流程 

上图

tryLockInnerAsync

redis 执行 lua脚本 : eval script numkeys key[ key....] arg[arg....]

eg:  EVAL "return {KEYS[1],KEYS[2],AGRV[1],AGRV2}" 2 key1 key2 first  second
结果 :

        “key1”
        “key2”

        “first”

        “second”

commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  "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]);",
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));

lua脚本  KEYS[1] : key的占位符

                ARGV[1]: value的占位符  

分析 lua 脚本 转为 伪代码 大概就是这样

if(redis里面  不存在 创建锁指定的key){
    
     // hset key - map
    // 也就是 key key value

     hset(指定的key ,根据线程id和UUid产生的key ,1 )
     // 设置 过期时间 为 30s
    pexpire(指定的 key , 30s)
    // lua的 nil 就相当于 null
    return null

}
if(我们设置 key 的 hash 里面 有 线程id和UUid产生的key){
       就将 key key value 的value 加一
       设置过期时间 为30s
        return null
}
retrun 我门设置 key  还有多长时间过期

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值