init: 导入RuoYi‑Vue‑Plus 6.X完整代码

This commit is contained in:
admin
2026-08-09 17:51:11 +08:00
commit 51cac459b3
914 changed files with 103833 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common</artifactId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-common-redis</artifactId>
<description>
ruoyi-common-redis 缓存服务
</description>
<dependencies>
<!-- RuoYi Common Core-->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- 序列化模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-json</artifactId>
</dependency>
<!--redisson-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<!-- Redisson 缓存 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-cache</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- 分布式锁 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redisson-spring-boot-starter</artifactId>
</dependency>
<!-- 本地缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- Jackson Databind -->
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Hutool 加密工具 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-crypto</artifactId>
</dependency>
<!-- Sa-Token 核心 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
</dependency>
<!-- redis序列化替代方案 比json快无数的跨语言二进制序列化 -->
<dependency>
<groupId>org.apache.fory</groupId>
<artifactId>fory-core</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,47 @@
package org.dromara.common.redis.annotation;
import org.dromara.common.redis.enums.LimitType;
import java.lang.annotation.*;
/**
* 限流注解
*
* @author Lion Li
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
/**
* 限流key,支持使用Spring el表达式来动态获取方法上的参数值
* 格式类似于 #code.id #{#code}
*/
String key() default "";
/**
* 限流时间,单位秒
*/
int time() default 60;
/**
* 限流次数
*/
int count() default 100;
/**
* 限流类型
*/
LimitType limitType() default LimitType.DEFAULT;
/**
* 提示消息 支持国际化 格式为 {code}
*/
String message() default "{rate.limiter.message}";
/**
* 限流策略超时时间 默认一天(策略存活时间 会清除已存在的策略数据)
*/
int timeout() default 86400;
}
@@ -0,0 +1,32 @@
package org.dromara.common.redis.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 自定义注解防止表单重复提交
*
* @author Lion Li
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 间隔时间(ms),小于此时间视为重复提交
*/
int interval() default 5000;
/**
* 重复提交间隔时间单位。
*/
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
/**
* 提示消息 支持国际化 格式为 {code}
*/
String message() default "{repeat.submit.message}";
}
@@ -0,0 +1,125 @@
package org.dromara.common.redis.aspectj;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.redis.annotation.RateLimiter;
import org.dromara.common.redis.enums.LimitType;
import org.dromara.common.redis.utils.RedisUtils;
import org.redisson.api.RateType;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.lang.reflect.Method;
/**
* 限流处理
*
* @author Lion Li
*/
@Slf4j
@Aspect
public class RateLimiterAspect {
/**
* 定义spel表达式解析器
*/
private final ExpressionParser parser = new SpelExpressionParser();
/**
* 定义spel解析模版
*/
private final ParserContext parserContext = new TemplateParserContext();
/**
* 方法参数解析器
*/
private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
/**
* 在限流注解方法执行前扣减令牌,令牌不足时阻断请求。
*
* @param point 切点信息
* @param rateLimiter 限流注解配置
*/
@Before("@annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
int time = rateLimiter.time();
int count = rateLimiter.count();
int timeout = rateLimiter.timeout();
try {
String combineKey = getCombineKey(rateLimiter, point);
RateType rateType = RateType.OVERALL;
if (rateLimiter.limitType() == LimitType.CLUSTER) {
rateType = RateType.PER_CLIENT;
}
long number = RedisUtils.rateLimiter(combineKey, rateType, count, time, timeout);
if (number == -1) {
String message = rateLimiter.message();
if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {
message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));
}
throw new ServiceException(message);
}
log.info("限制令牌 => {}, 剩余令牌 => {}, 缓存key => '{}'", count, number, combineKey);
} catch (Exception e) {
if (e instanceof ServiceException) {
throw e;
} else {
throw new RuntimeException("服务器限流异常,请稍候再试", e);
}
}
}
/**
* 组装限流缓存键。
*
* @param rateLimiter 限流注解配置
* @param point 切点信息
* @return 限流缓存键
*/
private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
String key = rateLimiter.key();
// 判断 key 不为空 和 不是表达式
if (StringUtils.isNotBlank(key) && StringUtils.containsAny(key, "#")) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method targetMethod = signature.getMethod();
Object[] args = point.getArgs();
MethodBasedEvaluationContext context =
new MethodBasedEvaluationContext(null, targetMethod, args, pnd);
context.setBeanResolver(new BeanFactoryResolver(SpringUtils.getBeanFactory()));
Expression expression;
if (StringUtils.startsWith(key, parserContext.getExpressionPrefix())
&& StringUtils.endsWith(key, parserContext.getExpressionSuffix())) {
expression = parser.parseExpression(key, parserContext);
} else {
expression = parser.parseExpression(key);
}
key = expression.getValue(context, String.class);
}
StringBuilder stringBuffer = new StringBuilder(GlobalConstants.RATE_LIMIT_KEY);
stringBuffer.append(ServletUtils.getRequest().getRequestURI()).append(StringUtils.COLON);
if (rateLimiter.limitType() == LimitType.IP) {
// 获取请求ip
stringBuffer.append(ServletUtils.getClientIP()).append(StringUtils.COLON);
} else if (rateLimiter.limitType() == LimitType.CLUSTER) {
// 获取客户端实例id
stringBuffer.append(RedisUtils.getClient().getId()).append(StringUtils.COLON);
}
return stringBuffer.append(key).toString();
}
}
@@ -0,0 +1,184 @@
package org.dromara.common.redis.aspectj;
import cn.dev33.satoken.SaManager;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.SecureUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.constant.HttpStatus;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.redis.annotation.RepeatSubmit;
import org.dromara.common.redis.utils.RedisUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import java.lang.reflect.Array;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
import java.util.StringJoiner;
/**
* 防止重复提交(参考美团GTIS防重系统)
*
* @author Lion Li
*/
@Aspect
public class RepeatSubmitAspect {
private static final ThreadLocal<String> KEY_CACHE = new ThreadLocal<>();
/**
* 请求进入前校验是否重复提交。
*
* @param point 切点
* @param repeatSubmit 防重复提交注解
*/
@Before("@annotation(repeatSubmit)")
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
// 如果注解不为0 则使用注解数值
long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
if (interval < 1000) {
throw new ServiceException("重复提交间隔时间不能小于'1'秒");
}
HttpServletRequest request = ServletUtils.getRequest();
String nowParams = argsArrayToString(point.getArgs());
// 请求地址(作为存放cache的key值)
String url = request.getRequestURI();
// 唯一值(没有消息头则使用请求地址)
String submitKey = StringUtils.trimToEmpty(request.getHeader(SaManager.getConfig().getTokenName()));
submitKey = SecureUtil.md5(submitKey + StringUtils.COLON + nowParams);
// 唯一标识(指定key + url + 消息头)
String cacheRepeatKey = GlobalConstants.REPEAT_SUBMIT_KEY + url + submitKey;
if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
KEY_CACHE.set(cacheRepeatKey);
} else {
String message = repeatSubmit.message();
if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {
message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));
}
throw new ServiceException(message);
}
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
try {
if (jsonResult instanceof R<?> r) {
// 成功则不删除redis数据 保证在有效时间内无法重复提交
if (r.getCode() == HttpStatus.SUCCESS) {
return;
}
deleteRepeatKey();
}
} finally {
KEY_CACHE.remove();
}
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "@annotation(repeatSubmit)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Exception e) {
try {
deleteRepeatKey();
} finally {
KEY_CACHE.remove();
}
}
/**
* 删除当前请求写入的防重键。
*/
private void deleteRepeatKey() {
String key = KEY_CACHE.get();
if (StringUtils.isNotBlank(key)) {
RedisUtils.deleteObject(key);
}
}
/**
* 参数拼装
*
* @param paramsArray 方法参数数组
* @return 拼装后的参数字符串
*/
private String argsArrayToString(Object[] paramsArray) {
StringJoiner params = new StringJoiner(" ");
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
}
for (Object o : paramsArray) {
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
params.add(JsonUtils.toJsonString(o));
}
}
return params.toString();
}
/**
* 判断是否需要过滤的对象。
*
* @param o 对象信息。
* @return 如果是需要过滤的对象,则返回true;否则返回false。
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray()) {
if (MultipartFile.class.isAssignableFrom(clazz.getComponentType())) {
return true;
}
int length = Array.getLength(o);
for (int i = 0; i < length; i++) {
Object value = Array.get(o, i);
if (ObjectUtil.isNotNull(value) && isFilterObject(value)) {
return true;
}
}
return false;
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection) {
if (ObjectUtil.isNotNull(value) && isFilterObject(value)) {
return true;
}
}
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.values()) {
if (ObjectUtil.isNotNull(value) && isFilterObject(value)) {
return true;
}
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
}
}
@@ -0,0 +1,45 @@
package org.dromara.common.redis.config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.dromara.common.redis.manager.PlusSpringCacheManager;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import java.util.concurrent.TimeUnit;
/**
* 缓存配置
*
* @author Lion Li
*/
@AutoConfiguration
@EnableCaching
public class CacheConfig {
/**
* caffeine 本地缓存处理器
*/
@Bean
public Cache<Object, Object> caffeine() {
return Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(30, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(1000)
.build();
}
/**
* 自定义缓存管理器 整合spring-cache
*/
@Bean
public CacheManager cacheManager() {
return new PlusSpringCacheManager();
}
}
@@ -0,0 +1,26 @@
package org.dromara.common.redis.config;
import org.dromara.common.redis.aspectj.RepeatSubmitAspect;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConfiguration;
/**
* 幂等功能配置
*
* @author Lion Li
*/
@AutoConfiguration(after = RedisConfiguration.class)
public class IdempotentConfig {
/**
* 创建重复提交切面。
*
* @return 重复提交切面
*/
@Bean
public RepeatSubmitAspect repeatSubmitAspect() {
return new RepeatSubmitAspect();
}
}
@@ -0,0 +1,32 @@
package org.dromara.common.redis.config;
import cn.hutool.http.HttpStatus;
import com.baomidou.lock.LockFailureStrategy;
import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration;
import org.dromara.common.core.exception.ServiceException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
/**
* Lock4j 配置
*
* @author AprilWind
*/
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@AutoConfiguration(before = LockAutoConfiguration.class)
public class Lock4jConfig {
/**
* Lock4j 获取锁失败策略。
*/
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public LockFailureStrategy defaultLockFailureStrategy() {
return (key, method, arguments) -> {
throw new ServiceException("业务处理中,请稍后再试...", HttpStatus.HTTP_UNAVAILABLE);
};
}
}
@@ -0,0 +1,27 @@
package org.dromara.common.redis.config;
import org.dromara.common.redis.aspectj.RateLimiterAspect;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConfiguration;
/**
* 限流功能配置。
*
* @author guangxin
* @date 2023/1/18
*/
@AutoConfiguration(after = RedisConfiguration.class)
public class RateLimiterConfig {
/**
* 创建限流切面。
*
* @return 限流切面
*/
@Bean
public RateLimiterAspect rateLimiterAspect() {
return new RateLimiterAspect();
}
}
@@ -0,0 +1,167 @@
package org.dromara.common.redis.config;
import cn.hutool.core.util.ObjectUtil;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.redis.config.properties.RedissonProperties;
import org.dromara.common.redis.handler.KeyPrefixHandler;
import org.dromara.common.redis.handler.RedisExceptionHandler;
import org.redisson.client.codec.StringCodec;
import org.redisson.codec.CompositeCodec;
import org.redisson.codec.TypedJsonJackson3Codec;
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import tools.jackson.databind.DefaultTyping;
import tools.jackson.databind.ext.javatime.deser.LocalDateTimeDeserializer;
import tools.jackson.databind.ext.javatime.ser.LocalDateTimeSerializer;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import tools.jackson.databind.module.SimpleModule;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;
/**
* redis配置
*
* @author Lion Li
*/
@Slf4j
@AutoConfiguration
@EnableConfigurationProperties(RedissonProperties.class)
public class RedisConfig {
@Autowired
private RedissonProperties redissonProperties;
/**
* 自定义 Redisson 序列化、线程与连接模式配置。
*
* @return Redisson 自动配置定制器
*/
@Bean
public RedissonAutoConfigurationCustomizer redissonCustomizer() {
return config -> {
SimpleModule simpleModule = new SimpleModule();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
simpleModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
simpleModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
JsonMapper jsonMapper = JsonMapper.builder()
.addModules(simpleModule)
.defaultTimeZone(TimeZone.getDefault())
.changeDefaultVisibility(visibilityChecker -> visibilityChecker.withVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY))
// 指定序列化输入的类型,类必须是非final修饰的。序列化时将对象全类名一起保存下来
// 因为安全策略,LaissezFaireSubTypeValidator 在 Jackson 3.X 中被禁止外部引用,在反序列化的数据来源不可信时,需要配置反序列化验证器来防止反序列化漏洞攻击,使用者需要自行执行哪些包或类是可以放行的
// 此处使用 BasicPolymorphicTypeValidator + 自定义类型匹配起替代,默认放行所有类型,此行为配置与 Jackson 2.X 中 LaissezFaireSubTypeValidator 的行为基本一致
// 一般而言,更加建议使用包名白名单的方式去进行匹配,以确保放行通过安全认可的类,如:BasicPolymorphicTypeValidator.builder().allowIfBaseType("org.dromara").allowIfSubType("org.dromara").build()
.activateDefaultTyping(BasicPolymorphicTypeValidator.builder().allowIfSubType((ctxt, clazz) -> true).build(), DefaultTyping.NON_FINAL)
.build();
// org.apache.fory.logging.LoggerFactory 包别引入错了
// LoggerFactory.useSlf4jLogging(true);
// ForyCodec foryCodec = new ForyCodec();
// CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, foryCodec, foryCodec);
TypedJsonJackson3Codec jsonCodec = new TypedJsonJackson3Codec(Object.class, jsonMapper);
// 组合序列化 key 使用 String 内容使用通用 json 格式
CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, jsonCodec, jsonCodec);
config.setThreads(redissonProperties.getThreads())
.setNettyThreads(redissonProperties.getNettyThreads())
// 缓存 Lua 脚本 减少网络传输(redisson 大部分的功能都是基于 Lua 脚本实现)
.setUseScriptCache(true)
//设置redis key前缀
.setNameMapper(new KeyPrefixHandler(redissonProperties.getKeyPrefix()))
.setCodec(codec);
// netty 对虚拟线程适配有问题 暂时禁止使用
//if (SpringUtils.isVirtual()) {
// config.setNettyExecutor(new VirtualThreadTaskExecutor("redisson-"));
//}
RedissonProperties.SingleServerConfig singleServerConfig = redissonProperties.getSingleServerConfig();
if (ObjectUtil.isNotNull(singleServerConfig)) {
// 使用单机模式
config.useSingleServer()
.setTimeout(singleServerConfig.getTimeout())
.setClientName(singleServerConfig.getClientName())
.setIdleConnectionTimeout(singleServerConfig.getIdleConnectionTimeout())
.setSubscriptionConnectionPoolSize(singleServerConfig.getSubscriptionConnectionPoolSize())
.setConnectionMinimumIdleSize(singleServerConfig.getConnectionMinimumIdleSize())
.setConnectionPoolSize(singleServerConfig.getConnectionPoolSize());
}
// 集群配置方式 参考下方注释
RedissonProperties.ClusterServersConfig clusterServersConfig = redissonProperties.getClusterServersConfig();
if (ObjectUtil.isNotNull(clusterServersConfig)) {
config.useClusterServers()
.setTimeout(clusterServersConfig.getTimeout())
.setClientName(clusterServersConfig.getClientName())
.setIdleConnectionTimeout(clusterServersConfig.getIdleConnectionTimeout())
.setSubscriptionConnectionPoolSize(clusterServersConfig.getSubscriptionConnectionPoolSize())
.setMasterConnectionMinimumIdleSize(clusterServersConfig.getMasterConnectionMinimumIdleSize())
.setMasterConnectionPoolSize(clusterServersConfig.getMasterConnectionPoolSize())
.setSlaveConnectionMinimumIdleSize(clusterServersConfig.getSlaveConnectionMinimumIdleSize())
.setSlaveConnectionPoolSize(clusterServersConfig.getSlaveConnectionPoolSize())
.setReadMode(clusterServersConfig.getReadMode())
.setSubscriptionMode(clusterServersConfig.getSubscriptionMode());
}
log.info("初始化 redis 配置");
};
}
/**
* 异常处理器
*/
@Bean
public RedisExceptionHandler redisExceptionHandler() {
return new RedisExceptionHandler();
}
/**
* redis集群配置 yml
*
* --- # redis 集群配置(单机与集群只能开启一个另一个需要注释掉)
* spring.data:
* redis:
* cluster:
* nodes:
* - 192.168.0.100:6379
* - 192.168.0.101:6379
* - 192.168.0.102:6379
* # 密码
* password:
* # 连接超时时间
* timeout: 10s
* # 是否开启ssl
* ssl.enabled: false
*
* redisson:
* # 线程池数量
* threads: 16
* # Netty线程池数量
* nettyThreads: 32
* # 集群配置
* clusterServersConfig:
* # 客户端名称
* clientName: ${ruoyi.name}
* # master最小空闲连接数
* masterConnectionMinimumIdleSize: 32
* # master连接池大小
* masterConnectionPoolSize: 64
* # slave最小空闲连接数
* slaveConnectionMinimumIdleSize: 32
* # slave连接池大小
* slaveConnectionPoolSize: 64
* # 连接空闲超时,单位:毫秒
* idleConnectionTimeout: 10000
* # 命令等待超时,单位:毫秒
* timeout: 3000
* # 发布和订阅连接池大小
* subscriptionConnectionPoolSize: 50
* # 读取模式
* readMode: "SLAVE"
* # 订阅模式
* subscriptionMode: "MASTER"
*/
}
@@ -0,0 +1,141 @@
package org.dromara.common.redis.config.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.redisson.config.ReadMode;
import org.redisson.config.SubscriptionMode;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Redisson 配置属性
*
* @author Lion Li
*/
@Data
@ConfigurationProperties(prefix = "redisson")
public class RedissonProperties {
/**
* redis缓存key前缀
*/
private String keyPrefix;
/**
* 线程池数量,默认值 = 当前处理核数量 * 2
*/
private int threads = Runtime.getRuntime().availableProcessors() * 2;
/**
* Netty线程池数量,默认值 = 当前处理核数量 * 2
*/
private int nettyThreads = Runtime.getRuntime().availableProcessors() * 2;
/**
* 单机服务配置
*/
private SingleServerConfig singleServerConfig;
/**
* 集群服务配置
*/
private ClusterServersConfig clusterServersConfig;
/**
* Redisson 单机服务配置。
*/
@Data
@NoArgsConstructor
public static class SingleServerConfig {
/**
* 客户端名称
*/
private String clientName;
/**
* 最小空闲连接数
*/
private int connectionMinimumIdleSize;
/**
* 连接池大小
*/
private int connectionPoolSize;
/**
* 连接空闲超时,单位:毫秒
*/
private int idleConnectionTimeout;
/**
* 命令等待超时,单位:毫秒
*/
private int timeout;
/**
* 发布和订阅连接池大小
*/
private int subscriptionConnectionPoolSize;
}
/**
* Redisson 集群服务配置。
*/
@Data
@NoArgsConstructor
public static class ClusterServersConfig {
/**
* 客户端名称
*/
private String clientName;
/**
* master最小空闲连接数
*/
private int masterConnectionMinimumIdleSize;
/**
* master连接池大小
*/
private int masterConnectionPoolSize;
/**
* slave最小空闲连接数
*/
private int slaveConnectionMinimumIdleSize;
/**
* slave连接池大小
*/
private int slaveConnectionPoolSize;
/**
* 连接空闲超时,单位:毫秒
*/
private int idleConnectionTimeout;
/**
* 命令等待超时,单位:毫秒
*/
private int timeout;
/**
* 发布和订阅连接池大小
*/
private int subscriptionConnectionPoolSize;
/**
* 读取模式
*/
private ReadMode readMode;
/**
* 订阅模式
*/
private SubscriptionMode subscriptionMode;
}
}
@@ -0,0 +1,24 @@
package org.dromara.common.redis.enums;
/**
* 限流类型
*
* @author ruoyi
*/
public enum LimitType {
/**
* 默认策略全局限流
*/
DEFAULT,
/**
* 根据请求者IP进行限流
*/
IP,
/**
* 实例限流(集群多后端实例)
*/
CLUSTER
}
@@ -0,0 +1,61 @@
package org.dromara.common.redis.handler;
import org.dromara.common.core.utils.StringUtils;
import org.redisson.config.NameMapper;
/**
* redis缓存key前缀处理
*
* @author ye
* @date 2022/7/14 17:44
* @since 4.3.0
*/
public class KeyPrefixHandler implements NameMapper {
private final String keyPrefix;
/**
* 创建 Redis Key 前缀处理器。
*
* @param keyPrefix Key 前缀
*/
public KeyPrefixHandler(String keyPrefix) {
//前缀为空 则返回空前缀
this.keyPrefix = StringUtils.isBlank(keyPrefix) ? "" : keyPrefix + StringUtils.COLON;
}
/**
* 为原始 Key 增加前缀。
*
* @param name 原始 Key
* @return 带前缀的 Key
*/
@Override
public String map(String name) {
if (StringUtils.isBlank(name)) {
return null;
}
if (StringUtils.isNotBlank(keyPrefix) && !name.startsWith(keyPrefix)) {
return keyPrefix + name;
}
return name;
}
/**
* 去除 Key 前缀。
*
* @param name 带前缀的 Key
* @return 原始 Key
*/
@Override
public String unmap(String name) {
if (StringUtils.isBlank(name)) {
return null;
}
if (StringUtils.isNotBlank(keyPrefix) && name.startsWith(keyPrefix)) {
return name.substring(keyPrefix.length());
}
return name;
}
}
@@ -0,0 +1,34 @@
package org.dromara.common.redis.handler;
import cn.hutool.http.HttpStatus;
import com.baomidou.lock.exception.LockFailureException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* Redis异常处理器
*
* @author AprilWind
*/
@Slf4j
@RestControllerAdvice
public class RedisExceptionHandler {
/**
* 处理 Lock4j 分布式锁获取失败异常。
*
* @param e 异常信息
* @param request 当前请求
* @return 统一失败响应
*/
@ExceptionHandler(LockFailureException.class)
public R<Void> handleLockFailureException(LockFailureException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("获取锁失败了'{}',发生Lock4j异常.", requestURI, e);
return R.fail(HttpStatus.HTTP_UNAVAILABLE, "业务处理中,请稍后再试...");
}
}
@@ -0,0 +1,183 @@
package org.dromara.common.redis.manager;
import org.dromara.common.core.utils.StringUtils;
import org.springframework.cache.Cache;
import java.util.concurrent.Callable;
/**
* Cache 装饰器模式(用于扩展 Caffeine 一级缓存)
*
* @author LionLi
*/
public class CaffeineCacheDecorator implements Cache {
private final String name;
private final Cache cache;
private final com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeine;
/**
* 创建带 Caffeine 一级缓存的缓存装饰器。
*
* @param name 缓存名称
* @param cache 被装饰的缓存实例
* @param caffeine 本地一级缓存实例
*/
public CaffeineCacheDecorator(String name, Cache cache, com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeine) {
this.name = name;
this.cache = cache;
this.caffeine = caffeine;
}
/**
* 获取缓存名称。
*
* @return 缓存名称
*/
@Override
public String getName() {
return name;
}
/**
* 获取底层原生缓存对象。
*
* @return 原生缓存对象
*/
@Override
public Object getNativeCache() {
return cache.getNativeCache();
}
/**
* 构造 Caffeine 一级缓存使用的唯一键。
*
* @param key 缓存键
* @return 唯一键
*/
public String getUniqueKey(Object key) {
return name + StringUtils.COLON + key;
}
/**
* 获取缓存值。
*
* @param key 缓存键
* @return 缓存值包装对象
*/
@Override
public ValueWrapper get(Object key) {
Object o = caffeine.get(getUniqueKey(key), k -> cache.get(key));
return (ValueWrapper) o;
}
/**
* 按指定类型获取缓存值。
*
* @param key 缓存键
* @param type 值类型
* @return 缓存值
*/
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Class<T> type) {
Object o = caffeine.get(getUniqueKey(key), k -> cache.get(key, type));
return (T) o;
}
/**
* 写入缓存值,并清理本地一级缓存。
*
* @param key 缓存键
* @param value 缓存值
*/
@Override
public void put(Object key, Object value) {
caffeine.invalidate(getUniqueKey(key));
cache.put(key, value);
}
/**
* 当键不存在时写入缓存值。
*
* @param key 缓存键
* @param value 缓存值
* @return 原缓存值包装对象
*/
@Override
public ValueWrapper putIfAbsent(Object key, Object value) {
caffeine.invalidate(getUniqueKey(key));
return cache.putIfAbsent(key, value);
}
/**
* 删除缓存值。
*
* @param key 缓存键
*/
@Override
public void evict(Object key) {
evictIfPresent(key);
}
/**
* 删除缓存值并返回是否删除成功。
*
* @param key 缓存键
* @return 是否删除成功
*/
@Override
public boolean evictIfPresent(Object key) {
boolean b = cache.evictIfPresent(key);
if (b) {
caffeine.invalidate(getUniqueKey(key));
}
return b;
}
/**
* 清空缓存。
*/
@Override
public void clear() {
clearLocalCache();
cache.clear();
}
/**
* 使缓存整体失效。
*
* @return 是否失效成功
*/
@Override
public boolean invalidate() {
boolean invalidated = cache.invalidate();
if (invalidated) {
clearLocalCache();
}
return invalidated;
}
/**
* 获取缓存值,不存在时通过回调加载。
*
* @param key 缓存键
* @param valueLoader 回调加载器
* @return 缓存值
*/
@SuppressWarnings("unchecked")
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
Object o = caffeine.get(getUniqueKey(key), k -> cache.get(key, valueLoader));
return (T) o;
}
/**
* 清理当前缓存命名空间下的本地一级缓存。
*/
private void clearLocalCache() {
String prefix = name + StringUtils.COLON;
caffeine.asMap().keySet().removeIf(key -> key instanceof String cacheKey && cacheKey.startsWith(prefix));
}
}
@@ -0,0 +1,305 @@
/**
* Copyright (c) 2013-2021 Nikita Koksharov
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <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 org.dromara.common.redis.manager;
import org.dromara.common.redis.utils.RedisUtils;
import org.redisson.api.RMap;
import org.redisson.api.RMapCache;
import org.redisson.api.map.event.MapEntryListener;
import org.redisson.spring.cache.CacheConfig;
import org.redisson.spring.cache.RedissonCache;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.transaction.TransactionAwareCacheDecorator;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* A {@link org.springframework.cache.CacheManager} implementation
* backed by Redisson instance.
* <p>
* 修改 RedissonSpringCacheManager 源码
* 重写 cacheName 处理方法 支持多参数
*
* @author Nikita Koksharov
*
*/
@SuppressWarnings("unchecked")
public class PlusSpringCacheManager implements CacheManager {
private boolean dynamic = true;
private boolean allowNullValues = true;
private boolean transactionAware = true;
Map<String, CacheConfig> configMap = new ConcurrentHashMap<>();
ConcurrentMap<String, Cache> instanceMap = new ConcurrentHashMap<>();
private final com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeine;
/**
* 创建基于 Redisson 的缓存管理器。
*/
public PlusSpringCacheManager() {
this(null);
}
/**
* 创建基于 Redisson 的缓存管理器。
*
* @param caffeine 本地一级缓存实例
*/
public PlusSpringCacheManager(com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeine) {
this.caffeine = caffeine;
}
/**
* Defines possibility of storing {@code null} values.
* <p>
* Default is <code>true</code>
*
* @param allowNullValues stores if <code>true</code>
*/
public void setAllowNullValues(boolean allowNullValues) {
this.allowNullValues = allowNullValues;
}
/**
* Defines if cache aware of Spring-managed transactions.
* If {@code true} put/evict operations are executed only for successful transaction in after-commit phase.
* <p>
* Default is <code>false</code>
*
* @param transactionAware cache is transaction aware if <code>true</code>
*/
public void setTransactionAware(boolean transactionAware) {
this.transactionAware = transactionAware;
}
/**
* Defines 'fixed' cache names.
* A new cache instance will not be created in dynamic for non-defined names.
* <p>
* `null` parameter setups dynamic mode
*
* @param names of caches
*/
public void setCacheNames(Collection<String> names) {
if (names != null) {
for (String name : names) {
getCache(name);
}
dynamic = false;
} else {
dynamic = true;
}
}
/**
* Set cache config mapped by cache name
*
* @param config object
*/
public void setConfig(Map<String, ? extends CacheConfig> config) {
if (config == null) {
this.configMap = new ConcurrentHashMap<>();
return;
}
this.configMap = new ConcurrentHashMap<>((Map<String, CacheConfig>) config);
}
/**
* 创建默认缓存配置。
*
* @return 默认缓存配置
*/
protected CacheConfig createDefaultConfig() {
return new CacheConfig();
}
/**
* 按缓存名称获取缓存实例,并支持通过扩展参数动态设置 TTL、最大空闲时间和容量。
*
* @param name 缓存名称,支持 `cacheName#ttl#maxIdle#maxSize#local` 格式
* @return 缓存实例
*/
@Override
public Cache getCache(String name) {
String cacheName = name;
// 重写 cacheName 支持多参数
String[] array = StringUtils.delimitedListToStringArray(name, "#");
name = array[0];
Cache cache = instanceMap.get(cacheName);
if (cache != null) {
return cache;
}
if (!dynamic) {
return cache;
}
CacheConfig config = resolveCacheConfig(cacheName, name, array);
int local = resolveLocal(array);
if (config.getMaxIdleTime() == 0 && config.getTTL() == 0 && config.getMaxSize() == 0) {
return createMap(cacheName, name, config, local);
}
return createMapCache(cacheName, name, config, local);
}
/**
* 解析缓存配置,支持从模板配置复制并叠加缓存名称中的扩展参数。
*
* @param cacheName 完整缓存名称
* @param name 基础缓存名称
* @param array 缓存名称拆分参数
* @return 缓存配置
*/
private CacheConfig resolveCacheConfig(String cacheName, String name, String[] array) {
CacheConfig config = configMap.get(cacheName);
if (config != null) {
return config;
}
CacheConfig template = configMap.get(name);
if (template != null) {
config = copyConfig(template);
}
if (config == null) {
config = createDefaultConfig();
}
if (array.length > 1) {
config.setTTL(DurationStyle.detectAndParse(array[1]).toMillis());
}
if (array.length > 2) {
config.setMaxIdleTime(DurationStyle.detectAndParse(array[2]).toMillis());
}
if (array.length > 3) {
config.setMaxSize(Integer.parseInt(array[3]));
}
configMap.put(cacheName, config);
return config;
}
/**
* 解析是否启用本地一级缓存。
*
* @param array 缓存名称拆分参数
* @return 本地缓存开关
*/
private int resolveLocal(String[] array) {
int local = 1;
if (array.length > 4) {
local = Integer.parseInt(array[4]);
}
return local;
}
/**
* 复制 Redisson 缓存配置,避免修改模板配置。
*
* @param source 模板缓存配置
* @return 新缓存配置
*/
private CacheConfig copyConfig(CacheConfig source) {
CacheConfig target = new CacheConfig();
target.setTTL(source.getTTL());
target.setMaxIdleTime(source.getMaxIdleTime());
target.setMaxSize(source.getMaxSize());
target.setEvictionMode(source.getEvictionMode());
for (MapEntryListener listener : source.getListeners()) {
target.addListener(listener);
}
return target;
}
/**
* 创建普通 Map 类型缓存。
*
* @param name 缓存名称
* @param config 缓存配置
* @param local 是否启用本地一级缓存
* @return 缓存实例
*/
private Cache createMap(String cacheName, String name, CacheConfig config, int local) {
RMap<Object, Object> map = RedisUtils.getClient().getMap(name);
Cache cache = new RedissonCache(map, allowNullValues);
if (local == 1 && caffeine != null) {
cache = new CaffeineCacheDecorator(cacheName, cache, caffeine);
}
if (transactionAware) {
cache = new TransactionAwareCacheDecorator(cache);
}
Cache oldCache = instanceMap.putIfAbsent(cacheName, cache);
if (oldCache != null) {
cache = oldCache;
}
return cache;
}
/**
* 创建带过期策略的 MapCache 类型缓存。
*
* @param name 缓存名称
* @param config 缓存配置
* @param local 是否启用本地一级缓存
* @return 缓存实例
*/
private Cache createMapCache(String cacheName, String name, CacheConfig config, int local) {
RMapCache<Object, Object> map = RedisUtils.getClient().getMapCache(name);
Cache cache = new RedissonCache(map, config, allowNullValues);
if (local == 1 && caffeine != null) {
cache = new CaffeineCacheDecorator(cacheName, cache, caffeine);
}
if (transactionAware) {
cache = new TransactionAwareCacheDecorator(cache);
}
Cache oldCache = instanceMap.putIfAbsent(cacheName, cache);
if (oldCache != null) {
cache = oldCache;
} else {
map.setMaxSize(config.getMaxSize());
for (MapEntryListener listener : config.getListeners()) {
map.addListener(listener);
}
}
return cache;
}
/**
* 获取当前缓存名称集合。
*
* @return 缓存名称集合
*/
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(configMap.keySet());
}
}
@@ -0,0 +1,84 @@
package org.dromara.common.redis.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.SpringUtils;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
/**
* 缓存操作工具类
*
* @author Michelle.Chung
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings(value = {"unchecked"})
public class CacheUtils {
/**
* 获取缓存值
*
* @param cacheNames 缓存组名称
* @param key 缓存key
* @return 缓存值
*/
public static <T> T get(String cacheNames, Object key) {
Cache.ValueWrapper wrapper = getRequiredCache(cacheNames).get(key);
return wrapper != null ? (T) wrapper.get() : null;
}
/**
* 保存缓存值
*
* @param cacheNames 缓存组名称
* @param key 缓存key
* @param value 缓存值
*/
public static void put(String cacheNames, Object key, Object value) {
getRequiredCache(cacheNames).put(key, value);
}
/**
* 删除缓存值
*
* @param cacheNames 缓存组名称
* @param key 缓存key
*/
public static void evict(String cacheNames, Object key) {
getRequiredCache(cacheNames).evict(key);
}
/**
* 清空缓存值
*
* @param cacheNames 缓存组名称
*/
public static void clear(String cacheNames) {
getRequiredCache(cacheNames).clear();
}
/**
* 获取必需存在的缓存实例。
*
* @param cacheNames 缓存组名称
* @return 缓存实例
*/
private static Cache getRequiredCache(String cacheNames) {
Cache cache = CacheManagerHolder.CACHE_MANAGER.getCache(cacheNames);
if (cache == null) {
throw new IllegalArgumentException("Cache '" + cacheNames + "' does not exist.");
}
return cache;
}
/**
* 延迟持有 Spring Cache 管理器。
*/
private static class CacheManagerHolder {
/**
* Spring Cache 管理器。
*/
private static final CacheManager CACHE_MANAGER = SpringUtils.getBean(CacheManager.class);
}
}
@@ -0,0 +1,144 @@
package org.dromara.common.redis.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.SpringUtils;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RPriorityBlockingQueue;
import org.redisson.api.RedissonClient;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;
/**
* 分布式队列工具
* 轻量级队列 重量级数据量 请使用 MQ
* 要求 redis 5.X 以上
*
* @author Lion Li
* @version 3.6.0 新增
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class QueueUtils {
private static final RedissonClient CLIENT = SpringUtils.getBean(RedissonClient.class);
/**
* 获取 Redisson 客户端实例。
*
* @return Redisson 客户端
*/
public static RedissonClient getClient() {
return CLIENT;
}
/**
* 添加普通队列数据
*
* @param queueName 队列名
* @param data 数据
* @return 是否添加成功
*/
public static <T> boolean addQueueObject(String queueName, T data) {
RBlockingQueue<T> queue = CLIENT.getBlockingQueue(queueName);
return queue.offer(data);
}
/**
* 通用获取一个队列数据 没有数据返回 null(不支持延迟队列)
*
* @param queueName 队列名
* @return 队列数据
*/
public static <T> T getQueueObject(String queueName) {
RBlockingQueue<T> queue = CLIENT.getBlockingQueue(queueName);
return queue.poll();
}
/**
* 删除普通队列中的指定数据。
*
* @param queueName 队列名
* @param data 数据
* @return 是否删除成功
*/
public static <T> boolean removeQueueObject(String queueName, T data) {
RBlockingQueue<T> queue = CLIENT.getBlockingQueue(queueName);
return queue.remove(data);
}
/**
* 销毁普通队列。
*
* @param queueName 队列名
* @return 是否销毁成功
*/
public static <T> boolean destroyQueue(String queueName) {
RBlockingQueue<T> queue = CLIENT.getBlockingQueue(queueName);
return queue.delete();
}
/**
* 添加优先队列数据
*
* @param queueName 队列名
* @param data 数据
* @return 是否添加成功
*/
public static <T> boolean addPriorityQueueObject(String queueName, T data) {
RPriorityBlockingQueue<T> priorityBlockingQueue = CLIENT.getPriorityBlockingQueue(queueName);
return priorityBlockingQueue.offer(data);
}
/**
* 优先队列获取一个队列数据 没有数据返回 null(不支持延迟队列)
*
* @param queueName 队列名
* @return 队列数据
*/
public static <T> T getPriorityQueueObject(String queueName) {
RPriorityBlockingQueue<T> queue = CLIENT.getPriorityBlockingQueue(queueName);
return queue.poll();
}
/**
* 删除优先队列中的指定数据。
*
* @param queueName 队列名
* @param data 数据
* @return 是否删除成功
*/
public static <T> boolean removePriorityQueueObject(String queueName, T data) {
RPriorityBlockingQueue<T> queue = CLIENT.getPriorityBlockingQueue(queueName);
return queue.remove(data);
}
/**
* 销毁优先队列。
*
* @param queueName 队列名
* @return 是否销毁成功
*/
public static <T> boolean destroyPriorityQueue(String queueName) {
RPriorityBlockingQueue<T> queue = CLIENT.getPriorityBlockingQueue(queueName);
return queue.delete();
}
/**
* 订阅阻塞队列元素。
*
* @param queueName 队列名
* @param consumer 消费逻辑
*/
public static <T> void subscribeBlockingQueue(String queueName, Function<T, CompletionStage<Void>> consumer) {
RBlockingQueue<T> queue = CLIENT.getBlockingQueue(queueName);
// 延迟队列已经被redisson官方废弃不建议使用
// if (isDelayed) {
// // 订阅延迟队列
// CLIENT.getDelayedQueue(queue);
// }
queue.subscribeOnElements(consumer);
}
}
@@ -0,0 +1,614 @@
package org.dromara.common.redis.utils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.SpringUtils;
import org.redisson.api.*;
import org.redisson.api.options.KeysScanOptions;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* redis 工具类
*
* @author Lion Li
* @version 3.1.0 新增
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings(value = {"rawtypes"})
public class RedisUtils {
private static final RedissonClient CLIENT = SpringUtils.getBean(RedissonClient.class);
/**
* 限流
*
* @param key 限流key
* @param rateType 限流类型
* @param rate 速率
* @param rateInterval 速率间隔
* @return -1 表示失败
*/
public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval) {
return rateLimiter(key, rateType, rate, rateInterval, 0);
}
/**
* 限流
*
* @param key 限流key
* @param rateType 限流类型
* @param rate 速率
* @param rateInterval 速率间隔
* @param timeout 超时时间
* @return -1 表示失败
*/
public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval, int timeout) {
RRateLimiter rateLimiter = CLIENT.getRateLimiter(key);
rateLimiter.trySetRate(rateType, rate, Duration.ofSeconds(rateInterval), Duration.ofSeconds(timeout));
if (rateLimiter.tryAcquire()) {
return rateLimiter.availablePermits();
} else {
return -1L;
}
}
/**
* 获取客户端实例
*/
public static RedissonClient getClient() {
return CLIENT;
}
/**
* 发布通道消息
*
* @param channelKey 通道key
* @param msg 发送数据
* @param consumer 自定义处理
*/
public static <T> void publish(String channelKey, T msg, Consumer<T> consumer) {
RTopic topic = CLIENT.getTopic(channelKey);
topic.publish(msg);
if (consumer != null) {
consumer.accept(msg);
}
}
/**
* 发布消息到指定的频道
*
* @param channelKey 通道key
* @param msg 发送数据
*/
public static <T> void publish(String channelKey, T msg) {
RTopic topic = CLIENT.getTopic(channelKey);
topic.publish(msg);
}
/**
* 订阅通道接收消息
*
* @param channelKey 通道key
* @param clazz 消息类型
* @param consumer 自定义处理
*/
public static <T> void subscribe(String channelKey, Class<T> clazz, Consumer<T> consumer) {
subscribeAndGetListenerId(channelKey, clazz, consumer);
}
/**
* 订阅通道接收消息,并返回监听器 ID。
*
* @param channelKey 通道key
* @param clazz 消息类型
* @param consumer 自定义处理
* @return 监听器 ID,可用于取消订阅
*/
public static <T> int subscribeAndGetListenerId(String channelKey, Class<T> clazz, Consumer<T> consumer) {
RTopic topic = CLIENT.getTopic(channelKey);
return topic.addListener(clazz, (channel, msg) -> consumer.accept(msg));
}
/**
* 取消通道订阅。
*
* @param channelKey 通道key
* @param listenerId 监听器 ID
*/
public static void unsubscribe(String channelKey, int listenerId) {
RTopic topic = CLIENT.getTopic(channelKey);
topic.removeListener(listenerId);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public static <T> void setCacheObject(final String key, final T value) {
setCacheObject(key, value, false);
}
/**
* 缓存基本的对象,保留当前对象 TTL 有效期
*
* @param key 缓存的键值
* @param value 缓存的值
* @param isSaveTtl 是否保留TTL有效期(例如: set之前ttl剩余90 set之后还是为90)
* @since Redis 6.X 以上使用 setAndKeepTTL 兼容 5.X 方案
*/
public static <T> void setCacheObject(final String key, final T value, final boolean isSaveTtl) {
RBucket<T> bucket = CLIENT.getBucket(key);
if (isSaveTtl) {
try {
bucket.setAndKeepTTL(value);
} catch (Exception e) {
long timeToLive = bucket.remainTimeToLive();
if (timeToLive <= 0) {
bucket.set(value);
} else {
bucket.set(value, Duration.ofMillis(timeToLive));
}
}
} else {
bucket.set(value);
}
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param duration 时间
*/
public static <T> void setCacheObject(final String key, final T value, final Duration duration) {
RBucket<T> bucket = CLIENT.getBucket(key);
bucket.set(value, duration);
}
/**
* 如果不存在则设置 并返回 true 如果存在则返回 false
*
* @param key 缓存的键值
* @param value 缓存的值
* @return set成功或失败
*/
public static <T> boolean setObjectIfAbsent(final String key, final T value, final Duration duration) {
RBucket<T> bucket = CLIENT.getBucket(key);
return bucket.setIfAbsent(value, duration);
}
/**
* 如果存在则设置 并返回 true 如果存在则返回 false
*
* @param key 缓存的键值
* @param value 缓存的值
* @return set成功或失败
*/
public static <T> boolean setObjectIfExists(final String key, final T value, final Duration duration) {
RBucket<T> bucket = CLIENT.getBucket(key);
return bucket.setIfExists(value, duration);
}
/**
* 注册对象监听器
* <p>
* key 监听器需开启 `notify-keyspace-events` 等 redis 相关配置
*
* @param key 缓存的键值
* @param listener 监听器配置
*/
public static <T> void addObjectListener(final String key, final ObjectListener listener) {
RBucket<T> result = CLIENT.getBucket(key);
result.addListener(listener);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public static boolean expire(final String key, final long timeout) {
return expire(key, Duration.ofSeconds(timeout));
}
/**
* 设置有效时间
*
* @param key Redis键
* @param duration 超时时间
* @return true=设置成功;false=设置失败
*/
public static boolean expire(final String key, final Duration duration) {
RBucket rBucket = CLIENT.getBucket(key);
return rBucket.expire(duration);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public static <T> T getCacheObject(final String key) {
RBucket<T> rBucket = CLIENT.getBucket(key);
return rBucket.get();
}
/**
* 获得key剩余存活时间
*
* @param key 缓存键值
* @return 剩余存活时间
*/
public static <T> long getTimeToLive(final String key) {
RBucket<T> rBucket = CLIENT.getBucket(key);
return rBucket.remainTimeToLive();
}
/**
* 删除单个对象
*
* @param key 缓存的键值
*/
public static boolean deleteObject(final String key) {
if (key == null) {
return false;
}
return CLIENT.getBucket(key).delete();
}
/**
* 删除集合对象
*
* @param collection 多个对象
*/
public static void deleteObject(final Collection<?> collection) {
if (collection == null || collection.isEmpty()) {
return;
}
RBatch batch = CLIENT.createBatch();
collection.forEach(t -> batch.getBucket(t.toString()).deleteAsync());
batch.execute();
}
/**
* 检查缓存对象是否存在
*
* @param key 缓存的键值
*/
public static boolean isExistsObject(final String key) {
return CLIENT.getBucket(key).isExists();
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public static <T> boolean setCacheList(final String key, final List<T> dataList) {
RList<T> rList = CLIENT.getList(key);
return rList.addAll(dataList);
}
/**
* 追加缓存List数据
*
* @param key 缓存的键值
* @param data 待缓存的数据
* @return 缓存的对象
*/
public static <T> boolean addCacheList(final String key, final T data) {
RList<T> rList = CLIENT.getList(key);
return rList.add(data);
}
/**
* 注册List监听器
* <p>
* key 监听器需开启 `notify-keyspace-events` 等 redis 相关配置
*
* @param key 缓存的键值
* @param listener 监听器配置
*/
public static <T> void addListListener(final String key, final ObjectListener listener) {
RList<T> rList = CLIENT.getList(key);
rList.addListener(listener);
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public static <T> List<T> getCacheList(final String key) {
RList<T> rList = CLIENT.getList(key);
return rList.readAll();
}
/**
* 获得缓存的list对象(范围)
*
* @param key 缓存的键值
* @param form 起始下标
* @param to 截止下标
* @return 缓存键值对应的数据
*/
public static <T> List<T> getCacheListRange(final String key, int form, int to) {
RList<T> rList = CLIENT.getList(key);
return rList.range(form, to);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public static <T> boolean setCacheSet(final String key, final Set<T> dataSet) {
RSet<T> rSet = CLIENT.getSet(key);
return rSet.addAll(dataSet);
}
/**
* 追加缓存Set数据
*
* @param key 缓存的键值
* @param data 待缓存的数据
* @return 缓存的对象
*/
public static <T> boolean addCacheSet(final String key, final T data) {
RSet<T> rSet = CLIENT.getSet(key);
return rSet.add(data);
}
/**
* 注册Set监听器
* <p>
* key 监听器需开启 `notify-keyspace-events` 等 redis 相关配置
*
* @param key 缓存的键值
* @param listener 监听器配置
*/
public static <T> void addSetListener(final String key, final ObjectListener listener) {
RSet<T> rSet = CLIENT.getSet(key);
rSet.addListener(listener);
}
/**
* 获得缓存的set
*
* @param key 缓存的key
* @return set对象
*/
public static <T> Set<T> getCacheSet(final String key) {
RSet<T> rSet = CLIENT.getSet(key);
return rSet.readAll();
}
/**
* 缓存Map
*
* @param key 缓存的键值
* @param dataMap 缓存的数据
*/
public static <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
if (dataMap != null) {
RMap<String, T> rMap = CLIENT.getMap(key);
rMap.putAll(dataMap);
}
}
/**
* 注册Map监听器
* <p>
* key 监听器需开启 `notify-keyspace-events` 等 redis 相关配置
*
* @param key 缓存的键值
* @param listener 监听器配置
*/
public static <T> void addMapListener(final String key, final ObjectListener listener) {
RMap<String, T> rMap = CLIENT.getMap(key);
rMap.addListener(listener);
}
/**
* 获得缓存的Map
*
* @param key 缓存的键值
* @return map对象
*/
public static <T> Map<String, T> getCacheMap(final String key) {
RMap<String, T> rMap = CLIENT.getMap(key);
return rMap.readAllMap();
}
/**
* 获得缓存Map的key列表
*
* @param key 缓存的键值
* @return key列表
*/
public static <T> Set<String> getCacheMapKeySet(final String key) {
RMap<String, T> rMap = CLIENT.getMap(key);
return rMap.keySet();
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public static <T> void setCacheMapValue(final String key, final String hKey, final T value) {
RMap<String, T> rMap = CLIENT.getMap(key);
rMap.put(hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public static <T> T getCacheMapValue(final String key, final String hKey) {
RMap<String, T> rMap = CLIENT.getMap(key);
return rMap.get(hKey);
}
/**
* 删除Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public static <T> T delCacheMapValue(final String key, final String hKey) {
RMap<String, T> rMap = CLIENT.getMap(key);
return rMap.remove(hKey);
}
/**
* 删除Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键
*/
public static <T> void delMultiCacheMapValue(final String key, final Set<String> hKeys) {
RBatch batch = CLIENT.createBatch();
RMapAsync<String, T> rMap = batch.getMap(key);
for (String hKey : hKeys) {
rMap.removeAsync(hKey);
}
batch.execute();
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public static <K, V> Map<K, V> getMultiCacheMapValue(final String key, final Set<K> hKeys) {
RMap<K, V> rMap = CLIENT.getMap(key);
return rMap.getAll(hKeys);
}
/**
* 设置原子值
*
* @param key Redis键
* @param value 值
*/
public static void setAtomicValue(String key, long value) {
RAtomicLong atomic = CLIENT.getAtomicLong(key);
atomic.set(value);
}
/**
* 获取原子值
*
* @param key Redis键
* @return 当前值
*/
public static long getAtomicValue(String key) {
RAtomicLong atomic = CLIENT.getAtomicLong(key);
return atomic.get();
}
/**
* 递增原子值
*
* @param key Redis键
* @return 当前值
*/
public static long incrAtomicValue(String key) {
RAtomicLong atomic = CLIENT.getAtomicLong(key);
return atomic.incrementAndGet();
}
/**
* 递减原子值
*
* @param key Redis键
* @return 当前值
*/
public static long decrAtomicValue(String key) {
RAtomicLong atomic = CLIENT.getAtomicLong(key);
return atomic.decrementAndGet();
}
/**
* 获得缓存的基本对象列表(全局匹配忽略租户 自行拼接租户id)
* <p>
* limit-设置扫描的限制数量(默认为0,查询全部)
* pattern-设置键的匹配模式(默认为null)
* chunkSize-设置每次扫描的块大小(默认为0,本方法设置为1000)
* type-设置键的类型(默认为null,查询全部类型)
* </P>
*
* @param pattern 字符串前缀
* @return 对象列表
* @see KeysScanOptions
*/
public static Collection<String> keys(final String pattern) {
return keys(KeysScanOptions.defaults().pattern(pattern).chunkSize(1000));
}
/**
* 通过扫描参数获取缓存的基本对象列表
*
* @param keysScanOptions 扫描参数
* <p>
* limit-设置扫描的限制数量(默认为0,查询全部)
* pattern-设置键的匹配模式(默认为null)
* chunkSize-设置每次扫描的块大小(默认为0)
* type-设置键的类型(默认为null,查询全部类型)
* </P>
* @return 对象列表
* @see KeysScanOptions
*/
public static Collection<String> keys(final KeysScanOptions keysScanOptions) {
Stream<String> keysStream = CLIENT.getKeys().getKeysStream(keysScanOptions);
return keysStream.collect(Collectors.toList());
}
/**
* 删除缓存的基本对象列表(全局匹配忽略租户 自行拼接租户id)
*
* @param pattern 字符串前缀
*/
public static void deleteKeys(final String pattern) {
CLIENT.getKeys().deleteByPattern(pattern);
}
/**
* 检查redis中是否存在key
*
* @param key 键
* @return 是否存在
*/
public static Boolean hasKey(String key) {
RKeys rKeys = CLIENT.getKeys();
return rKeys.countExists(key) > 0;
}
}
@@ -0,0 +1,351 @@
package org.dromara.common.redis.utils;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DatePattern;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.redisson.api.RIdGenerator;
import org.redisson.api.RedissonClient;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAccessor;
/**
* 发号器工具类
*
* @author 秋辞未寒
* @date 2024-12-10
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SequenceUtils {
/**
* 默认初始值
*/
public static final long DEFAULT_INIT_VALUE = 1L;
/**
* 默认步长
*/
public static final long DEFAULT_STEP_VALUE = 1L;
/**
* 默认过期时间-天
*/
public static final Duration DEFAULT_EXPIRE_TIME_DAY = Duration.ofDays(1);
/**
* 默认过期时间-分钟
*/
public static final Duration DEFAULT_EXPIRE_TIME_MINUTE = Duration.ofMinutes(1);
/**
* 默认最小ID容量位数 - 6位数(即至少可以生成的ID为999999个)
*/
public static final int DEFAULT_MIN_ID_CAPACITY_BITS = 6;
/**
* 获取Redisson客户端实例
*/
private static final RedissonClient REDISSON_CLIENT = SpringUtils.getBean(RedissonClient.class);
/**
* 获取ID生成器
*
* @param key 业务key
* @param expireTime 过期时间
* @param initValue ID初始值
* @param stepValue ID步长
* @return ID生成器
*/
public static RIdGenerator getIdGenerator(String key, Duration expireTime, long initValue, long stepValue) {
RIdGenerator idGenerator = REDISSON_CLIENT.getIdGenerator(key);
// 初始值和步长不能小于等于0
initValue = initValue <= 0 ? DEFAULT_INIT_VALUE : initValue;
stepValue = stepValue <= 0 ? DEFAULT_STEP_VALUE : stepValue;
// 设置初始值和步长
idGenerator.tryInit(initValue, stepValue);
// 设置过期时间
idGenerator.expire(expireTime);
return idGenerator;
}
/**
* 获取ID生成器
*
* @param key 业务key
* @param expireTime 过期时间
* @return ID生成器
*/
public static RIdGenerator getIdGenerator(String key, Duration expireTime) {
return getIdGenerator(key, expireTime, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
}
/**
* 获取指定业务key的唯一id
*
* @param key 业务key
* @param expireTime 过期时间
* @param initValue ID初始值
* @param stepValue ID步长
* @return 唯一id
*/
public static long getNextId(String key, Duration expireTime, long initValue, long stepValue) {
return getIdGenerator(key, expireTime, initValue, stepValue).nextId();
}
/**
* 获取指定业务key的唯一id (ID初始值=1,ID步长=1)
*
* @param key 业务key
* @param expireTime 过期时间
* @return 唯一id
*/
public static long getNextId(String key, Duration expireTime) {
return getIdGenerator(key, expireTime).nextId();
}
/**
* 获取指定业务key的唯一id字符串
*
* @param key 业务key
* @param expireTime 过期时间
* @param initValue ID初始值
* @param stepValue ID步长
* @return 唯一id
*/
public static String getNextIdString(String key, Duration expireTime, long initValue, long stepValue) {
return Convert.toStr(getNextId(key, expireTime, initValue, stepValue));
}
/**
* 获取指定业务key的唯一id字符串 (ID初始值=1,ID步长=1)
*
* @param key 业务key
* @param expireTime 过期时间
* @return 唯一id
*/
public static String getNextIdString(String key, Duration expireTime) {
return Convert.toStr(getNextId(key, expireTime));
}
/**
* 获取指定业务key的唯一id字符串 (ID初始值=1,ID步长=1),不足位数自动补零
*
* @param key 业务key
* @param expireTime 过期时间
* @param width 位数,不足左补0
* @return 补零后的唯一id字符串
*/
public static String getPaddedNextIdString(String key, Duration expireTime, Integer width) {
return StringUtils.leftPad(getNextIdString(key, expireTime), width, '0');
}
/**
* 获取 yyyyMMdd 格式的唯一id
*
* @return 唯一id
* @deprecated 请使用 {@link #getDateId(String)} 或 {@link #getDateId(String, boolean)}、{@link #getDateId(String, boolean, int)},确保不同业务的ID连续性
*/
@Deprecated
public static String getDateId() {
return getDateId("");
}
/**
* 获取 prefix + yyyyMMdd 格式的唯一id
*
* @param prefix 业务前缀
* @return 唯一id
*/
public static String getDateId(String prefix) {
return getDateId(prefix, true);
}
/**
* 获取 prefix + yyyyMMdd 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @return 唯一id
*/
public static String getDateId(String prefix, boolean isWithPrefix) {
return getDateId(prefix, isWithPrefix, -1);
}
/**
* 获取 prefix + yyyyMMdd 格式的唯一id (启用ID补位,补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})}
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @return 唯一id
*/
public static String getPaddedDateId(String prefix, boolean isWithPrefix) {
return getDateId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS);
}
/**
* 获取 prefix + yyyyMMdd 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
* @return 唯一id
*/
public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits) {
return getDateId(prefix, isWithPrefix, minIdCapacityBits, LocalDate.now());
}
/**
* 获取 prefix + yyyyMMdd 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
* @param time 时间
* @return 唯一id
*/
public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time) {
return getDateId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
}
/**
* 获取 prefix + yyyyMMdd 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
* @param time 时间
* @param initValue ID初始值
* @param stepValue ID步长
* @return 唯一id
*/
public static String getDateId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDate time, long initValue, long stepValue) {
return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATE_FORMATTER, DEFAULT_EXPIRE_TIME_DAY, initValue, stepValue);
}
/**
* 获取 yyyyMMddHHmmss 格式的唯一id
*
* @return 唯一id
* @deprecated 请使用 {@link #getDateTimeId(String)} 或 {@link #getDateTimeId(String, boolean)}、{@link #getDateTimeId(String, boolean, int)},确保不同业务的ID连续性
*/
@Deprecated
public static String getDateTimeId() {
return getDateTimeId("", false);
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id
*
* @param prefix 业务前缀
* @return 唯一id
*/
public static String getDateTimeId(String prefix) {
return getDateTimeId(prefix, true);
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @return 唯一id
*/
public static String getDateTimeId(String prefix, boolean isWithPrefix) {
return getDateTimeId(prefix, isWithPrefix, -1);
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id (启用ID补位,补位长度 = {@link #DEFAULT_MIN_ID_CAPACITY_BITS})}
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @return 唯一id
*/
public static String getPaddedDateTimeId(String prefix, boolean isWithPrefix) {
return getDateTimeId(prefix, isWithPrefix, DEFAULT_MIN_ID_CAPACITY_BITS);
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
* @return 唯一id
*/
public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits) {
return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, LocalDateTime.now());
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
* @param time 时间
* @return 唯一id
*/
public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time) {
return getDateTimeId(prefix, isWithPrefix, minIdCapacityBits, time, DEFAULT_INIT_VALUE, DEFAULT_STEP_VALUE);
}
/**
* 获取 prefix + yyyyMMddHHmmss 格式的唯一id
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
* @param initValue ID初始值
* @param stepValue ID步长
* @return 唯一id
*/
public static String getDateTimeId(String prefix, boolean isWithPrefix, int minIdCapacityBits, LocalDateTime time, long initValue, long stepValue) {
return getDatePatternId(prefix, isWithPrefix, minIdCapacityBits, time, DatePattern.PURE_DATETIME_FORMATTER, DEFAULT_EXPIRE_TIME_MINUTE, initValue, stepValue);
}
/**
* 获取指定业务key的指定时间格式的ID
*
* @param prefix 业务前缀
* @param isWithPrefix id是否携带业务前缀
* @param minIdCapacityBits 最小ID容量位数,小于该位数的ID,左补0(小于等于0表示不启用补位)
* @param temporalAccessor 时间访问器
* @param timeFormatter 时间格式
* @param expireTime 过期时间
* @param initValue ID初始值
* @param stepValue ID步长
* @return 唯一id
*/
private static String getDatePatternId(String prefix, boolean isWithPrefix, int minIdCapacityBits, TemporalAccessor temporalAccessor, DateTimeFormatter timeFormatter, Duration expireTime, long initValue, long stepValue) {
// 时间前缀
String timePrefix = timeFormatter.format(temporalAccessor);
// 业务前缀 + 时间前缀 构建 prefixKey
String prefixKey = StringUtils.format("{}{}", StringUtils.blankToDefault(prefix, ""), timePrefix);
// 获取id,例 -> 1
String nextId = getNextIdString(prefixKey, expireTime, initValue, stepValue);
// minIdCapacityBits 大于0,且 nextId 的长度小于 minIdCapacityBits,则左补0
if (minIdCapacityBits > 0 && nextId.length() < minIdCapacityBits) {
nextId = StringUtils.leftPad(nextId, minIdCapacityBits, '0');
}
// 是否携带业务前缀
if (isWithPrefix) {
// 例 -> P202507031
// 其中 P 为业务前缀,202507031 为 yyyyMMdd 格式时间, 1 为nextId
return StringUtils.format("{}{}", prefixKey, nextId);
}
// 例 -> 202507031
// 其中 202507031 为 yyyyMMdd 格式时间, 1 为nextId
return StringUtils.format("{}{}", timePrefix, nextId);
}
}
@@ -0,0 +1,5 @@
org.dromara.common.redis.config.RedisConfig
org.dromara.common.redis.config.CacheConfig
org.dromara.common.redis.config.IdempotentConfig
org.dromara.common.redis.config.RateLimiterConfig
org.dromara.common.redis.config.Lock4jConfig
@@ -0,0 +1,7 @@
{
"org.dromara.common.ratelimiter.annotation.RateLimiter@key": {
"method": {
"parameters": true
}
}
}