温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

基于Redis的SETNX操作怎么实现分布式锁

发布时间:2022-01-15 16:51:09 来源:亿速云 阅读:168 作者:iii 栏目:大数据

本文小编为大家详细介绍“基于Redis的SETNX操作怎么实现分布式锁”,内容详细,步骤清晰,细节处理妥当,希望这篇“基于Redis的SETNX操作怎么实现分布式锁”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。

import com.jd.jim.cli.Cluster;

import java.io.IOException;

import java.util.concurrent.TimeUnit;

/**

 * <pre>

 * 基于Redis的SETNX操作实现的分布式锁

 * </pre>

 * @author lzc.java@icloud.com

 *

 */

public class RedisDistributedLock {

    private Cluster redis;

    // 锁的名字

    private String lockKey;

    // 锁的值

    private String lockVal = "";

    // 默认锁的有效时长(毫秒)

    private long lockExpires;

    private boolean locked;

    // 当前jvm内持有该锁的线程(if have one)  

    private Thread exclusiveOwnerThread;

    /**

     *

     * @param redis

     * @param lockKey  lockKey

     * @param lockExpires lockKey过期时间,单位:毫秒

     * @throws IOException

     */

    public RedisDistributedLock(Cluster redis, String lockKey, long lockExpires){

        this.redis = redis;

        this.lockKey = lockKey;

        this.lockExpires = lockExpires;

    }

    /**

     * 阻塞式获取锁 ,不过有超时时间,超过了tryGetLockTime还未获取到锁将直接返回false

     * @param tryGetLockTime

     * @param tryGetLockUnit

     * @return

     * @throws InterruptedException

     */

    protected boolean lock(long tryGetLockTime, TimeUnit tryGetLockUnit){

        try {

            // 超时控制 的时间可以从本地获取, 因为这个和锁超时没有关系, 只是一段时间区间的控制

            long start = System.currentTimeMillis();

            long timeout = tryGetLockUnit.toMillis(tryGetLockTime); 

            //int tryTimes=0;

            while (System.currentTimeMillis() - start < timeout) {

                //tryTimes++;

                //锁超时时间

                long lockExpireTime = System.currentTimeMillis() + lockExpires + 1;

                String stringOfLockExpireTime = String.valueOf(lockExpireTime);

                if (setnx(lockKey, stringOfLockExpireTime)) { // 获取到锁

                    // 成功获取到锁, 设置相关标识

                    locked = true;

                    exclusiveOwnerThread = Thread.currentThread();

                    //System.out.println("拿到锁了,哈哈:"+tryTimes);

                    return true;

                }

                //说明未获取到锁,进一步检查锁是否已经超时

                String lockVal=redis.get(lockKey);

                //是存在lockVal=null的情况的,C1客户端获取锁,并且处理完后,DEL掉锁,在DEL锁之前。

                // C2通过SETNX向lockKey设置时间戳T0 发现有客户端已经获取锁,进入GET操作。

                // 这时候C1客户端DEL掉锁成功。

                // C2向lockKey发送GET命令,获取返回值T1(null)。

                if(lockVal!=null&&Long.parseLong(lockVal)<System.currentTimeMillis()){

                    //表明已经超时了,原来的线程可能可能出现意外未能及时释放锁

                    String oldLockVal=redis.getSet(lockKey,stringOfLockExpireTime);

                    //为什么会有下面这个判断呢?因为多线程情况下可能同时有多个线程在这一时刻发现锁过期,那么就会同时执行getSet获取锁操作,

                    //通过下面的比较,可以找到第一个执行getSet操作的线程,让其获得锁,其它的线程则重试

                    //oldLockVal也存在null的情况,大家可以想想为什么

                    if(lockVal.equals(oldLockVal)){

                        redis.expire(lockKey, lockExpires, TimeUnit.MILLISECONDS);

                        // 成功获取到锁, 设置相关标识

                        locked = true;

                        exclusiveOwnerThread = Thread.currentThread();

                        return true;

                    }

                }

                Thread.sleep(5L);

            }

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        return false;

    }

    /**

     * 非阻塞,立即返回是否获取到锁

     * @return

     */

    public boolean tryLock() {

        if (setnx(lockKey, lockVal)) { // 获取到锁

            //  成功获取到锁, 设置相关标识

            locked = true;

            //setExclusiveOwnerThread(Thread.currentThread());

            exclusiveOwnerThread = Thread.currentThread();

            return true;

        }

        return false;

    }

    private boolean setnx(String lockKey, Object val) {

        if (redis.setNX(lockKey, String.valueOf(val))) {

            redis.expire(lockKey, lockExpires, TimeUnit.MILLISECONDS);

            return true;

        }

        return false;

    }

    public boolean isLocked() {

        return locked;

    }

    /**

     * 释放锁

     */

    public void unlock() {

        // 检查当前线程是否持有锁

        if (Thread.currentThread() != exclusiveOwnerThread) {

            // 表明锁并非当前线程所持有,不应该由当前线程来释放锁

            System.out.println("表明锁并非当前线程所持有,不应该由当前线程来释放锁exclusiveOwnerThread:" + exclusiveOwnerThread + ",Thread.currentThread():"+Thread.currentThread()+",lockKey" + lockKey);

            return;

        }

        //gaohongtianluck 忽略了一个地方。用del命令释放锁,如果线程A获得锁之后运行太久,久到另已经获得的锁失效了。

        // 这时线程B进来,取缔了A上的锁,线程B运行到一半的时候,这时线程A也运行完了,杀一个回马枪把原本以为获取到的锁给del,

        // 实际上是B获得的锁,那么就会导致其他线程进来竞争,而B还以为自己独占锁

        //回复Ffadsfoadfjaodjfalkd:我也在思考这个问题,我觉得有一种写法可以尽量避免。在锁的时候,如果锁住了,回传超时时间,作为解锁时候的凭证,解锁时传入锁的键值和凭证。我思考的解锁时候有两种写法:

        //1、解锁前get一下键值的value,判断是不是和自己的凭证一样。但这样存在一些问题:

        //1)get时返回null的可能,此时表示有别的线程拿到锁并用完释放

        //2)get返回非null,但是不等于自身凭证。由于有getset那一步,当两个竞争线程都在这个过程中时,存在持有锁的线程凭证不等于value,而value是稍慢那一步线程设置的value。

        //

        //2、解锁前用凭证判断锁是否已经超时,如果没有超时,直接删除;如果超时,等着锁自动过期就好,免得误删别人的锁。但这种写法同样存在问题,由于线程调度的不确定性,判断到删除之间可能过去很久,并不是绝对意义上的正确解锁。

        //

        //关于解锁我只想到这么多,希望有帮助,欢迎拍砖多交流。

        //综上所述,lzc.java实现采用了非常简单的方法,如上所述,即超时的情况下可能会出现误释放锁的场景,所以使用的时候就需要合理设置超时时间了

        redis.del(lockKey);

        exclusiveOwnerThread = null;

    }

}

读到这里,这篇“基于Redis的SETNX操作怎么实现分布式锁”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注亿速云行业资讯频道。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI