前面的SpringCache緩存說過在沒有引入其他緩存中間件時,默認使用的是ConcurrentMapCacheManager=ConcurrentMapCache,是將數據保存在ConcurrentMap<Object, Object>
中。
在實際開發中,我們一般都會使用redis
、memcached
、ehcache
來作爲緩存中間件。
整合Redis
Redis
是一個開源(BSD許可)的,內存中的數據結構存儲系統,它可以用作數據庫、緩存和消息中間件。
當我們引入Reids
的starter
依賴的時候,使用RedisCacheManager
。RedisCacheManager
會幫我們創建RedisCache
來作爲緩存組件,RedisCache通過操作redis緩存數據。
1、引入Redis的starter的依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 連接池 - luttuce依賴該連接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
2、配置yml
配置文件
spring:
redis:
host: 127.0.0.1 # Redis連接地址
port: 6379 # redis連接端口
lettuce: # 使用lettuce客戶端
pool:
max-active: 8 # 連接池最大連接數(負值表示沒有限制)
max-idle: 8 # 連接池的最大空閒連接
min-idle: 0 # 連接池的最小空閒連接
max-wait: -1ms # 連接池最大阻塞等待時間(賦值表示沒有限制)
database: 0 # 默認使用索引爲0的數據庫
timeout: 5000ms # 連接超時時間
3、序列化配置
Reids默認使用的是JDK的序列化機制,我們通過RedisDesktopManager
去查看存儲的數據,是無法用肉眼看出存的是什麼的。
比如說我們要存一個對象進去,並且想直接肉眼看到是一個怎麼樣的數據,可以通過修改配置,讓其以Json
的格式去保存。
創建一個配置類繼承CachingConfigurerSupport
@Configuration
public class RedisConfiguration extends CachingConfigurerSupport {
/**
* 採用RedisCacheManager作爲緩存管理器
* @param connectionFactory Redis連接工廠 - 配置的是Luttuce客戶端,則這裏就是LuttuceFactory
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory){
RedisCacheManager redisCacheManager = RedisCacheManager.create(connectionFactory);
return redisCacheManager;
}
/**
* RedisTemplate模板
* @param factory Redis連接工廠
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用Jackson進行序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// 如果enableDefaultTyping過期(SpringBoot後續版本過期了),則使用下面這個代替
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key採用String的序列方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也採用String的序列方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化採用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化也採用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
4、測試一下
// 注入RedisTemplate
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
// 向Redis中存數據
@GetMapping("/test")
public String test(){
BoundHashOperations boundHashOps = redisTemplate.boundHashOps("User");
List list = new ArrayList();
User user1 = new User("柳成蔭", "男", 22);
list.add(user1);
list.add(user1);
boundHashOps.put("3",list);
return "ok";
}
// 向Redis取數據
@GetMapping("/test1")
public List test1(){
BoundHashOperations boundHashOps = redisTemplate.boundHashOps("User");
List<User> users = (List<User>) boundHashOps.get("3");
return users;
}
可以看到確實存入的是JSON序列化的結果
取出來也是沒有問題的
RedisUtils
自定義的RedisUtils用着比RidsTemplate方便很多,所以一般都會自己封裝一個工具類。
@Component
public class RedisUtils {
// 注入自己的RedisTemplate
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// ================================== 共用部分 ====================================
/**
* 指定緩存失效時間
* @param key 鍵
* @param time 時間(秒)
*/
public boolean expire(String key, long time){
try{
if(time > 0){
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 獲取過期時間
* @param key key值
* @return 時間(秒),返回0表示永久有效
*/
public long getExpire(String key){
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
/**
* 判斷Key是否存在
* @param key Key值
*/
public boolean hasKey(String key){
try {
return redisTemplate.hasKey(key);
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 刪除煥春
* @param key 一個值或多個值
* @return 刪除成功個數 - 失敗返回0
*/
public long delete(String... key){
if(key != null && key.length > 0){
if (key.length == 1){
return redisTemplate.delete(key[0]) ? 1l : 0l;
}else{
return redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
return 0l;
}
// ================================== String操作 ====================================
/**
* 普通緩存獲取
* @param key key值
*/
public Object get(String key){
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通緩存放入
* @param key Key值
* @param value Value值
* @return true成功 false失敗
*/
public boolean set(String key, Object value){
try{
redisTemplate.opsForValue().set(key,value);
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 普通緩存放入並設置失效時間
* @param key key值
* @param value value值
* @param time 時間(秒) time要大於0,如果time小於0,則設置無限期
* @return true成功 false失敗
*/
public boolean set(String key, Object value, long time){
try{
if(time > 0){
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}else{
set(key,value);
}
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 遞增/遞減
* @param key key值
* @param delta 正數(遞增) 負數(遞減)
*/
public long incr(String key, long delta){
if(delta < 0){
throw new RuntimeException("遞增因子必須大於0");
}
return redisTemplate.opsForValue().increment(key,delta);
}
// ================================== Hash操作 ====================================
/**
* HashGet
* @param key Key值
* @param item Value值
*/
public Object hget(String key, String item){
return redisTemplate.opsForHash().get(key,item);
}
/**
* 獲取hashKey對應的所有鍵值
* @param key Key值
* @return 對應的多個鍵值對(map)
*/
public Map<Object, Object> hmget(String key){
return redisTemplate.opsForHash().entries(key);
}
/**
* Hashset
* @param key Key值
* @param map 對應的多個鍵值對(map)
*/
public boolean hmset(String key, Map<String, Object> map){
try{
redisTemplate.opsForHash().putAll(key,map);
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* HashSet 並設置過期時間
* @param key Key值
* @param map 對應的多個鍵值對(map)
* @param time 時間(秒) 小於等於0表示永久
*/
public boolean hmset(String key, Map<String, Object> map, long time){
try{
redisTemplate.opsForHash().putAll(key,map);
if (time > 0){
expire(key, time);
}
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 向一張hash表中放入數據,如果不存在則創建
* @param key Key值
* @param item hash表中的Key值
* @param value hash表中的Value值
*/
public boolean hset(String key, String item, Object value){
try {
redisTemplate.opsForHash().put(key,item,value);
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 向一張hash表中放入數據,如果不存在則創建
* @param key Key值
* @param item hash表中的Key值
* @param value hash表中的Value值
* @param time 時間(秒) 注意:如果已存在的hash表有時間,這裏將會替換原有的時間
*/
public boolean hset(String key, String item, Object value,long time){
try {
redisTemplate.opsForHash().put(key,item,value);
if(time > 0){
expire(key,time);
}
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 刪除hash表中的值
* @param key Key值
* @param item hash表中的Key值 - 可以是多個
*/
public void hdelete(String key, Object... item){
redisTemplate.opsForHash().delete(key,item);
}
/**
* 判斷hash表中是否有該項的值
* @param key Key值
* @param item hash表中的Key值
*/
public boolean hHasKey(String key, String item){
return redisTemplate.opsForHash().hasKey(key,item);
}
/**
* hash遞增/遞減 如果不存在,就會創建一個 並把新增後的值返回
* @param key Key值
* @param item hash表中的Key值
* @param by 要增加/減少幾 正數(增加) 負數(減少)
*/
public double hincr(String key, String item, double by){
return redisTemplate.opsForHash().increment(key,item,by);
}
// ================================== Set操作 ====================================
/**
* 根據key獲取Set中的所有值
* @param key Key值
*/
public Set<Object> sGet(String key){
try{
return redisTemplate.opsForSet().members(key);
}catch (Exception e){
e.printStackTrace();
return null;
}
}
/**
* 查詢value是否存在於set
* @param key Key值
* @param value value直
*/
public boolean sHasKey(String key, Object value){
try {
return redisTemplate.opsForSet().isMember(key,value);
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 將數據放入set緩存
* @param key Key值
* @param values value值 - 可以是多個
*/
public long sSet(String key, Object... values){
try {
return redisTemplate.opsForSet().add(key,values);
}catch (Exception e){
e.printStackTrace();
return 0;
}
}
/**
* 將set數據放入緩存
* @param key Key值
* @param time 時間(秒)
* @param values Value值 - 可以是多個
*/
public long sSetAndTime(String key, long time, Object... values){
try{
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0){
expire(key,time);
}
return count;
}catch (Exception e){
e.printStackTrace();
return 0;
}
}
/**
* 獲取set緩存的長度
* @param key Key值
*/
public long sGetSetSize(String key){
try{
return redisTemplate.opsForSet().size(key);
}catch (Exception e){
e.printStackTrace();
return 0;
}
}
/**
* 移除值爲value的
* @param key Key值
* @param values value值 - 可以有多個
*/
public long setRemove(String key, Object... values){
try{
Long count = redisTemplate.opsForSet().remove(key,values);
return count;
}catch (Exception e){
e.printStackTrace();
return 0;
}
}
// ================================== List操作 ====================================
/**
* 獲取List緩存的長度
* @param key Key值
*/
public long lGetListSize(String key){
try {
return redisTemplate.opsForList().size(key);
}catch (Exception e){
e.printStackTrace();
return 0;
}
}
/**
* 通過索引獲取list中的值
* @param key Key值
* @param index 索引 index>=0時, 0 表頭,1 第二個元素,依次類推;index<0時,-1,表尾,-2倒數第二個元素,依次類推
*/
public Object lGetIndex(String key, long index){
try{
return redisTemplate.opsForList().index(key,index);
}catch (Exception e){
e.printStackTrace();
return null;
}
}
/**
* 將list放入緩存
* @param key Key值
* @param value Value值
*/
public boolean lSet(String key, Object value){
try{
redisTemplate.opsForList().rightPush(key,value);
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 將list放入緩存
* @param key key值
* @param value value值
* @param time 時間(秒)
*/
public boolean lSet(String key, Object value, long time){
try{
redisTemplate.opsForList().rightPush(key,value);
if (time > 0){
expire(key,time);
}
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 將list放入緩存
* @param key Key值
* @param values Value值 - List<Object>
*/
public boolean lSet(String key, List<Object> values){
try {
redisTemplate.opsForList().rightPushAll(key,values);
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 將list放入緩存
* @param key Key值
* @param values Value值 - List<Object>
* @param time 時間(秒)
*/
public boolean lSet(String key, List<Object> values, long time){
try {
redisTemplate.opsForList().rightPushAll(key, values);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根據索引修改list中的某條數據
* @param key key值
* @param index 索引
* @param value value值
*/
public boolean lUpdateIndex(String key, long index, Object value){
try{
redisTemplate.opsForList().set(key, index, value);
return true;
}catch (Exception e){
e.printStackTrace();
return false;
}
}
/**
* 移除N個值爲value
* @param key key值
* @param count 移除多少個
* @param value value值
*/
public long lRemove(String key, long count, Object value){
try {
Long remove = redisTemplate.opsForList().remove(key,count,value);
return remove;
}catch (Exception e){
e.printStackTrace();
return 0;
}
}
}
使用Cache
配置文件說明
# 緩存使用Redis
spring.cache.type=redis
# 過期時間 - 3600000ms
spring.cache.redis.time-to-live=3600000
# key的前綴,如果指定了前綴是用指定的
#spring.cache.redis.key-prefix=CACHE_
# 是否使用前綴 - 即註解中的value指定的值,使用的話就會有分區
spring.cache.redis.use-key-prefix=true
# 是否存儲空值,防止緩存穿透
spring.cache.redis.cache-null-values=true
上面的RedisConfiguration
中已經指定了緩存管理器爲RedisCacheManager
,也就是說Cache現在是可以存到Redis中的。SpringCache的使用見 SpringCache緩存
1、創建POJO類
@Data
public class User implements Serializable {
private Long id;
private String name;
private String sex;
private Integer age;
public User(){}
public User(Long id,String name, String sex, Integer age) {
this.id = id;
this.name = name;
this.sex = sex;
this.age = age;
}
}
2、創建Service類
爲了方便測試,直接創建了一個Service類,並且直接通過SpringBooTest
來直接調用測試。
@Service
@CacheConfig(cacheNames = "user_cache") // 緩存名
public class UserService {
@Cacheable(key = "#id") // 緩存key
public User getUser(Long id){
System.out.println("直接獲取User信息:");
User user = new User(1L,"柳成蔭","男",22);
return user;
}
}
3、測試
@Autowired
private UserService userService;
@Test
public void test1(){
User user = userService.getUser(2L);
System.out.println(user);
}
4、測試結果
通過RedisDesktopManager查看存儲情況。
5、可以使用自定義的Key
使用自定義Key規則,直接在RedisConfiguration
中重寫KeyGenerator
方法即可,示例:
/**
* 自定義生成Key的規則
* @return
*/
@Override
public KeyGenerator keyGenerator() {
return new KeyGeneratorImpl();
}
/**
* 生成Key的規則
*/
private class KeyGeneratorImpl implements KeyGenerator{
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName()).append("."); // 追加類名
sb.append(method.getName()).append("(");
for (Object object : params){
sb.append(object.toString()).append(",");
}
sb.deleteCharAt(sb.length() - 1);
sb.append(")");
System.out.println("調用了Redis緩存Key:" + sb.toString());
return sb.toString();
}
}
在Service類上或類中方法上指定緩存名
即可:
@Service
public class UserService {
@Cacheable(value = "abc") // 緩存名:abc
public User getUser(Long id){
System.out.println("直接獲取User信息:");
User user = new User(1L,"柳成蔭","男",22);
return user;
}
}
測試之後,結果如下:
SpringCache的不足
1、讀模式
緩存穿透:查詢一個null數據。解決方案:緩存空數據。
在配置文件中配置:
# 是否存儲空值,防止緩存穿透
spring.cache.redis.cache-null-values=true
緩存擊穿:大量併發進來同時查詢一個正好過期的數據。解決方案:加鎖。
在@Cacheable
中添加sync
屬性:
@Cacheable(value = {"category"}, key = "#root.method.name", sync = true)
緩存雪崩:大量的key同時過期。解決方案:加隨機過期時間。
在配置文件中配置:
# 過期時間 - 3600000ms
spring.cache.redis.time-to-live=3600000
2、寫模式
1、讀寫加鎖
2、引入Cannal,感知MySQL的更新
3、讀多寫多,直接查詢數據庫
SpringCache總結
1、常規性數據(讀多寫少,及時性和一致性要求不高的數據),完全可以使用SpringCache。寫模式只要緩存的數據有過期時間就足夠了。
2、特殊數據,特殊設計
序列化總結
①JdkSerializationRedisSerializer
是默認序列化方式,是最簡單的也是最安全的,只要實現了Serializer接口,實體類型,集合,Map等都能序列化與反序列化,但缺陷是序列化數據很多,會對redis造成更大壓力,且可讀性和跨平臺基本無法實現
②Jackson2JsonRedisSerializer
用的是json
的序列化方式,能解決JdkSerializationRedisSerializer
帶來的缺陷。
③jdk
的序列化方式字符串會比json
序列化文本大5倍。
SpringCache+Redis可以參考:SpringBoot整合Redis、SpringDataRedis最佳實踐