-
下载网址:windows64位redis
-
解压下载的安装包,我的系统只有一个C盘直接扔在C盘,新建一个redis文件夹放下载的压缩包,解压
-
修改默认配置,打开解压的文件,找到redis.windows.conf修改默认密码
-
启动服务,方法有很多,我喜欢比较快的,直接拖动redis.windows.conf到redis-server.exe就启动了,启动后的窗口不要关闭,可以最小化,不然服务就没了,如下视频
启动redis服务
- 连接测试,在文件夹中填写cmd按回车,打开命令窗口
输入命令连接服务redis-cli.exe -h 127.0.0.1 -p 6379 -a 123456
PS:host:127.0.0.1代表本机地址
port: 6379 端口号
authentication: 123456 密码
往redis写数据 set testKey 1
获取指定key数据get testKey
码垛,Redis都安装好了,不深入学习一波?直接来个java+springBoot+springRedis完成一个接口限流不香吗?
这是笔者在微信公众号上面看到的文章哈,都是各位大神的贡献成果我自己鞋废了,也分享给兄弟们,有侵权的地方兄弟们指出删帖哈,丧失狗叫权!
先说想法,理清思路好办事,达到事半功倍的效果。
- 自定义注解给限流的接口使用
- 通过aop实现拦截自定义的注解
- redis存储限制请求接口key在过期时间内叠加的次数
- 获取redis存储的次数并与指定的总次数做比对,返回是否超过限制次数结果
思路理清了,接下来开始编码
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--redis接口限流依赖-->
<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>
配置文件:
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=123456
编写自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestLimit {
// 限流key
String key() default "rate_limit:";
// 默认每60s限制请求100次以内接口
int time() default 60;
int count() default 100;
// 限流类型
LimitType limitType() default LimitType.Default;
}
写拦截,采用spring的面向切面编程思维,简称aop
@Aspect
@Component
@Slf4j
public class RateLimitAspect {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Autowired
private RedisScript<Long> limitScript;
@Before("@annotation(requestLimit)")
public void doBefore(JoinPoint point, RequestLimit requestLimit) {
String key = requestLimit.key();
key = buildKey(key, point.getSignature().getName(), requestLimit.limitType());
int time = requestLimit.time();
int count = requestLimit.count();
List<Object> keys = Collections.singletonList(key);
Long number = redisTemplate.execute(limitScript, keys, count, time);
if (number == null || number.intValue() > count) {
throw new WebServiceException("访问过于频繁,请稍后重试");
}
log.info("{}s内限制请求总次数:{},当前请求已用次数:{},缓存key:{}", time, count, number.intValue(), key);
}
/**
* key = 默认前缀:函数名:是否ip?ip:无
* @param key
* @param methodName
* @param type
* @return
*/
private String buildKey(String key, String methodName, LimitType type) {
StringBuffer result = new StringBuffer(key);
result.append(methodName);
if (type.equals(LimitType.IP)) {
HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = IPUtils.getIPAddress(req);
if(StringUtils.isNotBlank(ip)) result.append(":").append(ip);
}
return result.toString();
}
}
注意这里@Before(“@annotation(requestLimit)”)
Ip地址获取工具类,度娘里一大堆,不好用的自己去抠
public class IPUtils {
public static String getIPAddress(HttpServletRequest request) {
String ip = null;
//X-Forwarded-For:Squid 服务代理
String ipAddresses = request.getHeader("X-Forwarded-For");
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
//Proxy-Client-IP:apache 服务代理
ipAddresses = request.getHeader("Proxy-Client-IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
//WL-Proxy-Client-IP:weblogic 服务代理
ipAddresses = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
//HTTP_CLIENT_IP:有些代理服务器
ipAddresses = request.getHeader("HTTP_CLIENT_IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
//X-Real-IP:nginx服务代理
ipAddresses = request.getHeader("X-Real-IP");
}
//有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP
if (ipAddresses != null && ipAddresses.length() != 0) {
ip = ipAddresses.split(",")[0];
}
//还是不能获取到,最后再通过request.getRemoteAddr();获取
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses)) {
ip = request.getRemoteAddr();
}
return ip;
}
接下来,抄代码的时候有没看到limitScript这个对象?是不是好奇这个是啥,我先上代码你们看了就明白了
@Configuration
public class LuaExecuteConfig {
@Bean
public DefaultRedisScript<Long> limitScript(){
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/ratelimit.lua")));
redisScript.setResultType(Long.class);
return redisScript;
}
}
注意这一段代码
加载的是这个文件,路径很重要兄弟,不要粗心了乱放
文件里的内容
local key = KEYS[1]
local count = tonumber(ARGV[1])
local time = 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)
写到这里相信就明白了limitScript是干嘛用的了,没错,小伙伴们真滴C,就是Spring redisTemplate执行lua脚本用的,为啥要使用lua脚本?直接告诉你们一个特性:原子性,这段脚本意思是第一次请求,给过期时间(传参进来的),第二次进来就返回值并+1,场景就是访问次数+1
好了,注意一下这个
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
这里自定义序列化防止乱码
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
// 使用Jackson2JsonRedisSerializer序列化
Jackson2JsonRedisSerializer<Object> js = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
js.setObjectMapper(mapper);
redisTemplate.setKeySerializer(js);
redisTemplate.setValueSerializer(js);
redisTemplate.setHashValueSerializer(js);
redisTemplate.setHashKeySerializer(js);
return redisTemplate;
}
}
写完了,收工。额打扰了,还是测试下吧
@RestController
@Slf4j
@RequestMapping("/async/executor")
public class AsyncExecutorController {
private SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@GetMapping("ramitTest")
@RequestLimit(time = 20, count = 5, limitType = LimitType.IP)
public String testRateLimit() {
return "request time:" + df.format(new Date());
}
}
看结果吧,期望:20s内Ip的方式限制请求5次,超过5次得等过完20s才让重新请求
拿下redis新场景,码垛,兄弟们真滴C,直接鞋废了