redis|redis分布式限流,计数器,令牌桶

分布式限流方案 计数:简单,双倍临界情况
漏桶:恒定速度,不能应对峰值
令牌桶:允许一定突然,丢掉部分请求有待商榷,令牌桶普遍用得多一些
成熟方案可见,阿里Sentinel:https://sentinelguard.io/zh-cn/docs/basic-implementation.html
计数实现:
原理:没超出显示进行自增

local key = "rate.limit:" .. KEYS[1] local limit = tonumber(ARGV[1]) local expire_time = ARGV[2]local is_exists = redis.call("EXISTS", key) if is_exists == 1 then if redis.call("INCR", key) > limit then return 0 else return 1 end else redis.call("SET", key, 1) redis.call("EXPIRE", key, expire_time) return 1 end

令牌桶方案实现
方案一、在提供给业务方的Controller层进行控制。
1、使用guava提供工具库里的RateLimiter类(内部采用令牌捅算法实现)进行限流
2、使用Java自带delayqueue的延迟队列实现(编码过程相对麻烦,此处省略代码)
3、使用Redis实现,存储两个key,一个用于计时,一个用于计数。请求每调用一次,计数器增加1,若在计时器时间内计数器未超过阈值,则可以处理任务
方案二、在短信发送至服务商时做限流处理
方案三、同时使用方案一和方案二
原文链接:https://blog.csdn.net/sunlihuo/article/details/79700225
令牌桶原理 用map保存最大值,当前值和最后修改时间,利用每次查询时,先加token,在减去需要的token数,满则暂停放入,能获取就减去获取值,够则直接返回
流程 具体实现放lua,用java类构造key和相关参数,工具类识别集群还是非集群进行相关调用
脚本实现如下
redis/ratelimit.lua:
if KEYS[1] == nil then return -1 endif(redis.pcall("EXISTS",KEYS[1])==0) then --第一次访问,初始化 redis.pcall("HMSET",KEYS[1], "last_mill_second",ARGV[1], "curr_permits",ARGV[4], "max_burst",ARGV[3], "rate",ARGV[4], "app",ARGV[5]) end local ratelimit_info=redis.pcall("HMGET",KEYS[1],"last_mill_second","curr_permits","max_burst","rate","app") local last_mill_second=ratelimit_info[1] local curr_permits=tonumber(ratelimit_info[2]) local max_burst=tonumber(ratelimit_info[3]) local rate=tonumber(ratelimit_info[4]) local app=tostring(ratelimit_info[5])local local_curr_permits=max_burst; if(type(last_mill_second) ~='boolean' and last_mill_second ~=nil) then --计算可以加入的最大令牌 local reverse_permits=math.floor((ARGV[1]-last_mill_second)/1000)*rate if(reverse_permits>0) then --如果可以加入,则把当前时间作为最后加入令牌时间 redis.pcall("HMSET",KEYS[1],"last_mill_second",ARGV[1]) end --计算加入令牌后最大值 --防止节点转发出现时间早的后出现!!! reverse_permits=math.max(reverse_permits,0); local expect_curr_permits=reverse_permits+curr_permits --取最大容量和加入令牌后最大值的最小值作为当前容量 local_curr_permits=math.min(expect_curr_permits,max_burst); else redis.pcall("HMSET",KEYS[1],"last_mill_second",ARGV[1]) endlocal result=-1 if(local_curr_permits-ARGV[2]>0) then result=1 --如果可以获取令牌,则去掉此时需要拿走的令牌 redis.pcall("HMSET",KEYS[1],"curr_permits",local_curr_permits-ARGV[2]) else --如果不行,则把当前最新的令牌数写入内存 redis.pcall("HMSET",KEYS[1],"curr_permits",local_curr_permits) endreturn result


调用方法如下:
boolean getToken=redisUtil.limit(Common.SYSTEM_CODE,"1","20","2",Common.SYSTEM_CODE);

工具类实现如下:
@Autowired private StringRedisTemplate redisTemplate; @Qualifier("ratelimitLua") @Resource RedisScript ratelimitLua; @Qualifier("ratelimitInitLua") @Resource RedisScript ratelimitInitLua; /** * * @param key * @param argus * last_mill_second 最后时间毫秒 * curr_permits 当前可用的令牌 * max_burst 令牌桶最大值 * rate 每秒生成几个令牌 * app 应用 * @return */ public boolean limit(String key,String... argus ) { if (argus==null||argus.length<4){ logger.error("参数不合法:{}",argus); returnfalse; } //统一时间 Long currMillSecond = redisTemplate.execute( (RedisCallback) redisConnection -> redisConnection.time() ); //Long result = redisTemplate.execute(ratelimitLua, //Collections.singletonList(getKey(key)), currMillSecond.toString(), argus[0],argus[1],argus[2],argus[3]); List argusList=new ArrayList<>(); argusList.add(currMillSecond.toString()); argusList.add(argus[0]); argusList.add(argus[1]); argusList.add(argus[2]); argusList.add(argus[3]); logger.info("list:"+argusList.toString()); Long result =redisTemplate.execute((RedisConnection connection)-> { Object nativeConnection = connection.getNativeConnection(); // 集群模式和单点模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行 // 集群 if (nativeConnection instanceof JedisCluster) { return (Long) ((JedisCluster) nativeConnection).eval(ratelimitLua.getScriptAsString(),Collections.singletonList(getKey(key)) ,argusList); }// 单点 else if (nativeConnection instanceof Jedis) { return (Long) ((Jedis) nativeConnection).eval(ratelimitLua.getScriptAsString(),Collections.singletonList(getKey(key)) , argusList); } return null; }); logger.info("result:--{}",result); if (result == 0) { returnlimit(key, argus); } if (result == 1) { returntrue; } return false; }

bean注入:
@Configuration public class RedisConfig { @Bean("ratelimitLua") public DefaultRedisScript getRedisScript() { DefaultRedisScript redisScript = new DefaultRedisScript(); redisScript.setLocation(new ClassPathResource("redis/ratelimit.lua")); redisScript.setResultType(java.lang.Long.class); return redisScript; } @Bean("ratelimitInitLua") public DefaultRedisScript getInitRedisScript() { DefaultRedisScript redisScript = new DefaultRedisScript(); redisScript.setLocation(new ClassPathResource("redis/ratelimitInit.lua")); redisScript.setResultType(java.lang.Long.class); return redisScript; } }

lua:优势 使用Lua脚本的好处
1、减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延和请求次数。
2、原子性的操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
【redis|redis分布式限流,计数器,令牌桶】3、代码复用:客户端发送的脚步会永久存在redis中,这样,其他客户端可以复用这一脚本来完成相同的逻辑。
4、速度快:见 与其它语言的性能比较, 还有一个 JIT编译器可以显著地提高多数任务的性能; 对于那些仍然对性能不满意的人, 可以把关键部分使用C实现, 然后与其集成, 这样还可以享受其它方面的好处。
5、可以移植:只要是有ANSI C 编译器的平台都可以编译,你可以看到它可以在几乎所有的平台上运行:从 Windows 到Linux,同样Mac平台也没问题, 再到移动平台、游戏主机,甚至浏览器也可以完美使用 (翻译成JavaScript).6、源码小巧:20000行C代码,可以编译进182K的可执行文件,加载快,运行快。

    推荐阅读