With SpringData Redis

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

目录

框架集成

业务使用

用户、角色、权限


 

框架集成

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再存入的

参考: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.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即可

参考: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:缓存的key
  condition:条件表达式为true时才进行缓存
2.@CachEvict 
  value:缓存名称
  key:缓存的key
  condition:条件表达式为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 {

    @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);

 

业务使用

用户、角色、权限

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值