用redis处理高并发是个很常见的方式,因为redis的访问效率很高(直接访问内存),一般我们会用来处理网站一瞬间的并发量。
那如果要使用redis来进行高并发问题的解决的话,应注意以下几点:
1、首先我们要先知道,我们在存储时,应使用redis的setnx方法,不应该使用set方法,因为setnx拥有原子操作命令(正确点说法应该是使用setnx,根据其属性可以保证共享资源的原子性操作),当资源锁存在不能设置值,则返回0,而当锁不存在,则设置锁,返回1; 但如果使用set方法,则会出现在高并发情况下,进程同时获取锁状态为null,同时设置,锁之间相互覆盖,但是俩进程仍在并发执行业务代码的情况。
2、为了防止死锁,我们不应直接使用jedis.setnx(lock, 1) 来进行简单的加锁,这样会导致当进程执行出现问题,锁未释放,则其他进程永远处于阻塞状态,出现死锁。 为了避免死锁,我们在加锁时带上时间戳,setnx(lock, 时间戳+超时时间),当发现锁超时了,便可以对锁进行重置,避免死锁。
接下来,实际操作!
设置锁:
//其中currentTimeMullis为当前时间、valideTime为超时时间,key为资源
//对该资源进行锁获取,如果存在锁则会返回false,不存在则设置值并返回true
boolean lock = redisService.setnx(key, currentTimeMullis+valideTime);
//如果不存在并设置了值,则可以直接返回,因为已经获取资源锁成功
//否则,则代表存在这个锁,则进行锁是否超时的判断。获取该资源的锁时间,用于判断是否超时了
String keyTime = redisService.get(key);
if((Long.valueOf(currentTimeMullis)-Long.valueOf(keyTime))>valideTime){
//该判断代表该资源锁已经超时,那么便进行资源锁的重置,也就是进行资源锁的重新设置(删除并重新设置)
//重新设置成功后也返回,因为获取锁成功,可以进行操作啦。
}
//如果以上操作都没有成功,则返回失败,代表获取锁失败,不可以进行操作。
释放锁:
当对资源处理结束后,则调用释放方法释放锁资源
(经提醒,我发现我这里少了个判断逻辑…)
//在删除前,应该先对该资源锁进行获取,判断值与此时释放锁的线程所携带的值是否相等,也就是我们上面创建时用的currentTimeMullis+valideTime。
String keyLockTime = redisService.get(key);
if(keyLockTime!=null&&keyLockTime.equals(currentTimeMullis+valideTime)){
//此时锁还由当前线程保持则释放锁
redisService.del(key);
}else{
//此时说明该资源锁被其他线程重置或释放,已不再拥有锁的释放权
//结束
}
具体的枷锁方法
package com.isatk.yn.module.common.service;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryListener;
import com.github.rholder.retry.Retryer;
import com.github.rholder.retry.RetryerBuilder;
import com.github.rholder.retry.StopStrategies;
import com.github.rholder.retry.WaitStrategies;
import com.google.common.base.Preconditions;
import com.isatk.yn.module.common.util.RedisLockComponentUtil;
import com.isatk.yn.util.AssertUtil;
import lombok.extern.slf4j.Slf4j;
/**
*
* Redis分布式锁组件
*
* @author: hlong
*/
@Slf4j
@Component
public class RedisLockComponent {
private static final String SETNX_EXPIRE_TIME_SCRIPT = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then\n"
+ "return redis.call('pexpire', KEYS[1], ARGV[2]);\n" + "end\n" + "return 0;";
@Autowired
public RedisTemplate<String, String> redisTemplate;
public RedisLockComponent() {
}
public RedisTemplate<String, String> getRedisTemplate() {
return redisTemplate;
}
/**
* 获取Redis分布式锁,支持可重入性 setIfAbsent()与expire()组全并不满足原子性,所以使用lua脚本保证原子性
*/
public void getLock(String redisKey, int keepLockTimeLenght, TimeUnit keepLockTimeUnit) throws Exception {
AssertUtil.notNull(redisKey);
String currentThreadQualifiedId = RedisLockComponentUtil.getCurrentThreadQualifiedId();
String threadQualifiedId = (String) this.redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(threadQualifiedId)
&& StringUtils.equals(threadQualifiedId, currentThreadQualifiedId)) {
return;
}
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptText(SETNX_EXPIRE_TIME_SCRIPT);
List<String> keyList = Arrays.asList(redisKey);
long time = TimeUnit.MILLISECONDS.convert(keepLockTimeLenght, keepLockTimeUnit);
Long executeResult = (Long) this.redisTemplate.execute(redisScript, keyList, currentThreadQualifiedId, time);
boolean isGettingRedisLockSuccessful = (executeResult != null && executeResult == 1);
int retriedTimes = 0;
while (!isGettingRedisLockSuccessful) {
if (retriedTimes > 100) {
log.warn("{} 获取redisKey循环次数超过100,当前次数:{}", redisKey, retriedTimes);
}
if (retriedTimes > 300) {
throw new RuntimeException("获取redis key循环次数超过300");
}
Thread.sleep(210);
isGettingRedisLockSuccessful = this.redisTemplate.opsForValue().setIfAbsent(redisKey,
currentThreadQualifiedId);
retriedTimes++;
}
}
/**
* 获取Redis分布式锁,支持可重入性</br>
* 支持重试</br>
* setIfAbsent()与expire()组全并不满足原子性,所以使用lua脚本保证原子性
*/
public void getLockForRetry(String redisKey, int keepLockTimeLenght, TimeUnit keepLockTimeUnit) throws Exception {
Preconditions.checkNotNull(redisKey);
Preconditions.checkNotNull(keepLockTimeLenght);
Preconditions.checkNotNull(keepLockTimeUnit);
String currentThreadQualifiedId = RedisLockComponentUtil.getCurrentThreadQualifiedId();
String cachedThreadQualifiedId = this.redisTemplate.opsForValue().get(redisKey);
if (StringUtils.equals(cachedThreadQualifiedId, currentThreadQualifiedId)) {
return;
}
DefaultRedisScript<Long> setNELuaJscript = new DefaultRedisScript<>();
setNELuaJscript.setResultType(Long.class);
setNELuaJscript.setScriptText(SETNX_EXPIRE_TIME_SCRIPT);
List<String> redisKeyList = Arrays.asList(redisKey);
long timeInMillis = TimeUnit.MILLISECONDS.convert(keepLockTimeLenght, keepLockTimeUnit);
// 构造retryer
RetryerBuilder<Boolean> retryerBuilder = RetryerBuilder.<Boolean> newBuilder()
.withWaitStrategy(WaitStrategies.incrementingWait(1, TimeUnit.SECONDS, 5, TimeUnit.SECONDS));
retryerBuilder.withStopStrategy(StopStrategies.stopAfterDelay(3, TimeUnit.MINUTES));
retryerBuilder.withRetryListener(new RetryListener() {
@Override
public <Boolean> void onRetry(Attempt<Boolean> attempt) {
log.info("第【{}】次重试,获取锁redisKey:{},结果:{},重试时间离第1次间隔为:{}(ms)!", attempt.getAttemptNumber(), redisKey,
attempt.getResult(), attempt.getDelaySinceFirstAttempt());
}
});
retryerBuilder.retryIfResult(getLockSuccessful -> !getLockSuccessful);
Retryer<Boolean> retryer = retryerBuilder.build();
retryer.call(new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
Long executeResult = (Long) redisTemplate.execute(setNELuaJscript, redisKeyList,
currentThreadQualifiedId, timeInMillis);
boolean getLockSuccessful = (executeResult != null && executeResult == 1);
return getLockSuccessful;
}
});
log.info("最后一次,获取分布式锁redisKey:{}成功!", redisKey);
}
/**
*
* 释放Redis锁
*
* @param redisKey
* void
*/
public void releaseLock(String redisKey) {
if (StringUtils.isEmpty(redisKey)) {
return;
}
this.redisTemplate.delete(redisKey);
}
/**
* 获取分布式锁
*
* @param redisKey
* @param keepLockTimeLenght
* @param keepLockTimeUnit
* @return
*/
public boolean tryLock(String redisKey, int keepLockTimeLenght, TimeUnit keepLockTimeUnit) {
AssertUtil.notNull(redisKey);
AssertUtil.notNull(keepLockTimeLenght);
AssertUtil.notNull(keepLockTimeUnit);
String currentThreadQualifiedId = RedisLockComponentUtil.getCurrentThreadQualifiedId();
String threadQualifiedId = (String) this.redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(threadQualifiedId)
&& StringUtils.equals(threadQualifiedId, currentThreadQualifiedId)) {
return true;
}
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptText(SETNX_EXPIRE_TIME_SCRIPT);
List<String> keyList = Arrays.asList(redisKey);
long time = TimeUnit.MILLISECONDS.convert(keepLockTimeLenght, keepLockTimeUnit);
Long executeResult = (Long) this.redisTemplate.execute(redisScript, keyList, currentThreadQualifiedId, time);
boolean isGettingRedisLockSuccessful = (executeResult != null && executeResult == 1);
if (!isGettingRedisLockSuccessful) {
log.warn("redisKey:{},已经被占用,无法获取锁,不进行重试!", redisKey);
}else{
log.info("redisKey:{},获取锁成功", redisKey);
}
return isGettingRedisLockSuccessful;
}
public boolean trySetValue(String redisKey,String value, int keepLockTimeLenght, TimeUnit keepLockTimeUnit) {
AssertUtil.notNull(redisKey);
AssertUtil.notNull(value);
AssertUtil.notNull(keepLockTimeLenght);
AssertUtil.notNull(keepLockTimeUnit);
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptText(SETNX_EXPIRE_TIME_SCRIPT);
List<String> keyList = Arrays.asList(redisKey);
long time = TimeUnit.MILLISECONDS.convert(keepLockTimeLenght, keepLockTimeUnit);
Long executeResult = (Long) this.redisTemplate.execute(redisScript, keyList, value, time);
boolean isGettingRedisLockSuccessful = (executeResult != null && executeResult == 1);
if (!isGettingRedisLockSuccessful) {
log.warn("redisKey:{},已经被占用,trySetValue失败,不进行重试!", redisKey);
}else{
log.info("redisKey:{},trySetValue成功,value:{}", redisKey,value);
}
return isGettingRedisLockSuccessful;
}
public String getLockOwner(String redisLockKey) {
AssertUtil.notNull(redisLockKey);
String threadQualifiedId = (String) this.redisTemplate.opsForValue().get(redisLockKey);
return threadQualifiedId;
}
}