【问题标题】:Limiting call frequency to a static method将调用频率限制为静态方法
【发布时间】:2020-12-21 15:14:32
【问题描述】:

我们有一个从多个线程调用并访问外部数据库的方法。为了不减慢其他客户端的数据库速度,对该方法的调用应限制为 1 次/秒。

我喜欢保持简单,所以我这样做了:

private static final Object SYNC_LOCK = new Object();

public static double myMethod(int param1, ...) {
  synchronized(SYNC_LOCK) {
    //do something...
    Thread.sleep(1000);
    return result;
  }
}

现在,我们正在使用 sonarqube 进行代码分析,这种睡眠被视为“阻塞”错误。 通过查看代码,我可以排除死锁。实现一种基于令牌的方法对我来说似乎有点多。

您是否同意 sonarqube 需要更改此代码?

现在,我们可以使用例如线程池来实现如下所述。但对我来说,第一个例子似乎更圆滑。

private static ExecutorService es = Executors.newFixedThreadPool(1);
private static long lastCall = 0;

public static Double myMethod(int param1, ...) {
    Future<Double> f = es.submit(new Callable<Double>() {
        @Override
        public Double call() throws Exception {
            long diff = System.currentTimeMillis() - lastCall;
            if (diff < 1000) {
                long sleepMillis = 1000 - diff;
                Thread.sleep(sleepMillis);
            }
            //do something...
            lastCall = System.currentTimeMillis();
            return result;
        }
    });
    try {
        return f.get();
    } catch (InterruptedException | ExecutionException e) {
        //handle this
        return null;
    }
}

【问题讨论】:

  • 你让线程在方法中休眠?
  • 是的,因为强迫调用者等待是个坏主意。
  • 休眠在计算上并不昂贵,那么为什么该方法在工作完成时要休眠呢?同步可以防止多个线程同时调用该方法,因此这已经将其限制为单线程操作。这还不够吗?
  • Kayaman 是对的,请解释一下“1 call/second”的用途。 SonarQube 也是对的,Thread.sleep 在那里看起来像废话。由于 SonarQube 不是人类,我怀疑它是否可以“理解”原因,如果有的话。
  • 添加睡眠正是因为它的计算成本不高。关键是,对该方法的请求突发不会对应用程序的其他部分产生不良的性能影响。

标签: java concurrency synchronization


【解决方案1】:

我想这是一种有效的创可贴。但它会产生一个瓶颈,即一次只有一个请求可以取得进展。对于您的客户来说,他们必须预先支付时间罚金,而不是您检查自上次请求以来是否有足够的时间过去,这对您的客户来说也是粗暴的。

Sonarqube 是一个静态分析工具,它所能做的就是找到代码中的模式并将规则应用于它们。一般来说,不带锁睡觉的规则很有意义。 当一个线程持有锁时,其他线程显然被阻塞,并且当一个线程处于睡眠状态时它没有工作,所以它显然不是最佳的。在很多情况下,您会看到程序员添加睡眠是为了避免丢失通知和其他错误而绝望(且不明智)的尝试,我认为这就是 Sonarqube 试图标记的。

首先,由于访问外部数据库是一个痛点,并且您想减轻它的负载,因此请尝试尽可能多地缓存结果。

当您使用 ThreadPoolExecutor 时,您可以通过配置工作人员的数量、设置拒绝策略等来更好地控制工作速率。一旦缓存减少了外部数据库上的负载,您就需要多个请求有时,您可以调整工作线程的数量以提高吞吐量。

【讨论】:

  • 这对我来说绝对有意义。我想将实现更改为线程池确实比简单地删除 sonarqube 警告有更多好处。也许我们会走那条路。
  • @user3726374:是的,这种方式工作量更大,但可以让你走得更远。也可以查看 websockets。
【解决方案2】:

用其他方法提取业务逻辑(比如下面的myExpensiveMethod())然后考虑实现客户端Rate Limiter(我假设并发调用没有副作用)-

RateLimiterConfig config = RateLimiterConfig.custom()
  .limitForPeriod(1)
  .limitRefreshPeriod(Duration.ofSeconds(1))
  .timeoutDuration(Duration.ofSeconds(1))
  .build();

然后从myMethod() 致电myExpensiveMethod()

public static Double myMethod() {

  RateLimiterRegistry registry = RateLimiterRegistry.of(config);
  RateLimiter limiter = registry.rateLimiter("myMethod");
  Supplier<Double> dbQuerySupplier = 
       RateLimiter.decorateSupplier(limiter,
                  () -> myExpensiveMethod());
  return dbQuerySupplier.get();
}

【讨论】:

  • 为什么要求客户端更改而不在该方法中使用速率限制器?
  • 我的意思是,这似乎正是我想要做的。但是对于只想快速浏览的其他人来说肯定会很难理解。
  • @rascio '- 我确实提到在方法中使用速率限制器或者如果您可以在其他方法中提取业务逻辑(比如下面的 myExpensiveMethod())
  • @user3726374 - 我不知道为什么 - 对于只想快速浏览的其他人来说肯定很难理解
  • @Pankaj 也许我对我不知道的库有点偏见。可能这值得研究。
【解决方案3】:

IMO 最简单的方法是做这样的事情(请原谅伪库调用):

public static double myRealMethod() {
  synchronized(SYNC_LOCK) {
    //do something...
    Thread.sleep(1000);
    return result;
  }
}

private static double cachedResult;
private static Somekindoftimestamp cachedResultDate = A_LONG_TIME_AGO;
public static double myMethod() {
  synchronized(SYNC_LOCK) {
    if (cachedResultDate.isTooOldForMyLiking()) {
      cachedResult = myRealMethod();
    }
    return cachedResult;
  }
}

一个明显的缺点:某些对myMethod() 的调用会比其他大多数调用花费更长的时间。


一个可能的改进(取决于您的应用程序的需要):让myMethod()总是返回缓存的结果,并创建一个周期性的Timer 任务,每秒调用一次myRealMethod() 以更新是否需要缓存结果。

【讨论】:

  • 引入缓存并不是“我有一个计算量很大的方法,它每小时都会被调用一次(我正在同步块内试图解决它)”的正确答案。
  • @Kayaman,对不起,我错过了看到 OP 在哪里声明了排除缓存的任何要求。返回值显然必须依赖于某些东西,但是 OP 的示例没有给我们任何线索。同样,OP 也没有告诉我们什么取决于返回值。我个人遇到过许多情况,其中缓存绝对是正确的答案——它计算系统的全局状态的一些摘要,许多调用者对此感兴趣——我也遇到过其他根本不起作用的情况。我相信 OP 足够聪明,能够识别其中的差异。
  • P.S.,不要忘记这里的“答案”不仅仅针对 OP。 StackOverflow 不是解决问题的服务。如果我的回答不适用于 OP,那么它可能适用于其他人。
  • 如果我可以关闭并删除这个问题,因为不清楚。
  • 是的,抱歉,我这边的伪代码有点太多了。 myMethod 当然有与计算相关的参数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-20
相关资源
最近更新 更多