Redis处理分布式锁

  上一章说了在单体应用中加锁解决缓存击穿问题,但是在分布式中,每个服务会有很多个,如果使用本地锁,它只锁自己的服务,而不能实现在所有的服务中只查询一次数据库,所以在这种情况下,我们可以考虑使用分布式锁
  

基本原理

在这里插入图片描述
  所有的服务都去一个公共的地方占锁,当一个服务拿到锁以后,他就可以执行相关的逻辑,而其他的服务就处于等待状态,这个公共的地方可以使MySQL,也可以是Redis,当然,我们的服务开发使用Redis做缓存,肯定是在Redis中加锁更加的方便,而Redis本身也提供对应的占锁的命令,详细请查看官方文档(传送门)
在这里插入图片描述
EX seconds-设置指定的到期时间,以秒为单位。
PX 毫秒-设置指定的到期时间(以毫秒为单位)。
NX -只有key不存在时设置key,相当于Redis中有锁,线程等待,我们在使用时就基于此
XX -只有key存在时设置key。
KEEPTTL -保留与key关联的生存时间。
GET-返回存储在key处的旧值;如果key不存在,则返回nil。
  先来测试哈这个命令,可以看到,当我同时向Redis中set数据,并且使用NX,只有一个成功,这就是分布式锁的基本原理
在这里插入图片描述
  用Java代码来表达

public void redisLock() throws InterruptedException {
    
    
    // 通过NX方式存值,占分布式锁
    Boolean flag = template.opsForValue().setIfAbsent("lock", "lock is not exist");
    if (flag) {
    
    //占锁成功
        //执行业务
        System.out.println(template.opsForValue().get("lock"));
        //业务执行成功后,需要删除锁给其他人用
        template.delete("lock");
    } else {
    
    //占锁失败,进行重试
        TimeUnit.SECONDS.sleep(50);
        redisLock();
    }
}

  这个锁相当于一个占位符,当服务拿到这个锁就可以进行后面的逻辑代码,而没有拿到锁的服务只能进行等待,这个锁是不参与实际业务的
  但是这个锁就会衍生出一个问题,当我们的业务代码出现异常,而导致该进程结束而没有删除锁,那么锁将一直被占用,这就造成了死锁,这个问题可以通过设置过期时间来解决,在加锁时设置锁的过期时间,但是要保证这两个操作原子性

public void redisLock() throws InterruptedException {
    
    
    // 通过NX方式存值,占分布式锁,设置过期时间防止死锁
    Boolean flag = template.opsForValue().setIfAbsent("lock", "lock is not exist",30,TimeUnit.SECONDS);
    if (flag) {
    
    //占锁成功
        //执行业务
        System.out.println(template.opsForValue().get("lock"));
        //业务执行成功后,需要删除锁给其他人用
        template.delete("lock");
    } else {
    
    //占锁失败,进行重试
        TimeUnit.SECONDS.sleep(50);
        redisLock();
    }
}

  同样的在删除锁的还会出现问题,例如:业务逻辑执行时间较长,而锁的过期时间较短,当业务还没执行完锁已经过期,而其他的业务已经可以占锁了,当第一个业务逻辑执行完后进行删锁,就删除了是其他服务的锁。解决办法:给自己的服务指定uuid唯一值

public void redisLock() throws InterruptedException {
    
    
    // 通过NX方式存值,占分布式锁,设置过期时间防止死锁
    String uuid = UUID.randomUUID().toString();
    Boolean flag = template.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
    if (flag) {
    
    //占锁成功
        //执行业务
        String lock = template.opsForValue().get("lock");
        System.out.println(lock);
        //业务执行成功后,需要删除锁给其他人用
        if (lock.equals(uuid)){
    
    //判断当前锁的值,防止误删其他服务的锁
            template.delete("lock");
        }
    } else {
    
    //占锁失败,进行重试
        TimeUnit.SECONDS.sleep(50);
        redisLock();
    }
}

  在删锁的时候还有其它问题,当获取锁的值,由于网络传输的延时,在比对成功后还没执行删除之前,这个锁的就失效了,所以这个时候删除的锁依旧是其他服务的锁,这是有网络传输需要消耗时间造成的,这个需要使用lua脚本进行处理,Redis官方也是推荐给我们这么做,而我们的代码时是没办法直接处理的,所以最后就演变成下面的结果

public void redisLock() throws InterruptedException {
    
    
    // 通过NX方式存值,占分布式锁,设置过期时间防止死锁
    String uuid = UUID.randomUUID().toString();
    Boolean flag = template.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
    if (flag) {
    
    //占锁成功
        try {
    
    
            //执行业务
            String lock = template.opsForValue().get("lock");
            System.out.println(lock);
        } finally {
    
    
            //业务执行成功后,需要删除锁给其他人用
            //lua脚本解锁
            String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
            //删除锁
            template.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
        }
    } else {
    
    //占锁失败,进行重试
        TimeUnit.SECONDS.sleep(50);
        redisLock();
    }
}

  分布式锁还有更加强大专业的框架来处理,下一章就使用Redisson来解决这一些的问题

猜你喜欢

转载自blog.csdn.net/weixin_45481406/article/details/113272570