当前位置: 代码迷 >> 综合 >> With SpringData Redis
  详细解决方案

With SpringData Redis

热度:99   发布时间:2023-12-12 15:20:08.0

以下内容纯属个人扯淡,仅供参考

目录

框架集成

业务使用

用户、角色、权限


 

框架集成

SpringDataRedis集成

概览

环境
引入依赖
yml配置
配置类
注解式使用
工具类使用

(1)环境

jdk:1.8.0_221

IDEA:Ultimate 2019.3

maven:使用IDEA自带的Bundled版本,并配置阿里镜像仓库

SpringBoot:2.2.2.RELEASE

其他:本工程使用的是MybatisPlus,因此实体类、mapper就不给出了

(2)引入依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
?</dependency>
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency>

分析自动配置类

---RedisAutoConfiguration
//导入两个配置类
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })---JedisConnectionConfiguration//由于类路径下没有Jedis等,因此该配置类失效@ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class })---LettuceConnectionConfigurationredisConnectionFactory()getLettuceClientConfiguration()createBuilder()//由于我们在yml配置文件中设置了pool属性,因此会执行下一句new PoolBuilderFactory().createBuilder(pool)---LettuceConnectionConfiguration#PoolBuilderFactorygetPoolConfig

最后的getPoolConfig要求返回一个

import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

因此仅配置第1个依赖时,项目启动会报错,如下

Error creating bean with name 'redisConnectionFactory' defined in class path resource

通过点击redisConnectionFactory也可以跟踪到上述代码

疑问:项目中如何决定选择哪一种呢?Jedis还是Lettrue

参考:Redis的三个框架:Jedis,Redisson,Lettuce

本例是选择letture。

(3)yml配置

?spring:cache:type: redisredis:database: 0host: 127.0.0.1port: 6379timeout: 10000ms #连接超时时间lettuce:pool:max-active: 1000max-wait: -1  #最大阻塞等待时间,负值-没有限制max-idle: 100 #最大空闲连接min-idle: 1 #最小空闲连接

疑问:pool中的max-active等这些参数配置依据是什么?实际应用时如何根据业务配置合适的值?

(4)配置类

这里最核心的配置是RedisCacheManager的注入,其中配置了过期时间、key、value的序列化器,这些都是为@Cacheable等注解的配置,如果项目中完全不采用"工具类手动式使用redis",那么就没必要使用redisTemplate。

@Configuration
@EnableCaching //开启基于注解的缓存
@Slf4j
public class CacheConfig {
?/*** 1.缓存生存时间
?     */private Duration timeToLive = Duration.ofDays(1);
?/*** 2.缓存管理器** @date 13:21 2020/5/9* @author 李文龙* @param connectionFactory:* @return**/@Beanpublic RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {//1.redis缓存配置RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(this.timeToLive).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer())).disableCachingNullValues();//根据redis缓存配置和reid连接工厂生成redis缓存管理器RedisCacheManager redisCacheManager = RedisCacheManager.builder(connectionFactory).cacheDefaults(config).transactionAware().build();log.info("自定义RedisCacheManager加载完成");return redisCacheManager;}
?/*** 3.提供给其他类对redis数据库进行操作** @date 13:09 2020/5/9* @author 李文龙* @return**/@Bean(name = "redisTemplate")public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactory);//使用自定义的序列化器redisTemplate.setKeySerializer(keySerializer());redisTemplate.setValueSerializer(valueSerializer());redisTemplate.setHashKeySerializer(keySerializer());redisTemplate.setHashValueSerializer(valueSerializer());log.info("自定义RedisTemplate加载完成");return redisTemplate;}/*** 键序列化使用StringRedisSerializer*/private RedisSerializer<String> keySerializer() {return new StringRedisSerializer();}/*** 3.值序列化使用json序列化器*/private RedisSerializer<Object> valueSerializer() {//采用json格式:具有对象的class名,便于反序列化return new GenericJackson2JsonRedisSerializer();}
}

通过CacheConfig,我们可以得到以下结论:

1.CacheConfig的配置决定了,本工程支持两种方式来操作redis:
1) @Cacheable等注解
这些注解是由于在CacheConfig中注入了RedisCacheManager,其中设置了过期时间、key/value的序列化器2) redisTemplate类
我们使用RedisTemplate<String,Object>代替了默认的RedisTemplate<Object,Object>,并且该
redisTemplate也指定了key/value序列化器,理论上我们是能通过这个redisTemplate去获取由@Cacheable缓存的k-v的
另外还使用了stringRedisTemplate,这用的是默认的,用于写简单的字符串型2.另外,RedisTemplate<String,Object>中,v是Obejct是任意来的超类类型,说明,我们可以将List、Map
等任何Java对象都使用redisTemplate来存入。实际上是只使用了redis本身的5种数据类型:string、set、
list、map、zset中的string
即:是将java对象序列化为string再存入的

参考:Spring Cache抽象详解

参考:Redis 数据类型

分析1:缓存时间

Duration.ofDays(1)

这里设置为1天,这是为"注解式使用redis"所使用的,而工具类操作是有对应的api。

如果不设置过期时间,那么会根据redis本身配置的过期策略决定

参考:Redis 的过期策略都有哪些? 疑问:实际业务中如何考虑过期时间的设置?

分析2:缓存管理器

cacheManager

通过调试源码:

---CacheAutoConfiguration
@Import({ CacheConfigurationImportSelector.class, CacheManagerEntityManagerFactoryDependsOnPostProcessor.class })---CacheAutoConfiguration#CacheConfigurationImportSelector这个Selector是CacheAutoConfiguration的内部类,实际上是导入各类基础设施的自动配置类selectImports()。通过对该方法断点调试,可以知道,这个Selector实际是导入了11个如RedisCacheConfiguration配置类,这些配置类为对应的基础设施提供自动配置,这11个是"包可见"的默认情况下是SimpleCacheConfiguration生效,而其他基础设施配置类失效---SimpleCacheConfiguration向容器注入一个ConcurrentMapCacheManager,使用ConcurrentMapCache做为具体Cache,是基于应用程序的ConcurrentHashMap实现

由于本工程在CacheConfig中配置了cacheManager,所以实际上下面这个包下的那些配置类都失效了

org.springframework.boot.autoconfigure.cacheEhCacheCacheConfiguration
GenericCacheConfiguration
RedisCacheConfiguration
SimpleCacheConfiguration
...都注解了:@ConditionalOnMissingBean(CacheManager.class)

参考:Spring Boot 自动配置 : CacheAutoConfiguration

疑问:如何知道IOC容器中的某个组件是由哪个配置类注入的呢?哪些配置类被注入容器后但由于@Conditional不通过而失效了呢?

分析3:缓存模板

redisTemplate提供给"工具类手动式使用redis"来使用的,若只采用"注解式使用redis",则不需要配置这些redisTemplate

本工程使用redis,并且使用了@EnableAutoConfiguration,因此RedisAutoConfiguration会生效,该配置类向容器中注入了以下2个Bean

//容器中若有id=redisTemplate的bean,则该Bean不注入
@ConditionalOnMissingBean(name = "redisTemplate") 
RedisTemplate<Object, Object> redisTemplateStringRedisTemplate stringRedisTemplate

由于我们倾向于key直接使用string,因此本工程CacheConfig注入了一个RedisTemplate<String,Object>。这两个bean,我们将在"工具类手动式使用redis"中使用。

分析4:序列化器

RedisSerializer

疑问:序列化器是什么?为什么要使用序列化器

通过跟踪RedisTemplate源码可以发现

 RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAwareRedisAccessor implements InitializingBean

凡是实现InitializingBean接口的类,在初始化Bean实例时会回调afterPropertiesSet,因此我们可以看到:

---RedisTemplate<K, V>#afterPropertiesSet()
在该方法中,4个序列化器:
keySerializer
valueSerializer
hashKeySerializer
hashValueSerializer
若未被设置,并且enableDefaultSerializer=true(默认就为true)时,就统一都使用默认的序列化器
JdkSerializationRedisSerialzer,该序列化器是有一定缺点的:除了有额外的内容外也不易阅读注意:keySerializer、hashKeySerializer一般用StringRedisSerializer即可

参考:RedisTemplate的key默认序列化器问题

本工程使用的是:GenericJackson2JsonRedisSerializer,除了实例本身存储的属性数据外还有类名、包名等,但也有缺点

参考:GenericJackson2JsonRedisSerializer 反序列化问题

还有另一个常用的序列化器:Jackson2JsonRedisSerializer,好像也是有点缺点的

@Bean
public Jackson2JsonRedisSerializer jackson2JsonRedisSerializer() {Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);return jackson2JsonRedisSerializer;
}

疑问:实际使用时如何选择哪种序列化器呢?参考:使用Spring Data Redis时,遇到的几个问题

分析5:自定义键生成器(本工程未采纳使用)

通常是这样配置的

CacheConfig extends CachingConfigurerSupport@Bean("keyGenerator")
public KeyGenerator keyGenerator() {return (target, method, params) -> {StringBuilder sb = new StringBuilder();sb.append(target.getClass().getName());sb.append(method.getName());for (Object obj : params) {sb.append(obj.toString());}return sb.toString();};
}

使用示例

@Override
@Cacheable(value = PARAMS_CACHE_NAME, keyGenerator = "keyGenerator")
public List<PctParam> findAllParams() {//使用的是MybatisPlus为Service类自动实现的方法return list();
}

在@Cacheable等注解里,key和keyGenerator只能选其一使用,若都不指定,则默认是使用keyGenerator属性

疑问:指定的默认的keyGenerator实例是SimpleKeyGenerator?这是哪里去指定的

前者需要在一个个的注解里去指定具体的key值,而keyGenerator属性是使用通用策略去生成key,统一key的生成策略

疑问:在项目中没有使用keyGenerator方式,而是每个方法都指定key值,为什么没采用keyGenerator呢?

测试代码

@Test
public void test1()List<PctParam> list = redisTestService.findAllParams();
}

结果

key值如下,会生成3条缓存记录,它们的key都不同,但value值是相同的,因此本项目中没有采纳keyGenerator

params::com.yihuacomputer.yhcloud.service.RedisTestServiceImpl$$EnhancerBySpringCGLIB$$479e2712findAllParams
params::com.yihuacomputer.yhcloud.service.RedisTestServiceImpl$$EnhancerBySpringCGLIB$$e8d71ba6findAllParams
params::com.yihuacomputer.yhcloud.service.RedisTestServiceImplfindAllParams

(5)注解式使用redis

1.@Cacheable:主要针对方法配置,能够根据方法的请求参数对其结果进行缓存value:缓存名称key:缓存的keycondition:条件表达式为true时才进行缓存
2.@CachEvict value:缓存名称key:缓存的keycondition:条件表达式为true时才清空allEntries:是否清空所有缓存beforeInvocation:是否在方法执行前进行清空默认情况下,若方法发生异常,则不会清空缓存
3.@Cacheput

参考:SpringCache之 @CachePut

由于是基于SpringAOP实现的,因此一般Service里所注解的方法需被Controller层直接调用,而不能在Service里面互相简接调用

使用示例

@Service
public class RedisTestServiceImpl extends ServiceImpl<PctParamMapper,PctParam> implements RedisTestService{/*** cacheName,注解方式必须指定,也可以使用String[]指定多个*/private static final String PARAMS_CACHE_NAME = "params";@Autowired(required = false)private PctParamMapper pctParamMapper;@Override@CacheEvict(value = PARAMS_CACHE_NAME, allEntries = true, beforeInvocation = true)public PctParam saveByPctParam(PctParam param) {if (param != null && param.getId() != null) {pctParamMapper.updateById(param);} else {pctParamMapper.insert(param);}return param;}@Override@Cacheable(value = PARAMS_CACHE_NAME, key = "'params_all'")public List<PctParam> findAllParams() {return list();}@Override@Cacheable(value = PARAMS_CACHE_NAME, key = "'params_id_'+#paramId", unless = "#result == null")public PctParam findByParamId(Long paramId) {return pctParamMapper.selectById(paramId);}@Override@Cacheable(value = PARAMS_CACHE_NAME, key = "'params_count'")public Integer getCount() {return pctParamMapper.selectCount(null);}@Override@CacheEvict(value = PARAMS_CACHE_NAME, allEntries = true, beforeInvocation = true)public void removeByParamId(Long paramId) {pctParamMapper.deleteById(paramId);}
}

分析1:测试

@Test
public void test3() {PctParam pctParam = new PctParam();pctParam.setId(1L);pctParam.setName("参数名");pctParam.setValue("参数值");pctParam.setDescp("参数描述aaa");pctParam.setStatus(1);pctParam.setRemark("备注");pctParam.setOperator("admin");pctParam.setOperateTime(new Date());redisTestService.saveByPctParam(pctParam);
}

结果

缓存的key=params::params_id_1,value是一个对象json字符串形式。其中数据部分的operateTime类型是Date,因此java.util.Date全限定类名被存进,并且还有一个@class属性,其值指出了该对象PctParam的全限定路径,之所有是这样的数据格式,是因为前面为value指定了GenericJackson2JsonRedisSerializer序列化器

测试

@Test
public void test3() {PctParam pctParam = new PctParam();pctParam.setId(1L);pctParam.setName("参数名");pctParam.setValue("参数值");pctParam.setDescp("参数描述aaa");pctParam.setStatus(1);pctParam.setRemark("备注");pctParam.setOperator("admin");pctParam.setOperateTime(new Date());redisTestService.saveByPctParam(pctParam);
}

由于该方法的注解

@CacheEvict(value = PARAMS_CACHE_NAME, allEntries = true, beforeInvocation = true)

因此,在PARAMS_CACHE_NAME="params"名称空间下的所有缓存,因为allEntries=true,就都会被清除

分析2:cacheName与redis#namespace

private static final String PARAMS_CACHE_NAME = "params";

这是SpringCache抽象概念中的"缓存名称",用于在@Cacheable等这些注解中为value属性赋值,这些注解若不指定value值时方法调用会抛出异常。

cacheName=params,key=params_all那么对应redis的中该缓存的key值为:key=params::params_all

在redis领域概念中,以":"区分namespace-命名空间,其实我的理解就是一种类似分组分类、分文件夹等思想。只要key中有":"出现,就会分割命名空间,那么在redis就可以看到这样:相当于redis为我们的key分好组了

当然,也可以不使用namespace概念,即key中不使用":"。那么你的redis将像下面这样:岂不是根本找不到?

理论上,如果这时候我们使用"工具类手动使用redis"去尝试这样设置

@Test
public void test4() {PctParam pctParam = new PctParam();pctParam.setId(1L);pctParam.setName("参数名");pctParam.setValue("参数值");pctParam.setDescp("参数描述aaa");pctParam.setStatus(1);pctParam.setRemark("备注");pctParam.setOperator("admin");pctParam.setOperateTime(new Date());redisTestService.saveByPctParam(pctParam);redisUtil.setObj("params::params_all", pctParam );
}

与前面的方式会是同样的key的

(6)工具类手动式使用redis

@Component
public class RedisUtil {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Autowiredprivate RedisTemplate<String,Object> redisTemplate;// Key(键),简单的key-value操作/*** 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。** @param key* @return*/public long ttl(String key) {return stringRedisTemplate.getExpire(key);}/*** 实现命令:expire 设置过期时间,单位秒** @param key* @return*/public void expire(String key, long timeout) {stringRedisTemplate.expire(key, timeout, TimeUnit.SECONDS);}/*** 实现命令:INCR key,增加key一次** @param key* @return*/public long incr(String key, long delta) {return stringRedisTemplate.opsForValue().increment(key, delta);}/*** 实现命令:key,减少key一次** @param key* @return*/public long decr(String key, long delta) {if(delta<0){
//            throw new RuntimeException("递减因子必须大于0");del(key);return 0;}return stringRedisTemplate.opsForValue().increment(key, -delta);}/*** 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key*/public Set<String> keys(String pattern) {return stringRedisTemplate.keys(pattern);}/*** 实现命令:DEL key,删除一个key** @param key*/public void del(String key) {stringRedisTemplate.delete(key);}// String(字符串)/*** 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)** @param key* @param value*/public void set(String key, String value) {stringRedisTemplate.opsForValue().set(key, value);}/*** 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)** @param key* @param value* @param timeout (以秒为单位)*/public void set(String key, String value, long timeout) {stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);}/*** 实现命令:GET key,返回 key所关联的字符串值。** @param key* @return value*/public String get(String key) {return (String) stringRedisTemplate.opsForValue().get(key);}// Hash(哈希表)/*** 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value** @param key* @param field* @param value*/public void hset(String key, String field, Object value) {stringRedisTemplate.opsForHash().put(key, field, value);}/*** 实现命令:HGET key field,返回哈希表 key中给定域 field的值** @param key* @param field* @return*/public String hget(String key, String field) {return (String) stringRedisTemplate.opsForHash().get(key, field);}/*** 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。** @param key* @param fields*/public void hdel(String key, Object... fields) {stringRedisTemplate.opsForHash().delete(key, fields);}/*** 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。** @param key* @return*/public Map<Object, Object> hgetall(String key) {return stringRedisTemplate.opsForHash().entries(key);}// List(列表)/*** 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头** @param key* @param value* @return 执行 LPUSH命令后,列表的长度。*/public long lpush(String key, String value) {return stringRedisTemplate.opsForList().leftPush(key, value);}/*** 实现命令:LPOP key,移除并返回列表 key的头元素。** @param key* @return 列表key的头元素。*/public String lpop(String key) {return (String) stringRedisTemplate.opsForList().leftPop(key);}/*** 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。** @param key* @param value* @return 执行 LPUSH命令后,列表的长度。*/public long rpush(String key, String value) {return stringRedisTemplate.opsForList().rightPush(key, value);}/*** 设置对象** @date 10:04 2020/5/11* @author 李文龙* @param key:* @param obj:* @return**/public void setObj(String key, Object obj) {redisTemplate.opsForValue().set(key,obj);}/*** 获取缓存中的对象** @date 10:04 2020/5/11* @author 李文龙* @param key:* @exception {@link ClassCastException}* @return**/public Object getObj(String key) {return redisTemplate.opsForValue().get(key);}
}

stringRedisTemplate使用的是RedisAutoConfiguration配置类中注入的,专用于<String,String>这种类型,而redisTemplate是在

CacheConfig配置类中配置的,<String,Object>类型,实际上对redis来说,都只是应用了:string、hash、list、set、zset中的string

使用示例

redisUtil.set("pctParam",pctParam.toString());
redisUtil.setObj("test3",pctParam);

 

业务使用

用户、角色、权限