【Redis学习06】分布式锁及其优化

文章目录

    • 前言
    • 1. 什么是分布式锁
    • 2. 分布式锁的实现
      • 2.1 基于Redis的分布式锁实现方法
      • 2.2 基于redis实现分布式锁的初级版本
      • 2.3 改进分布式锁
      • 2.4 基于Lua脚本改善分布式锁

前言

上一篇博客我们讲到秒杀问题的一人一单在单机模式下使用synchronized添加悲观锁能解决并发问题。

但是在集群模式下,我们使用悲观锁就无法解决并发问题,因为集群中每个java虚拟机不是共用一个锁,而是每个java虚拟机都拥有属于自己的锁,因而就无法保证在并发模式下的一人一单问题。
【Redis学习06】分布式锁及其优化_第1张图片

怎么解决呢?

那就要引入我们今天的要学习的内容—分布式锁

我们这里先简要介绍一下分布式锁原理:集群模式悲观锁失效是因为每个JVM有其独自的锁,而集群模式就必须共享同一个锁,因此,我们取消每个JVM中的锁,让所有的JVM都去共用同一个锁监视器,这就是分布式锁的基本思想。
【Redis学习06】分布式锁及其优化_第2张图片

1. 什么是分布式锁

分布式锁:满足分布式系统或者集群模式下多进程可见并且互斥的锁

它需要满足以下特点:
【Redis学习06】分布式锁及其优化_第3张图片

实现效果:同一时刻只有一个线程能拥有互斥锁,只有该线程释放互斥锁,其他线程才能获得互斥锁的使用权
【Redis学习06】分布式锁及其优化_第4张图片

2. 分布式锁的实现

【Redis学习06】分布式锁及其优化_第5张图片
这里我们使用redis提供的分布式锁功能。

2.1 基于Redis的分布式锁实现方法

【Redis学习06】分布式锁及其优化_第6张图片
这里我们要明确一个点,因为我们学习redis需要考虑在高并发模式下,很多线程共享资源的问题。资源共享就会涉及进程阻塞问题,因此每一步操作就需要尽量满足原子性,减少进程阻塞导致的并发问题。

因此,我们在获取锁的方式可以修改为如下方式

将设置互斥锁和超时时间同步完成,是为了解决在设置锁的时候发生进程阻塞导致锁未设置超时时间,导致锁无法释放的问题
【Redis学习06】分布式锁及其优化_第7张图片
OK,接下来我们就根据以上方法设置我们的第一个分布式锁。

2.2 基于redis实现分布式锁的初级版本

  1. 需求分析
    【Redis学习06】分布式锁及其优化_第8张图片

  2. 梳理流程
    【Redis学习06】分布式锁及其优化_第9张图片

  3. 代码实现

public class SimpleRedisLock implements ILock{

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";

    private StringRedisTemplate stringRedisTemplate;
    private String name;

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeSec) {
        //获取线程id
        String threadId = ID_PREFIX+Thread.currentThread();

        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeSec, TimeUnit.SECONDS);

        //返回的是boolean类型,是基本数据类型,而我们返回的Boolean类型是包装类,避免拆箱出现空指针。
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {
        //删除锁
        stringRedisTemplate.delete(KEY_PREFIX + name);

    }
}

这里说明两点

  • 我们设置的锁需要满足互斥以及具有超时时间,我们使用setIfAbsent()方法,它不仅满足互斥条件,还能同时设置超时时间,满足需要。
    【Redis学习06】分布式锁及其优化_第10张图片

  • 拆箱与装箱

1)什么是拆箱
• 拆箱就是自动将包装器类型转换为基本数据类型
• 拆箱调用Integer.intValue方法

2)什么是装箱
• 装箱就是自动将基本数据类型转换为包装器类型
• 装箱调用的Integer.valueOf方法
【Redis学习06】分布式锁及其优化_第11张图片

我们在尝试获取锁的方法中返回值时基本数据类型,而设置锁返回的时包装器类型,如果我们将其直接返回会涉及拆箱和装箱过程,我们在开发过程中要尽量避免拆箱和装箱,因为可能会出现空指针问题
【Redis学习06】分布式锁及其优化_第12张图片

在秒杀券的一人一单代码实现之前获取我们设置的分布式互斥锁,添加基本的分布式锁,由于后面执行代码可能抛异常,因此需要try/finally释放锁

 //添加基本的分布式锁,由于后面执行代码可能抛异常,因此需要try/finally释放锁
        SimpleRedisLock redisLock = new SimpleRedisLock(redisTemplate, "order:" + userId);
        boolean flag = redisLock.tryLock(5);
        if(!flag){
            return Result.fail("一人只能购买一张优惠券");
        }


        try {
            //一人一单
            int count = this.query().eq("user_id", userId)
                    .eq("voucher_id", voucherId).count();
            if(count>0){
                return Result.fail("一个用户只能购买一个优惠券");
            }

            //当更新时查询的库存大于0时进行库存减一
            boolean success = seckillVoucherService.update()
                    .setSql("stock=stock-1")
                    .gt("voucher_id", 0)
                    .eq("stock", voucher.getStock()).update();

            if (!success) {
                return Result.fail("优惠券已被抢完");
            }

            //6. 创建订单
            //6.1 设置id
            VoucherOrder voucherOrder = new VoucherOrder();
            Long voucherOrderId = RedisIdWorker.nextId("voucherOrder");
            voucherOrder.setId(voucherOrderId);

            //6.2 设置user_id
            voucherOrder.setUserId(userId);

            //6.3 设置优惠券id
            voucherOrder.setVoucherId(voucherId);
            this.save(voucherOrder);

            return Result.ok(voucherOrderId);
        } finally {
            redisLock.unLock();
        }

2.3 改进分布式锁

我们结合下面的图进行分析一下使用上述分布式锁可能发生的情况:

  1. 当线程1获取redis锁后执行业务,这时业务阻塞,因而导致redis锁超时释放。
  2. 线程2此时获取到redis锁,执行业务,线程2执行业务过程中,线程1被唤醒,完成业务释放了redis锁,而业务2还在执行。
  3. 线程3获取redis锁,也开始执行业务,此时,线程2和线程3都在执行业务,我们的redis锁在并发模式下又形同虚设了。

【Redis学习06】分布式锁及其优化_第13张图片
怎么解决呢?

我们想想之前是如何解决超卖问题呢?是不是使用了CAS方法。即扣减库存的时候判断库存是否跟之前查询的库存相等,相等再去扣减库存。

方法类似,即我们在删除锁的时候可以设置一个线程标识,标识此时是哪个线程获取了锁。假如一旦获取锁的线程1阻塞,锁超时释放,此时线程2获取了锁,将锁的标识更换了,线程2执行业务的时候线程1唤醒,想要去释放锁,结果发现锁已经不是自己的,就不去释放。问题就解决了。

ok,我们看一下解决的流程图
【Redis学习06】分布式锁及其优化_第14张图片
我们只需要在删除锁的代码进行修改即可。

	public void unLock() {

        //获取当前线程ID
        String currentId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);

        //线程标识
        String threadId = ID_PREFIX+Thread.currentThread();

        //判断当前线程与线程标识是否相同,相同再进行释放
        if (threadId.equals(currentId)) {
            //删除锁
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }

        //删除锁
        //stringRedisTemplate.delete(KEY_PREFIX + name);

    }

2.4 基于Lua脚本改善分布式锁

上述改进能否完美解决因线程阻塞而导致的锁错误释放问题呢?

我们不妨来看一下下面这张图。

看不明白没关系,我帮你们读懂一下。

  1. 首先,线程1尝试获取锁成功,执行业务,执行完成后,尝试去释放锁,当程序判断完成是自己的锁后刚要去执行释放锁操作,诶,真不巧 ,线程被阻塞了,锁又被超时释放了。
  2. 这时,线程2尝试获取锁成功,执行业务,此时,线程1又醒了,醒了还没完,因为线程1已经判断锁是自己的,因此人家醒了二话不说就把锁给释放了
  3. 最后,线程3进来了,它也获取锁成功,执行业务,此时线程2和线程3就并行执行业务,我们的梦想又破灭了。。。
    【Redis学习06】分布式锁及其优化_第15张图片

怎么解决呢?

我们想想问题的关键是不是因为判断锁和删除锁是分开进行的,因而才会出现线程阻塞问题。那我们能不能将这两步操作改成原子级别的操作呢?

单纯靠java实现可能不太行,这里就要引入我们要讲的主角,Lua脚本

在这里插入图片描述

【Redis学习06】分布式锁及其优化_第16张图片
【Redis学习06】分布式锁及其优化_第17张图片
【Redis学习06】分布式锁及其优化_第18张图片

ok,现在我们基于Lua脚本来改进一下我们的释放锁的逻辑。

Lua脚本

-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del', KEYS[1])
end
return 0
public class SimpleRedisLock implements ILock{

    private static final String KEY_PREFIX = "lock:";
    private static final String ID_PREFIX = UUID.randomUUID().toString(true)+"-";

    //代码开始就加载静态代码块,也就是lua脚本
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    private StringRedisTemplate stringRedisTemplate;
    private String name;

    public SimpleRedisLock(StringRedisTemplate stringRedisTemplate, String name) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.name = name;
    }

    @Override
    public boolean tryLock(long timeSec) {
        //获取线程id
        String threadId = ID_PREFIX+Thread.currentThread();

        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX + name, threadId, timeSec, TimeUnit.SECONDS);

        //返回的是boolean类型,是基本数据类型,而我们返回的Boolean类型是包装类,避免拆箱出现空指针。
        /*boolean flag = Boolean.TRUE.equals(success);
        return flag;*/
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unLock() {

        //使用lua脚本执行原子级别的操作,不会因为线程阻塞导致释放锁发生错误。
        stringRedisTemplate.execute(UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX+Thread.currentThread());

    }

}

执行lua脚本调用 stringRedisTemplate.execute的API,传递三个参数
【Redis学习06】分布式锁及其优化_第19张图片
【Redis学习06】分布式锁及其优化_第20张图片
经过三个版本的分布式锁迭代,我们设计的锁就基本能够满足我们的使用需求了。

你可能感兴趣的:(Redis,redis,分布式,学习)