>  기사  >  Java  >  면접관: 결제 인터페이스에서는 동일한 주문에 대한 반복 결제에 대해 한 번만 공제할 수 있나요?

면접관: 결제 인터페이스에서는 동일한 주문에 대한 반복 결제에 대해 한 번만 공제할 수 있나요?

Java后端技术全栈
Java后端技术全栈앞으로
2023-08-22 15:57:311357검색

안녕하세요 여러분 저는 Tian 형제입니다

어제 친구를 위해 모의 인터뷰를 하던 중, 인터페이스 멱등성을 어떻게 구현하나요? 대답하는 톤을 보면 팔다리 수필을 외우고 있다는 걸 알 수 있다.

그래서 모든 사람이 인터페이스의 멱등성 구현을 쉽게 경험할 수 있도록 Tian 형제

가 오늘 이 글을 준비했습니다.

이 글은 총 9개의 주요 내용으로 구성되어 있습니다:

면접관: 결제 인터페이스에서는 동일한 주문에 대한 반복 결제에 대해 한 번만 공제할 수 있나요?

1. 멱등성의 개념 Imdempotence는 일반인의 용어로 동일한 요청을 여러 번 시작하는 인터페이스입니다. . 작업은 한 번만 실행될 수 있어야 합니다. 예:

  • 주문 인터페이스, 주문은 여러 번 생성할 수 없습니다
  • 결제 인터페이스, 동일한 주문에 대한 반복 결제는 한 번만 차감될 수 있습니다.
  • Alipay 콜백 인터페이스, 여러 콜백이 있을 수 있으며 반복 콜백을 처리해야 합니다
  • 일반적인 양식 제출 인터페이스는 네트워크 시간 초과 및 기타 이유로 인해 제출을 여러 번만 클릭할 수 있으며 한 번만 성공할 수 있습니다.
    고유 인덱스--새로운 더티 데이터 방지

토큰 메커니즘--반복적인 페이지 제출 방지비관적 잠금--데이터 획득 시 잠금(테이블 잠금 또는 행 잠금)

  1. 낙관적 잠금-구현됨 버전 번호 기준으로 데이터 업데이트 시 데이터를 순간적으로 확인
  2. 분산 잠금 - redis(jedis, redisson) 또는 Zookeeper 구현
  3. 상태 머신 - 상태 변경, 데이터 업데이트 시 상태 판단
  4. 3. 이 기사의 구현
  5. 이 기사에서는 두 번째 구현 방법, 즉
    메커니즘을 통해 인터페이스 멱등성 확인을 구현하는 방법을 사용합니다.

    4. 구현 아이디어

    멱등성을 보장해야 하는 각 요청에 대한 고유 식별자를 만듭니다token, 先获取token, 并将此token存入redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断redis中是否存在此token:

    • 존재하는 경우 비즈니스 로직을 정상적으로 처리하고 redis토큰, 토큰이 삭제되어 확인을 통과할 수 없으며 작업을 반복하지 마세요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

    면접관: 결제 인터페이스에서는 동일한 주문에 대한 반복 결제에 대해 한 번만 공제할 수 있나요?

    8. 주의사항 (매우 중요)

    면접관: 결제 인터페이스에서는 동일한 주문에 대한 반복 결제에 대해 한 번만 공제할 수 있나요?

    위 그림에서는 동시성 보안 문제가 있기 때문에 삭제 성공 여부를 확인하지 않고 단순히 토큰을 직접 삭제할 수는 없습니다. 왜냐하면, 여러 스레드가 동시에 46행에 도달할 가능성이 있기 때문입니다. 이때 토큰이 삭제되지 않았으므로 실행이 계속됩니다. jedisUtil.del(token)의 삭제 결과를 확인하지 않고 직접 해제하면 반복되는 문제가 있습니다. 실제로 실제 삭제 작업만 있는 경우에도 제출은 계속 발생합니다. 아래에서 재현해 보세요.

    코드 약간 수정:

    면접관: 결제 인터페이스에서는 동일한 주문에 대한 반복 결제에 대해 한 번만 공제할 수 있나요?

    다시 요청

    면접관: 결제 인터페이스에서는 동일한 주문에 대한 반복 결제에 대해 한 번만 공제할 수 있나요?

    콘솔을 다시 살펴보세요

    면접관: 결제 인터페이스에서는 동일한 주문에 대한 반복 결제에 대해 한 번만 공제할 수 있나요?

    토큰 중 하나만 삭제되었지만 삭제 결과가 확인되지 않았기 때문에 여전히 남아 있습니다. 따라서 동시성 문제를 확인해야 합니다

    9. 요약

    사실 아이디어는 매우 간단합니다. 즉, 각 요청이 고유함을 보장하므로 保证幂等性, 通过拦截器+注解, 就不用每次请求都写重复代码, 其实也可以利用Spring AOP 달성됩니다.

    네, 오늘은 여기서 공유하겠습니다.

위 내용은 면접관: 결제 인터페이스에서는 동일한 주문에 대한 반복 결제에 대해 한 번만 공제할 수 있나요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 Java后端技术全栈에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제