基于redis的分布式锁

使用场景

系统使用过程中有如下情况:
对某一个变量进行加/减等操作时,如果同一时间有多个线程对该变量进行操作,会出现变量值重复的情况。如:

@PostMapping("/testSync")
public void testSync(){
    flag--;
    System.out.println(flag);
}

在1S之内模拟发起100个请求,执行结果可能会出现如下情况:
基于redis的分布式锁_第1张图片
库存超卖问题:
在电商系统中提交订单时的简易执行步骤:

  1. 检查库存是否足够;
  2. 更新库存数量;

如果此时商品的数量只有一个,有两个用户提交了订单,第一个用户的请求执行完步骤1还未执行步骤2时,第二个用户的请求到达,该请求发现还有库存,就继续执行步骤2,这样就造成了1个商品卖了两次,但实际库存只有一个。

针对以上问题的解决方案:将操作用synchronized锁住,当前线程执行完之后,下一个线程才能继续执行。

@PostMapping("/testSync")
public void testSync(){
    synchronized (syncFlag){
        flag--;
        System.out.println(flag);
    }
}

但这种解决方式只适用于单机系统,如果将系统部署到两台服务器上并分别启动,两个系统同时处理接收到的请求,还是会出现超卖的情况,因为部署的两个系统运行在不同的JVM中,synchronized加的锁只对属于自己JVM里面的线程有效,对于其他JVM的线程是无效的。
基于以上问题,需要保证多个系统中加的锁是同一个锁,所以用到了分布式锁的方案,本文主要介绍的是基于redis实现的分布式锁。

分布式锁的实现

使用Redis做分布式锁的思路大概是这样的:在redis中设置一个值表示加了锁,然后释放锁的时候就把这个值删除。
redis中的命令setnx可以实现上面的思路:执行setnx返回1表示写入值成功,当前线程获得了锁,返回0表示redis中已经存在当前值,当前线程抢锁失败。

代码实现1:

@PostMapping("/testSync")
public void testSync(){
    Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(syncFlag, syncFlag);
    if (!absent){
        System.out.println("抢锁失败");
    }
    flag--;
    System.out.println(flag);
    stringRedisTemplate.delete(syncFlag);
}

上面的代码中如果setIfAbsent()方法向redis中写入值syncFlag成功,返回true,表示当前没有线程执行该操作,同样的,下一个线程在当前线程执行完并且将redis中的值删掉之后才能继续操作。
但是假如上面的代码在执行过程中出错,就会使redis中的key永远不会被删除,这段代码就会永远被锁住,不会被其他线程执行,所以,key 必须设置一个超时时间,以保证即使没有被显式删除,也要在一定时间后自动删除。

代码实现2:

@PostMapping("/testSync")
public void testSync(){
    Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(syncFlag, syncFlag, 
                30, TimeUnit.SECONDS);
    if (!absent){
        System.out.println("抢锁失败");
    }
    flag--;
    System.out.println(flag);
    stringRedisTemplate.delete(syncFlag);
}

springboot2.1之前的版本不支持这种方式,需要额外的代码。

还有一种情况:线程A抢到了锁,并将redis中锁的过期时间设置了30S,但是由于某些原因线程A在30S之内并未执行完被锁定的代码,锁就被释放了,这时线程B抢到了锁,接着线程A执行完了任务,要删除redis中的锁,此时线程B还未执行完任务,线程A删除的实际上是线程B加的锁。
针对这种情况,可以在加锁的时候随机生成一个数当做 value,并放入ThreadLocal中,删除锁时判断redis中的value是否和ThreadLocal中的值相等:

代码实现3:

private ThreadLocal<String> threadLocal = new ThreadLocal<>();

@PostMapping("/testSync")
public void testSync(){
	String uuid = UUID.randomUUID() + "";
    threadLocal.set(uuid);
    Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(syncFlag, uuid,
            30, TimeUnit.SECONDS);
    if (!absent){
        System.out.println("抢锁失败");
    }
    flag--;
    System.out.println(flag);
    if (threadLocal.get().equals(stringRedisTemplate.opsForValue().get(syncFlag))){
        stringRedisTemplate.delete(syncFlag);
        threadLocal.remove();
    }
}

上面的方式还是会出现并发情况:线程A在执行任务时还未执行完锁被释放了,线程B拿到锁,线程A和B可能在在同一时间同时执行加锁的代码块。
针对这种情况,我们可以在锁快要过期的时候给锁续命。可以新建一个线程,在当前线程没有中断的时候,每睡眠10S将redis中的值续命10S。

代码实现4:

private ThreadLocal<String> threadLocal = new ThreadLocal<>();

@PostMapping("/testSync")
public void testSync(){
    String uuid = UUID.randomUUID() + "";
    threadLocal.set(uuid);
    Thread thread = new Thread() {
        @Override
        public void run() {
            while (true && !Thread.currentThread().isInterrupted()) {
                try {
                    sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                stringRedisTemplate.expire(syncFlag, 10, TimeUnit.SECONDS);
            }
        }
    };
    Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(syncFlag, uuid, 
    		30, TimeUnit.SECONDS);
    if (!absent){
        System.out.println("抢锁失败");
    }
    thread.start();
    flag--;
    System.out.println(flag);
    if (threadLocal.get().equals(stringRedisTemplate.opsForValue().get(syncFlag))){
        threadLocal.remove();
        stringRedisTemplate.delete(syncFlag);
    }
}

在现有代码的情况中,同时有100个请求发起,其中一个抢占了锁,其余99个请求就会结束,在电商系统中就会出现还有库存但提示库存不足的情况。
解决办法:可以让剩余99个请求在没有拿到锁的时候一直等待,等到拿到锁的线程执行完之后释放锁,然后重新抢锁,一直到抢锁成功,开始执行代码块。

代码实现5:

private ThreadLocal<String> threadLocal = new ThreadLocal<>();

@PostMapping("/testSync")
public void testSync(){
    String uuid = UUID.randomUUID() + "";
    threadLocal.set(uuid);
    Thread thread = new Thread() {
        @Override
        public void run() {
            while (true && !Thread.currentThread().isInterrupted()) {
                try {
                    sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                stringRedisTemplate.expire(syncFlag, 10, TimeUnit.SECONDS);
            }
        }
    };
    Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(syncFlag, uuid,
    		30, TimeUnit.SECONDS);
    if (!absent){
        while (true){
            absent = stringRedisTemplate.opsForValue().setIfAbsent(syncFlag, uuid,
            		30, TimeUnit.SECONDS);
            if (absent){
                break;
            }
        }
    }
    thread.start();
    flag--;
    System.out.println(flag);
    if (threadLocal.get().equals(stringRedisTemplate.opsForValue().get(syncFlag))){
        threadLocal.remove();
        stringRedisTemplate.delete(syncFlag);
    }
}

给等待锁的线程加一个等待上限,如果超过了10S,则该线程结束。
使用FutureTask限制任务的执行时间,get()阻塞等待异步调用结果,直到超时或任务执行完毕。
cancal(boolean):是否需要中断任务执行线程,如果为true,会调用interrupt()。

代码实现6:

private ThreadLocal<String> threadLocal = new ThreadLocal<>();

@PostMapping("/testSync")
public void testSync(){
    String uuid = UUID.randomUUID() + "";
    threadLocal.set(uuid);
    Thread thread = new Thread() {
        @Override
        public void run() {
            while (true && !Thread.currentThread().isInterrupted()) {
                try {
                    sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                stringRedisTemplate.expire(syncFlag, 10, TimeUnit.SECONDS);
            }
        }
    };
    Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(syncFlag, uuid,
    		30, TimeUnit.SECONDS);
    if (!absent){
        FutureTask futureTask = new FutureTask<>(() -> {
            while (true){
                if (stringRedisTemplate.opsForValue().setIfAbsent(syncFlag, uuid,
            			30, TimeUnit.SECONDS)){
                    return true;
                }
            }
        });
        try {
            // 实例化线程并启动,该线程获得CPU时间后调用futureTask中的run()
            new Thread(futureTask).start();
            // 熊get被调用开始,阻塞时间超过10S,则会抛出超时异常
            futureTask.get(10, TimeUnit.SECONDS);
        } catch (Exception e) {
            futureTask.cancel(true);
            threadLocal.remove();
            return;
        }
    }
    thread.start();
    flag--;
    if (threadLocal.get().equals(stringRedisTemplate.opsForValue().get(syncFlag))){
        threadLocal.remove();
        stringRedisTemplate.delete(syncFlag);
    }
}

你可能感兴趣的:(java)