안녕하세요 여러분 저는 Tian 형제입니다
어제 친구를 위해 모의 인터뷰를 하던 중, 인터페이스 멱등성을 어떻게 구현하나요? 대답하는 톤을 보면 팔다리 수필을 외우고 있다는 걸 알 수 있다.
그래서 모든 사람이 인터페이스의 멱등성 구현을 쉽게 경험할 수 있도록 Tian 형제가 오늘 이 글을 준비했습니다.
이 글은 총 9개의 주요 내용으로 구성되어 있습니다:
멱등성을 보장해야 하는 각 요청에 대한 고유 식별자를 만듭니다token
, 先获取token
, 并将此token
存入redis, 请求接口时, 将此token
放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token
:
토큰
, 토큰
이 삭제되어 확인을 통과할 수 없으며 작업을 반복하지 마세요
Prompttoken
, 那么, 如果是重复请求, 由于token
已被删除, 则不能通过校验, 返回请勿重复操作
提示Spring Boot
Redis
@ApiIdempotent
注解 + 拦截器对请求进行拦截@ControllerAdvice
全局异常处理Jmeter
5. 프로젝트 소개
🎜스프링 부트
🎜🎜🎜🎜Redis
🎜🎜🎜🎜@ApiIdempotent
주석 + 인터셉터가 요청을 가로챕니다 🎜🎜🎜🎜@ControllerAdvice
전역 예외 처리🎜🎜🎜🎜스트레스 테스트 도구:Jmeter
🎜🎜🎜🎜설명:🎜🎜🎜이 문서는 다음에 중점을 둡니다. 멱등성의 핵심 구현, Spring Boot Redis, ServerResponse, ResponseCode 및 기타 세부 사항 통합에 대한 세부 사항은 이 기사의 범위를 벗어납니다.🎜
1、maven
依赖maven
依赖
<!-- Redis-Jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <!--lombok 本文用到@Slf4j注解, 也可不引用, 自定义log即可--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.10</version> </dependency>
2、JedisUtil
@Component @Slf4j 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) { 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(); } } }
3、自定义注解@ApiIdempotent
/** * 在需要保证 接口幂等性 的Controller的方法上使用此注解 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface ApiIdempotent { }
4、ApiIdempotentInterceptor
拦截器
/** * 接口幂等性拦截器 */ 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) { check(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示 } return true; } private void check(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 { } }
5、TokenServiceImpl
@Service public class TokenServiceImpl implements TokenService { private static final String TOKEN_NAME = "token"; @Autowired private JedisUtil jedisUtil; @Override public 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()); } @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()); } } }
6、TestApplication
@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(); } }
好了,以上便是代码的实现部分,下面我们就来验证一下。
获取token
的控制器TokenController
@RestController @RequestMapping("/token") public class TokenController { @Autowired private TokenService tokenService; @GetMapping public ServerResponse token() { return tokenService.createToken(); } }2、
JedisUtil
🎜@RestController @RequestMapping("/test") @Slf4j public class TestController { @Autowired private TestService testService; @ApiIdempotent @PostMapping("testIdempotence") public ServerResponse testIdempotence() { return testService.testIdempotence(); } }🎜3、自定义注解
@ApiIdempotent
🎜rrreee🎜4、ApiIdempotentInterceptor
拦截器🎜rrreee🎜5、TokenServiceImpl🎜rrreee🎜6、<code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px; background-color: rgba(27, 31, 35, 0.05);글꼴 계열: 'Operator Mono', Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">TestApplication
🎜rrreee🎜好了,以上便是代码的实现分,下面我们就来验证一下。🎜🎜🎜🎜🎜七、测试验证🎜🎜 🎜🎜🎜获取토큰
의 태그 제조TokenController
:🎜@RestController @RequestMapping("/token") public class TokenController { @Autowired private TokenService tokenService; @GetMapping public ServerResponse token() { return tokenService.createToken(); } }
TestController
, 注意@ApiIdempotent
注解, 在需要幂等性校验的方法上声明此注解即可, 不需要校验的无影响:
@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
。
위 그림에서는 동시성 보안 문제가 있기 때문에 삭제 성공 여부를 확인하지 않고 단순히 토큰을 직접 삭제할 수는 없습니다. 왜냐하면, 여러 스레드가 동시에 46행에 도달할 가능성이 있기 때문입니다. 이때 토큰이 삭제되지 않았으므로 실행이 계속됩니다. jedisUtil.del(token)
의 삭제 결과를 확인하지 않고 직접 해제하면 반복되는 문제가 있습니다. 실제로 실제 삭제 작업만 있는 경우에도 제출은 계속 발생합니다. 아래에서 재현해 보세요.
코드 약간 수정:
다시 요청
콘솔을 다시 살펴보세요
토큰 중 하나만 삭제되었지만 삭제 결과가 확인되지 않았기 때문에 여전히 남아 있습니다. 따라서 동시성 문제를 확인해야 합니다
사실 아이디어는 매우 간단합니다. 즉, 각 요청이 고유함을 보장하므로 保证幂等性
, 通过拦截器+注解
, 就不用每次请求都写重复代码, 其实也可以利用Spring AOP
달성됩니다.
네, 오늘은 여기서 공유하겠습니다.
위 내용은 면접관: 결제 인터페이스에서는 동일한 주문에 대한 반복 결제에 대해 한 번만 공제할 수 있나요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!