限制调用静态方法的频率

huangapple go评论72阅读模式
英文:

Limiting call frequency to a static method

问题

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

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

private static final Object SYNC_LOCK = new Object();

public static double myMethod(int param1, ...) {
  synchronized(SYNC_LOCK) {
    //做一些事情...
    Thread.sleep(1000);
    return result;
  }
}

现在,我们正在使用SonarQube进行代码分析,这个sleep被认为是一个"blocker"错误。
从代码的角度来看,我可以排除死锁的可能性。而且,实现一种基于令牌的方法对我来说似乎有点过于复杂。

你是否同意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);
            }
            //做一些事情...
            lastCall = System.currentTimeMillis();
            return result;
        }
    });
    try {
        return f.get();
    } catch (InterruptedException | ExecutionException e) {
        //处理这个异常
        return null;
    }
}
英文:

We have a method which is being called from multiple threads and accesses an external database. In order not to slow down the database for other clients, the calls to this method shall be limited to let's say 1call/second.

I like to keep things simple, so I just did this:

private static final Object SYNC_LOCK = new Object();

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

Now, we are using sonarqube for code analysis and this sleep is considered as a "blocker" bug.
From looking at the code, I can rule out deadlocks. And implementing a kind of token based approach seems a bit much to me.

Would you agree with sonarqube that this code needs to be changed?

Now, we could use for example a thread pool to achieve the same as written below. But the first example seems much more sleek to me.

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

public static Double myMethod(int param1, ...) {
    Future&lt;Double&gt; f = es.submit(new Callable&lt;Double&gt;() {
        @Override
        public Double call() throws Exception {
            long diff = System.currentTimeMillis() - lastCall;
            if (diff &lt; 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;
    }
}

答案1

得分: 1

我猜这就像是一种勉强能够工作的权宜之计。但它会在某一时刻创建一个瓶颈,只有一个请求能够同时取得进展。而且对于你的客户来说,他们不得不提前付出时间成本,而不是在你检查自上次请求以来是否已经过去足够的时间。

Sonarqube 是一个静态分析工具,它只能在代码中查找模式并对其应用规则。通常情况下,保持不在持有锁的情况下睡眠的原则是有很多道理的。
当一个线程持有锁时,其他线程显然会被阻塞,而当一个线程正在睡眠时,它就不在执行工作,因此这显然是不太理想的。在许多情况下,你会看到程序员添加睡眠,作为绝望(并且不明智)的尝试,以避免丢失通知和其他错误,我想这可能是 Sonarqube 正试图标记的内容。

首先,由于访问外部数据库是一个痛点,并且你想要减轻其负担,尽量将结果进行缓存。
当你使用 ThreadPoolExecutor 时,可以通过配置工作线程的数量、设置拒绝策略等来更好地控制工作的速率。一旦缓存降低了对外部数据库的负载,以至于你想要同时处理多个请求,你可以调整工作线程的数量以增加吞吐量。

英文:

I guess this is a band-aid that kind of works. But it creates a bottleneck where only one request can make progress at a time. It's also rough on your clients that they have to pay the time penalty up front, rather than your checking if enough time has gone by since the last request.

Sonarqube is a static analysis tool, all it can do is find patterns in code and apply rules to them. In general the rule of not sleeping with a lock held makes a lot of sense.
When a thread holds a lock obviously other threads are blocked, and when a thread is sleeping it's not doing work, so it is clearly not optimal. In a lot of cases you see programmers adding sleeps as desperate (and ill-advised) attempts to avoid lost-notifications and other bugs, and I think that's what Sonarqube is trying to flag.

First, since hitting the external database is a pain point and you want to relieve the load on it, try caching the results as much as possible.

When you use a ThreadPoolExecutor you get better control over the rate of work by configuring the number of workers, setting up a rejection policy, etc. Once caching reduces the load on the external database enough that you want more than one request at a time, you can tweak the number of worker threads to increase throughput.

答案2

得分: 0

将业务逻辑提取到另一个方法中(比如下面的 myExpensiveMethod() ),然后考虑实现客户端速率限制器(我假设并发调用没有副作用)-

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();
}
英文:

Extract business logic in some other method (say myExpensiveMethod() below) then think of implementing client side Rate Limiter (I'm assuming that there is no side effect of concurrent call) -

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

And from myMethod() call myExpensiveMethod()

public static Double myMethod() {

  RateLimiterRegistry registry = RateLimiterRegistry.of(config);
  RateLimiter limiter = registry.rateLimiter(&quot;myMethod&quot;);
  Supplier&lt;Double&gt; dbQuerySupplier = 
       RateLimiter.decorateSupplier(limiter,
                  () -&gt; myExpensiveMethod());
  return dbQuerySupplier.get();
}

答案3

得分: -1

简单的方法是像这样做(请忽略伪造的库调用):

public static double myRealMethod() {
  synchronized(SYNC_LOCK) {
    // 做一些操作...
    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() 来更新缓存的结果,无论是否需要。

英文:

IMO Simplest approach would be to do something like this (please excuse the pseudo-library calls):

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;
  }
}

One obvious down-side: Some calls to myMethod() would take a lot longer than most others.


One possible improvement (depending on your application's needs): Have myMethod() always return the cached result, and create a periodic Timer task that calls myRealMethod() once per second to update the cached result whether it's wanted or not.

huangapple
  • 本文由 发表于 2020年9月2日 20:40:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/63705738.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定