一、概念

幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次

比如:

订单接口, 不能多次创建订单

支付接口, 重复支付同一笔订单只能扣一次钱

支付宝回调接口, 可能会多次回调, 必须处理重复回调

普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次

等等

二、常见解决方案

唯一索引 -- 防止新增脏数据

token机制 -- 防止页面重复提交

悲观锁 -- 获取数据的时候加锁(锁表或锁行)

乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据

分布式锁 -- redis(jedis、redisson)或zookeeper实现

状态机 -- 状态变更, 更新数据时判断状态

三、本文实现

本文采用第2种方式实现, 即通过redis + token机制实现接口幂等性校验

四、实现思路

为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:

如果存在, 正常处理业务逻辑, 并从redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示

如果不存在, 说明参数不合法或者是重复请求, 返回提示即可

五、项目简介

springboot

redis

@ApiIdempotent注解 + 拦截器对请求进行拦截

@ControllerAdvice全局异常处理

压测工具: jmeter

说明:

本文重点介绍幂等性核心实现, 关于springboot如何集成redis、ServerResponse、ResponseCode等细枝末节不在本文讨论范围之内, 有兴趣的小伙伴可以查看我的Github项目: https://github.com/wangzaiplus/springboot/tree/wxw

六、代码实现

pom

redis.clients

jedis

2.9.0

JedisUtil

package com.wangzaiplus.test.util;

import com.wangzaiplus.test.common.Constant;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import redis.clients.jedis.Jedis;

import redis.clients.jedis.JedisPool;

import java.util.Set;

@Component

public class JedisUtil {

@Autowired

private JedisPool jedisPool;

private Jedis getJedis() {

return jedisPool.getResource();

}

/**

* 设值

*

* @param key

* @param value

* @return

*/

public String set(String key, String value) {

Jedis jedis = null;

try {

jedis = getJedis();

return jedis.set(key, value);

} catch (Exception e) {

throw new RuntimeException("set异常: key: " + key + ", cause: " + e.getMessage());

} finally {

close(jedis);

}

}

/**

* 设值

*

* @param key

* @param value

* @param expireTime 过期时间, 单位: s

* @return

*/

public String set(String key, String value, int expireTime) {

Jedis jedis = null;

try {

jedis = getJedis();

String result = jedis.set(key, value);

if (Constant.Redis.OK.equals(result)) {

jedis.expire(key, expireTime);

}

return result;

} catch (Exception e) {

throw new RuntimeException("set-expireTime异常: key: " + key + ", cause: " + e.getMessage());

} finally {

close(jedis);

}

}

/**

* 删除key

*

* @param key

* @return

*/

public Long del(String key) {

Jedis jedis = null;

try {

jedis = getJedis();

return jedis.del(key.getBytes());

} catch (Exception e) {

throw new RuntimeException("del异常: key: " + key + ", cause: " + e.getMessage());

} finally {

close(jedis);

}

}

/**

* 判断key是否存在

*

* @param key

* @return

*/

public Boolean exists(String key) {

Jedis jedis = null;

try {

jedis = getJedis();

return jedis.exists(key.getBytes());

} catch (Exception e) {

throw new RuntimeException("exists异常: key: " + key + ", cause: " + e.getMessage());

} finally {

close(jedis);

}

}

public void close(Jedis jedis) {

if (null != jedis) {

jedis.close();

}

}

}

说明: 篇幅有限, 完整JedisUtil请参考GitHub

自定义注解@ApiIdempotent

package com.wangzaiplus.test.annotation;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

/**

* 在需要保证 接口幂等性 的Controller的方法上使用此注解

*/

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

public @interface ApiIdempotent {

}

ApiIdempotentInterceptor拦截器

package com.wangzaiplus.test.interceptor;

import com.wangzaiplus.test.annotation.ApiIdempotent;

import com.wangzaiplus.test.exception.ServiceException;

import com.wangzaiplus.test.service.TokenService;

import org.springframework.beans.factory.annotation.Autowired;

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;

/**

* 接口幂等性拦截器

*/

public class ApiIdempotentInterceptor 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();

ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class);

if (methodAnnotation != null) {

checkApiIdempotent(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示

}

return true;

}

private void checkApiIdempotent(HttpServletRequest request) {

tokenService.checkToken(request);

}

@Override

public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

}

@Override

public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

}

}

TokenServiceImpl

package com.wangzaiplus.test.service.impl;

import com.wangzaiplus.test.common.Constant;

import com.wangzaiplus.test.common.ResponseCode;

import com.wangzaiplus.test.common.ServerResponse;

import com.wangzaiplus.test.exception.ServiceException;

import com.wangzaiplus.test.service.TokenService;

import com.wangzaiplus.test.util.JedisUtil;

import com.wangzaiplus.test.util.RandomUtil;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;

import org.apache.commons.lang3.text.StrBuilder;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;

@Service

public class TokenServiceImpl implements TokenService {

private static final String TOKEN_NAME = "token";

@Autowired

private JedisUtil jedisUtil;

@Override

public ServerResponse createToken() {

String str = RandomUtil.generateStr(24);

StrBuilder token = new StrBuilder();

token.append(Constant.Redis.TOKEN_PREFIX).append(str);

jedisUtil.set(token.toString(), token.toString(), Constant.Redis.EXPIRE_TIME_MINUTE);

return ServerResponse.success(token.toString());

}

@Override

public void checkToken(HttpServletRequest request) {

String token = request.getHeader(TOKEN_NAME);

if (StringUtils.isBlank(token)) {// header中不存在token

token = request.getParameter(TOKEN_NAME);

if (StringUtils.isBlank(token)) {// parameter中也不存在token

throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg());

}

}

if (!jedisUtil.exists(token)) {

throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());

}

Long del = jedisUtil.del(token);

if (del <= 0) {

throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());

}

}

}

TestApplication

package com.wangzaiplus.test;

import com.wangzaiplus.test.interceptor.ApiIdempotentInterceptor;

import org.mybatis.spring.annotation.MapperScan;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.context.annotation.Bean;

import org.springframework.web.cors.CorsConfiguration;

import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import org.springframework.web.filter.CorsFilter;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@SpringBootApplication

@MapperScan("com.wangzaiplus.test.mapper")

public class TestApplication extends WebMvcConfigurerAdapter {

public static void main(String[] args) {

SpringApplication.run(TestApplication.class, args);

}

/**

* 跨域

* @return

*/

@Bean

public CorsFilter corsFilter() {

final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();

final CorsConfiguration corsConfiguration = new CorsConfiguration();

corsConfiguration.setAllowCredentials(true);

corsConfiguration.addAllowedOrigin("*");

corsConfiguration.addAllowedHeader("*");

corsConfiguration.addAllowedMethod("*");

urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);

return new CorsFilter(urlBasedCorsConfigurationSource);

}

@Override

public void addInterceptors(InterceptorRegistry registry) {

// 接口幂等性拦截器

registry.addInterceptor(apiIdempotentInterceptor());

super.addInterceptors(registry);

}

@Bean

public ApiIdempotentInterceptor apiIdempotentInterceptor() {

return new ApiIdempotentInterceptor();

}

}

OK, 目前为止, 校验代码准备就绪, 接下来测试验证

七、测试验证

获取token的控制器TokenController

package com.wangzaiplus.test.controller;

import com.wangzaiplus.test.common.ServerResponse;

import com.wangzaiplus.test.service.TokenService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@RequestMapping("/token")

public class TokenController {

@Autowired

private TokenService tokenService;

@GetMapping

public ServerResponse token() {

return tokenService.createToken();

}

}

TestController, 注意@ApiIdempotent注解, 在需要幂等性校验的方法上声明此注解即可, 不需要校验的无影响

package com.wangzaiplus.test.controller;

import com.wangzaiplus.test.annotation.ApiIdempotent;

import com.wangzaiplus.test.common.ServerResponse;

import com.wangzaiplus.test.service.TestService;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@RequestMapping("/test")

@Slf4j

public class TestController {

@Autowired

private TestService testService;

@ApiIdempotent

@PostMapping("testIdempotence")

public ServerResponse testIdempotence() {

return testService.testIdempotence();

}

}

获取token

查看redis

测试接口安全性: 利用jmeter测试工具模拟50个并发请求, 将上一步获取到的token作为参数

header或参数均不传token, 或者token值为空, 或者token值乱填, 均无法通过校验, 如token值为"abcd"

八、注意点(非常重要)

上图中, 不能单纯的直接删除token而不校验是否删除成功, 会出现并发安全性问题, 因为, 有可能多个线程同时走到第46行, 此时token还未被删除, 所以继续往下执行, 如果不校验jedisUtil.del(token)的删除结果而直接放行, 那么还是会出现重复提交问题, 即使实际上只有一次真正的删除操作, 下面重现一下

稍微修改一下代码:

再次请求

再看看控制台

虽然只有一个真正删除掉token, 但由于没有对删除结果进行校验, 所以还是有并发问题, 因此, 必须校验

九、总结

其实思路很简单, 就是每次请求保证唯一性, 从而保证幂等性, 通过拦截器+注解, 就不用每次请求都写重复代码, 其实也可以利用spring aop实现, 无所谓

如果小伙伴有什么疑问或者建议欢迎提出

springboot幂等性_springboot + redis + 注解 + 拦截器 实现接口幂等性校验相关推荐

  1. @slf4j注解_SpringBoot + Redis + 注解 + 拦截器 实现接口幂等性校验

    一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如: 订单接口, 不能多次创建订单 支付接口, 重复支付同一笔订单只能扣一次钱 支付宝回调接口, 可能会多 ...

  2. redis 判断存在性_springboot + redis + 注解 + 拦截器 实现接口幂等性校验

    提醒:后面有些图片模糊,请点击原文查看清晰图片 一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如: 订单接口, 不能多次创建订单 支付接口, 重复支付同 ...

  3. springboot + redis + 注解 + 拦截器 实现接口幂等性校验

    点击上方"方志朋",选择"设为星标" 做积极的人,而不是积极废人 来源:https://www.jianshu.com/p/6189275403ed 一.概念 ...

  4. jmeter校验结果_Springboot + redis + 注解 + 拦截器来实现接口幂等性校验

    来自:简书,作者:wangzaiplus 链接:https://www.jianshu.com/p/6189275403ed 一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证 ...

  5. Springboot + redis + 注解 + 拦截器来实现接口幂等性校验

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者:wangzaiplus www.jianshu.com/p/ ...

  6. springboot redis token_Spring Boot + Redis + 注解 + 拦截器来实现接口幂等性校验

    优质文章,及时送达 作者 | wangzaiplus 链接 | www.jianshu.com/p/6189275403ed 一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证 ...

  7. springboot幂等性_Spring Boot + Redis + 注解 + 拦截器来实现接口幂等性校验

    一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如:订单接口, 不能多次创建订单 支付接口, 重复支付同一笔订单只能扣一次钱 支付宝回调接口, 可能会多次 ...

  8. redis 判断存在性_实战 | springboot+redis+拦截器 实现接口幂等性校验

    来源:https://www.jianshu.com/p/6189275403ed 一.概念 幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如: 订单接口, 不能 ...

  9. springboot 自定义注解拦截器

    springboot 自定义注解拦截器 最近在工作中,发现自定义注解拦截使用起来特别方便,现在来写出来给大家看看 环境springboot 首先写一个自定义注解 package com.study.c ...

  10. spring boot 1.5.4 整合redis、拦截器、过滤器、监听器、静态资源配置(十六)

    上一篇:spring boot 1.5.4 整合webService(十五) 1      Spring Boot整合redis和缓存 Spring Boot中除了对常用的关系型数据库提供了优秀的自动 ...

最新文章

  1. 特斯拉“自动召唤”有软肋!正面识别很厉害,侧面物体看不见,实测差点碾过行人脚面...
  2. Thread中,join()方法
  3. 分享一个自用的,随便找到的组件,留作记录
  4. 依赖包报错Invalid options object. Less Loader has been initialized using an options object that does not
  5. linux安装汉语输入法,在linux中安装google拼音输入法
  6. java中的starts_Java Math类静态double nextAfter(double starts,double direction)示例
  7. 利用Fiddler抓包调试工具,实现mock数据特殊场景深度测试(二)
  8. Let导航网系统源码系统+一键收录
  9. (03)System Verilog 常用数据类型详解
  10. 编译原理-提取左公因子---消除左递归(自己看)
  11. python以文本方式字节流_如何在Python3中将文本流编码为字节流?
  12. Atitit 项目常用模块 非业务模块 通用技术模块 attilax大总结 理论上可行。但要限制接口方式。 不然现在很多ui与后端接口模式很多,导致组合爆炸。。。 常用模块也就100来个而已。。
  13. Armbian : sudo must be owned by uid 0 and have the setuid bit set错误处理
  14. python报错: arry[i] = t IndexError: list assignment index out of range
  15. 2022年国家高新技术企业认定最新规定及国家高新补助重点,补贴20-50万
  16. 设计模式之——面向对象的六大原则
  17. Java程序员进阶必知的分布式系统专业术语分析
  18. pygame实现动态小鸟飞行 + 打包发布
  19. OpenEuler安装docker
  20. Github如何使用详细介绍(保姆级教学)

热门文章

  1. kali 32位 更换 xfce4 桌面
  2. MSSQL如何将查询结果拼接成字符串
  3. array_filter()用法
  4. asp.net 生成图形验证码(字母和数字混合)
  5. 电脑蓝屏后你该做的几桩要事
  6. MSDN精选:Lambda 表达式(C# 编程指南)
  7. springboot访问路径添加项目名称、tomcat端口号
  8. 使用@AspectJ注解开发Spring AOP
  9. Spring 定时器的使用(task)
  10. jQuery的对象访问函数(get,index,size,each)