一、背景
线上有一个接口需要限流10W的QPS,这个时候如果再使用Redisson的RRateLimiter做限流,就会造成热key问题,会压垮redis。
二、解决方案
方案一:通过请求参数取模打散创建多个的RRateLimiter
暂时不使用这种方案。
方案二:每次从Redis取多个令牌到本地机器
每次从Redis取多个令牌到本地机器,本地机器的令牌消耗完了再用RRateLimiter向Redis取令牌。
方案三:方案一+方案二的一个组合
2.1 引入maven依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.4</version>
</dependency>
2.2 实现代码
2.2.1 本地限流器
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.LockSupport;
import org.redisson.api.RRateLimiter;
public class CustomRateLimiter {
private final RRateLimiter rateLimiter;
// 用原子变量来缓存令牌数量
private final AtomicLong tokens;
// 每次从 Redis 获取的令牌数量
private final long batchSize;
private final AtomicBoolean replenishing = new AtomicBoolean(false);
private final long maxRetries;
public CustomRateLimiter(RRateLimiter rateLimiter, long batchSize, long maxRetries) {
this.rateLimiter = rateLimiter;
this.batchSize = batchSize;
// 初始化令牌数量
this.tokens = new AtomicLong(0);
this.maxRetries = maxRetries;
}
public boolean tryAcquire() {
for (int retries = 0; retries < maxRetries; retries++) {
long current = tokens.get();
if (current > 0) {
if (tokens.compareAndSet(current, current - 1)) {
return true;
}
continue; // CAS失败,重试循环
}
// 本地令牌不足,尝试补充
if (replenishing.compareAndSet(false, true)) {
try {
// 双重检查,避免其他线程已补充
if (tokens.get() > 0) {
// 其他线程已补充,重试消耗
continue;
}
// 从Redis获取batchSize
if (rateLimiter.tryAcquire(batchSize)) {
// 填充完整批次, 当前线程立即消耗一个
tokens.addAndGet(batchSize - 1);
return true;
} else {
// Redis令牌不足
return false;
}
} finally {
replenishing.set(false);
}
} else {
// 已经有一个线程去找redis获取batchSize个令牌,其他的线程不排队,也直接去找redis获取令牌
return rateLimiter.tryAcquire(1);
}
}
return rateLimiter.tryAcquire(1);
}
}
2.2.2 用bean管理本地限流器
@Service
public class RateLimiterConfiguration {
@Autowired
@Qualifier("RRateLimiter")
private RRateLimiter myRRateLimiter;
@Bean(name = "myCustomRateLimiter")
public CustomRateLimiter getMyRateLimiter() {
return new CustomRateLimiter(myRRateLimiter, 10);
}
}
2.2.3 RRateLimiter配置项
@Configuration
public class RedissonConfiguration {
@Bean(name = "rateLimiterRedissonClient")
public RedissonClient getRateLimiterRedissonClient() {
Config config = new Config();
String clusterNodes = "redis集群节点信息(127.0.0.1:6379)";
int connectionMinIdleSize = 10; // 连接池最小空闲线程数
int connectionMaxSize = 100; // 连接池最大连接数
if (StringUtils.isBlank(clusterNodes)) {
throw new RuntimeException("Redisson 初始化失败, 没有配置集群地址");
}
String[] nodes = clusterNodes.split(",");
List<String> newNodes = new ArrayList<>(nodes.length);
Arrays.stream(nodes)
.forEach((index) -> newNodes.add(index.startsWith("redis://") ? index : "redis://" + index));
config.useClusterServers()
.addNodeAddress(newNodes.toArray(new String[0]))
.setMasterConnectionMinimumIdleSize(connectionMinIdleSize)
.setSlaveConnectionMinimumIdleSize(connectionMinIdleSize)
.setMasterConnectionPoolSize(connectionMaxSize)
.setSlaveConnectionPoolSize(connectionMaxSize);
config.setTransportMode(TransportMode.NIO);
return Redisson.create(config);
}
}
@Configuration
public class RRateLimiterConfiguration {
@Autowired
@Qualifier("rateLimiterRedissonClient")
private RedissonClient rateLimiterRedissonClient;
@Bean(name = "RRateLimiter")
public RRateLimiter getUserTimeGetCoinRRateLimiter() {
String RRateLimiterName = "限流器名字";
RRateLimiter RRateLimiter = rateLimiterRedissonClient.getRateLimiter(RRateLimiterName);
long RRateLimiterFre = 3; // 限流器令牌桶最大大小 也是N秒内发放的令牌桶数量
long RRateLimiterTime = 5; // N 秒内限制通过令牌的数量的N(时间参数)
RRateLimiter.trySetRate(RateType.OVERALL,
RRateLimiterFre, RRateLimiterTime, RateIntervalUnit.SECONDS);
return RRateLimiter;
}
}
2.2.4 使用
@Autowired
@Qualifier("myCustomRateLimiter")
private CustomRateLimiter myCustomRateLimiter;
if (myCustomRateLimiter.tryAcquire()) {
// 执行被限流的业务逻辑
} else {
// 被限流后的操作
}
🚀 如果你觉得这篇文章对你有帮助,不妨看看我整理的系列文章合集:
每周更新,欢迎关注+收藏!