赞
踩
注意:
提前配置好redis。
import cn.hutool.core.lang.Assert; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; import com.alibaba.fastjson.serializer.SerializerFeature; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cache.Cache; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import reactor.util.annotation.Nullable; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.HashMap; import java.util.Map; @Slf4j @Configuration @EnableCaching @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) public class RedisConfig extends CachingConfigurerSupport { /** * 设置 redis 数据默认过期时间,默认2小时 * 设置@cacheable 序列化方式 */ @Bean public RedisCacheConfiguration redisCacheConfiguration() { FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig(); configuration = configuration.serializeValuesWith(RedisSerializationContext. SerializationPair.fromSerializer(fastJsonRedisSerializer)).entryTtl(Duration.ofHours(2)); return configuration; } @SuppressWarnings("all") @Bean(name = "redisTemplate") @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); //序列化 FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class); // value值的序列化采用fastJsonRedisSerializer template.setValueSerializer(fastJsonRedisSerializer); template.setHashValueSerializer(fastJsonRedisSerializer); // 全局开启AutoType,这里方便开发,使用全局的方式 ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // 建议使用这种方式,小范围指定白名单 // ParserConfig.getGlobalInstance().addAccept("me.zhengjie.domain"); // key的序列化采用StringRedisSerializer template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setConnectionFactory(redisConnectionFactory); return template; } /** * 自定义缓存key生成策略,默认将使用该策略 */ @Bean @Override public KeyGenerator keyGenerator() { return (target, method, params) -> { Map<String, Object> container = new HashMap<>(4); Class<?> targetClassClass = target.getClass(); // 类地址 container.put("class", targetClassClass.toGenericString()); // 方法名称 container.put("methodName", method.getName()); // 包名称 container.put("package", targetClassClass.getPackage()); // 参数列表 for (int i = 0; i < params.length; i++) { container.put(String.valueOf(i), params[i]); } // 转为JSON字符串 String jsonString = JSON.toJSONString(container); // 做SHA256 Hash计算,得到一个SHA256摘要作为Key return DigestUtils.sha256Hex(jsonString); }; } @Bean @Override public CacheErrorHandler errorHandler() { // 异常处理,当Redis发生异常时,打印日志,但是程序正常走 log.info("初始化 -> [{}]", "Redis CacheErrorHandler"); return new CacheErrorHandler() { @Override public void handleCacheGetError(RuntimeException e, Cache cache, Object key) { log.error("Redis occur handleCacheGetError:key -> [{}]", key, e); } @Override public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) { log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e); } @Override public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) { log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e); } @Override public void handleCacheClearError(RuntimeException e, Cache cache) { log.error("Redis occur handleCacheClearError:", e); } }; } } /** * Value 序列化 */ class FastJsonRedisSerializer<T> implements RedisSerializer<T> { private final Class<T> clazz; FastJsonRedisSerializer(Class<T> clazz) { super(); this.clazz = clazz; } @Override public byte[] serialize(T t) { if (t == null) { return new byte[0]; } return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(StandardCharsets.UTF_8); } @Override public T deserialize(byte[] bytes) { if (bytes == null || bytes.length <= 0) { return null; } String str = new String(bytes, StandardCharsets.UTF_8); return JSON.parseObject(str, clazz); } } /** * 重写序列化器 */ class StringRedisSerializer implements RedisSerializer<Object> { private final Charset charset; StringRedisSerializer() { this(StandardCharsets.UTF_8); } private StringRedisSerializer(Charset charset) { Assert.notNull(charset, "Charset must not be null!"); this.charset = charset; } @Override public String deserialize(byte[] bytes) { return (bytes == null ? null : new String(bytes, charset)); } @Override public @Nullable byte[] serialize(Object object) { String string = JSON.toJSONString(object); if (org.apache.commons.lang3.StringUtils.isBlank(string)) { return null; } string = string.replace("\"", ""); return string.getBytes(charset); } }
@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 }
@Aspect @Component public class LimitAspect { private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class); private final RedisTemplate<Object, Object> redisTemplate; public LimitAspect(RedisTemplate<Object, Object> redisTemplate) { this.redisTemplate = redisTemplate; } @Pointcut("@annotation(me.zhengjie.annotation.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 = StringUtils.getIp(request); } else { key = signatureMethod.getName(); } } ImmutableList<Object> keys = ImmutableList.of(StringUtils.join(limit.prefix(), "_", key, "_", request.getRequestURI().replace("/", "_"))); String luaScript = buildLuaScript(); RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class); Number count = redisTemplate.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 { throw new BadRequestException("访问次数受限制"); } } /** * 限流脚本 */ 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;"; }
@RestController @RequestMapping("/api/limit") @Api(tags = "系统:限流测试管理") public class LimitController { private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(); /** * 测试限流注解,下面配置说明该接口 60秒内最多只能访问 10次,保存到redis的键名为 limit_test, */ @AnonymousGetMapping @ApiOperation("测试") @Limit(key = "test", period = 60, count = 10, name = "testLimit", prefix = "limit") public int testLimit() { return ATOMIC_INTEGER.incrementAndGet(); } }
注:
以上为个人经验,希望能给大家一个参考,如有错误或未考虑完全的地方,望不吝赐教!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。