一步一步实现若依框架--2.2实现后台限流rate_limiter
在项目中使用到了若依,想从头实现一下。思路就是把项目中涉及到的知识内容单独拎出来理解和做测试,然后再合到系统里去,重点的地方会将涉及到的知识进行总结和扩展。顺序是由后端到前端。 代码地址:https://github.com/hunji/RYMirror.代码中有打tag,跟着步骤来的,可以边看程序边看总结。本篇2.2是实现后台限流。
原理:想要实现的效果就是在给定时间内如果对某个请求的次数超出了一个阈值,返回报错不允许进行处理。使用了在redis中设置过期时间,根据key来记录访问次数,使用AOP进行请求前的统一处理
其实AOP的使用比较简单,重要的是redis的使用。
涉及到的redis中的典型应用场景:计数器相关问题.redis由于incrby命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等.限时业务的运用:redis中可以使用expire命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景.
1)redisConfig
springboot 使用redis。在若依中,系统的缓存使用的是redis,使用注解@EnableCaching进行了指定。如果不进行制定,默认的缓存是SimpleCacheConfiguration,其实就是一个存在内存中的键值对。redis配置如下:
@Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){ RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); /** * 这里的序列化参考了jeecgboot的实现方式 */ Jackson2JsonRedisSerializer<Object> serializer = jacksonSerializer(); // 使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); // Hash的key也采用StringRedisSerializer的序列化方式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } private Jackson2JsonRedisSerializer jacksonSerializer() { Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); return jackson2JsonRedisSerializer; } }
这里的json序列化工具没有用若依采用的FastJson2JsonRedisSerializer,参考jeecgboot使用了Jackson2JsonRedisSerializer,这样配置简单点,而且没有发现有什么问题。
2)lua脚本
Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。在redis中使用lua脚本的好处:
-
减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
-
原子操作。Redis会将整个脚本作为一个整体执行,中间不会被其他请求插入。因此在脚本运行过程中无需担心会出现竞态条件,无需使用事务。
-
复用。客户端发送的脚本会永久存在redis中,这样其他客户端可以复用这一脚本,而不需要使用代码完成相同的逻辑。
3)注解 + AOP
RateLimiter注解参数包括key\time\count\type。其中type如果是ip,表示会区分访问的客户端,在redis的key中拼接ip进行区别,默认的不区分ip,对所有的请求进行计数后限制。RateLimiterAspect切面类就是在redis中执行完逻辑后进行判断,如果超限了抛出异常,不允许访问。
4)全局异常处理
通用的写法,定义不通过类型的异常,和全局异常处理器GlobalExceptionHandler。
5) 报错 Factory method 'redisConnectionFactory' threw exception; nested exception is java.lang.NoClassDefFoundError: org/apache/commons/pool2/impl/GenericObjectPoolConfig。需要在common中添加依赖
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
6)redis的lua脚本单独出文件进行加载。若依中是写的字符串,独立出文件好一点。
local key = KEYS[1]
local time = tonumber(ARGV[1])
local count = tonumber(ARGV[2])
local current = redis.call('get',key)
if current and tonumber(current)>count then
return tonumber(current)
end
current = redis.call('incr', key)
if tonumber(current)==1 then
redis.call('expire',key,time)
end
return tonumber(current)
@Bean public DefaultRedisScript<Long> limitScript(){ DefaultRedisScript<Long> script = new DefaultRedisScript<>(); script.setResultType(Long.class); script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/limit.lua"))); return script; }
7)测试。测试controller
@RestController public class HelloController { @GetMapping("/hello") /** * 限流,10 秒之内,这个接口可以访问 3 次 */ @RateLimiter(time = 10000, count = 3,limitType = LimiType.IP) public String hello() { return "hello"; } }
测试结果和redis中数据:
代码地址:
https://github.com/hunji/RYMirror/releases/tag/2.2%E9%99%90%E6%B5%81
还可以使用Guava库的RateLimiter,直接是个工具类进行调用。