SpringBoot - 整合Redis緩存cache及鍵值序列化

前面的SpringCache緩存說過在沒有引入其他緩存中間件時,默認使用的是ConcurrentMapCacheManager=ConcurrentMapCache,是將數據保存在ConcurrentMap<Object, Object>中。
在實際開發中,我們一般都會使用redismemcachedehcache來作爲緩存中間件。

整合Redis

Redis是一個開源(BSD許可)的,內存中的數據結構存儲系統,它可以用作數據庫、緩存和消息中間件。
當我們引入Reidsstarter依賴的時候,使用RedisCacheManagerRedisCacheManager會幫我們創建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去查看存儲的數據,是無法用肉眼看出存的是什麼的。
JDK序列化機制
比如說我們要存一個對象進去,並且想直接肉眼看到是一個怎麼樣的數據,可以通過修改配置,讓其以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序列化的結果
RedisDesktopManager
取出來也是沒有問題的
在這裏插入圖片描述

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整合RedisSpringDataRedis最佳實踐

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