用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;
	}
 
}