基于Redis的Spring Boot 幂等性插件模块封装

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,经济版 1GB 1个月
简介: [基于Redis支持集群、哨兵等模式]

幂等性注解定义

importjava.lang.annotation.*;
importjava.util.concurrent.TimeUnit;
/*** @author jeckxu*/@Inherited@Target(ElementType.METHOD)
@Retention(value=RetentionPolicy.RUNTIME)
public@interfaceIdempotent {
/*** 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数* @return Spring-EL expression*/Stringkey() default"";
/*** 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来* @return expireTime*/intexpireTime() default1;
/*** 时间单位 默认:s* @return TimeUnit*/TimeUnittimeUnit() defaultTimeUnit.SECONDS;
/*** 提示信息,可自定义* @return String*/Stringinfo() default"ROOT.IDEMPOTENT.DO_NOT_REPEAT_THE_OPERATION";
/*** 是否在业务完成后删除key true:删除 false:不删除* @return boolean*/booleandelKey() defaulttrue;
}

幂等性配置类

importorg.springframework.boot.context.properties.ConfigurationProperties;
/*** @author Jeckxu*/@ConfigurationProperties(MagicalIdempotentProperties.PREFIX)
publicclassMagicalIdempotentProperties {
publicstaticfinalStringPREFIX="magical.idempotent";
/*** 是否开启:默认为:false,便于生成配置提示。*/privateBooleanenabled=Boolean.FALSE;
/*** 单机配置:redis 服务地址*/privateStringaddress="redis://127.0.0.1:6379";
/*** 密码配置*/privateStringpassword;
/*** db*/privateIntegerdatabase=0;
/*** 连接池大小*/privateIntegerpoolSize=20;
/*** 最小空闲连接数*/privateIntegeridleSize=5;
/*** 连接空闲超时,单位:毫秒*/privateIntegeridleTimeout=60000;
/*** 连接超时,单位:毫秒*/privateIntegerconnectionTimeout=3000;
/*** 命令等待超时,单位:毫秒*/privateIntegertimeout=10000;
/*** 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster*/privateModemode=Mode.single;
/*** 主从模式,主地址*/privateStringmasterAddress;
/*** 主从模式,从地址*/privateString[] slaveAddress;
/*** 哨兵模式:主名称*/privateStringmasterName;
/*** 哨兵模式地址*/privateString[] sentinelAddress;
/*** 集群模式节点地址*/privateString[] nodeAddress;
publicenumMode {
/*** 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster*/single, master, sentinel, cluster;
    }
/*** 是否开启:默认为:false,便于生成配置提示。*/publicBooleangetEnabled() {
returnthis.enabled;
    }
/*** 单机配置:redis 服务地址*/publicStringgetAddress() {
returnthis.address;
    }
/*** 密码配置*/publicStringgetPassword() {
returnthis.password;
    }
/*** db*/publicIntegergetDatabase() {
returnthis.database;
    }
/*** 连接池大小*/publicIntegergetPoolSize() {
returnthis.poolSize;
    }
/*** 最小空闲连接数*/publicIntegergetIdleSize() {
returnthis.idleSize;
    }
/*** 连接空闲超时,单位:毫秒*/publicIntegergetIdleTimeout() {
returnthis.idleTimeout;
    }
/*** 连接超时,单位:毫秒*/publicIntegergetConnectionTimeout() {
returnthis.connectionTimeout;
    }
/*** 命令等待超时,单位:毫秒*/publicIntegergetTimeout() {
returnthis.timeout;
    }
/*** 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster*/publicModegetMode() {
returnthis.mode;
    }
/*** 主从模式,主地址*/publicStringgetMasterAddress() {
returnthis.masterAddress;
    }
/*** 主从模式,从地址*/publicString[] getSlaveAddress() {
returnthis.slaveAddress;
    }
/*** 哨兵模式:主名称*/publicStringgetMasterName() {
returnthis.masterName;
    }
/*** 哨兵模式地址*/publicString[] getSentinelAddress() {
returnthis.sentinelAddress;
    }
/*** 集群模式节点地址*/publicString[] getNodeAddress() {
returnthis.nodeAddress;
    }
/*** 是否开启:默认为:false,便于生成配置提示。*/publicvoidsetEnabled(finalBooleanenabled) {
this.enabled=enabled;
    }
/*** 单机配置:redis 服务地址*/publicvoidsetAddress(finalStringaddress) {
this.address=address;
    }
/*** 密码配置*/publicvoidsetPassword(finalStringpassword) {
this.password=password;
    }
/*** db*/publicvoidsetDatabase(finalIntegerdatabase) {
this.database=database;
    }
/*** 连接池大小*/publicvoidsetPoolSize(finalIntegerpoolSize) {
this.poolSize=poolSize;
    }
/*** 最小空闲连接数*/publicvoidsetIdleSize(finalIntegeridleSize) {
this.idleSize=idleSize;
    }
/*** 连接空闲超时,单位:毫秒*/publicvoidsetIdleTimeout(finalIntegeridleTimeout) {
this.idleTimeout=idleTimeout;
    }
/*** 连接超时,单位:毫秒*/publicvoidsetConnectionTimeout(finalIntegerconnectionTimeout) {
this.connectionTimeout=connectionTimeout;
    }
/*** 命令等待超时,单位:毫秒*/publicvoidsetTimeout(finalIntegertimeout) {
this.timeout=timeout;
    }
/*** 集群模式,单机:single,主从:master,哨兵模式:sentinel,集群模式:cluster*/publicvoidsetMode(finalModemode) {
this.mode=mode;
    }
/*** 主从模式,主地址*/publicvoidsetMasterAddress(finalStringmasterAddress) {
this.masterAddress=masterAddress;
    }
/*** 主从模式,从地址*/publicvoidsetSlaveAddress(finalString[] slaveAddress) {
this.slaveAddress=slaveAddress;
    }
/*** 哨兵模式:主名称*/publicvoidsetMasterName(finalStringmasterName) {
this.masterName=masterName;
    }
/*** 哨兵模式地址*/publicvoidsetSentinelAddress(finalString[] sentinelAddress) {
this.sentinelAddress=sentinelAddress;
    }
/*** 集群模式节点地址*/publicvoidsetNodeAddress(finalString[] nodeAddress) {
this.nodeAddress=nodeAddress;
    }
}

幂等性切面类

importorg.aspectj.lang.JoinPoint;
importorg.aspectj.lang.annotation.After;
importorg.aspectj.lang.annotation.Aspect;
importorg.aspectj.lang.annotation.Before;
importorg.aspectj.lang.annotation.Pointcut;
importorg.aspectj.lang.reflect.MethodSignature;
importorg.jeckxu.magical.core.idempotent.annotation.Idempotent;
importorg.jeckxu.magical.core.idempotent.exception.IdempotentException;
importorg.jeckxu.magical.core.idempotent.expression.KeyResolver;
importorg.redisson.api.RMapCache;
importorg.redisson.api.RedissonClient;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.springframework.util.CollectionUtils;
importorg.springframework.util.StringUtils;
importorg.springframework.web.context.request.RequestContextHolder;
importorg.springframework.web.context.request.ServletRequestAttributes;
importjavax.servlet.http.HttpServletRequest;
importjava.lang.reflect.Method;
importjava.time.LocalDateTime;
importjava.util.Arrays;
importjava.util.HashMap;
importjava.util.Map;
importjava.util.concurrent.TimeUnit;
/*** @author jeckxu*/@AspectpublicclassIdempotentAspect {
privatestaticfinalLoggerLOGGER=LoggerFactory.getLogger(IdempotentAspect.class);
privateThreadLocal<Map<String, Object>>threadLocal=newThreadLocal();
privatestaticfinalStringRMAPCACHE_KEY="magical:idempotent";
privatestaticfinalStringKEY="key";
privatestaticfinalStringDELKEY="delKey";
privatefinalRedissonClientredissonClient;
privatefinalKeyResolverkeyResolver;
@Pointcut("@annotation(org.jeckxu.magical.core.idempotent.annotation.Idempotent)")
publicvoidpointCut() {
    }
@Before("pointCut()")
publicvoidbeforePointCut(JoinPointjoinPoint) throwsException {
ServletRequestAttributesrequestAttributes= (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequestrequest=requestAttributes.getRequest();
MethodSignaturesignature= (MethodSignature) joinPoint.getSignature();
Methodmethod=signature.getMethod();
if (!method.isAnnotationPresent(Idempotent.class)) {
return;
        }
Idempotentidempotent=method.getAnnotation(Idempotent.class);
Stringkey;
// 若没有配置 幂等 标识编号,则使用 url + 参数列表作为区分if (StringUtils.isEmpty(idempotent.key())) {
Stringurl=request.getRequestURL().toString();
StringargString=Arrays.asList(joinPoint.getArgs()).toString();
key=url+argString;
        } else {
// 使用jstl 规则区分key=keyResolver.resolver(idempotent, joinPoint);
        }
longexpireTime=idempotent.expireTime();
Stringinfo=idempotent.info();
TimeUnittimeUnit=idempotent.timeUnit();
booleandelKey=idempotent.delKey();
// do not need check nullRMapCache<String, Object>rMapCache=redissonClient.getMapCache(RMAPCACHE_KEY);
Stringvalue=LocalDateTime.now().toString().replace("T", " ");
Objectv1;
if (null!=rMapCache.get(key)) {
// had storedthrownewIdempotentException(info);
        }
synchronized (this) {
v1=rMapCache.putIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
if (null!=v1) {
thrownewIdempotentException(info);
            } else {
LOGGER.info("[idempotent]:has stored key={},value={},expireTime={}{},now={}", key, value, expireTime, timeUnit, LocalDateTime.now().toString());
            }
        }
Map<String, Object>map=CollectionUtils.isEmpty(threadLocal.get()) ?newHashMap<>(4) : threadLocal.get();
map.put(KEY, key);
map.put(DELKEY, delKey);
threadLocal.set(map);
    }
@After("pointCut()")
publicvoidafterPointCut(JoinPointjoinPoint) {
Map<String, Object>map=threadLocal.get();
if (CollectionUtils.isEmpty(map)) {
return;
        }
RMapCache<Object, Object>mapCache=redissonClient.getMapCache(RMAPCACHE_KEY);
if (mapCache.size() ==0) {
return;
        }
Stringkey=map.get(KEY).toString();
booleandelKey= (boolean) map.get(DELKEY);
if (delKey) {
mapCache.fastRemove(key);
LOGGER.info("[idempotent]:has removed key={}", key);
        }
threadLocal.remove();
    }
publicIdempotentAspect(finalRedissonClientredissonClient, finalKeyResolverkeyResolver) {
this.redissonClient=redissonClient;
this.keyResolver=keyResolver;
    }
}

幂等性异常定义

/*** Idempotent Exception If there is a custom global exception, you need to inherit the* custom global exception.** @author jeckxu*/publicclassIdempotentExceptionextendsException {
publicIdempotentException() {
super();
   }
publicIdempotentException(Stringmessage) {
super(message);
   }
publicIdempotentException(Stringmessage, Throwablecause) {
super(message, cause);
   }
publicIdempotentException(Throwablecause) {
super(cause);
   }
protectedIdempotentException(Stringmessage, Throwablecause, booleanenableSuppression,
booleanwritableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
   }
}


幂等性唯一标志处理器

importorg.aspectj.lang.JoinPoint;
importorg.jeckxu.magical.core.idempotent.annotation.Idempotent;
/*** @author jeckxu* @date 2020/11/10* <p>* 唯一标志处理器*/publicinterfaceKeyResolver {
/*** 解析处理 key* @param idempotent 接口注解标识* @param point 接口切点信息* @return 处理结果*/Stringresolver(Idempotentidempotent, JoinPointpoint);
}
importorg.aspectj.lang.JoinPoint;
importorg.aspectj.lang.reflect.MethodSignature;
importorg.jeckxu.magical.core.idempotent.annotation.Idempotent;
importorg.springframework.core.LocalVariableTableParameterNameDiscoverer;
importorg.springframework.expression.Expression;
importorg.springframework.expression.spel.standard.SpelExpressionParser;
importorg.springframework.expression.spel.support.StandardEvaluationContext;
importjava.lang.reflect.Method;
/*** @author jeckxu*/publicclassExpressionResolverimplementsKeyResolver {
privatestaticfinalSpelExpressionParserPARSER=newSpelExpressionParser();
privatestaticfinalLocalVariableTableParameterNameDiscovererDISCOVERER=newLocalVariableTableParameterNameDiscoverer();
@OverridepublicStringresolver(Idempotentidempotent, JoinPointpoint) {
Object[] arguments=point.getArgs();
String[] params=DISCOVERER.getParameterNames(getMethod(point));
StandardEvaluationContextcontext=newStandardEvaluationContext();
for (intlen=0; len<params.length; len++) {
context.setVariable(params[len], arguments[len]);
      }
Expressionexpression=PARSER.parseExpression(idempotent.key());
returnexpression.getValue(context, String.class);
   }
/*** 根据切点解析方法信息* @param joinPoint 切点信息* @return Method 原信息*/privateMethodgetMethod(JoinPointjoinPoint) {
MethodSignaturesignature= (MethodSignature) joinPoint.getSignature();
Methodmethod=signature.getMethod();
if (method.getDeclaringClass().isInterface()) {
try {
method=joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
method.getParameterTypes());
         }
catch (SecurityException|NoSuchMethodExceptione) {
thrownewRuntimeException(e);
         }
      }
returnmethod;
   }
}

幂等性异常统一处理

importorg.jeckxu.magical.core.idempotent.exception.IdempotentException;
importorg.jeckxu.magical.core.launch.destroy.MagicalDestroying;
importorg.jeckxu.magical.core.launch.utils.I18nMessageUtils;
importorg.jeckxu.magical.core.tool.api.R;
importorg.springframework.boot.autoconfigure.condition.ConditionalOnClass;
importorg.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.core.annotation.Order;
importorg.springframework.http.HttpStatus;
importorg.springframework.web.bind.annotation.ExceptionHandler;
importorg.springframework.web.bind.annotation.ResponseStatus;
importorg.springframework.web.bind.annotation.RestControllerAdvice;
importorg.springframework.web.servlet.DispatcherServlet;
importjavax.servlet.Servlet;
@Order(3)
@Configuration(proxyBeanMethods=false)
@ConditionalOnWebApplication(type=ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class})
@RestControllerAdvicepublicclassMagicalIdempotentExceptionTranslatorimplementsMagicalDestroying {
privatestaticfinalorg.slf4j.Loggerlog=org.slf4j.LoggerFactory.getLogger(MagicalIdempotentExceptionTranslator.class);
@ExceptionHandler(IdempotentException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
publicR<Object>handleError(IdempotentExceptione) {
e.printStackTrace();
returnR.fail(1999, I18nMessageUtils.get(e.getMessage()));
    }
}

幂等插件自动配置【初始化】

importorg.jeckxu.magical.core.idempotent.aspect.IdempotentAspect;
importorg.jeckxu.magical.core.idempotent.expression.ExpressionResolver;
importorg.jeckxu.magical.core.idempotent.expression.KeyResolver;
importorg.jeckxu.magical.core.idempotent.prop.MagicalIdempotentProperties;
importorg.jeckxu.magical.core.launch.destroy.MagicalDestroying;
importorg.jeckxu.magical.core.tool.utils.StringUtil;
importorg.redisson.Redisson;
importorg.redisson.api.RedissonClient;
importorg.redisson.config.*;
importorg.springframework.boot.autoconfigure.AutoConfigureAfter;
importorg.springframework.boot.autoconfigure.condition.ConditionalOnClass;
importorg.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
importorg.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
importorg.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
importorg.springframework.boot.context.properties.EnableConfigurationProperties;
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;
/*** 幂等插件初始化* @author jeckxu*/@Configuration(proxyBeanMethods=false)
@ConditionalOnClass(RedissonClient.class)
@EnableConfigurationProperties(MagicalIdempotentProperties.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnProperty(value="magical.idempotent.enabled", havingValue="true")
publicclassIdempotentAutoConfigurationimplementsMagicalDestroying {
privatestaticConfigsingleConfig(MagicalIdempotentPropertiesproperties) {
Configconfig=newConfig();
SingleServerConfigserversConfig=config.useSingleServer();
serversConfig.setAddress(properties.getAddress());
Stringpassword=properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
      }
serversConfig.setDatabase(properties.getDatabase());
serversConfig.setConnectionPoolSize(properties.getPoolSize());
serversConfig.setConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
returnconfig;
   }
privatestaticConfigmasterSlaveConfig(MagicalIdempotentPropertiesproperties) {
Configconfig=newConfig();
MasterSlaveServersConfigserversConfig=config.useMasterSlaveServers();
serversConfig.setMasterAddress(properties.getMasterAddress());
serversConfig.addSlaveAddress(properties.getSlaveAddress());
Stringpassword=properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
      }
serversConfig.setDatabase(properties.getDatabase());
serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
returnconfig;
   }
privatestaticConfigsentinelConfig(MagicalIdempotentPropertiesproperties) {
Configconfig=newConfig();
SentinelServersConfigserversConfig=config.useSentinelServers();
serversConfig.setMasterName(properties.getMasterName());
serversConfig.addSentinelAddress(properties.getSentinelAddress());
Stringpassword=properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
      }
serversConfig.setDatabase(properties.getDatabase());
serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
returnconfig;
   }
privatestaticConfigclusterConfig(MagicalIdempotentPropertiesproperties) {
Configconfig=newConfig();
ClusterServersConfigserversConfig=config.useClusterServers();
serversConfig.addNodeAddress(properties.getNodeAddress());
Stringpassword=properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
      }
serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
returnconfig;
   }
/*** 切面 拦截处理所有 @Idempotent* @return Aspect*/@BeanpublicIdempotentAspectidempotentAspect(MagicalIdempotentPropertiesproperties) {
returnnewIdempotentAspect(redissonClient(properties), newExpressionResolver());
   }
/*** key 解析器* @return KeyResolver*/@Bean@ConditionalOnMissingBean(KeyResolver.class)
publicKeyResolverkeyResolver() {
returnnewExpressionResolver();
   }
privatestaticRedissonClientredissonClient(MagicalIdempotentPropertiesproperties) {
MagicalIdempotentProperties.Modemode=properties.getMode();
Configconfig;
switch (mode) {
casesentinel:
config=sentinelConfig(properties);
break;
casecluster:
config=clusterConfig(properties);
break;
casemaster:
config=masterSlaveConfig(properties);
break;
casesingle:
config=singleConfig(properties);
break;
default:
config=newConfig();
break;
      }
returnRedisson.create(config);
   }
}

yml配置项

magical:  idempotent:    enabled: false    address: redis://192.168.0.1:26379    password: ${YOUR_PASSWORD}
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1天前
|
缓存 NoSQL Java
Spring Boot整合Redis缓存的最佳实践
Spring Boot整合Redis缓存的最佳实践
|
3天前
|
缓存 NoSQL Java
Spring Boot中集成Redis实现缓存功能
Spring Boot中集成Redis实现缓存功能
|
3天前
|
缓存 NoSQL Java
Spring Boot与Redis集成的最佳实践
Spring Boot与Redis集成的最佳实践
|
4天前
|
缓存 负载均衡 NoSQL
Redis系列学习文章分享---第十四篇(Redis多级缓存--封装Http请求+向tomcat发送http请求+根据商品id对tomcat集群负载均衡)
Redis系列学习文章分享---第十四篇(Redis多级缓存--封装Http请求+向tomcat发送http请求+根据商品id对tomcat集群负载均衡)
9 1
|
4天前
|
缓存 NoSQL Java
Redis系列学习文章分享---第四篇(Redis快速入门之Java客户端--商户查询缓存+更新+双写一致+穿透+雪崩+击穿+工具封装)
Redis系列学习文章分享---第四篇(Redis快速入门之Java客户端--商户查询缓存+更新+双写一致+穿透+雪崩+击穿+工具封装)
9 0
|
4天前
|
缓存 NoSQL Java
Spring Boot与Redis的缓存一致性问题
Spring Boot与Redis的缓存一致性问题
|
4天前
|
IDE Java 机器人
Spring Boot中的多模块项目构建
Spring Boot中的多模块项目构建
|
4天前
|
缓存 NoSQL Java
Spring Boot整合Redis缓存的最佳实践
Spring Boot整合Redis缓存的最佳实践
|
4天前
|
消息中间件 NoSQL Java
Spring Boot中使用Redis和Lua脚本实现延时队列
Spring Boot中使用Redis和Lua脚本实现延时队列
|
4天前
|
缓存 NoSQL Java
Spring Boot2 系列教程(二十九)Spring Boot 整合 Redis
Spring Boot2 系列教程(二十九)Spring Boot 整合 Redis