Maison > Article > base de données > Comment SpringBoot combine Aop+Redis pour empêcher la soumission répétée d'interfaces
Dans les projets de développement réels, une interface exposée en externe fait souvent face à de nombreuses demandes. Expliquons le concept d'idempotence : L'impact de toute exécution multiple est le même que l'impact d'une seule exécution. Selon ce sens, le sens final est que l'impact sur la base de données ne peut être qu'une seule fois et ne peut pas être traité de manière répétée. Comment garantir son idempotence implique généralement les méthodes suivantes :
1 La base de données établit un index unique pour garantir qu'une seule donnée est finalement insérée dans la base de données.
2. Mécanisme de jeton. Obtenez un jeton avant chaque demande d'interface, puis ajoutez ce jeton au corps de l'en-tête de la demande la prochaine fois. Si la vérification réussit, le jeton est supprimé et la suivante. la demande est jugée à nouveau.
3. Le verrouillage pessimiste ou le verrouillage optimiste peut garantir que les autres SQL ne peuvent pas mettre à jour les données à chaque fois (lorsque le moteur de base de données est innodb, la condition de sélection doit être un index unique pour empêcher la table entière d'être verrouillée)
4, Interrogez d'abord puis jugez. Tout d'abord, demandez si les données existent dans la base de données. Si l'existence prouve que la demande a été faite, la demande sera directement rejetée. Si elle n'existe pas, cela prouve qu'il s'agit bien de la demande. première fois que vous entrez et la demande sera publiée directement.
Pourquoi devrions-nous empêcher la soumission répétée d’interfaces ?
Pour certaines interfaces d'opération sensibles, telles que les nouvelles interfaces de données et les interfaces de paiement, si l'utilisateur clique plusieurs fois de manière incorrecte sur le bouton d'envoi, ces interfaces seront demandées plusieurs fois, ce qui peut éventuellement conduire à des exceptions système.
Comment contrôler le front-end ?
Le front-end peut être contrôlé via js. Lorsque l'utilisateur clique sur le bouton de soumission,
1. Le bouton est configuré pour ne pas être cliquable pendant un certain nombre de secondes.
2. Une fois le bouton cliqué, une boîte de dialogue d'invite de chargement apparaît. évitez de cliquer à nouveau jusqu'à ce que la demande d'interface revienne.
3. Cliquez sur le bouton Puis passez à une nouvelle page
Cependant, n'oubliez pas de ne jamais faire confiance au comportement de l'utilisateur, car vous ne savez pas quelles opérations étranges l'utilisateur fera, donc le plus l'important est de le traiter sur le backend.
Utilisez aop+redis pour le traitement d'interception
1. Créez la classe d'aspect RepeatSubmitAspect
Processus d'implémentation : après la requête d'interface, le chemin du jeton+de la requête est utilisé comme valeur de clé pour lire les données dans redis. , cela prouve qu'il a été soumis à plusieurs reprises. Le contraire n'est pas vrai. S'il ne s'agit pas d'une soumission répétée, elle sera publiée directement, et la clé sera écrite dans Redis et configurée pour expirer dans un certain délai (j'ai défini une expiration de 5 secondes ici)
Dans les projets Web traditionnels, afin d'éviter les soumissions répétées, l'approche habituelle est la suivante : Le backend génère un jeton de soumission unique (uuid) et le stocke sur le serveur lorsque la page initie une requête, elle transporte le jeton après vérification. la demande pour garantir le caractère unique de la demande.
Cependant, la méthode d'appel nécessite des modifications à la fois au niveau du front et du back-end. Si elle est au début du projet, elle peut être réalisée. Cependant, à un stade ultérieur du projet, de nombreuses fonctions ont été mises en œuvre et c'est le cas. impossible de procéder à des changements à grande échelle.
Idée
1. Personnalisez l'annotation @NoRepeatSubmit pour marquer toutes les demandes soumises dans le contrôleur
2 Interceptez toutes les méthodes marquées avec @NoRepeatSubmit via AOP
3, obtenez le jeton de l'utilisateur actuel ou JSessionId+current. L'adresse de requête est utilisée comme clé unique pour obtenir le verrou distribué Redis. Si elle est obtenue simultanément, un seul thread peut l'obtenir.
4. Une fois l'activité exécutée, libérez le verrou
À propos du verrouillage distribué Redis
L'utilisation de Redis est destinée au déploiement d'équilibrage de charge. S'il s'agit d'un projet autonome, vous pouvez utiliser un cache thread-safe local pour remplacer 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(); } }
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!