关于分布式架构下接口的幂等性和并发性控制
废话不多说直接上代码
主要思路通过注解形式实现幂等,利用redis的lua脚本解决并发执行
定义注解
/**
* 幂等注解
* get请求直接 key
* post的请求 body格式 user.name
*/
@Target(ElementType.METHOD) //注解目标(方法上,类上)
@Documented
@Retention(RetentionPolicy.RUNTIME)//编译生效
public @interface Idempotence {
String value() default "";
}
根据注解进行环绕切面
我们可以跟前端约定一个页面的随机数 仅针对于页面的 idemKey 放入请求头里
@Aspect
@Component
public class IdempotenceAspect {
private final static Integer IdemError = 91111;
private final static String IdemWebHeader = "idemKey";//和H5约定的页面随机数
private final static String IDEM = "IDEM:";
private final static Integer ttl1 = 1000;
private final static Integer ttl10 = 10000;
@Autowired(required = false)//不强行注入
private HttpServletRequest request;
@Autowired(required = false)//不强行注入
private HttpServletResponse response;
//业务层日志
@Around("@annotation(idempotence)")
public void idem(ProceedingJoinPoint joinPoint, Idempotence idempotence) throws Throwable {
if (request == null) {
return;
}
String methodType = request.getMethod();//获取请求方法
String idemWeb = request.getHeader(IdemWebHeader);//获取请求头参数
String url = request.getRequestURI();//获取路径
ArrayList<String> signList = new ArrayList<>();//参数集
signList.add(url);
if (StringUtils.isNotEmpty(idemWeb)) {
signList.add(idemWeb);
}
//处理get和requestParam的参数集
Map<String, String[]> parameterMap = request.getParameterMap();
if (parameterMap != null && parameterMap.size() > 0) {
for (String key : parameterMap.keySet()) {
signList.add(key + parameterMap.get(key)[0]);
}
}
String signKey = "";
if ("GET".equals(methodType)) {
} else if ("POST".equals(methodType)) {
signList.addAll(getBodyParam(joinPoint)); //处理post-body对象属性
}
Collections.sort(signList);//注:一定要用集合工具的排序方法排序,以保证这个参数集是按一定规则排序后的结果集
for (String it : signList) {
signKey = signKey + it; //拼接redis的key
}
String key = IDEM + getMD5(signKey);//以免key过长 做长度加密控制 32位
if (idemRedis(key, signList)) return;//redis处理
joinPoint.proceed();
}
/**
* 获取方法的body参数
*/
List getBodyParam(ProceedingJoinPoint joinPoint) throws IllegalAccessException, InstantiationException {
ArrayList<String> arrayList = new ArrayList<>();
//获取进入的方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();//拿到方法
for (Parameter parameter : methodSignature.getMethod().getParameters()) {
//遍历参数
if (parameter.isAnnotationPresent(RequestBody.class)) {
for (Object arg : joinPoint.getArgs()) {
//获取入参对象
if(arg.getClass().getName().equals(parameter.getParameterizedType().getTypeName())){
//将对象反射 处理期属性名和属性值
Field[] declaredFields = arg.getClass().getDeclaredFields();
for (Field field : declaredFields) {
field.setAccessible(true);
arrayList.add(field.getName() + field.get(arg));
}
break;
}
}
}
}
return arrayList;
}
/**
* MD532位加密并大写
*
* @param message
* @return
*/
public static String getMD5(String message) {
MessageDigest messageDigest = null;
StringBuffer md5StrBuff = new StringBuffer();
try {
messageDigest = MessageDigest.getInstance("MD5");
messageDigest.reset();
messageDigest.update(message.getBytes("UTF-8"));
byte[] byteArray = messageDigest.digest();
for (int i = 0; i < byteArray.length; i++) {
if (Integer.toHexString(0xFF & byteArray[i]).length() == 1) {
md5StrBuff.append("0").append(Integer.toHexString(0xFF & byteArray[i]));
} else {
md5StrBuff.append(Integer.toHexString(0xFF & byteArray[i]));
}
}
} catch (Throwable t) {
System.out.println("加密异常" + t.getMessage());
}
return md5StrBuff.toString().toUpperCase();// 字母大写
}
/***
* redis幂等处理
* @param signList 参数签名集
* @param key 幂等缓存id
* @return ture 重复提交 false 非重复提交
*/
private boolean idemRedis(String key, ArrayList<String> signList) {
//想简单化的实现 采用的lua脚本
String script = "if (redis.call('exists',KEYS[1])==0) then " +
"redis.call('set', KEYS[1], ARGV[1]);" +
"redis.call('pexpire', KEYS[1], ARGV[2]);" +
"return 1 ;" +
"end " +
"return 0 ;";
DefaultRedisScript<Boolean> redisScript = new DefaultRedisScript<>(script, Boolean.class);
Boolean isHave = (Boolean) RedisUtils.redisTemplate.execute(redisScript, Collections.singletonList(key), signList, ttl10);
if (isHave) {
log.info("请求成功入参:{}", signList);
} else {
log.info("请求幂等处理,存在重复提交参数,入参:{}", signList);
returnJson("该请求以重复提交");//这个里面可以封装成自己框架内的同一响应对象
return true;
}
return false;
}
/**
* 返回客户端数据
*
* @param json
* @throws Exception
*/
private void returnJson(String json) {
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);
} catch (IOException e) {
} finally {
if (writer != null) {
writer.close();
}
}
}
记录一次工作中的接口幂等处理形式,只要是一种思想分享,如果大家有更好的想法麻烦告诉我下,或者有什么可以改进的地方欢迎留言,知识一定越学越多,技术一定越磨越精~