redis分布式锁相关知识

关于redis分布式锁的逐步升级完善

  • 一、redis原始写法
  • 二、redis使用stringRedisTemplate增加分布式锁
  • 三、redis使用stringRedisTemplate继续改进升级分布式锁
  • 四、redis使用redisson实现看门狗机制
  • 五、redis红锁使用方式
  • 六、redis锁的优化使用

一、redis原始写法

public String deductStockOriginal() {
        int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); 
        if (stock > 0) {
            int realStock = stock - 1;
            stringRedisTemplate.opsForValue().set("stock", realStock + "");
            System.out.println("扣减成功,剩余库存:" + realStock);
        } else {
            System.out.println("扣减失败,库存不足");
        }

        return "end";
    }

问题说明:此时查询和扣减库存不是原子性操作,并发场景会出现超卖现象。此时需要增加分布式锁,减少超卖现象发生。

二、redis使用stringRedisTemplate增加分布式锁

1、第一种情况redis分布式锁未设置超时时间

public String deductStockIfNoTime() {
        String lockKey = "lock:product_101";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lockValue"); //jedis.setnx(k,v)
        if (!result) {
            return "error_code";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            stringRedisTemplate.delete(lockKey);
        }

        return "end";
    }

问题描述:如果没有设置超时时间,则出现宕机问题时,会出现死锁,这个锁将一直不能释放,影响其他请求获取锁。

2、将设置缓存和超时时间分开未做到原子性
redis分布式锁相关知识_第1张图片
问题描述:如果将设置锁和设置超时时间分开,则存在原子性问题,如果设置锁后,过期时间还没设置好,此时宕机,则锁没有超时时间将一直存在

3、设置了超时时间,但是没有续期逻辑,会存在逻辑没执行完锁就过期的情况

/* *
     * @description: 使用stringRedisTemplate的IfAbsent实现分布式锁
     * 此时如果1号线程10s未执行完,锁超时过期,2号线程可以获取到锁,从而执行扣减库存操作
     *
     * @author: quwuju
     * @date: 2023/12/22 14:27
     * @param    【null】
     * @return: null
     */
    @RequestMapping("/deduct_stock1")
    public String deductStockIfAbsent() {
        String lockKey = "lock:product_101";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "lockValue", 10, TimeUnit.SECONDS); //jedis.setnx(k,v)
        if (!result) {
            return "error_code";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
                stringRedisTemplate.delete(lockKey);
        }

        return "end";
    }

问题说明:此时存在一个问题是如果当前1号线程执行时间较长,过了redis缓存的时间了,那新的2号线程进来就能加锁成功,此时1号线程业务逻辑结束之后,删除锁,删除的是2号线程的锁,很有可能因为某种原因2号线程的业务逻辑还没结束。那3号线程再来加锁也可以加锁成功,线程2结束时删除的是线程3的锁。。。。。。

三、redis使用stringRedisTemplate继续改进升级分布式锁

设置一个clientId作为标识,每个线程只能删除自己的锁

/* *
     * @description: 使用stringRedisTemplate实现分布式锁  setnx
     * 升级,设置一个clientId作为标识,每个线程只能删除自己的锁
     *
     * @author: quwuju
     * @date: 2023/12/22 14:27
     * @param    【null】
     * @return: null
     */
    @RequestMapping("/deduct_stock")
    public String deductStock() {
        String lockKey = "lock:product_101";
        // 产生一个uuid来区分
        String clientId = UUID.randomUUID().toString();
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS); //jedis.setnx(k,v)
        if (!result) {
            return "error_code";
        }
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
            
                stringRedisTemplate.delete(lockKey);
            }
        }

        return "end";
    }

问题描述:升级,设置一个clientId作为标识,每个线程只能删除自己的锁。但是超时时间不易确定是第一个问题,第二个问题是假设判断cliedntId的时候刚判断完,卡顿了1s,锁过期了,新的线程又加锁成功,此时执行到删除的逻辑,会将新线程的锁删除掉,此时又不是删除了自己的锁,所以此时就引入redisson来做看门狗锁续期。

四、redis使用redisson实现看门狗机制

1、线程加锁续命逻辑分析

public String deductStockRedisson(String a, int b) {
        String lockKey = "lock:product_101";
        //获取锁对象
        RLock redissonLock = redisson.getLock(lockKey);
        //加分布式锁
        redissonLock.lock();  //  .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
        
        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            //解锁
            redissonLock.unlock();
        }


        return "end";
    }

redisson锁流程图
redis分布式锁相关知识_第2张图片

使用lua脚本保证原子性,使用脚本加锁设置超时时间,设置完成之后会回调续期代码逻辑
redis分布式锁相关知识_第3张图片
加锁完成后执行续期,递归续期
redis分布式锁相关知识_第4张图片

2、线程抢夺锁自旋逻辑分析

  1. 加锁过程中如果成功返回一个空值,直接执行下面逻辑,如果加锁失败会得到当前锁剩余过期时间。redis分布式锁相关知识_第5张图片
  2. 此时如果加锁失败,则开始返回值不为null,开始死循环(自旋)
    redis分布式锁相关知识_第6张图片
    此时会阻塞(当前锁还剩余的过期时间),等等完了剩余的超时时间后开始执行逻辑

getLatch方法里面是获取一个信号量,如果未获取到锁,会等待上一个线程还剩余的超时时间,到时间后继续循环获取锁 信号量
信号量
redis分布式锁相关知识_第7张图片

在redisson的解锁逻辑中会通过redis的发布订阅功能。在抢锁过程中会订阅一个channel,解锁的时候会发布消息给订阅的队列,通知等待的线程继续抢锁。

五、redis红锁使用方式

红锁的实现方式是整多个节点(非主从节点,各自独立),每次加锁必须半数以上节点返回成功才算加锁成功。一般也会给每个节点配置从节点。

@RequestMapping("/redlock")
    public String redlock() {
        String lockKey = "product_001";
        //这里需要自己实例化不同redis实例的redisson客户端连接,这里只是伪代码用一个redisson客户端简化了
        RLock lock1 = redisson.getLock(lockKey);
        RLock lock2 = redisson.getLock(lockKey);
        RLock lock3 = redisson.getLock(lockKey);

        /**
         * 根据多个 RLock 对象构建 RedissonRedLock (最核心的差别就在这里)
         */
        RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
        try {
            /**
             * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
             * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
             */
            boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
            if (res) {
                //成功获得锁,在这里处理业务
            }
        } catch (Exception e) {
            throw new RuntimeException("lock fail");
        } finally {
            //无论如何, 最后都要解锁
            redLock.unlock();
        }

        return "end";
    }

redis分布式锁相关知识_第8张图片
如果宕机的slave节点重启之后也不存在当前这个key,那2号节点和3号节点都可以加锁成功,此时就是半数以上节点成功,所以红锁失效。

红锁存在的问题可参考文章:红锁存在的问题

红锁相关分析及问题,暂不做赘述:引用别人的文章 红锁相关分析及问题

六、redis锁的优化使用

1.锁的粒度一定要小
2.可参考concurrentHashMap分段锁实现,比如有1000个商品要加锁,可以分10个锁,每100个商品一个锁。

你可能感兴趣的:(redis相关,redis,分布式)