一、概念
1. 幂等性定义
幂等性原本是数学上的概念,公式:f(x)=f(f(x)) 能够成立的数学性质。用在编程领域,则意为对同一个系统,使用同样的条件,一次请求和重复的多次请求对系统资源的影响是一致的
。举个简单例子来说,就是我们在添加一个学生信息的时候,由于某种原因(网络抖动之类),导致发送多次请求,只能保存一次提交的信息。
2. 幂等性需注意的问题
- 幂等性的实质是一次或多次请求同一个资源,其结果是相同的。其关注的是对资源产生的影响(副作用)而不是结果,结果可以不同
- 网络超时、服务宕机等问题,不是幂等的范围
3. 重复提交和幂等对比
- 重复提交:重复提交是在第一次请求成功的情况下,人为的进行多次操作,从而导致不满足幂等性要求的服务多次改变数据状态。
- 幂等:更多使用的情况是第一次请求知道结果(比如常见的网络抖动导致连接超时)或者失败异常情况下,发起多次请求的,其目的是多次确认第一次请求成功,却不会因为多次请求而出现多次的状态变化。
二、实现思路
- 前端发送请求前获取token,将token存入redis中
- 请求接口时携带token,接口添加注解
- 后端拦截器拦截本次请求,判断redis中是否有此token
- token存在,正常执行,token不存在,属于重复提交,参数中未携带token,终止操作。
三、代码具体实现
项目所用技术:SpringBoot+redis,导包以及redis工具类此处省略,只写校验流程,以免篇幅过长。
接口幂等性拦截器
/**
* @author lqh
* @date 2020/9/21
* 接口幂等性拦截器
*/
@Component
public class IdempotenceInterceptor implements HandlerInterceptor{
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
if(!(handler instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod= (HandlerMethod) handler;
Method method=handlerMethod.getMethod();
Idempotence methodIdempotence=method.getAnnotation(Idempotence.class);
if(methodIdempotence != null){
// 幂等性校验, 校验通过则放行, 校验失败则抛出异常(自定义异常,返回重复提交信息)
check(request);
}
return true;
}
private void check(HttpServletRequest request) {
tokenService.checkToken(request);
}
}
注册拦截器
/**
* @author lqh
* @date 2020/9/21
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private IdempotenceInterceptor idempotenceInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(idempotenceInterceptor);
}
}
自定义注解
/**
* @author lqh
* @date 2020/9/21
* * 自定义注解,在需要保证幂等性接口的Controller的方法上使用此注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotence {
}
业务处理接口
public interface TokenService {
/**
* 生成token
* @return
*/
ServerResponse createToken();
/**
* 校验token
* @param request
*/
void checkToken(HttpServletRequest request);
}
业务处理接口实现类(重点)
@Service("TokenService")
public class TokenServiceImpl implements TokenService {
private static final String TOKEN_NAME="token";
@Autowired
private RedisTemplate businessTemplate;
@Override
public ServerResponse createToken() {
String str= RandomUtil.UUID32();
StrBuilder token=new StrBuilder();
token.append(Constant.TOKEN_PREFIX).append(str);
RedisUtil.set(token.toString(),token.toString(),300,businessTemplate);
return ServerResponse.success(token.toString());
}
@Override
public void checkToken(HttpServletRequest request) {
String token=request.getHeader(TOKEN_NAME);
if(StringUtils.isBlank(token)){
token=request.getParameter(TOKEN_NAME);
if(StringUtils.isBlank(token)){
throw new ServiceException(ResponseCodeEnum.ILLEGAL_ARGUMENT.getMsg());
}
}
if(!RedisUtil.hasKey(token,businessTemplate)){
throw new ServiceException(ResponseCodeEnum.REPETITIVE_OPERATION.getMsg());
}
if(!RedisUtil.del(businessTemplate,token)){
throw new ServiceException(ResponseCodeEnum.REPETITIVE_OPERATION.getMsg());
}
}
}
校验方法中的删除token一定校验是否删除成功,不能采用直接删除token的方式
(如果多个线程同时操作到删除这个地方,不校验的话,会出现并发问题,仍然会有重复提交操作的发生)
Controller获取token
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
private TokenService tokenService;
@RequestMapping("/getToken")
public ServerResponse token() {
return tokenService.createToken();
}
/* @Idempotence
@RequestMapping("/testToken")
public ServerResponse testToken() {
return ServerResponse.success("测试接口");
}*/
}
自定义异常
public class ServiceException extends RuntimeException{
private String code;
private String msg;
public ServiceException() {
}
public ServiceException(String msg) {
this.msg = msg;
}
public ServiceException(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
枚举类响应状态
public enum ResponseCodeEnum {
// 系统模块
SUCCESS(200, "操作成功"),
SAVE_SUCCESS(201,"保存成功"),
DELETE_SUCCESS(202,"删除成功!"),
UPDATE_SUCCESS(403,"更新成功!"),
ERROR(400, "操作失败"),
SAVE_ERROR(401,"保存失败"),
DELETE_ERROR(402,"删除失败!"),
UPDATE_ERROR(403,"更新成功"),
SERVER_ERROR(500, "服务器异常"),
EXCEPTION(-1,"Exception"),
// 通用模块 1xxxx
ILLEGAL_ARGUMENT(10000, "参数不合法"),
REPETITIVE_OPERATION(10001, "请勿重复操作"),
ACCESS_LIMIT(10002, "请求太频繁, 请稍后再试"),
MAIL_SEND_SUCCESS(10003, "邮件发送成功"),
PARAMETER_NOT_EMPTY(10004,"参数不能为空"),
GRMMAR_RULES_ILLEGAL(10005,"语法规则有误,请检查!"),
//**模块
;
ResponseCodeEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
private Integer code;
private String msg;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
四、前端实践方案说明
这里采用更新操作说明,假设你要修改学生的基本信息。
- 修改信息的Controller接口添加@Idempotence注解
- 调出修改信息对话框的同时获取token
- 提交信息时,将token放入headers里面或者参数中即可
按我上面说的案例,采用token机制实现幂等性,有一个问题就是,在我获取token时存入redis的时间是五分钟,假如我六分钟之后再次提交,就会提示重复操作。对于这个问题,我还没有想到最优的方案,欢迎各位评论区指导,感谢。