關於解決系統接口冪等性問題的解決

什麼是冪等性?

對於同一個業務操作,不管調用多少次,在數據庫的存儲,或者得到的結果應該是一樣的。

冪等性的設計思想

以支付寶或者微信的充值爲例,在我們支付完之後,支付寶、微信會給我們回調,來通知我們的系統支付成功,而在這之前,我們系統中已經存儲了這條訂單信息,我們要做的事就是需要在支付寶或者微信給我們回調之後,不管回調多少次,我們接口應該針對於同一個訂單得到的結果是一樣的。

針對於我們系統中應該是有一個唯一的商品訂單號:out_trade_no

支付寶中的回調給我們返回了:out_trade_no【商戶訂單號(自己定義)】,trade_no【支付寶交易號】

冪等性接口的實現方式

方式一(最常用的,最方便的方式,但是有問題):

爲什麼會說這是最常用的呢,因爲這樣實現是最簡單的,並沒有太多的思考,

過程如下面:

  1. 收到支付寶的回調,
  2. 根據trade_no和out_trade_no 查詢數據庫訂單信息,該訂單是否被處理
  3. 如果該訂單已經處理,則直接返回,如果未處理繼續向下執行
  4. 開啓本地事務,
  5. 本地系統給用戶賬戶加錢
  6. 將訂單狀態修改爲交易成功
  7. 提交本地事務

思考:

看似該方式很合適,但是,如果,支付寶、微信給我們的通知多次,同時到達步驟二,會發生什麼事情呢?顯而易見,查詢到訂單都是未處理的,那麼就會發生多次給賬戶加錢的情況,所以,我們最常用的這種方式是有問題的

方式二(加鎖):

想一想,方式一中,是因爲出現了併發問題,所以導致賬戶重複充值的情況,那麼我們可不可以用加鎖的方式來解決呢?當然是可以的

過程如下:

  1. 接收到支付寶的支付成功回調請求
  2. 調用java中的鎖,
  3. 根據trade_no和out_trade_no 查詢數據庫訂單信息,該訂單是否被處理
  4. 如果訂單已處理直接返回,若未處理,繼續向下執行
  5. 開啓本地事務
  6. 本地系統給用戶加錢
  7. 將訂單狀態置爲成功
  8. 提交本地事務
  9. 釋放Lock鎖

思考:

這樣看來已經沒什麼問題了,是吧?其實並不然,想想看,如果我們的應用只是部署了一份,這樣做是沒有問題的,但是,如果我們的應用做了負載均衡,部署了多臺機器,這樣做是不是會出現問題,支付寶的回調過來之後,經過負載均衡服務,將請求分配到不同的機器上,這種方式是不是就不行了呢?此時相當於是無鎖處理了,又會出現方式一的結果。

想一想nginx負載均衡中請求的策略中可以設置,同一ip的請求,都在同一臺服務器上,採用這種配置,是否可以解決該問題呢?

方式三:(悲觀鎖方式)

使用數據庫的悲觀鎖方式,其實跟方式二的加鎖方式很像,只不過是依靠數據庫來實現,數據庫中悲觀鎖方式是使用 for update來實現的,過程如下:

  1. 接收到支付寶的支付成功請求。
  2. 打開本地事務
  3. 查詢訂單信息並加上悲觀鎖(select * from t_order where order_id = trade_no for update;)
  4. 判斷訂單是否已被處理,如果已處理,直接返回,如果未處理,繼續向下執行
  5. 本地系統給用戶賬戶加錢
  6. 將訂單狀態修改爲已處理
  7. 提交本地事務

思考:

該方式其實主要用運了數據庫的 for update,關於 for update 做出解釋:

1.當線程A執行for update,數據會對當前記錄加鎖,其他線程執行到此行代碼的時候,會等待線程A釋放鎖之後,纔可以獲取鎖,繼續後續操作。
2.事物提交時,for update獲取的鎖會自動釋放。

如果我們的業務系統邏輯比較複雜,那麼,在併發情況下,會導致後面的線程處於無效的等待狀態,都在等待獲取 for update悲觀鎖,這樣不利於系統的併發操作

方式四:樂觀鎖

利用數據庫中的樂觀鎖來實現,過程如下:

      1.接收到支付寶的成功支付回調請求

      2.查詢訂單信息(select * from t_order where order_id = trade_no;)

      3.判斷訂單信息是否已被處理,如果已被處理直接返回,如果未被處理繼續執行

      4.打開本地事務

      5.本地系統給用戶賬號加錢

      6.使用類似下面的僞代碼給修改訂單狀態爲成功

update t_order set status = 1 where order_id = trade_no where status = 0;
//上面的update操作會返回影響的行數num
if(num==1){
 //表示更新成功
 提交事務;
}else{
 //表示更新失敗
 回滾事務;
}

      思考:

update t_order set status = 1 where order_id = trade_no where status = 0;

是依靠樂觀鎖來實現的,執行這條sql,如果有多個線程同事到達這段代碼,數據庫內部會保證update同一條記錄會排隊,最終只有一條update會執行成功,其他未成功的返回的num是0,然後根據num來進行提交或者回滾操作

方式五:唯一約束

依賴數據庫中的唯一約束來實現,其實也很簡單,過程如下:

首先,我們需要建一張表,t_uq_dipose,這張表裏面呢包含了一個業務類型字段,和該業務類型在系統中的唯一的訂單號,業務來時,先查詢該表中有沒有對應的數據,如果沒有,繼續執行,如果要是有,直接返回

先創建一張表:

CREATE TABLE `t_uq_dipose` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `ref_type` varchar(32) NOT NULL DEFAULT '' COMMENT '關聯對象類型',
  `ref_id` varchar(64) NOT NULL DEFAULT '' COMMENT '關聯對象id',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_1` (`ref_type`,`ref_id`) COMMENT '保證業務唯一性'
);

可以看到,我們建了唯一性約束,ref_type和ref_id 這樣的話可以保證插入到該表中的數據絕對是唯一的。

過程如下:

  1. 收到支付寶成功支付回調
  2. 查詢 t_uq_dipose 表可以判斷訂單是否已處理
  3. 判斷訂單是否已處理,如果已處理直接返回,如果未處理,繼續向下執行
  4. 打開本地事務
  5. 給本地系統中用戶加錢
  6. 將訂單狀態修改爲成功
  7. 向 t_uq_dipose 表中插入數據,插入成功提交本地事務,插入不成功,回滾本地事務。

總結

實現冪等性常見的方法有:悲觀鎖、樂觀鎖、唯一約束

幾種方式,按照最優排序:樂觀鎖 > 唯一約束 > 悲觀鎖

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章