一般接口并发控制的做法:
- 前端做loading限制(如果绕过前端发起请求这种方式则会失效)
- 后端根据特定请求参数、间隔时间来限制(后端会有一定开发工作量)
基于上述情况,开发了一款基于注解使用的并发控制器,需要使用到redis来做分布式锁
使用说明:
- @CurrentControl 注解申明
- field 请求中的哪些字段需要用来做唯一校验,如果不写那么就是全字段
- locktime 锁的时间(秒数),方法运行结束会自动释放锁,如果未运行结束,会在locktime到期后释放锁
- handling 并发控制触发后的处理方式,默认HandlingType.THROWEXP(抛出业务异常),HandlingType.QUEUE(队列模式,暂未开放)
- expMessage 自定义异常信息,如果没有自定义异常信息会抛出系统默认异常信息
需要的朋友可以根据下面的源码自行修改使用,想直接使用的话只需要把redis注入改下就行了
/**
* <p>Title:并发控制注解</p>
* <p>Description:</p>
* @author QIQI
* @params
* @return
* @throws
* @date 2020/11/13 14:15
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentControl {
String[] field() default {"$"}; //筛选字段,哪些字段用来判断是否被并发
long locktime() default 1000; //锁定时间,毫秒
HandlingType handling() default HandlingType.THROWEXP; //并发控制触发后的处理方式
String expMessage() default "触发并发控制,请求被拒绝,请稍候尝试!"; //触发后的处理方式选择THROWEXP,那么自定义抛出的异常信息,如果未自定义将会使用默认错误信息返回
}
/**
* <p>Title:</p>
* <p>Description:</p>
*
* @author QIQI
* @date
*/
@Data
public class CurrentBean implements Serializable {
private static final long serialVersionUID = 356924975742559410L;
private String[] field; //筛选字段,哪些字段用来判断是否被并发
private long locktime; //锁定时间,毫秒
private HandlingType handling; //并发控制触发后的处理方式
private String expMessage; //触发后的处理方式选择THROWEXP,那么自定义抛出的异常信息,如果未自定义将会使用默认错误信息返回
}
/**
* <p>Title:</p>
* <p>Description:</p>
*
* @author QIQI
* @date
*/
@Slf4j
@Aspect
@Order(-1)
@Component
public class CurrentControlAspect {
@Resource(name = "redisPoolManagerImpl")
private RedisManager redisManager;
@Autowired
private RequestParsing requestParsing;
private static final String CURRENT_PREFIX = "CURRENT_PREFIX:";
private static final ThreadLocal<String> LOCAL_ASPECT = new TransmittableThreadLocal<>(); //记录当前请求redis锁的key,不重复解析
private static final ThreadLocal<Boolean> LOCAL_ASPECT_CHECK = new TransmittableThreadLocal<>(); //记录本线程redis加锁状态
@Pointcut("@annotation(com.wms.framework.concurrency.CurrentControl)")
public void aspect() {
}
/**
* <p>Title:前置切面</p>
* <p>Description:</p>
*
* @return void
* @throws
* @author QIQI
* @params [point]
* @date 2019-07-19 13:55
*/
@Before("aspect() && @annotation(currentControl)")
public void before(JoinPoint point, CurrentControl currentControl) {
CurrentBean currentBean = getAnnotation( currentControl );
HashMap<String, String> lockMap = new HashMap<>();
requestParsing.getLockMap( lockMap, new ArrayList<>( Arrays.asList( currentBean.getField() ) ) );
String key = CURRENT_PREFIX + lockMap;
String resp = redisManager.set( key, "", "NX", "EX", currentBean.getLocktime() );
if (null != resp && resp.equalsIgnoreCase( "OK" )) {
LOCAL_ASPECT_CHECK.set( true );
LOCAL_ASPECT.set( key );
} else {
if (currentBean.getHandling().equals( HandlingType.THROWEXP )) {
log.warn( "触发并发控制,lockMap is {}", lockMap );
throwexp();
}
}
}
private void throwexp() {
//抛出自定义异常或者默认异常信息
LOCAL_ASPECT_CHECK.set( false );
throw new RuntimeException( "并发限制触发" );
}
/**
* <p>Title:后置切面</p>
* <p>Description:</p>
*
* @return void
* @throws
* @author QIQI
* @params [point]
* @date 2019-07-19 13:55
*/
@After("aspect() && @annotation(currentControl)")
public void after(JoinPoint point, CurrentControl currentControl) {
if (LOCAL_ASPECT_CHECK.get()) {
redisManager.del( LOCAL_ASPECT.get() ); //处理结束,无论是否成功,请求返回之前必须释放锁
LOCAL_ASPECT.remove();
}
LOCAL_ASPECT_CHECK.remove();
}
/**
* <p>Title:获取注解值</p>
* <p>Description:</p>
*
* @return java.util.Map<java.lang.String, java.lang.Object>
* @throws
* @author QIQI
* @params [point]
* @date 2019-07-19 13:52
*/
private CurrentBean getAnnotation(CurrentControl currentControl) {
CurrentBean currentBean = new CurrentBean();
currentBean.setExpMessage( currentControl.expMessage() );
currentBean.setField( currentControl.field() );
currentBean.setHandling( currentControl.handling() );
currentBean.setLocktime( currentControl.locktime() );
return currentBean;
}
}
/**
* <p>Title:并发控制触发时的处理方式</p>
* <p>Description:</p>
*
* @author QIQI
* @date
*/
public enum HandlingType {
THROWEXP,
QUEUE
}
@Component
@Order(10000)
@WebFilter(filterName = "HttpServletRequestFilter", urlPatterns = "/")
public class HttpServletRequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig){
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
String path = "";
if (servletRequest instanceof HttpServletRequest && servletRequest.getInputStream() != null) {
path = ((HttpServletRequest) servletRequest).getRequestURI();
if(path.equals( "/health" )){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
requestWrapper = new RequestWrapper((HttpServletRequest) servletRequest);
}
//获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中
//在chain.doFiler方法中传递新的request对象
if (null == requestWrapper) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
filterChain.doFilter(requestWrapper, servletResponse);
}
if(servletResponse.getOutputStream() != null){
RequestWrapper.transmittableThreadLocal.remove();
}
}
@Override
public void destroy() {
}
}
/**
* <p>Title:请求解析器</p>
* <p>Description:</p>
*
* @author QIQI
* @date
*/
@Slf4j
@Service
public class RequestParsing {
@Autowired
private HttpServletRequest request;
/**
* <p>Title:获取锁定Map</p>
* <p>Description:</p>
*
* @return void
* @throws
* @author QIQI
* @params [locakMap, fieldList]
* @date 2020/11/13 16:37
*/
public void getLockMap(HashMap<String, String> locakMap, List<String> fieldList) {
//每次解析都会判断fieldList长度是否 > 0,如果== 0说明已经解析完成,无需再次深入解析
//解析优先级 header > parameter > body
parseHeader( locakMap, fieldList );
parseParameter( locakMap, fieldList );
parseObject( locakMap, fieldList );
}
/**
* <p>Title:解析head值</p>
* <p>Description:</p>
*
* @return void
* @throws
* @author QIQI
* @params [locakMap, fieldList]
* @date 2020/11/13 16:35
*/
private void parseHeader(HashMap<String, String> locakMap, List<String> fieldList) {
if (fieldList.get( 0 ).equals( "$" )) {
Enumeration<String> stringEnumeration = request.getHeaderNames();
while (stringEnumeration.hasMoreElements()) {
String key = stringEnumeration.nextElement();
locakMap.put( key, request.getHeader( key ) );
}
} else {
if (fieldList.size() > 0) {
forEachList( fieldList, "header", locakMap );
}
}
}
private void forEachList(List<String> fieldList, String type, HashMap<String, String> locakMap) {
Iterator<String> iterator = fieldList.iterator();
while (iterator.hasNext()) {
String key = iterator.next();
String headVal = "";
if (type.equals( "header" )) {
headVal = request.getHeader( key );
} else if (type.equals( "params" )) {
headVal = request.getParameter( key );
}
if (Optional.ofNullable( headVal ).isPresent()) {
locakMap.put( key, headVal );
iterator.remove();
}
}
}
/**
* <p>Title:解析parameter值</p>
* <p>Description:</p>
*
* @return void
* @throws
* @author QIQI
* @params [locakMap, fieldList]
* @date 2020/11/13 16:36
*/
private void parseParameter(HashMap<String, String> locakMap, List<String> fieldList) {
if (fieldList.size() > 0 && fieldList.get( 0 ).equals( "$" )) {
Enumeration<String> stringEnumeration = request.getParameterNames();
while (stringEnumeration.hasMoreElements()) {
String key = stringEnumeration.nextElement();
locakMap.put( key, request.getParameter( key ) );
}
} else {
if (fieldList.size() > 0) {
forEachList( fieldList, "params", locakMap );
}
}
}
/**
* <p>Title:解析body对象内部值</p>
* <p>Description:</p>
*
* @return void
* @throws
* @author QIQI
* @params [locakMap, fieldList]
* @date 2020/11/13 16:36
*/
private void parseObject(HashMap<String, String> locakMap, List<String> fieldList) {
if (fieldList.size() > 0 && fieldList.get( 0 ).equals( "$" )) {
Enumeration<String> stringEnumeration = request.getParameterNames();
while (stringEnumeration.hasMoreElements()) {
String key = stringEnumeration.nextElement();
locakMap.put( key, request.getParameter( key ) );
}
} else {
if (fieldList.size() > 0) {
JSONObject jsonObject = JSON.parseObject( RequestWrapper.transmittableThreadLocal.get() );
Iterator<String> iterator = fieldList.iterator();
while (iterator.hasNext()){
String key = iterator.next();
if(null != jsonObject.get( key )){
String val = String.valueOf( jsonObject.get( key ) );
locakMap.put( key,val );
iterator.remove();
}
}
}
}
}
}
/**
* <p>Title:HttpServletRequest 包装器</p>
* <p>Description:
* 解决: request.getInputStream()只能读取一次的问题
* 目标: 流可重复读
* </p>
* @author QIQI
* @params
* @return
* @throws
* @date 2020/11/16 14:24
*/
public class RequestWrapper extends HttpServletRequestWrapper {
public static final ThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();
/**
* 请求体
*/
private String mBody;
public RequestWrapper(HttpServletRequest request) {
super(request);
// 将body数据存储起来
mBody = getBody(request);
transmittableThreadLocal.set(mBody);
}
/**
* 获取请求体
*
* @param request 请求
* @return 请求体
*/
private String getBody(HttpServletRequest request) {
return getBodyString(request);
}
/**
* 获取请求体
*
* @return 请求体
*/
public String getBody() {
return mBody;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
// 创建字节数组输入流
final ByteArrayInputStream bais = new ByteArrayInputStream(mBody.getBytes( StandardCharsets.UTF_8));
return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return bais.read();
}
};
}
/**
* 获取请求Body
*
* @param request
* @return
*/
private String getBodyString(ServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
}