spring boot:用redis+lua实现基于ip地址的分布式流量限制(限流/简单计数器算法)(spring boot 2.2.0)
一,限流有哪些环节?
1,为什么要限流?
目的:通过对并发请求进行限速或者一个时间单位内的的请求进行限速,目的是保护系统可正常提供服务,避免被压力太大无法响应服务.
如果达到限制速率则可以采取预定的处理:
例如:
拒绝服务(定向到错误页面或返回错误提示信息)
排队或等待(秒杀/评论/下单)
降级(只返回兜底数据或默认数据)
2,需要应用限流的环节
防火墙:firewalld/iptables层的限流,针对每台机器
接入层:nginx的limit_req模块,对每单位时间的平均速率限流,针对某个站点或某个url
应用层:可以针对某个url或某个方法
说明:刘宏缔的架构森林是一个专注架构的博客,
网站:https://blog.imgtouch.com
本文: https://blog.imgtouch.com/index.php/2023/05/22/springboot-yong-redislua-shi-xian-ji-yu-ip-di-zhi-de-fen-bu-shi-liu-liang-xian-zhi-xian-liu-jian-dan/
对应的源码可以访问这里获取: https://github.com/liuhongdi/
说明:作者:刘宏缔 邮箱: 371125307@qq.com
二,演示项目的说明
1,项目的原理:
如果仅仅是单机上对某个接口做限流,
可以直接使用google的guava包中的流量限制功能,
但如果是有多台机器上统一做限流,
则需要借助redis的功能
当后端接收到请求时,会把限流的方法名和ip做为key值保存到redis,
每次接收到请求,对key值加1,
当请求数量在指定时间内超过了限制数量,
则返回'访问过于频繁'的提示信息
2,项目在github上的地址:
https://github.com/liuhongdi/ratelimiter
3,项目的目录结构:
三,lua代码的说明:
ratelimit.lua
local key = KEYS[1] local limit = tonumber(KEYS[2]) local length = tonumber(KEYS[3]) --redis.log(redis.LOG_NOTICE,' length: '..length) local current = redis.call('GET', key) if current == false then --redis.log(redis.LOG_NOTICE,key..' is nil ') redis.call('SET', key,1) redis.call('EXPIRE',key,length) --redis.log(redis.LOG_NOTICE,' set expire end') return '1' else --redis.log(redis.LOG_NOTICE,key..' value: '..current) local num_current = tonumber(current) if num_current+1 > limit then return '0' else redis.call('INCRBY',key,1) return '1' end end
说明:
key:在redis中记录访问次数的index,在这里我们用method的名字加ip地址进行限制
limit: 单ip对此method最多可以访问的次数
length: 限制次数生效的时长
原理:
key不存在时,新建一个key,value设置为1,并设定过期时间
如果key存在,看是否超过单位时间内允许访问的最高次数,
如果超过,返回0,
如果不超过,返回1
说明:为什么使用lua脚本?
redis上的lua脚本的执行是原子性的,不存在多个线程的并发问题,
使用lua脚本能保证高并发时也不会出现超出流量限制
四,java代码的说明:
1,RedisLuaUtil.java
@Service public class RedisLuaUtil { @Resource private StringRedisTemplate stringRedisTemplate; private static final Logger logger = LogManager.getLogger("bussniesslog"); /* run a lua script luaFileName: lua file name,no path keyList: list for redis key return 0: fail 1: success */ public String runLuaScript(String luaFileName,List<String> keyList) { DefaultRedisScript<String> redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/"+luaFileName))); redisScript.setResultType(String.class); String result = ""; String argsone = "none"; try { result = stringRedisTemplate.execute(redisScript, keyList,argsone); } catch (Exception e) { logger.error("发生异常",e); } return result; } }
说明:
DefaultRedisScript:负责封装lua脚本
luaFileName: lua文件名
keyList: redis中的key列表,我们把参数放在这里面传递
stringRedisTemplate:负责执行脚本
argsone:值参数,我们传一个空字串即可
2,RedisRateLimiterAspect,它负责调用RedisLuaUtil类,执行lua脚本:
/* * check is reach limit in time * run by lua * */ private boolean checkByRedis(RedisRateLimiter limit, String key) { List<String> keyList = new ArrayList(); keyList.add(key); keyList.add(String.valueOf(limit.count())); keyList.add(String.valueOf(limit.time())); String res = redisLuaUtil.runLuaScript("ratelimit.lua",keyList); System.out.println("------------------lua res:"+res); if (res.equals("1")) { return true; } else { return false; } }
说明:
keyList中我们添加了三个变量:
key: 在redis中记录访问次数的index,在这里我们用method的名字加ip地址进行限制
count: 同一个ip限制访问的次数
time: 限制访问的时间段
3,其他java代码的说明:
RedisRateLimiter:定义了一个注解
RedisRateLimiterAspect:AOP的切面程序,使限流不侵入业务代码
RateController: 控制器
在spring框架中使用AOP或Interceptor可以使通用的一些功能例如安全、检验等不影响业务代码,
我们在这个例子中使用的是AOP,也可以选择Interceptor,这里仅供参考
五,测试限流的效果:
1,查看controller中定义的值:
@RestController @RequestMapping("/rate") public class RateController { @RequestMapping("/redislimit") @RedisRateLimiter(count = 3, time = 1) public Object redisLimit() { return ServerResponseUtil.success(); } }
可以看到流量限制的值为:对redisLimit方法,同一个ip在1秒钟内最多可访问3次
2,用ab测试并发情况下的流量限制是否生效?
#-c:指定请求的并发数量
#-n:指定请求的总数量
[liuhongdi@localhost ~]$ ab -c 20 -n 20 http://127.0.0.1:8080/rate/redislimit
查看代码运行中打印出的数据:
------------------lua res:1 ------------------lua res:1 ------------------lua res:1 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0 ------------------lua res:0
可见在20个并发中,只有3个是生效的,允许正常访问,其他的超出了访问的数量限制
六,查看spring boot的版本
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.2.0.RELEASE)