<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ù)提交的方案

          共 23374字,需瀏覽 47分鐘

           ·

          2021-03-07 11:53

          1.什么是冪等


          在我們編程中常見(jiàn)冪等 

          1)select查詢天然冪等   

          2)delete刪除也是冪等,刪除同一個(gè)多次效果一樣 

          3)update直接更新某個(gè)值的,冪等 

          4)update更新累加操作的,非冪等 

          5)insert非冪等操作,每次新增一條



          2.產(chǎn)生原因


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

          1)點(diǎn)擊提交按鈕兩次; 

          2)點(diǎn)擊刷新按鈕; 

          3)使用瀏覽器后退按鈕重復(fù)之前的操作,導(dǎo)致重復(fù)提交表單; 

          4)使用瀏覽器歷史記錄重復(fù)提交表單; 

          5)瀏覽器重復(fù)的HTTP請(qǐng); 

          6)nginx重發(fā)等情況; 

          7)分布式RPC的try重發(fā)等;




          3.解決方案



          在提交后執(zhí)行頁(yè)面重定向,這就是所謂的Post-Redirect-Get (PRG)模式。

          簡(jiǎn)言之,當(dāng)用戶提交了表單后,你去執(zhí)行一個(gè)客戶端的重定向,轉(zhuǎn)到提交成功信息頁(yè)面。

          這能避免用戶按F5導(dǎo)致的重復(fù)提交,而其也不會(huì)出現(xiàn)瀏覽器表單重復(fù)提交的警告,也能消除按瀏覽器前進(jìn)和后退按導(dǎo)致的同樣問(wèn)題。



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


          比較復(fù)雜  不適合移動(dòng)端APP的應(yīng)用 這里不詳解


          insert使用唯一索引 update使用 樂(lè)觀鎖 version版本法

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



          使用select ... for update  ,這種和 synchronized 

          鎖住先查再insert or update一樣,但要避免死鎖,效率也較差 

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



          原理:使用了 ConcurrentHashMap 并發(fā)容器 putIfAbsent 方法,和 ScheduledThreadPoolExecutor 定時(shí)任務(wù),也可以使用guava cache的機(jī)制, gauva中有配有緩存的有效時(shí)間 也是可以的key的生成 Content-MD5 Content-MD5 是指 Body 的 MD5 值,只有當(dāng) Body 非Form表單時(shí)才計(jì)算MD5,計(jì)算方式直接將參數(shù)和參數(shù)名稱統(tǒng)一加密MD5。

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

          當(dāng)然本地鎖只適用于單機(jī)部署的應(yīng)用.


          ①配置注解


          import java.lang.annotation.*;

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

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

              int delaySeconds() default 20;
          }


          ②實(shí)例化鎖


          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 ConcurrentHashMapLOCK_CACHE = new ConcurrentHashMap<>(200);
              private static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5new ThreadPoolExecutor.DiscardPolicy());


             // private static final CacheCACHES = CacheBuilder.newBuilder()
                      // 最大緩存 100 個(gè)
             // .maximumSize(1000)
                      // 設(shè)置寫(xiě)緩存后 5 秒鐘過(guò)期
             // .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 對(duì)應(yīng)的key
               * @param value
               * @return
               */

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

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

              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ù)提交校驗(yàn)
           * @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 = "";
                  //獲取第一個(gè)參數(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和解鎖時(shí)間
                      ResubmitLock.getInstance().unLock(lock, key, delaySeconds);
                  }
              }
          }


          ④注解使用案例


          @ApiOperation(value = "保存我的帖子接口", notes = "保存我的帖子接口")
              @PostMapping("/posts/save")
              @Resubmit(delaySeconds = 10)
              public ResponseDTOsaveBbsPosts(@RequestBody @Validated RequestDTOrequestDto) {
                  return bbsPostsBizService.saveBbsPosts(requestDto);
              }


          以上就是本地鎖的方式進(jìn)行的冪等提交  使用了Content-MD5 進(jìn)行加密   只要參數(shù)不變,參數(shù)加密 密值不變,key存在就阻止提交

          當(dāng)然也可以使用  一些其他簽名校驗(yàn)  在某一次提交時(shí)先 生成固定簽名  提交到后端 根據(jù)后端解析統(tǒng)一的簽名作為 每次提交的驗(yàn)證token 去緩存中處理即可.



          在 pom.xml 中添加上 starter-web、starter-aop、starter-data-redis 的依賴即可


          <dependencies>
              <dependency>
                  <groupId>org.springframework.bootgroupId>
                  <artifactId>spring-boot-starter-webartifactId>
              dependency>
              <dependency>
                  <groupId>org.springframework.bootgroupId>
                  <artifactId>spring-boot-starter-aopartifactId>
              dependency>
              <dependency>
                  <groupId>org.springframework.bootgroupId>
                  <artifactId>spring-boot-starter-data-redisartifactId>
              dependency>
          dependencies>


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


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


          主要實(shí)現(xiàn)方式: 熟悉 Redis 的朋友都知道它是線程安全的,我們利用它的特性可以很輕松的實(shí)現(xiàn)一個(gè)分布式鎖,如 opsForValue().setIfAbsent(key,value)它的作用就是如果緩存中沒(méi)有當(dāng)前 Key 則進(jìn)行緩存同時(shí)返回 true 反之亦然;當(dāng)緩存后給 key 在設(shè)置個(gè)過(guò)期時(shí)間,防止因?yàn)橄到y(tǒng)崩潰而導(dǎo)致鎖遲遲不釋放形成死鎖;那么我們是不是可以這樣認(rèn)為當(dāng)返回 true 我們認(rèn)為它獲取到鎖了,在鎖未釋放的時(shí)候我們進(jì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è)置過(guò)期時(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 如果演示的話需要注釋該代碼;實(shí)際應(yīng)該放開(kāi)
                      redisLockHelper.unlock(lockKey, value);
                  }
              }
          }


          RedisLockHelper 通過(guò)封裝成 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 = "|";

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

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

              private final StringRedisTemplate stringRedisTemplate;

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

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

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

              /**
               * 獲取鎖
               *
               * @param lockKey lockKey
               * @param uuid UUID
               * @param timeout 超時(shí)時(shí)間
               * @param unit 過(guò)期單位
               * @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 Redis Documentation: SET
               */

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

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

              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/


          推薦閱讀:

          【重磅分享】從零到一搭建推薦系統(tǒng)指南白皮書(shū).pdf(附48頁(yè)下載鏈接)

          億級(jí)(無(wú)限級(jí))并發(fā),沒(méi)那么難

          世界的真實(shí)格局分析,地球人類社會(huì)底層運(yùn)行原理

          數(shù)據(jù)中臺(tái):基于標(biāo)簽體系的360°用戶畫(huà)像

          不是你需要中臺(tái),而是一名合格的架構(gòu)師(附各大廠中臺(tái)建設(shè)PPT)

          【干貨】愛(ài)奇藝推薦中臺(tái)探索與實(shí)踐.pdf(附下載鏈接)

          字節(jié)跳動(dòng)MySQL學(xué)習(xí)筆記火了,完整版開(kāi)放下載!

          瀏覽 54
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  俺去啦新网 | 成人无码在线播放 | 99国产精品久久久久久久久久久久久 | 亚洲艹逼 | 国产精品无码久久久久久免费 |