最近在准备面试,又看到了redis实现分布式锁,同事也想到了zk也可以实现分布式锁。之前也有看过这两种方式实现分布式锁的原理,但是时间一长就又忘记了!今天把它整理一下,希望能帮到面试的人,同事也为了加强记忆吧!好了,废话不多说了。开始正篇。

redis实现分布式锁(实现思路)

以下是基本的算法,还有一种是redis官网提供的基于redLock算法实现的分布式锁,此处就不做介绍了

1、获取当前时间戳与锁的过期时间相加后得到锁要过期的时间,把这个要过期的时间设置为value,锁作为key。调用setnx方法,如果返回1证明锁不存在,可以成功获取锁,获取成功后,设置expire超市时间。返回获取锁

2、如果setnx返回0,证明原来锁存在,没有获取成功。然后调用get方法,获取第一步中存入的过期时间。与当前时间戳比较,如果当前时间大于过期时间,则证明锁已过期,调用getset方法,把新的时间存进去,返回获取锁成功。这里进行时间比较主要是应对expire执行失败,或服务重启情况下出现的无法释放锁的情况。

代码

import com.eduapi.common.component.RedisComponent;
import com.eduapi.common.util.BeanUtils;
import org.apache.commons.lang3.StringUtils;

/**
 * @Description: 利用redis实现分布式锁.
 * @Author: ZhaoWeiNan .
 * @CreatedTime: 2017/3/20 .
 * @Version: 1.0 .
 */
public class RedisLock {

    private RedisComponent redisComponent;

    private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;

    private String lockKey;

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

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

    private volatile boolean isLocked = false;

    public RedisLock(RedisComponent redisComponent, String lockKey) {
        this.redisComponent = redisComponent;
        this.lockKey = lockKey;
    }

    public RedisLock(RedisComponent redisComponent, String lockKey, int timeoutMillisCond) {
        this(redisComponent, lockKey);
        this.timeoutMillisCond = timeoutMillisCond;
    }

    public RedisLock(RedisComponent redisComponent, String lockKey, int timeoutMillisCond, int expireMillisCond) {
        this(redisComponent, lockKey, timeoutMillisCond);
        this.expireMillisCond = expireMillisCond;
    }

    public RedisLock(RedisComponent redisComponent, int expireMillisCond, String lockKey) {
        this(redisComponent, lockKey);
        this.expireMillisCond = expireMillisCond;
    }

    public String getLockKey() {
        return lockKey;
    }

    /**
     * 获得 lock. (把大神的思路粘过来了)
     * 实现思路: 主要是使用了redis 的setnx命令,缓存了锁.
     * reids缓存的key是锁的key,所有的共享, value是锁的到期时间(注意:这里把过期时间放在value了,没有时间上设置其超时时间)
     * 执行过程:
     * 1.通过setnx尝试设置某个key的值,成功(当前没有这个锁)则返回,成功获得锁
     * 2.锁已经存在则获取锁的到期时间,和当前时间比较,超时的话,则设置新的值
     *
     * @return true if lock is acquired, false acquire timeouted
     * @throws InterruptedException in case of thread interruption
     */
    public synchronized boolean lock() throws InterruptedException {
        int timeout = timeoutMillisCond;

        boolean flag = false;

        while (timeout > 0){
            //设置所得到期时间
            Long expires = System.currentTimeMillis() + expireMillisCond;
            String expiresStr = BeanUtils.convertObject2String(expires);

            //原来redis里面没有锁,获取锁成功
            if (this.redisComponent.setNx(lockKey,expiresStr) > 0){
                //设置锁的过期时间
                this.redisComponent.expire(lockKey,expireMillisCond);
                isLocked = true;
                return true;
            }

            flag = compareLock(expiresStr);

            if (flag){
                return flag;
            }

            timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;

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

    /**
     * 排他锁。作用相当于 synchronized 同步快
     * @return
     * @throws InterruptedException
     */
    public synchronized boolean excludeLock() {

        //设置所得到期时间
        long expires = System.currentTimeMillis() + expireMillisCond;
        String expiresStr = BeanUtils.convertObject2String(expires);

        //原来redis里面没有锁,获取锁成功
        if (this.redisComponent.setNx(lockKey,expiresStr) > 0){
            //设置锁的过期时间
            this.redisComponent.expire(lockKey,expireMillisCond);
            isLocked = true;
            return true;
        }

        return compareLock(expiresStr);
    }

    /**
     * 比较是否可以获取锁
     * 锁超时时 获取
     * @param expiresStr
     * @return
     */
    private boolean compareLock(String expiresStr){
        //假如两个线程走到这里
        //因为redis是单线程的获取到
        // A线程获取  currentValueStr = 1 B线程获取 currentValueStr = 1
        String currentValueStr = this.redisComponent.get(lockKey);

        //锁
        if (StringUtils.isNotEmpty(currentValueStr) && Long.parseLong(currentValueStr) < System.currentTimeMillis()){

            //获取上一个锁到期时间,并设置现在的锁到期时间,
            //只有一个线程才能获取上一个线程的设置时间,因为jedis.getSet是同步的
            //只有A线程 把 2 存进去了。 取出了 1, 对比获得了锁
            //B线程 吧 2存进去了。 获取 2.。对比 没有获得锁,
            String oldValue = this.redisComponent.getSet(lockKey,expiresStr);

            if (StringUtils.isNotEmpty(oldValue) && StringUtils.equals(oldValue,currentValueStr)){

                //防止误删(覆盖,因为key是相同的)了他人的锁——这里达不到效果,这里值会被覆盖,但是因为什么相差了很少的时间,所以可以接受
                //[分布式的情况下]:如过这个时候,多个线程恰好都到了这里,但是只有一个线程的设置值和当前值相同,他才有权利获取锁
                isLocked = true;
                return true;
            }
        }
        return false;
    }

    /**
     * 释放锁
     */
    public synchronized void unlock(){
        if (isLocked){
            this.redisComponent.delete(lockKey);
            isLocked = false;
        }
    }

}

调用代码

 

//创建锁对象, redisComponent 为redis组件的对象   过期时间  锁的key
        RedisLock redisLock = new RedisLock(redisComponent,1000 * 60,RedisCacheKey.REDIS_LOCK_KEY + now_mm);
        //获取锁
        if (redisLock.excludeLock()){
            try {
                //拿到了锁,读取定时短信有序集合
                set = this.redisComponent.zRangeByScore(RedisCacheKey.MSG_TIME_LIST,0,end);

                if (set != null && set.size() > 0){
                    flag = true;
                }
            } catch (Exception e) {
                LOGGER.error("获取定时短信有序集合异常,异常为{}",e.toString());
            }

 

基于zookpeer实现分布式锁:

两种分布式锁的比较

1、redis分布式锁需要自己不断尝试去获取锁,比较消耗性能。

2、zk分布式锁获取不到锁时只需要监听n-1节点的删除事件即可!不需要不断去获取锁,性能开销小

3、redis获取锁的客户端挂了的话,那么只能等待超时后释放锁(这点应该可以通过ttl方法进行检测来删除锁)。而zk是基于临时顺序节点进行的分布式锁,客户端挂的话,临时节点会自动删除,锁就会释放。