暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Redis分布式锁的实现

阿哲是哲学的哲 2021-02-05
1517

在高可用系统中, 加锁已经是常态, 分布式锁也是面试的热点. 

既然如此重要, 那就必须熟练的掌握对吧

一. 什么是分布式锁

  1. 问题描述

    在分布式环境下, 我们常常需要解决的是, 多台机器对同个资源的争抢问题

例如: 1、库存超卖 2、防止用户重复下单 3、MQ消息去重 4、订单操作变更  等等... 这些情况需要我们对某个资源进行串行化操作

  • 单机情况下, 我们可以简单的通过关键字 synchronize、或者 读写锁 的操作使得流程串行化
  • 多节点、多进程. 就得使用分布式锁了
  1. 具体流程图如下

  1. 分布式锁的状态

    获取锁: 各个客户端通过竞争获取锁到锁, 才能对共享的资源进行操作

    占有锁: 操作资源的时间段, 客户端一直会持有这个锁, 以保证别的节点获取不到锁

    阻塞: 当其他节点获取不到锁的时候, 就会进入阻塞状态, 无法对这个资源操作

    释放锁: 资源操作完毕后, 持有锁的客户端, 将会释放锁资源. 让其他节点可以获取锁

  2. 分布式锁特点

    • 互斥性: 任意时刻只能有一个客户端持有锁
    • 高可用, 容错性: 锁服务应该做集群高可用, 保证所有客户端能正常的获取释放锁
    • 避免死锁: 锁机制不能无限时间锁住资源, 应该有一定的机制自动释放锁 (正常释放或者超时释放)
    • 解铃还须系铃人: 加锁解锁要保证同一个客户端所为

二. 分布式锁的实现

分布式锁实现有很多种产品可以实现, 其中比较主流的有

  • 基于zookeeper时节点的分布式锁
  • 基于Redis的分布式锁 (基于内存, 性能突出)
  • DB数据库 实现  (性能较差, 但是原子性可靠性较好)

其中较为大家熟知的是用Redis, 我相信大家也是用Redis去做的对吧, 所以本文也是介绍Redis去实现

1. 获取锁 tryLock

  • Redis2.6.12版本之前,使用Lua脚本保证原子性

    由于 2.6.12 版本之前没有 setnx 命令, 也即是 1.查看是否有 2.设置值 是两步操作, 如果使用代码去执行的话, 将会是两个指令, 这不满足操作的原子性.

    而使用 jedis.eval(script, keys, argv), 我们可以传入 Lua 脚本来保证两个操作的原子性, 因为Lua脚本再执行的时候, 是可以保证所有Lua脚本全部执行完毕, 才继续执行其他指令的.
    注意: 参数 key 是LIst<String>  他的第一位对应 Lua 脚本中的 KEYS[1] , argv同理

  • 当然你也可以利用这个特性, 编写一段具有事物特性的Redis指令块, 并不一定只拿来做锁

    (关于 Lua 脚本脚本语言, 本文不会深入讲解, 感兴趣的小伙伴可以上菜鸟教程看看....)

  • 以下是我简单写的一段模仿SETNX 的LUA代码, 并且可以直接设置过期时间 (代码写得有些仓促, 有些许地方还需要优化, 大家将就看看)

    -- 如果不存在值, 则设置值并且设置过期时间 返回1 , 存在值则返回0
    if redis.call('EXISTS',KEYS[1]) == 0 then
    redis.call('SET',KEYS[1],ARGV[1])
    redis.call('EXPIRE',KEYS[1],ARGV[2])
    return 1
    else
    return 0
    end
    • 客户端执行
      @Test
      public void test() throws InterruptedException {
      Jedis jedis = jedisPool.getResource();
      for(int i = 0 ;i<=10; i++){
      System.out.println("此时的key" + jedis.get("REDIS_TEST_KEY"));
      Long eval = (Long) jedis.eval(LUA, Arrays.asList("REDIS_TEST_KEY"), Arrays.asList("value","1")); // 设置锁的值为: value, 过期时间为1s
      if (0l == eval){
      System.out.println("获取锁失败!");
      }else {
      System.out.println("获取锁成功" + jedis.get("REDIS_TEST_KEY"));
      }
      Thread.sleep(500l);
      }
      }


      控制台打印


      此时的keynull
      获取锁成功value
      此时的keyvalue
      获取锁失败!
      此时的keynull
      获取锁成功value
      此时的keyvalue
      获取锁失败!
      此时的keynull
      获取锁成功value
      此时的keyvalue
      • 增加重试机制
        @Test
        public void testTryLock() throws InterruptedException {
        for(int i = 0 ;i<=10; i++){
        if (tryLock("REDIS_TEST_KEY",1000l,0l,3)){
        System.out.println("取锁成功");
        }else{
        System.out.println("取锁失败");
        }
        }
        }


        /**
        * @param key 解锁 key
        * @param weitTime 最多等待时间(单位:毫秒)
        * @param leaseTime 上锁后自动释放锁时间(单位:毫秒)
        */
        public Boolean tryLock(final String key,
        final Long weitTime,
        final Long nowTime,
        final int leaseTime){
        Long DEF_WEIT = 500L; //默认每次叠加0.5s的等待时间
        //尝试去获取设置锁标志
        Jedis jedis = jedisPool.getResource();
        Long lock = (Long) jedis.eval(LUA, Arrays.asList(key), Arrays.asList("value",String.valueOf(leaseTime)));
        // Redis2.6.12 之后也可以直接使用SETNX 进行设值
        //long lock = jedis.setnx(key, "value");
        //if (01 == lock){
        // jedis.expire(key, leaseTime);
        //}
        jedis.close(); //释放资源
        if (0l == lock){
        if (weitTime >= nowTime) { //如果还未超过等待时间
        //获取锁失败
        try {
        Thread.sleep(DEF_WEIT);
        LOG.info("【加锁】等待重试:{}",nowTime);
        } catch (InterruptedException e) {
        LOG.warn("【加锁】【等待时间】等待失效,key={}", key);
        }
        return tryLock(key,weitTime, nowTime + DEF_WEIT, leaseTime);
        }
        return Boolean.FALSE;
        }else {
        return Boolean.TRUE;
        }
        }




        控制台打印


        取锁成功
        [2020-09-05 23:58:37.411] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:0
        [2020-09-05 23:58:37.928] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:500
        [2020-09-05 23:58:38.442] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:1000
        取锁失败
        [2020-09-05 23:58:38.970] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:0
        [2020-09-05 23:58:39.484] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:500
        [2020-09-05 23:58:39.999] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:1000
        取锁成功
        [2020-09-05 23:58:40.527] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:0
        [2020-09-05 23:58:41.041] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:500
        [2020-09-05 23:58:41.556] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:1000
        取锁失败
        [2020-09-05 23:58:42.085] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:0
        [2020-09-05 23:58:42.600] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:500
        [2020-09-05 23:58:43.115] INFO cn.test.TestRedisLuaLock - 【加锁】等待重试:1000
        ........


        • 因为我们在tryLock方法内 , 没有主动的去使用 del 命令去删除锁. 所以锁只有被过期失效掉, 或者是正常业务的结束被删除.

          这也符合了 解铃还须系铃人的原则了 (当然铃铛挂久了也会老化, 就自己失效调咯)

        三. 总结

        Redis 分布式锁的实现的主要思路是, 设置成功就返回设置结果 的 思路. 这个SETNX不谋而合, SETNX命令就是: 如果设置成功则返回1.

        当然在版本2.6.1的Redis是没有SETNX命令的, 这个时候就使用Lua脚本来帮助我们将多个命令合并成一个原子性的操作

        Redis的过期机制, 主要是防止单一程序长时间占用资源, 或者是不正常的结束进程. 导致锁没有正常的释放.

        重试机制: 主要是在特短时间内, 允许线程阻塞等待一段时间, 直至取锁成功


        关于分布式锁暂时说这么多


        小编熬夜码字不易

        需要好心人点点关注鼓励鼓励

        实在不行点个👍也还行


        祝各位好梦



        往期回顾


        反向操作-Eurka的读写锁

        Collection 太快受不了 - 集合迭代稳定性

        文章转载自阿哲是哲学的哲,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

        评论