Redis分布式锁解决超卖问题

 更新时间:2023年12月11日 10:51:03   作者:Eliauk-_-  
超卖问题是典型的多线程安全问题,本文就来介绍一下Redis分布式锁解决超卖问题,具有一定的参考价值,感兴趣的可以了解一下

一、使用redisTemplate中的setIfAbsent方法。

      // setIfAbsent就是对应redis的setnx
        Boolean setIfAbsent = redisTemplate.opsForValue().setIfAbsent(lockKey, lockKey, 10, TimeUnit.SECONDS);
        if (Boolean.TRUE.equals(setIfAbsent)) {
            LOG.info("恭喜,抢到锁了!lockKey:{}", lockKey);
        } else {
             LOG.info("很遗憾,没抢到锁!lockKey:{}", lockKey);
        }

缺点:

  • 性能不高,可能会导致少买。
  • 假如业务执行时间长,设置的锁的过期时间短的话,可能出现超卖问题。

二、使用Redisson解决(看门狗方式)

2.1、实现原理

redisson在获取锁之后,会开启一个守护线程(看门狗线程),当锁即将过期还没有释放时,不断的延长锁key的生存时间

2.2、SpringBoot集成Redisson

2.2.1、添加pom.xml依赖

            <!--至少3.18.0版本,才支持spring boot 3-->
            <!--升级到3.20.0,否则打包生产会报错:Could not initialize class org.redisson.spring.data.connection.RedissonConnection-->
            <dependency>
                <groupId>org.redisson</groupId>
                <artifactId>redisson-spring-boot-starter</artifactId>
                <version>3.21.0</version>
            </dependency>

2.2.2、注入RedissonClient对象

@Autowired
private RedissonClient redissonClient;

2.2.3、使用Redisson

RLock lock = null;
   try {
            // 使用redisson,自带看门狗
             lock = redissonClient.getLock(lockKey);
             /**
               waitTime – the maximum time to acquire the lock 等待获取锁时间(最大尝试获得锁的时间),超时返回false
               leaseTime – lease time 锁时长,即n秒后自动释放锁
               time unit – time unit 时间单位
              */
             // boolean tryLock = lock.tryLock(30, 10, TimeUnit.SECONDS); // 不带看门狗
             boolean tryLock = lock.tryLock(0, TimeUnit.SECONDS); // 带看门狗
             if (tryLock) {
                 LOG.info("恭喜,抢到锁了!");
             } else {
                 LOG.info("很遗憾,没抢到锁");
             }
   } catch (InterruptedException e) {
             LOG.error("发生异常", e);
        } finally {
             LOG.info("释放锁!");
            //当lock不等于null并且lock是当前线程的时候去释放锁
             if (null != lock && lock.isHeldByCurrentThread()) {
                 lock.unlock();
             }
        }

缺点:

  • 看门狗启动后,对整体性能也会有一定影响
  • 当redis宕机后,获取不到锁,业务中断。
  • 正常情况下,如果加锁成功了,那么master节点会异步复制给对应的slave节点。但是如果在这个过程中发生master节点宕机,主备切换,slave节点从变为了 master节点,而锁还没从旧master节点同步过来,这就发生了锁丢失,会导致多个客户端可以同时持有同一把锁的问题

三、Redis红锁

2.1、Redis红锁名称来源

Redis 红锁的名称来源于 Redis 的logo,Redis 的 logo 是一个红色热气球,而红色的热气球上有一把锁的图案,因此这种分布式锁解决方案也被称为"Redlock",中文翻译为"红锁"。

2.2、原理

现在假设有5个Redis主节点(大于3的奇数个),这样基本保证他们不会同时都宕掉,获取锁和释放锁的过程中,客户端会执行以下操作:

  • 获取当前Unix时间,以毫秒为单位,并设置超时时间过期时间(过期时间要大于正常业务执行的时间 + 获取所有redis服务消耗时间 + 时钟漂移)
  • 依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁,当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间TTL,这样可以避免客户端死等。比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
  • 客户端 获取所有能获取的锁后的时间 减去 第(1)步的时间,就得到锁的获取时间。锁的获取时间要小于锁失效时间TTL,并且至少从半数以上的Redis节点取到锁,才算获取成功锁
  • 如果成功获得锁,key的真正有效时间 = TTL - 锁的获取时间 - 时钟漂移。比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s
  • 如果因为某些原因,获取锁失败(没有在半数以上实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁,无论Redis实例是否加锁成功,因为可能服务端响应消息丢失了但是实际成功了。

2.3、代码实现

2.3.1、注册红锁的RedissonClient

@Component
public class RedisConfig {
	@Bean(name = "redissonClient1")
    @Primary
    public RedissonClient redissonRed1(){
        Config config = new Config();
        config.useSingleServer().setAddress("127.0.0.1:6379").setDatabase(0);
        return Redisson.create(config);
    }
    @Bean(name = "redissonClient2")
    public RedissonClient redissonRed2(){
        Config config = new Config();
        config.useSingleServer().setAddress("127.0.0.1:6380").setDatabase(0);
        return Redisson.create(config);
    }
    @Bean(name = "redissonClient3")
    public RedissonClient redissonRed3(){
        Config config = new Config();
        config.useSingleServer().setAddress("127.0.0.1:6381").setDatabase(0);
        return Redisson.create(config);
    }
    @Bean(name = "redissonClient4")
    public RedissonClient redissonRed4(){
        Config config = new Config();
        config.useSingleServer().setAddress("127.0.0.1:6382").setDatabase(0);
        return Redisson.create(config);
    }
    @Bean(name = "redissonClient5")
    public RedissonClient redissonRed5(){
        Config config = new Config();
        config.useSingleServer().setAddress("127.0.0.1:6383").setDatabase(0);
        return Redisson.create(config);
    }
  }

2.3.2、注入Redis红锁对象

    // 红锁
    @Autowired
    @Qualifier("redissonClient1")
    private RedissonClient redissonClient1;
    @Autowired
    @Qualifier("redissonClient2")
    private RedissonClient redissonClient2;
    @Autowired
    @Qualifier("redissonClient3")
    private RedissonClient redissonClient3;
    @Autowired
    @Qualifier("redissonClient4")
    private RedissonClient redissonClient4;
    @Autowired
    @Qualifier("redissonClient5")
    private RedissonClient redissonClient5;

2.3.3、使用Redis红锁

        /*
           假设有五台redis机器, A B C D E
           线程1: A B C D E(获取到锁)
           线程2: C D E(获取到锁)
           线程3: C(未获取到锁)
        */
            RLock lock1 = null;
            RLock lock2 = null;
            RLock lock3 = null;
            RLock lock4 = null;
            RLock lock5 = null;
        try {
            lock1 = redissonClient1.getLock(lockKey);
            lock2 = redissonClient2.getLock(lockKey);
            lock3 = redissonClient3.getLock(lockKey);
            lock4 = redissonClient4.getLock(lockKey);
            lock5 = redissonClient5.getLock(lockKey);
            // 红锁的写法
            RedissonRedLock redissonRedLock = new RedissonRedLock(lock1, lock2, lock3, lock4, lock5);
            boolean tryLock = redissonRedLock.tryLock(0, TimeUnit.SECONDS);
            if (tryLock) {
                LOG.info("恭喜,抢到锁了!");
            } else {
                LOG.info("很遗憾,没抢到锁");
            }
        } catch (InterruptedException e) {
            LOG.error("发生异常", e);
        } finally {
            LOG.info("释放锁!");
            //当lock不等于null并且lock是当前线程的时候去释放锁
            if (null != lock && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }

注意:

  • 按照顺序获取锁、不然会出现每个线程都拿不到锁的情况。
  • 当redis宕机后,切换主备或者重启时间需大于锁的时间,不然会有线程同时获取到锁(比如线程1获取到a,b,c,获得了锁后c宕机重启了,如果数据没有备份,c中没有key,线程2有可能获取到了c,d,e,也获取到了锁)。
  • 尽可能的获取到更多redis实例的锁。
  • 获取锁的时间要注意,需要设置超时时间,如果超时时间内拿不到锁,结束线程

到此这篇关于Redis分布式锁解决超卖问题的文章就介绍到这了,更多相关Redis分布式锁超卖内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • Redis中有序集合的内部实现方式的详细介绍

    Redis中有序集合的内部实现方式的详细介绍

    本文主要介绍了Redis中有序集合的内部实现方式的详细介绍,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • 解析Redis未授权访问漏洞复现与利用危害

    解析Redis未授权访问漏洞复现与利用危害

    这篇文章主要介绍了Redis未授权访问漏洞复现与利用,介绍了redis未授权访问漏洞的基本概念及漏洞的危害,本文给大家介绍的非常详细,需要的朋友可以参考下
    2022-01-01
  • Redis中哈希分布不均匀的解决办法

    Redis中哈希分布不均匀的解决办法

    这篇文章主要介绍了Redis中哈希分布不均匀的解决办法的相关资料,需要的朋友可以参考下
    2021-02-02
  • redis如何实现清空缓存

    redis如何实现清空缓存

    这篇文章主要介绍了redis如何实现清空缓存,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • 基于redis实现世界杯排行榜功能项目实战

    基于redis实现世界杯排行榜功能项目实战

    前段时间,做了一个世界杯竞猜积分排行榜。对世界杯64场球赛胜负平进行猜测,猜对+1分,错误+0分,一人一场只能猜一次。下面通过本文给大家分享基于redis实现世界杯排行榜功能项目实战,感兴趣的朋友一起看看吧
    2018-10-10
  • Redis之SDS数据结构的使用

    Redis之SDS数据结构的使用

    本文主要介绍了Redis之SDS数据结构的使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • Redis分布式锁防止缓存击穿的实现

    Redis分布式锁防止缓存击穿的实现

    本文主要介绍了Redis分布式锁防止缓存击穿的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • 使用Grafana监控Redis的操作方法

    使用Grafana监控Redis的操作方法

    这篇文章主要介绍了使用Grafana监控Redis,号称下一代可视化监控系统,结合SpringBoot使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-04-04
  • 解析Redis 数据结构之简单动态字符串sds

    解析Redis 数据结构之简单动态字符串sds

    Redis 的 string 类型为何使用sds而不是 C 字符串,本文主要介绍 string 的数据结构—— 简单动态字符串(Simple Dynamic String) 简称sds的相关知识,需要的朋友可以参考下
    2021-11-11
  • Redis击穿穿透雪崩产生原因分析及解决思路面试

    Redis击穿穿透雪崩产生原因分析及解决思路面试

    这篇文章主要为大家介绍了Redis击穿穿透雪崩产生原因及解决思路的面试问题答案参考,有需要的朋友可以借鉴参考下,希望能够有所帮助祝大家多多进步
    2022-03-03

最新评论