RateLimiter –发现Google Guava

RateLimiter类最近被添加到Guava库中 (从13.0开始),它已经是我最喜欢的工具之一。 看看JavaDoc所说的:
让我们从一个简单的例子开始。 假设我们有一个长期运行的过程,需要将其进度广播给提供的侦听器:
def longRunning(listener: Listener) {
    var processed = 0
    for(item <- items) {
        //..do work...
        processed += 1
        listener.progressChanged(100.0 * processed / items.size)
    }
}

trait Listener {
    def progressChanged(percentProgress: Double)
}

请原谅我此Scala代码的命令式风格,但这不是重点。 一旦我们使用一些具体的侦听器启动应用程序,我想强调的问题就变得显而易见:

class ConsoleListener extends Listener {
    def progressChanged(percentProgress: Double) {
        println('Progress: ' + percentProgress)
    }
}

longRunning(new ConsoleListener)

想象一下, longRunning()方法处理数百万个items但是每次迭代只需要一秒钟的时间。 记录消息的数量简直是疯了,更不用说控制台输出比处理自身需要更多的时间。 您可能已经多次遇到这样的问题,并且有一个简单的解决方法:

if(processed % 100 == 0) {
    listener.progressChanged(100.0 * processed / items.size)
}

在那里,我修好了! 我们仅在第100次迭代中打印进度。 但是,这种方法有几个缺点:

  • 代码被无关逻辑污染
  • 无法保证每100次迭代就足够慢...
  • …还是太慢了?

我们真正想要实现的是限制进度更新的频率(例如:每秒两次)。 好,深入兔子洞:

def longRunning(listener: Listener) {
    var processed = 0
    var lastUpdateTimestamp = 0L
    for(item <- items) {
        //..do work...
        processed += 1
        if(System.currentTimeMillis() - lastUpdateTimestamp > 500) {
            listener.progressChanged(100.0 * processed / items.size)
            lastUpdateTimestamp = System.currentTimeMillis()
        }
    }
}

您是否还感到我们走错了方向? 女士们,先生们,我给你RateLimiter

var processed = 0
val limiter = RateLimiter.create(2)
for (item <- items) {
    //..do work...
    processed += 1
    if (limiter.tryAcquire()) {
        listener.progressChanged(100.0 * processed / items.size)
    }
}

越来越好? 如果API不清楚,我们首先创建一个RateLimiter ,每秒2个许可。 这意味着我们可以在一秒钟内获得最多两个许可证,如果我们试图做更经常tryAcquire()将返回false (或者线程将阻塞acquire()来代替1)。 因此,上面的代码保证了侦听器每秒不会被调用两次以上。
另外,如果您想完全摆脱业务逻辑中的无关节制代码,则可以使用装饰器模式进行救援。 首先让我们创建一个监听器,该监听器包装另一个(具体的)监听器,并仅以给定的速率委派给它:

class RateLimitedListener(target: Listener) extends Listener {

    val limiter = RateLimiter.create(2)

    def progressChanged(percentProgress: Double) {
        if (limiter.tryAcquire()) {
            target.progressChanged(percentProgress)
        }
    }
}

装饰器模式的最好之处在于,使用侦听的代码和具体的实现都不知道装饰器。 客户端代码也变得更加简单(本质上我们回到了原来的样子):

def longRunning(listener: Listener) {
    var processed = 0
    for (item <- items) {
        //..do work...
        processed += 1
        listener.progressChanged(100.0 * processed / items.size)
    }
}

longRunning(new RateLimitedListener(new ConsoleListener))

但是,我们只是在刮擦可以使用RateLimiter的地方! 假设我们要避免上述拒绝服务攻击或减慢我们API的自动客户端的速度。 使用RateLimiter和servlet过滤器非常简单:

@WebFilter(urlPatterns=Array('/*'))
class RateLimiterFilter extends Filter {

    val limiter = RateLimiter.create(100)

    def init(filterConfig: FilterConfig) {}

    def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        if(limiter.tryAcquire()) {
            chain.doFilter(request, response)
        } else {
            response.asInstanceOf[HttpServletResponse].sendError(SC_TOO_MANY_REQUESTS)
        }
    }

    def destroy() {}
}

另一个自我描述的样本。 这次我们限制我们的API每秒处理不超过100个请求(当然RateLimiter是线程安全的)。 通过我们的过滤器的所有HTTP请求均受速率限制。 如果我们无法处理传入的请求,则会发送HTTP 429 – Too Many Requests错误代码(在servlet规范中尚不可用)。 或者,您可能希望阻止客户一段时间,而不是急于拒绝它。 那也很简单:

def doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
    limiter.acquire()
    chain.doFilter(request, response)
}

limiter.acquire()将阻塞,直到需要保持每秒100个请求的限制为止。 另一个选择是使用tryAcquire()并设置超时(阻塞给定的时间)。 如果您要避免向客户端发送错误,则阻塞方法会更好。 但是,在高负载下,很容易想象几乎所有HTTP线程都在等待RateLimiter阻塞,最终导致servlet容器拒绝连接。 因此,只能部分避免客户流失。
该过滤器是构建更复杂解决方案的良好起点。 通过IP或用户名定义速率限制器的映射就是很好的例子。

我们尚未涵盖的范围是一次获得多个许可证。 事实证明, RateLimiter也可以用于例如限制网络带宽或正在发送/接收的数据量。 假设您创建了一个搜索servlet,并且希望每秒返回不超过1000个结果。 在每个请求中,用户决定每个响应要接收多少个结果:可以是500个请求,每个包含2个结果,或者1个巨大的请求一次请求1000个结果。 但是平均在一秒钟内不会有超过1000个结果。 用户可以随意使用自己的配额:

@WebFilter(urlPatterns = Array ('/search'))
class SearchServlet extends HttpServlet {

    val limiter = RateLimiter.create(1000)

    override def doGet(req: HttpServletRequest, resp: HttpServletResponse) {
        val resultsCount = req.getParameter('results').toInt
        limiter.acquire(resultsCount)
        //process and return results...
    }
}

默认情况下,我们每次调用acquire()一个许可。 非阻塞servlet将调用limiter.tryAcquire(resultsCount)并检查结果,到目前为止,您已经知道了。 如果您对网络流量的速率限制感兴趣,请不要忘记使用Servlet 3.0异步处理来查看服务器吞吐量的十倍增长 。 由于具有阻塞特性, RateLimiter不太适合用于通过节流编写可伸缩的上载/下载服务器。

我想与您分享的最后一个示例是限制客户端代码,以避免使我们正在与之通信的服务器过载。 想象一下一个批量导入/导出过程,该过程调用某些服务器数千次交换数据。 如果我们不限制客户端,并且服务器端没有速率限制,则服务器可能会过载并崩溃。 RateLimiter再次非常有用:

val limiter = RateLimiter.create(20)

def longRunning() {
    for (item <- items) {
        limiter.acquire()
        server.sync(item)
    }
}

该示例与第一个示例非常相似。 区别在于这次我们阻止而不是丢弃丢失的许可证。 多亏了阻止,对server.sync(item)外部调用不会使第三方服务器过载,每秒最多调用20次。 当然,如果您有多个与服务器交互的线程,它们可以共享相同的RateLimiter

总结:

  • RateLimiter允许您执行特定动作的频率不超过给定频率
  • 这是一个小型轻量级的类(不涉及线程!)您可以创建数千个速率限制器(每个客户端?)或在多个线程中共享一个
  • 我们没有介绍预热功能–如果RateLimiter长时间完全闲置,它将在配置的时间内逐渐将允许的频率增加到配置的最大值,而不是从一开始就允许最大频率

我有种感觉,我们很快会回到这堂课。 希望您在下一个项目中发现它有用!

1 –我正在使用Guava 14.0-SNAPSHOT。 如果在您阅读本文时尚未提供14.0稳定版,则必须使用更详细的tryAcquire(1, 0, TimeUnit.MICROSECONDS)代替tryAcquire()并使用acquire(1)代替acquire()

祝您编程愉快,别忘了分享!

参考: RateLimiter –Java和社区博客上从我们的JCG合作伙伴 Tomasz Nurkiewicz 发现Google Guava


翻译自: https://www.javacodegeeks.com/2012/10/ratelimiter-discovering-google-guava.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值