redis分布式锁unlock方法

redis作为分布式锁的运用,网上有无数的案例,这里提供一个我自己设计的unlock解锁方案。

相对于加锁,解锁的过程相对简单,之前我项目里解锁就是直接delete lock_key,由于加锁的过程设置了超时时间,简单的delete lock_key显然有造成误删锁的风险(下面会具体介绍误删的原因)。本人梳理了下解锁的流程,设计了一个大部分情况下可以避免误删的解锁方案。

首先引用一个典型的加锁流程。利用的jedis客户端,其中setnx方法判断锁是否被其他线程占用,如果被占用的话也不是马上退出,而是比较占用的锁是否超时。这儿getSet方法用来避免多个线程同时删除过期锁,从而同时获取到锁的情况,保证只有一个线程能修改过期锁。这个过程网上有无数的案例,在此不作过多描述。下面分析该方法的unlock方法如何解锁。

/*
    * 典型加锁流程
    * */
    public boolean acquireLock(String lock) {

        boolean success = false;
        Jedis jedis = pool.getResource();
        //过期时间3分钟
        long value = System.currentTimeMillis() + 3 * 60 * 1000 + 1;
        long acquired = jedis.setnx(lock, String.valueOf(value));
        //SETNX成功,则成功获取一个锁
        if (acquired == 1)
            success = true;
            //SETNX失败,说明锁仍然被其他对象保持,检查其是否已经超时
        else {
            long oldValue = Long.valueOf(jedis.get(lock));

            //超时
            if (oldValue < System.currentTimeMillis()) {
                String getValue = jedis.getSet(lock, String.valueOf(value));
                // 获取锁成功
                if (Long.valueOf(getValue) == oldValue)
                    success = true;
                    // 已被其他进程捷足先登了
                else
                    success = false;
            }
            //未超时,则直接返回失败
            else
                success = false;
        }
        pool.returnResource(jedis);
        return success;
    }

关于unlock方法,如果直接删除lock_key,显然可能存在这样的错误:

当T1加锁成功,但执行过程被挂起,导致执行时间超过3分钟,另一个线程T2修改了T1的锁过期时间,此时T2加锁成功。当T1执行到解锁的流程,如果T1直接delete lock_key,删除的是T2修改过的lock_key,这时候T2未必执行完毕。如果再有T3过来,将直接加锁成功,导致T2和T3并发。

问题的关键是,T1解锁的时候,没有验证删除的是自己锁。为了让T1线程能“认识”自己加的锁,我们修改了lock方法,返回加锁的时间戳,用于unlock判断。加锁代码:

 /*
    * 加锁成功返回时间戳,加锁失败返回空对象null
    * */
    public String lock(String lock) {

        Jedis jedis = pool.getResource();

        long value = System.currentTimeMillis() + 3 * 60 * 1000 + 1;
        String timeStamp = String.valueOf(value);
        long acquired = jedis.setnx(lock, timeStamp);
        //SETNX成功,则成功获取一个锁
        if (acquired == 1) {
            return timeStamp;
        } else {
            long oldValue = Long.valueOf(jedis.get(lock));

            //超时
            if (oldValue < System.currentTimeMillis()) {
                String getValue = jedis.getSet(lock, timeStamp);
                // 这种情况不能加锁
                if (Long.valueOf(getValue) != oldValue) {
                    timeStamp = null;
                }
            } else {
                //锁未超时,也不能加锁
                timeStamp = null;
            }

        }
        pool.returnResource(jedis);
        //只有加锁成功情况,才返回了加锁的时间戳
        return timeStamp;
    }
lock方法可以返回时间戳,当调用unlock方法时,用这个时间戳作为验证的参数

/*
    * 解锁要验证加锁返回的时间戳
    * */
    public void unlock(String lock, String timeStamp) {

        Jedis jedis = pool.getResource();

        String value = jedis.get(lock); .......................................(1)
        long expireTime = Long.parseLong(value);
        if (System.currentTimeMillis() > expireTime
                && value.equals(timeStamp)) {
            jedis.del(lock);    ...............................................(2)
        }

        pool.returnResource(jedis);

    }

这儿解释一下delete lock_key的条件:

1、当锁未超时(小于currentTimeMillis)

2、当锁的value和timeStamp相等

为什么要验证锁未超时?因为如果锁已经超时,就算value和timeStamp相等,即线程自己加的锁,如果直接删除这个超时的锁,可能删除的是另一个线程的锁,具体过程像这样:

(1)T1线程查看锁是否是自己的timeStamp:

String value = jedis.get(lock);

if(value.equals(timeStamp)) ==> true

//判断timeStamp是不是和value相同,结果相同,因此T1线程正准备删除key

(2)这时候T2线程过来获取锁,由于T1的锁已经超时,T2直接加锁成功了,这时候lock的value其实不在是timeStamp

(3)T1执行到删除的命令,然后悲剧发生,T2的lock_key被删掉了。。。

整个错误的核心在于,unlock方法里面(1)、(2)两个步骤之间,不能有别的线程加锁。

按照这个思路,redis的watch似乎可以更简洁的处理lock和unlock的过程。但据说watch涉及到redis事务开销,很少有用watch实现分布锁。如果读者有更好的方式,欢迎向博主推荐

(个人原创,转载请注明出处)


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值