什麼是冪等性?
對於同一個業務操作,不管調用多少次,在數據庫的存儲,或者得到的結果應該是一樣的。
冪等性的設計思想
以支付寶或者微信的充值爲例,在我們支付完之後,支付寶、微信會給我們回調,來通知我們的系統支付成功,而在這之前,我們系統中已經存儲了這條訂單信息,我們要做的事就是需要在支付寶或者微信給我們回調之後,不管回調多少次,我們接口應該針對於同一個訂單得到的結果是一樣的。
針對於我們系統中應該是有一個唯一的商品訂單號:out_trade_no
支付寶中的回調給我們返回了:out_trade_no【商戶訂單號(自己定義)】,trade_no【支付寶交易號】
冪等性接口的實現方式
方式一(最常用的,最方便的方式,但是有問題):
爲什麼會說這是最常用的呢,因爲這樣實現是最簡單的,並沒有太多的思考,
過程如下面:
- 收到支付寶的回調,
- 根據trade_no和out_trade_no 查詢數據庫訂單信息,該訂單是否被處理
- 如果該訂單已經處理,則直接返回,如果未處理繼續向下執行
- 開啓本地事務,
- 本地系統給用戶賬戶加錢
- 將訂單狀態修改爲交易成功
- 提交本地事務
思考:
看似該方式很合適,但是,如果,支付寶、微信給我們的通知多次,同時到達步驟二,會發生什麼事情呢?顯而易見,查詢到訂單都是未處理的,那麼就會發生多次給賬戶加錢的情況,所以,我們最常用的這種方式是有問題的
方式二(加鎖):
想一想,方式一中,是因爲出現了併發問題,所以導致賬戶重複充值的情況,那麼我們可不可以用加鎖的方式來解決呢?當然是可以的
過程如下:
- 接收到支付寶的支付成功回調請求
- 調用java中的鎖,
- 根據trade_no和out_trade_no 查詢數據庫訂單信息,該訂單是否被處理
- 如果訂單已處理直接返回,若未處理,繼續向下執行
- 開啓本地事務
- 本地系統給用戶加錢
- 將訂單狀態置爲成功
- 提交本地事務
- 釋放Lock鎖
思考:
這樣看來已經沒什麼問題了,是吧?其實並不然,想想看,如果我們的應用只是部署了一份,這樣做是沒有問題的,但是,如果我們的應用做了負載均衡,部署了多臺機器,這樣做是不是會出現問題,支付寶的回調過來之後,經過負載均衡服務,將請求分配到不同的機器上,這種方式是不是就不行了呢?此時相當於是無鎖處理了,又會出現方式一的結果。
想一想nginx負載均衡中請求的策略中可以設置,同一ip的請求,都在同一臺服務器上,採用這種配置,是否可以解決該問題呢?
方式三:(悲觀鎖方式)
使用數據庫的悲觀鎖方式,其實跟方式二的加鎖方式很像,只不過是依靠數據庫來實現,數據庫中悲觀鎖方式是使用 for update來實現的,過程如下:
- 接收到支付寶的支付成功請求。
- 打開本地事務
- 查詢訂單信息並加上悲觀鎖(select * from t_order where order_id = trade_no for update;)
- 判斷訂單是否已被處理,如果已處理,直接返回,如果未處理,繼續向下執行
- 本地系統給用戶賬戶加錢
- 將訂單狀態修改爲已處理
- 提交本地事務
思考:
該方式其實主要用運了數據庫的 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 這樣的話可以保證插入到該表中的數據絕對是唯一的。
過程如下:
- 收到支付寶成功支付回調
- 查詢 t_uq_dipose 表可以判斷訂單是否已處理
- 判斷訂單是否已處理,如果已處理直接返回,如果未處理,繼續向下執行
- 打開本地事務
- 給本地系統中用戶加錢
- 將訂單狀態修改爲成功
- 向 t_uq_dipose 表中插入數據,插入成功提交本地事務,插入不成功,回滾本地事務。
總結
實現冪等性常見的方法有:悲觀鎖、樂觀鎖、唯一約束
幾種方式,按照最優排序:樂觀鎖 > 唯一約束 > 悲觀鎖