欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Redis - 实现分布式锁的阶段演进

程序员文章站 2024-02-03 18:17:10
...

①演进阶段一

获得锁就执行业务逻辑,没有获得锁就继续调用这个方法形成一个自旋,就类似于synchronized
Redis - 实现分布式锁的阶段演进

伪代码:
public void getData(){
    boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111");
    if(lock){
        // 执行业务..
        
        // 删除锁
        redisTemplate.delete("lock");  
    }else{
         // 休眠一段时间
        // 继续调用getData,等待锁的释放
        getData();
    }
}
存在问题:

setnx占好了位,业务代码异常或者程序在页面过程中宕机。没有执行删除锁逻辑,这就造成了死锁

解决方法:

给锁设置一个过期时间,即使代码异常或是程序宕机,锁都会因为过期时间到了自动删除。

②演进阶段二

解决阶段一的问题。
Redis - 实现分布式锁的阶段演进

伪代码:
public void getData(){
    boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111");
    if(lock){
        // 设置过期时间
        redisTemplate.expire("lock",180,TimeUnit.SECONDS);
        
        // 执行业务..
        
        // 删除锁
        redisTemplate.delete("lock");  
    }else{
        // 休眠一段时间
        // 继续调用getData,等待锁的释放
        getData();
    }
}
存在问题:

获得锁之后,正要去设置过期时间,这是服务宕机/断电,这个时间还没设置上去,又导致了死锁。

解决方法:

这就需要保证设置value和过期时间是原子性操作,这就需要使用Redis的setnx ex命令。
原来将键key设定为指定的“字符串”值,如果 key 已经保存了一个值,那么这个操作会直接覆盖原来的值,并且忽略原始类型。
在2.6.12版本开始,redis为SET命令增加了一系列选项:

EX seconds              设置键key的过期时间,单位时秒
PX milliseconds         设置键key的过期时间,单位时毫秒
NX                      只有键key不存在的时候才会设置key的值
XX                      只有键key存在的时候才会设置key的值
命令:
SET key value [EX seconds] [PX milliseconds] [NX|XX]

③演进阶段三

解决阶段二的问题。
Redis - 实现分布式锁的阶段演进

伪代码:
public void getData(){
    // 设置了过期时间 - 180s
    boolean lock = redisTemplate.opsForValue.setUfAbsent("lock","1111",180,TimeUnit.SECONDS);
    if(lock){
        // 执行业务...
        
        // 删除锁
        redisTemplate.delete("lock");  
    }else{
        // 休眠一段时间
        // 继续调用getData,等待锁的释放
    }
}
存在问题:

如果由于业务时间很长,锁自己过期了,我们直接删除,有可能把别人正在持有的锁删除了。

场景:

比如我们执行一个业务,线程1拿到了Redis锁,Redis锁过期时间是180s,我们这个业务却执行了300s,也就是说,我们执行到180s的时候,锁已经过期了,那另外一个线程2拿到了锁,等我们线程1业务执行完的时候,线程2业务还在执行中,线程1要去删锁,这时删除的锁其实是线程2的。

解决方法:

使用随机的大字符串作为value值,删除前先对比value值是否相同,相同就删除。

④演进阶段四

解决阶段三的问题。
Redis - 实现分布式锁的阶段演进

伪代码:
public void getData(){
    // 使用UUID生成不重复值
    String uuid = UUID.randomUUID().toString();
    boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
    if(lock){
        // 执行业务...
        
        // 判断uuid是否相同
        String str = redisTemplate.get("lock");
       if(uuid.equals(str){ // 相同,就删除锁
           redisTemplate.delete("lock"); 
       }
    }else{
        // 休眠一段时间
        // 继续调用getData,等待锁的释放
    }
}
解决方法:

保证对比value值和删除value值是一个原子性操作,使用Redis+Lua脚本来完成。

⑤演进阶段五

解决阶段四的问题,最终形态。
Redis - 实现分布式锁的阶段演进

伪代码:
public void getData(){
    // 使用UUID生成不重复值
    String uuid = UUID.randomUUID().toString();
    boolean lock = redisTemplate.opsForValue.setUfAbsent("lock",uuid,180,TimeUnit.SECONDS);
    if(lock){
        try{
            // 执行业务...
        }finally{
            // Lua脚本 - 2个参数 key和value
           String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
           Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class),Arrays.asList("lock"),uuid);
       }
    }else{
        // 休眠一段时间
        // 继续调用getData,等待锁的释放
    }
}
存在问题:

锁的过期时间,如果业务没执行完,锁应该续期,最简单的解决方法就是把锁的过期时间设置大一点。

分布式锁框架

分布式锁框架Redisson,基于Redis实现的各种分布式锁。
Redisson的GitHub地址