redis 注解式 限流操作

  当并发量比较大的时候,通常会用到限流。

  这里讲解一个redis实现的限流方法。

  首先这里有一个坑(我自己造成的)

  我在编写注解式缓存的时候,重写了redisTemplete实现Bean,导致这个问题一天没解决,其实早就应该想到。

  首先我们把Bean修改过来:

  

@Bean
public RedisTemplate<String, Object>
limitRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}

编写注解类
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {
// 资源名称,用于描述接口功能
String name() default "";

// 资源 key
String key() default "";

// key prefix
String prefix() default "";

// 时间的,单位秒
int period();

// 限制访问次数
int count();

// 限制类型
LimitType limitType() default LimitType.CUSTOMER;
}
作用在方法上,各参数相互配合。
public enum LimitType {
// 默认
CUSTOMER,
// by ip addr
IP;
}
分两种形式限流,一个对应IP,一个对应请求方法。

注解实现类:
@Slf4j
@Aspect
@Component
public class LimitAspect {

private static final String UNKNOWN = "unknown";

@Autowired
private RedisTemplate<String, Object> limitRedisTemplate;

private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);

@Pointcut("@annotation(com.example.demo.limit.Limit)")
public void pointcut() {
}

@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = RequestHolder.getHttpServletRequest();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method signatureMethod = signature.getMethod();
Limit limit = signatureMethod.getAnnotation(Limit.class);
LimitType limitType = limit.limitType();
String key = limit.key();
if (StringUtils.isEmpty(key)) {
if (limitType == LimitType.IP) {
key = getIp(request);
} else {
key = signatureMethod.getName();
}
}

ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limit.prefix(), "_", key, "_", request.getRequestURI().replaceAll("/","_")));
String luaScript = buildLuaScript();
RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number count = limitRedisTemplate.execute(redisScript, keys, limit.count(), limit.period());
if (null != count && count.intValue() <= limit.count()) {
logger.info("第{}次访问key为 {},描述为 [{}] 的接口", count, keys, limit.name());
return joinPoint.proceed();
} else {
logger.info("访问次数受限制");
//throw new RuntimeException("已经到设置限流次数");
return Integer.parseInt("0");
}
}

/**
* 获取ip地址
*/
public static String getIp(HttpServletRequest request) {
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();
}
String comma = ",";
String localhost = "127.0.0.1";
if (ip.contains(comma)) {
ip = ip.split(",")[0];
}
if (localhost.equals(ip)) {
// 获取本机真正的ip地址
try {
ip = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
return ip;
}

/**
* 限流脚本
*/
private String buildLuaScript() {
return "local c" +
"\nc = redis.call('get',KEYS[1])" +
"\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
"\nreturn c;" +
"\nend" +
"\nc = redis.call('incr',KEYS[1])" +
"\nif tonumber(c) == 1 then" +
"\nredis.call('expire',KEYS[1],ARGV[2])" +
"\nend" +
"\nreturn c;";
}
}

注意看限流脚本lua,不懂的大家可以去看看,redis允许lua脚本操作。

我们在学习MQ的时候,也学习过MQ有削峰限流的作用,大家看看源码它是怎么实现的。

其次,我们的限流能不能直接用MQ 就直接做到,不需要redis。大家来讨论讨论。

posted on 2020-03-20 16:26  Jason_LZP  阅读(589)  评论(0)    收藏  举报