上一篇文章《微服务实战spring cloud alibaba(二十一)使用注解的方式实现单个接口的令牌桶限流》我们介绍了使用guava框架实现令牌筒算法限流,这篇文章我们再升级一下,因为真实的环境中我们很少会把guava的令牌桶集成到咱们的项目中来,一般我们都是使用redis来实现令牌筒算法限流的,因此这篇文章我们介绍下使用Redis来实现令牌筒限流。下面直接来实操一下:
一、创建一个maven项目,并且引入如下的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
<!-- log start -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.6</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.6.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- OKHttp3依赖 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>二、定义一个redis limit的注解
我们这里的演示还是用上一篇文章里面的注解方式进行修改,提倡大家使用注解,因为非常的方便。完整代码示例如下:
package com.demo.api.aop;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface RedisLimitAnnotation {
/**
* 资源的key,唯一
* 作用:不同的接口,不同的流量控制
*/
String key() default "";
/**
* 最多的访问限制次数
*/
long permitsPerSecond() default 2;
/**
* 过期时间也可以理解为单位时间,单位秒,默认60
*/
long expire() default 60;
/**
* 得不到令牌的提示语
*/
String msg() default "系统繁忙,请稍后再试.";
}内容和之前使用guava都差不多。
三、编写redis limit注解的逻辑处理
有了注解,那肯定是需要触发的,这里我们需要把注解的逻辑实现一下,完整代码示例如下:
package com.demo.api.aop;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
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.core.io.ClassPathResource;
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.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import com.alibaba.fastjson.JSON;
import com.demo.api.model.BaseResponse;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Aspect
@Component
public class RedisLimitAspect {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private DefaultRedisScript<Long> redisScript;
@Pointcut("@annotation(com.demo.api.aop.RedisLimitAnnotation)")
private void check() {
}
@PostConstruct
public void init() {
redisScript = new DefaultRedisScript<>();
redisScript.setResultType(Long.class);
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("rateLimiter.lua")));
}
@Before("check()")
public void before(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 拿到RedisLimit注解,如果存在则说明需要限流
RedisLimitAnnotation redisLimit = method.getAnnotation(RedisLimitAnnotation.class);
if (redisLimit != null) {
// 获取redis的key
String key = redisLimit.key();
String className = method.getDeclaringClass().getName();
String name = method.getName();
String limitKey = key + className + method.getName();
log.info(limitKey);
if (StringUtils.isEmpty(key)) {
this.responseFail(redisLimit.msg());
}
long limit = redisLimit.permitsPerSecond();
long expire = redisLimit.expire();
List<String> keys = new ArrayList<>();
keys.add(key);
Long count = stringRedisTemplate.execute(redisScript, keys, String.valueOf(limit), String.valueOf(expire));
log.info("Access try count is {} for key={}", count, key);
if (count != null && count == 0) {
log.debug("令牌桶={},获取令牌失败", key);
this.responseFail(redisLimit.msg());
}
}
}
/**
* 直接向前端抛出异常
*
* @param msg 提示信息
*/
private void responseFail(String msg) {
try {
HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getResponse();
ServletOutputStream outputStream = response.getOutputStream();
// 使用setCharacterEncoding方法设置输出内容使用UTF-8进行编码
response.setCharacterEncoding("UTF-8");
BaseResponse rs = BaseResponse.fail(msg);
String jsonString = JSON.toJSONString(rs);
outputStream.write(jsonString.getBytes());
outputStream.flush();
outputStream.close();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}在这里代码我们有使用到redisScript,这个其实主要是使用的redis的lua脚本,我们在这里是把redis的lua脚本作为一个单独的文件进行处理的,首先在src/main/resources文件夹下新创建一个名为rateLimiter.lua的脚本,然后把下面的值放进去:
--获取KEY
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local curentLimit = tonumber(redis.call('get', key) or "0")
if curentLimit + 1 > limit
then return 0
else
-- 自增长 1
redis.call('INCRBY', key, 1)
-- 设置过期时间
redis.call('EXPIRE', key, ARGV[2])
return curentLimit + 1
end四、初始化redis的配置
上面我们用到的是redistemplate,我们需要配置下redistemplate的序列化和反序列化,代码如下:
package com.demo.api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(factory);
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// 初始化参数和初始化工作
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}五、添加redis的配置文件
redis的配置比较简单,把下面的内容抄过去即可。
spring: redis: database: 0 host: 192.168.31.30 port: 6379 password: 3c0bef8420ca0961a74ff1fbe8c66ef4 jedis: pool: max-active: 8 max-wait: -1 max-idle: 8 min-idle: 0 timeout: 500
六、编写controller
这里我们新起一个接口,然后把redis的注解添加进去
@GetMapping("/testLimiter2")
@RedisLimitAnnotation(key = "redis-limit:testLimiter2", permitsPerSecond = 2, expire = 1, msg = "当前排队人数较多,请稍后再试!")
public BaseResponse testLimiter2() {
return BaseResponse.success("请求成功,获取令牌成功,时间:" + LocalDateTime.now().format(dtf));
}七、测试
这里我们还是使用前面文章的okhttp多线程来进行测试,把url修改下即可。
然后我们把服务端启动起来,然后再启动测试类,看下效果。
测试下来完全没有问题。
备注:
1、这里使用redis的话,这里的注解实现是before和之前使用的guava不一样,一定要注意下实现方法。
以上就是使用redis作为令牌筒对接口进行限流的案例,最后按照惯例,附上本案例的源码,登录后即可下载。












还没有评论,来说两句吧...