springboot集成redis分布式锁

1、导入redis包


         org.springframework.boot
         spring-boot-starter-data-redis
         2.2.5.RELEASE
   

2、版本一,适合无并发情况

    /**
     * 版本一
     * 读取redis剩余票
     * 如果大于0,执行卖出
     */
    @GetMapping("/one")
    public void versionOne() {
        Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
        if (ticketLeft>0) {
            ticketLeft = ticketLeft -1;
            System.out.println("卖出第几张票:" + ticketLeft);
            redisTemplate.opsForValue().set(TICKET,ticketLeft);
        }
    }

这个代码正常执行没有问题,但是如果出现高并发,会发生超卖现象,多个线程同一时刻取到了同样的剩余票数,用jmeter模拟高并发测试


image.png

image.png

发现高并发情况下,这种逻辑不适用,会出现一张票贩卖多次的情况

3、版本二,修改代码,适合并发情况

多个线程同时请求redis,通过setIfAbsent设置锁,相当于setnx,如果返回true,说明redis没有人设置过key,第一次跑 ,如果返回false,说明有人已经设置过了,正在执行代码,这时候直接给他返回,或者等待别的线程执行结束。

程序执行结束,一定要解除redis锁,给下个线程跑

    /**
     * 版本二
     * 适合高并发情况下使用
     */
    @GetMapping("/two")
    public void test() {
        // 给线程上锁,同一时刻只有一个线程可以从redis获取数据,true第一次上锁,false已有key,说明别人加过锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "lock");
        if (lock) {
            // 如果上锁成功,执行代码
            Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
            System.out.println("卖出第几张票:" + ticketLeft);
            redisTemplate.opsForValue().set(TICKET,ticketLeft - 1+"");
        } else {
            System.out.println("获取锁失败");
        }
        // 买票结束,释放锁资源
        redisTemplate.delete(LOCK_KEY);
    }

使用jmeter并发测试,发现没有出现超卖情况


image.png

这段代码会有个问题,如果程序抛异常,那么最后的解锁步骤不会执行,会导致后面所有的线程全部获取锁失败,所以我们给锁加个过期时间

4、 版本三,防止代码异常,出现死锁

    /**
     * 版本三
     * 防止死锁
     */
    @GetMapping("/three")
    public void three() {
        // 给线程上锁,同一时刻只有一个线程可以从redis获取数据,true第一次上锁,false已有key,说明别人加过锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "lock");
        redisTemplate.expire(LOCK_KEY,6, TimeUnit.SECONDS);
        try {
            if (lock) {
                // 如果上锁成功,执行代码
                Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
                System.out.println("卖出第几张票:" + ticketLeft);
                redisTemplate.opsForValue().set(TICKET,ticketLeft - 1+"");
            } else {
                System.out.println("获取锁失败");
            }
        }finally {
            // 买票结束,释放锁资源
            redisTemplate.delete(LOCK_KEY);
        }
    }

这个代码还有个问题,如果刚执行完上锁操作,服务器宕机,后面的过期时间没有设置,还是会出现死锁,所以需要保证上锁和设置过期时间同步执行,继续修改

5、 版本四,继续修改死锁情况

    /**
     * 版本四
     * 防止死锁
     */
    @GetMapping("/four")
    public void four() {
        // 给线程上锁,同一时刻只有一个线程可以从redis获取数据,true第一次上锁,false已有key,说明别人加过锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, "lock",6,TimeUnit.SECONDS);
        try {
            if (lock) {
                // 如果上锁成功,执行代码
                Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
                System.out.println("卖出第几张票:" + ticketLeft);
                redisTemplate.opsForValue().set(TICKET,ticketLeft - 1+"");
            } else {
                System.out.println("获取锁失败");
            }
        }finally {
            // 买票结束,释放锁资源
            redisTemplate.delete(LOCK_KEY);
        }
    }

setIfAbsent有个方法,同时传入时间和单位,他会同步发送给redis,保证上锁和设置时间同步执行

Boolean setIfAbsent(K key, V value, long timeout, TimeUnit unit);

代码还有个问题,现在设置的过期时间是6s,假如有个线程1,业务代码需要执行10s,那么在执行到第7s的时候,锁失效,线程2获取到锁,开始执行业务代码,这时候会有线程1,线程2两段业务逻辑同步执行
继续执行到第10s的时候,线程1结束,执行解锁操作,删除key
但是,线程1的key已经失效过期,所以他删除的其实是线程2的key,这时候会导致线程3获取到锁执行代码,无限往复
继续修改代码

6、版本五,防止线程互删锁

解决思路,给每个线程一个不重复的随机数,解锁的时候先判断redis的锁是不是当初的锁,是的话,执行解锁

    /**
     * 版本5
     * 防止线程互删锁
     */
    @GetMapping("/five")
    public void five() {
        String value = UUID.randomUUID().toString();
        // 给线程上锁,同一时刻只有一个线程可以从redis获取数据,true第一次上锁,false已有key,说明别人加过锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, value,6,TimeUnit.SECONDS);
        try {
            if (lock) {
                // 如果上锁成功,执行代码
                Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
                System.out.println("卖出第几张票:" + ticketLeft);
                redisTemplate.opsForValue().set(TICKET,ticketLeft - 1+"");
            } else {
                System.out.println("获取锁失败");
            }
        }finally {
            // 如果现在的锁还是当初上的锁,执行解锁
            if (value.equals(redisTemplate.opsForValue().get(LOCK_KEY))){
                // 买票结束,释放锁资源
                redisTemplate.delete(LOCK_KEY);
            }
        }
    }

继续优化代码,redis锁过期时间应该比程序运行时间长,但是程序运行时间不可控,所以加入守护线程给redis续期,只要程序在运行就一直给锁续期,知道程序结束

7、版本六,锁续期

    /**
     * 版本六,锁续期
     */
    @GetMapping("/six")
    public void six() {
        String value = UUID.randomUUID().toString();
        // 给线程上锁,同一时刻只有一个线程可以从redis获取数据,true第一次上锁,false已有key,说明别人加过锁
        Boolean lock = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY, value,10,TimeUnit.SECONDS);
        try {
            if (lock) {
                // 执行续签
                Thread thread = new Thread(new RedisDaemonTask(LOCK_KEY,value,redisTemplate));
                thread.setDaemon(true);
                thread.start();
                Thread.sleep(30000);
                // 如果上锁成功,执行代码
                Integer ticketLeft = Integer.valueOf(redisTemplate.opsForValue().get(TICKET).toString());
                System.out.println("卖出第几张票:" + ticketLeft);
                redisTemplate.opsForValue().set(TICKET,ticketLeft - 1+"");
            } else {
                System.out.println("获取锁失败");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 如果现在的锁还是当初上的锁,执行解锁
            if (value.equals(redisTemplate.opsForValue().get(LOCK_KEY))){
                // 买票结束,释放锁资源
                redisTemplate.delete(LOCK_KEY);
            }
            System.out.println("程序结束");
        }
    }

守护线程,如果锁没过期,并且还是当前进程的锁,就续期

public class RedisDaemonTask implements Runnable {

    private String key;
    private String value;
    // 是否继续
    private Boolean isContinue;

    private RedisTemplate redisTemplate;

    public RedisDaemonTask() {
    }

    public  RedisDaemonTask(String key, String value, RedisTemplate redisTemplate) {
        this.key = key;
        this.value = value;
        this.redisTemplate = redisTemplate;
        this.isContinue = true;
    }

    @Override
    public void run() {
        while (isContinue) {
            try {
                // 原key过期时间
                Long lock = redisTemplate.getExpire(key);
                System.out.println("过期时间" + lock);
                if (lock>0 && value.equals(redisTemplate.opsForValue().get(key))) {
                    // key还存在没有过期,手动续期
                    redisTemplate.expire("lock",lock + 5, TimeUnit.SECONDS);
                    System.out.println("续期成功");
                } else {
                    // 如果没有获取到value,说明主线程解锁了,不继续续期
                    isContinue = false;
                }
                Thread.sleep(3000);
            }catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

你可能感兴趣的:(springboot集成redis分布式锁)