一文带你搞懂Redis分布式锁

 更新时间:2022年09月19日 08:28:41   作者:指北君  
本篇文章主要来介绍一下如何Redis实现分布式锁的演进过程,以及为什么不能直接用Setnx实现分布式锁,文中的示例代码讲解详细,需要的可以参考一下

1、分布式锁简介

分布式锁是控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。

业界流行的分布式锁实现,一般有这3种方式:

  • 基于数据库实现的分布式锁
  • 基于Redis实现的分布式锁
  • 基于Zookeeper实现的分布式锁

这里主要介绍如何通过 Redis 来实现分布式锁。在介绍 Redis 分布式锁之前,我们首先介绍一下实现Redis 分布式锁的关键命令。

2、setnx

setnx key value

Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。

设置成功,返回 1 。设置失败,返回 0 。

PS:Redis 官方是不推荐基于 setnx 命令来实现分布式锁的,因为会存在很多问题,

①、单点问题。比如:

1、客户端A 从master拿到锁lock01

2、master正要把lock01同步(Redis的主从同步通常是异步的)给slave时,突然宕机了,导致lock01没同步给slave

3、主从切换,slave节点被晋级为master节点

4、客户端B到master拿lock01照样能拿到。这样必将导致同一把锁被多人使用。

②、锁的高级用法,比如读写锁、可重入锁等等,setnx 都比较难实现。

这里先介绍基于 sentnx 实现的分布式锁,后面会介绍官方推荐的基于 redisson 来实现分布式锁。

3、Redis-分布式锁-阶段1

接到上文,查询三级分类数据,如果我们部署了多个商品服务,然后多个线程同时去获取三级分类数据,如果不加分布式锁,就会导致,每一个部署的商品服务第一次查询都会走 DB。

public Map<String, List<Catelog2Vo>> getCatelogJsonWithRedisLock() throws InterruptedException {
    // 一、获取分布式锁
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");

    if(lock){
        // true 表示加锁成功,执行相关业务
        Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
        stringRedisTemplate.delete("lock");
        return dataFromDb;
    }else{
        System.out.println("获取分布式锁失败...等待重试...");
        //加锁失败...重试机制
        //休眠一百毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //自旋的方式
        return getCatelogJsonWithRedisLock();
    }
}

4、Redis-分布式锁-阶段2

设置锁自动过期

public Map<String, List<Catelog2Vo>> getCatelogJsonWithRedisLock() throws InterruptedException {
    // 一、获取分布式锁
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");

    if(lock){
        // true 表示加锁成功,执行相关业务
        // 设置过期时间
        stringRedisTemplate.expire("lock",30,TimeUnit.SECONDS);
        Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
        stringRedisTemplate.delete("lock");
        return dataFromDb;
    }else{
        System.out.println("获取分布式锁失败...等待重试...");
        //加锁失败...重试机制
        //休眠一百毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //自旋的方式
        return getCatelogJsonWithRedisLock();
    }
}

5、Redis-分布式锁-阶段3

setnx 命令和过期时间保证原子性。

public Map<String, List<Catelog2Vo>> getCatelogJsonWithRedisLock() throws InterruptedException {
    // 一、获取分布式锁
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111",30,TimeUnit.SECONDS);

    if(lock){
        // true 表示加锁成功,执行相关业务
        // 设置过期时间
        //stringRedisTemplate.expire("lock",30,TimeUnit.SECONDS);
        Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
        stringRedisTemplate.delete("lock");
        return dataFromDb;
    }else{
        System.out.println("获取分布式锁失败...等待重试...");
        //加锁失败...重试机制
        //休眠一百毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //自旋的方式
        return getCatelogJsonWithRedisLock();
    }
}

6、Redis-分布式锁-阶段4

保证删除的是自己的锁。

public Map<String, List<Catelog2Vo>> getCatelogJsonWithRedisLock() throws InterruptedException {
    // 一、获取分布式锁
    String uuid = UUID.randomUUID().toString();
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,30,TimeUnit.SECONDS);

    if(lock){
        // true 表示加锁成功,执行相关业务 
        // 设置过期时间
        //stringRedisTemplate.expire("lock",30,TimeUnit.SECONDS);
        Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
        String lockValue = stringRedisTemplate.opsForValue().get("lock");
        if(uuid.equals(lockValue)){
            stringRedisTemplate.delete("lock");
        }
        return dataFromDb;
    }else{
        System.out.println("获取分布式锁失败...等待重试...");
        //加锁失败...重试机制
        //休眠一百毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //自旋的方式
        return getCatelogJsonWithRedisLock();
    }
}

7、Redis-分布式锁-阶段5

通过Lua脚本保证删除锁和判断锁两个操作原子性

public Map<String, List<Catelog2Vo>> getCatelogJsonWithRedisLock(){
    // 一、获取分布式锁
    String uuid = UUID.randomUUID().toString();
    Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,30,TimeUnit.SECONDS);

    if (lock) {
        System.out.println("获取分布式锁成功...");
        Map<String, List<Catelog2Vo>> dataFromDb = null;
        try {
            //加锁成功...执行业务
            dataFromDb = getDataFromDb();
        } finally {
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

            //删除锁
            stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);

        }
        //先去redis查询下保证当前的锁是自己的
        //获取值对比,对比成功删除=原子性 lua脚本解锁
        // String lockValue = stringRedisTemplate.opsForValue().get("lock");
        // if (uuid.equals(lockValue)) {
        //     //删除我自己的锁
        //     stringRedisTemplate.delete("lock");
        // }

        return dataFromDb;
    }else{
        System.out.println("获取分布式锁失败...等待重试...");
        //加锁失败...重试机制
        //休眠一百毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //自旋的方式
        return getCatelogJsonWithRedisLock();
    }
}

这也是分布式锁的最终模式,需要保证两个点:加锁【设置锁+过期时间】和删除锁【判断+删除】原子性。

到此这篇关于一文带你搞懂Redis分布式锁的文章就介绍到这了,更多相关Redis分布式锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 手把手带你实现第一个Mybatis程序

    手把手带你实现第一个Mybatis程序

    这篇文章主要介绍了mybatis实现过程详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2021-07-07
  • Java Array与ArrayList区别详解

    Java Array与ArrayList区别详解

    这篇文章主要介绍了Java Array与ArrayList区别详解的相关资料,需要的朋友可以参考下
    2017-01-01
  • Spring-boot 中@Async使用遇到的坑

    Spring-boot 中@Async使用遇到的坑

    这篇文章主要介绍了Spring-boot 中@Async使用的坑,首先使用@Async 需要在Spring启动类上添加注解@EnableAsyn或者在你们线程池配置类添加@EnableAsyn,需要的朋友可以参考下
    2024-01-01
  • springboot加载命令行参数ApplicationArguments的实现

    springboot加载命令行参数ApplicationArguments的实现

    本文主要介绍了springboot加载命令行参数ApplicationArguments的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-04-04
  • java自带的MessageDigest实现文本的md5加密算法

    java自带的MessageDigest实现文本的md5加密算法

    这篇文章主要介绍了java自带的MessageDigest实现文本的md5加密算法,需要的朋友可以参考下
    2015-12-12
  • Sentinel整合Feign流程详细讲解

    Sentinel整合Feign流程详细讲解

    要想整合Feign,首先要了解Feign的使用以及执行过程,然后看 Sentinel如何整合进去,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • Java list foreach修改元素方式

    Java list foreach修改元素方式

    这篇文章主要介绍了Java list foreach修改元素方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-11-11
  • java正则表达式解析html示例分享

    java正则表达式解析html示例分享

    这篇文章主要介绍了java正则表达式解析html示例,用到获取url的正则表达式,获取图片的正则表达式,需要的朋友可以参考下
    2014-02-02
  • Java使用wait/notify实现线程间通信下篇

    Java使用wait/notify实现线程间通信下篇

    wait()和notify()是直接隶属于Object类,也就是说所有对象都拥有这一对方法,下面这篇文章主要给大家介绍了关于使用wait/notify实现线程间通信的相关资料,需要的朋友可以参考下
    2022-12-12
  • Mybatis返回插入主键id的方法

    Mybatis返回插入主键id的方法

    这篇文章主要介绍了 Mybatis返回插入主键id的方法,在文章底部给大家补充了Mybatis中insert中返回主键ID的方法,非常不错,需要的朋友可以参考下
    2017-04-04

最新评论