用Redisson的RRateLimiter做限流【2】——大QPS限流策略

62 阅读2分钟

一、背景

线上有一个接口需要限流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 {
    // 被限流后的操作
}

🚀 如果你觉得这篇文章对你有帮助,不妨看看我整理的系列文章合集:

👉 大厂后端晋升全攻略(持续更新中)

每周更新,欢迎关注+收藏!