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

 

業務使用

用戶、角色、權限

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章