点击上方 Java后端,选择 设为星标
优质文章,及时送达![68f6266154345502c57e9049a0e5ce6b.png](https://i-blog.csdnimg.cn/blog_migrate/f2f8f63046cecc68cfc24c2c1900603b.jpeg)
一、概念
幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次比如:
- 订单接口, 不能多次创建订单
- 支付接口, 重复支付同一笔订单只能扣一次钱
- 支付宝回调接口, 可能会多次回调, 必须处理重复回调
- 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次
等等
二、常见解决方案
- 唯一索引 -- 防止新增脏数据
- 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
https://github.com/wangzaiplus/springboot/tree/wxw
六、代码实现
pom
<dependency><groupId>redis.clientsgroupId><artifactId>jedisartifactId><version>2.9.0version>dependency><dependency><groupId>org.projectlombokgroupId><artifactId>lombokartifactId><version>1.16.10version>dependency>
JedisUtil
@Component@Slf4jpublic class JedisUtil {@Autowiredprivate 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) {
log.error("set key:{} value:{} error", key, value, e);return null;
} 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();return jedis.setex(key, expireTime, value);
} catch (Exception e) {
log.error("set key:{} value:{} expireTime:{} error", key, value, expireTime, e);return null;
} finally {
close(jedis);
}
}/**
* 取值
*
* @param key
* @return
*/public String get(String key) {
Jedis jedis = null;try {
jedis = getJedis();return jedis.get(key);
} catch (Exception e) {
log.error("get key:{} error", key, e);return null;
} 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) {
log.error("del key:{} error", key, e);return null;
} 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) {
log.error("exists key:{} error", key, e);return null;
} finally {
close(jedis);
}
}/**
* 设值key过期时间
*
* @param key
* @param expireTime 过期时间, 单位: s
* @return
*/public Long expire(String key, int expireTime) {
Jedis jedis = null;try {
jedis = getJedis();return jedis.expire(key.getBytes(), expireTime);
} catch (Exception e) {
log.error("expire key:{} error", key, e);return null;
} finally {
close(jedis);
}
}/**
* 获取剩余时间
*
* @param key
* @return
*/public Long ttl(String key) {
Jedis jedis = null;try {
jedis = getJedis();return jedis.ttl(key);
} catch (Exception e) {
log.error("ttl key:{} error", key, e);return null;
} finally {
close(jedis);
}
}private void close(Jedis jedis) {if (null != jedis) {
jedis.close();
}
}
}
自定义注解@ApiIdempotent
/**
* 在需要保证 接口幂等性 的Controller的方法上使用此注解
*/@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
ApiIdempotentInterceptor拦截器
/**
* 接口幂等性拦截器
*/public class ApiIdempotentInterceptor implements HandlerInterceptor {@Autowiredprivate TokenService tokenService;@Overridepublic 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) {
check(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}return true;
}private void check(HttpServletRequest request) {
tokenService.checkToken(request);
}@Overridepublic void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}@Overridepublic void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
TokenServiceImpl
@Servicepublic class TokenServiceImpl implements TokenService {private static final String TOKEN_NAME = "token";@Autowiredprivate JedisUtil jedisUtil;@Overridepublic ServerResponse createToken() {
String str = RandomUtil.UUID32();
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());
}@Overridepublic 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中也不存在tokenthrow 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
@SpringBootApplication@MapperScan("com.wangzaiplus.test.mapper")public class TestApplication extends WebMvcConfigurerAdapter {public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}/**
* 跨域
* @return
*/@Beanpublic 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);
}@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 接口幂等性拦截器
registry.addInterceptor(apiIdempotentInterceptor());super.addInterceptors(registry);
}@Beanpublic ApiIdempotentInterceptor apiIdempotentInterceptor() {return new ApiIdempotentInterceptor();
}
}
OK, 目前为止, 校验代码准备就绪, 接下来测试验证
七、测试验证
1、获取token的控制器TokenController@RestController@RequestMapping("/token")
public class TokenController {@Autowired
private TokenService tokenService;@GetMapping
public ServerResponse token() {return tokenService.createToken();
}
}
2、TestController, 注意@ApiIdempotent注解, 在需要幂等性校验的方法上声明此注解即可, 不需要校验的无影响
@RestController@RequestMapping("/test")@Slf4jpublic class TestController { @Autowired private TestService testService; @ApiIdempotent @PostMapping("testIdempotence") public ServerResponse testIdempotence() { return testService.testIdempotence(); }}
3、获取token
![3917bd279c221e8ccfeffb42335fc984.png](https://i-blog.csdnimg.cn/blog_migrate/e1539ba7ce81cd229f42266d7800ddd0.jpeg)
![8610dd5053b12f4b71ba70d94ff49121.png](https://i-blog.csdnimg.cn/blog_migrate/6682105a40c2836802e27f0f18aee3d3.jpeg)
![daa64c5bdeb779fb2b63bd822a859b6c.png](https://i-blog.csdnimg.cn/blog_migrate/c2ae3463762b1b28cb141774bf276f9a.jpeg)
![d3e896781ecd567a8c7c53230abcc4b0.png](https://i-blog.csdnimg.cn/blog_migrate/5d927dc94e204e34d0ea0b10189074e2.jpeg)
![552a691784a8bab4e5f6fab5fce46397.png](https://i-blog.csdnimg.cn/blog_migrate/c6e0c57bd5677d749798c0de93d6e36e.jpeg)
八、注意点(非常重要)
![7cc876cd944732663b605bf0bb9ae08c.png](https://i-blog.csdnimg.cn/blog_migrate/7d195d2528d02b931ea03ac10809b01e.jpeg)
![83a978f4491cf74c4b6704c1c647804b.png](https://i-blog.csdnimg.cn/blog_migrate/919fe8ca11bce34b624cc790476422f3.jpeg)
![fc52b1bcf61efbc9de5f55e453ebb091.png](https://i-blog.csdnimg.cn/blog_migrate/ba66775c78632521de626ecee0002cb5.jpeg)
![c7e4178c2776cf22a51b97707aa74b29.png](https://i-blog.csdnimg.cn/blog_migrate/8e8f86d21f06b0bf8a8c2abdd3c5e105.jpeg)
九、总结
其实思路很简单, 就是每次请求保证唯一性, 从而保证幂等性, 通过拦截器+注解, 就不用每次请求都写重复代码, 其实也可以利用spring aop实现, 无所谓。 Githubhttps://github.com/wangzaiplus/springboot/tree/wxw
- END -
推荐阅读 1. Github标星10.8K!Java 实战博客项目分享2. 浅析VO、DTO、DO、PO的概念、区别和用处3. 为什么年终奖是一个彻头彻尾的职场圈套?4. 什么是一致性 Hash 算法?
5. 团队开发中 Git 最佳实践
↓ 公众号推荐,方向:机器学习、深度学习 ↓
喜欢文章,点个在看 ![2aa3351ac6e65a47716c1d2e2f6df5cc.png](https://i-blog.csdnimg.cn/blog_migrate/c17fcb33c7d6c6904dba511e56ee84fd.jpeg)