redis分散式鎖的應用
阿新 • • 發佈:2020-12-11
前言:
專案需求,搞了搞
- 實現了鎖的重入
- 參考了別人的博文實現了AOP註解形式的鎖、統一配置
參考博文地址:
https://www.cnblogs.com/lijiasnong/p/9952494.html
這邊看了下比較主流幾個分散式鎖的應用,最終選擇的redis
原因是:
1、懶(伺服器已有redis做快取,不想再去安裝zuukeeper)
2、評估認為redis的分散式鎖已能滿足當下應用
正文 - 摘錄核心程式碼:
- 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(); } }
- 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
- 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 ""; }
- 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];
}
}
- 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
修改其中某個引數、導致不是同一把鎖,兩個請求均通過(通過的場景就不截圖了)