<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 種方案解決重復提交問題!冪等

          共 25415字,需瀏覽 51分鐘

           ·

          2021-06-22 22:14

          來源:juejin.im/post/5d31928c51882564c966a71c

          • 1.什么是冪等
          • 2.產(chǎn)生原因
          • 3.解決方案
          • ①配置注解
          • ②實例化鎖
          • ③AOP 切面
          • ④注解使用案例

          1.什么是冪等

          在我們編程中常見冪等

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

          2.產(chǎn)生原因

          由于重復點擊或者網(wǎng)絡重發(fā) eg:

          • 點擊提交按鈕兩次;
          • 點擊刷新按鈕;
          • 使用瀏覽器后退按鈕重復之前的操作,導致重復提交表單;
          • 使用瀏覽器歷史記錄重復提交表單;
          • 瀏覽器重復的HTTP請;
          • nginx重發(fā)等情況;
          • 分布式RPC的try重發(fā)等;

          3.解決方案

          1)前端js提交禁止按鈕可以用一些js組件

          2)使用Post/Redirect/Get模式

          在提交后執(zhí)行頁面重定向,這就是所謂的Post-Redirect-Get (PRG)模式。簡言之,當用戶提交了表單后,你去執(zhí)行一個客戶端的重定向,轉到提交成功信息頁面。這能避免用戶按F5導致的重復提交,而其也不會出現(xiàn)瀏覽器表單重復提交的警告,也能消除按瀏覽器前進和后退按導致的同樣問題。

          3)在session中存放一個特殊標志

          在服務器端,生成一個唯一的標識符,將它存入session,同時將它寫入表單的隱藏字段中,然后將表單頁面發(fā)給瀏覽器,用戶錄入信息后點擊提交,在服務器端,獲取表單中隱藏字段的值,與session中的唯一標識符比較,相等說明是首次提交,就處理本次請求,然后將session中的唯一標識符移除;不相等說明是重復提交,就不再處理。

          4)其他借助使用header頭設置緩存控制頭Cache-control等方式

          比較復雜 不適合移動端APP的應用 這里不詳解

          5)借助數(shù)據(jù)庫

          insert使用唯一索引 update使用 樂觀鎖 version版本法

          這種在大數(shù)據(jù)量和高并發(fā)下效率依賴數(shù)據(jù)庫硬件能力,可針對非核心業(yè)務

          6)借助悲觀鎖

          使用select … for update ,這種和 synchronized 鎖住先查再insert or update一樣,但要避免死鎖,效率也較差

          針對單體 請求并發(fā)不大 可以推薦使用

          7)借助本地鎖(本文重點)

          原理:

          使用了 ConcurrentHashMap 并發(fā)容器 putIfAbsent 方法,和 ScheduledThreadPoolExecutor 定時任務,也可以使用guava cache的機制, gauva中有配有緩存的有效時間也是可以的key的生成

          Content-MD5

          Content-MD5 是指 Body 的 MD5 值,只有當 Body 非Form表單時才計算MD5,計算方式直接將參數(shù)和參數(shù)名稱統(tǒng)一加密MD5

          MD5在一定范圍類認為是唯一的 近似唯一 當然在低并發(fā)的情況下足夠了

          本地鎖只適用于單機部署的應用.

          ①配置注解

          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
           * 重復提交鎖
           */

          @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)
                      // 設置寫緩存后 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   對應的key
               * @param value
               * @return
               */

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

              /**
               * 延時釋放鎖 用以控制短時間內(nèi)的重復提交
               *
               * @param lock         是否需要解鎖
               * @param key          對應的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ù)重復提交校驗
           * @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 {
                      //設置解鎖key
                      lock = ResubmitLock.getInstance().lock(key, PRESENT);
                      if (lock) {
                          //放行
                          return joinPoint.proceed();
                      } else {
                          //響應重復提交異常
                          return new ResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);
                      }
                  } finally {
                      //設置解鎖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存在就阻止提交

          當然也可以使用 一些其他簽名校驗 在某一次提交時先 生成固定簽名 提交到后端 根據(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 相關的配置項

          spring.redis.host=localhost
          spring.redis.port=6379
          spring.redis.password=123456

          主要實現(xiàn)方式:

          熟悉 Redis 的朋友都知道它是線程安全的,我們利用它的特性可以很輕松的實現(xiàn)一個分布式鎖,如 opsForValue().setIfAbsent(key,value)它的作用就是如果緩存中沒有當前 Key 則進行緩存同時返回 true 反之亦然;

          當緩存后給 key 在設置個過期時間,防止因為系統(tǒng)崩潰而導致鎖遲遲不釋放形成死鎖;那么我們是不是可以這樣認為當返回 true 我們認為它獲取到鎖了,在鎖未釋放的時候我們進行異常的拋出…

          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 {
                      // 假設上鎖成功,但是設置過期時間失效,以后拿到的都是 false
                      final boolean success = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());
                      if (!success) {
                          throw new RuntimeException("重復提交");
                      }
                      try {
                          return pjp.proceed();
                      } catch (Throwable throwable) {
                          throw new RuntimeException("系統(tǒng)異常");
                      }
                  } finally {
                      // TODO 如果演示的話需要注釋該代碼;實際應該放開
                      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;
              }

              /**
               * 獲取鎖(存在死鎖風險)
               *
               * @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的提交參照

          - END -

          歡迎添加程序汪個人微信 itwang007  進粉絲群或圍觀朋友圈



          往期資源  需要請自取

          Java項目分享 最新整理全集,找項目不累啦 03版

          臥槽!字節(jié)跳動《算法中文手冊》火了,完整版 PDF 開放下載

          字節(jié)跳動總結的設計模式 PDF 火了,完整版開放下載!


          堪稱神級的Spring Boot手冊,從基礎入門到實戰(zhàn)進階


          臥槽!阿里大佬總結的《圖解Java》火了,完整版PDF開放下載!

          喜歡就"在看"唄^_^

          瀏覽 43
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  91啪啪啪视频专用网站 | 国产精品九九九九九九九九 | 毛片网络| 女人高潮a毛片在线看 | 免费在线黄片视频 |