幂等性
什么是幂等性
HTTP/1.1中对幂等性的定义是:一次和多次请求某一个资源**对于资源本身**应该具有同样的结果(网络超时等问题除外)。也就是说,**其任意多次执行对资源本身所产生的影响均与一次执行的影响相同**。
简单来说,是指无论调用多少次都不会有不同结果的 HTTP 方法。
什么情况下需要幂等
业务开发中,经常会遇到重复提交的情况,无论是由于网络问题无法收到请求结果而重新发起请求,或是前端的操作抖动而造成重复提交情况。 在交易系统,支付系统这种重复提交造成的问题有尤其明显,比如:
1. 用户在APP上连续点击了多次提交订单,后台应该只产生一个订单;
2. 向支付宝发起支付请求,由于网络问题或系统BUG重发,支付宝应该只扣一次钱。 **很显然,声明幂等的服务认为,外部调用者会存在多次调用的情况,为了防止外部多次调用对系统数据状态的发生多次改变,将服务设计成幂等。**
解决方案
1. 乐观锁:基于版本号version实现, 在更新数据那一刻校验数据(会出现ABA问题)
2. 布式锁:redis 或 zookeeper 实现
3. version令牌: 防止页面重复提交
4. 防重表:防止新增脏数据
5. 消息队列:把请求快速缓冲起来,然后异步任务处理,优点:提高吞吐量,不足:不能及时响应返回对应结果,需要后续接口监听异步接口
实现幂等性
本次采用version令牌的方式实现幂等性,即采用 redis + version机制拦截器实现接口幂等性校验;
实现思路:
- 首先网关是全部请求的入口点,为了保证幂等性,即需要全局统一的version机制,先获取version,并且把version放入到redis中,然后请求业务接口时候,将上一步获取的version,放到header中(或者参数中)进行请求
- 服务端接收到对应的请求,首先采用拦截器的方式拦截对应参数,去redis中查找是否有存在该version
- 如果存在,执行业务逻辑之前在删除version,那么如果重复提交,由于version被删除,则返回给客户端提示 参数异常
- 如果本身就不存在,直接说明参数不合法
打开项目: common-spring-boot-starter
1.定义需要扫描的注解
com.open.capacity.common.annotation.ApiIdempotent
package com.open.capacity.common.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/\*\*
\* 定义接口 幂等的注解
\*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
2.定义需要启动幂等拦截器的注解,采用Import的方式
com.open.capacity.common.annotation.EnableApiIdempotent
package com.open.capacity.common.annotation;
import com.open.capacity.common.selector.ApiIdempotentImportSelector;
import org.springframework.context.annotation.Import;
import java.lang.annotation.\*;
/\*\*
\* 启动幂等拦截器
\* @author gitgeek
\* @create 2019年9月5日
\* 自动装配starter
\* 选择器
\*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(ApiIdempotentImportSelector.class)
public @interface EnableApiIdempotent {
}
3.导入的选择器(这里填写好要导入的全类名就行),导入ApiIdempotentConfig
com.open.capacity.common.selector.ApiIdempotentImportSelector
package com.open.capacity.common.selector;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;
/\*\*
\*
\*/
public class ApiIdempotentImportSelector implements ImportSelector {
/\*\*
\* Select and return the names of which class(es) should be imported based on
\* the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
\*
\* @param importingClassMetadata
\*/
@Override
public String\[\] selectImports(AnnotationMetadata importingClassMetadata) {
return new String\[\]{
"com.open.capacity.common.config.ApiIdempotentConfig"
};
}
}
4.ApiIdempotentConfig自动配置类,定义好ApiIdempotentInterceptor拦截器
com.open.capacity.common.config.ApiIdempotentConfig
package com.open.capacity.common.config;
import com.open.capacity.common.interceptor.ApiIdempotentInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
@ConditionalOnClass(WebMvcConfigurer.class)
public class ApiIdempotentConfig implements WebMvcConfigurer {
@Resource
private RedisTemplate redisTemplate ;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ApiIdempotentInterceptor(redisTemplate)).addPathPatterns("/\*\*") ;
}
}
5.ApiIdempotentInterceptor拦截器,对ApiIdempotent注解的方法 或者类进行拦截幂等接口
com.open.capacity.common.interceptor.ApiIdempotentInterceptor
package com.open.capacity.common.interceptor;
import com.open.capacity.common.annotation.ApiIdempotent;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
@AllArgsConstructor
public class ApiIdempotentInterceptor implements HandlerInterceptor {
private static final String VERSION\_NAME = "version";
private RedisTemplate redisTemplate ;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// TODO: 2019-08-27 获取目标方法上的幂等注解
ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);
if (methodAnnotation != null) {
checkApiIdempotent(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}
return true;
}
private void checkApiIdempotent(HttpServletRequest request) {
String version = request.getHeader(VERSION\_NAME);
if (StringUtils.isBlank(version)) {// header中不存在version
version = request.getParameter(VERSION\_NAME);
if (StringUtils.isBlank(version)) {// parameter中也不存在version
throw new IllegalArgumentException("无效的参数");
}
}
if (!redisTemplate.hasKey(version)) {
throw new IllegalArgumentException("不存在对应的参数");
}
Boolean bool = redisTemplate.delete(version);
if (!bool) {
throw new IllegalArgumentException("没有删除对应的version");
}
}
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
## 如何使用
1.UserCenterApp 用户中心,在启动类上加@EnableApiIdempotent启动幂等拦截器,然后通过@ApiIdempotent注解
**com.open.capacity.UserCenterApp**
package com.open.capacity;
import com.open.capacity.common.annotation.EnableApiIdempotent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Configuration;
import com.open.capacity.common.port.PortApplicationEnvironmentPreparedEventListener;
import com.open.capacity.log.annotation.EnableLogging;
/\*\*
\* @author 作者 owen E-mail: 624191343@qq.com
\* @version 创建时间:2018年4月5日 下午19:52:21
\* 类说明
\*/
@Configuration
@EnableLogging
@EnableDiscoveryClient
@SpringBootApplication
@EnableApiIdempotent
public class UserCenterApp {
public static void main(String\[\] args) {
// 固定端口启动
// SpringApplication.run(UserCenterApp.class, args);
//随机端口启动
SpringApplication app = new SpringApplication(UserCenterApp.class);
app.addListeners(new PortApplicationEnvironmentPreparedEventListener());
app.run(args);
}
}
2.SysUserController 控制层
@ApiIdempotent 标记了该方法需要接口幂等
**com.open.capacity.user.controller.SysUserController**
/\*\*
\* @author 作者 owen E-mail: 624191343@qq.com
\* @version 创建时间:2017年11月12日 上午22:57:51
\*用户
\*/
@Slf4j
@RestController
@Api(tags = "USER API")
public class SysUserController {
@Autowired
private SysUserService sysUserService;
/\*\*
\* 测试幂等接口
\* @param sysUser
\* @return
\*/
@PostMapping("/users/save")
@ApiIdempotent
public Result save(@RequestBody SysUser sysUser) {
return sysUserService.saveOrUpdate(sysUser);
}
}
## 整体流程
1.首先进去网关,**api-gateway**项目,先通过 getVersion 获取对应的版本号,这个版本号可以根据自己业务修改对应的格式
**com.open.capacity.client.controller.UserController**
/\*\*
\* @author 作者 owen E-mail: 624191343@qq.com
\* @version 创建时间:2018年4月5日 下午19:52:21
\*/
@RestController
public class UserController {
@GetMapping("/getVersion")
public Result token() {
String str = RandomUtil.randomString(24);
StrBuilder token = new StrBuilder();
token.append(str);
redisTemplate.opsForValue().set(token.toString(), token.toString(),300);
return Result.succeed(token.toString(),"");
}
}
curl -i -X GET \\
'http://127.0.0.1:9200/getVersion'
{
"datas": "8329lw34ii7ctsgibdfdkm2z",
"resp\_code": 0,
"resp\_msg": ""
}
2.请求幂等接口,这里单独写一个接口;@ApiIdempotent被该注解标记的接口,需要在在头部或者在参数加入version参数,否则无法过接口;
com.open.capacity.user.controller.SysUserController
/\*\*
\* @author 作者 owen E-mail: 624191343@qq.com
\* @version 创建时间:2017年11月12日 上午22:57:51
\*用户
\*/
@Slf4j
@RestController
@Api(tags = "USER API")
public class SysUserController {
@Autowired
private SysUserService sysUserService;
/\*\*
\* 测试幂等接口
\* @param sysUser
\* @return
\*/
@PostMapping("/users/save")
@ApiIdempotent
public Result save(@RequestBody SysUser sysUser) {
return sysUserService.saveOrUpdate(sysUser);
}
}
curl -i -X POST \\
-H "Content-Type:application/json" \\
-H "version:qcrro9jkymsx2t5b6ij3lc0p" \\
-d \\
'{
"id": "",
"username": "admin",
"nickname": "admin",
"phone": "15914395926",
"sex": "0",
"roleId": "1"
}' \\
'http://127.0.0.1:9200/api-user/users/save'