Redis分布式锁的手动实现

手动实现Redis分布式锁存在的问题

  • 锁被别人释放了怎么办?
  • 定义的过期时间太长或太短,怎么定义?
  • 锁的释放不是原子操作怎么处理?

小试牛刀

首先我们要定义锁的实现,也就是下面的getLock方法,使用的是RedisTemplate,并没有去定义工具类。

getLock方法获取锁

getLock方法使用的是redis的SETNX命令,但是由于SETNX命令无法进行原子性操作过期时间,所以我们在这里使用SET命令,从redis2.6.12版本开始,SET命令的行为可以通过一系列参数来修改:

  • EX seconds : 将键的过期时间设置为 seconds 秒。 执行 SET key value EX seconds 的效果等同于执行 SETEX key seconds value。
  • PX milliseconds : 将键的过期时间设置为 milliseconds 毫秒。 执行 SET key value PX milliseconds 的效果等同于执行 PSETEX key milliseconds value。
  • NX : 只在键不存在时, 才对键进行设置操作。 执行SET key value NX 的效果等同于执行 SETNX key value 。
  • XX : 只在键已经存在时, 才对键进行设置操作。

同时使用SET LOCK ID EX 30 NX命令的好处就是可以加上ID这个唯一标识。后续会讲到。

release方法释放锁

在获取锁的时候我们设置了key,也就是锁的名称,ID就是键值对的value。好处就是为了释放锁的时候出问题,举个例子:当线程A获取到锁进行业务逻辑的时候,线程B的误操作,直接释放了锁。为了解决类似的异常情况,我们将当前的线程ID和锁进行绑定。
设计这个方法的步骤和思想如下:

  • 首先释放锁的时候需要传入当前线程信息,因为锁的value值是和线程id绑定的,同时需要传入锁的名称。
  • 获取到当前锁的value值,与当前需要释放锁线程ID是否一致。
  • 一致则可以释放,不一致不能释放。

问题它又来了,这里的get+del命令又是两条命令了,又得回到原子性的问题

  • 客户端1执行get命令,判断锁是不是自己的
  • 客户端2执行set命令,强制的获取到了锁
  • 客户端1执行了del命令,删掉了客户端2的锁

所以我们使用redis可以执行lua脚本的特性,写了一个脚本来原子性执行,解决这个问题。具体的实现如下代码所示:

@Component
public class RedisInfo {
    
    

    @Autowired
    private RedisTemplate<Object, Object> redisTemplate;

    /**
     * Title:getLock <br>
     * Description:获取锁的实现 <br>
     * author:于琦海 <br>
     * date:2022/10/25 10:58 <br>
     * @param key 锁的名称
     * @param time 预估的过期时间
     * @param timeUnit 时间单位
     * @param thread 当前操作获取锁的线程
     * @return Boolean
     */
    public Boolean getLock(String key, Long time, Thread thread, TimeUnit timeUnit) {
    
    
        return redisTemplate.opsForValue().setIfAbsent(key, thread.getId(), time, timeUnit);
    }

    /**
     * Title:release <br>
     * Description:释放锁的原子实现 <br>
     * author:于琦海 <br>
     * date:2022/10/25 11:12 <br>
     * @param key 锁的名称
     * @param thread 当前操作释放锁的线程
     * @return Long
     */
    public Long release(String key, Thread thread) {
    
    
        String lua = " if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end";
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setResultType(Long.class);
        script.setScriptText(lua);
        return redisTemplate.execute(script, Arrays.asList(key), thread.getId());
    }

    /**
     * Title:ttl <br>
     * Description:查询key的剩余过期时间 <br>
     * author:于琦海 <br>
     * date:2022/10/25 14:13 <br>
     * @param key 锁的名称
     * @param timeUnit 时间单位
     * @return Long
     */
    public Long ttl(String key, TimeUnit timeUnit) {
    
    
        return redisTemplate.getExpire(key, timeUnit);
    }

    /**
     * Title:expire <br>
     * Description:更新key的过期时间 <br>
     * author:于琦海 <br>
     * date:2022/10/25 14:33 <br>
     * @param key 锁的名称
     * @param time 预估的过期时间
     * @param timeUnit 时间单位
     * @return Boolean
     */
    public Boolean expire(String key, long time, TimeUnit timeUnit) {
    
    
        return redisTemplate.expire(key, time, timeUnit);
    }
}

续期问题

假设我们设置锁的持有时间为30s,但是由于网络问题或者机器问题导致执行我的业务逻辑超过了30s,而此时锁已经失效了,这种情况就会有问题。那如果我设置为一个超大的时间,比如100s一定能执行完,此时锁的效率又会变得非常低。

根据这个情况,我们可以写一个守护线程来根据剩余时间来对锁进行续期,类似于Redisson的看门狗机制。

思路如下:

  • 首先开启一个守护线程 。
  • 获取当前锁的剩余过期时间。
  • 如果锁的剩余时间小于等于我设置的锁过期时间的三分之一(我设置30s,除以3,也就是10s)。
  • 对当前的锁续期。
@Service
public class RedisLockDemo {
    
    

    @Autowired
    private RedisInfo redisInfo;

    /**
     * Title:businessInfo <br>
     * Description:这里面是业务逻辑伪代码 <br>
     * author:于琦海 <br>
     * date:2022/10/25 11:34 <br>
     */
    public void businessInfo() {
    
    
        // 获取锁
        Boolean lock = redisInfo.getLock("qhyu", 30L, Thread.currentThread(), TimeUnit.SECONDS);
        if (lock) {
    
    
            try {
    
    
                // 开启一个守护线程,去刷新过期时间,实现看门狗的机制
                Runnable thread = () -> {
    
    
                    while (true) {
    
    
                        // 会不会
                        // 获取key的剩余过期时间
                        Long timeLess = redisInfo.ttl("qhyu", TimeUnit.SECONDS);
                        // 当时间小于等于10s的时候,续命
                        if (timeLess <= 10L) {
    
    
                            System.out.println("续命");
                            redisInfo.expire("qhyu", 30L, TimeUnit.SECONDS);
                        }
                    }
                };
                Thread thread1 = new Thread(thread);
                thread1.setDaemon(true);
                thread1.start();
                int i = 0;
                while (true) {
    
    
                    if (i < 50) {
    
    
                        i++;
                        // 执行我们的业务逻辑
                        System.out.println("这里是我的业务逻辑" + i);
                        Thread.sleep(10000L);
                    } else {
    
    
                        break;
                    }
                }
            } catch (Exception e) {
    
    
                throw new RuntimeException(e);
            } finally {
    
    
                redisInfo.release("qhyu", Thread.currentThread());
            }
        }
    }
}

测试

根据上面的代码,写了个controller掉用了一下,能达到自动续期的目的。

@RestController
@RequestMapping(("/qhyu/redis"))
public class RedisLockController {
    
    

    @Autowired
    private RedisLockDemo redisLockDemo;

    @GetMapping("/test")
    public void test(){
    
    
        redisLockDemo.businessInfo();
    }
}

在时钟正常的情况下,我们预计没执行两次业务逻辑,就会续命一次。以下的结果能证明这一点。
在这里插入图片描述
那么如果不是守护线程,直接开一个线程不行吗?肯定是不行的,因为当线程执行完成之后,新开的线程不会被销毁,如下所示,会一直去对锁进行续命,就会出现问题。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Tanganling/article/details/127517358