利用redis实现分布式锁

一. 对于分布式的应用,一定程度上会增加处理的速度。但是也会带来一些分布式上的麻烦,比如有个需求:后台程序部署在多台服务器上,client向该后台程序发送参数为 用户账号和 账号类型 的rpc请求,后台程序需要返回该账号对应的身份信息(逻辑很简单,先判断库中有没有该账号信息,有就返回,没有就新生成一个新的身份信息 返回)。设想如果多个client 同时发送多个一样的账号和账号类型 到后台程序,由于同时查库没有该账号信息,这样岂不是要新生成多个不同的身份信息。解决该问题也许可以不通过分布式锁(项目中不是用的分布式锁,但不属于本文的内容的范畴,故省略),下面介绍用redis 实现分布式锁。

二 .不安全的做法

       使用jedis.setnx(key,value) . 伪代码如下

                if(jedis.setnx(key,value)  ==1 ){  //get the lock

                      jedis.expire(key,timeout)  ;  //设置锁超时时间

                     try{

                           do something ..

                    }finally{

                         jedis.del(key) ; //删除(释放)  锁

                    }
 

                  

           }  

    使用该做法有3点不安全的隐患

      隐患1: setnx  和 expire 不是同步的,如果刚setnx完成,还没来得及 expire key ,就宕机了,那该锁就 是“长生不老的”

      隐患2: del 可能是删除 “别人”设置的锁,由于自己执行任务时间的比较长,设置的锁因为超时已经过期了。这个时候别的线程已经拿到该锁,那删除的时候,锁的别的线程设置的。

     隐患3: 可能出现 多个线程并存的情况。如果 thread1 拿到锁,设置的锁已经过期了,但是还没有执行完成 。 thread2 访问拿到了锁 。这样thread1 和 thread2 同时存在。

 

    上述隐患解决方案:

     隐患1解决方案 : 如果向要setnx 和expire 是原子操作,可以使用jedis.set(key,value,nxxx,expx,time).)(redis2.6以上) 。具体的用法可以自己查询baidu/google

    隐患2解决方案  : 将set(key,value.nxxx,expx,time)中的value 设置为 Thread.currentThread.getId().即根据该value判断锁是否是自己设置的。如果是自己设置才删除

      伪代码:

          if(Thead.currentThread.getId().equal(jedis.get(key))){ //步骤1

                 del(key) ;  //步骤2

          }

     有人可能会说,执行步骤1 判断的时候锁过期了,“别人”拿到了锁,那删除的时候也删除了别人的锁了,即要保持 步骤1 判断和步骤2 删除也是原子性的,这个可以做到吗?答案是肯定的。lua脚本如下:

String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(luaScript, Collections.singletonList(key),Collections.singletonList(Thread.currentThead.getId()));

代码的大致意思和伪代码差不多,只不过该过程是原子的。

 

       隐患3解决方案:出现隐患3的根本原因是,执行时间过长,超出了过期时间。如果我们再开启一个守护线程,在某个拿到锁的线程快要过期的时候时候延长 过期时间, 保证锁不会由于过期时间而删除,是要由执行del 命令删除的(宕机情况例外)

 

三 . 代码部分

    

   public boolean getLock(String value){
        Jedis jedis = null;
        boolean isGetLock = false;
        try {
             jedis =  redisPoolServer.getJedis();
             String result = jedis.set("key",value,"nx","ex",20);
             if(result != null){
                 isGetLock = true;
             }
             return isGetLock;
        }catch(Exception e ){
            e.printStackTrace();
            return false;
        }finally {
            if(jedis != null){
                jedis.close();
            }
        }

    }


    private void delLock(String threadId){
        Jedis jedis = null;
        try{
            jedis =  redisPoolServer.getJedis();
            String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            jedis.eval(luaScript, Collections.singletonList("key"),Collections.singletonList(threadId));
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            if(jedis != null){
                jedis.close();
            }
        }
    }

    //守护线程 获取存储的value值
    private String getLockValue(){
        Jedis jedis = null;
        try{
            jedis =  redisPoolServer.getJedis();
            jedis.select(15);
            return jedis.get("key");
        }catch(Exception e){
            e.printStackTrace();
            return null;
        }finally {
            if(jedis != null){
                jedis.close();
            }
        }
    }

    //守护线程 获取key的剩余时间
    private Long getTtl(){
        Jedis jedis = null;
        try{
            jedis =  redisPoolServer.getJedis();
            return jedis.ttl("key");
        }catch(Exception e){
            e.printStackTrace();
            return null;
        }finally {
            if(jedis != null){
                jedis.close();
            }
        }
    }

    //守护线程发现快要过期,延长锁的过期时间
    private void spanExpireTime(int seconde){
        Jedis jedis = null;
        try{
            jedis =  redisPoolServer.getJedis();
            jedis.expire("key",seconde);
        }catch(Exception e){
            e.printStackTrace();
        }finally {
            if(jedis != null){
                jedis.close();
            }
        }
    }

    private  class SubThread implements Runnable{
        
        public void run() {
            //如果拿到锁,执行了较长的时间,超出过期时间,那么别的线程会得到该锁,那么这个时候就同时有多个线程同时访问。为了防止这种情况当达到过期时间的90%时 ,延长过期时间
            System.out.println("subThread:"+Thread.currentThread().getName());
            Thread daemonThead = new Thread(new DaemonThred(Thread.currentThread()));
            daemonThead.setDaemon(true);
            daemonThead.start();

            try {
                while (true) {
                    boolean isGetLock =  getLock(new Long(Thread.currentThread().getId()).toString());
                    if(isGetLock){
                        System.out.println(Thread.currentThread().getName()+new Long(Thread.currentThread().getId()).toString()+"get the redis lock ,doing something");
                        Thread.sleep(5000); //do somethings
                       
                        //判断和删除同步
                        delLock(new Long(Thread.currentThread().getId()).toString());
                        Thread.sleep(1000);

                    }else{
                        System.out.println(Thread.currentThread().getName()+new Long(Thread.currentThread().getId()).toString()+"do not get the redis lock.sleep 1s");
                        Thread.sleep(1000);
                    }

                }
            }catch(Exception e ){
                 e.printStackTrace();
            }
        }
    }

    private class DaemonThred implements Runnable{
        private Thread userThead;
        public DaemonThred(Thread userThead){
            this.userThead = userThead;
        }
        public void run() {
            while(true && userThead.isAlive()){
                Long ttl = getTtl();
                if((ttl != null) && (ttl+2 !=0) && (ttl < 20*0.1) && new Long(userThead.getId()).toString().equals(getLockValue()) ){
                    //设置的ttl值剩下不到 10% 了 ,延迟该key的时间
                    //log info : 一般情况是不会达到过期时间的,可以打印日志,便于分析情况
                    spanExpireTime(10);
                }else{
                    try {
                        Thread.sleep(100);
                    }catch(Exception e){
                        e.printStackTrace();
                    }
                }
            }
        }
    }

 

四 . 后续

    4.1 该代码不是正式线上的代码,只是写的一个demo,由好的实现方式或者问题,欢迎吐槽。

    4.2 大家可能会问,守护线程 的判断剩余时间、获取value值   和  设置延长过期时间  不是原子操作,会不会存在和隐患2中 判断是否自己设置的锁 和后续 删除锁一样的问题 --不是原子操作的问题 ?  我这里设置的锁 过期时间是20s,还有2s的时候我就延长过期时间,也就是 守护线程 有足够的时间 执行剩余时间判断 和 执行延长过期时间的操作 。一般来说,如果使用分布式锁, thread访问很快。不会等到后台守护线程去延长过期时间 。如果线程 很多时候 get 锁 do somthings 需要花费很长时间。这个时候需要考虑下是不是需要分布式锁,或者架构上的设计是否有改善的地方。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值