<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          8種方案解決重復(fù)提交問題!

          共 26282字,需瀏覽 53分鐘

           ·

          2024-06-27 09:19

          往期熱門文章:

          
              

          1、項目終于用上了插入式注解,真香!

          2、教你用三種方式模擬兩個線程搶票

          3、MySQL中varchar(50)和varchar(500)區(qū)別是什么?

          4、頂級Javaer都在使用的類庫,真香!

          5、最適合程序員的畫圖工具?

          本文來源:juejin.im/post/5d31928c51882564c966a71c

          1.什么是冪等

          在我們編程中常見冪等
          • select查詢天然冪等
          • delete刪除也是冪等,刪除同一個多次效果一樣
          • update直接更新某個值的,冪等
          • update更新累加操作的,非冪等
          • insert非冪等操作,每次新增一條

          2.產(chǎn)生原因

          由于重復(fù)點擊或者網(wǎng)絡(luò)重發(fā) eg:
          • 點擊提交按鈕兩次;
          • 點擊刷新按鈕;
          • 使用瀏覽器后退按鈕重復(fù)之前的操作,導(dǎo)致重復(fù)提交表單;
          • 使用瀏覽器歷史記錄重復(fù)提交表單;
          • 瀏覽器重復(fù)的HTTP請;
          • nginx重發(fā)等情況;
          • 分布式RPC的try重發(fā)等;

          3.解決方案

          1)前端js提交禁止按鈕可以用一些js組件
          2)使用Post/Redirect/Get模式
          在提交后執(zhí)行頁面重定向,這就是所謂的Post-Redirect-Get (PRG)模式。簡言之,當(dāng)用戶提交了表單后,你去執(zhí)行一個客戶端的重定向,轉(zhuǎn)到提交成功信息頁面。這能避免用戶按F5導(dǎo)致的重復(fù)提交,而其也不會出現(xiàn)瀏覽器表單重復(fù)提交的警告,也能消除按瀏覽器前進和后退按導(dǎo)致的同樣問題。
          3)在session中存放一個特殊標(biāo)志
          在服務(wù)器端,生成一個唯一的標(biāo)識符,將它存入session,同時將它寫入表單的隱藏字段中,然后將表單頁面發(fā)給瀏覽器,用戶錄入信息后點擊提交,在服務(wù)器端,獲取表單中隱藏字段的值,與session中的唯一標(biāo)識符比較,相等說明是首次提交,就處理本次請求,然后將session中的唯一標(biāo)識符移除;不相等說明是重復(fù)提交,就不再處理。
          4)其他借助使用header頭設(shè)置緩存控制頭Cache-control等方式
          比較復(fù)雜 不適合移動端APP的應(yīng)用 這里不詳解
          5)借助數(shù)據(jù)庫
          insert使用唯一索引 update使用 樂觀鎖 version版本法
          這種在大數(shù)據(jù)量和高并發(fā)下效率依賴數(shù)據(jù)庫硬件能力,可針對非核心業(yè)務(wù)
          6)借助悲觀鎖
          使用select … for update ,這種和 synchronized 鎖住先查再insert or update一樣,但要避免死鎖,效率也較差
          針對單體 請求并發(fā)不大 可以推薦使用
          7)借助本地鎖(本文重點)
          原理:
          使用了 ConcurrentHashMap 并發(fā)容器 putIfAbsent 方法,和 ScheduledThreadPoolExecutor 定時任務(wù),也可以使用guava cache的機制, gauva中有配有緩存的有效時間也是可以的key的生成 
          Content-MD5 
          Content-MD5 是指 Body 的 MD5 值,只有當(dāng) Body 非Form表單時才計算MD5,計算方式直接將參數(shù)和參數(shù)名稱統(tǒng)一加密MD5
          MD5在一定范圍類認(rèn)為是唯一的 近似唯一 當(dāng)然在低并發(fā)的情況下足夠了
          本地鎖只適用于單機部署的應(yīng)用.

          ①配置注解

          import java.lang.annotation.*;

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

              /**
               * 延時時間 在延時多久后可以再次提交
               *
               * @return Time unit is one second
               */

              int delaySeconds() default 20;
          }

          ②實例化鎖

          import com.google.common.cache.Cache;
          import com.google.common.cache.CacheBuilder;
          import lombok.extern.slf4j.Slf4j;
          import org.apache.commons.codec.digest.DigestUtils;

          import java.util.Objects;
          import java.util.concurrent.ConcurrentHashMap;
          import java.util.concurrent.ScheduledThreadPoolExecutor;
          import java.util.concurrent.ThreadPoolExecutor;
          import java.util.concurrent.TimeUnit;

          /**
           * @author lijing
           * 重復(fù)提交鎖
           */

          @Slf4j
          public final class ResubmitLock {


              private static final ConcurrentHashMap<String, Object> LOCK_CACHE = new ConcurrentHashMap<>(200);
              private static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5new ThreadPoolExecutor.DiscardPolicy());


             // private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder()
                      // 最大緩存 100 個
             //          .maximumSize(1000)
                      // 設(shè)置寫緩存后 5 秒鐘過期
             //         .expireAfterWrite(5, TimeUnit.SECONDS)
             //         .build();


              private ResubmitLock() {
              }

              /**
               * 靜態(tài)內(nèi)部類 單例模式
               *
               * @return
               */

              private static class SingletonInstance {
                  private static final ResubmitLock INSTANCE = new ResubmitLock();
              }

              public static ResubmitLock getInstance() {
                  return SingletonInstance.INSTANCE;
              }


              public static String handleKey(String param) {
                  return DigestUtils.md5Hex(param == null ? "" : param);
              }

              /**
               * 加鎖 putIfAbsent 是原子操作保證線程安全
               *
               * @param key   對應(yīng)的key
               * @param value
               * @return
               */

              public boolean lock(final String key, Object value) {
                  return Objects.isNull(LOCK_CACHE.putIfAbsent(key, value));
              }

              /**
               * 延時釋放鎖 用以控制短時間內(nèi)的重復(fù)提交
               *
               * @param lock         是否需要解鎖
               * @param key          對應(yīng)的key
               * @param delaySeconds 延時時間
               */

              public void unLock(final boolean lock, final String key, final int delaySeconds) {
                  if (lock) {
                      EXECUTOR.schedule(() -> {
                          LOCK_CACHE.remove(key);
                      }, delaySeconds, TimeUnit.SECONDS);
                  }
              }
          }

          ③AOP 切面

          import com.alibaba.fastjson.JSONObject;
          import com.cn.xxx.common.annotation.Resubmit;
          import com.cn.xxx.common.annotation.impl.ResubmitLock;
          import com.cn.xxx.common.dto.RequestDTO;
          import com.cn.xxx.common.dto.ResponseDTO;
          import com.cn.xxx.common.enums.ResponseCode;
          import lombok.extern.log4j.Log4j;
          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.springframework.stereotype.Component;

          import java.lang.reflect.Method;

          /**
           * @ClassName RequestDataAspect
           * @Description 數(shù)據(jù)重復(fù)提交校驗
           * @Author lijing
           * @Date 2019/05/16 17:05
           **/

          @Log4j
          @Aspect
          @Component
          public class ResubmitDataAspect {

              private final static String DATA = "data";
              private final static Object PRESENT = new Object();

              @Around("@annotation(com.cn.xxx.common.annotation.Resubmit)")
              public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable {
                  Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
                  //獲取注解信息
                  Resubmit annotation = method.getAnnotation(Resubmit.class);
                  int delaySeconds = annotation.delaySeconds();
                  Object[] pointArgs = joinPoint.getArgs();
                  String key = "";
                  //獲取第一個參數(shù)
                  Object firstParam = pointArgs[0];
                  if (firstParam instanceof RequestDTO) {
                      //解析參數(shù)
                      JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());
                      JSONObject data = JSONObject.parseObject(requestDTO.getString(DATA));
                      if (data != null) {
                          StringBuffer sb = new StringBuffer();
                          data.forEach((k, v) -> {
                              sb.append(v);
                          });
                          //生成加密參數(shù) 使用了content_MD5的加密方式
                          key = ResubmitLock.handleKey(sb.toString());
                      }
                  }
                  //執(zhí)行鎖
                  boolean lock = false;
                  try {
                      //設(shè)置解鎖key
                      lock = ResubmitLock.getInstance().lock(key, PRESENT);
                      if (lock) {
                          //放行
                          return joinPoint.proceed();
                      } else {
                          //響應(yīng)重復(fù)提交異常
                          return new ResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);
                      }
                  } finally {
                      //設(shè)置解鎖key和解鎖時間
                      ResubmitLock.getInstance().unLock(lock, key, delaySeconds);
                  }
              }
          }

          ④注解使用案例

          @ApiOperation(value = "保存我的帖子接口", notes = "保存我的帖子接口")
              @PostMapping("/posts/save")
              @Resubmit(delaySeconds = 10)
              public ResponseDTO<BaseResponseDataDTO> saveBbsPosts(@RequestBody @Validated RequestDTO<BbsPostsRequestDTO> requestDto) {
                  return bbsPostsBizService.saveBbsPosts(requestDto);
              }
          以上就是本地鎖的方式進行的冪等提交 使用了Content-MD5 進行加密 只要參數(shù)不變,參數(shù)加密 密值不變,key存在就阻止提交
          當(dāng)然也可以使用 一些其他簽名校驗 在某一次提交時先 生成固定簽名 提交到后端 根據(jù)后端解析統(tǒng)一的簽名作為 每次提交的驗證token 去緩存中處理即可.
          8)借助分布式redis鎖 (參考其他)
          在 pom.xml 中添加上 starter-web、starter-aop、starter-data-redis 的依賴即可
          <dependencies>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-web</artifactId>
              </dependency>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-aop</artifactId>
              </dependency>
              <dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-data-redis</artifactId>
              </dependency>
          </dependencies>

          屬性配置 在 application.properites 資源文件中添加 redis 相關(guān)的配置項

          spring.redis.host=localhost
          spring.redis.port=6379
          spring.redis.password=123456
          主要實現(xiàn)方式:
          熟悉 Redis 的朋友都知道它是線程安全的,我們利用它的特性可以很輕松的實現(xiàn)一個分布式鎖,如 opsForValue().setIfAbsent(key,value)它的作用就是如果緩存中沒有當(dāng)前 Key 則進行緩存同時返回 true 反之亦然;
          當(dāng)緩存后給 key 在設(shè)置個過期時間,防止因為系統(tǒng)崩潰而導(dǎo)致鎖遲遲不釋放形成死鎖;那么我們是不是可以這樣認(rèn)為當(dāng)返回 true 我們認(rèn)為它獲取到鎖了,在鎖未釋放的時候我們進行異常的拋出…
          package com.battcn.interceptor;

          import com.battcn.annotation.CacheLock;
          import com.battcn.utils.RedisLockHelper;
          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.springframework.beans.factory.annotation.Autowired;
          import org.springframework.context.annotation.Configuration;
          import org.springframework.util.StringUtils;

          import java.lang.reflect.Method;
          import java.util.UUID;

          /**
           * redis 方案
           *
           * @author Levin
           * @since 2018/6/12 0012
           */

          @Aspect
          @Configuration
          public class LockMethodInterceptor {

              @Autowired
              public LockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator) {
                  this.redisLockHelper = redisLockHelper;
                  this.cacheKeyGenerator = cacheKeyGenerator;
              }

              private final RedisLockHelper redisLockHelper;
              private final CacheKeyGenerator cacheKeyGenerator;


              @Around("execution(public * *(..)) && @annotation(com.battcn.annotation.CacheLock)")
              public Object interceptor(ProceedingJoinPoint pjp) {
                  MethodSignature signature = (MethodSignature) pjp.getSignature();
                  Method method = signature.getMethod();
                  CacheLock lock = method.getAnnotation(CacheLock.class);
                  if (StringUtils.isEmpty(lock.prefix())) {
                      throw new RuntimeException("lock key don't null...");
                  }
                  final String lockKey = cacheKeyGenerator.getLockKey(pjp);
                  String value = UUID.randomUUID().toString();
                  try {
                      // 假設(shè)上鎖成功,但是設(shè)置過期時間失效,以后拿到的都是 false
                      final boolean success = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());
                      if (!success) {
                          throw new RuntimeException("重復(fù)提交");
                      }
                      try {
                          return pjp.proceed();
                      } catch (Throwable throwable) {
                          throw new RuntimeException("系統(tǒng)異常");
                      }
                  } finally {
                      // TODO 如果演示的話需要注釋該代碼;實際應(yīng)該放開
                      redisLockHelper.unlock(lockKey, value);
                  }
              }
          }

          RedisLockHelper 通過封裝成 API 方式調(diào)用,靈活度更加高

          package com.battcn.utils;

          import org.springframework.boot.autoconfigure.AutoConfigureAfter;
          import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
          import org.springframework.context.annotation.Configuration;
          import org.springframework.data.redis.connection.RedisStringCommands;
          import org.springframework.data.redis.core.RedisCallback;
          import org.springframework.data.redis.core.StringRedisTemplate;
          import org.springframework.data.redis.core.types.Expiration;
          import org.springframework.util.StringUtils;

          import java.util.concurrent.Executors;
          import java.util.concurrent.ScheduledExecutorService;
          import java.util.concurrent.TimeUnit;
          import java.util.regex.Pattern;

          /**
           * 需要定義成 Bean
           *
           * @author Levin
           * @since 2018/6/15 0015
           */

          @Configuration
          @AutoConfigureAfter(RedisAutoConfiguration.class)
          public class RedisLockHelper {


              private static final String DELIMITER = "|";

              /**
               * 如果要求比較高可以通過注入的方式分配
               */

              private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);

              private final StringRedisTemplate stringRedisTemplate;

              public RedisLockHelper(StringRedisTemplate stringRedisTemplate) {
                  this.stringRedisTemplate = stringRedisTemplate;
              }

              /**
               * 獲取鎖(存在死鎖風(fēng)險)
               *
               * @param lockKey lockKey
               * @param value   value
               * @param time    超時時間
               * @param unit    過期單位
               * @return true or false
               */

              public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit) {
                  return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));
              }

              /**
               * 獲取鎖
               *
               * @param lockKey lockKey
               * @param uuid    UUID
               * @param timeout 超時時間
               * @param unit    過期單位
               * @return true or false
               */

              public boolean lock(String lockKey, final String uuid, long timeout, final TimeUnit unit) {
                  final long milliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();
                  boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
                  if (success) {
                      stringRedisTemplate.expire(lockKey, timeout, TimeUnit.SECONDS);
                  } else {
                      String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
                      final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));
                      if (Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()) {
                          return true;
                      }
                  }
                  return success;
              }


              /**
               * @see <a >Redis Documentation: SET</a>
               */

              public void unlock(String lockKey, String value) {
                  unlock(lockKey, value, 0, TimeUnit.MILLISECONDS);
              }

              /**
               * 延遲unlock
               *
               * @param lockKey   key
               * @param uuid      client(最好是唯一鍵的)
               * @param delayTime 延遲時間
               * @param unit      時間單位
               */

              public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {
                  if (StringUtils.isEmpty(lockKey)) {
                      return;
                  }
                  if (delayTime <= 0) {
                      doUnlock(lockKey, uuid);
                  } else {
                      EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);
                  }
              }

              /**
               * @param lockKey key
               * @param uuid    client(最好是唯一鍵的)
               */

              private void doUnlock(final String lockKey, final String uuid) {
                  String val = stringRedisTemplate.opsForValue().get(lockKey);
                  final String[] values = val.split(Pattern.quote(DELIMITER));
                  if (values.length <= 0) {
                      return;
                  }
                  if (uuid.equals(values[1])) {
                      stringRedisTemplate.delete(lockKey);
                  }
              }

          }

          redis的提交參照

          https://blog.battcn.com/2018/06/13/springboot/v2-cache-redislock/

             
          往期熱門文章:

          1、Logback 與 log4j2 性能哪個更強?
          2、只用Tomcat,不用Nginx行不行?
          3、聽說你還在用Xshell?
          4、驚艷到我的 10個 MySQL高級查詢技巧!
          5、我有點想用JDK17了
          6、解放大腦:ChatGPT + PlantUML = 不用畫圖了
          7、高逼格的SQL寫法:行行比較
          8、限流算法哪家強?時間窗口,令牌桶與漏桶算法對比
          9、每天都提交代碼,那你知道.git目錄內(nèi)部的秘密嗎?
          10、我患上了空指針后遺癥

          瀏覽 239
          1點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          1點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产日韩一区二区三免费高清 | 成人网视频在线 | 在线观看免费欧美操逼视频 | 成人黄色网址 | 日本黄免费看 |