Java 中的限流模式:优化高需求应用程序中的资源使用
大约 5 分钟
又称
- 速率限制
限流设计模式的意图
限流模式,也称为速率限制,限制系统在给定时间段内可以处理的请求数量,以防止过载并确保稳定性。对于 Java 应用程序中的资源管理至关重要。
使用真实世界示例详细解释限流模式
真实世界示例
想象一个受欢迎的游乐园,它限制了每小时可以进入的游客数量,以防止过度拥挤。这确保了所有游客都能在没有长时间等待的情况下享受公园,并保持愉快的体验。同样,软件中的限流设计模式控制着对系统的请求速率,防止它不堪重负,并确保所有用户都能获得一致的性能。
通俗地说
限流模式用于对资源访问进行速率限制。
Microsoft 文档 说道
控制应用程序实例、单个租户或整个服务使用的资源消耗。这使系统即使在需求增加导致资源极度负荷时也能继续运行并满足服务级别协议。
Java 中限流模式的编程示例
在这个 Java 示例中,我们演示了限流。一个年轻的人类和一个年迈的矮人走进酒吧。他们开始向酒保点啤酒。酒保立刻发现年轻人不应该喝太多酒,速度太快,如果时间不够,就拒绝提供服务。对于年迈的矮人来说,服务速度可以更快。
BarCustomer
类代表了 Bartender
API 的客户端。CallsCount
跟踪每个 BarCustomer
的调用次数。
@Getter
public class BarCustomer {
private final String name;
private final int allowedCallsPerSecond;
public BarCustomer(String name, int allowedCallsPerSecond, CallsCount callsCount) {
if (allowedCallsPerSecond < 0) {
throw new InvalidParameterException("Number of calls less than 0 not allowed");
}
this.name = name;
this.allowedCallsPerSecond = allowedCallsPerSecond;
callsCount.addTenant(name);
}
}
@Slf4j
public final class CallsCount {
private final Map<String, AtomicLong> tenantCallsCount = new ConcurrentHashMap<>();
public void addTenant(String tenantName) {
tenantCallsCount.putIfAbsent(tenantName, new AtomicLong(0));
}
public void incrementCount(String tenantName) {
tenantCallsCount.get(tenantName).incrementAndGet();
}
public long getCount(String tenantName) {
return tenantCallsCount.get(tenantName).get();
}
public void reset() {
tenantCallsCount.replaceAll((k, v) -> new AtomicLong(0));
LOGGER.info("reset counters");
}
}
接下来,介绍了租户调用的服务。为了跟踪调用次数,使用了一个限流计时器。
public interface Throttler {
void start();
}
public class ThrottleTimerImpl implements Throttler {
private final int throttlePeriod;
private final CallsCount callsCount;
public ThrottleTimerImpl(int throttlePeriod, CallsCount callsCount) {
this.throttlePeriod = throttlePeriod;
this.callsCount = callsCount;
}
@Override
public void start() {
new Timer(true).schedule(new TimerTask() {
@Override
public void run() {
callsCount.reset();
}
}, 0, throttlePeriod);
}
}
Bartender
为 BarCustomer
提供 orderDrink
服务。顾客可能不知道啤酒供应速度是受他们外表限制的。
class Bartender {
private static final Logger LOGGER = LoggerFactory.getLogger(Bartender.class);
private final CallsCount callsCount;
public Bartender(Throttler timer, CallsCount callsCount) {
this.callsCount = callsCount;
timer.start();
}
public int orderDrink(BarCustomer barCustomer) {
var tenantName = barCustomer.getName();
var count = callsCount.getCount(tenantName);
if (count >= barCustomer.getAllowedCallsPerSecond()) {
LOGGER.error("I'm sorry {}, you've had enough for today!", tenantName);
return -1;
}
callsCount.incrementCount(tenantName);
LOGGER.debug("Serving beer to {} : [{} consumed] ", barCustomer.getName(), count + 1);
return getRandomCustomerId();
}
private int getRandomCustomerId() {
return ThreadLocalRandom.current().nextInt(1, 10000);
}
}
现在可以看到完整的示例的实际运行情况。BarCustomer
年轻人类的速率限制为每秒 2 次调用,而年迈的矮人则为每秒 4 次。
@Slf4j
public class App {
public static void main(String[] args) {
var callsCount = new CallsCount();
var human = new BarCustomer("young human", 2, callsCount);
var dwarf = new BarCustomer("dwarf soldier", 4, callsCount);
var executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> makeServiceCalls(human, callsCount));
executorService.execute(() -> makeServiceCalls(dwarf, callsCount));
executorService.shutdown();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
}
private static void makeServiceCalls(BarCustomer barCustomer, CallsCount callsCount) {
var timer = new ThrottleTimerImpl(1000, callsCount);
var service = new Bartender(timer, callsCount);
// Sleep is introduced to keep the output in check and easy to view and analyze the results.
IntStream.range(0, 50).forEach(i -> {
service.orderDrink(barCustomer);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
LOGGER.error("Thread interrupted: {}", e.getMessage());
}
});
}
}
示例控制台输出的摘录
18:46:36.218 [Timer-0] INFO com.iluwatar.throttling.CallsCount - reset counters
18:46:36.218 [Timer-1] INFO com.iluwatar.throttling.CallsCount - reset counters
18:46:36.242 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [1 consumed]
18:46:36.242 [pool-1-thread-1] DEBUG com.iluwatar.throttling.Bartender - Serving beer to young human : [1 consumed]
18:46:36.342 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [2 consumed]
18:46:36.342 [pool-1-thread-1] DEBUG com.iluwatar.throttling.Bartender - Serving beer to young human : [2 consumed]
18:46:36.443 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.443 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [3 consumed]
18:46:36.544 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.544 [pool-1-thread-2] DEBUG com.iluwatar.throttling.Bartender - Serving beer to dwarf soldier : [4 consumed]
18:46:36.645 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.645 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.745 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.745 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.846 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:36.846 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.947 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:36.947 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:37.048 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
18:46:37.048 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:37.148 [pool-1-thread-1] ERROR com.iluwatar.throttling.Bartender - I'm sorry young human, you've had enough for today!
18:46:37.148 [pool-1-thread-2] ERROR com.iluwatar.throttling.Bartender - I'm sorry dwarf soldier, you've had enough for today!
何时在 Java 中使用限流模式
- 您需要保护资源免受过多请求的淹没。
- 您希望确保多个用户之间公平使用服务。
- 您需要在高负载条件下保持服务质量。
Java 中限流模式的真实世界应用
- AWS、Google Cloud 和 Azure 等主要云提供商的 API 使用限流来管理资源使用。
- Web 服务,通过限制来自单个 IP 地址的请求数量,防止拒绝服务 (DoS) 攻击。
- 像社交媒体网站和电子商务网站这样的在线平台,确保服务器负载均匀分布。
限流模式的优缺点
优势
- 防止资源耗尽,确保系统稳定性。
- 有助于保持一致的性能和服务质量。
- 通过在高负载下避免系统崩溃,提高容错能力。
权衡
- 可能会导致请求处理延迟或延迟增加。
- 需要仔细调整,以平衡资源保护和用户体验。
- 如果配置不正确,可能会导致合法用户无法访问服务。
相关的 Java 设计模式
- 断路器:与限流协同工作,防止重复尝试访问过载服务。
- 舱壁:隔离系统的不同部分,以限制限流对其他组件的影响。