Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?

Release: 2023-08-22 15:57:31
forward
1366 people have browsed it

Hello everyone, I’m Brother Tian

Yesterday, I was doing ## for a friend # mock interview, how to implement interface idempotence? It can be seen from the tone of his answer that he is memorizing eight-part essay. So, in order to let everyone easily experience the idempotent implementation of the interface, Brother Tian

arranged this article today

. This article has nine main contents:

Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?
##1. Power Concept of equality

Idempotence, in layman’s terms, is an interface. If you initiate the same request multiple times, you must ensure that the operation can only be executed once. For example:
  • Order interface, orders cannot be created multiple times
  • Payment interface, payment for the same order can only be deducted once
  • Alipay callback interface, there may be multiple callbacks, and repeated callbacks must be processed
  • Ordinary form submission interface, due to network timeout and other reasons, you can only click submit multiple times, and you can only succeed once. Wait

2. Common solutions

  1. Unique index -- Prevent new dirty data
  2. Token mechanism-- Prevent page submission from repeated
  3. Pessimistic lock-- Lock when acquiring data ( Lock table or row)
  4. Optimistic lock--implemented based on version number, verify the data at the moment the data is updated
  5. Distributed Lock -- redis (jedis, redisson) or zookeeper implements
  6. State machine -- state change, determine the state when updating data

3. Implementation of this article

This article uses the second method to implement, that is, through the Redis token mechanism to achieve interface idempotence check.

4. Implementation Ideas

Create a unique identifier for each request that needs to ensure idempotence token, first obtain token, and store this token in redis. When requesting the interface, put this token in the header or as a request parameter to request the interface. The backend interface determines whether this token:

exists in redis
  • If it exists, process the business logic normally and delete this token from redis. Then, if it is a repeated request, since token has been deleted, then it cannot pass the verification and returns Do not repeat the operationPrompt
  • If it does not exist, it means that the parameter is illegal or it is a repeated request, just return the prompt

5. Project Introduction

  • Spring Boot
  • Redis
  • ##@ApiIdempotentAnnotation interceptor intercepts requests
  • @ControllerAdviceGlobal exception handling
  • Stress testing tool:
    Jmeter
Note:

This article focuses on the core implementation of idempotence. The details of how Spring Boot integrates redis, ServerResponse, ResponseCode and other details are beyond the scope of this article.

6. Code implementation

1, mavenDependency

<!-- 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>
Copy after login

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();
        }
    }

}
Copy after login

3. Custom annotation @ApiIdempotent

/**
 * 在需要保证 接口幂等性 的Controller的方法上使用此注解
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
Copy after login

4. ApiIdempotentInterceptor Interceptor

/**
 * 接口幂等性拦截器
 */
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 {
    }
}
Copy after login

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());
        }
    }

}
Copy after login

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();
    }

}
Copy after login

Okay, the above is the implementation part of the code. Next we Let’s verify it.

7. Test verification

Get the controller of tokenTokenController:

@RestController
@RequestMapping("/token")
public class TokenController {

    @Autowired
    private TokenService tokenService;

    @GetMapping
    public ServerResponse token() {
        return tokenService.createToken();
    }

}
Copy after login

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

@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {

    @Autowired
    private TestService testService;

    @ApiIdempotent
    @PostMapping("testIdempotence")
    public ServerResponse testIdempotence() {
        return testService.testIdempotence();
    }

}
Copy after login

获取token

Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?

查看Redis

Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?

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

Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?
Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?

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

Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?

8. Notes (very important)

Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?
## In the above figure, you cannot simply delete the token directly without verifying whether the deletion is successful. Concurrency security issues will arise because multiple threads may reach line 46 at the same time, and the token has not been deleted at this time. So continue to execute. If you do not verify the deletion result of

jedisUtil.del(token) and directly release it, then there will still be a duplicate submission problem, even if there is actually only one real deletion operation, reproduce it below one time.

Modify the code slightly:

Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?
Request again

Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?
Look at the console again

Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?
Although only one token is actually deleted, since the deletion result is not verified, there is still a concurrency problem. Therefore, it must be verified

9. Summary

In fact, the idea is very simple, that is, each request is guaranteed to be unique, thereby ensuring idempotence Property, through interceptor annotation, you don’t need to write repeated code for every request. In fact, it can also be implemented using Spring AOP.

Okay, I’ll share it here today.

##

The above is the detailed content of Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:Java后端技术全栈
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template