文章目录
- Redis + Lua 限流实现
- 1. 导入依赖
- 2. 配置application.properties
- 3. 配置RedisTemplate实例
- 4. 定义限流类型枚举类
- 5. 自定义注解
- 6. 切面代码实现
- 7. 控制层实现
- 8. 测试
相比 Redis
事务, Lua脚本
的优点:
- 减少网络开销:使用
Lua
脚本,无需向Redis
发送多次请求,执行一次即可,减少网络传输 - 原子操作:
Redis
将整个Lua
脚本作为一个命令执行,原子,无需担心并发 - 复用:
Lua
脚本一旦执行,会永久保存Redis
中,,其他客户端可复用
Redis + Lua 限流实现
技术栈:自定义注解
、aop
、Redis + Lua
实现限流
1. 导入依赖
<dependencies><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>com.google.guava</groupId><artifactId>guava</artifactId><version>30.1-jre</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency> </dependencies>
2. 配置application.properties
spring.redis.host=10.1.61.121spring.redis.port=6379spring.redis.password=123456
3. 配置RedisTemplate实例
package com.lihw.lihwtestboot.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;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;import java.io.Serializable;@Configurationpublic class RedisLimiterHelper {@Beanpublic 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;}}
4. 定义限流类型枚举类
package com.lihw.lihwtestboot.schemas;/** * @explain: 限流类型 * @author: lihewei*/public enum LimitType {/** * 自定义key */CUSTOMER,/** * 请求者IP */IP;}
5. 自定义注解
period
表示请求限制时间段count
表示在period
这个时间段内允许放行请求的次数。limitType
代表限流的类型,可以根据请求的IP
、自定义key
,如果不传limitType
属性则默认用方法名作为默认key。
package com.lihw.lihwtestboot.anno;import com.lihw.lihwtestboot.schemas.LimitType;import java.lang.annotation.*;/** * @explain: 自定义限流注解 * @author: lihewei*/@Target({ElementType.METHOD, ElementType.TYPE})//作用于方法上@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface Limit {/** * 名字 */String name() default "";/** * key */String key() default "";/** * Key的前缀 */String prefix() default "";/** * 给定的时间范围 单位(秒) */int period();/** * 一定时间内最多访问次数 */int count();/** * 限流的类型(用户自定义key 或者 请求ip) */LimitType limitType() default LimitType.CUSTOMER;}
6. 切面代码实现
package com.lihw.lihwtestboot.aop;import com.google.common.collect.ImmutableList;import com.lihw.lihwtestboot.anno.Limit;import com.lihw.lihwtestboot.schemas.LimitType;import org.apache.commons.lang3.StringUtils;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.script.DefaultRedisScript;import org.springframework.data.redis.core.script.RedisScript;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;/** * @explain: 限流切面实现 * @author: lihewei*/@Aspect@Configurationpublic class LimitInterceptor {private static final Logger logger = LoggerFactory.getLogger(LimitInterceptor.class);private static final String UNKNOWN = "unknown";private final RedisTemplate<String, Serializable> limitRedisTemplate;@Autowiredpublic LimitInterceptor(RedisTemplate<String, Serializable> limitRedisTemplate) {this.limitRedisTemplate = limitRedisTemplate;}/** * @author lihw * @description 切面 */@Around("execution(public * *(..)) && @annotation(com.lihw.lihwtestboot.anno.Limit)")public Object interceptor(ProceedingJoinPoint pjp) {MethodSignature signature = (MethodSignature) pjp.getSignature();Method method = signature.getMethod();Limit limitAnnotation = method.getAnnotation(Limit.class);LimitType limitType = limitAnnotation.limitType();String name = limitAnnotation.name();String key;int limitPeriod = limitAnnotation.period();int limitCount = limitAnnotation.count();/** * 根据限流类型获取不同的key ,如果不传我们会以方法名作为key */switch (limitType) {case IP:key = getIpAddress();break;case CUSTOMER:key = limitAnnotation.key();break;default:key = StringUtils.upperCase(method.getName());}ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix(), key));try {String luaScript = buildLuaScript();RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);logger.info("Access try count is {} for name={} and key = {}", count, name, key);if (count != null && count.intValue() <= limitCount) {return pjp.proceed();} else {throw new RuntimeException("You have been dragged into the blacklist");}} catch (Throwable e) {if (e instanceof RuntimeException) {throw new RuntimeException(e.getLocalizedMessage());}throw new RuntimeException("server exception");}}/** * @description 编写 redis Lua 限流脚本 */public String buildLuaScript() {StringBuilder lua = new StringBuilder();lua.append("local c");lua.append("\nc = redis.call('get',KEYS[1])");// 调用不超过最大值,则直接返回lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");lua.append("\nreturn c;");lua.append("\nend");// 执行计算器自加lua.append("\nc = redis.call('incr',KEYS[1])");lua.append("\nif tonumber(c) == 1 then");// 从第一次调用开始限流,设置对应键值的过期lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");lua.append("\nend");lua.append("\nreturn c;");return lua.toString();}/** * @description 获取id地址 */public String getIpAddress() {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String ip = request.getHeader("x-forwarded-for");if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}return ip;}}
7. 控制层实现
package com.lihw.lihwtestboot.controller;import com.lihw.lihwtestboot.anno.Limit;import com.lihw.lihwtestboot.schemas.LimitType;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import java.util.concurrent.atomic.AtomicInteger;@RestControllerpublic class LimiterController {private static final AtomicInteger ATOMIC_INTEGER_1 = new AtomicInteger();private static final AtomicInteger ATOMIC_INTEGER_2 = new AtomicInteger();private static final AtomicInteger ATOMIC_INTEGER_3 = new AtomicInteger();@Limit(key = "limitTest", period = 10, count = 3)@GetMapping("/limitTest1")public int testLimiter1() {return ATOMIC_INTEGER_1.incrementAndGet();}@Limit(key = "customer_limit_test", period = 10, count = 3, limitType = LimitType.CUSTOMER)@GetMapping("/limitTest2")public int testLimiter2() {return ATOMIC_INTEGER_2.incrementAndGet();}@Limit(key = "ip_limit_test", period = 10, count = 3, limitType = LimitType.IP)@GetMapping("/limitTest3")public int testLimiter3() {return ATOMIC_INTEGER_3.incrementAndGet();}}
8. 测试
10s内连续请求三次以上拒绝请求