redis配置
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@EnableAspectJAutoProxy //V2 开启AOP自动代理
public class RedisConfig
{
/**
* @param lettuceConnectionFactory
* @return
*
* redis序列化的工具配置类,下面这个请一定开启配置
* 127.0.0.1:6379> keys *
* 1) "ord:102" 序列化过
* 2) "\xac\xed\x00\x05t\x00\aord:102" 野生,没有序列化过
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
{
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
自定义注解
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RedisLimitAnnotation
{
/**
* 资源的key,唯一
* 作用:不同的接口,不同的流量控制
*/
String key() default "";
/**
* 最多的访问限制次数
*/
long permitsPerSecond() default 3;
/**
* 过期时间(计算窗口时间),单位秒默认30
*/
long expire() default 30;
/**
* 默认温馨提示语
*/
String msg() default "系统繁忙,请稍后再试";
}
AOP
import com.annotations.RedisLimitAnnotation;
import com.exception.RedisLimitException;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
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.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import org.springframework.core.io.ClassPathResource;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@Aspect
@Component
public class RedisLimitAop
{
Object result = null;
@Resource
private StringRedisTemplate stringRedisTemplate;
private DefaultRedisScript<Long> redisLuaScript;
@PostConstruct
public void init()
{
redisLuaScript = new DefaultRedisScript<>();
redisLuaScript.setResultType(Long.class);
redisLuaScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
}
@Around("@annotation(com.atguigu.interview2.annotations.RedisLimitAnnotation)")
public Object around(ProceedingJoinPoint joinPoint)
{
System.out.println("---------环绕通知1111111");
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//拿到RedisLimitAnnotation注解,如果存在则说明需要限流
RedisLimitAnnotation redisLimitAnnotation = method.getAnnotation(RedisLimitAnnotation.class);
if (redisLimitAnnotation != null)
{
//获取redis的key
String key = redisLimitAnnotation.key();
String className = method.getDeclaringClass().getName();
String methodName = method.getName();
String limitKey = key +"\t"+ className+"\t" + methodName;
log.info(limitKey);
if (null == key)
{
throw new RedisLimitException("it's danger,limitKey cannot be null");
}
long limit = redisLimitAnnotation.permitsPerSecond();
long expire = redisLimitAnnotation.expire();
List<String> keys = new ArrayList<>();
keys.add(key);
Long count = stringRedisTemplate.execute(
redisLuaScript,
keys,
String.valueOf(limit),
String.valueOf(expire));
if (count != null && count == 0)
{
// 启动限流功能
//throw new RedisLimitException(redisLimitAnnotation.msg());
return redisLimitAnnotation.msg();
}
}
try {
result = joinPoint.proceed();//放行
} catch (Throwable e) {
throw new RuntimeException(e);
}
System.out.println("---------环绕通知2222222");
System.out.println();
System.out.println();
return result;
}
lua脚本
--获取KEY,针对那个接口进行限流,Lua脚本中的数组索引默认是从1开始的而不是从零开始。
local key = KEYS[1]
--获取注解上标注的限流次数
local limit = tonumber(ARGV[1])
local curentLimit = tonumber(redis.call('get', key) or "0")
--超过限流次数直接返回零,否则再走else分支
if curentLimit + 1 > limit
then return 0
-- 首次直接进入
else
-- 自增长 1
redis.call('INCRBY', key, 1)
-- 设置过期时间
redis.call('EXPIRE', key, ARGV[2])
return curentLimit + 1
end
--@RedisLimitAnnotation(key = "redisLimit", permitsPerSecond = 2, expire = 1, msg = "当前排队人数较多,请稍后再试!")
测试
@Slf4j
@RestController
public class RedisLimitController
{
/**
* Redis+Lua脚本+AOP+反射+自定义注解,打造我司内部基础架构限流组件
* http://localhost:24618/redis/limit/test
*
* 在redis中,假定一秒钟只能有3次访问,超过3次报错
* key = redisLimit
* Value = permitsPerSecond设置的具体值
* 过期时间 = expire设置的具体值,
* permitsPerSecond = 3, expire = 10
* 表示本次10秒内最多支持3次访问,到了3次后开启限流,过完本次10秒钟后才解封放开,可以重新访问
*/
@GetMapping("/redis/limit/test")
@RedisLimitAnnotation(key = "redisLimit", permitsPerSecond = 3, expire = 10, msg = "当前访问人数较多,请稍后再试,自定义提示!")
public String redisLimit()
{
return "正常业务返回,订单流水:"+ IdUtil.fastUUID();
}
}