日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學(xué)無先后,達(dá)者為師

網(wǎng)站首頁(yè) 編程語(yǔ)言 正文

用戶手抖,連續(xù)點(diǎn)了兩次??jī)?yōu)雅解決表單重復(fù)提交

作者:earthly exile丶 更新時(shí)間: 2022-07-12 編程語(yǔ)言

在項(xiàng)目中,用戶在操作某些重要操作時(shí),經(jīng)常會(huì)出現(xiàn)手抖,導(dǎo)致重復(fù)提交,該怎么處理呢?

? ? ? ? ?我的答案是交給前端,用戶提交的時(shí)候禁用按鈕,等接口返回后再啟用。

? ? ? ? ?哈哈,開個(gè)玩笑,今天給大家示范一下后端如何處理。

?首先通常我們?yōu)楹我苊獗韱沃貜?fù)提交?這其實(shí)跟接口冪等有關(guān),既然說到這里,那么我們就來回顧一下什么是接口冪等吧!

1. 接口冪等的定義:

? ? ? ? 一個(gè)接口多次調(diào)用而沒有副作用,那么我們稱之為接口冪等。所謂沒有副作用簡(jiǎn)單來講就是一個(gè)接口被執(zhí)行多次數(shù)據(jù)不會(huì)亂。每次返回的結(jié)果都一致。

? 2.哪些地方天然冪等,那么地方可能導(dǎo)致接口不冪等?

? ? ? ? ? ?基本業(yè)務(wù)操作中,查詢和刪除,天然冪等。更新和插入是接口不冪等的高發(fā)地。

? 3.業(yè)務(wù)中重點(diǎn)觸發(fā)場(chǎng)景

  1. 用戶重復(fù)操作:例如我們的系統(tǒng)中,用戶開方后點(diǎn)擊確認(rèn),手抖觸發(fā)了兩次,發(fā)了兩次請(qǐng)求給后臺(tái)。
  2. 代碼重試:很多RPC框架在進(jìn)行遠(yuǎn)程調(diào)用時(shí)都有重試機(jī)制,當(dāng)?shù)谝淮握?qǐng)求發(fā)出后在指定時(shí)間內(nèi)沒有收到回復(fù),會(huì)再發(fā)一次。例如dubbo,默認(rèn)2次。
  3. 消息重復(fù)消費(fèi):多次觸發(fā)消費(fèi)者消費(fèi)邏輯。
  4. 網(wǎng)絡(luò)波動(dòng):同一個(gè)請(qǐng)求服務(wù)端收到多次。

4.常用解決方案

  1. Token+Redis:每次接口先獲取token,后臺(tái)將生成的token放入redis,請(qǐng)求時(shí)在header中加上token,收到請(qǐng)求后驗(yàn)證redis中是否存在改token,通過后刪除token。-------保證網(wǎng)絡(luò)波動(dòng)導(dǎo)致的重復(fù)請(qǐng)求。(最常用的解決方案)。
  2. 唯一索引去重:保證最終插入數(shù)據(jù)庫(kù)只有一天數(shù)據(jù)。
  3. 狀態(tài)機(jī):解決狀態(tài)update問題,多線程下,多用戶對(duì)同一訂單處理問題。
  4. 樂觀鎖:(雖然比悲觀鎖效率好點(diǎn),但任然用的不多)
  5. 悲觀鎖:for update (效率問題,不推薦,除非安全等級(jí)要求極高的地方)
  6. 先查詢后判斷+版本控制
  7. 建去重表
  8. 前端攔截
  9. JVM鎖:僅單機(jī)狀態(tài)
  10. 分布式鎖

5.我們遇到的重復(fù)請(qǐng)求問題

????????1.對(duì)于較重的請(qǐng)求,因?yàn)楸容^耗時(shí),有時(shí)候用戶會(huì)重復(fù)點(diǎn)擊,例如查詢某個(gè)復(fù)雜報(bào)表,用戶查詢后遲遲未收到回復(fù),便再次多次點(diǎn)擊,導(dǎo)致重復(fù)請(qǐng)求過多,浪費(fèi)系統(tǒng)資源。

?????????2.網(wǎng)絡(luò)波動(dòng),用戶客戶端提交了一次,服務(wù)端卻收到了多次請(qǐng)求。

? ? ? ? ?3.用戶手抖,同一時(shí)間內(nèi)點(diǎn)擊了兩次表單提交按鈕,導(dǎo)致服務(wù)端收到兩次請(qǐng)求。

6.解決思路

? ? ? ? 對(duì)于上面所訴的三個(gè)問題,重點(diǎn)就是過濾掉重復(fù)的請(qǐng)求。我們采用redis+token 的思路來處理,由于我們?cè)趃ateway中已經(jīng)在請(qǐng)求參數(shù)中追加了用戶信息(里面擁有accountId,因系統(tǒng)只允許單點(diǎn)登錄,可唯一區(qū)分是同一個(gè)用戶發(fā)出的請(qǐng)求)具體實(shí)現(xiàn)參見我的另一篇博客合理使用gateWay過濾器,實(shí)現(xiàn)Concroller自動(dòng)注入用戶信息

,因此可以省去獲取token這一步驟,我們?cè)谡?qǐng)求進(jìn)入controller之前,采用aop攔截,將獲取的請(qǐng)求的方法簽名和參數(shù)(方法參數(shù)?+ 用戶信息?)作為參數(shù) 使用MD5運(yùn)算出一個(gè)key存入redis(存入時(shí)使用redis set if not exists來保證原子性)。根據(jù)存入結(jié)果判斷當(dāng)前請(qǐng)求是否正在被處理,若正在被處理,則不再執(zhí)行當(dāng)前請(qǐng)求,直接返回,否則正常執(zhí)行,當(dāng)請(qǐng)求執(zhí)行完成后后置處理將redis中的key移除。

7.實(shí)現(xiàn)代碼

? ? ? ? ? 1.添加注解,方便后續(xù)使用。代碼如下

package com.lvyuanji.common.annotation;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FormRepeatSubmitValidation {

	String value() default "表單重復(fù)提交驗(yàn)證";
	//禁止重試時(shí)間
	int timeout() default 8*1000;
}

2.aop切面

package com.lvyuanji.common.aspect;


import cn.hutool.crypto.digest.MD5;
import com.lvyuanji.common.annotation.FormRepeatSubmitValidation;
import com.lvyuanji.common.asserts.BusinessAsserts;
import com.lvyuanji.common.exception.Exceptions;
import org.apache.dubbo.common.utils.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * 防止表單重復(fù)提交 Aspect,僅用于Controller層
 *
 * @author moky
 * @date 2021/04/21.
 */
@Aspect
@Component
public class FormRepeatSubmitAspect {


    //日志記錄
    private static final Logger logger = LoggerFactory.getLogger(FormRepeatSubmitAspect.class);

    //redis客戶端
    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    @Around("@annotation(formRepeatSubmitValidation)")
    public Object doProcess(ProceedingJoinPoint joinPoint, FormRepeatSubmitValidation formRepeatSubmitValidation) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //獲取方法簽名
        String methodSignature = signature.toLongString();
        // 請(qǐng)求的方法參數(shù)值,因小藍(lán)本機(jī)制,會(huì)在請(qǐng)求參數(shù)中寫入accountEntity,因此通過參數(shù)可以唯一定位到具體用戶
        Object[] args = joinPoint.getArgs();
        String argString = StringUtils.toArgumentString(args);
        //通過MD5運(yùn)算加密
        MD5 md5 = new MD5();
        String digexStr = md5.digestHex(methodSignature + argString);
        //將字符串存入 redis,根據(jù)返回值判斷是否該請(qǐng)求仍然正在處理
        ValueOperations<String, String> operations = redisTemplate.opsForValue();
        Boolean isSuccess = operations.setIfAbsent(digexStr, "lock", formRepeatSubmitValidation.timeout(), TimeUnit.MILLISECONDS);
        BusinessAsserts.isTrue(isSuccess, Exceptions.System.repet_submit);

        //執(zhí)行被被代理的方法
        try {
            return joinPoint.proceed(args);
        } finally {
            //方法執(zhí)行結(jié)束,解鎖
            redisTemplate.delete(digexStr);
        }
    }

}

? ? ? ? 3.完成以上兩步,后面我們面對(duì)需要攔截重復(fù)提交的請(qǐng)求時(shí),只需要在controller中添加注解即可實(shí)現(xiàn)動(dòng)態(tài)攔截。

注意此處僅處理了客戶端重復(fù)請(qǐng)求的問題,遠(yuǎn)程服務(wù)調(diào)用重復(fù)請(qǐng)求問題我會(huì)在后續(xù)給出處理方案。

????????

????????

原文鏈接:https://blog.csdn.net/weixin_47987440/article/details/125695249

欄目分類
最近更新