以下内容纯属个人扯淡,仅供参考
目录
框架集成
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 })
---LettuceConnectionConfiguration
redisConnectionFactory()
getLettuceClientConfiguration()
createBuilder()
//由于我们在yml配置文件中设置了pool属性,因此会执行下一句
new PoolBuilderFactory().createBuilder(pool)
---LettuceConnectionConfiguration#PoolBuilderFactory
getPoolConfig
最后的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: redis
redis:
database: 0
host: 127.0.0.1
port: 6379
timeout: 10000ms #连接超时时间
lettuce:
pool:
max-active: 1000
max-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
**/
@Bean
public 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再存入的
参考: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.cache
EhCacheCacheConfiguration
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> redisTemplate
StringRedisTemplate stringRedisTemplate
由于我们倾向于key直接使用string,因此本工程CacheConfig注入了一个RedisTemplate<String,Object>。这两个bean,我们将在"工具类手动式使用redis"中使用。
分析4:序列化器
RedisSerializer
疑问:序列化器是什么?为什么要使用序列化器
通过跟踪RedisTemplate源码可以发现
RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware
RedisAccessor implements InitializingBean
凡是实现InitializingBean接口的类,在初始化Bean实例时会回调afterPropertiesSet,因此我们可以看到:
---RedisTemplate<K, V>#afterPropertiesSet()
在该方法中,4个序列化器:
keySerializer
valueSerializer
hashKeySerializer
hashValueSerializer
若未被设置,并且enableDefaultSerializer=true(默认就为true)时,就统一都使用默认的序列化器
JdkSerializationRedisSerialzer,该序列化器是有一定缺点的:除了有额外的内容外也不易阅读
注意:keySerializer、hashKeySerializer一般用StringRedisSerializer即可
本工程使用的是: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:缓存的key
condition:条件表达式为true时才进行缓存
2.@CachEvict
value:缓存名称
key:缓存的key
condition:条件表达式为true时才清空
allEntries:是否清空所有缓存
beforeInvocation:是否在方法执行前进行清空
默认情况下,若方法发生异常,则不会清空缓存
3.@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 {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private 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);
业务使用
用户、角色、权限