方式系统接口的重复调用,在分布式系统中A服务重复向B服务发送指令,导致B服务重复消费消息。
在单体系统中类似新增操作重复指令,导致系统参数多天重复数据。
方式一
数据库增加唯一索引,保证数据的唯一性
方式二
接口增加幂等性校验
具体方式如下:
①定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Ide {
}
②aop 实现
@Aspect
@Component
public class IdeAspect {
protected Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private RedisTemplate redisTemplate;
private final String msg ="token 失效,请刷新页面后再进行提交!";
@Pointcut("@annotation(com.laiease.common.annotation.Ide)")
public void idePointCut() {
}
@Around("idePointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
JSONObject json = JSONObject.parseObject(HttpHelper.getBodyString(request));
Optional<JSONObject> optional = Optional.ofNullable(json);
String ide = optional.map(item -> item.getString("ide")).orElseThrow(() -> new RuntimeException(msg));
Map map = (Map) redisTemplate.opsForValue().get(ide);
if (map!=null&&(methodName.equals(map.get("method"))&&1==(int)map.get("status"))) {
map.put("status",0);
redisTemplate.opsForValue().set(ide, map);
} else {
throw new RuntimeException(msg);
}
R r = (R) joinPoint.proceed();
Map result = (Map) r.get("data");
if(null!= result.get("status") && (boolean)result.get("status")){
redisTemplate.delete(ide);
}else{
map.put("status",1);
redisTemplate.opsForValue().set(ide, map);
}
return r;
}
@AfterThrowing(pointcut = "idePointCut()", throwing = "ex")
public void afterThrowing(Throwable ex) {
if (!msg.equals(ex.getMessage())) {
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
JSONObject json = JSONObject.parseObject(HttpHelper.getBodyString(request));
String ide = json.getString("ide");
Map map = (Map) redisTemplate.opsForValue().get(ide);
if (map != null) {
map.put("status", 1);
redisTemplate.opsForValue().set(ide, map);
}
}
}
}
③定义获取token的接口供前端调用,实现接口可以使用雪花算法或者其他
@Override
public String getMethodsToken(JSONObject jsonObject) {
Map map = new HashMap();
Optional<JSONObject> optional = Optional.ofNullable(jsonObject);
String method = optional.map(item -> item.getString("method")).orElseThrow(() -> new RuntimeException("异常操作"));
String ide = CmUtil.getUUID();
map.put("method",method);
map.put("status",1);
redisTemplate.opsForValue().set(ide, map);
return ide;
}
④使用
在Controller 层 新增接口或者其他方法接口上增加注解
@Ide
@Lelog("保存用户")
@ApiOperation(value = "保存用户")
@PostMapping(value = "/save")
public R save(@RequestBody User user) throws Exception {
return R.ok(userService.saveOne(user));
}
补充:HttpHelper 工具类
@Slf4j
public class HttpHelper {
private final static String xssWhitelist = PropertiesUtil.builder("config.properties").getProperty("sys.xss.whitelist");
public static String getBodyString(HttpServletRequest request) {
StringBuilder sb = new StringBuilder();
String result = "";
try (InputStream inputStream = request.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
result = sb.toString();
if (!isUrlPass(request.getRequestURI())) {
// xss sql 过滤
result = xssSqlLeach(result);
}
} catch (IOException e) {
log.error(e.toString());
}
return result;
}
private static String xssSqlLeach(String body) {
if (body == null || body.isEmpty()) {
return body;
}
StringBuilder sb = new StringBuilder(body.length());
for (int i = 0; i < body.length(); i++) {
char c = body.charAt(i);
switch (c) {
case '>':
sb.append("》");// 转义大于号
break;
case '<':
sb.append("《");// 转义小于号
break;
case '\'':
sb.append("‘");// 转义单引号
break;
case '\"':
sb.append('"');// 转义双引号
break;
case '&':
sb.append("&");// 转义&
break;
default:
String s1 = c + "";
String s = s1.replaceAll(".*([';]+|(--)+).*", "");
sb.append(s);
break;
}
}
return sb.toString();
}
private static boolean isUrlPass(String url) {
String[] urlList = xssWhitelist.split(";");
return ArrayUtils.contains(urlList, url);
}
}
使用redis 分布式锁处理接口幂等性 redis 分布式锁处理接口幂等性
网友评论