关于使用 resilience4j 实现基于访客 IP 的限流,网上已经有很多文章了,我这里就不再赘述。
本文主要是要解决一个问题:如果访客量很大,会占用太多的内存来存放限流对象。
假设一个系统每天有上百万的访客,而基于 IP 的限流策略会导致 resilience4j 中用来存放每个 IP 对应的限流对象的哈希表巨大无比。
下面是限流的策略:
//每分钟最多访问100次
RateLimiterConfig config = RateLimiterConfig.custom()
.limitRefreshPeriod(Duration.ofMinutes(1))
.limitForPeriod(100)
.timeoutDuration(Duration.ofMillis(0))
.build();
var registry = RateLimiterRegistry.of(config);
然后每来一个请求,就调用 registry.rateLimiter (<ip>) 方法做进一步限流,当这个 IP 量非常大时,所有这些信息都存在内存中,会导致内存占用膨胀太厉害。
所以我引入了 J2Cache 来做 rateLimiter 的缓存,内存中设置固定的数量,确保不会把所有的对象都塞到内存中。
写了一个新的类 CacheRateLimiterRegistry,直接从 InMemoryRateLimiterRegistry 继承而来:
package com.xxx.security;
import com.xxx.cache.CacheMgr;
import io.github.resilience4j.core.ConfigurationNotFoundException;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import io.github.resilience4j.ratelimiter.internal.AtomicRateLimiter;
import io.github.resilience4j.ratelimiter.internal.InMemoryRateLimiterRegistry;
import io.vavr.collection.Map;
import java.util.Objects;
import java.util.function.Supplier;
/**
* 使用缓存来管理 RateLimiter 实例,解决的问题:
* 1. 分布式环境下的使用(暂时还无法共享状态)
* 2. 避免内存过载
*/
public class CacheRateLimiterRegistry extends InMemoryRateLimiterRegistry {
private final static String REGION = "rateLimiter";
private String catalog;
public CacheRateLimiterRegistry(String catalog, RateLimiterConfig defaultConfig) {
super(defaultConfig);
this.catalog = catalog;
}
@Override
public RateLimiter rateLimiter(String name, RateLimiterConfig rateLimiterConfig, Map<String, String> tags) {
String key = catalog + name;
return CacheMgr.get(REGION, key, () -> new AtomicRateLimiter(name,
Objects.requireNonNull(rateLimiterConfig, CONFIG_MUST_NOT_BE_NULL), getAllTags(tags)));
}
@Override
public RateLimiter rateLimiter(String name, Supplier<RateLimiterConfig> rateLimiterConfigSupplier, Map<String, String> tags) {
String key = catalog + name;
return CacheMgr.get(REGION, key, () -> new AtomicRateLimiter(name, Objects.requireNonNull(
Objects.requireNonNull(rateLimiterConfigSupplier, SUPPLIER_MUST_NOT_BE_NULL).get(),
CONFIG_MUST_NOT_BE_NULL), getAllTags(tags)));
}
@Override
public RateLimiter rateLimiter(String name, String configName, Map<String, String> tags) {
String key = catalog + name;
return CacheMgr.get(REGION, key, () -> RateLimiter.of(name, getConfiguration(configName)
.orElseThrow(() -> new ConfigurationNotFoundException(configName)), getAllTags(tags)));
}
}
使用方法如下:
var registry = new CacheRateLimiterRegistry("prefix", config);
上述的 prefix 主要是为不同的限流场景提供一个前缀标识而已。
CacheMgr 类很简单,只是对 J2Cache 做一个简单封装:
package com.xxx.cache;
import net.oschina.j2cache.CacheChannel;
import net.oschina.j2cache.CacheException;
import net.oschina.j2cache.J2CacheBuilder;
import net.oschina.j2cache.J2CacheConfig;
import java.io.IOException;
import java.util.function.Supplier;
/**
* Cache manager
*/
public class CacheMgr {
private static final String CONFIG_FILE = "/cache.properties";
private final static J2CacheBuilder builder;
private final static CacheChannel cache;
static {
try {
J2CacheConfig config = J2CacheConfig.initFromConfig(CONFIG_FILE);
builder = J2CacheBuilder.init(config);
cache = builder.getChannel();
} catch (IOException e) {
throw new CacheException("Failed to load j2chache config file" + CONFIG_FILE, e);
}
}
public static <T> T get(String region, String key, Supplier<T> loader) {
return (T) cache.get(region, key, k -> loader.get(), true).getValue();
}
public static boolean exists(String region, String key) {
return cache.exists(region, key);
}
public static void set(String region, String key, Object value) {
cache.set(region, key, value);
}
public static void evict(String region, String... keys) {
cache.evict(region, keys);
}
public static void close() {
cache.close();
}
}
完事在 j2cache 的一级缓存定义中增加一个 rateLimiter 的 region 即可,想要设置内存中的对象数量就在这个文件中配置即可。