系统幂等性设计与实践

幂等性

什么是幂等性

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'  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值