In actual development projects, an externally exposed interface often faces many requests. Let us explain the concept of idempotence: The impact of any multiple executions is the same as the impact of one execution. According to this meaning, the final meaning is that the impact on the database can only be one-time and cannot be processed repeatedly. How to ensure its idempotence usually involves the following methods:
1. Establish a unique index in the database to ensure that only one piece of data is ultimately inserted into the database.
2. Token mechanism. Obtain a token before each interface request, and then add this token to the header body of the request the next time. Verification is performed in the background. If the verification passes, the token is deleted. Next The token is judged again for each request.
3. Pessimistic lock or optimistic lock. Pessimistic lock can ensure that other SQL cannot update data every time for update (when the database engine is innodb, the select condition must be a unique index to prevent the entire table from being locked. )
4. Query first and then judge. First, query the database to see if the data exists. If it exists, it proves that the request has been made, and the request is directly rejected. If it does not exist, it proves that it is the first time to come in, and it is directly released.
Why should we prevent repeated submission of interfaces?
For some sensitive operation interfaces, such as new data interfaces and payment interfaces, if the user improperly clicks the submit button multiple times, these interfaces will be requested multiple times, which may eventually lead to system exceptions.
How can the front end be controlled?
The front end can be controlled through js. When the user clicks the submit button,
1. Set the button to be unclickable for a number of seconds.
2. After the button is clicked, a loading prompt box will pop up to avoid clicking again until the interface request returns. After
3. Click the button to jump to a new page
However, please remember, never trust the user’s behavior, because you don’t know what weird operations the user will do, so the most important thing is It still has to be processed on the back end.
Use aop redis for interception processing
1. Create the aspect class RepeatSubmitAspect
Implementation process: After the interface request, the token request path is used as the key value to read data from redis. If the key can be found, It proves that it was submitted repeatedly, and vice versa. If it is not a repeated submission, it will be released directly, and the key will be written into redis, and set to expire within a certain period of time (I set an expiration of 5s here)
In traditional web projects, in order to prevent For repeated submissions, the usual approach is: the backend generates a unique submission token (uuid) and stores it on the server. When the page initiates a request, it carries the secondary token. The backend deletes the token after verifying the request to ensure the uniqueness of the request.
However, the appeal method requires changes to both the front and back ends. If it is in the early stage of the project, it can be achieved. However, in the later stage of the project, many functions have been implemented and it is impossible to make large-scale changes.
Ideas
1. Customize the annotation @NoRepeatSubmit to mark all requests submitted in the Controller
2. Intercept all methods marked with @NoRepeatSubmit through AOP
3. Execute the business method Before, obtain the current user's token or JSessionId current request address as a unique key to obtain the redis distributed lock. If concurrent acquisition is performed at this time, only one thread can obtain it.
4. After the business is executed, release the lock
About Redis distributed lock
Using Redis is for load balancing deployment. If it is a stand-alone project, you can use a local thread-safe Cache to replace Redis
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @ClassName NoRepeatSubmit * @Description 这里描述 * @Author admin * @Date 2021/3/2 16:16 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NoRepeatSubmit { /** * 设置请求锁定时间 * * @return */ int lockTime() default 10; }
package com.hongkun.aop; /** * @ClassName RepeatSubmitAspect * @Description 这里描述 * @Author admin * @Date 2021/3/2 16:15 */ import com.hongkun.until.ApiResult; import com.hongkun.until.Result; import com.hongkun.until.RedisLock; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * @author liucheng * @since 2020/01/15 * 防止接口重复提交 */ @Aspect @Component public class RepeatSubmitAspect { private static final Logger LOGGER = LoggerFactory.getLogger(RepeatSubmitAspect.class); @Autowired private RedisLock redisLock; @Pointcut("@annotation(noRepeatSubmit)") public void pointCut(NoRepeatSubmit noRepeatSubmit) { } @Around("pointCut(noRepeatSubmit)") public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable { int lockSeconds = noRepeatSubmit.lockTime(); RequestAttributes ra = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes sra = (ServletRequestAttributes) ra; HttpServletRequest request = sra.getRequest(); Assert.notNull(request, "request can not null"); // 此处可以用token或者JSessionId String token = request.getHeader("token"); String path = request.getServletPath(); String key = getKey(token, path); String clientId = getClientId(); boolean isSuccess = redisLock.lock(key, clientId, lockSeconds,TimeUnit.SECONDS); LOGGER.info("tryLock key = [{}], clientId = [{}]", key, clientId); if (isSuccess) { LOGGER.info("tryLock success, key = [{}], clientId = [{}]", key, clientId); // 获取锁成功 Object result; try { // 执行进程 result = pjp.proceed(); } finally { // 解锁 redisLock.unlock(key, clientId); LOGGER.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId); } return result; } else { // 获取锁失败,认为是重复提交的请求 LOGGER.info("tryLock fail, key = [{}]", key); return ApiResult.success(200, "重复请求,请稍后再试", null); } } private String getKey(String token, String path) { return "00000"+":"+token + path; } private String getClientId() { return UUID.randomUUID().toString(); } }
The above is the detailed content of How SpringBoot combines Aop+Redis to prevent repeated submission of interfaces. For more information, please follow other related articles on the PHP Chinese website!