基于 redis setNx 分布式锁实现

public class RedisLock {

    private static Logger logger = LoggerFactory.getLogger(RedisLock.class);
    
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

    private String lockKey;

    /**
     * 锁超时时间,防止线程在入锁以后,无限的执行等待
     */
    private int expireMsecs = 60 * 1000;

    /**
     * 锁等待时间,防止线程饥饿
     */
    private int timeoutMsecs = 20 * 1000;

    public volatile boolean locked = false;

    /**
     * Detailed constructor with default acquire timeout 10000 msecs and lock expiration of 60000 msecs.
     *
     * @param lockKey lock key (ex. account:1, ...)
     */
    public RedisLock(String lockKey) {
        this.lockKey = "lock_"+lockKey ;
    }

    /**
     */
    public RedisLock( String lockKey, int timeoutMsecs) {
        this(lockKey);
        this.timeoutMsecs = timeoutMsecs;
    }

    /**
     *
     */
    public RedisLock( String lockKey, int timeoutMsecs, int expireMsecs) {
        this( lockKey, timeoutMsecs);
        this.expireMsecs = expireMsecs;
    }

    /**
     */
    public String getLockKey() {
        return lockKey;
    }

    private String get(final String key) {
        String res = null;
        Jedis jedis=null;
        try {
            jedis=JedisUtil.getJedis();
            jedis.select(10);
            res=jedis.get(key);
        } catch (Exception e) {
            logger.error("get redis error, key : {}", key);
        }finally {
            if(jedis!=null) {
                jedis.close();
            }
        }
        return res;
    }
    /**
     * 尝试获取锁
     * @return 是否获取成功
     */
    public boolean tryGetLock() {
        String res = null;
        Jedis jedis=null;
        try {
            jedis=JedisUtil.getJedis();
            jedis.select(10);
            Date now=new Date();
            
            Thread thread=Thread.currentThread();
            res = jedis.set(lockKey, now.getTime()+"_"+thread.getId()+"_"+thread.hashCode()+"_"+hashCode(), SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireMsecs);
            if (LOCK_SUCCESS.equals(res)) {
                locked=true;
                jedis.hset("lock_"+DateFormatUtil.format(now, "yyyy-MM-dd_HH_mm"),lockKey,"");
                return true;
            }
        } catch (Exception e) {
            logger.error("getset redis error, key : {}", lockKey);
        }finally {
            if(jedis!=null) {
                jedis.close();
            }
        }

        return false;

    }
    /**
     */
    public synchronized void unlock() {
        if (locked) {
            Jedis jedis=null;
            try {
                jedis=JedisUtil.getJedis();
                jedis.select(10);
                String res=jedis.get(lockKey);
                if(StringUtils.isNotBlank(res)){
                    
                    // 解锁的时候 取值对比  hashCode 判断是不是 当前锁对象加的锁,如果不是 则不执行任何操作
                    int lockObjHashCode=Integer.parseInt(res.split("_")[3]);
                    if(lockObjHashCode!=hashCode()) {
                        return;
                    }
                    
                    jedis.del(lockKey);
                    Date keyDate=new Date(Long.parseLong(res.split("_")[0]));
                    jedis.hdel("lock_"+DateFormatUtil.format(keyDate, "yyyy-MM-dd_HH_mm"),lockKey);
                }
                locked = false;
            } catch (Exception e) {
                logger.error("unlock redis error, lockKey : {}", lockKey);
            }finally {
                if(jedis!=null) {
                    jedis.close();
                }
            }
          
        }
    }
    /**
     * 获得 lock.
     * 实现思路: 主要是使用了redis 的setnx命令,缓存了锁.
     * reids缓存的key是锁的key,所有的共享, value是锁过期时间+线程ID+线程hashCode+锁对象hashCode
     * 执行过程:
     * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
     * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
     * 3.锁的内容 添加 线程ID 以及 线程hashCode ,用于处理 同一线程下,多层子逻辑需要对同一个数据重复加锁异常的问题  对比判断如果是同一线程的子操作,则默认已经锁定,无需锁等待以及重复锁
     * 4.锁的内容 添加 加锁对象本身hashCode ,为了解锁的时候对比判断是不是 加锁的那个对象执行的解锁操作, 避免多层业务 重复锁, 子操作 解锁导致,锁被非正常解锁
     *
     */
    public synchronized boolean lock() throws InterruptedException {
        int timeout = timeoutMsecs;
        while (timeout >= 0) {
            if (this.tryGetLock()) {
                locked = true;
                return true;
            }
            

            String currentValueStr = this.get(lockKey); //redis里的时间
            
            Long cacheTime=Long.parseLong(currentValueStr.split("_")[0]);
            long threadId=Long.parseLong(currentValueStr.split("_")[1]);
            int threadHashCode=Integer.parseInt(currentValueStr.split("_")[2]);
            
            Thread thread=Thread.currentThread();
            if(threadHashCode==thread.hashCode()&&threadId==thread.getId()) {
                return true;
            }
            
            if (currentValueStr != null &&cacheTime+expireMsecs + 1 < System.currentTimeMillis()) {
                return false;
            }
            timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;

            /*
                延迟100 毫秒,  这里使用随机时间可能会好一点,可以防止饥饿进程的出现,即,当同时到达多个进程,
                只会有一个进程获得锁,其他的都用同样的频率进行尝试,后面有来了一些进行,也以同样的频率申请锁,这将可能导致前面来的锁得不到满足.
                使用随机的等待时间可以一定程度上保证公平性
             */
            Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);

        }
        return false;
    }


   

}

 

你可能感兴趣的:(基于 redis setNx 分布式锁实现)