Redis做分布式锁的问题

93 阅读3分钟

参考

谷粒商城p158:www.bilibili.com/video/BV1np…

redis官网:www.redis.cn/commands/se…

提要

分布式场景下将原有的本地锁换为,基于redis的setnx命令的分布式锁

  • getCatalogJsonFromDb:从数据库查数据
  • getCatalogJsonFromDbWithLocalLock:利用本地锁查数据
  • getCatalogJsonFromDbWithRedisLock:利用redis的setnx命令的分布式锁查数据,现需要完成的

分布式锁演进-阶段一

代码

public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
    
    // 1、占分布式锁,setnx
	Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111");
    if(lock){
        // 加锁成功。。。执行任务
        Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
        redisTemplate.delete("lock");
        return catalogJsonFromDb;
    }else{
        // 加锁失败。。。重试
        // 休眠100ms重试
        // 自旋方式
        return getCatalogJsonFromDbWithRedisLock();
    }

}

问题

  • setnx占好了位置,业务代码异常或程序宕机,没有执行删锁逻辑,死锁!!!

解决

  • 设置锁自动过期,即使没有删除,到期也会消失

分布式锁演进-阶段二

代码

public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
    
    // 1、占分布式锁,setnx
	Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111");
    if(lock){
        // 加锁成功。。。执行任务
        
        // 2、设置过期时间
        redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
        
        Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
        redisTemplate.delete("lock");
        return catalogJsonFromDb;
    }else{
        // 加锁失败。。。重试
        // 休眠100ms重试
        // 自旋方式
        return getCatalogJsonFromDbWithRedisLock();
    }

}

问题

  • 加锁与设置过期时间非原子操作,所以仍会出现死锁问题

解决

  • 利用redis提供的setexnx,做原子操作

分布式锁演进-阶段三

代码

public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
    
    // 1、占分布式锁,setnx
    // 同时设置时间
	Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
    if(lock){
        // 加锁成功。。。执行任务
        
        // 2、设置过期时间
        // redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
        
        Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
        redisTemplate.delete("lock");
        return catalogJsonFromDb;
    }else{
        // 加锁失败。。。重试
        // 休眠100ms重试
        // 自旋方式
        return getCatalogJsonFromDbWithRedisLock();
    }

}

问题

  • 如果业务时间过长,我们的锁过期自动删除,这时直接删锁,有可能把别人正在持有的锁删除了

解决

  • 占锁时指定uuid保证唯一性,删锁需要验证是否是自己的锁

分布式锁演进-阶段四

代码

public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {
    
    // 1、占分布式锁,setnx
    // 同时设置时间
	String uuid = UUID.randomUUID().toString();
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
    if(lock){
        // 加锁成功。。。执行任务
        
        // 2、设置过期时间
        // redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
        
        Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
        // redisTemplate.delete("lock");
        String lockValue = redisTemplate.opsForValue().get("lock");
        // 验证是否是自己的锁
        if(uuid.equals(lockValue)){
            // 是,删锁
            redisTemplate.delete("lock");
        }
        return catalogJsonFromDb;
    }else{
        // 加锁失败。。。重试
        // 休眠100ms重试
        // 自旋方式
        return getCatalogJsonFromDbWithRedisLock();
    }

}

问题

  • 注意在redisget锁的值时,即String lockValue = redisTemplate.opsForValue().get("lock");,这时可能redis还存在我们的锁,这时返回的正是我们uuid,但是因为网络传输的延时,我们要执行delete操作时,我们的锁已经因为过期策略删除了,所以虽然这是的锁不是我们的,但程序代码仍然会执行删除锁(并非我们的),本质仍是非原子操作问题

解决

  • lua脚本,实现原子操作

分布式锁演进-阶段五

代码

public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {

    // 1、占分布式锁,setnx
    // 同时设置时间
    String uuid = UUID.randomUUID().toString();
    Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
    Map<String, List<Catalog2Vo>> catalogJsonFromDb = null;
    if (lock) {
        // 加锁成功
        
        // 2、设置过期时间
        // redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
        
        try {
            catalogJsonFromDb = getCatalogJsonFromDb();
        } finally {
            // 获取值对比+对比成功删除=原子操作
            // String lockValue = redisTemplate.opsForValue().get("lock");
            // if (uuid.equals(lockValue)) {
            //     redisTemplate.delete("lock");
            // }

            // 脚本解锁
            String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
            Long unlock = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
        }
        return catalogJsonFromDb;

    } else {
        // 加锁失败。。。重试
        // 休眠100ms重试
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return getCatalogJsonFromDbWithRedisLock();
    }

}

总结

使用redis做分布式锁

  • 加锁和设置过期时间的原子性问题
  • 解锁与验证锁的归属的原子性问题
  • 还有业务时间与过期时间的设置,有时需要延长过期时间

可以考虑使用Redisson