


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

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);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}
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个并发请求, 将上一步获取到的token作为参数


header或参数均不传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个并发请求, 将上一步获取到的token作为参数

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

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

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

Undress AI Tool
Undress images for free

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

MinGW - Minimalist GNU for Windows
This project is in the process of being migrated to osdn.net/projects/mingw, you can continue to follow us there. MinGW: A native Windows port of the GNU Compiler Collection (GCC), freely distributable import libraries and header files for building native Windows applications; includes extensions to the MSVC runtime to support C99 functionality. All MinGW software can run on 64-bit Windows platforms.

Dreamweaver Mac version
Visual web development tools

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

SublimeText3 Chinese version
Chinese version, very easy to use

PhpStorm Mac version
The latest (2018.2.1) professional PHP integrated development tool
