Redis实现分布式锁(setnx、getset、incr)以及如何处理超时情况(二)

(友情提示:本博客中的代码不能运行,只是提供一个思路!!!!!若需要精准可上生产的代码,请联系我~)

阅读了本章之后,读者一般就能写出一个适合自己系统的防并发的方案!
注意点
1、上一节我基于时间戳setnx到redis里,其实没有必要,因为时间戳可能会出现多机的不一致性,可以直接设置固定值
2、setnx和incr的抉择
setnx:防并发,轮循
incr:这种方式是一种全局的计数器,那么10笔并发请求,9笔会被拒绝,注意9笔只能被拒绝,因为通过计数器的方式,你无法判断第一笔请求什么时候会被处理结束,所以你无法处理超时以及获取上一笔请求结果。这个方便是方便,但是应用场景会少一点,适用于可以通过计数解决问题的场景,如秒杀、限速、全局计数等
(这个笔者也还在探索)
3、什么时候需要轮循
4、setnx(lockKey) + getset(newlockKey) 控制超时 setnx(lockKey) + set(resultKey) 控制处理超时 的抉择

方式一:

    public String incr(String key) {
        long count = RedisUtils.incr("lockKey");
        String result;
        if (count == 1L) {
            //(1)处理业务逻辑
            result = process();
            //(2)把第一笔线程处理的结果放到缓存中
            RedisUtils.set("resultKey"+ count, result);
        } else {
            //(3)其余9笔全部从缓存中拿取结果
            result = RedisUtils.get("resultKey"+ count);
        }
        return result;
    }

很明显,上面的代码在第(3)处没有考虑到,第一笔线程超时的情况,(3)出result获取到的是null,用了incr是无法较好地处理这种情况。
其实incr这种模式最擅长的地方就是,暴力防并发,即只让第一笔处理请求,其他的全部拒绝。对于1w笔的incr和1w笔的setnx这两种情况哪个性能好,这个笔者还没有试验,后续补上。暂且读者是认为计数器更快!

方式二:

public String setnx(String key) {
        boolean value = RedisClientUtils.setnx(key, "lockKey",100);
        String result;
        if (value){
           //1、处理业务逻辑
            result = process();
            //(2)把第一笔线程处理的结果放到缓存中
            RedisClientUtils.set("resultKey",result);
        }else {
            //(3)其余9笔全部从缓存中拿取结果
            result = RedisClientUtils.get("resultKey");
        }
        return result;
    }

很明显,上面的代码在第(3)处没有考虑到,第一笔线程超时的情况。下面我们用轮循来解决超时情况

方式三:

    //线程内全局变量,用来统计递归次数
    static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };

    public String setnx(String key) {
        boolean value = RedisClientUtils.setnx(key, "lockKey", 100);
        String result;
        long startTime = System.currentTimeMillis();
        int timeout = 2;
        if (value) {
            //业务逻辑处理
            result = process();
            RedisClientUtils.set("resultKey", result);
        } else {
            while (true) {
                try {
                    //休眠
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //尝试获取上一笔处理的结果
                result = RedisClientUtils.get("resultKey");
                if (result != null) {
                    return result;
                }
                //如果轮循2秒,即40次,还是无结果;则递归,允许递归3次
                if ((System.currentTimeMillis() - startTime) / 1000 > timeout) {
                    if (threadLocal.get() > 3) {
                        //超过重试次数
                        throw new AppException("超过重试次数");
                    } else {
                        setnx("lockKey");
                    }
                }
            }
        }
        return result;
    }

其实这里面有个细节,上述例子中,setnx中的lockKey会超时,超时时间100ms。轮循也会超时。
如果轮循超时,跳出,但是第一笔的lockKey还没放开,又进入轮循状态;如果第一笔的lockKey先超时了,那么9笔也还是在轮循状态。所以不管那种超时我们都还在轮循等待结果的状态,这就实现了我们的初衷。这种使用的场景,可能会出现在动态获取token等场景。比如10笔获取token的场景,首先我们要防住并发获取,其次要防住强依赖于第一笔,否则如果第一笔获取失败或者超时,那业务将无法进行下去。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值