提交 cbaec03a 编写于 作者: 如梦技术's avatar 如梦技术 🐛

开源 mica 2.0 mica-redis 模块.

上级 a58fe532
......@@ -23,6 +23,7 @@ dependencyManagement {
dependency "net.dreamlu:mica-captcha:${VERSION}"
dependency "net.dreamlu:mica-jobs:${VERSION}"
dependency "net.dreamlu:mica-mongo:${VERSION}"
dependency "net.dreamlu:mica-redis:${VERSION}"
// commons
dependency "com.google.code.findbugs:jsr305:${findbugsVersion}"
dependency "io.swagger:swagger-annotations:${swaggerAnnotationsVersion}"
......
# redis-plus-redis
- redis cache 增强
- 分布式限流组件
## 依赖引用
### maven
```xml
<dependency>
<groupId>net.dreamlu</groupId>
<artifactId>mica-redis</artifactId>
<version>${version}</version>
</dependency>
```
### gradle
```groovy
compile("net.dreamlu:mica-redis:${version}")
```
## 1. redis cache 增强
1. 支持 # 号分隔 cachename 和 超时,支持 ms(毫秒),s(秒默认),m(分),h(小时),d(天)等单位。
示例:
```java
@Cacheable(value = "user#5m", key = "#id")
public String selectById(Serializable id) {
log.info("selectById");
return "selectById:" + id;
}
```
### MicaRedisCache
MicaRedisCache 为简化 redis 使用的 bean。
```java
@Autowired
private MicaRedisCache redisCache;
@Override
public String findById(Serializable id) {
return redisCache.get("user:" + id, () -> userMapper.selectById(id));
}
```
## 2. 分布式限流
### 2.1 开启限流组件
```yaml
mica:
redis:
rate-limiter:
enable: true
```
### 2.2 使用注解
```java
@RateLimiter
```
注解变量:
```java
/**
* 限流的 key 支持,必须:请保持唯一性
*
* @return key
*/
String value();
/**
* 限流的参数,可选,支持 spring el # 读取方法参数和 @ 读取 spring bean
*
* @return param
*/
String param() default "";
/**
* 支持的最大请求,默认: 2500
*
* @return 请求数
*/
long max() default 2500L;
/**
* 持续时间,默认: 3600
*
* @return 持续时间
*/
long ttl() default 3600L;
/**
* 时间单位,默认为秒
*
* @return TimeUnit
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
```
### 2.3 使用 Client
```java
@Autowired
private RateLimiterClient rateLimiterClient;
```
方法:
```java
/**
* 服务是否被限流
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间,单位默认为秒(seconds)
* @return 是否允许
*/
boolean isAllowed(String key, long max, long ttl);
/**
* 服务是否被限流
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param timeUnit 时间单位
* @return 是否允许
*/
boolean isAllowed(String key, long max, long ttl, TimeUnit timeUnit);
/**
* 服务限流,被限制时抛出 RateLimiterException 异常,需要自行处理异常
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param supplier Supplier 函数式
* @return 函数执行结果
*/
<T> T allow(String key, long max, long ttl, CheckedSupplier<T> supplier);
/**
* 服务限流,被限制时抛出 RateLimiterException 异常,需要自行处理异常
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param supplier Supplier 函数式
* @return 函数执行结果
*/
<T> T allow(String key, long max, long ttl, TimeUnit timeUnit, CheckedSupplier<T> supplier);
```
\ No newline at end of file
dependencies {
api project(":mica-core")
api "org.springframework.boot:spring-boot-starter-aop"
api "org.springframework.boot:spring-boot-starter-data-redis"
compileOnly "org.springframework.cloud:spring-cloud-context"
annotationProcessor "net.dreamlu:mica-auto:${micaAutoVersion}"
testImplementation "org.springframework.boot:spring-boot-starter-test"
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.cache;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import org.springframework.lang.Nullable;
import java.time.Duration;
/**
* cache key 封装
*
* @author L.cm
*/
@Getter
@ToString
@AllArgsConstructor
public class CacheKey {
/**
* redis key
*/
private String key;
/**
* 超时时间 秒
*/
@Nullable
private Duration expire;
public CacheKey(String key) {
this.key = key;
}
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.cache;
import net.dreamlu.mica.core.utils.ObjectUtil;
import net.dreamlu.mica.core.utils.StringPool;
import net.dreamlu.mica.core.utils.StringUtil;
import javax.annotation.Nullable;
import java.time.Duration;
/**
* cache key
*
* @author L.cm
*/
public interface ICacheKey {
/**
* 获取前缀
*
* @return key 前缀
*/
String getPrefix();
/**
* 超时时间
*
* @return 超时时间
*/
@Nullable
default Duration getExpire() {
return null;
}
/**
* 组装 cache key
*
* @param suffix 参数
* @return cache key
*/
default CacheKey getKey(Object... suffix) {
String prefix = this.getPrefix();
// 拼接参数
String key;
if (ObjectUtil.isEmpty(suffix)) {
key = prefix;
} else {
key = prefix.concat(StringUtil.join(suffix, StringPool.COLON));
}
Duration expire = this.getExpire();
return expire == null ? new CacheKey(key) : new CacheKey(key, expire);
}
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@NonNullApi
@NonNullFields
package net.dreamlu.mica.redis.cache;
import org.springframework.lang.NonNullApi;
import org.springframework.lang.NonNullFields;
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.config;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 扩展redis-cache支持注解cacheName添加超时时间
* <p>
*
* @author L.cm
*/
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@EnableConfigurationProperties(CacheProperties.class)
public class MicaRedisCacheAutoConfiguration {
/**
* 序列化方式
*/
private final RedisSerializer<Object> redisSerializer;
private final CacheProperties cacheProperties;
private final CacheManagerCustomizers customizerInvoker;
@Nullable
private final RedisCacheConfiguration redisCacheConfiguration;
MicaRedisCacheAutoConfiguration(RedisSerializer<Object> redisSerializer,
CacheProperties cacheProperties,
CacheManagerCustomizers customizerInvoker,
ObjectProvider<RedisCacheConfiguration> redisCacheConfiguration) {
this.redisSerializer = redisSerializer;
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
this.redisCacheConfiguration = redisCacheConfiguration.getIfAvailable();
}
@Primary
@Bean("redisCacheManager")
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
RedisCacheConfiguration cacheConfiguration = this.determineConfiguration();
List<String> cacheNames = this.cacheProperties.getCacheNames();
Map<String, RedisCacheConfiguration> initialCaches = new LinkedHashMap<>();
if (!cacheNames.isEmpty()) {
Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap<>(cacheNames.size());
cacheNames.forEach(it -> cacheConfigMap.put(it, cacheConfiguration));
initialCaches.putAll(cacheConfigMap);
}
boolean allowInFlightCacheCreation = true;
boolean enableTransactions = false;
RedisAutoCacheManager cacheManager = new RedisAutoCacheManager(redisCacheWriter, cacheConfiguration, initialCaches, allowInFlightCacheCreation);
cacheManager.setTransactionAware(enableTransactions);
return this.customizerInvoker.customize(cacheManager);
}
private RedisCacheConfiguration determineConfiguration() {
if (this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
} else {
CacheProperties.Redis redisProperties = this.cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
/**
* redis 配置
*
* @author L.cm
*/
@Getter
@Setter
@RefreshScope
@ConfigurationProperties("mica.redis")
public class MicaRedisProperties {
/**
* 序列化方式
*/
private SerializerType serializerType = SerializerType.JSON;
public enum SerializerType {
/**
* json 序列化
*/
JSON,
/**
* jdk 序列化
*/
JDK
}
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.config;
import net.dreamlu.mica.redis.ratelimiter.RedisRateLimiterAspect;
import net.dreamlu.mica.redis.ratelimiter.RedisRateLimiterClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import java.util.List;
/**
* 基于 redis 的分布式限流自动配置
*
* @author L.cm
*/
@Configuration
@ConditionalOnProperty(value = "mica.redis.rate-limiter.enable")
public class RateLimiterAutoConfiguration {
@SuppressWarnings("unchecked")
private RedisScript<List<Long>> redisRateLimiterScript() {
DefaultRedisScript redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("META-INF/scripts/mica_rate_limiter.lua")));
redisScript.setResultType(List.class);
return redisScript;
}
@Bean
@ConditionalOnMissingBean
public RedisRateLimiterClient redisRateLimiter(StringRedisTemplate redisTemplate,
Environment environment) {
RedisScript<List<Long>> redisRateLimiterScript = redisRateLimiterScript();
return new RedisRateLimiterClient(redisTemplate, redisRateLimiterScript, environment);
}
@Bean
@ConditionalOnMissingBean
public RedisRateLimiterAspect redisRateLimiterAspect(RedisRateLimiterClient rateLimiterClient) {
return new RedisRateLimiterAspect(rateLimiterClient);
}
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.config;
import net.dreamlu.mica.core.utils.StringPool;
import net.dreamlu.mica.core.utils.StringUtil;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.lang.Nullable;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Map;
/**
* redis cache 扩展cache name自动化配置
*
* @author L.cm
*/
public class RedisAutoCacheManager extends RedisCacheManager {
public RedisAutoCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
}
@Override
protected RedisCache createRedisCache(String name, @Nullable RedisCacheConfiguration cacheConfig) {
if (StringUtil.isBlank(name) || !name.contains(StringPool.HASH)) {
return super.createRedisCache(name, cacheConfig);
}
String[] cacheArray = name.split(StringPool.HASH);
if (cacheArray.length < 2) {
return super.createRedisCache(name, cacheConfig);
}
String cacheName = cacheArray[0];
if (cacheConfig != null) {
// 转换时间,支持时间单位例如:300ms,第二个参数是默认单位
Duration duration = DurationStyle.detectAndParse(cacheArray[1], ChronoUnit.SECONDS);
cacheConfig = cacheConfig.entryTtl(duration);
}
return super.createRedisCache(cacheName, cacheConfig);
}
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.config;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* CacheManagerCustomizers配置
*
* @author L.cm
*/
@Configuration
@ConditionalOnMissingBean(CacheManagerCustomizers.class)
public class RedisCacheManagerConfig {
@Bean
public CacheManagerCustomizers cacheManagerCustomizers(
ObjectProvider<List<CacheManagerCustomizer<?>>> customizers) {
return new CacheManagerCustomizers(customizers.getIfAvailable());
}
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.config;
import net.dreamlu.mica.redis.cache.MicaRedisCache;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
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.core.ValueOperations;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* RedisTemplate 配置
*
* @author L.cm
*/
@EnableCaching
@Configuration
@AutoConfigureBefore(RedisAutoConfiguration.class)
@EnableConfigurationProperties(MicaRedisProperties.class)
public class RedisTemplateConfiguration {
/**
* value 值 序列化
*
* @return RedisSerializer
*/
@Bean
@ConditionalOnMissingBean(RedisSerializer.class)
public RedisSerializer<Object> redisSerializer(MicaRedisProperties properties) {
MicaRedisProperties.SerializerType serializerType = properties.getSerializerType();
if (MicaRedisProperties.SerializerType.JDK == serializerType) {
return new JdkSerializationRedisSerializer();
}
return new GenericJackson2JsonRedisSerializer();
}
@Bean(name = "redisTemplate")
@ConditionalOnMissingBean(RedisTemplate.class)
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory, RedisSerializer<Object> redisSerializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// key 序列化
StringRedisSerializer keySerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setHashKeySerializer(keySerializer);
// value 序列化
redisTemplate.setValueSerializer(redisSerializer);
redisTemplate.setHashValueSerializer(redisSerializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
@Bean
@ConditionalOnMissingBean(ValueOperations.class)
public ValueOperations valueOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public MicaRedisCache redisClient(RedisTemplate<String, Object> redisTemplate) {
return new MicaRedisCache(redisTemplate);
}
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.ratelimiter;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 分布式 限流注解,默认速率为 600/ms
*
* @author L.cm
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RateLimiter {
/**
* 限流的 key 支持,必须:请保持唯一性
*
* @return key
*/
String value();
/**
* 限流的参数,可选,支持 spring el # 读取方法参数和 @ 读取 spring bean
*
* @return param
*/
String param() default "";
/**
* 支持的最大请求,默认: 100
*
* @return 请求数
*/
long max() default 100L;
/**
* 持续时间,默认: 3600
*
* @return 持续时间
*/
long ttl() default 1L;
/**
* 时间单位,默认为分
*
* @return TimeUnit
*/
TimeUnit timeUnit() default TimeUnit.MINUTES;
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.ratelimiter;
import net.dreamlu.mica.core.function.CheckedSupplier;
import net.dreamlu.mica.core.utils.Exceptions;
import java.util.concurrent.TimeUnit;
/**
* RateLimiter 限流 Client
*
* @author L.cm
*/
public interface RateLimiterClient {
/**
* 服务是否被限流
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间,单位默认为秒(seconds)
* @return 是否允许
*/
default boolean isAllowed(String key, long max, long ttl) {
return this.isAllowed(key, max, ttl, TimeUnit.SECONDS);
}
/**
* 服务是否被限流
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param timeUnit 时间单位
* @return 是否允许
*/
boolean isAllowed(String key, long max, long ttl, TimeUnit timeUnit);
/**
* 服务限流,被限制时抛出 RateLimiterException 异常,需要自行处理异常
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param supplier Supplier 函数式
* @return 函数执行结果
*/
default <T> T allow(String key, long max, long ttl, CheckedSupplier<T> supplier) {
return allow(key, max, ttl, TimeUnit.SECONDS, supplier);
}
/**
* 服务限流,被限制时抛出 RateLimiterException 异常,需要自行处理异常
*
* @param key 自定义的key,请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param supplier Supplier 函数式
* @return 函数执行结果
*/
default <T> T allow(String key, long max, long ttl, TimeUnit timeUnit, CheckedSupplier<T> supplier) {
boolean isAllowed = this.isAllowed(key, max, ttl, timeUnit);
if (isAllowed) {
try {
return supplier.get();
} catch (Throwable e) {
throw Exceptions.unchecked(e);
}
}
throw new RateLimiterException(key, max, ttl, timeUnit);
}
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.ratelimiter;
import lombok.Getter;
import java.util.concurrent.TimeUnit;
/**
* 限流异常
*
* @author L.cm
*/
@Getter
public class RateLimiterException extends RuntimeException {
private final String key;
private final long max;
private final long ttl;
private final TimeUnit timeUnit;
public RateLimiterException(String key, long max, long ttl, TimeUnit timeUnit) {
super(String.format("您的访问次数已超限:%s,速率:%d/%ds", key, max, timeUnit.toSeconds(ttl)));
this.key = key;
this.max = max;
this.ttl = ttl;
this.timeUnit = timeUnit;
}
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.ratelimiter;
import lombok.RequiredArgsConstructor;
import net.dreamlu.mica.core.spel.MicaExpressionEvaluator;
import net.dreamlu.mica.core.utils.CharPool;
import net.dreamlu.mica.core.utils.StringUtil;
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.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* redis 限流
*
* @author L.cm
*/
@Aspect
@RequiredArgsConstructor
public class RedisRateLimiterAspect implements ApplicationContextAware {
/**
* 表达式处理
*/
private final MicaExpressionEvaluator evaluator = new MicaExpressionEvaluator();
/**
* redis 限流服务
*/
private final RedisRateLimiterClient rateLimiterClient;
private ApplicationContext applicationContext;
/**
* AOP 环切 注解 @RateLimiter
*/
@Around("@annotation(limiter)")
public Object aroundRateLimiter(ProceedingJoinPoint point, RateLimiter limiter) throws Throwable {
String limitKey = limiter.value();
Assert.hasText(limitKey, "@RateLimiter value must have length; it must not be null or empty");
// el 表达式
String limitParam = limiter.param();
// 表达式不为空
String rateKey;
if (StringUtil.isNotBlank(limitParam)) {
String evalAsText = evalLimitParam(point, limitParam);
rateKey = limitKey + CharPool.COLON + evalAsText;
} else {
rateKey = limitKey;
}
long max = limiter.max();
long ttl = limiter.ttl();
TimeUnit timeUnit = limiter.timeUnit();
return rateLimiterClient.allow(rateKey, max, ttl, timeUnit, point::proceed);
}
/**
* 计算参数表达式
*
* @param point ProceedingJoinPoint
* @param limitParam limitParam
* @return 结果
*/
private String evalLimitParam(ProceedingJoinPoint point, String limitParam) {
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
Object[] args = point.getArgs();
Object target = point.getTarget();
Class<?> targetClass = target.getClass();
EvaluationContext context = evaluator.createContext(method, args, target, targetClass, applicationContext);
AnnotatedElementKey elementKey = new AnnotatedElementKey(method, targetClass);
return evaluator.evalAsText(limitParam, elementKey, context);
}
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
/*
* Copyright (c) 2019-2029, Dreamlu 卢春梦 (596392912@qq.com & www.dreamlu.net).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.dreamlu.mica.redis.ratelimiter;
import lombok.RequiredArgsConstructor;
import net.dreamlu.mica.core.utils.CharPool;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* redis 限流服务
*
* @author dream.lu
*/
@RequiredArgsConstructor
public class RedisRateLimiterClient implements RateLimiterClient {
/**
* redis 限流 key 前缀
*/
private static final String REDIS_KEY_PREFIX = "limiter:";
/**
* 失败的默认返回值
*/
private static final long FAIL_CODE = 0;
/**
* redisTemplate
*/
private final StringRedisTemplate redisTemplate;
/**
* redisScript
*/
private final RedisScript<List<Long>> script;
/**
* env
*/
private final Environment environment;
@Override
public boolean isAllowed(String key, long max, long ttl, TimeUnit timeUnit) {
// redis key
String redisKeyBuilder = REDIS_KEY_PREFIX +
getApplicationName(environment) + CharPool.COLON + key;
List<String> keys = Collections.singletonList(redisKeyBuilder);
// 毫秒,考虑主从策略和脚本回放机制,这个time由客户端获取传入
long now = System.currentTimeMillis();
// 转为毫秒,pexpire
long ttlMillis = timeUnit.toMillis(ttl);
// 执行命令
List<Long> results = this.redisTemplate.execute(this.script, keys, max + "", ttlMillis + "", now + "");
// 结果为空返回失败
if (results == null || results.isEmpty()) {
return false;
}
// 判断返回成功
Long result = results.get(0);
return result != FAIL_CODE;
}
private static String getApplicationName(Environment environment) {
return environment.getProperty("spring.application.name", "");
}
}
{
"properties": [
{
"name": "mica.redis.rate-limiter.enable",
"type": "java.lang.Boolean",
"description": "是否开启 redis 分布式限流.",
"defaultValue": "false"
}
]
}
-- lua 下标从 1 开始
-- 限流 key
local key = KEYS[1]
-- 限流大小
local max = tonumber(ARGV[1])
-- 超时时间
local ttl = tonumber(ARGV[2])
-- 考虑主从策略和脚本回放机制,这个time由客户端获取传入
local now = tonumber(ARGV[3])
-- 已经过期的时间点
local expired = now - ttl
-- 清除过期的数据,移除指定分数(score)区间内的所有成员
redis.call('zremrangebyscore', key, 0, expired)
-- 获取当前流量大小
local currentLimit = tonumber(redis.call('zcard', key))
local nextLimit = currentLimit + 1
if nextLimit > max then
-- 达到限流大小 返回 0
return 0;
else
-- 没有达到阈值 value + 1
redis.call("zadd", key, now, now)
-- 秒为单位设置 key 的生存时间
redis.call("pexpire", key, ttl)
return nextLimit
end
package net.dreamlu.mica;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* redis 测试
*
* @author L.cm
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class RedisApplicationTests {
@SpringBootApplication
public static class App {
}
@Test
public void contextLoads() {
}
}
......@@ -8,3 +8,4 @@ include "mica-swagger"
include "mica-captcha"
include "mica-jobs"
include "mica-mongo"
include "mica-redis"
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册