参考资料
https://dwz.cn/Gvviwswi
https://dwz.cn/pO9mWjhq
采用了redis来作为限流器的实现
但是我们也知道,限流器在每次请求令牌和放入令牌操作中,存在一个协同的问题,即获取令牌操作要尽可能保证原子性,否则无法保证限流器是否能正常工作。在RateLimiter的实现中使用了mutex作为互斥锁来保证操作的原子性,那么在redis中就需要一个类似于事务的机制来保证获取令牌中多重操作的原子性。 面对这样的需求,我们有几个选择:
经过权衡,我采用了第四种方式,通过redis和lua来编写令牌桶算法来完成分布式限流的需求。
本项目基于SpringBoot 2.1.5,使用到 Redis + Lua 限流脚本
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.5.RELEASE</version> <relativePath/> </parent> <groupId>com.xd</groupId> <artifactId>redis-lua-limit</artifactId> <version>0.0.1-SNAPSHOT</version> <name>redis-lua-limit</name> <description>基于Spring Boot Redis+Lua高并发限流</description>
<properties> <java.version>1.8</java.version> </properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.8</version> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
spring.application.name=spring-boot-redis-lua-limit
# Redis数据库索引 默认为0spring.redis.database=0# Redis地址spring.redis.host=localhost# Redis端口 默认6379spring.redis.port=6379# Redis服务器连接密码(默认为空)spring.redis.password=# 连接池最大连接数(使用负值表示没有限制)spring.redis.jedis.pool.max-active=8# 连接池最大阻塞等待时间(使用负值表示没有限制)spring.redis.jedis.pool.max-wait=-1# 连接池中的最大空闲连接spring.redis.jedis.pool.max-idle=8# 连接池中的最小空闲连接spring.redis.jedis.pool.min-idle=0# 连接超时时间(毫秒)spring.redis.timeout=10000
--Lua脚本
--- 限流KEY资源唯一标识local key = "rate.limit:" .. KEYS[1]--- 时间窗最大并发数local limit = tonumber(ARGV[1])--- 时间窗内当前并发数local current = tonumber(redis.call('get', key) or "0")--如果超出限流大小if current + 1 > limit then return 0else --请求数+1,并设置2秒过期 redis.call("INCRBY", key,"1") redis.call("expire", key,"2") return current + 1end
--IP限流Lua脚本
--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
1、我们通过KEYS[1] 获取传入的key参数 2、通过ARGV[1]获取传入的limit参数 3、redis.call方法,从缓存中get和key相关的值,如果为nil那么就返回0 4、接着判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0 5、如果未超过,那么该key的缓存值+1,并设置过期时间为1秒钟以后,并返回缓存值+1
自定义注解的目的,是在需要限流的方法上使用
package com.xd.redislualimit.annotation;
import java.lang.annotation.*;
/** * @Author 李号东 * @Description 限流注解 * @Date 17:49 2019-05-25 **/@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface RateLimit {
/** * 限流唯一标示 * * @return */ String key() default "";
/** * 限流时间 * * @return */ int time();
/** * 限流次数 * * @return */ int count();}
package com.xd.redislualimit.config;
import org.springframework.context.annotation.Bean;import org.springframework.core.io.ClassPathResource;import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;import org.springframework.scripting.support.ResourceScriptSource;import org.springframework.stereotype.Component;
import java.io.Serializable;
/** * @Classname commons * @Description 配置 * @Author 李号东 lihaodongmail@163.com * @Date 2019-05-25 20:13 * @Version 1.0 */@Componentpublic class commons {
/** * 读取限流脚本 * * @return */ @Bean public DefaultRedisScript<Number> redisluaScript() { DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>(); redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redisLimit.lua"))); //返回类型 redisScript.setResultType(Number.class); return redisScript; }
/** * RedisTemplate * * @return */ @Bean public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) { RedisTemplate<String, Serializable> template = new RedisTemplate<>(); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setConnectionFactory(redisConnectionFactory); return template; }}
package com.xd.redislualimit.config;
import com.xd.redislualimit.annotation.RateLimit;import com.xd.redislualimit.utils.IPUtil;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.reflect.MethodSignature;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.core.StringRedisTemplate;import org.springframework.data.redis.core.script.DefaultRedisScript;import org.springframework.web.context.request.RequestContextHolder;import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;import java.io.Serializable;import java.lang.reflect.Method;import java.util.Collections;import java.util.List;import java.util.Objects;
/** * @Classname LimitAspect * @Description 注解拦截 * @Author 李号东 lihaodongmail@163.com * @Date 2019-05-25 20:15 * @Version 1.0 */@Slf4j@Aspect@Configurationpublic class LimitAspect {
@Autowired private RedisTemplate<String, Serializable> redisTemplate;
@Autowired private DefaultRedisScript<Number> redisluaScript;
//执行redis的具体方法,限制method,保证没有其他的东西进来 @Around("execution(* com.xd.redislualimit.controller ..*(..) )") public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Class<?> targetClass = method.getDeclaringClass();
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
if (rateLimit != null) { HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); String ipAddress = IPUtil.getIp(request);
String string = ipAddress + "-" + targetClass.getName() + "- " + method.getName() + "-" + rateLimit.key(); List<String> keys = Collections.singletonList(string); Number number = redisTemplate.execute(redisluaScript, keys, rateLimit.count(), rateLimit.time());
if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) { log.info("限流时间段内访问第:{} 次", number.toString()); return joinPoint.proceed(); }
} else { return joinPoint.proceed(); } log.error("已经到设置限流次数"); throw new RuntimeException("已经到设置限流次数"); }
}
package com.xd.redislualimit.controller;
import com.xd.redislualimit.annotation.RateLimit;import org.apache.commons.lang3.time.DateFormatUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.support.atomic.RedisAtomicInteger;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/** * @Classname LimiterController * @Description 测试控制层 * @Author 李号东 lihaodongmail@163.com * @Date 2019-05-25 20:16 * @Version 1.0 */@RestControllerpublic class LimiterController {
@Autowired private RedisTemplate redisTemplate;
// 10 秒中,可以访问5次 @RateLimit(key = "test", time = 10, count = 5) @GetMapping("/test") public String luaLimiter() { // 简单测试方法 RedisAtomicInteger entityIdCounter = new RedisAtomicInteger("counter", redisTemplate.getConnectionFactory()); String date = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS"); return date + " 累计访问次数:" + entityIdCounter.getAndIncrement(); }}
启动程序 打开浏览器访问 http://localhost:8080/test
连续访问5次 控制台打印
限流成功!
项目地址: https://github.com/LiHaodong888/SpringBootLearn/
点好看,送你小花花~