Redis中的分布式锁

150 阅读3分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

问题描述

随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

这里使用redis来解决分布式锁的相关问题

命令

上锁

image-20211209142759407

释放锁

image-20211209142823424

为了防止程序在上锁完成后,程序崩溃,导致无法释放锁以至于其他程序无法正常操作,这里设置锁的自动过期时间

设置key过期时间

image-20211209143034670

为了防止在上锁完成后到设置过期时间程序崩溃以至于其他程序无法正常操作的情况,把上锁和设置过期时间变成一个原子操作

image-20211209143405314

代码

@GetMapping("testLock")
public void testLock(){
    //1获取锁,setne
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
    //2获取锁成功、查询num的值
    if(lock){
        Object value = redisTemplate.opsForValue().get("num");
        //2.1判断num为空return
        if(StringUtils.isEmpty(value)){
            return;
        }
        //2.2有值就转成成int
        int num = Integer.parseInt(value+"");
        //2.3把redis的num加1
        redisTemplate.opsForValue().set("num", ++num);
        //2.4释放锁,del
        redisTemplate.delete("lock");

    }else{
        //3获取锁失败、每隔0.1秒再获取
        try {
            Thread.sleep(100);
            testLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

改良版——设置过期时间

 Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",3,TimeUnit.SECONDS);

新问题

问题1:

如果业务逻辑的执行时间是7s。执行流程如下

  1. index1业务逻辑因为程序异常没执行完,3秒后锁被自动释放。

  2. index2获取到锁,执行业务逻辑,3秒后锁被自动释放。

  3. index3获取到锁,执行业务逻辑

  4. index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。

最终出现没锁的情况。

问题图示:

image-20211209144118357

最终b的锁被释放,出现无锁情况,造成程序运行出错

问题解决方案

setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁

图解:

image-20211209144328734

再每次准备删除锁的时候,先根据uuid判断当前的锁是否属于自己,属于自己再进行删除

代码

image-20211209144444810

问题2:

  1. index1执行删除时,查询到的lock值确实和uuid相等

​ uuid=v1

​ set(lock,uuid);

  1. index1执行删除前,lock刚好过期时间已到,被redis自动释放。在redis中没有了lock,没有了锁。

  2. index2获取了lock,index2线程获取到了cpu的资源,开始执行方法

    uuid=v2

    set(lock,uuid);

  3. index1执行删除,此时会把index2的lock删除

总结来说:

image-20211209144718198

index1在成功判断后,准备执行删除锁的操作时,锁自动过期了,index2成功获取锁,然后index1执行删除锁,index2还没执行完业务代码就出现了无锁状态。

图解:

image-20211209144842178

问题解决方案

优化之LUA脚本保证删除的原子性

1、加锁

// 1. 从redis中获取锁,set k1 v1 px 20000 nx
String uuid = UUID.randomUUID().toString();
Boolean lock = this.redisTemplate.opsForValue()
      .setIfAbsent("lock", uuid, 2, TimeUnit.SECONDS);

2、使用lua释放锁

// 2.释放锁 del 
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";  
// 设置lua脚本返回的数据类型 
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();  
// 设置lua脚本返回的数据类型Long 
redisScript.setResultType(Long.class); 
redisScript.setScriptText(script);  
redisTemplate.execute(redisScript, Arrays.asList("lock"),uuid);

3、重试

Thread.sleep(500);
testLock();