以下內容純屬個人扯淡,僅供參考
目錄
框架集成
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);
業務使用
用戶、角色、權限