這是《百圖解碼支付系統(tǒng)設(shè)計與實(shí)現(xiàn)》專欄系列文章中的第(14)篇。點(diǎn)擊上方關(guān)注,深入了解支付系統(tǒng)的方方面面。
本篇主要介紹分布式場景下常用的并發(fā)流量控制方案,包括固定時間窗口、滑動時間窗口、漏桶、令牌桶、分布式消息中間件等,并重點(diǎn)講清楚固定時間窗口應(yīng)用原理和應(yīng)用場景,以及使用reids實(shí)現(xiàn)的核心代碼。
在非支付場景,也常常需要用到這些并發(fā)流量控制方案。
1. 前言
在互聯(lián)網(wǎng)應(yīng)用里面,并發(fā)流量控制無所不在。在支付系統(tǒng)中,流量控制同樣是一個關(guān)鍵的技術(shù)方面,主要用于確保系統(tǒng)的穩(wěn)定性和可靠性,尤其在高流量的情況下。以下是一些主要使用流量控制的場景:
- 對外API限流:對外提供的API(如支付接口)需要限流來保護(hù)后端服務(wù)不會過載。
- 保護(hù)外部渠道:大促時,對下流渠道的支付流量要做削峰填谷,避免突發(fā)流量把渠道打掛。
- 保護(hù)內(nèi)部應(yīng)用:大促時,內(nèi)部各應(yīng)用要根據(jù)流量模型配置限流值,避免形成雪崩。
- 滿足外部退款限流要求:電商批量提交退款時,支付系統(tǒng)內(nèi)部要在分布式集群環(huán)境下對某個渠道實(shí)現(xiàn)低至1TPS的退款并發(fā),避免超過渠道退款并發(fā)導(dǎo)致大批量失敗。
特別說明的是,流量控制通常包括限流和限速。
限流:就是流量達(dá)到一定程度,超過的流量會全部立即拒絕掉,也就是快速失敗。比如上面的API限流。
限速:一般是指接收流量后,先保存到隊(duì)列中,然后按指定的速度發(fā)出去,如果超過隊(duì)列最大值,才會拒絕。比如上面的支付流量和退款流量打到外部渠道。
另外,支付和退款流量控制雖然都是流量控制,但有一些細(xì)小的區(qū)別:
- 支付的限流TPS通常比較高,從十幾TPS到幾百TPS都有,排隊(duì)時效性要求很高,秒級內(nèi)就要付出去。
- 退款的限流TPS通常比較低,在國外的基礎(chǔ)設(shè)施建設(shè)很差,甚至部分渠道要求退款1TPS。但是排隊(duì)時效性要求很低,幾天內(nèi)退出去就行。
2. 幾種方案對比
固定窗口:算法簡單,對突然流量響應(yīng)不夠靈活。超過流量的會直接拒絕,通常用于限流。
滑動窗口: 算法簡單,對突然流量響應(yīng)比固定窗口靈活。超過流量的會直接拒絕,通常用于限流。
漏桶算法:在固定窗口的基礎(chǔ)之上,使用隊(duì)列緩沖流量。提供了穩(wěn)定的流量輸出,適用于對流量平滑性有嚴(yán)格要求的場景。后面會介紹如何應(yīng)用到外部渠道退款場景。
令牌桶算法:在滑動窗口的基礎(chǔ)之上,使用隊(duì)列緩沖流量。能夠允許一定程度的突發(fā)性流量,但實(shí)現(xiàn)較為復(fù)雜。
分布式消息中間件:如Kafka和RabbitMQ等,能夠有效地對消息進(jìn)行緩沖和管理,增加系統(tǒng)復(fù)雜性,且如果需要精確控制流量還需要引入額外的機(jī)制。后面會介紹如何應(yīng)用到外部渠道支付場景。
3. 固定時間窗口原理
固定窗口算法,也稱為時間窗口算法,是一種流量控制和速率限制策略。此算法將時間軸分割成等長、不重疊的時間段,稱為“窗口”。每個窗口都有一個獨(dú)立的計數(shù)器,用于跟蹤窗口期間的事件數(shù)量(如API調(diào)用、數(shù)據(jù)包傳輸?shù)龋?/p>
固定窗口算法的好處是簡單,缺點(diǎn)也很明顯,就是無法應(yīng)對突發(fā)流量,比如每秒30并發(fā),如果前100ms來了30個請求,那么在10ms內(nèi)就會把30個請求打出去,后面的900ms的請求全部拒絕。
工作流程:
- 窗口定義:首先確定窗口大小,比如1秒鐘。
- 計數(shù):每當(dāng)發(fā)生一個事件(比如一個請求到達(dá)),就在當(dāng)前窗口的計數(shù)器上加一。
- 限制檢查:如果當(dāng)前窗口的計數(shù)器達(dá)到預(yù)設(shè)閥值,則拒絕新的請求。直到下一個窗口開始。
- 窗口重置:當(dāng)前窗口結(jié)束時,計算數(shù)器重置為零,開始下一個窗口計數(shù)。
4. 固定時間窗口在支付系統(tǒng)中的應(yīng)用場景
主要用于簡單的限流。比如在渠道網(wǎng)關(guān)做限流,發(fā)送渠道的請求最大不能超過測算出來的值,避免渠道側(cè)過載,可能會導(dǎo)致支付請求批量失敗。
是有損服務(wù)的一種實(shí)現(xiàn)方式。
5. 使用redis實(shí)現(xiàn)的核心代碼
為什么選擇redis?因?yàn)樵诜植际綀鼍跋?,限流需要有一個集群共用的計算數(shù)來保存當(dāng)前時間窗口的請求量,redis是一個比較優(yōu)的方案。
場景示例:WPG渠道的支付每秒不能超過20TPS。
那么設(shè)計key=“WPG-PAY” + 當(dāng)前時間戳(精確到S),數(shù)據(jù)過期時間為2S(這個過期時間主要是兼容各服務(wù)器的時間差)。
下面是流程圖:
lua腳本:limit.lua
local key = KEYS[1]
-- 默認(rèn)為2S超期,精確到S級。也可以改造成由外面?zhèn)鬟M(jìn)來 --
local expireTime = 2
-- 先自增,如果不存在就自動創(chuàng)建 --
redis.incr(key);
local count = tonumber(redis.call("get", key))
-- 如果結(jié)果為1,說明是新增的,設(shè)置超時時間 --
if count == 1 then
redis.call("expire", key, expireTime)
end
return count;
redis操作類:RedisLimitUtil
/**
* redis限流操作類
*/
@Component
public class RedisLimitUtil {
// 限流腳本
private static final String LIMIT_SCRIPT_LUA = "limit.lua";
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private DefaultRedisScript<Long> limitScript;
/**
* 緩存腳本
*/
@PostConstruct
public void cacheScript() {
limitScript = new DefaultRedisScript();
limitScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(LIMIT_SCRIPT_LUA)));
limitScript.setResultType(Long.class);
List<Boolean> cachedScripts = redisTemplate.getConnectionFactory().getConnection().scriptExists(
limitScript.getSha1());
// 需要緩存
if (CollectionUtils.isEmpty(cachedScripts) || !cachedScripts.get(0)) {
redisTemplate.getConnectionFactory().getConnection().
scriptLoad(redisTemplate.getStringSerializer().serialize(limitScript.getScriptAsString()));
}
}
/**
* 判斷是否限流
* 這里不考慮超過long最大值的情況,系統(tǒng)在達(dá)到long最大值前就奔潰了。
*/
public boolean isLimited(String key, long countLimit) {
Long count = redisTemplate.execute(limitScript, Lists.newArrayList(key));
return countLimit >= count;
}
}
使用:PayServiceImpl
/**
* 支付服務(wù)示例
*/
public class PayServiceImpl implements PayService {
@Autowired
private RedisLimitUtil redisLimitUtil;
@Override
public PayOrder pay(PayRequest request) {
if (isLimited(request)) {
throw new RequestLimitedException(buildExceptionMessage(request));
}
// 其它業(yè)務(wù)處理
... ...
}
/*
* 限流判斷
*/
private boolean isLimited(PayRequest request) {
// 限流KEY,這里以[業(yè)務(wù)類型 + 渠道]舉例
String key = request.getBizType() + request.getChannel();
// 限流值
Long countLimit = countLimitMap.get(key);
// 如果key對應(yīng)的限流值沒有配置,或配置為-1,說明不限流
if (null == countLimit || -1 == countLimit) {
return false;
}
return redisLimitUtil.isLimited(key + buildTime(), countLimit);
}
}
注釋寫得比較清楚,沒有什么需要補(bǔ)充的。
6. 結(jié)束語
分布式流控有很多實(shí)現(xiàn)方案,使用redis實(shí)現(xiàn)的固定時間窗口是最簡單的方案,而且也非常實(shí)用,應(yīng)付一般的場景已經(jīng)足夠使用。
下一篇會介紹滑動時間窗口算法及實(shí)現(xiàn)。
7. 傳送門
支付系統(tǒng)設(shè)計與實(shí)現(xiàn)是一個專業(yè)性非常強(qiáng)的領(lǐng)域,里面涉及到的很多設(shè)計思路和理論也可以應(yīng)用到其它行業(yè)的軟件設(shè)計中,比如冪等性,加解密,領(lǐng)域設(shè)計思想,狀態(tài)機(jī)設(shè)計等。
在《百圖解碼支付系統(tǒng)設(shè)計與實(shí)現(xiàn)》的知識宇宙,每一篇深入淺出的文章都是一顆既獨(dú)立但又彼此強(qiáng)關(guān)聯(lián)的星球,有必要提供一個傳送門以便讓大家即刻到達(dá)想要了解的文章。
專欄地址:百圖解碼支付系統(tǒng)設(shè)計與實(shí)現(xiàn)
領(lǐng)域相關(guān):
支付行業(yè)黑話:支付系統(tǒng)必知術(shù)語一網(wǎng)打盡
跟著圖走,學(xué)支付:在線支付系統(tǒng)設(shè)計的圖解教程
支付交易的三重奏:收單、結(jié)算與拒付在支付系統(tǒng)中的協(xié)奏曲
在線支付系統(tǒng)的精英搭檔:深入剖析收銀核心與支付引擎的協(xié)同作戰(zhàn)(一)
在線支付系統(tǒng)的精英搭檔:深入剖析收銀核心與支付引擎的協(xié)同作戰(zhàn)(二)文章來源:http://www.zghlxwxcb.cn/news/detail-804978.html
技術(shù)專題:
交易流水號的藝術(shù):掌握支付系統(tǒng)的業(yè)務(wù)ID生成指南
揭密支付安全:為什么你的交易無法被篡改
金融密語:揭秘支付系統(tǒng)的加解密藝術(shù)
支付系統(tǒng)日志設(shè)計完全指南:構(gòu)建高效監(jiān)控和問題排查體系的關(guān)鍵基石
避免重復(fù)扣款:分布式支付系統(tǒng)的冪等性原理與實(shí)踐
支付系統(tǒng)的心臟:簡潔而精妙的狀態(tài)機(jī)設(shè)計與核心代碼實(shí)現(xiàn)
精確掌控并發(fā):分布式環(huán)境下并發(fā)流量控制的設(shè)計與實(shí)現(xiàn)(一)
精確掌控并發(fā):分布式環(huán)境下并發(fā)流量控制的設(shè)計與實(shí)現(xiàn)(二)
金融疆界:在線支付系統(tǒng)渠道網(wǎng)關(guān)的創(chuàng)新設(shè)計(一)文章來源地址http://www.zghlxwxcb.cn/news/detail-804978.html
到了這里,關(guān)于精確掌控并發(fā):固定時間窗口算法在分布式環(huán)境下并發(fā)流量控制的設(shè)計與實(shí)現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!