平时项目中做分布式限流可能会使用一些框架,如Sentinel,Hystrix等开源框架.今天介绍一种自己开发的分布式限流方案.使用Redis+Lua的方式.
平时研发中可能存在这种场景.调用外部API接口时由于外部接口存在限频,如阿里云短信发送短信接口每秒只允许调用100次,超过这个频次后请求被拒绝.对于这种场景不清楚Sentinel和Hystrix等是否可以使用.那么为了保证短信消息的不丢失,短信系统需要针对外部接口的限频做一些消息可靠性保证.本文已短信消息为原型进行介绍分布式限流的方案.
Redis+Lua+MQ实现限流的同时保证消息不丢失与及时发送
1.首先使用Redis的list存储消息的消息到来时刻的时间戳
2.redis+lua保证分布式系统中操作的原子性
3.消息队列(RocketMQ)的延迟性保证消息不丢失与及时发送
首先定义几个变量:
限频数量阀值:100
限频时间阀值:1min
即1min只允许发送100条消息
下面我们看看一条消息是如何发送出去的.
local function addToQueue(x,time)
local count=0
for i=1,x,1 do
redis.call('lpush',KEYS[1],time)
count=count+1
end
return count
end
local result=0
local timeBase = redis.call('lindex',KEYS[1], tonumber(ARGV[2])-tonumber(ARGV[1]))
if (timeBase == false) or (tonumber(ARGV[4]) - tonumber(timeBase)>tonumber(ARGV[3])) then
result=result+addToQueue(tonumber(ARGV[1]),tonumber(ARGV[4]))
end
if (timeBase~=false) then
redis.call('ltrim',KEYS[1],0,tonumber(ARGV[2])-1)
end
return result
如何在代码中使用lua脚本
public boolean acquirePromise(String redisKey,long count, long rateCount, long rateTime) {
validate(redisKey, count, rateCount, rateTime);
String luaScript = ScriptUtil.getScript("rateLimit.lua");
RedisScript redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Long currMillSecond = redisTemplate.execute((RedisCallback) redisConnection -> redisConnection.time());
Number result = redisTemplate.execute(redisScript, Collections.singletonList(redisKey), String.valueOf(count), String.valueOf(rateCount), String.valueOf(rateTime), String.valueOf(currMillSecond));
if(result != null && result.intValue() > 0) {
//放行
return true;
}
//限流
return false;
}
Tips:
1.如果使用RocketMQ的延迟Topic需要使用阿里云版本的RocketMQ,因为开源的RocketMQ不支持毫秒级别的延迟
2.redis 4.0 以后开始支持扩展模块,redis-cell是一个用rust语言编写的基于令牌桶算法的的限流模块,提供原子性的限流功能,并允许突发流量.