POOPE 发表于 2021-7-9 11:48:03

redis-分布式锁

  
  redis-分布式锁

redis实现分布式锁四大条件:

  

[*]互斥:key
[*]不能死锁:过期时间
[*]解铃还须系铃人:value存储 uuid+threadId
[*]容错: 关于容错,redis集群环境下是没办法保证分布式锁的容错性的,具体原因如下:
如图所示:一个三主三从的redis集群,当客户端发送写命令时,master会直接返回给用户写成功,并不会等master把命令复制到slave上再返回给用户,也就是说redis的复制是异步复制的,这会导致一个问题:
试想当master把锁信息写入成功,返回给用户了,此时master挂掉了,slvae变成master,但是由于slave没有锁信息,导致第二个线程进来加锁成功。
  解决方案: RedLock, 但其实我个人觉得这种方案很鸡肋,为了一个锁我得单独开好几个独立的redis服务,其实很得不偿失,说白了内部只不过维护了一个过半机制而已,做分布式锁最好还是用Zk,为什么?原因如下:

[*]zk提供了临时节点,不用像redis还要设置过期时间,设置完后极端情况业务可能还要看门狗续期,麻不麻烦啊
[*]zk是一个强一致性的注册中心,他的同步机制是必须有半数以上的机器同步成功才会响应给用户,那如果master挂掉了,重新选主采用过半机制选主也一定会选出一个拥有最新数据的master,另外过半机制还防脑裂,一举多得。
  说了这么多redis的不足,那为啥还要学redis的分布式锁呢?我也不知道。。。
talk is cheap,show me the code
  方案一:
set k v ex 30 nx
k就是业务标识,v是uuid+threadId(唯一就好) ,过期时间30s,nx:不存在才设置
pom
<dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.10.0-m1</version>
</dependency>
  常量
private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";//NX 不存在才会set
    private static final String SET_WITH_EXPIRE_TIME = "PX";//EX:s PX:ms
  加锁
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
      //setnx指令没办法续期,需要使用lua脚本实现分布式锁进行续期操作
      String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

      if (LOCK_SUCCESS.equals(result)) {
            //启动看门狗
            Thread t = new Thread(new RedisLock.WatchDog((long)expireTime+System.currentTimeMillis()-2000,lockKey));
            t.setDaemon(true);
            t.start();
            return true;
      }
      return false;

    }

  解锁
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) == ARGV then return redis.call('del', KEYS) else return 0 end";
      Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
      if (RELEASE_SUCCESS.equals(result)) {
            return true;
      }
      return false;
    }
  看门狗
static class WatchDog implements Runnable{

      private String name;
      private Long future;

      public WatchDog(Long future, String name) {
            this.name = name;
            this.future = future;
      }

      @Override
      public void run() {
            System.out.println("启动看门狗,lock="+name);
            while(true){
                long cur = System.currentTimeMillis();
                //如果到过期时间节点锁还没有被释放,给锁续期10s
                if(cur>=future){
                  //判断锁存不存在,如果存在就续期10s
                  StringBuilder script = new StringBuilder();

                  script.append("if redis.call('exists',KEYS) ==1 then ")
                              .append("redis.call('expire',KEYS,10000);")
                              .append("return 1;")
                              .append("end;")
                            .append("return 0;");

                  long ret = (long) JedisTemplate.operate().eval(script.toString(),1,name);
                  if(1==ret){
                        future = System.currentTimeMillis()+10000-2000;
                        System.out.println("续期10s,lock="+name);
                  }
                }
                try {
                  Thread.sleep(1000);
                } catch (InterruptedException e) {
                  e.printStackTrace();
                }
            }
      }
    }
  可重入锁
public class ReentrantRedisLock {
    //<key,重入次数>
    private final ThreadLocal<Map<String,Integer>> locks = new MapThreadLocal();
    private static class MapThreadLocal extends ThreadLocal<Map<String, Integer>> {
      @Override
      protected Map<String, Integer> initialValue() {
            return new HashMap();
      }
    }


    private Jedis jedis;
    private int expireTime;//过期时间ms
    private String reqId;

    public ReentrantRedisLock(Jedis jedis,int expireTime,String reqId){
      this.jedis = jedis;
      this.expireTime = expireTime;
      this.reqId = reqId;
    }


    public boolean lock(String key){
      Map<String,Integer> refs = locks.get();
      Integer refCount = refs.get(key);

      if(null!=refCount){
            refs.put(key,refCount+1);
            return true;
      }

      boolean ok = RedisLock.tryGetDistributedLock(jedis,key,reqId,expireTime);
      if(!ok)
            return false;

      refs.put(key,1);
      return true;
    }


    public boolean unlock(String key){
      Map<String,Integer> refs = locks.get();
      Integer refCount = refs.get(key);

      if(null==refCount){
            return false;
      }
      refCount-=1;
      if(refCount>0){
            refs.put(key,refCount);
      }else{
            refs.remove(key);
            RedisLock.releaseDistributedLock(jedis,key,reqId);
      }
      return true;
    }
}
  方案二:
全部使用lua脚本,锁使用hash
hset key field value
hset key uuid 重入次数
过期时间采用pexpire指令
pexpire key 时间ms
@Slf4j
public class DistributeLock {

    public static final String READ_LOCK_PREFIX = "distribute_lock_";
    public static String getLockKey(String name){
      return READ_LOCK_PREFIX + name;
    }
    public static String getUUID(){
      return JedisConnectPoll.JEDIS_CONNECT_POLL_UUID.toString() + ":" + Thread.currentThread().getId();
    }

    public void lock(String name){
      tryLock(name, Long.MAX_VALUE, 30, TimeUnit.SECONDS);
    }

    public void lock(String name, long leaseTime, TimeUnit unit){
      tryLock(name, Long.MAX_VALUE, leaseTime, unit);
    }

    /**
   *
   * @param name 业务名称,加锁唯一key
   * @param waitTime 加锁过程最多消耗的时间,超过这个时间失败
   * @param leaseTime 过期时间
   * @param unit 时间单位
   * @return
   */
    public boolean tryLock(String name, long waitTime, long leaseTime, TimeUnit unit){
      Long waitUntilTime = unit.toMillis(waitTime) + System.currentTimeMillis();
      if(waitUntilTime < 0){
            waitUntilTime = Long.MAX_VALUE;
      }
      Long leastTimeLong = unit.toMillis(leaseTime);
      StringBuilder script = new StringBuilder();
      //如果锁不存在,加锁,设置过期时间,设置重入次数1
      //如果锁存在,判断uuid是不是自己,是的话设置过期时间,设置重入次数+1
      //否则,加锁失败
      script.append("if redis.call('exists',KEYS) ==0 then ")
                  .append("redis.call('hset',KEYS,ARGV,1);")
                  .append("redis.call('pexpire',KEYS,ARGV);")
                .append("return -1;")//代表加锁成功
                .append("end;")

                .append("if redis.call('hexists',KEYS,ARGV)==1 then ")
                  .append("redis.call('hincrby',KEYS,ARGV,1);")
                  .append("redis.call('pexpire',KEYS,ARGV);")
                .append("return -2;")//代表重入成功
                .append("end;")

                //没有获取锁,返回过期时间
                .append("return redis.call('pttl',KEYS);");

      for(;;){
            if(System.currentTimeMillis() > waitUntilTime){
                log.info("线程"+Thread.currentThread()+"在指定时间获锁失败,lock="+getLockKey(name));
                return false;
            }

            Long res = (Long) JedisTemplate.operate().eval(script.toString(),1,getLockKey(name),leastTimeLong.toString(),getUUID());
            if(res.equals(-1L)){
                log.info("线程"+Thread.currentThread()+"获锁成功,lock="+getLockKey(name));
                //启动看门狗
                Thread t = new Thread(new WatchDog(leastTimeLong+System.currentTimeMillis()-2000,name,getUUID()));
                t.setDaemon(true);
                t.start();

                break;
            }else if(res.equals(-2L)){
                log.info("线程"+Thread.currentThread()+"获锁成功-重入获锁,lock="+getLockKey(name));
                break;
            }else if(res>0){
//                log.info("线程"+Thread.currentThread()+"休眠一会等待别人释放锁,lock="+getLockKey(name));
                try {

                  Thread.sleep(1);
                }catch (InterruptedException e) {
                  log.info("线程"+Thread.currentThread()+"休眠一会等待别人释放锁-出现异常,lock="+getLockKey(name));
                  e.printStackTrace();
                }
            }
      }
      return true;

    }

    /**
   * 释放锁逻辑
   * 根据名字+uuid找count,如果找到了,-1,如果<=0 就释放锁
   * @param name
   */
    public void unlock(String name){
      StringBuilder script = new StringBuilder();
      script.append("local count = redis.call('hget',KEYS,KEYS);")
                .append("if count then ")
                  .append("local delCount = redis.call('hincrby', KEYS, KEYS, -1); ")
                  .append("if tonumber(delCount)<=0 then ")
                        .append("redis.call('HDEL',KEYS,KEYS);")
                  .append("end;")
                .append("else ")
                  .append("redis.call('HDEL',KEYS,KEYS);")
                .append("end;")
                .append("return;");

      JedisTemplate.operate().eval(script.toString(),2,getLockKey(name),getUUID());
      log.info("线程"+Thread.currentThread()+"释放锁成功,lock="+getLockKey(name));
    }


    class WatchDog implements Runnable{

      private String name;
      private Long future;
      private String uuid;
      public WatchDog(Long future, String name,String uuid) {
            this.name = name;
            this.future = future;
            this.uuid = uuid;
      }

      @Override
      public void run() {
            System.out.println("启动看门狗,lock="+getLockKey(name));
            while(true){
                long cur = System.currentTimeMillis();
                //如果到过期时间节点锁还没有被释放,给锁续期10s
                if(cur>=future){
                  //判断锁存不存在,如果存在就续期10s
                  StringBuilder script = new StringBuilder();

                  script.append("if redis.call('exists',KEYS) ==1 then ")
                            .append("if redis.call('hexists',KEYS,ARGV) == 1 then ")
                              .append("redis.call('pexpire',KEYS,10000);")
                              .append("return 1;")
                            .append("end;")
                            .append("end;")
                            .append("return 0;");

                  long ret = (long) JedisTemplate.operate().eval(script.toString(),1,getLockKey(name),uuid);
                  if(1==ret){
                        future = System.currentTimeMillis()+10000-2000;
                        log.info("给线程"+Thread.currentThread()+"续期10s,lock="+getLockKey(name));
                  }
                }
                try {
                  Thread.sleep(1000);
                } catch (InterruptedException e) {
                  e.printStackTrace();
                }
            }
      }
    }

}

  看门狗测试:

  

  
文档来源:51CTO技术博客https://blog.51cto.com/u_12856278/3020894
页: [1]
查看完整版本: redis-分布式锁