redis + lua 脚本实现分布式限流
redis环境搭建,请自行百度。
windows搭建lua环境,参考链接:https://blog.csdn.net/weixin_41725792/article/details/113827606
redis内置了lua解释器,我们在redis中可以使用redis 关键字 eval 运行Lua代码,如下:
# eval 关键字
# 脚本" return { KEYS[1],ARGV[1]} "
# 2 参数个数
# 参数 K1 K2 ,值 V1 V2
eval " return { KEYS[1],ARGV[1]} " 2 K1 K2 V1 V2
redis预加载:
script load "return 'aaa'"
evalsha "e20bf1be2a7b6dc909965de4a871a2b34a6d900d" 0
带参数:
# lua 连接符是 ..
script load "return 'hello '..KEYS[1]"
evalsha "c42984269dae236a5a42bea45409b728a873605e" 1 jiabengshi
# 查看脚本是否存在 存在返回1 不存在返回0
script exists "c42984269dae236a5a42bea45409b728a873605e"
# 清空脚本
script flush
接下来进入正题:使用java操作redis,实现分布式限流。
1. 编写lua限流脚本
-- 声明变量
local methodKey = KEYS[1]
-- 打印在控制台
print("key is "..methodKey)
-- redis控制台打印日志 注意配置文件配置等级 可去配置文件中logfile="" 路劲下查看
redis.log(redis.LOG_DEBUG, "key is ", methodKey)
-- 传入的限流值
print("value is "..ARGV[1])
local limit = tonumber(ARGV[1])
print("convert is "..limit)
-- 获取当前限流个数 可能为nil(key不存在)
local count = tonumber(redis.call("get", methodKey) or "0")
print("count is "..count)
if count + 1 > limit then
-- 超出限流数 拒绝
redis.log(redis.LOG_INFO, "limit result is ", false)
return false
end
-- 自增1
redis.call('INCRBY',methodKey,1)
-- 设置过期时间 一秒
redis.call('EXPIRE',methodKey,1)
return true
2. 编写java程序(插件式)
<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>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
META-INF
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.redis.lua.config.RedisLuaConfiguration
package com.redis.lua.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
@Configuration
@ComponentScan("com.redis.lua")
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisTemplate.class)
public class RedisLuaConfiguration {
@Bean
public DefaultRedisScript defaultRedisScript() {
DefaultRedisScript<Boolean> booleanDefaultRedisScript = new DefaultRedisScript<>();
booleanDefaultRedisScript.setLocation(new ClassPathResource("ratelimiter.lua"));
booleanDefaultRedisScript.setResultType(Boolean.class);
return booleanDefaultRedisScript;
}
}
package com.redis.lua.service;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class RedisLuaService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisScript<Boolean> redisScript;
public Boolean limit(String methodKey, Integer limitCount) {
log.info("key {} limitCount {}", methodKey, limitCount);
Boolean result = stringRedisTemplate.execute(redisScript, Lists.newArrayList(methodKey), limitCount.toString());
if (!result) {
throw new RuntimeException("more than limit" + limitCount);
}
return result;
}
}
package com.redis.lua.aop;
import com.redis.lua.anno.LuaLimit;
import com.redis.lua.service.RedisLuaService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Component
@Aspect
@Slf4j
public class RedisLuaAop {
@Autowired
private RedisLuaService redisLuaService;
@Value("${lua.limit:1}")
private Integer limitCount;
@Pointcut("@annotation(com.redis.lua.anno.LuaLimit)")
public void pointCut() {
}
@Before("pointCut()")
public void beforePointCut(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LuaLimit annotation = method.getAnnotation(LuaLimit.class);
redisLuaService.limit(method.getName(), annotation.limitCount() == 0 ? 0 : limitCount);
}
}
package com.redis.lua.anno;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface LuaLimit {
int limitCount() default 1;
}
3. 验证
新建项目
<dependency>
<groupId>com.huazhi</groupId>
<artifactId>redis-lua</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
spring:
application:
name: redis
redis:
host: localhost
port: 6379
@GetMapping("/testLua")
@LuaLimit(limitCount = 1)
public String testLua() {
return "success";
}
一秒点击一次
一秒点击多次