一、背景介绍
Redisson 做分布式限流的原理底层算法是令牌桶。核心实现是用 Lua 脚本操作 Redis,使用 hash key 来存储相关数据。确实可能会存在 Redis 单节点瓶颈问题。同时,本身是用 Java 实现的,需要操作 Jedis,因此一般只在 Java 环境下使用。
解决办法
后续如果限流的业务场景增多,可以自己编写代码,让限流的业务多创建几个 RRateLimiter
(可以理解为一个 limiter 对应一个 hash key),从而实现对 key 的打散,避免多个业务的 key 都存储到一个节点上,造成限流瓶颈问题。
二、使用方式
2.1 引入 Maven 依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.4</version>
</dependency>
Java 和 Jedis 的版本请选择合适的进行引用。
2.2 配置项
@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.3 业务场景下使用
@Autowired
@Qualifier("RRateLimiter")
private RRateLimiter RRateLimiter;
if (RRateLimiter.tryAcquire(1)) {
// 执行被限流的业务逻辑
} else {
// 被限流后的操作
}
三、原理
1、getRateLimiter
// 声明一个限流器,名称叫 key
redissonClient.getRateLimiter(key);
2、trySetRate
trySetRate
方法的底层实现如下:
@Override
public RFuture<Boolean> trySetRateAsync(RateType type, long rate, long rateInterval, RateIntervalUnit unit) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"redis.call('hsetnx', KEYS[1], 'rate', ARGV[1]);"
+ "redis.call('hsetnx', KEYS[1], 'interval', ARGV[2]);"
+ "return redis.call('hsetnx', KEYS[1], 'type', ARGV[3]);",
Collections.<Object>singletonList(getName()), rate, unit.toMillis(rateInterval), type.ordinal());
}
举个例子以便更容易理解:
例如下面这段代码,5 秒内产生 3 个令牌,并且所有实例共享(RateType.OVERALL
为所有实例共享,RateType.CLIENT
为单实例共享):
trySetRate(RateType.OVERALL, 3, 5, RateIntervalUnit.SECONDS);
那么 Redis 中将会设置 3 个参数:
hsetnx key rate 3
hsetnx key interval 5
hsetnx key type 0
3、接着看 tryAcquire(1) 方法
底层源码如下:
private <T> RFuture<T> tryAcquireAsync(RedisCommand<T> command, Long value) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"local rate = redis.call('hget', KEYS[1], 'rate');" //1
+ "local interval = redis.call('hget', KEYS[1], 'interval');" //2
+ "local type = redis.call('hget', KEYS[1], 'type');" //3
+ "assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized')" //4
+ "local valueName = KEYS[2];" //5
+ "if type == 1 then "
+ "valueName = KEYS[3];" //6
+ "end;"
+ "local currentValue = redis.call('get', valueName); " //7
+ "if currentValue ~= false then "
+ "if tonumber(currentValue) < tonumber(ARGV[1]) then " //8
+ "return redis.call('pttl', valueName); "
+ "else "
+ "redis.call('decrby', valueName, ARGV[1]); " //9
+ "return nil; "
+ "end; "
+ "else " //10
+ "redis.call('set', valueName, rate, 'px', interval); "
+ "redis.call('decrby', valueName, ARGV[1]); "
+ "return nil; "
+ "end;",
Arrays.<Object>asList(getName(), getValueName(), getClientValueName()),
value, commandExecutor.getConnectionManager().getId().toString());
}
你可能会想,既然是令牌桶,为什么没有初始化容量。在这段 Lua 脚本中其实就给出了答案。在步骤 5、6、7 中,如果发现容量没有设置,就会把 rate
设置为容量。
四、压测限流实际的表现
我用压测工具压测了几轮,发现qps始终不会超过我设置的速率。速率=RRateLimiterFre/RRateLimiterTime 还是很稳定的。