精华内容
下载资源
问答
  • Java面试题之:基于 Redis 分布式锁
    2022-01-26 10:17:54

    Java面试题之:基于 Redis 分布式锁

      获取锁的时候,使用 setnx(SETNX key val:当且仅当 key 不存在时,set 一个 key为 val 的字符串,返回 1;若 key 存在,则什么都不做,返回 0)加锁,锁的 value值为一个随机生成的 UUID,在释放锁的时候进行判断。并使用 expire 命令为锁添加一个超时时间,超过该时间则自动释放锁。

      获取锁的时候调用 setnx,如果返回 0,则该锁正在被别人使用,返回 1 则成功获取锁。 还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

      释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete 进行锁释放。

    更多相关内容
  • 主要介绍了Redis分布式锁的实现方式(面试常见),需要的朋友可以参考下
  • 最近整理一份关于Redis常见面试题的,也会根据自己的经验, 标注一些出现的概率,最高5颗★出现的概率最高。比如这样: Redis 最适合的场景, 可以简单的说说吗? 出现概率: ★★★★ 整体目录大概如下: 目录 一、...

    最近整理一份关于Redis常见面试题的,也会根据自己的经验, 标注一些出现的概率,最高5颗★出现的概率最高。比如这样:

    Redis 最适合的场景, 可以简单的说说吗?
    出现概率: ★★★★

    整体目录大概如下:

    目录

    • 一、Redis基础知识
      • 1、什么是 Redis, 有哪些优缺点?
      • 2、Redis 最适合的场景, 可以简单的说说吗?
      • 3、Redis 相比 Memcached 有哪些优势?
      • 4、一个字符串类型的值能存储最大容量是多少?
      • 5、Redis 读写分离
    • 二、数据结构
      • 1、Redis的数据类型有哪些?
      • 2、说说 Redis 哈希槽的概念?
      • 3、Hash如何实现O(1)的查询和设置速度, 以及扩容原理
      • 4、布隆过滤器
    • 三、事务
      • 1、怎么理解 Redis 事务?
      • 2、Redis事务执行过程
      • 3、Redis事务的一些使用场景
      • 4、Redis事务与Redis pipeline的区别
      • 5、集群模式下Redis事务如何保证原子性
    • 四、Redis数据持久化
      • 1、为什么 Redis 需要把所有数据放到内存中?
      • 2、Redis如何做持久化的?
      • 3、Redis key 的过期时间和永久有效分别怎么设置?
    • 五、Redis集群
      • 1、Redis 是单进程单线程的?
      • 2、是否使用过 Redis 集群,集群的原理是什么?
      • 3、可以简单说说你对Redis Sentinel的理解
      • 4、Redis Sentinal和Redis Cluster的区别
      • 5、Redis 的同步机制了解么?
      • 6、Redis 集群最大节点个数是多少?
    • 六、Redis淘汰策略
      • 1、Redis过期键的删除策略?
      • 2、你可以简单聊聊Redis内存淘汰机制(回收策略)
    • 七、Redis分布式锁
      • 1、你知道实现实现分布式锁有哪些方案?
    • 八、Redis缓存问题
      • 1、Redis缓存雪崩
      • 2、Redis缓存击穿
      • 3、Redis缓存穿透
      • 4、缓存预热
      • 5、缓存降级
    • 九、运维和部署
      • 1、Redis 如何设置密码及验证密码?
      • 2、Redis 如何做内存优化?

    • 你知道实现实现分布式锁有哪些方案?

    #你知道实现实现分布式锁有哪些方案?

    出现概率: ★★★★★

    分布式锁无论是在平时开发中还是面试中都是很常见的问题, 所以建议自己多梳理一下。知其然, 且知其所以然。

    1、在开始之前首先要知道什么是分布式锁 ?

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

    在分布式锁方面, Redis有广泛应用, 日常开发中分布式锁的一些常见常见有秒杀下单、抢红包等等。

    2、分布式锁的特点如下:

    • 互斥性:和我们本地锁一样互斥性是最基本,但是分布式锁需要保证在不同节点的不同线程的互斥。
    • 可重入性:同一个节点上的同一个线程如果获取了锁之后那么也可以再次获取这个锁。
    • 锁超时:和本地锁一样支持锁超时,防止死锁。
    • 高效,高可用:加锁和解锁需要高效,同时也需要保证高可用防止分布式锁失效,可以增加降级。
    • 支持阻塞和非阻塞:和 ReentrantLock 一样支持 lock 和 trylock 以及 tryLock(long timeOut)。
    • 支持公平锁和非公平锁(可选):公平锁的意思是按照请求加锁的顺序获得锁,非公平锁就相反是无序的。这个一般来说实现的比较少。

    3、一些比较常见的分布式锁方案

    • SETNX + EXPIRE
    • SETNX + value值是(系统时间+过期时间)
    • SET EX PX NX + 校验唯一随机值,再释放锁
    • 多机实现的分布式锁Redlock
    • ZooKeeper实现分布式锁
    • 使用数据库实现分布式锁

    1)、方案一: SETNX和EXPIRE

    伪代码如下:

    if (setnx(key, 1) == 1){
        expire(key, 10)
        try {
            //TODO 业务逻辑
        } finally {
            del(key)
        }
    }
    

    这种方案的优点是优点是实现简单,通过修改过期时间可以支持锁重入,锁超时自动释放;

    缺点:因为上述命令是分两步执行,如果第二步执行失败,将造成无法解锁, 很容易导致死锁。

    2)、方案二: SETNX + value值是(系统时间+过期时间)

    当前时间:  '2022-04-17 11:00:00'.to_time 
    value = '2022-04-17 11:00:10'.to_time // 10s后过期
    if (setnx(key, value) == 1){ // 代码1
        try {
            //TODO 业务逻辑
        } finally {
            del(key)
        }
    }else{
      if  redisClient.get(key_resource_id)  < Time.now { // 表示已过期
         del(key)
         goto 代码1 
      }
    }
    

    优点:加锁是原子操作,解决了方案一的缺点,避免了死锁问题。
    缺点:实现复杂,每个机器的时间必须保持同步,其他加锁线程会修改过期时间,锁有可能被其他线程错误释放。

    3)、SET EX PX NX + 校验唯一随机值,再释放锁

    SETNX和EXPIRE 为两个指令, 不是原子性操作,如果 SETNX 成功,在设置锁超时时间后,服务器挂掉、重启或网络问题等,导致 EXPIRE 命令没有执行,锁没有设置超时时间变成死锁。针对该问题,redis 在2.6.12版本过后增加新的解决方案。

    set key value [expiration EX seconds|PX milliseconds] [NX|XX]
    

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

    比如当key不存在时, 设置10s的锁, 可以这么设置:

    SET product:10001 true  ex  10  nx
    

    如果SET操作成功后,返回的是OK,失败返回NIL

    最后为删除时,防止可能被其他线程误删。可以加锁时设置一下当前线程的一个随机ID, 然后在删除时判断一下。 伪代码如下:

    if(redisClient.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
        try {
            ... //业务处理
        }
     finally {
           //判断是不是当前线程加的锁,是才释放
           if (redisClient.get(key_resource_id) == uni_request_id) {
              redisClient.del(lockKey); //释放锁
            }
       }
    }
    

    这种方案的优点是可以保证加锁的原子性,使用LUA释放锁的话,锁不会被其他线程错误释放。

    缺点:锁没有自动续期机制,锁无法支持重入。不过其实平时开发中, 大部分场景不用考虑自动续期机制。

    4)、多机实现的分布式锁Redlock

    Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫Redlock,此种方式比原先的单节点的方法更安全。

    Redlock 的方案基于 2 个前提:

    • 不再需要部署从库和哨兵实例,只部署主库
    • 但主库要部署多个,官方推荐至少 5 个实例

    整体的流程是这样的,一共分为 5 步:

    a)、客户端先获取「当前时间戳T1」
    b)、客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
    c)、如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
    d)、加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
    e)、加锁失败,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)

    Redlock加锁步骤相对还是比较繁琐的,有点重,官方给出的解释是为了「容错」,部分实例异常宕机,剩余的实例加锁成功,整个锁服务依旧可用。

    其实Redlock一出来, 就受到了业界著名的分布式系统专家Martin的质疑,他马上写了篇文章,质疑这个 Redlock 的算法模型是有问题的,并对分布式锁的设计,提出了自己的看法,之后,Redis 作者 Antirez 面对质疑,不甘示弱,也写了一篇文章,反驳了对方的观点,并详细剖析了 Redlock 算法模型的更多设计细节。这里就不展开细讲了, 感兴趣的朋友可以看看这个文章。(opens new window)

    5)、也可以使用ZooKeeper实现分布式锁

    Zookeeper的节点Znode有四种类型:

    • 持久节点:默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在。
    • 持久节点顺序节点:所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号,持久节点顺序节点就是有顺序的持久节点。
    • 临时节点:和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。
    • 临时顺序节点:有顺序的临时节点。

    Zookeeper分布式锁实现应用了临时顺序节点, 这里大概讲下zk分布式锁的实现原理吧。

    大致思想为:每个客户端对某个方法加锁时,在ZooKeeper上与该方法对应的指定节点的目录下,生成一个唯一的临时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个临时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。

    优点:

    • 有效的解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题
    • 实现较为简单

    缺点:

    • 性能上不如使用缓存实现的分布式锁,因为每次在创建锁和释放锁的过程中,都要动态创建、销毁临时节点来实现锁功能
    • 需要对ZooKeeper的原理有所了解

    这里就不展开细讲ZooKeeper的原理, 后面会专门用一篇文章讲如何用Zookeeper实现分布式锁。

    6)、使用数据库实现分布式锁

    可以使用select ... for update 来实现分布式锁。我们自己的项目,分布式定时任务,就使用类似的实现方案

    优点:

    • 简单,使用方便,不需要引入Redis、zookeeper等中间件。

    缺点:

    • 不适合高并发的场景
    • db操作性能较差,有锁表的风险;

    结语:

    • 从性能角度(从高到低)Redis > Zookeeper >= 数据库;
    • 从理解的难易程度角度(从低到高)数据库 > Redis > Zookeeper;
    • 从实现的复杂性角度(从低到高)Zookeeper > Redis > 数据库;
    • 从可靠性角度(从高到低)Zookeeper > Redis > 数据库。
    • 平时开发中 SET EX PX NX + 校验唯一随机值,再释放锁 方案就可以了。

    这些知识点的导图和问题的答案详解的PDF文档都可以免费分享给大家

     

    展开全文
  • 在学习Java多线程编程的时候,是一个很重要也很基础的概念,可以看成是多线程情况下访问共享资源的一种线程同步机制。这是对于单进程应用而言的,即... 掌握redis分布式锁常见的面试题 以下是课程部分讲义截图:
  • Redis分布式锁原理 分布式,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的...

    Redis分布式锁原理

    分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

    使用setnx、getset、expire、del这4个redis命令实现

    1. setnx 是『SET if Not eXists』(如果不存在,则 SET)的简写。 命令格式:SETNX key value;使用:只在键 key 不存在的情况下,将键 key 的值设置为 value 。若键 key 已经存在, 则 SETNX 命令不做任何动作。返回值:命令在设置成功时返回 1 ,设置失败时返回 0 。
    2. getset 命令格式:GETSET key value,将键 key 的值设为 value ,并返回键 key 在被设置之前的旧的value。返回值:如果键 key 没有旧值, 也即是说, 键 key 在被设置之前并不存在, 那么命令返回 nil 。当键 key 存在但不是字符串类型时,命令返回一个错误。
    3. expire 命令格式:EXPIRE key seconds,使用:为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。返回值:设置成功返回 1 。 当 key 不存在或者不能为 key 设置生存时间时(比如在低于 2.1.3 版本的 Redis 中你尝试更新 key 的生存时间),返回 0 。
    4. del 命令格式:DEL key [key …],使用:删除给定的一个或多个 key ,不存在的 key 会被忽略。返回值:被删除 key 的数量。

    redis 分布式锁原理一

    过程分析:

    1. A尝试去获取锁lockkey,通过setnx(lockkey,currenttime+timeout)命令,对lockkey进行setnx,将value值设置为当前时间+锁超时时间;
    2. 如果返回值为1,说明redis服务器中还没有lockkey,也就是没有其他用户拥有这个锁,A就能获取锁成功;
    3. 在进行相关业务执行之前,先执行expire(lockkey),对lockkey设置有效期,防止死锁。因为如果不设置有效期的话,lockkey将一直存在于redis中,其他用户尝试获取锁时,执行到setnx(lockkey,currenttime+timeout)时,将不能成功获取到该锁;
    4. 执行相关业务;
    5. 释放锁,A完成相关业务之后,要释放拥有的锁,也就是删除redis中该锁的内容,del(lockkey),接下来的用户才能进行重新设置锁新值。

    原理图如下
    在这里插入图片描述
    代码实现:

    public void redis1() {
            log.info("关闭订单定时任务启动");
            long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("lock.timeout", "5000"));
            //这个方法的缺陷在这里,如果setnx成功后,锁已经存到Redis里面了,服务器异常关闭重启,将不会执行closeOrder,也就不会设置锁的有效期,这样的话锁就不会释放了,就会产生死锁
            Long setnxResult = RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, String.valueOf(System.currentTimeMillis() + lockTimeout));
            if (setnxResult != null && setnxResult.intValue() == 1) {
                //如果返回值为1,代表设置成功,获取锁
                closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            } else {
                log.info("没有获得分布式锁:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            }
            log.info("关闭订单定时任务结束");
        }
    private void closeOrder(String lockName) {
            //对锁设置有效期
            RedisShardedPoolUtil.expire(lockName, 5);//有效期为5秒,防止死锁
            log.info("获取锁:{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());
            //执行业务
            int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour", "2"));
            iOrderService.closeOrder(hour);
            //执行完业务后,释放锁
            RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            log.info("释放锁:{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());
            log.info("=================================");
        }
    
    

    缺陷:

    如果A在setnx成功后,A成功获取锁了,也就是锁已经存到Redis里面了,此时服务器异常关闭或是重启,将不会执行closeOrder,也就不会设置锁的有效期,这样的话锁就不会释放了,就会产生死锁。

    解决方法:

    关闭Tomcat有两种方式,一种通过温柔的执行shutdown关闭,一种通过kill杀死进程关闭,使用shutdown关闭tomcat,以下的方法会在关闭前执行,即关闭前删除锁

    //通过温柔的执行shutdown关闭时,以下的方法会在关闭前执行,即可以释放锁,而对于通过kill杀死
    //进程关闭时,以下方法不会执行,即不会释放锁
    //这种方式释放锁的缺点在于,如果关闭的锁过多,将造成关闭服务器耗时过长
      @PreDestroy
      public void delLock() {
          RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
      }
    
    

    redis 分布式锁原理2(优化版)

    为了解决原理1中会出现的死锁问题,提出原理2双重防死锁,可以更好解决死锁问题。
    过程分析:

    1. 当A通过setnx(lockkey,currenttime+timeout)命令能成功设置lockkey时,即返回值为1,过程与原理1一致;
    2. 当A通过setnx(lockkey,currenttime+timeout)命令不能成功设置lockkey时,这是不能直接断定获取锁失败;因为我们在设置锁时,设置了锁的超时时间timeout,当当前时间大于redis中存储键值为lockkey的value值时,可以认为上一任的拥有者对锁的使用权已经失效了,A就可以强行拥有该锁;具体判定过程如下;
    3. A通过get(lockkey),获取redis中的存储键值为lockkey的value值,即获取锁的相对时间lockvalueA
    4. lockvalueA!=null && currenttime>lockvalue,A通过当前的时间与锁设置的时间做比较,如果当前时间已经大于锁设置的时间临界,即可以进一步判断是否可以获取锁,否则说明该锁还在被占用,A就还不能获取该锁,结束,获取锁失败;
    5. 步骤4返回结果为true后,通过getSet设置新的超时时间,并返回旧值lockvalueB,以作判断,因为在分布式环境,在进入这里时可能另外的进程获取到锁并对值进行了修改,只有旧值与返回的值一致才能说明中间未被其他进程获取到这个锁
    6. lockvalueB == null || lockvalueA==lockvalueB,判断:若果lockvalueB为null,说明该锁已经被释放了,此时该进程可以获取锁;旧值与返回的lockvalueB一致说明中间未被其他进程获取该锁,可以获取锁;否则不能获取锁,结束,获取锁失败。

    原理图如下:
    在这里插入图片描述

    代码实现:

    public void redis2() {
            log.info("关闭订单定时任务启动");
            long lockTimeout = Long.parseLong(PropertiesUtil.getProperty("lock.timeout", "5000"));
            Long setnxResult = RedisShardedPoolUtil.setnx(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, String.valueOf(System.currentTimeMillis() + lockTimeout));
            if (setnxResult != null && setnxResult.intValue() == 1) {
                //如果返回值为1,代表设置成功,获取锁
                closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            } else {
                //未获取到锁,继续判断,判断时间戳,看是否可以重置并获取到锁
                String lockValueStr = RedisShardedPoolUtil.get(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                //通过当前的时间与锁设置的时间做比较,如果当前时间已经大于锁设置的时间临界,即可以进一步判断是否可以获取锁,否则说明该锁还在被占用,不能获取该锁
                if (lockValueStr != null && System.currentTimeMillis() > Long.parseLong(lockValueStr)) {
                    //通过getSet设置新的超时时间,并返回旧值,以作判断,因为在分布式环境,在进入这里时可能另外的进程获取到锁并对值进行了修改,只有旧值与返回的值一致才能说明中间未被其他进程获取到这个锁
                    String getSetResult = RedisShardedPoolUtil.getSet(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, String.valueOf(System.currentTimeMillis() + lockTimeout));
                    //再次用当前时间戳getset。
                    //返回给定的key的旧值,与旧值判断,是否可以获取锁
                    //当key没有旧值时,即key不存在时,返回nil ->获取锁
                    //这里我们set了一个新的value值,获取旧的值。
                    //若果getSetResult为null,说明该锁已经被释放了,此时该进程可以获取锁;旧值与返回的getSetResult一致说明中间未被其他进程获取该锁,可以获取锁
                    if (getSetResult == null || (getSetResult != null && StringUtils.equals(lockValueStr, getSetResult))) {
                        //真正获取到锁
                        closeOrder(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                    } else {
                        log.info("没有获得分布式锁:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                    }
                } else {
                    log.info("没有获得分布式锁:{}", Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
                }
            }
            log.info("关闭订单定时任务结束");
        }
        private void closeOrder(String lockName) {
            //对锁设置有效期
            RedisShardedPoolUtil.expire(lockName, 5);//有效期为5秒,防止死锁
            log.info("获取锁:{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());
            //执行业务
            int hour = Integer.parseInt(PropertiesUtil.getProperty("close.order.task.time.hour", "2"));
            iOrderService.closeOrder(hour);
            //执行完业务后,释放锁
            RedisShardedPoolUtil.del(Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK);
            log.info("释放锁:{},ThreadName:{}",Const.REDIS_LOCK.CLOSE_ORDER_TASK_LOCK, Thread.currentThread().getName());
            log.info("=================================");
        }
    
    

    优化点:

    加入了超时时间判断锁是否超时了,及时A在成功设置了锁之后,服务器就立即出现宕机或是重启,也不会出现死锁问题;因为B在尝试获取锁的时候,如果不能setnx成功,会去获取redis中锁的超时时间与当前的系统时间做比较,如果当前的系统时间已经大于锁超时时间,说明A已经对锁的使用权失效,B能继续判断能否获取锁,解决了redis分布式锁的死锁问题。

    Redisson实现Redis分布式锁的底层原理

    在这里插入图片描述

    (1)加锁机制

    咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。

    这里注意,仅仅只是选择一台机器!这点很关键!

    紧接着,就会发送一段lua脚本到redis上,那段lua脚本如下所示:
    在这里插入图片描述
    为啥要用lua脚本呢?

    因为一大坨复杂的业务逻辑,可以通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性。(lua脚本的原子性见文章最后补充内容)

    那么,这段lua脚本是什么意思呢?

    KEYS[1]代表的是你加锁的那个key,比如说:
    
    RLock lock = redisson.getLock("myLock");
    

    这里你自己设置了加锁的那个锁key就是“myLock”。

    **ARGV[1]**代表的就是锁key的默认生存时间,默认30秒。
    
    **ARGV[2]**代表的是加锁的客户端的ID,类似于下面这样:
    
    8743c9c0-0795-4907-87fd-6c719a6b4586:1
    

    给大家解释一下,第一段if判断语句,就是用“exists myLock”命令判断一下,如果你要加锁的那个锁key不存在的话,你就进行加锁。

    如何加锁呢?很简单,用下面的命令:

    hset myLock 8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
    

    通过这个命令设置一个hash数据结构,这行命令执行后,会出现一个类似下面的数据结构:
    在这里插入图片描述
    上述就代表“8743c9c0-0795-4907-87fd-6c719a6b4586:1”这个客户端对“myLock”这个锁key完成了加锁。

    接着会执行“pexpire myLock 30000”命令,设置myLock这个锁key的生存时间是30秒。

    好了,到此为止,ok,加锁完成了。

    (2)锁互斥机制

    那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?

    很简单,第一个if判断会执行“exists myLock”,发现myLock这个锁key已经存在了。

    接着第二个if判断,判断一下,myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。

    所以,客户端2会获取到pttl myLock返回的一个数字,这个数字代表了myLock这个锁key的**剩余生存时间。**比如还剩15000毫秒的生存时间。

    此时客户端2会进入一个while循环,不停的尝试加锁。

    (3)watch dog自动延期机制

    客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

    简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

    (4)可重入加锁机制

    那如果客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢?

    比如下面这种代码:
    在这里插入图片描述
    这时我们来分析一下上面那段lua脚本。

    第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。

    第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”

    此时就会执行可重入加锁的逻辑,他会用:

    incrby myLock 8743c9c0-0795-4907-87fd-6c71a6b4586:1 1
    

    通过这个命令,对客户端1的加锁次数,累加1。

    此时myLock数据结构变为下面这样:
    在这里插入图片描述
    大家看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数

    (5)释放锁机制

    如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。

    其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。

    如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:

    “del myLock”命令,从redis里删除这个key。

    然后呢,另外的客户端2就可以尝试完成加锁了。

    这就是所谓的分布式锁的开源Redisson框架的实现机制。

    一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。

    Redis分布式锁缺点

    其实上面那种方案最大的问题,就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。

    但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。

    接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。

    此时就会导致多个客户端对一个分布式锁完成了加锁。

    这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。

    所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:

    在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。

    补充-lua脚本的原子性:

    Redis使用同一个Lua解释器来执行所有命令,同时,Redis保证以一种原子性的方式来执行脚本:当lua脚本在执行的时候,不会有其他脚本和命令同时执行,这种语义类似于 MULTI/EXEC。从别的客户端的视角来看,一个lua脚本要么不可见,要么已经执行完。

    然而这也意味着,执行一个较慢的lua脚本是不建议的,由于脚本的开销非常低,构造一个快速执行的脚本并非难事。但是你要注意到,当你正在执行一个比较慢的脚本时,所以其他的客户端都无法执行命令。

    学习参考:
    https://blog.csdn.net/dazou1/article/details/88088223?ops_request_misc=%25257B%252522request%25255Fid%252522%25253A%252522160862098916780276355198%252522%25252C%252522scm%252522%25253A%25252220140713.130102334…%252522%25257D&request_id=160862098916780276355198&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-1-88088223.first_rank_v2_pc_rank_v29&utm_term=redis分布式锁

    https://blog.csdn.net/qq_43778308/article/details/111058472?ops_request_misc=&request_id=&biz_id=102&utm_term=redis%25E5%2588%2586%25E5%25B8%2583%25E5%25BC%258F%25E9%2594%2581&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-1-111058472.first_rank_v2_pc_rank_v29

    展开全文
  • Redis面试题-Redis分布式锁

    千次阅读 2021-04-01 13:29:25
    Redis分布式锁 什么是分布式 要介绍分布式,首先要提到与分布式锁相对应的是线程、进程。 线程:主要用来给方法、代码块加锁。当某个方法或代码使用,在同一时刻仅有一个线程执行该方法或该代码段。线程...

    本文参考 嗨客网 Redis面试题

    Redis分布式锁

    什么是分布式锁

    要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。

    线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一 JVM 中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如 synchronized 是共享对象头,显示锁 Lock 是共享某个变量(state)。

    进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过 synchronized 等线程锁实现进程锁。

    分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。

    分布式锁的使用场景

    线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。

    有这样一个情境,线程 A 和线程 B 都共享某个变量 X。如果是单机情况下(单 JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。如果是分布式情况下(多 JVM),线程 A 和线程 B 很可能不是在同一 JVM 中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。

    分布式锁实现

    说明

    分布式锁一般有三种实现方式即,数据库乐观锁、基于 Redis 的分布式锁和基于 ZooKeeper 的分布式锁。

    可靠性

    首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

    1. 互斥性。在任意时刻,只有一个客户端能持有锁。
    2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
    3. 具有容错性。只要大部分的 Redis 节点正常运行,客户端就可以加锁和解锁。
    4. 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

    代码实现

    组件依赖

    首先我们要通过 Maven 引入 Jedis 开源组件,在 pom.xml 文件加入下面的代码:

    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>2.9.0</version>
    </dependency>
    

    加锁代码

    正确姿势

    Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:

    public class RedisTool {
    
        private static final String LOCK_SUCCESS = "OK";
        private static final String SET_IF_NOT_EXIST = "NX";
        private static final String SET_WITH_EXPIRE_TIME = "PX";
    
        /**
         * 尝试获取分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @param expireTime 超期时间
         * @return 是否获取成功
         */
        public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
    
            String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
    
            if (LOCK_SUCCESS.equals(result)) {
                return true;
            }
            return false;
    
        }
    }
    

    可以看到,我们加锁就一行代码:

    jedis.set(String key, String value, String nxxx, String expx, int time)
    

    这个 set() 方法一共有五个形参:

    1. 第一个为 key,我们使用 key 来当锁,因为 key 是唯一的。
    2. 第二个为 value,我们传的是 requestId,很多童鞋可能不明白,有 key 作为锁不就够了吗,为什么还要用到 value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给 value 赋值为 requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId 可以使用 UUID.randomUUID().toString() 方法生成。
    3. 第三个为 nxxx,这个参数我们填的是 NX,意思是 SET IF NOT EXIST,即当 key 不存在时,我们进行 set 操作;若 key 已经存在,则不做任何操作;
    4. 第四个为 expx,这个参数我们传的是 PX,意思是我们要给这个 key 加一个过期的设置,具体时间由第五个参数决定。
    5. 第五个为 time,与第四个参数相呼应,代表 key 的过期时间。

    总的来说,执行上面的 set() 方法就只会导致两种结果:

    1. 当前没有锁(key 不存在),那么就进行加锁操作,并对锁设置个有效期,同时 value 表示加锁的客户端。
    2. 已有锁存在,不做任何操作。

    心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set() 加入了 NX 参数,可以保证如果已有 key 存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。

    其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即 key 被删除),不会发生死锁。

    最后,因为我们将 value 赋值为 requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑 Redis 单机部署的场景,所以容错性我们暂不考虑。

    错误示例1

    比较常见的错误示例就是使用 jedis.setnx() 和 jedis.expire() 组合实现加锁,代码如下:

    public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
    
        Long result = jedis.setnx(lockKey, requestId);
        if (result == 1) {
            // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
            jedis.expire(lockKey, expireTime);
        }
    
    }
    

    setnx() 方法作用就是 SET IF NOT EXIST,expire() 方法就是给锁加一个过期时间。乍一看好像和前面的 set() 方法结果一样,然而由于这是两条 Redis 命令,不具有原子性,如果程序在执行完 setnx() 之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的 jedis 并不支持多参数的 set() 方法。

    错误示例2

    这一种错误示例就比较难以发现问题,而且实现也比较复杂。实现思路:使用 jedis.setnx() 命令实现加锁,其中key 是锁,value 是锁的过期时间。执行过程:

    1. 通过 setnx() 方法尝试加锁,如果当前锁不存在,返回加锁成功。
    2. 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。

    代码如下:

    public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {
    
        long expires = System.currentTimeMillis() + expireTime;
        String expiresStr = String.valueOf(expires);
    
        // 如果当前锁不存在,返回加锁成功
        if (jedis.setnx(lockKey, expiresStr) == 1) {
            return true;
        }
    
        // 如果锁存在,获取锁的过期时间
        String currentValueStr = jedis.get(lockKey);
        if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
            // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
            String oldValueStr = jedis.getSet(lockKey, expiresStr);
            if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
                // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
                return true;
            }
        }
            
        // 其他情况,一律返回加锁失败
        return false;
    }
    

    那么这段代码问题在哪里?

    1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。
    2. 当锁过期的时候,如果多个客户端同时执行 jedis.getSet() 方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。
    3. 锁不具备拥有者标识,即任何客户端都可以解锁。

    解锁代码

    正确姿势

    还是先展示代码,再带大家慢慢解释为什么这样实现:

    public class RedisTool {
    
        private static final Long RELEASE_SUCCESS = 1L;
    
        /**
         * 释放分布式锁
         * @param jedis Redis客户端
         * @param lockKey 锁
         * @param requestId 请求标识
         * @return 是否释放成功
         */
        public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
    
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
    
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
    
        }
    
    }
    

    可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的 Lua 脚本代码。第二行代码,我们将 Lua 代码传到 jedis.eval() 方法里,并使参数 KEYS[1] 赋值为 lockKey,ARGV[1] 赋值为 requestId。eval() 方法是将 Lua 代码交给 Redis 服务端执行。

    那么这段 Lua 代码的功能是什么呢?其实很简单,首先获取锁对应的 value 值,检查是否与 requestId 相等,如果相等则删除锁(解锁)。那么为什么要使用 Lua 语言来实现呢?因为要确保上述操作是原子性的。那么为什么执行 eval() 方法可以确保原子性,源于 Redis 的特性,下面是官网对 eval 命令的部分解释:

    简单来说,就是在 eval 命令执行 Lua 代码的时候,Lua 代码将被当成一个命令去执行,并且直到 eval 命令执行完成,Redis 才会执行其他命令。

    错误示例1

    最常见的解锁代码就是直接使用 jedis.del() 方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

    public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
        jedis.del(lockKey);
    }
    

    错误示例2

    这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

    public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
            
        // 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            // 若在此时,这把锁突然不是这个客户端的,则会误解锁
            jedis.del(lockKey);
        }
    }
    

    如代码注释,问题在于如果调用 jedis.del() 方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?

    答案是肯定的,比如客户端 A 加锁,一段时间之后客户端 A 解锁,在执行 jedis.del() 之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端 A 再执行 del() 方法,则将客户端 B 的锁给解除了。

    更多

    原文链接链接

    其他目录

    更多文章,可以关注下方公众号:

    嗨客网(www.haicoder.net)

    展开全文
  • 面试题——Redis分布式锁

    千次阅读 2020-07-16 23:36:55
    先在redis中存入一个ticket,值为50,作为被抢的票。 抢票接口: @RestController public class TestController { @Autowired StringRedisTemplate template; @GetMapping("/test") public String test() ...
  • 1. 事务简介 2. 事务操作 3. 分布式锁 4. redssion
  • Redis分布式锁,蚂蚁金服面试题

    千次阅读 2019-10-17 11:09:39
    1. 什么是分布式锁 分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种实现。如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止批次干扰。 分布式锁可以保证分布式...
  • 采用Redis的官方推荐的Redisson作为redis分布式锁redis重点面试题 redis的应用场景。 (1)作为缓存 (2)分布式锁 今天就来介绍分布式锁的使用场景。 采用Redis的官方推荐的Redisson作为redis分布式锁redis...
  • 使用Redis分布式锁的详细方案是什么? 一个很简单的答案就是去使用 Redission 客户端。Redission 中的方案就是 Redis 分布式的比较完美的详细方案。 那么,Redission 中的方案为什么会比较完美呢? 正好,我...
  • redis分布式锁实现-面试题

    万次阅读 2018-11-05 09:52:54
    3 释放(传入key和value,比较value,若相同,则删除key) distributedLocker.unlock(BalanceAccountConstants.AccountRedisKey.LOCKER_BALANCE_ACCOUNT_LOCK, lockerId); public void unlock(String...
  • Redis 分布式缓存 面试题重点 总结 常用数据类型 String 类型面试分析 博客的字数统计如何实现?(strlen) 如何将审计日志不断追加到指定key? (append) 你如何实现一个分布式自增id?(incr-雪花算法) 如何实现一个...
  • redis分布式锁面试题

    千次阅读 多人点赞 2020-07-09 16:35:45
    首先想想为什么要有分布式锁? 保证一个方法在高并发情况下的同一时间只能被同一个线程执行。 的用途? (1)允许多个客户端操作共享资源 这种情况下,对共享资源...redis分布式锁如何实现? 正常情况下redis使用setn
  • 13.分布式锁面试题(高频面试题

    万次阅读 2021-08-11 10:18:05
    面试经常被问到分布式,今天我就带大家深入剖析下分布式的各种方案,redis分布式锁,zookeeper分布式等等,我给大家深入剖析每种解决方案的优缺点以及应用场景。
  • 对同一个资源进行操作,单一的缓存读取没问题了,但是存在并发的时候怎么办呢,为了避免数据不一致,我们需要在操作共享资源之前进行...图2:分布式站点使用分布式锁 当然我们暂时用不了这么复杂的场景,我们就简..
  • 本文先回顾的概念,再介绍分布式锁,以及如何用Redis来实现分布式锁。 一、的基本了解 首先,回顾一下我们工作学习中的的概念。 为什么要先讲再讲分布式锁呢? 我们都清楚,的作用是要解决多线程对...
  • 二、Redisson实现Redis分布式锁的底层原理 (1)加锁机制 (2)互斥机制 (3)watch dog自动延期机制 (4)可重入加锁机制 (5)释放机制 (6)上述Redis分布式锁的缺点 三、手动实现redis分布式锁 一、...
  • 分布式锁是控制分布式系统之间同步访问共享资源的一种方式。 在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来...
  • 前言 之前的个人网站已经下线了,https://upheart.cn/,维护太花时间了,之后会把...使用分布式锁的目的,无外乎就是保证同一时间只有一个客户端可以对共享资源进行操作 我们在分布式应用进行逻辑处理时经常会遇到并发
  • 面试难题:Redis 分布式锁,真的完美无缺吗?
  • 高频面试Redis分布式锁

    千次阅读 2021-09-02 14:31:41
    Redis实现分布式锁的方式有很多,可借助setnx、lua等方式实现。但是现实往往要考虑更多的问题。如超时时间、可重入、加锁形式、的安全释放、的自动延期等等。所以我们重点分析Redssion的分布式锁实现。 2、...
  • redis分布式锁的方案,无论用何种方式实现都会有续约问题与集群同步延迟问题。 总的来说,是一个不太靠谱的方案。如果追求高正确率,不能采用这种方案。 但是它也有优点,就是比较简单,在某些非严格要求的场景是...
  • Redis分布式锁的实现原理(面试

    千次阅读 2020-07-17 11:11:38
    如果在公司里落地生产环境用分布式的时候,一定是会用开源类库的,比如Redis分布式锁,一般就是用Redisson框架就好了,非常的简便易用。 可以去看看Redisson的官网,看看如何在项目中引入Redisson的依赖,然后...
  • 这几年一直在it行业里摸爬滚打,一路走来,不少总结了一些python行业里的高频面试,看到大部分初入行的新鲜血液,还在为各样的面试题答案或收录有各种困难问题 于是乎,我自己开发了一款面试宝典,希望能帮到大家,...
  • 其实上面那种方案最大的问题,就是如果你对某个 redis master 实例,写入了 myLock 这种 key 的 value,此时会异步复制给对应的 master slave 实例。但是这个过程中一旦发生 redis m aster 宕机,主备切换,redis ...
  • redis分布式锁如何实现 使用set(String key, String value, String nxxx, String expx, long time)方法; 方法参数详解: key 的key值不做过多解释 value 很多同学会问弄个还要value干啥?value可以去控制谁能...
  • 并非是一个工具,而是redis官方提出的一种分布式锁的算法。 就在刚刚介绍完的redisson中,就实现了redLock版本的。也就是说除了getLock方法,还有getRedLock方法。 笔者大概画了一下对红的理解: 如果你不...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 46,190
精华内容 18,476
关键字:

redis分布式锁面试题

友情链接: GaussSmooth.rar