前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何设计一个幂等接口

如何设计一个幂等接口

作者头像
贪挽懒月
发布2020-06-02 16:03:44
1.2K0
发布2020-06-02 16:03:44
举报
文章被收录于专栏:JavaEEJavaEE

一、什么叫接口幂等性

幂等性,就是只多次操作的结果是一致的。这里可能有人会有疑问。

问:为什么要多次操作结果都一致呢?比如我查询数据,每次查出来的都一样,即使我修改了每次查出来的也都要一样吗?

答:我们说的多次,是指同一次请求中的多次操作。这个多次操作可能会在如下情况发生:

  • 前端重复提交。比如这个业务处理需要2秒钟,我在2秒之内,提交按钮连续点了3次,如果非幂等性接口,那么后端就会处理3次。如果是查询,自然是没有影响的,因为查询本身就是幂等操作,但如果是新增,本来只是新增1条记录的,连点3次,就增加了3条,这显然不行。
  • 响应超时而导致请求重试:在微服务相互调用的过程中,假如订单服务调用支付服务,支付服务支付成功了,但是订单服务接收支付服务返回的信息时超时了,于是订单服务进行重试,又去请求支付服务,结果支付服务又扣了一遍用户的钱。如果真这样的话,用户估计早就提着砍刀来了。

二、如何设计一个幂等接口

经过上面的描述,相信大家已经清楚了什么叫接口幂等性及其重要性。那么如何设计呢?大致有以下几种方案:

  • 数据库记录状态机制:即每次操作前先查询状态,根据数据库记录的状态来判断是否要继续执行操作。比如订单服务调用支付服务,每次调用之前,先查询该笔订单的支付状态,从而避免重复操作。
  • token机制:请求业务接口之前,先请求token接口(会将生成的token放入redis中)获取一个token,然后请求业务接口时,带上token。在进行业务操作之前,我们先获取请求中携带的token,看看在redis中是否有该token,有的话,就删除,删除成功说明token校验通过,并且继续执行业务操作;如果redis中没有该token,说明已经被删除了,也就是已经执行过业务操作了,就不让其再进行业务操作。大致流程如下:

token机制

  • 其他方案:接口幂等性设计还有很多其他方案,比如全局唯一id、乐观锁等。本文主要讲token机制的使用,若感兴趣可以自行研究。

三、用token机制实现接口的幂等性

1、pom.xml:主要是引入了redis相关依赖

代码语言:javascript
复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- spring-boot-starter-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<!-- jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<!-- commons-lang3 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>
<!-- org.json/json -->
<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20190722</version>
</dependency>

2、application.yml:主要是配置redis

代码语言:javascript
复制
server:
  port: 6666
spring:
  application:
    name: idempotent-api
  redis:
    host: 192.168.2.43
    port: 6379

3、业务代码:

  • 新建一个枚举,列出常用返回信息,如下:
代码语言:javascript
复制
@Getter
@AllArgsConstructor
public enum ResultEnum {
    REPEATREQUEST(405, "重复请求"),
    OPERATEEXCEPTION(406, "操作异常"),
    HEADERNOTOKEN(407, "请求头未携带token"),
    ERRORTOKEN(408, "token正确")
    ;
    private Integer code;
    private String msg;
}
  • 新建一个JsonUtil,当请求异常时往页面中输出json:
代码语言:javascript
复制
public class JsonUtil {
    private JsonUtil() {}
    public static void writeJsonToPage(HttpServletResponse response, String msg) {
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try {
            writer = response.getWriter();
            writer.print(msg);
        } catch (IOException e) {
        } finally {
            if (writer != null)
                writer.close();
        }
    }
}
  • 新建一个RedisUtil,用来操作redis:
代码语言:javascript
复制
@Component
public class RedisUtil {
    
    private RedisUtil() {}

    private static RedisTemplate redisTemplate;

    @Autowired
    public  void setRedisTemplate(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //设置序列化Value的实例化对象
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        RedisUtil.redisTemplate = redisTemplate;
    }
    
    /**
     * 设置key-value,过期时间为timeout秒
     * @param key
     * @param value
     * @param timeout
     */
    public static void setString(String key, String value, Long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }
    
    /**
     * 设置key-value
     * @param key
     * @param value
     */
    public static void setString(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }
    
    /**
     * 获取key-value
     * @param key
     * @return
     */
    public static String getString(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }
    
    /**
     * 判断key是否存在
     * @param key
     * @return
     */
    public static boolean isExist(String key) {
        return redisTemplate.hasKey(key);
    }
    
    /**
     * 删除key
     * @param key
     * @return
     */
    public static boolean delKey(String key) {
        return redisTemplate.delete(key);
    }
}
  • 新建一个TokenUtil,用来生成和校验token:生成token没什么好说的,这里为了简单直接用uuid生成,然后放入redis中。校验token,如果用户没有携带token,直接返回false;如果携带了token,但是redis中没有这个token,说明已经被删除了,即已经访问了,返回false;如果redis中有,但是redis中的token和用户携带的token不一致,也返回false;有且一致,说明是第一次访问,就将redis中的token删除,然后返回true。
代码语言:javascript
复制
public class TokenUtil {

    private TokenUtil() {}
    
    private static final String KEY = "token";
    private static final String CODE = "code";
    private static final String MSG = "msg";
    private static final String JSON = "json";
    private static final String RESULT = "result";
    
    /**
     * 生成token并放入redis中
     * @return
     */
    public static String createToken() {
        String token = UUID.randomUUID().toString();
        RedisUtil.setString(KEY, token, 60L);
        return RedisUtil.getString(KEY);
    }
    
    /**
     * 校验token
     * @param request
     * @return
     * @throws JSONException 
     */
    public static Map<String, Object> checkToken(HttpServletRequest request) throws JSONException {
        String headerToken = request.getHeader(KEY);
        JSONObject json = new JSONObject();
        Map<String, Object> resultMap = new HashMap<>();
        // 请求头中没有携带token,直接返回false
        if (StringUtils.isEmpty(headerToken)) {
            json.put(CODE, ResultEnum.HEADERNOTOKEN.getCode());
            json.put(MSG, ResultEnum.HEADERNOTOKEN.getMsg());
            resultMap.put(RESULT, false);
            resultMap.put(JSON, json.toString());
            return resultMap;
        }
        
        if (StringUtils.isEmpty(RedisUtil.getString(KEY))) {
            // 如果redis中没有token,说明已经访问成功过了,直接返回false
            json.put(CODE, ResultEnum.REPEATREQUEST.getCode());
            json.put(MSG, ResultEnum.REPEATREQUEST.getMsg());
            resultMap.put(RESULT, false);
            resultMap.put(JSON, json.toString());
            return resultMap;
        } else {
            // 如果redis中有token,就删除掉,删除成功返回true,删除失败返回false
            String redisToken = RedisUtil.getString(KEY);
            boolean result = false;
            if (!redisToken.equals(headerToken)) {
                json.put(CODE, ResultEnum.ERRORTOKEN.getCode());
                json.put(MSG, ResultEnum.ERRORTOKEN.getMsg());
            } else {
                result = RedisUtil.delKey(KEY);
                String msg = result ? null : ResultEnum.OPERATEEXCEPTION.getMsg();
                json.put(CODE, 400);
                json.put(MSG, msg);
            }
            resultMap.put(RESULT, result);
            resultMap.put(JSON, json.toString());
            return resultMap;
        }
    }
}
  • 新建一个注解,用来标注需要进行幂等的接口:
代码语言:javascript
复制
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedIdempotent {
}
  • 接着要新建一个拦截器,对有@NeedIdempotent注解的方法进行拦截,进行自动幂等。
代码语言:javascript
复制
public class IdempotentInterceptor implements HandlerInterceptor{

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,Object object) throws JSONException {
        // 拦截的不是方法,直接放行
        if (!(object instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) object;
        Method method = handlerMethod.getMethod();
        // 如果是方法,并且有@NeedIdempotent注解,就自动幂等
        if (method.isAnnotationPresent(NeedIdempotent.class)) {
            Map<String, Object> resultMap = TokenUtil.checkToken(httpServletRequest);
            boolean result = (boolean) resultMap.get("result");
            String json = (String) resultMap.get("json");
            if (!result) {
                JsonUtil.writeJsonToPage(httpServletResponse, json);
            }
            return result;
        } else {
            return true;
        }
    }
    
    @Override
    public void postHandle(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse, Object o,ModelAndView modelAndView) {
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse,Object o, Exception e) {
    }
}
  • 然后将这个拦截器配置到spring中去:
代码语言:javascript
复制
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(idempotentInterceptor())
                .addPathPatterns("/**");   
    }
    @Bean
    public IdempotentInterceptor idempotentInterceptor() {
        return new IdempotentInterceptor();
    }

}
  • 最后新建一个controller,就可以愉快地进行测试了。
代码语言:javascript
复制
@RestController
@RequestMapping("/idempotent")
public class IdempotentApiController {

    @NeedIdempotent
    @GetMapping("/hello")
    public String hello() {
        return "are you ok?";
    }
    
    @GetMapping("/token")
    public String token() {
        return TokenUtil.createToken();
    }
}

访问/token,不需要什么校验,访问/hello,就会自动幂等,每一次访问都要先获取token,一个token不能用两次。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、什么叫接口幂等性
  • 二、如何设计一个幂等接口
  • 三、用token机制实现接口的幂等性
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档