!!! 这个是 原文链接
按照原文链接操作了下,发现最后有报错,原先以为是lua脚本的问题,后来查了半天是redisTemplate序列化的问题,记录一下
ERR Error running script (call to f_c79b3e806a8d1920de018f895a7c77b74651efe1): @user_script:3: user_script:3: attempt to compare number with nil
pom依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1-jre</version>
</dependency>
注解
package com.example.demo.annotation;
import com.example.demo.enums.LimitType;
import java.lang.annotation.*;
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisLimit {
// 资源名称
String name() default "";
// 资源key
String key() default "";
// 前缀
String prefix() default "";
// 时间
int period();
// 最多访问次数
int count();
// 类型
LimitType limitType() default LimitType.CUSTOMER;
// 提示信息
String msg() default "系统繁忙,请稍后再试";
}
切面类
package com.example.demo.aspect;
import com.example.demo.annotation.RedisLimit;
import com.example.demo.enums.LimitType;
import com.example.demo.exception.LimitException;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
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.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;
@Slf4j
@Aspect
@Configuration
public class RedisLimitAspect {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Around("@annotation(com.example.demo.annotation.RedisLimit)")
public Object around(ProceedingJoinPoint pjp) throws LimitException {
MethodSignature methodSignature = (MethodSignature)pjp.getSignature();
Method method = methodSignature.getMethod();
RedisLimit annotation = method.getAnnotation(RedisLimit.class);
LimitType limitType = annotation.limitType();
String name = annotation.name();
String key;
int period = annotation.period();
int count = annotation.count();
switch (limitType){
case IP:
key = getIpAddress();
break;
case CUSTOMER:
key = annotation.key();
break;
default:
key = StringUtils.upperCase(method.getName());
}
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(annotation.prefix(), key));
try {
String luaScript = buildLuaScript();
DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number number = redisTemplate.execute(redisScript, keys, count, period);
log.info("Access try count is {} for name = {} and key = {}", number, name, key);
if(number != null && number.intValue() == 1){
return pjp.proceed();
}
throw new LimitException(annotation.msg());
}catch (Throwable e){
if(e instanceof LimitException){
log.debug("令牌桶={},获取令牌失败",key);
throw new LimitException(e.getLocalizedMessage());
}
e.printStackTrace();
throw new RuntimeException("服务器异常");
}
}
public String buildLuaScript(){
return "redis.replicate_commands(); local listLen,time" +
"\nlistLen = redis.call('LLEN', KEYS[1])" +
// 不超过最大值,则直接写入时间
"\nif listLen and tonumber(listLen) < tonumber(ARGV[1]) then" +
"\nlocal a = redis.call('TIME');" +
"\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])" +
"\nelse" +
// 取出现存的最早的那个时间,和当前时间比较,看是小于时间间隔
"\ntime = redis.call('LINDEX', KEYS[1], -1)" +
"\nlocal a = redis.call('TIME');" +
"\nif a[1]*1000000+a[2] - time < tonumber(ARGV[2])*1000000 then" +
// 访问频率超过了限制,返回0表示失败
"\nreturn 0;" +
"\nelse" +
"\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])" +
"\nredis.call('LTRIM', KEYS[1], 0, tonumber(ARGV[1])-1)" +
"\nend" +
"\nend" +
"\nreturn 1;";
}
public String getIpAddress(){
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(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-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getRemoteAddr();
}
return ip;
}
}
枚举类-ip还是自定义限流
package com.example.demo.enums;
public enum LimitType {
CUSTOMER,IP;
}
异常类
package com.example.demo.exception;
public class LimitException extends Exception {
public LimitException(String message) {
super(message);
}
}
controller接口
注意:注解要加在方法上,不然不生效
package com.example.demo.controller;
import com.example.demo.domain.Student;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
public interface DemoController {
@GetMapping("/demo/test")
String demoTest();
}
package com.example.demo.controller;
import com.alibaba.fastjson.JSON;
import com.example.demo.annotation.RedisLimit;
import com.example.demo.domain.Student;
import com.example.demo.service.DemoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@Slf4j
public class DemoControllerImpl implements DemoController {
@Autowired
private DemoService demoService;
@Override
@RedisLimit(key = "cachingTest", count = 1, period = 1, msg = "当前排队人数较多,请稍后再试!")
public String demoTest() {
log.info("this is a test log");
try {
Thread.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return "test";
}
}
问题
如果上述操作完后,启动运行报错, 要对redis添加序列化设置
package com.example.demo.config;
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;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;
@SpringBootConfiguration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
//设置value的序列化方式json
redisTemplate.setValueSerializer(redisSerializer());
//设置key序列化方式String
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置hash key序列化方式String
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//设置hash value序列化json
redisTemplate.setHashValueSerializer(redisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
public RedisSerializer<Object> redisSerializer() {
//创建JSON序列化器
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//必须设置,否则无法序列化实体类对象
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
}
测试
使用apipost调用1次:
使用apipost并发调用3次: