Redis分布式锁实现

Redis锁主要用到了Redis中的如下几个API,这里引用官方的API文档介绍说明一下:redis

SETNX

Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed. SETNX is short for "SET if Not eXists".分布式

Example:ide

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis> this

GETSET

GETSET can be used together with INCR for counting with atomic reset. For example: a process may call INCR against the key mycounter every time some event occurs, but from time to time we need to get the value of the counter and reset it to zero atomically. This can be done using GETSET mycounter "0":atom

 

Example:线程

 

redis> SET mykey "Hello"
"OK"
redis> GETSET mykey "World"
"Hello"
redis> GET mykey
"World"
redis> orm

expire

Integer reply, specifically:
1 if the timeout was set.
0 if key does not exist.接口

Example:ip

redis> SET mykey "Hello"
"OK"
redis> EXPIRE mykey 10
(integer) 1
redis> TTL mykey
(integer) 10
redis> SET mykey "Hello World"
"OK"
redis> TTL mykey
(integer) -1
redis> ci

官方文档介绍的也比较浅显易懂,这里不过多赘述。Redis可使用Redission,也可使用JedisCulster,而且以Api的方式提供给业务方

这里实现方式以下,互斥key业务方本身定制

接口层:

/**
 * Redis分布式锁
 */
public interface DistributeLock {
    /**
     * 默认锁有效时间(单位毫秒)
     */
    public static final long DEFAULT_LOCK_EXPIRE_TIME_MS = 60000;
    /**
     * 默认睡眠时间(单位毫秒)
     */
    public static final long DEFAULT_SLEEP_TIME_MS = 100;

    /**
     * 尝试锁
     *
     * @param lock 锁的键
     * @param requestTimeoutMS 请求超时 ms
     * @return 若是锁成功,则返回true;不然返回false
     */
    boolean tryLock(String lock, long requestTimeoutMS);

    /**
     * 尝试锁
     *
     * @param lock 锁的键
     * @param lockExpireTimeMS 锁有效期 ms
     * @param requestTimeoutMS 请求超时 ms
     */
    boolean tryLock(String lock, long lockExpireTimeMS, long requestTimeoutMS);

    /**
     * 解锁
     *
     * @param lock 锁的键
     */
    void unlock(String lock);
}

 

具体实现层:

/**
 * Description:分布式锁
 * Created by Jiyajie on 2017/12/15.
 */
@Service
public class RedisDistributeLock implements DistributeLock {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisDistributeLock.class);
    /**
     * 每秒的毫秒数
     */
    private static final long MILLIS_PER_SECOND = 1000L;
    /**
     * 0值
     */
    private static final int INT_ZERO = 0;
    /**
     * 1值
     */
    private static final int INT_ONE = 1;
    /**
     * jedisCluster
     */
    @Autowired
    private JedisCluster jedisCluster;


    @Override
    public boolean tryLock(String lock, long requestTimeoutMS) {
        return this.tryLock(lock, DEFAULT_LOCK_EXPIRE_TIME_MS, requestTimeoutMS);
    }

    @Override
    public boolean tryLock(String lock, long lockExpireTimeMS, long requestTimeoutMS) {
        Preconditions.checkArgument(StringUtils.isNotBlank(lock), "lock invalid");
        Preconditions.checkArgument(lockExpireTimeMS > INT_ZERO, "lockExpireTimeMS invalid");
        Preconditions.checkArgument(requestTimeoutMS > INT_ZERO, "requestTimeoutMS invalid");

        while (requestTimeoutMS > INT_ZERO) {
            String expire = String.valueOf(System.currentTimeMillis() + lockExpireTimeMS + INT_ONE);

            Long result = jedisCluster.setnx(lock, expire);
            if (result > INT_ZERO) {
                //目前没有线程占用此锁
                jedisCluster.expire(lock, Long.valueOf(lockExpireTimeMS / MILLIS_PER_SECOND).intValue());
                return true;
            }
            String currentValue = jedisCluster.get(lock);
            if (currentValue == null) {
                //锁已经被其余线程删除立刻重试获取锁
                continue;
            } else if (Long.parseLong(currentValue) < System.currentTimeMillis()) {
                //此处判断出锁已经超过了其有效的存活时间
                String oldValue = jedisCluster.getSet(lock, expire);
                if (oldValue == null || oldValue.equals(currentValue)) {
                    //1.若是拿到的旧值是空则说明在此线程作getSet以前已经有线程将锁删除,因为此线程getSet操做以后已经对锁设置了值,实际上至关于它已经占有了锁
                    //2.若是拿到的旧值不为空且等于前面查到的值,则说明在此线程进行getSet操做以前没有其余线程对锁设置了值,则此线程是第一个占有锁的
                    jedisCluster.expire(lock, Long.valueOf(lockExpireTimeMS / MILLIS_PER_SECOND).intValue());
                    return true;
                }
            }
            long sleepTime;
            if (requestTimeoutMS > DEFAULT_SLEEP_TIME_MS) {
                sleepTime = DEFAULT_SLEEP_TIME_MS;
                requestTimeoutMS -= DEFAULT_SLEEP_TIME_MS;
            } else {
                sleepTime = requestTimeoutMS;
                requestTimeoutMS = INT_ZERO;
            }
            try {
                TimeUnit.MILLISECONDS.sleep(sleepTime);
            } catch (InterruptedException e) {
                LOGGER.warn("分布式锁线程休眠异常{}", lock, e);
            }
        }
        return false;
    }

    @Override
    public void unlock(String lock) {
        String value = jedisCluster.get(lock);
        if (null != value && Long.parseLong(value) > System.currentTimeMillis()) {
            //若是锁还存在而且还在有效时间则进行删除
            jedisCluster.del(lock);
        }
    }
}

这里对实现步骤,以及实现原理进行分析:

1:参数校验,对传入的互斥key,锁超时时间,请求超时时间进行检验

2:在请求超时时间以内的请求,这里以while死循环的方式不断进行获取锁重试

3:设置锁过时时间,并尝试用setnx命令,redis以前不存在key的状况下,设置key,同时把过时时间expire做为value设置进去。若是获取成功,这里给锁加上真正的过时时间,获取锁成功~

4:在第三步没有成功的状况下,咱们直接再次获取锁。若是为空,则说明锁已通过期,或者已经被其余线程解锁,那么咱们马上结束本次循环,尝试从新获取~

5:若是第四步获取锁成功,咱们须要进行一下判断:1拿到锁的过时时间(key对应的value),并判断锁是否在过时时间以内,若是在的话,用具备原子性操做的命令getset,取出以前的过时时间oldValue值,这里会有两种状况:

//1.若是拿到的旧值是空则说明在此线程作getSet以前已经有线程将锁删除,因为此线程getSet操做以后已经对锁设置了值,实际上至关于它已经占有了锁

//2.若是拿到的旧值不为空且等于前面查到的值,则说明在此线程进行getSet操做以前没有其余线程对锁设置了值,则此线程是第一个占有锁的
两种状况都说明已经获取锁成功,结束循环

以上步骤都是创建在,请求超时时间以内的,这里每次循环获取,间隔100毫秒~,当获取时间超过请求超时时间的话:也是锁获取失败的一种状况

至此分布式锁的获取结束~

解锁操做很简单,这里也再也不赘述~

相关文章
相关标签/搜索