


Interviewer: In the payment interface, money can only be deducted once for repeated payments for the same order. How to do this?
Aug 22, 2023 pm 03:57 PMHello 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:

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
Unique index -- Prevent new dirty data
Token mechanism-- Prevent page submission from repeated
Pessimistic lock-- Lock when acquiring data ( Lock table or row)
Optimistic lock--implemented based on version number, verify the data at the moment the data is updated
Distributed Lock -- redis (jedis, redisson) or zookeeper implements
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 operation
Prompt
-
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, maven
Dependency
<!-- 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. Custom annotation @ApiIdempotent
/**
* 在需要保證 接口冪等性 的Controller的方法上使用此注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiIdempotent {
}
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);// 冪等性校驗, 校驗通過則放行, 校驗失敗則拋出異常, 并通過統(tǒng)一異常處理返回友好提示
}
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();
}
}
Okay, the above is the implementation part of the code. Next we Let’s verify it.
7. Test verification
Get the controller of token
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個并發(fā)請求, 將上一步獲取到的token作為參數(shù)


header或參數(shù)均不傳token, 或者token值為空, 或者token值亂填, 均無法通過校驗, 如token值為abcd
。

8. Notes (very important)

## 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:

Request again

Look at the console again

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.
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: Redis token
mechanism to achieve interface idempotence check. 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
: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 operation
PromptSpring Boot
Redis
Annotation interceptor intercepts requests
Global exception handling
maven
DependencyJedisUtil
@ApiIdempotent
ApiIdempotentInterceptor
InterceptorTokenServiceImpl
TestApplication
token
TokenController
:TestController
, 注意@ApiIdempotent
注解, 在需要冪等性校驗的方法上聲明此注解即可, 不需要校驗的無影響:token
:
Redis
:
Jmeter
測試工具模擬50個并發(fā)請求, 將上一步獲取到的token作為參數(shù)

abcd
。

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.



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
. 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!

Hot AI Tools

Undress AI Tool
Undress images for free

Undresser.AI Undress
AI-powered app for creating realistic nude photos

AI Clothes Remover
Online AI tool for removing clothes from photos.

Clothoff.io
AI clothes remover

Video Face Swap
Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

Notepad++7.3.1
Easy-to-use and free code editor

SublimeText3 Chinese version
Chinese version, very easy to use

Zend Studio 13.0.1
Powerful PHP integrated development environment

Dreamweaver CS6
Visual web development tools

SublimeText3 Mac version
God-level code editing software (SublimeText3)

Hot Topics

You must know Spring, so let’s talk about the order of all notifications of Aop. How does Spring Boot or Spring Boot 2 affect the execution order of aop? Tell us about the pitfalls you encountered in AOP?

OOM means that there is a vulnerability in the program, which may be caused by the code or JVM parameter configuration. This article talks to readers about how to troubleshoot when a Java process triggers OOM.

Don’t underestimate the written examination questions of many companies. There are pitfalls and you can fall into them accidentally. When you encounter this kind of written test question about cycles, I suggest you think calmly and take it step by step.

?This article will take a look at 5 interview questions about the Java String class. I have personally experienced several of these five questions during the interview process. This article will help you understand why the answers to these questions are like this.

Last week, a friend in the group went for an interview with Ping An Insurance. The result was a bit regretful, which is quite a pity, but I hope you don’t get discouraged. As you said, basically all the questions encountered in the interview can be solved by memorizing the interview questions. It’s solved, so please work hard!

The extra chapter of the Java concurrent programming series, C A S (Compare and swap), is still in an easy-to-understand style with pictures and texts, allowing readers to have a crazy conversation with the interviewer.

The data structure of Java is the focus of the interview. Anyone who has participated in a Java interview must have some experience. When interviewers ask such questions, they often want to check whether you have studied the underlying structures of commonly used data types in Java, rather than simply staying at the level of "knowing how to use".

When we want to use a class, we need to load the class into memory through ClassLoader.
