评论

收藏

[NoSQL] redis-分布式锁

数据库 数据库 发布于:2021-07-09 11:48 | 阅读数:218 | 评论:0

  
  redis-分布式锁

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

  

  • 互斥:key
  • 不能死锁:过期时间
  • 解铃还须系铃人:value存储 uuid+threadId
  • 容错: 关于容错,redis集群环境下是没办法保证分布式锁的容错性的,具体原因如下:
    DSC0000.png 如图所示:一个三主三从的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[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;
  }
  看门狗
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]) ==1 then ")
                .append("redis.call('expire',KEYS[1],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[1]) ==0 then ")
          .append("redis.call('hset',KEYS[1],ARGV[2],1);")
          .append("redis.call('pexpire',KEYS[1],ARGV[1]);")
        .append("return -1;")//代表加锁成功
        .append("end;")
        .append("if redis.call('hexists',KEYS[1],ARGV[2])==1 then ")
          .append("redis.call('hincrby',KEYS[1],ARGV[2],1);")
          .append("redis.call('pexpire',KEYS[1],ARGV[1]);")
        .append("return -2;")//代表重入成功
        .append("end;")
        //没有获取锁,返回过期时间
        .append("return redis.call('pttl',KEYS[1]);");
    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[1],KEYS[2]);")
        .append("if count then ")
          .append("local delCount = redis.call('hincrby', KEYS[1], KEYS[2], -1); ")
          .append("if tonumber(delCount)<=0 then ")
            .append("redis.call('HDEL',KEYS[1],KEYS[2]);")
          .append("end;")
        .append("else ")
          .append("redis.call('HDEL',KEYS[1],KEYS[2]);")
        .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]) ==1 then ")
              .append("if redis.call('hexists',KEYS[1],ARGV[1]) == 1 then ")
                .append("redis.call('pexpire',KEYS[1],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();
        }
      }
    }
  }
}
  看门狗测试:
DSC0001.png
  

  
关注下面的标签,发现更多相似文章