(友情提示:本博客中的代码不能运行,只是提供一个思路!!!!!若需要精准可上生产的代码,请联系我~)
阅读了本章之后,读者一般就能写出一个适合自己系统的防并发的方案!
注意点
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的场景,首先我们要防住并发获取,其次要防住强依赖于第一笔,否则如果第一笔获取失败或者超时,那业务将无法进行下去。