精确掌控并发:令牌桶算法在分布式环境下并发流量控制的设计与实现

这篇具有很好参考价值的文章主要介绍了精确掌控并发:令牌桶算法在分布式环境下并发流量控制的设计与实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

这是《百图解码支付系统设计与实现》专栏系列文章中的第(17)篇,也是流量控制系列的第(4)篇。点击上方关注,深入了解支付系统的方方面面。

本篇重点讲清楚令牌桶原理,在支付系统的应用场景,以及使用reids实现的核心代码。

1. 前言

在流量控制系列文章中的前三篇,分别介绍了固定时间窗口算法、滑动时间窗口算法、漏桶原理、应用场景和redis核心代码。

我们做个简单回顾:

固定窗口:算法简单,对突然流量响应不够灵活。超过流量的会直接拒绝,通常用于限流。

滑动窗口: 算法简单,对突然流量响应比固定窗口灵活。超过流量的会直接拒绝,通常用于限流。

漏桶算法:在固定窗口的基础之上,使用队列缓冲流量。提供了稳定的流量输出,适用于对流量平滑性有严格要求的场景。

今天讲的令牌桶算法,其实只需要在滑动窗口的基础之上,使用队列缓冲流量。令牌桶能够允许一定程度的突发性流量,但实现稍为复杂。

2. 令牌桶算法原理

精确掌控并发:令牌桶算法在分布式环境下并发流量控制的设计与实现,百图解码支付系统设计与实现,分布式,令牌桶,令牌桶算法,支付系统设计与实现

令牌桶算法是一种流量整形和流量控制机制。它的核心思想是以固定速率放入令牌到桶中,每个传入请求需要获取一个令牌才能继续执行。如果桶中无令牌可用,则请求要么等待,要么被拒绝。这种机制允许突发流量的发生,同时通过限制令牌的放入速率控制数据的平均传输率。

  1. 令牌桶:令牌桶本质上是一个存放令牌的容器,其中令牌代表着可以执行某个操作的许可或权利。这个桶以固定的速率往其中放入令牌。
  2. 令牌生成速率:令牌桶算法规定了每秒往令牌桶中添加令牌的速率,通常以令牌/秒(Tokens Per Second,TPS)或类似的单位来表示。
  3. 令牌消耗:当一个请求到来时,它需要获取一个令牌才能继续执行。如果桶中有可用令牌,请求将获得一个令牌,并被允许执行。否则,请求将被阻塞或拒绝。
  4. 限制速率:通过控制令牌生成速率,令牌桶算法限制了请求的速率,确保不会发生过于频繁的请求,从而避免了系统的过载。
  5. 处理突发流量:令牌桶允许短时间内处理突发流量,因为它可以在桶中积累多个令牌,允许一次性处理多个请求,但仍然受到桶的容量限制。
  6. 令牌桶容量:令牌桶还具有一个容量,表示桶中最多可以存放多少个令牌。如果桶已满,新的令牌将被丢弃。

从上面的图中可以看到,实际实现时和漏桶很像。只是漏桶是以固定速率流出,而令牌桶允许一定的突发流量。

3. 支付系统应用场景

在支付系统中,令牌桶算法用于控制交易请求的并发数。比如前面漏桶那一篇中对渠道退款的流量控制。比如漏桶好一点的是平滑性更好。

4. 基于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)存储在集合中。通过移除旧于当前时间窗口的请求来维护滑动窗口。通过检查集合中的元素数量,以确定是否超过了设定的最大请求数。

  • zremrangeByScore 用于移除窗口之外的旧请求。
  • zcard 获取当前窗口内的请求数量。
  • zadd 将新请求添加到集合中。

使用一个队列来缓存数据,可以使用本机内存队列,也可以使用消息中间件,上面示例直接使用了内存队列,下面还有一个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();
    }
}

在代码中可以看到,退款请求来后,只需要往桶里扔就完事。然后等另外的线程按固定速度发出去。

代码中还存在的问题:

  1. 上述代码只是示例,真实的代码还有很多异常处理,比如队列数据丢失,需要重新处理。
  2. 暂时只能用于退款,因为退款的时效要求不高。另外,单机只需要开一个线程就行,因为服务器是分布式部署,多个服务器合并起来仍然是多个线程在并发处理。对退款是足够的。

5. 令牌桶使用注意事项

在实际应用中,要考虑以下几点以确保令牌桶算法的有效性和高效性:

  1. 合理设置参数:令牌生成的速率和桶的容量需要根据实际情况调整,以平衡响应性和限制性。
  2. 系统时间同步:在分布式环境中,确保所有节点的系统时间同步非常重要,以避免时间偏差导致的算法执行错误。
  3. 资源预留:在高并发场景下,令牌桶算法可能导致大量请求被暂时阻塞,需要确保系统有足够的资源来处理这些积压的请求。
  4. 监控与调整:持续监控令牌桶的性能,并根据系统负载情况进行动态调整。

6. 结束语

令牌桶算法提供了一种有效的机制来控制和管理分布式环境下的并发请求。它不仅可以防止系统过载,还能够应对突发的高流量,从而保障支付系统的稳定性和可靠性。

下一篇聊聊分布式消息中间件在流控中的应用。

7.精选

专栏地址百图解码支付系统设计与实现
《百图解码支付系统设计与实现》专栏介绍
《百图解码支付系统设计与实现》专栏大纲及文章链接汇总(进度更新于2023.1.15)
领域相关(部分)
支付行业黑话:支付系统必知术语一网打尽
跟着图走,学支付:在线支付系统设计的图解教程
图解收单平台:打造商户收款的高效之道
图解结算平台:准确高效给商户结款
图解收银台:支付系统承上启下的关键应用
图解支付引擎:资产流动的枢纽
图解渠道网关:不只是对接渠道的接口(一)

技术专题(部分)
交易流水号的艺术:掌握支付系统的业务ID生成指南
揭密支付安全:为什么你的交易无法被篡改
金融密语:揭秘支付系统的加解密艺术
支付系统日志设计完全指南:构建高效监控和问题排查体系的关键基石
避免重复扣款:分布式支付系统的幂等性原理与实践
支付系统的心脏:简洁而精妙的状态机设计与核心代码实现
精确掌控并发:固定时间窗口算法在分布式环境下并发流量控制的设计与实现
精确掌控并发:滑动时间窗口算法在分布式环境下并发流量控制的设计与实现文章来源地址https://www.toymoban.com/news/detail-804356.html

到了这里,关于精确掌控并发:令牌桶算法在分布式环境下并发流量控制的设计与实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 雪花算法,在分布式环境下实现高效的ID生成

    其实雪花算法比较简单,可能称不上什么算法,就是一种构造UID的方法。 点1:UID是一个long类型的41位时间戳,10位存储机器码,12位存储序列号。 点2:时间戳的单位是毫秒,可以同时链接1024台机器,每台机器每毫秒可以使用4096个序列号,我们会给生成id上一个同步锁,阻塞

    2024年02月15日
    浏览(55)
  • 分布式调用与高并发处理 Zookeeper分布式协调服务

    单机架构 一个系统业务量很小的时候所有的代码都放在一个项目中就好了,然后这个项目部署在一台服务器上,整个项目所有的服务都由这台服务器提供。 缺点: 服务性能存在瓶颈,用户增长的时候性能下降等。 不可伸缩性 代码量庞大,系统臃肿,牵一发动全身 单点故障

    2024年02月12日
    浏览(61)
  • Redis高并发分布式锁

    针对单机环境下的并发访问,可以通过锁机制(Syschronized或独占锁等)来进行控制,使得一个资源在一段时间内只能被一个线程访问;但在多服务器的分布式环境下,并发访问同一个资源,可能会导致被同时修改或更新,原因在于juc包下的并发控制机制,都是基于JVM层面的,而

    2024年02月02日
    浏览(80)
  • 论文-分布式-并发控制-并发控制问题的解决方案

    目录 参考文献 问题 解法与证明 易读版本 参考文献 Dijkstra于1965年发表文章Solution of a Problem in Concurrent Programming Control,引出并发系统下的互斥(mutual exclusion)问题,自此开辟了分布式计算领域 Dijkstra在文中给出了基于共享存储原子性访问的解决方案只有十多行代码,但阅读起来

    2024年02月08日
    浏览(44)
  • 服务端⾼并发分布式结构演进之路

    应⽤(Application)/系统(System) 为了完成一整套服务的一个程序或相互配合的程序群 模块(Module)/组件(Component) 当应⽤较复杂时,为了分离职责,将其中具有清晰职责的、内聚性强的部分,抽象出概念,便于理解 分布式(Distributed) 分布式(Distributed)是指将计算、任务

    2024年02月13日
    浏览(94)
  • 4、Redis高并发分布式锁实战

    在分布式系统中,保证数据的一致性和避免竞争条件是至关重要的。分布式锁是一种常用的机制,而Redis作为一款高性能的内存数据库,提供了简单而强大的分布式锁方案。本文将深入探讨如何利用Redis高并发分布式锁来解决分布式系统中的并发控制问题,并提供实战案例。

    2024年01月18日
    浏览(54)
  • 分布式和高并发的详细介绍

    分布式系统和高并发性能是现代计算领域中的两个关键概念。随着互联网和计算技术的迅速发展,越来越多的应用需要能够处理大规模的数据和用户并发。在本文中,我们将深入介绍分布式系统和高并发性能的概念、特点、挑战和应对方法。 分布式系统是由多个独立的计算机

    2024年02月14日
    浏览(47)
  • 简述JMeter实现分布式并发及操作

    为什么要分布式并发? JMeter性能实践过程中,一旦进行高并发操作时就会出现以下尴尬场景,JMeter客户端卡死、请求错误或是超时等,导致很难得出准确的性能测试结论。 目前知道的有两个方法可以解决JMeter支撑高并发: 一是将JMeter部署在Linux服务器上,可以支撑的并发量

    2024年02月16日
    浏览(41)
  • 分布式集群与多线程高并发

      后台数据的处理语言有很多,Java 是对前端采集的数据的一种比较常见的开发语言。互联网移动客户端的用户量特别大,大量的数据处理需求应运而生。可移动嵌入式设备的表现形式   很多,如 PC 端,手机移动端,智能手表,Google  眼镜等。Server2client 的互联网开发模式比

    2024年02月08日
    浏览(45)
  • 设计高并发分布式锁架构的实用指南

    在面对Java超大并发需求时,设计一个高效的分布式锁架构是至关重要的。本文将为您提供一套清晰明了、实践方便的设计指南,以确保系统在高并发场景下能够稳定可靠地运行。 首先,了解业务需求对分布式锁的具体要求至关重要。考虑到系统的高并发性质,通常需要满足

    2024年01月24日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包