这是《百图解码支付系统设计与实现》专栏系列文章中的第(17)篇,也是流量控制系列的第(4)篇。点击上方关注,深入了解支付系统的方方面面。
本篇重点讲清楚令牌桶原理,在支付系统的应用场景,以及使用reids实现的核心代码。
在流量控制系列文章中的前三篇,分别介绍了固定时间窗口算法、滑动时间窗口算法、漏桶原理、应用场景和redis核心代码。
我们做个简单回顾:
固定窗口:算法简单,对突然流量响应不够灵活。超过流量的会直接拒绝,通常用于限流。
滑动窗口: 算法简单,对突然流量响应比固定窗口灵活。超过流量的会直接拒绝,通常用于限流。
漏桶算法:在固定窗口的基础之上,使用队列缓冲流量。提供了稳定的流量输出,适用于对流量平滑性有严格要求的场景。
今天讲的令牌桶算法,其实只需要在滑动窗口的基础之上,使用队列缓冲流量。令牌桶能够允许一定程度的突发性流量,但实现稍为复杂。
令牌桶算法是一种流量整形和流量控制机制。它的核心思想是以固定速率放入令牌到桶中,每个传入请求需要获取一个令牌才能继续执行。如果桶中无令牌可用,则请求要么等待,要么被拒绝。这种机制允许突发流量的发生,同时通过限制令牌的放入速率控制数据的平均传输率。
从上面的图中可以看到,实际实现时和漏桶很像。只是漏桶是以固定速率流出,而令牌桶允许一定的突发流量。
在支付系统中,令牌桶算法用于控制交易请求的并发数。比如前面漏桶那一篇中对渠道退款的流量控制。比如漏桶好一点的是平滑性更好。
又回到redis代码。因为直接把滑动时间窗口算法,再加一个队列就可以了。
参考滑动时间窗口算法中的redis代码实现,使用有序集合(sorted set)来实现了令牌桶算法:
class TokenBucketHolding {
private final LinkedBlockingQueue<Data> bucket;
private int limit;
private String bizType;
public LeakyBucketHolding(String bizType, int capacity, int limit) {
this.bizType = bizType;
this.bucket = new LinkedBlockingQueue<>(capacity);
this.limit = limit;
}
// 其它代码略
}
@Component
public class TokenBucket {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private Map<String, LeakyBucketHolding> bucketHoldingMap = new HashMap();
// 滑动时间窗口大小
private static final long WINDOW_SIZE_IN_SECONDS = 1000;
public boolean getToken(String key, String reuqestId, long countLimit) {
// 使用Redis的多个命令来实现滑动窗口
redisTemplate.zremrangeByScore(key, 0, currentTimeMillis - WINDOW_SIZE_IN_SECONDS);
long count = redisTemplate.zcard(key);
if (countLimit >= count) {
redisTemplate.zadd(key, currentTimeMillis, reuqestId);
return true;
} else {
return false;
}
}
// 添加数据到桶中
public boolean addData(Data data) {
String key = buildKey(data);
TokenBucketHolding holding = bucketHoldingMap.get(key);
if (null == holding) {
holding = buildHolding(data);
bucketHoldingMap.put(key, holding);
}
return holding.getLinkedBlockingQueue().offer(data);
}
public Data getData() {
for(TokenBucketHolding holding : bucketHoldingMap.values()) {
if(holding.getBucket().size() == 0) {
return null;
}
// 注意这里的uuid()是生成一个随机不重复的uuid,只是占位使用
boolean limited = !getToken(holding.getBizType(), uuid(), holding.getLimit());
if (limited) {
return null;
}
try {
return holding.getBucket().poll(10, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
log.log("Leaking process interrupted");
}
return null;
}
}
}
每个请求都以其发生的时间戳作为分数(SCORE)存储在集合中。通过移除旧于当前时间窗口的请求来维护滑动窗口。通过检查集合中的元素数量,以确定是否超过了设定的最大请求数。
使用一个队列来缓存数据,可以使用本机内存队列,也可以使用消息中间件,上面示例直接使用了内存队列,下面还有一个redis做为示例。在使用时需要根据实际情况做出技术选型。
public class RedisQ {
// 其它代码略
... ...
// 添加数据到队列中
public void addData(Data data) {
return redisTemplate.rpush(data.getBizType(), data);
}
// 添加数据到队列中
public Data getData(String bizType) {
return redisTemplate.lpop(bizType);
}
// 其它代码略
... ...
}
退款流量控制实例:RefundServiceImpl
/**
* 支付服务示例
*/
public class RefundServiceImpl implements RefudnService {
@Autowiread
private TokenBucket tokenBucket;
@Override
public RefundOrder refund(RefundRequest request) {
// 前置业务处理
... ...
Data data = buildData(request);
tokenBucket.addData(data);
// 其它业务处理
... ...
}
@PostConstruct
public void init() {
new Thread(() -> {
while (true) {
Data data = tokenBucket.getData();
if (null != data) {
process(data);
} else {
sleep(10);
}
}
}).start();
}
}
在代码中可以看到,退款请求来后,只需要往桶里扔就完事。然后等另外的线程按固定速度发出去。
代码中还存在的问题:
在实际应用中,要考虑以下几点以确保令牌桶算法的有效性和高效性:
令牌桶算法提供了一种有效的机制来控制和管理分布式环境下的并发请求。它不仅可以防止系统过载,还能够应对突发的高流量,从而保障支付系统的稳定性和可靠性。
下一篇聊聊分布式消息中间件在流控中的应用。
专栏地址:百图解码支付系统设计与实现
《百图解码支付系统设计与实现》专栏介绍
《百图解码支付系统设计与实现》专栏大纲及文章链接汇总(进度更新于2023.1.15)
领域相关(部分):
支付行业黑话:支付系统必知术语一网打尽
跟着图走,学支付:在线支付系统设计的图解教程
图解收单平台:打造商户收款的高效之道
图解结算平台:准确高效给商户结款
图解收银台:支付系统承上启下的关键应用
图解支付引擎:资产流动的枢纽
图解渠道网关:不只是对接渠道的接口(一)
技术专题(部分):
交易流水号的艺术:掌握支付系统的业务ID生成指南
揭密支付安全:为什么你的交易无法被篡改
金融密语:揭秘支付系统的加解密艺术
支付系统日志设计完全指南:构建高效监控和问题排查体系的关键基石
避免重复扣款:分布式支付系统的幂等性原理与实践
支付系统的心脏:简洁而精妙的状态机设计与核心代码实现
精确掌控并发:固定时间窗口算法在分布式环境下并发流量控制的设计与实现
精确掌控并发:滑动时间窗口算法在分布式环境下并发流量控制的设计与实现