1. 程式人生 > 資料庫 >redis分散式鎖的應用

redis分散式鎖的應用

前言:

專案需求,搞了搞

  1. 實現了鎖的重入
  2. 參考了別人的博文實現了AOP註解形式的鎖、統一配置

參考博文地址:

https://www.cnblogs.com/lijiasnong/p/9952494.html

這邊看了下比較主流幾個分散式鎖的應用,最終選擇的redis
原因是:
1、懶(伺服器已有redis做快取,不想再去安裝zuukeeper)
2、評估認為redis的分散式鎖已能滿足當下應用

正文 - 摘錄核心程式碼:

  1. RedisReentrantLock

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * redis分散式鎖 - 使用 ThreadLocal 做執行緒隔離,實現鎖的重入機制
 * @date 2020/12/11 9:13
 * @author wei.heng
 */
@Component
public class RedisReentrantLock {

	/** 鎖的失效時間 */
	private static final Integer EXPIRE_SECONDS = 20;
	/** 鎖的字首,方便統一檢視 */
	private static final String LOCK_PREFIX = "LOCK:";

	private ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<>();

	private RedisService redisService;

	@Autowired
	public RedisReentrantLock(RedisService redisService) {
		this.redisService = redisService;
	}

	/**
	 *
	 * 新增分散式鎖
	 * @param key 鍵
	 * @return boolean 請求鎖是否成功
	 * @date 2020/12/11 10:52
	 * @author wei.heng
	 */
	public boolean lock(String key){
		key = LOCK_PREFIX + key;
		Map<String, Integer> refs = currentLockers();
		Integer refCnt = refs.get(key);
		if(refCnt != null){
			// 當前執行緒已經加鎖,這裡屬於鎖的重入,計數器加1
			refs.put(key, refCnt + 1);
			return true;
		}
		boolean ok = this._lock(key);
		if(!ok){
			return false;
		}
		refs.put(key, 1);
		return true;
	}

	/**
	 *
	 * 釋放鎖
	 * @param key 鍵
	 * @return boolean 釋放是否成功
	 * @date 2020/12/11 10:53
	 * @author wei.heng
	 */
	public boolean unlock(String key){
		key = LOCK_PREFIX + key;
		Map<String, Integer> refs = currentLockers();
		Integer refCnt = refs.get(key);
		if(refCnt == null){
			return false;
		}
		refCnt -= 1;
		if(refCnt > 0){
			refs.put(key, refCnt);
		} else {
			refs.remove(key);
			this._unlock(key);
		}
		return true;
	}

	private boolean _lock(String key){
		return redisService.setIfAbsent(key, "", EXPIRE_SECONDS);
	}

	private void _unlock(String key){
		redisService.deleteObject(key);
	}

	private Map<String, Integer> currentLockers(){
		Map<String, Integer> refs = lockers.get();
		if(refs != null){
			return refs;
		}
		lockers.set(new HashMap<>());
		return lockers.get();
	}

}

  1. RedisService 核心程式碼
    /**
     *
     * 如果key不存在,則設值
     * @param key 鍵
     * @param value 值
     * @param exptime 過期時間 - 單位:秒
     * @return boolean 設值是否成功(如果已有該key存在,則返回false)
     * @date 2020/12/11 10:42
     * @author wei.heng
     */
    public boolean setIfAbsent(final String key, final Serializable value, final long exptime) {
        Boolean result = (Boolean) redisTemplate.execute((RedisCallback<Boolean>) connection -> {
            RedisSerializer valueSerializer = redisTemplate.getValueSerializer();
            RedisSerializer keySerializer = redisTemplate.getKeySerializer();
            Object obj = connection.execute("set", keySerializer.serialize(key),
                valueSerializer.serialize(value),
                "NX".getBytes(StandardCharsets.UTF_8),
                "EX".getBytes(StandardCharsets.UTF_8),
                String.valueOf(exptime).getBytes(StandardCharsets.UTF_8));
            return obj != null;
        });
        return result;
    }

到這裡,redis的分散式鎖已經可以使用了。
看到網上別人的帖子,為了方便,
下面繼續做了下抄襲、修改
再次重申,抄襲的地址:

https://www.cnblogs.com/lijiasnong/p/9952494.html
  1. RedisLockable 新建註解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * redis分散式鎖
 * @date 2020/12/11 11:48
 * @author wei.heng
 */
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLockable {
    /** redis鎖的關鍵字(keys),可使用SpEL表示式代表引數值 */
    String[] value() default "";
}
  1. ReflectParamNames
    這個類需要引入jar包,我這裡搜了個最新的
        <dependency>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.12.1.GA</version>
        </dependency>

import javassist.*;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 反射獲取引數名
 * @date 2020/12/11 14:03
 * @author wei.heng
 */
public class ReflectParamNames {

	private static Logger log = LoggerFactory.getLogger(ReflectParamNames.class);
	private  static ClassPool pool = ClassPool.getDefault();

	static{
		ClassClassPath classPath = new ClassClassPath(ReflectParamNames.class);
		pool.insertClassPath(classPath);
	}

	public static String[] getNames(String className,String methodName) {
		CtClass cc = null;
		try {
			cc = pool.get(className);
			CtMethod cm = cc.getDeclaredMethod(methodName);
			// 使用javaassist的反射方法獲取方法的引數名
			MethodInfo methodInfo = cm.getMethodInfo();
			CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
			LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
			if (attr == null) return new String[0];

			int begin = 0;

			String[] paramNames = new String[cm.getParameterTypes().length];
			int count = 0;
			int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;

			for (int i = 0; i < attr.tableLength(); i++){
				//  為什麼 加這個判斷,發現在windows 跟linux執行時,引數順序不一致,通過觀察,實際的引數是從this後面開始的
				if (attr.variableName(i).equals("this")){
					begin = i;
					break;
				}
			}

			for (int i = begin+1; i <= begin+paramNames.length; i++){
				paramNames[count] = attr.variableName(i);
				count++;
			}
			return paramNames;
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			try {
				if(cc != null) cc.detach();
			} catch (Exception e2) {
				log.error(e2.getMessage());
			}


		}
		return new String[0];
	}
}
  1. RedisLockAspect
import com.google.common.base.Joiner;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 註解形式的redis分散式鎖
 * @date 2020/12/11 14:10
 * @author wei.heng
 */
@Aspect
@Component
public class RedisLockAspect {

	private RedisReentrantLock redisLock;

	@Autowired
	public RedisLockAspect(RedisReentrantLock redisLock) {
		this.redisLock = redisLock;
	}

	@Around("@annotation(com.ruoyi.common.security.annotation.RedisLockable)")
	public Object around(ProceedingJoinPoint point) throws Throwable {
		Signature signature = point.getSignature();
		MethodSignature methodSignature = (MethodSignature) signature;
		Method method = methodSignature.getMethod();
		String targetName = point.getTarget().getClass().getName();
		String methodName = point.getSignature().getName();
		Object[] arguments = point.getArgs();

		RedisLockable redisLockable = method.getAnnotation(RedisLockable.class);
		String redisKey = getLockKey(targetName, methodName, redisLockable.value(), arguments);
		boolean isLock = redisLock.lock(redisKey);
		if(isLock) {
			try {
				return point.proceed();
			} finally {
				redisLock.unlock(redisKey);
			}
		} else {
			System.out.println("===未獲取到鎖:" + redisKey);
			throw new GetRedisLockFailureException();
		}
	}

	private String getLockKey(String targetName, String methodName, String[] keys, Object[] arguments) {

		StringBuilder sb = new StringBuilder();
		sb.append(targetName).append(".").append(methodName);

		if(keys != null) {
			String keyStr = Joiner.on(".").skipNulls().join(keys);
			String[] parameters = ReflectParamNames.getNames(targetName, methodName);
			ExpressionParser parser = new SpelExpressionParser();
			Expression expression = parser.parseExpression(keyStr);
			EvaluationContext context = new StandardEvaluationContext();
			int length = parameters.length;
			if (length > 0) {
				for (int i = 0; i < length; i++) {
					context.setVariable(parameters[i], arguments[i]);
				}
			}
			String keysValue = expression.getValue(context, String.class);
			sb.append("#").append(keysValue);
		}
		return sb.toString();
	}

}

到這裡全部結束,可以直接使用@RedisLockable 進行分散式鎖的同步控制了()

測試:

在這裡插入圖片描述
發出兩個postman請求,引數相同,第一個OK,第二個返回409
修改其中某個引數、導致不是同一把鎖,兩個請求均通過(通過的場景就不截圖了)
在這裡插入圖片描述