基于Spring的AOP实现接口锁
需求
当调用某一个接口的时候,如果相同参数同时访问的时候需要进行有序访问。但是如果参数不同的情况下就不需要进行等待直接访问。举个例子:现在有A、B、C三个线程同时访问一个接口的时候。A和B的参数是完全相同的,但是C的参数和A、B不同。假如A线程先访问的接口、B就只能等待A线程执行完成才能访问或者等待时间超时返回。但是这时候由于C的参数不同可以不用等待A、B的执行完成。直接执行
通过AOP实现目标需求
package org.jeecg.common.aspect.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @Description:接口锁注解
*
* 使用:
* @InterfaceLock(key = "travel:%s:#userId:#contracttype",limit = 10)
* %S 代表当前登录人
* #参数 代表从参数中获取,支持多个参数
* 生成的redis key: attachDto:e9ca23d68d884d4ebb19d07889727dae:userId--1123123:contracttype--0
*
* @Author JH050180
* @Date 2023/3/15 11:48
* @Version: V1.0
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface InterfaceLock {
String key();
/**
* 最大的等待时间 默认10s
*/
long maxWaitTime() default 10;
/**
* 最大的等待时间单位。 超过该时间就直接返回。默认单位为秒
* @return java.util.concurrent.TimeUnit
* @Author: lwc
* @Date: 2023/3/15 11:48
**/
TimeUnit companyTime() default TimeUnit.SECONDS;
}
切面的实现
package org.jeecg.common.aspect;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.aspect.annotation.InterfaceLock;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.modules.base.util.StringUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
/**
* @Author lwc
* @Description TODO 接口参数锁
* 对于一个接口,如果相同参数的两个请求同一时间请求到接口。这时候可能对系统数据造成影响,这时候这两个线程必须依次执行
* 当然。如果这时候第三个请求进来的时候 如果请求参数不同,可以不需要等待直接执行。那下面的接口锁AOP适用于该场景。
* 实现:
* 当不同的请求,请求的接口是一样的,并且参数一样的情况下。它们在locks获取的ReentrantLock对象是同一个对象。但是
* 不同的请求参数拿到的ReentrantLock不是同一个对象。因为它们具有线程之间的隔离性。而下面concurrentLinkedDeque队列
* 它的主要作用是用来缓存ReentrantLock对象的。为什么在调用完目标方法以后,并且没有其他线程尝试加锁的时候。
* 需要将locks里面的数据清除。这是为了能够回收多余的ReentrantLock对象。并将对象放入到concurrentLinkedDeque
* 从而达到对象重复利用的效果,减少对象的创建提高程序的性能(有点想线程池的里面的核心线程)当concurrentLinkedDeque里面
* 空闲的对象大于maxLockSum的时候就会将对象释放。帮助jvm进行gc
*
* 使用:
* 请看@InterfaceLock注解
*
* @Date 2023/3/16 11:07
* @Version 1.0
*/
@Aspect
@Component
@Slf4j
public class InterfaceLockAspect {
private Map<String, ReentrantLock> locks = new ConcurrentHashMap<>();
private ConcurrentLinkedDeque<ReentrantLock> concurrentLinkedDeque = new ConcurrentLinkedDeque();
private Integer maxLockSum = 20;
@Pointcut("@annotation(org.jeecg.common.aspect.annotation.InterfaceLock)")
private void pointcut() {}
public InterfaceLockAspect() {
for (int i = 0; i < 10; i++) {
concurrentLinkedDeque.add(new ReentrantLock());
}
}
@Around("pointcut()")
public Object handleLock(ProceedingJoinPoint joinPoint) throws Throwable {
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
InterfaceLock interfaceLock = method.getAnnotation(InterfaceLock.class);
long maxWaitTime = interfaceLock.maxWaitTime();//最大等待时间
TimeUnit timeUnit = interfaceLock.companyTime();//时间的单位
String methodName = method.getName();
StringBuffer sb = new StringBuffer()
.append(methodName)
.append(": ")
.append(interfaceLock.key());
String lockKey = sb.toString();
String key = getLockKey(sysUser,joinPoint, lockKey);
ReentrantLock lock = locks.computeIfAbsent(key, new Function<String, ReentrantLock>() {
@Override
public ReentrantLock apply(String key) {
ReentrantLock reentrantLock = concurrentLinkedDeque.pollFirst();
if (Objects.isNull(reentrantLock)) {
/**说明队列当中已经没有了 ReentrantLock锁 需要手动创建*/
reentrantLock = new ReentrantLock();
}
return reentrantLock;
}
});
/**尝试获取锁*/
if (lock.tryLock(maxWaitTime, timeUnit)) {
try {
/**执行目标方法*/
return joinPoint.proceed();
} finally {
//是否有其他线程在等待获取锁。 如果存在则为true
if (!lock.hasQueuedThreads()) {
//如果没有其他相同参数的请求在尝试获取锁的时候,将locks的锁给删除(为了GC)
// 并将该锁入队(目的是为了重复利用,减少锁的创建)
locks.remove(key);
concurrentLinkedDeque.offerLast(lock);
int size = concurrentLinkedDeque.size();
if (size > maxLockSum) {
//如果目前的锁队列的数量大于最大数量限制需要减少空闲锁队列里面一半的锁
for (int i = 0; i < size / 2; i++) {
//help gc
concurrentLinkedDeque.pollLast();
}
}
}
lock.unlock();
}
} else {
return Result.error("接口请求超时,请稍后再试");
}
}
private String getLockKey(LoginUser sysUser, ProceedingJoinPoint joinPoint, String key) throws IllegalAccessException {
List<Map<String, Object>> parametersList = new ArrayList<>();
key = key.replace("%s", String.valueOf(sysUser.getId()));
// 获取方法的参数
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = signature.getParameterNames();
Parameter[] parameters = signature.getMethod().getParameters();
// 获取post请求的参数
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
if (parameter.isAnnotationPresent(RequestBody.class)) {
Object obj = args[i];
Class<?> clazz = obj.getClass();
while (clazz != null && clazz != Object.class) {
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
Object value = field.get(obj);
String paramName = field.getName();
Map<String, Object> map = new HashMap<>();
map.put("name", paramName);
map.put("value", value != null ? value.toString() : null);
addParameter(map, key, parametersList);
}
clazz = clazz.getSuperclass();
}
}
}
// 获取get请求的参数
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
Enumeration<String> parameterNamesEnum = request.getParameterNames();
while (parameterNamesEnum.hasMoreElements()) {
String paramName = parameterNamesEnum.nextElement();
String paramValue = request.getParameter(paramName);
Map<String, Object> map = new HashMap<>();
map.put("name", paramName);
map.put("value", paramValue);
addParameter(map, key, parametersList);
}
}
// 用实际值替换参数占位符
for (Map<String, Object> map : parametersList) {
String paramName = map.get("name").toString();
String paramValue = map.get("value") != null ? map.get("value").toString() : "null";
key = key.replace("#" + paramName, paramName + "--" + paramValue);
}
return key;
}
private void addParameter(Map<String, Object> parameter, String key, List<Map<String, Object>> parametersList) {
if (parameter != null && parameter.get("name") != null) {
String paramName = parameter.get("name").toString();
if (key.contains("#" + paramName)) {
parametersList.add(parameter);
}
}
}
}
纯属分享,实现思路
大家可以进行多方面测试,有任何问题可以留言。