SpringBoot实现分布式锁解决秒杀或者抢单问题

一,分布式锁诞生的原因

      为什么分布式锁会诞生?类似于淘宝双11的秒杀活动,同一件商品怎么才能只被一个用户抢到,其他用户抢不到?分布式锁就能巧妙地解决类似秒杀和抢单的问题。技术源于生活,更高于生活。对于阿里的那种的大型秒杀活动,分布式锁只是其中的一环,单单靠分布式锁不足以支撑那种大并发的情景,后续解决方案会陆续更新。本期只讲解分布式锁。

 

二,常见的分布式锁

     1,基于数据库实现的分布式锁

          基于表实现的分布式锁,如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁

     2,Redis分布式锁

     主要通过redis的存值取值进行判断的,根据返回的参数判断是否能拿到锁

     3,Zookeeper分布式锁

     利用Zookeeper的顺序临时节点,来实现分布式锁和等待队列。Zookeeper设计的初衷,就是为了实现分布式锁服务的。

 

三,SpringBoot项目中的Redis分布式锁

 

      1,引入相关的依赖

              首先你的springboot项目是需要正常的能够跑通的,然后在你的主pom文件引入redis的相关依赖

   <!--springboot 集成reids-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <version>2.1.3.RELEASE</version>
            </dependency>

      2,redis的分布锁的原理

     一般情况像这种经常用到的代码,单独抽一个Redis工具类出来,方便自己查看和调用。比如,我们现在进行一个活动,整点秒杀一台macPro电脑。想要将商品秒杀到,那只需要改变数据库的商品表的该商品的状态置为已下架,同时创建订单即可。在整点的时候,很多人同时秒杀更新数据库,那我们如何保证只被一个人拿到?道理很简单,我们将下单(更新数据库,创建订单)打包成一个方法,在方法的外面加锁,该锁被第一个A线程拿到后,其他线程未拿到锁则返回【很遗憾~您手慢啦~】,A线程将该商品置为已下架并且创建订单后,释放该锁,防止死锁。哪怕后面的线程延迟,再A线程释放锁后又拿到下单方法,因为商品的状态为已下架同样没有办法进行创建订单。至此,达到我们最初的目的,秒杀功能完成。

      3,加锁操作

   redis有StringRedisTemplate 和RedisTemplate 。我这里使用前者实现。

  @Autowired
    private StringRedisTemplate redisTemplate;

    在高并发的情况下,确保某一个方法只能被一个人调用,那么我们只要在该方法外调用工具类的加锁方法,该加锁方法返回true,则代表该方法没有被其他线程占用。若返回false,则代表该方法已经被其他线程占用,同步返回【很遗憾~您手慢啦~】。

  /**
     * 对传过来的redis的key进行加锁
     *  TimeUnit.SECONDS  秒
     * @param key   需要加锁的key
     * @param expire   过期时间
     * @return
     */
    public Boolean lockEnable(String key, long expire) {

        //这是将当前线程的名字置为key的value值,表明该锁被谁拿到
        String keyValue = Thread.currentThread().getName();

        //1,这是StringRedisTemplate在set key的同时增加了过期时间,防止死锁。保证了原子性。
        //2,setIfAbsent该方法如果该key不存在时候,设置值进去后,返回true;若是已经存在,则返回false;
        Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, keyValue, expire, TimeUnit.SECONDS);
        Long surplusTime = redisTemplate.getExpire(key, TimeUnit.SECONDS);
        if (!aBoolean) {
            log.info("该线程【{}】加锁失败,该key【{}】剩余过期时间【{}】秒", keyValue, key, surplusTime);
            return false;
        }
        log.info("该线程【{}】加锁成功,该key【{}】剩余过期时间【{}】秒", keyValue, key, surplusTime);
        return true;
    }

 

      4,解锁操作

  解锁这边不可以单纯的删除redis的值,这里需要对key和value两个参数进行和redis里面存储的是否一致,防止误删别人的锁。避免其他错误的产生,由于我们设置锁的时候,锁和失效时间有原子性,故不存在加完锁后就宕机,导致死锁。

   @Autowired
    private DefaultRedisScript<Long> redisScript;
 /**
     * lua脚本
     *
     * @return
     */
    @Bean
    public DefaultRedisScript<Long> defaultRedisScript() {
        DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
        defaultRedisScript.setResultType(Long.class);
        defaultRedisScript.setScriptText("if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end");
        return defaultRedisScript;
    }

同样的在完成该方法后一定要记得解锁,不要因为有过期时间就不释放锁,不要给自己埋坑,自己偷的懒早晚要还回来的。

   /**
     * 对传过来的redis的key进行解锁
     * key和value不一致时,返回:【0】
     * key和value不一致时,返回:【1】
     * @param key
     * @return
     */
    public Boolean lockUnable(String key) {
        String keyValue = Thread.currentThread().getName();
        //key和value不一致时,返回:【0】
        //key和value不一致时,返回:【1】
        Long execute = redisTemplate.execute(redisScript, Arrays.asList(key, keyValue));
        if(execute != 1 ){
            Boolean aBoolean = redisTemplate.hasKey(key);
            Long surplusTime = redisTemplate.getExpire(key, TimeUnit.SECONDS);
            log.info("该key【{}】解锁失败,是否存在【{}】,剩余过期时间【{}】秒", key, aBoolean, surplusTime);
            return false;
        }
        log.info("该key【{}】解锁成功", key);
        Boolean aBoolean = redisTemplate.hasKey(key);
        log.info("该key是否存在【{}】",aBoolean);
        return true;
    }

     5,分布式锁的测试类

    由于数据敏感问题,我这边自己使用了自己创建的员工表进行模拟秒杀,效果是一致的,我们锁的多台服务器上面的同一个方法。

/**
     * 1000个线程抢一条数据
     */
    @Test
    public void catchData() {
        for (int i = 0; i <1000; i++) {
            Thread thread = new Thread(() -> {
                threadTest();
            });
            thread.start();
            thread.setName("thread" + i);
        }
        while (true){

        }
    }
 /**
     * 线程调用的测试方法
     */
    public void threadTest() {

        /**
         * 对某一条数据的id进行加锁
         */
        Boolean aBoolean = redisUtils.lockEnable("狄仁杰", 600);


        if (!aBoolean) {
            log.info("线程【{}】没有拿到锁,结束流程",Thread.currentThread().getName());
            return;
        }
        UpdateUserDTO updateUserDTO = new UpdateUserDTO();
        updateUserDTO.setUserName("狄仁杰");
        updateUserDTO.setUserStatus("UNABLE");
        updateUserDTO.setMobilePhone("15555406855");
        updateUserDTO.setUpdatedBy(Thread.currentThread().getName());
        Result<Boolean> result = userBaseInfo.updateUser(updateUserDTO);
        if (!result.getResult()) {
            log.info("线程【{}】更新数据失败",Thread.currentThread().getName());
            return;
        }
        log.info("线程【{}】更新数据成功",Thread.currentThread().getName());


        /**
         * 释放该条数据的锁
         */
        Boolean aBoolean1 = redisUtils.lockUnable("狄仁杰");
        log.info("线程【{}】是否成功释放锁:【{}】",Thread.currentThread().getName(),aBoolean1);
    }

 5,分布式锁的测试结果展示

数据库之前的数据

跑完测试之后的数据

在打印的全部日志中,1000个线程只有一个线程拿到锁,其他线程全部失败。测试成功。

 

觉得写得你还满意,点下关注哈~如果有问题可以下面评论一起探讨下^~^

 

 

 

 

 

 

 

 

 

 

 

 

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值