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

分布式系統(tǒng)中接口的冪等性

開發(fā) 前端 分布式
支付系統(tǒng)將這些支付信息生成對應(yīng)的支付訂單并落庫,同時(shí)針對每筆還款記錄生產(chǎn)一個(gè)消息信息到消息系統(tǒng),消息的消費(fèi)者就是訂單系統(tǒng)。訂單系統(tǒng)接受到消息后去結(jié)算當(dāng)前用戶的金額清算:先還本金,本金還清再還滯納金,都還清則該筆訂單結(jié)清并提升可借貸額度。

業(yè)務(wù)場景 #

某公司有個(gè)借貸的項(xiàng)目,具體業(yè)務(wù)類似于阿里的螞蟻借唄,用戶在平臺上借款,然后規(guī)定一個(gè)到期時(shí)間,在該時(shí)間內(nèi)用戶需將借款還清并收取一定的手續(xù)費(fèi),如果規(guī)定時(shí)間逾期未還上,則會(huì)產(chǎn)生滯納金。

[[375620]]

用戶發(fā)起借款因此會(huì)產(chǎn)生一筆借款訂單,用戶可通過支付寶或在系統(tǒng)中綁定銀行卡到期自動(dòng)扣款等方式進(jìn)行還款。還款流程都走支付系統(tǒng),因此用戶還款是否逾期以及逾期天數(shù)、逾期費(fèi)等都通過系統(tǒng)來計(jì)算。

 

分布式系統(tǒng)中接口的冪等性

但是在做訂單系統(tǒng)的時(shí)候,遇到這樣一個(gè)業(yè)務(wù)場景,由于業(yè)務(wù)原因允許用戶通過線下支付寶還款,即我們提供一個(gè)公司官方的支付寶二維碼,用戶掃碼還款,然后財(cái)務(wù)不定期的去拉取該支付寶賬戶下的還款清單并生成規(guī)范化的Excel表格錄入到支付系統(tǒng)。

支付系統(tǒng)將這些支付信息生成對應(yīng)的支付訂單并落庫,同時(shí)針對每筆還款記錄生產(chǎn)一個(gè)消息信息到消息系統(tǒng),消息的消費(fèi)者就是訂單系統(tǒng)。訂單系統(tǒng)接受到消息后去結(jié)算當(dāng)前用戶的金額清算:先還本金,本金還清再還滯納金,都還清則該筆訂單結(jié)清并提升可借貸額度,……,整個(gè)流程大致如下:

 

分布式系統(tǒng)中接口的冪等性

從上面的流程描述可以知道,相當(dāng)于原來線上的支付現(xiàn)在轉(zhuǎn)移到線下進(jìn)行,這會(huì)產(chǎn)生一個(gè)問題:支付結(jié)算的不及時(shí)。例如用戶的訂單在今天19-05-27到期,但是用戶在19-05-26還清,財(cái)務(wù)在19-05-27甚至更晚的時(shí)候從支付寶拉取清單錄入支付系統(tǒng)。這樣就造成了實(shí)際上用戶是未逾期還清借款而我們這邊卻記錄的是用戶未還清且產(chǎn)生了滯納金。

當(dāng)然以上的是業(yè)務(wù)范疇的問題,我們今天要說的是支付系統(tǒng)發(fā)送消息到訂單系統(tǒng)的環(huán)節(jié)中的一個(gè)問題。大家都知道為了避免消息丟失或者訂單系統(tǒng)處理異常或者網(wǎng)絡(luò)問題等問題,我們設(shè)計(jì)消息系統(tǒng)的時(shí)候都需要考慮消息持久化和消息的失敗重試機(jī)制。

 

分布式系統(tǒng)中接口的冪等性

對于重試機(jī)制,假如訂單系統(tǒng)消費(fèi)了消息,但是由于網(wǎng)絡(luò)等問題消息系統(tǒng)未收到反饋是否已成功處理。這時(shí)消息系統(tǒng)會(huì)根據(jù)配置的規(guī)則隔段時(shí)間就 retry 一次。你 retry 一次沒錯(cuò),是為了保證系統(tǒng)的處理正常性,但是如果這時(shí)網(wǎng)絡(luò)恢復(fù)正常,我第一次收到的消息成功處理了,這時(shí)我又收到了一條消息,如果沒有做一些防護(hù)措施,會(huì)產(chǎn)生如下情況:用戶付款一次但是訂單系統(tǒng)計(jì)算了兩次,這樣會(huì)造成財(cái)務(wù)賬單異常對不上賬的情況發(fā)生。那就可能用戶笑呵呵老板哭兮兮了。

接口冪等性 #

為了防止上述情況的發(fā)生,我們需要提供一個(gè)防護(hù)措施,對于同一筆支付信息如果我其中某一次處理成功了,我雖然又接收到了消息,但是這時(shí)我不處理了,即保證接口的 冪等性 。

維基百科上的定義:

  • 冪等(idempotent、idempotence)是一個(gè)數(shù)學(xué)與計(jì)算機(jī)學(xué)概念,常見于抽象代數(shù)中。

在編程中一個(gè)冪等操作的特點(diǎn)是其任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同。冪等函數(shù),或冪等方法,是指可以使用相同參數(shù)重復(fù)執(zhí)行,并能獲得相同結(jié)果的函數(shù)。這些函數(shù)不會(huì)影響系統(tǒng)狀態(tài),也不用擔(dān)心重復(fù)執(zhí)行會(huì)對系統(tǒng)造成改變。例如,“setTrue()”函數(shù)就是一個(gè)冪等函數(shù),無論多次執(zhí)行,其結(jié)果都是一樣的,更復(fù)雜的操作冪等保證是利用唯一交易號(流水號)實(shí)現(xiàn).

任意多次執(zhí)行所產(chǎn)生的影響均與一次執(zhí)行的影響相同,這是冪等性的核心特點(diǎn)。其實(shí)在我們編程中主要操作就是CURD,其中讀取(Retrieve)操作和刪除(Delete)操作是天然冪等的,受影響的就是創(chuàng)建(Create)、更新(Update)。

對于業(yè)務(wù)中需要考慮冪等性的地方一般都是接口的重復(fù)請求,重復(fù)請求是指同一個(gè)請求因?yàn)槟承┰虮欢啻翁峤弧?dǎo)致這個(gè)情況會(huì)有幾種場景:

  • 前端重復(fù)提交 :提交訂單,用戶快速重復(fù)點(diǎn)擊多次,造成后端生成多個(gè)內(nèi)容重復(fù)的訂單。
  • 接口超時(shí)重試 :對于給第三方調(diào)用的接口,為了防止網(wǎng)絡(luò)抖動(dòng)或其他原因造成請求丟失,這樣的接口一般都會(huì)設(shè)計(jì)成超時(shí)重試多次。
  • 消息重復(fù)消費(fèi) :MQ消息中間件,消息重復(fù)消費(fèi)。

對于一些業(yè)務(wù)場景影響比較大的,接口的冪等性是個(gè)必須要考慮的問題,例如金錢的交易方面的接口。否則一個(gè)錯(cuò)誤的、考慮不周的接口可能會(huì)給公司帶來巨額的金錢損失,那么背鍋的肯定是程序員自己了。

冪等性實(shí)現(xiàn)方式 #

對于和web端交互的接口,我們可以在前端攔截一部分,例如防止表單重復(fù)提交,按鈕置灰、隱藏、不可點(diǎn)擊等方式。

但是前端做控制實(shí)際效益不是很高,懂點(diǎn)技術(shù)的都會(huì)模擬請求調(diào)用你的服務(wù),所以安全的策略還是需要從后端的接口層來做。

那么后端要實(shí)現(xiàn)分布式接口的冪等性有哪些策略方式呢?主要可以從以下幾個(gè)方面來考慮實(shí)現(xiàn):

Token機(jī)制 #

針對前端重復(fù)連續(xù)多次點(diǎn)擊的情況,例如用戶購物提交訂單,提交訂單的接口就可以通過 Token 的機(jī)制實(shí)現(xiàn)防止重復(fù)提交。

 

分布式系統(tǒng)中接口的冪等性

主要流程就是:

  1. 服務(wù)端提供了發(fā)送token的接口。我們在分析業(yè)務(wù)的時(shí)候,哪些業(yè)務(wù)是存在冪等問題的,就必須在執(zhí)行業(yè)務(wù)前,先去獲取token,服務(wù)器會(huì)把token保存到redis中。(微服務(wù)肯定是分布式了,如果單機(jī)就適用jvm緩存)。
  2. 然后調(diào)用業(yè)務(wù)接口請求時(shí),把token攜帶過去,一般放在請求頭部。
  3. 服務(wù)器判斷token是否存在redis中,存在表示第一次請求,這時(shí)把redis中的token刪除,繼續(xù)執(zhí)行業(yè)務(wù)。
  4. 如果判斷token不存在redis中,就表示是重復(fù)操作,直接返回重復(fù)標(biāo)記給client,這樣就保證了業(yè)務(wù)代碼,不被重復(fù)執(zhí)行。

數(shù)據(jù)庫去重表 #

往去重表里插入數(shù)據(jù)的時(shí)候,利用數(shù)據(jù)庫的唯一索引特性,保證唯一的邏輯。唯一序列號可以是一個(gè)字段,例如訂單的訂單號,也可以是多字段的唯一性組合。例如設(shè)計(jì)如下的數(shù)據(jù)庫表。

 

  1. CREATE TABLE `t_idempotent` ( 
  2.   `id` int(11) NOT NULL COMMENT 'ID'
  3.   `serial_no` varchar(255)  NOT NULL COMMENT '唯一序列號'
  4.   `source_type` varchar(255)  NOT NULL COMMENT '資源類型'
  5.   `status` int(4) DEFAULT NULL COMMENT '狀態(tài)'
  6.   `remark` varchar(255)  NOT NULL COMMENT '備注'
  7.   `create_by` bigint(20) DEFAULT NULL COMMENT '創(chuàng)建人'
  8.   `create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間'
  9.   `modify_by` bigint(20) DEFAULT NULL COMMENT '修改人'
  10.   `modify_time` datetime DEFAULT NULL COMMENT '修改時(shí)間'
  11.   PRIMARY KEY (`id`) 
  12.   UNIQUE KEY `key_s` (`serial_no`,`source_type`, `remark`)  COMMENT '保證業(yè)務(wù)唯一性' 
  13. ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='冪等性校驗(yàn)表'

我們注意看如下這幾個(gè)關(guān)鍵性字段,

  1. @IdempotentKey 

由于數(shù)據(jù)建立了 serial_no , source_type , remark 三個(gè)字段組合構(gòu)成的唯一索引,所以可以通過這個(gè)來去重達(dá)到接口的冪等性,具體的代碼設(shè)計(jì)如下,

 

  1. public class PaymentOrderReq { 
  2.     /** 
  3.      * 支付寶流水號 
  4.      */ 
  5.     @IdempotentKey(order=1) 
  6.     private String alipayNo; 
  7.     /** 
  8.      * 支付訂單ID 
  9.      */ 
  10.     @IdempotentKey(order=2) 
  11.     private String paymentOrderNo; 
  12.     /** 
  13.      * 支付金額 
  14.      */ 
  15.     private Long amount; 

因?yàn)橹Ц秾毩魉柡陀唵翁栐谙到y(tǒng)中是唯一的,所以唯一序列號可由他們組合 MD5 生成,具體的生成方式如下:

 

  1. private void getIdempotentKeys(Object keySource, Idempotent idempotent) { 
  2.     TreeMap<Integer, Object> keyMap = new TreeMap<Integer, Object>(); 
  3.     for (Field field : keySource.getClass().getDeclaredFields()) { 
  4.         if (field.isAnnotationPresent(IdempotentKey.class)) { 
  5.             try { 
  6.                 field.setAccessible(true); 
  7.                 keyMap.put(field.getAnnotation(IdempotentKey.class).order(), 
  8.                         field.get(keySource)); 
  9.             } catch (IllegalArgumentException | IllegalAccessException e) { 
  10.                 logger.error("", e); 
  11.                 return
  12.             } 
  13.         } 
  14.     } 
  15.     generateIdempotentKey(idempotent, keyMap.values().toArray()); 

生成冪等Key,如果有多個(gè)key可以通過分隔符 "|" 連接,

 

  1. private void generateIdempotentKey(Idempotent idempotent, Object... keyObj) { 
  2.      if (keyObj.length == 0) { 
  3.          logger.info("idempotentkey is empty,{}", keyObj); 
  4.          return
  5.      } 
  6.      StringBuilder serialNo= new StringBuilder(); 
  7.      for (Object key : keyObj) { 
  8.          serialNo.append(key.toString()).append("|"); 
  9.      } 
  10.      idempotent.setRemark(serialNo.toString()); 
  11.      idempotent.setSerialNo(md5(serialNo)); 
  12.  } 

一切準(zhǔn)備就緒,則可對外提供冪等性校驗(yàn)的接口方法,接口方法為:

 

  1. public <T> void idempotentCheck(IdempotentTypeEnum idempotentType, T keyObj) throws IdempotentException { 
  2.     Idempotent idempotent = new Idempotent(); 
  3.     getIdempotentKeys(keyObj, idempotent ); 
  4.     if (StringUtils.isBlank(idempotent.getSerialNo())) { 
  5.         throw new ServiceException("fail to get idempotentkey"); 
  6.     } 
  7.     idempotentEvent.setSourceType(idempotentType.name()); 
  8.     try { 
  9.         idempotentMapper.saveIdempotent(idempotent); 
  10.     } catch (DuplicateKeyException e) { 
  11.         logger.error("idempotent check fail", e); 
  12.         throw new IdempotentException(idempotent); 
  13.     } 

當(dāng)然這個(gè)接口的方法具體在項(xiàng)目中合理的使用就看項(xiàng)目要求了,可以通過 @Autowire 注解注入到需要使用的地方,但是缺點(diǎn)就是每個(gè)地方都需要調(diào)用。我個(gè)人推薦的是自定義一個(gè)注解,在需要冪等性保證的接口上加上該注解,然后通過攔截器方法攔截使用。這樣簡單便不會(huì)造成代碼侵入和污染。

另外,使用數(shù)據(jù)庫防重表的方式它有個(gè)嚴(yán)重的缺點(diǎn),那就是系統(tǒng)容錯(cuò)性不高,如果冪等表所在的數(shù)據(jù)庫連接異常或所在的服務(wù)器異常,則會(huì)導(dǎo)致整個(gè)系統(tǒng)冪等性校驗(yàn)出問題。如果做數(shù)據(jù)庫備份來防止這種情況,又需要額外忙碌一通了啊。

Redis實(shí)現(xiàn) #

上面介紹過防重表的設(shè)計(jì)方式和偽代碼,也說過它的一個(gè)很明顯的缺點(diǎn)。所以我們另外介紹一個(gè)Redis的實(shí)現(xiàn)方式。

Redis實(shí)現(xiàn)的方式就是將唯一序列號作為Key,唯一序列號的生成方式和上面介紹的防重表的一樣,value可以是你想填的任何信息。唯一序列號也可以是一個(gè)字段,例如訂單的訂單號,也可以是多字段的唯一性組合。當(dāng)然這里需要設(shè)置一個(gè) key 的過期時(shí)間,否則 Redis 中會(huì)存在過多的 key。具體校驗(yàn)流程如下圖所示,實(shí)現(xiàn)代碼也很簡單這里就不寫了。

 

分布式系統(tǒng)中接口的冪等性

由于企業(yè)如果考慮在項(xiàng)目中使用 Redis,因?yàn)榇蟛糠謺?huì)拿它作為緩存來使用,那么一般都會(huì)是集群的方式出現(xiàn),至少肯定也會(huì)部署兩臺Redis服務(wù)器。所以我們使用Redis來實(shí)現(xiàn)接口的冪等性是最適合不過的了。

狀態(tài)機(jī) #

對于很多業(yè)務(wù)是有一個(gè)業(yè)務(wù)流轉(zhuǎn)狀態(tài)的,每個(gè)狀態(tài)都有前置狀態(tài)和后置狀態(tài),以及最后的結(jié)束狀態(tài)。例如流程的待審批,審批中,駁回,重新發(fā)起,審批通過,審批拒絕。訂單的待提交,待支付,已支付,取消。

以訂單為例,已支付的狀態(tài)的前置狀態(tài)只能是待支付,而取消狀態(tài)的前置狀態(tài)只能是待支付,通過這種狀態(tài)機(jī)的流轉(zhuǎn)我們就可以控制請求的冪等。

 

  1. public enum OrderStatusEnum { 
  2.     UN_SUBMIT(0, 0, "待提交"), 
  3.     UN_PADING(0, 1, "待支付"), 
  4.     PAYED(1, 2, "已支付待發(fā)貨"), 
  5.     DELIVERING(2, 3, "已發(fā)貨"), 
  6.     COMPLETE(3, 4, "已完成"), 
  7.     CANCEL(0, 5, "已取消"), 
  8.     ; 
  9.     //前置狀態(tài) 
  10.     private int preStatus; 
  11.     //狀態(tài)值 
  12.     private int status; 
  13.     //狀態(tài)描述 
  14.     private String desc
  15.     OrderStatusEnum(int preStatus, int status, String desc) { 
  16.         this.preStatus = preStatus; 
  17.         this.status = status; 
  18.         this.desc = desc
  19.     } 
  20.     //... 

假設(shè)當(dāng)前狀態(tài)是已支付,這時(shí)候如果支付接口又接收到了支付請求,則會(huì)拋異常或拒絕此次處理。

總結(jié)

通過以上的了解我們可以知道,針對不同的業(yè)務(wù)場景我們需要靈活的選擇冪等性的實(shí)現(xiàn)方式。

例如防止類似于前端重復(fù)提交、重復(fù)下單的場景就可以通過 Token 的機(jī)制實(shí)現(xiàn),而那些有狀態(tài)前置和后置轉(zhuǎn)換的場景則可以通過狀態(tài)機(jī)的方式實(shí)現(xiàn)冪等性,對于那些重復(fù)消費(fèi)和接口重試的場景則使用數(shù)據(jù)庫唯一索引的方式實(shí)現(xiàn)更合理。

責(zé)任編輯:未麗燕 來源: segmentfault.com
相關(guān)推薦

2023-03-07 08:19:16

接口冪等性SpringBoot

2024-07-03 11:59:40

2025-02-14 14:22:40

2023-10-26 07:32:42

2021-12-01 10:13:48

場景分布式并發(fā)

2022-01-12 09:01:24

分布式系統(tǒng)容錯(cuò)服務(wù)

2021-01-18 14:34:59

冪等性接口客戶端

2021-07-28 08:39:25

分布式架構(gòu)系統(tǒng)

2023-05-12 08:23:03

分布式系統(tǒng)網(wǎng)絡(luò)

2024-03-13 15:18:00

接口冪等性高并發(fā)

2023-07-19 08:22:01

分布式系統(tǒng)數(shù)據(jù)

2024-10-18 08:00:00

分布式系統(tǒng)背壓數(shù)據(jù)庫

2025-02-26 08:20:18

2020-07-15 08:14:12

高并發(fā)

2023-02-11 00:04:17

分布式系統(tǒng)安全

2018-12-14 10:06:22

緩存分布式系統(tǒng)

2023-05-29 14:07:00

Zuul網(wǎng)關(guān)系統(tǒng)

2013-08-09 09:27:31

2022-04-14 10:24:27

分布式系統(tǒng)性能

2021-03-28 09:45:05

冪等性接口數(shù)據(jù)
點(diǎn)贊
收藏

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