支付功能設計
主要包括:訂單表,訂單日志表,訂單隊列,定時任務。
主要考慮:事務性、冪等性、安全性。
表結構設計
- 訂單表:
訂單表,最主要的就是訂單號、支付狀態。
CREATE TABLE `t_order` (
`fid` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵,自增id',
`forder_id` varchar(35) NOT NULL COMMENT '訂單號,唯一',
`fpay_status` varchar(15) DEFAULT '00' COMMENT '00:未支付,01:支付成功,10:訂單關閉,02:支付失敗,03:已下單,04:申請退款,05:退款成功,06:退款失敗 ',
`fuser_id` int(11) DEFAULT NULL COMMENT '用戶id',
`ftotal_price` decimal(25,2) NOT NULL COMMENT '總價',
`fcreate_time` datetime DEFAULT NULL COMMENT '購買時間',
PRIMARY KEY (`fid`),
UNIQUE KEY `idx_order` (`forder_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單表';
- 訂單日志表:
訂單日志表,最主要的就是訂單號,支付狀態,操作記錄,支付渠道。
CREATE TABLE `t_order_log` (
`fid` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵,自增id',
`forder_id` varchar(35) NOT NULL COMMENT '訂單號',
`fuser_id` int(11) DEFAULT NULL COMMENT '用戶id',
`fcreate_time` datetime DEFAULT NULL COMMENT '操作時間',
`fpay_status` varchar(15) DEFAULT '00' COMMENT '00:未支付,01:支付成功,10:訂單關閉,02:支付失敗,03:已下單,04:申請退款,05:退款成功,06:退款失敗 ',
`faction` tinyint(2) unsigned DEFAULT NULL COMMENT '操作記錄:1,提交;2,關閉;3,第三方回調;4.前端輪詢;5.后台查詢第三方;6.定時任務查詢',
`fresult` text COMMENT '訂單的回調結果',
`ftotal_price` decimal(25,2) NOT NULL COMMENT '總價',
`fpay_channel` varchar(25) DEFAULT NULL COMMENT '支付渠道。1,微信支付;2,支付寶;3,銀聯支付;',
PRIMARY KEY (`fid`),
UNIQUE KEY `idx_order` (`forder_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單日志表';
此文主要涉及到訂單表,以及訂單日志表。
略去用戶表,商品表,商品詳情表,商品訂單關系表。
支付流程及時序圖
商品系統:指的是購物網站等系統。
如果要進行"去庫存"的處理,可以在第10步中進行。
支付系統:指的是提供聚合支付服務的系統,同時提供微信支付,支付寶,銀聯等多種支付方式。
如果項目不需要這種聚合支付系統,也可以直接對接微信支付等。
第1步,用戶點擊"購買";
第2步,"商品系統"展示商品信息,生成訂單id;
第3步,用戶選擇商品信息,提交訂單。
訂單入Redis隊列,方便定時任務主動去取訂單進行支付結果查詢;
第4步,訪問"支付系統",並提供appId,訂單id,支付回調url等;
第5步,"支付系統"返回支付的url;
第6步,"商品系統"展示支付頁面,提供多種支付方式;
第7步,用戶選擇支付方式,進行支付;
第8步,"支付系統"通知支付結果;
第9步,"支付系統"調用支付回調url,告知支付結果詳情。
支付回調,每隔一段時間就會不斷地回調,比如 1/1/4/8/16/32/.... 直到接收到回復為止。
這個其實就是一種重試機制。通過延時隊列實現,一次回調不成功,就再次回調,直到成功為止。
第10步,根據支付狀態,決定是否執行商品業務邏輯。
第11步,通知支付結果。
支付回調
第9步和第10步是整個支付模塊中最重要的部分。
支付回調的接口,需要保證冪等性、事務性、安全性。
冪等性
- Q:怎么保證訂單接口的冪等性?
使用UUID生成32位數字字母組成的唯一訂單號,放入緩存中。
Redis實現的方式就是將訂單id作為Key,支付狀態作為value,並設置一個 key 的過期時間。
如果發現緩存中已經有了同樣的訂單id,就視為重復,不會進行支付請求,就直接返回。
如果支付成功,則刪除緩存中對應的訂單id。
- Q:假設Redis掛掉了,怎么避免並發插入導致訂單id重復?
由於訂單表中的訂單id加了唯一索引,所以即使並發查詢后並發插入,也不會出現訂單id重復的情況。
-
Q: 怎么保證支付回調接口的冪等性?
-
Q: 支付回調並發更新怎么處理?
-
Q: 支付系統會對發票系統進行回調,當沒有支付成功時, 就會每隔一段時間進行繼續回調,如何保證多次回調,只成功一次?
數據庫排它鎖。Select for update。性能太差,應付不了並發。
樂觀鎖的版本機制。添加version字段。在這種場景下太過冗余。
對於樂觀鎖和悲觀鎖的理解,詳情見:https://blog.csdn.net/puhaiyang/article/details/72284702
支付場景下,更好的做法是:使用狀態機制,直接利用訂單表已有的支付狀態。
當回調結果為支付成功,而且數據庫的支付狀態不是支付成功時,才將支付狀態改為支付成功,並執行業務功能。
如下:
UPDATE t_order SET fpay_status='01' WHERE forder_id='xxxxx' AND fpay_status!='01'
事務性
- Q: 怎么保證用戶付款后(支付狀態改變),相應的業務邏輯會執行,並且只執行一次?不多執行,也不少執行?
執行業務功能和修改支付狀態,要做事務處理,保證事務性。
如果支付成功,但是后續的業務功能執行失敗,就會回滾。
安全性
- Q:如何保證支付的安全性?
數據要加密,包括商戶號等信息。
支付回調接口,一定要校驗商品信息/商品價格是否正確,防止薅羊毛。
訂單日志表,記錄下所有的操作,包括生成訂單,提交訂單,支付回調,支付狀態,操作記錄(是第三方回調,還是前端輪詢,還是定時任務)等。
訂單日志表,還能分析訂單的整個流程,從訂單的開始到結束。
萬一出現訂單丟失,可以通過訂單日志表的記錄恢復訂單。
定時任務
- Q:為什么要引入定時任務?
定時任務:為了避免支付回調不成功,出現用戶付款成功,卻沒有執行功能服務的情況。
可以使用定時任務,主動去"支付系統"中查詢訂單的支付狀態,這是一種補償機制。
引入定時任務后,會有很多值得思考又有趣的問題。
- Q: 支付失敗怎么辦?
支付失敗,訂單會重新入Redis隊列,進行重試。
當重試次數達到限度,給用戶支付失敗的提醒,並將訂單出列。
當訂單過期時,給用戶提示訂單已經過期,並將訂單出列。
- Q: 怎么保證定時任務和第三方回調接口,同時發生時,用戶付一次錢,只執行一次業務功能?
同上"怎么保證支付回調接口的冪等性"
多個事務,同時執行更新,具體的分析見:
https://www.cnblogs.com/expiator/p/12084882.html
- Q:大量的訂單未支付怎么辦?
假如有很多筆訂單,進入Redis隊列后,又一直都沒有支付,那就可能會變成臟數據。
可以用另一個新的Redis隊列,當失敗達到一次的次數后,就用新隊列來存放這些未支付或者支付失敗的訂單。
- Q:如果Redis隊列中,存在100萬條訂單,怎么處理?
開多線程往Redis隊列里面取數據。