自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從零搭建開(kāi)發(fā)腳手架 保證服務(wù)的冪等性和防止重復(fù)請(qǐng)求

開(kāi)發(fā) 架構(gòu)
前端同步阻塞按鈕置灰,用戶(hù)點(diǎn)擊“發(fā)布”按鈕后,在網(wǎng)絡(luò)請(qǐng)求沒(méi)有返回,或者超時(shí)之前,用戶(hù)都不可以繼續(xù)點(diǎn)擊“發(fā)布按鈕”,界面可以將按鈕置灰或者轉(zhuǎn)圈。
本文轉(zhuǎn)載自微信公眾號(hào)「Java大廠面試官」,作者laker。轉(zhuǎn)載本文請(qǐng)聯(lián)系Java大廠面試官公眾號(hào)。
  •  什么是冪等?
  • 重復(fù)請(qǐng)求原因
  • 解決方案
    • 方案一:前端同步阻塞按鈕置灰
    • 方案二:前后端搭配干活,預(yù)生成訂單號(hào)
    • 方案三:通用方案,鎖模式
  • 實(shí)現(xiàn)
    • 自定義注解限制重復(fù)提交
    • 自定義切面攔截過(guò)濾處理
    • 使用示例

什么是冪等?

多次執(zhí)行的結(jié)果和一次執(zhí)行的結(jié)果相同,例如查詢(xún)操作天然就是冪等的。

重復(fù)請(qǐng)求原因

我們以電商場(chǎng)景中的下單來(lái)舉例,造成下單重復(fù)一般有以下幾個(gè)原因:

  • 用戶(hù)手抖點(diǎn)快了,導(dǎo)致多次重復(fù)下單。
  • 網(wǎng)絡(luò)抖動(dòng)導(dǎo)致失敗或者超時(shí)重傳,例如nginx、Fegin、RPC框架等

解決方案

方案一:前端同步阻塞按鈕置灰

前端同步阻塞按鈕置灰,用戶(hù)點(diǎn)擊“發(fā)布”按鈕后,在網(wǎng)絡(luò)請(qǐng)求沒(méi)有返回,或者超時(shí)之前,用戶(hù)都不可以繼續(xù)點(diǎn)擊“發(fā)布按鈕”,界面可以將按鈕置灰或者轉(zhuǎn)圈。

優(yōu)點(diǎn):實(shí)現(xiàn)成本極低

缺點(diǎn):

  1. 只能防御用戶(hù)手抖的誤操作。
  2. 確防不住遠(yuǎn)程調(diào)用的重試以及惡意重放。

方案二:前后端搭配干活,預(yù)生成訂單號(hào)

可以通過(guò)預(yù)先生成訂單號(hào)(在進(jìn)入下單頁(yè)面的時(shí)候生成訂單號(hào)),然后利用數(shù)據(jù)庫(kù)中訂單號(hào)的唯一約束這個(gè)特性,避免重復(fù)寫(xiě)入訂單。

時(shí)序圖如下:

細(xì)節(jié)如下:

訂單號(hào)生成時(shí)機(jī)

是在進(jìn)入訂單頁(yè)面,而不是提交訂單的時(shí)候 。

訂單號(hào)生成規(guī)則

  • 小規(guī)模系統(tǒng)完全可以用MySQL的Sequence或者Redis來(lái)生成。大規(guī)模系統(tǒng)也可以采用類(lèi)似雪花算法之類(lèi)的方式分布式生成GUID。
  • 訂單號(hào)中最好包含一些品類(lèi)、時(shí)間等信息,便于業(yè)務(wù)處理,它不能是一個(gè)單純自增的ID,否則別人很容易根據(jù)訂單號(hào)計(jì)算出你大致的銷(xiāo)量,所以訂單號(hào)的生產(chǎn)算法在保證不重復(fù)的前提下,一般都會(huì)加入很多業(yè)務(wù)規(guī)則在里面。

訂單號(hào)是否是主鍵

方式一:使用訂單號(hào)做主鍵

如果訂單號(hào)不是遞增的可能造成頻繁頁(yè)分裂,導(dǎo)致并發(fā)高的時(shí)候性能降低,所以要保證訂單號(hào)全局遞增。

方式二:有自增主鍵和訂單號(hào)列并設(shè)置唯一索引

因?yàn)橛唵翁?hào)不是主鍵,所以根據(jù)訂單號(hào)查詢(xún)會(huì)多一次回表操作,且如果訂單號(hào)不遞增二級(jí)訂單號(hào)索引也會(huì)有頁(yè)分裂。

訂單號(hào)可以由前端生成嗎

不可以,訂單號(hào)一定是在后端生成,后端生成可以保證全局唯一,且可以用于做安全認(rèn)證,不是后端頒發(fā)的訂單號(hào)不予處理。

提交訂單的時(shí)候,一種是先拿著訂單號(hào)去查庫(kù),讓業(yè)務(wù)代碼校驗(yàn)是否存在,另一種是直接利用庫(kù)表主鍵唯一約束拋異常,這兩種處理方式哪種性能更好?

選后者,等查完庫(kù)確定不存在再插入的時(shí)候,可能數(shù)據(jù)已經(jīng)變化了,訂單存在了,還是要拋異常,檢查意義不大。

方案三:通用方案,鎖模式

使用鎖來(lái)控制一段時(shí)間內(nèi)的重復(fù)請(qǐng)求,注意: 鎖的粒度為用戶(hù)+業(yè)務(wù)。

請(qǐng)求流程如下:

  • 1.請(qǐng)求接口時(shí),獲取一個(gè)鎖 鎖的粒度 :同一用戶(hù)的同一操作邏輯 鎖名稱(chēng)規(guī)則:業(yè)務(wù)名稱(chēng)+用戶(hù)ID
  • 2.給鎖設(shè)置過(guò)期時(shí)間10秒,防止業(yè)務(wù)邏輯執(zhí)行錯(cuò)誤,用戶(hù)一直被鎖住
  • 3.如果被鎖了,返回“正在處理,請(qǐng)勿重復(fù)提交”
  • 4.沒(méi)有被鎖,執(zhí)行正常邏輯,在邏輯結(jié)束后,刪掉鎖

實(shí)現(xiàn)

針對(duì)方案三實(shí)現(xiàn)如下:

自定義注解限制重復(fù)提交

  1. @Target(ElementType.METHOD) 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. @Documented 
  4. @Inherited 
  5. public @interface RepeatSubmitLimit { 
  6.     /** 
  7.      * 業(yè)務(wù)key,例如下單業(yè)務(wù) order 
  8.      */ 
  9.     String businessKey(); 
  10.  
  11.     /** 
  12.      * 業(yè)務(wù)參數(shù),用于做更細(xì)粒度鎖,例如鎖到具體 訂單id #orderId 
  13.      */ 
  14.     String businessParam() default ""
  15.  
  16.     /** 
  17.      * 是否用戶(hù)隔離,默認(rèn)啟用 
  18.      */ 
  19.     boolean userLimit() default true
  20.  
  21.     /** 
  22.      * 鎖時(shí)間 默認(rèn)10s 
  23.      */ 
  24.     int time() default 10; 

自定義切面攔截過(guò)濾處理

  1. @Component 
  2. @Aspect 
  3. @Slf4j 
  4. public class LimitSubmitAspect { 
  5.     LFUCache<Object, Object> LFUCACHE = CacheUtil.newLFUCache(100, 60 * 1000); 
  6.  
  7.     @Pointcut("@annotation(RepeatSubmitLimit)"
  8.     private void pointcut() { 
  9.     } 
  10.  
  11.     @Around("pointcut()"
  12.     public Object handleSubmit(ProceedingJoinPoint joinPoint) throws Throwable { 
  13.  
  14.  
  15.         Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); 
  16.         //獲取注解信息 
  17.         RepeatSubmitLimit repeatSubmitLimit = method.getAnnotation(RepeatSubmitLimit.class); 
  18.         int limitTime = repeatSubmitLimit.time(); 
  19.         String key = getLockKey(joinPoint, repeatSubmitLimit); 
  20.         Object result = LFUCACHE.get(keyfalse); 
  21.         if (result != null) { 
  22.             throw new BusinessException("請(qǐng)勿重復(fù)訪問(wèn)!"); 
  23.         } 
  24.         LFUCACHE.put(key, StpUtil.getLoginId(), limitTime * 1000); 
  25.         try { 
  26.             Object proceed = joinPoint.proceed(); 
  27.             return proceed; 
  28.         } catch (Throwable e) { 
  29.             log.error("Exception in {}.{}() with cause = \'{}\' and exception = \'{}\'", joinPoint.getSignature().getDeclaringTypeName(), 
  30.                     joinPoint.getSignature().getName(), e.getCause() != null ? e.getCause() : "NULL", e.getMessage(), e); 
  31.             throw e; 
  32.         } finally { 
  33.             LFUCACHE.remove(key); 
  34.         } 
  35.     } 
  36.  
  37.     private static final ParameterNameDiscoverer NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); 
  38.  
  39.     private static final ExpressionParser PARSER = new SpelExpressionParser(); 
  40.  
  41.     private String getLockKey(ProceedingJoinPoint joinPoint, RepeatSubmitLimit repeatSubmitLimit) { 
  42.         String businessKey = repeatSubmitLimit.businessKey(); 
  43.         boolean userLimit = repeatSubmitLimit.userLimit(); 
  44.         String businessParam = repeatSubmitLimit.businessParam(); 
  45.         if (userLimit) { 
  46.             businessKey = businessKey + ":" + StpUtil.getLoginId(); 
  47.         } 
  48.  
  49.         if (StrUtil.isNotBlank(businessParam)) { 
  50.             Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); 
  51.             EvaluationContext context = new MethodBasedEvaluationContext(null, method, joinPoint.getArgs(), NAME_DISCOVERER); 
  52.             String key = PARSER.parseExpression(businessParam).getValue(context, String.class); 
  53.             businessKey = businessKey + ":" + key
  54.         } 
  55.         return businessKey; 
  56.     } 

使用示例

  1. @RepeatSubmitLimit(businessKey = "tokenInfo", businessParam = "#name"
  2.   @GetMapping("/api/v1/tokenInfo"
  3.   public Response tokenInfo(String name) { 
  4.   } 

請(qǐng)求示例:http://localhost:8080/api/v1/tokenInfo?name=123

鎖粒度為:taokeninfo:1:123

防重效果:

  1.  code: "500"
  2.  msg: "請(qǐng)勿重復(fù)訪問(wèn)!" 

參考:

后端存儲(chǔ)實(shí)踐課

 

責(zé)任編輯:武曉燕 來(lái)源: Java大廠面試官
相關(guān)推薦

2021-04-28 16:10:48

開(kāi)發(fā)腳手架 Spring

2021-05-13 17:02:38

MDC腳手架日志

2021-07-13 18:42:38

Spring Boot腳手架開(kāi)發(fā)

2021-04-13 14:47:53

認(rèn)證授權(quán)Java

2021-02-19 22:43:50

開(kāi)發(fā)腳手架Controller

2021-07-29 18:49:49

Spring開(kāi)發(fā)腳手架

2020-08-19 08:55:47

Redis緩存數(shù)據(jù)庫(kù)

2021-04-20 19:24:16

腳手架 Java微信

2021-09-01 10:07:43

開(kāi)發(fā)零搭建Groovy

2021-03-09 17:11:09

數(shù)據(jù)庫(kù)腳手架開(kāi)發(fā)

2021-03-11 14:16:47

Spring Boo開(kāi)發(fā)腳手架

2021-04-14 17:18:27

冪等性數(shù)據(jù)源MySQL

2016-08-10 14:59:41

前端Javascript工具

2023-11-21 17:36:04

OpenFeignSentinel

2021-01-07 05:34:07

腳手架JDK緩存

2023-09-01 15:27:31

2018-08-30 16:08:37

Node.js腳手架工具

2018-06-11 14:39:57

前端腳手架工具node.js

2014-08-15 09:36:06

2020-07-15 08:14:12

高并發(fā)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)