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[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();
                }
            }
        }
    }

}

看门狗测试:
在这里插入图片描述

仓库地址:redis(代码在com.lry.basic)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值