第一步 引入依赖pom.xml:
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.16.3</version> <!-- 使用最新版本 --></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.11.1</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-core</artifactId><version>2.11.1</version></dependency><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-annotations</artifactId><version>2.11.1</version></dependency></dependencies>
第二步 增加配置:
redis:host: localhost # Redis服务器地址database: 4 # Redis数据库索引(默认为0)port: 6379 # Redis服务器连接端口password: w23456timeout: 30000ms # 连接超时时间(毫秒)
第三步 增加自定义注解:
package com.hxnwm.ny.diancan.common.annotaiton;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @ClassName SubmitLimit* @Description TODO* @Author wdj* @Date 2023/7/25 18:01* @Version*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface SubmitLimit {/*** 指定时间内不可重复提交(仅相对上一次发起请求时间差),单位毫秒* @return*/int waitTime() default 1000;/*** 指定请求头部key,可以组合生成签名* @return*/String[] customerHeaders() default {};/*** 自定义重复提交提示语* @return*/String customerTipMsg() default "";}
package com.hxnwm.ny.diancan.common.aspect;import com.hxnwm.ny.diancan.common.annotaiton.SubmitLimit;
import com.hxnwm.ny.diancan.common.lock.DistributedLockService;
import com.hxnwm.ny.diancan.common.result.Result;
import com.hxnwm.ny.diancan.common.utils.JacksonUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.ExtendedServletRequestDataBinder;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Objects;
/*** @ClassName SubmitLimitAspect* @Description TODO* @Author wdj* @Date 2023/7/27 9:07* @Version*/
@Order(1)
@Aspect
@Component
public class SubmitLimitAspect {private static final Logger LOGGER = LoggerFactory.getLogger(SubmitLimitAspect.class);/*** redis分割符*/private static final String REDIS_SEPARATOR = ":";/*** 默认重复提交提示语*/private static final String DEFAULT_TIP_MSG = "服务正在处理,请勿重复提交!";@Autowiredprivate DistributedLockService distributedLockService;/*** 方法调用环绕拦截*/@Around(value = "@annotation(com.hxnwm.ny.diancan.common.annotaiton.SubmitLimit)")public Object doAround(ProceedingJoinPoint joinPoint){HttpServletRequest request = getHttpServletRequest();if(Objects.isNull(request)){return Result.error(5001,"请求参数不能为空!");}//获取注解配置的参数SubmitLimit submitLimit = getSubmitLimit(joinPoint);//组合生成key,通过key实现加锁和解锁String lockKey = buildSubmitLimitKey(joinPoint, request, submitLimit.customerHeaders());//尝试在指定的时间内加锁boolean lock = distributedLockService.acquireLock(lockKey, submitLimit.waitTime());if(!lock){String tipMsg = StringUtils.isEmpty(submitLimit.customerTipMsg()) ? DEFAULT_TIP_MSG : submitLimit.customerTipMsg();return Result.error(5001,tipMsg);}try {//继续执行后续流程return execute(joinPoint);} finally {//执行完毕之后,手动将锁释放distributedLockService.releaseLock();}}/*** 执行任务* @param joinPoint* @return*/private Object execute(ProceedingJoinPoint joinPoint){try {return joinPoint.proceed();} catch (Exception e) {return Result.error(5001,e.getMessage());} catch (Throwable e) {LOGGER.error("业务处理发生异常,错误信息:",e);return Result.error(5001,"业务处理发生异常");}}/*** 获取请求对象* @return*/private HttpServletRequest getHttpServletRequest(){RequestAttributes ra = RequestContextHolder.getRequestAttributes();ServletRequestAttributes sra = (ServletRequestAttributes)ra;HttpServletRequest request = sra.getRequest();return request;}/*** 获取注解值* @param joinPoint* @return*/private SubmitLimit getSubmitLimit(JoinPoint joinPoint){MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();Method method = methodSignature.getMethod();SubmitLimit submitLimit = method.getAnnotation(SubmitLimit.class);return submitLimit;}/*** 组合生成lockKey* 生成规则:接口名+方法名+请求参数签名(对请求头部参数+请求body参数,取SHA1值)* @param joinPoint* @param request* @param customerHeaders* @return*/private String buildSubmitLimitKey(JoinPoint joinPoint, HttpServletRequest request, String[] customerHeaders){//请求参数=请求头部+请求bodyString requestHeader = getRequestHeader(request, customerHeaders);String requestBody = getRequestBody(joinPoint.getArgs());String requestParamSign = DigestUtils.sha1Hex(requestHeader + requestBody);String submitLimitKey = new StringBuilder().append(joinPoint.getSignature().getDeclaringType().getSimpleName()).append(REDIS_SEPARATOR).append(joinPoint.getSignature().getName()).append(REDIS_SEPARATOR).append(requestParamSign).toString();return submitLimitKey;}/*** 获取指定请求头部参数* @param request* @param customerHeaders* @return*/private String getRequestHeader(HttpServletRequest request, String[] customerHeaders){if (Objects.isNull(customerHeaders)) {return "";}StringBuilder sb = new StringBuilder();for (String headerKey : customerHeaders) {sb.append(request.getHeader(headerKey));}return sb.toString();}/*** 获取请求body参数* @param args* @return*/private String getRequestBody(Object[] args){if (Objects.isNull(args)) {return "";}StringBuilder sb = new StringBuilder();for (Object arg : args) {if (arg instanceof HttpServletRequest|| arg instanceof HttpServletResponse|| arg instanceof MultipartFile|| arg instanceof BindResult|| arg instanceof MultipartFile[]|| arg instanceof ModelMap|| arg instanceof Model|| arg instanceof ExtendedServletRequestDataBinder|| arg instanceof byte[]) {continue;}sb.append(JacksonUtils.toJson(arg));}return sb.toString();}
}
第四步 增加分布式服务:
package com.hxnwm.ny.diancan.common.lock;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;@Component
public class DistributedLockService {// 锁的过期时间,单位秒private static final int LOCK_EXPIRE = 30;@Autowiredprivate RedissonClient redissonClient;private RLock lock;public boolean acquireLock(String lockKey,int lockExpire) {if (lockExpire<=0){lockExpire=LOCK_EXPIRE;}try {lock= redissonClient.getLock(lockKey);// 尝试获取锁,设置锁的自动过期时间为 LOCK_EXPIRE 秒boolean tryLock = lock.tryLock(lockExpire, TimeUnit.SECONDS);return tryLock;} catch (Exception e) {e.printStackTrace();Thread.currentThread().interrupt();return false;}}public void releaseLock() {lock.unlock();}
}
最后一步:使用。在控制层增加@SubmitLimit(customerHeaders = {"token"}, customerTipMsg = "正在加紧为您处理,请勿重复下单!")
在不改变原有逻辑上增加额外的功能
/*** 下单详情** @author knight* @time 2022/3/22 13:51*/@SubmitLimit(customerHeaders = {"token"}, customerTipMsg = "正在加紧为您处理,请勿重复下单!")@RequestMapping(value = "submit/detail", method = RequestMethod.POST)public Result submitDetail(@Validated @RequestBody CartOrderInfo cartOrderInfo) {return Result.handlerData(this.orderManage.submitDetail(cartOrderInfo));}