什么是幂等?
在编程中一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。例如,“setTrue()”函数就是一个幂等函数,无论多次执行,其结果都是一样的.更复杂的操作幂等保证是利用唯一交易号(流水号)实现。 ——来自百度百科
通俗的讲:
同一个系统,同一个接口方法,相同的参数执行多次,得到的结果都是一样的。
幂等解决的场景
如果某些场景接口方法重复执行的话,系统就会出现预想不到的后果,如:
- 前端信息多次提交
- 消息服务多次触发
- 系统对接外部接口等
应对这些场景时,幂等性就尤为重要了。
接口幂等实现方案(Redis+AOP)
实现思路:
- 设置注解redis过期时间;
- 通过AOP切面,拦截请求;
- 每次执行请求时,将接口加密信息保存至redis数据库中;
- 插入数据成功则请求成功;
- 否则拦截此次请求。
代码实现:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
/**
* 过期时间 毫秒
*/
long expireMillis();
}
@Aspect
@Component
@Slf4j
@ConditionalOnClass(RedisTemplate.class)
public class IdempotentAspect{
@Resource
private RedisTemplate<String,String> redisTemplate;
private static final String KEY_FORMAT = "idempotentSubmit:%s";
@Pointcut("@annotation(com.test.anno.Idempotent)")
public void execute(){}
@Around("execute()")
@SneakyThrows
public Object around(ProceedingJoinPoint joinPoint) {
Method method = ((MethodSignature)joinPoint.getSignature()).getMethod();
Idempotent annotation = method.getAnnotation(Idempotent.class);
if (Objects.isNull(annotation)) {
return joinPoint.proceed();
}
String key = getKeyString(joinPoint, method);
String redisKey = MD5Util.string2MD5(key);
Boolean b = redisTemplate.opsForValue().setIfAbsent(redisKey, redisKey);
if (b != null && b) {
redisTemplate.expire(redisKey,annotation.expireMillis(), TimeUnit.MILLISECONDS);
return joinPoint.proceed();
}else {
throw new RuntimeException("您操作的太快,请稍后再试");;
}
}
private String getKeyString(ProceedingJoinPoint joinPoint, Method method) {
Object[] args = joinPoint.getArgs();
StringBuilder builder = new StringBuilder(method.getName()+"-");
for (Object arg : args) {
//针对请求参数不为空才去处理
if (arg != null) {
if (arg instanceof HttpSession) {
HttpSession session = (HttpSession) arg;
String email = (String) session.getAttribute("email");
builder.append(email).append("-");
}else {
builder.append(MD5Util.string2MD5(arg.toString())).append("-");
}
}
}
return String.format(KEY_FORMAT,builder.deleteCharAt(builder.length() - 1).toString());
}
}
@RestController
@RequestMapping("/idempotent")
@Slf4j
public class IdempotentController {
@Idempotent(key = "/testIdempotent", expireMillis = 2000, desc = "测试")
@GetMapping("/testIdempotent")
public String testIdempotent(){
return "hello";
}
}