问题描述:接口幂等性问题
解决办法:接口幂等问题传统方式是基于redis解决,由于项目中没有用到redis所以用ConcurrentHashMap+锁的方式模拟redis实现接口幂等问题的处理
上代码:
第一版:直接在业务层添加代码,代码入侵性比较高
//每次请求的唯一key
String key = record.getSuperviseId().toString() + record.getSendCourtId().toString() + record.getReceiveCourtId().toString();
try {
//对保全局map进行操作时进行加锁操作 注:这里的锁采用com.google.common.collect.Interners包下的字符串锁
// this会锁整个类对象范围太大
synchronized (lock.intern("superviseChatRecord" + key)) {
//在map里面获取key 如果能获取到则证明在间隔时间内已经执行过一次
Long time = KEY_MAP.get(key);
if (time != null && System.currentTimeMillis() - time < 1000) {
throw new BusinessException("不能重复发送消息!");
}
//如果获取不到则证明是第一次或者超过间隔时间保存的key 则运行继续保存或执行后续的操作
//将唯一键保存入map
KEY_MAP.put(key, System.currentTimeMillis());
//执行保存逻辑
return this.save(record);
}
} finally {
//最后如果key对应的时间值大于间隔时间则移除key
//移除原因 1.防止map过大内存溢出 2.下次请求大于间隔时间则认为是两次请求不是连点造成的幂等性问题
if (KEY_MAP.get(key)-System.currentTimeMillis()>1000){
KEY_MAP.remove(key);
}
}
第二版:用自定义注解+AOP的方式解决
package com.iexecloud.shzxzh.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Description : 幂等 自定义注解,基于post请求,请求入参为json
* @Author : ;lirui
* @Date : 2022/9/22 14:06
* @Version : 1.0
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/**
* 需要排除的字段,逗号分割
*/
String exclude() default "";
/**
* 指定字段幂等,逗号分割
*/
String include() default "";
}
package com.iexecloud.shzxzh.aspect;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.iexecloud.shzxzh.annotation.Idempotent;
import com.iexecloud.shzxzh.exception.BusinessException;
import com.iexecloud.shzxzh.util.ReqDedupHelper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Description : 基于注解的幂等功能
* @Author : ;lirui
* @Date : 2022/9/22 14:06
* @Version : 1.0
*/
@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class IdempotentAspect {
private static final ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap<>();
@Around("@annotation(com.iexecloud.shzxzh.annotation.Idempotent)")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
if (sra == null) {
return joinPoint.proceed();
}
Object[] args = joinPoint.getArgs();
HttpServletRequest request = sra.getRequest();
String methodType = request.getMethod();
if (!"POST".equalsIgnoreCase(methodType) || args == null || args.length == 0) {
return joinPoint.proceed();
}
//只对POST请求做幂等校验
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
String methodName = method.getName();
Object response;
Idempotent idempotent = method.getAnnotation(Idempotent.class);
String include = idempotent.include();
String exclude = idempotent.exclude();
List includeList = StrUtil.split(include, ",");
List excludeList = StrUtil.split(exclude, ",");
String dedupMD5 = methodName + new ReqDedupHelper().dedupParamMD5(JSONUtil.toJsonStr(args[0]), includeList, excludeList);
synchronized (this) {
Long time = concurrentHashMap.get(dedupMD5);
if (time != null && System.currentTimeMillis() - time < 2000) {
throw new BusinessException("请勿重复提交");
}
concurrentHashMap.put(dedupMD5, System.currentTimeMillis());
}
try {
response = joinPoint.proceed();
} finally {
concurrentHashMap.remove(dedupMD5);
}
return response;
}
}
最后在controller层添加相关注解:
/**
* 发送消息
* @param
* @return
*/
//这里是指定唯一key包含的字段
@Idempotent(include = "superviseId,sendCourtId,receiveCourtId,content")
@PostMapping("sendMessage")
public R sendMessage(@RequestBody SuperviseChatRecord record) {
Boolean flag = superviseChatRecordService.sendMessage(record);
if (flag){
return R.ok("发送消息成功");
}
return R.failed("发送消息失败");
}
以上就是为解决消息幂等性所想到的解决方案,如有问题请及时提出,当然实现接口幂等性的解决方案还有很多种,包括用数据库实现,或者redis实现等等。
后续在自己的demo里添加了redis然后用redis实现的代码:
String key = record.getSuperviseId().toString() + record.getSendCourtId().toString() + record.getReceiveCourtId().toString();
synchronized (this){
//获取redis里的key
String time = redis.opsForValue().get(key);
//如果key已经存在或者key对应的时间小于间隔时间则抛异常
if (StringUtils.isNotBlank(time)&&System.currentTimeMillis()-Long.valueOf(time)<1000){
throw new BusinessException("切勿重复提交");
}
//如果redis里面没有值则添加key并设置过期时间
redis.opsForValue().set(key, String.valueOf(System.currentTimeMillis()),1, TimeUnit.SECONDS);
}
//执行业务逻辑