Spring Boot In Practice (1):Redis快取實戰

我真的是故意的發表於2017-06-14

閱讀本文需要對Spring和Redis比較熟悉。

Spring Framework 提供了Cache Abstraction對快取層進行了抽象封裝,通過幾個annotation可以透明給您的應用增加快取支援,而不用去關心底層快取具體由誰實現。目前支援的快取有java.util.concurrent.ConcurrentMap,Ehcache 2.x,Redis等。

一般我們使用最常用的Redis做為快取實現(Spring Data Redis),

  • 需要引入的starter: spring-boot-starter-data-redis,spring-boot-starter-cache;
  • 自動配置生成的Beans: RedisConnectionFactory, StringRedisTemplate , RedisTemplate, RedisCacheManager,自動配置的Bean可以直接注入我們的程式碼中使用;

I. 配置

application.properties

# REDIS (RedisProperties)
spring.redis.host=localhost # Redis server host.
spring.redis.port=6379 # Redis server port.
spring.redis.password= # Login password of the redis server.複製程式碼

具體對Redis cluster或者Sentinel的配置可以參考這裡

開啟快取支援

@SpringBootApplication
@EnableCaching//開啟caching
public class NewsWebServer {
//省略內容
}複製程式碼

定製RedisTemplate

自動配置的RedisTemplate並不能滿足大部分專案的需求,比如我們基本都需要設定特定的Serializer(RedisTemplate預設會使用JdkSerializationRedisSerializer)。

Redis底層中儲存的資料只是位元組。雖然Redis本身支援各種型別(List, Hash等),但在大多數情況下,這些指的是資料的儲存方式,而不是它所代表的內容(內容都是byte)。使用者自己來決定資料如何被轉換成String或任何其他物件。使用者(自定義)型別和原始資料型別之間的互相轉換通過RedisSerializer介面(包org.springframework.data.redis.serializer)來處理,顧名思義,它負責處理序列化/反序列化過程。多個實現可以開箱即用,如:StringRedisSerializer和JdkSerializationRedisSerialize。Jackson2JsonRedisSerializer或GenericJackson2JsonRedisSerializer來處理JSON格式的資料。請注意,儲存格式不僅限於value 它可以用於key,Hash的key和value。

宣告自己的RedisTemplate覆蓋掉自動配置的Bean:

//通用的RedisTemplate
@Bean
public RedisTemplate<String, Object> redisTemplate(JedisConnectionFactory jedisConnectionFactory) {
    RedisTemplate<String, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(jedisConnectionFactory);
    template.setKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    //template.setHashKeySerializer(template.getKeySerializer());
    //template.setHashValueSerializer(template.getValueSerializer());
    return template;
}複製程式碼

這裡我們使用GenericJackson2JsonRedisSerializer而不是Jackson2JsonRedisSerializer,後者的問題是你需要為每一個需要序列化進Redis的類指定一個Jackson2JsonRedisSerializer因為其建構函式中需要指定一個型別來做反序列化:

redis.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));複製程式碼

如果我們應用中有大量物件需要快取,這顯然是不合適的,而前者直接把型別資訊序列化到了JSON格式中,讓一個例項可以操作多個物件的反序列化。

定製RedisCacheManager

有時候Spring Boot自動給我們配置的RedisCacheManager也不能滿足我們應用的需求,我看到很多用法都直接宣告瞭一個自己的RedisCacheManager,其實使用CacheManagerCustomizer可以對自動配置的RedisCacheManager進行定製化:

    @Bean
    public CacheManagerCustomizer<RedisCacheManager> cacheManagerCustomizer() {
        return new CacheManagerCustomizer<RedisCacheManager>() {
            @Override
            public void customize(RedisCacheManager cacheManager) {
                cacheManager.setUsePrefix(true); //事實上這是Spring Boot的預設設定,為了避免key衝突

                Map<String, Long> expires = new HashMap<>();
                expires.put("myLittleCache", 12L*60*60);  // 設定過期時間 key is cache-name
                expires.put("myBiggerCache", 24L*60*60);
                cacheManager.setExpires(expires);  // expire per cache

                cacheManager.setDefaultExpiration(24*60*60);// 預設過期時間:24 hours
            }
        };
    }複製程式碼

II. 使用

快取Key的生成

我們都知道Redis是一個key-value的儲存系統,無論我們想要快取什麼值,都需要制定一個key。

@Cacheable(cacheNames = "user")
 public User findById(long id) {
     return userMapper.findById(id);
 }複製程式碼

上面的程式碼中,findById方法返回的物件會被快取起來,key由預設的org.springframework.cache.interceptor.SimpleKeyGenerator生成,生成策略是根據被標註方法的引數生成一個SimpleKey物件,然後由RedisTemplate中定義的KeySerializer序列化後作為key(注意StringRedisSerializer只能序列化String型別,對SimpleKey物件無能為力,你只能定義其他Serializer)。

不過大多數情況下我們都會採用自己的key生成方案,方式有兩種:

1.實現自己的KeyGenerator;

@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {
    @Bean
    public KeyGenerator customKeyGenerator() {
        return new KeyGenerator() {
          @Override
          public Object generate(Object o, Method method, Object... objects) {
            StringBuilder sb = new StringBuilder();
            sb.append(o.getClass().getName());
            sb.append(method.getName());
            for (Object obj : objects) {
              sb.append(obj.toString());
            }
            return sb.toString();
          }
        };
   }
}複製程式碼

2.在@Cacheable標註中直接宣告key:

@Cacheable(cacheNames = "user", key="#id.toString()") ❶
 public User findById(long id) {
     return userMapper.findById(id);
 }

@Cacheable(cacheNames = "user", key="'admin'") ❷
 public User findAdmin() {
     return userMapper.findAdminUser();
 }

@Cacheable(cacheNames = "user", key="#userId + ':address'") ❸
 public List<Address> findUserAddress(long userId) {
     return userMapper.findUserAddress(userId);
 }複製程式碼

key的宣告形式支援SpEL
❶ 最終生成的Redis key為:user:100234,user部分是因為cacheManager.setUsePrefix(true),cacheName會被新增到key作為字首避免引起key的衝突。之所以#id.toString()要long型轉為String是因為我們設定的KeySerializer為StringRedisSerializer只能用來序列化String。
❷ 如果被標註方法沒有引數,我們可以用一個靜態的key值,最終生成的key為user:admin
❸ 最終生成的key為user:100234:address

這種方式更符合我們以前使用Redis的習慣,所以推薦。

直接使用RedisTemplate

有時候標註不能滿足我們的使用場景,我們想要直接使用更底層的RedisTemplate

@Service
public class FeedService {

    @Resource(name="redisTemplate") ❶
    private ZSetOperations<String, Feed> feedOp;

    public List<Feed> getFeed(int count, long maxId) {
        return new ArrayList<>(feedOp.reverseRangeByScore(FEED_CACHE_KEY, 0, maxId, offset, count));
    }   
  //省略
}複製程式碼

❶ 我們可以直接把RedisTemplate的例項注入為ZSetOperationsListOperationsValueOperations等型別(Spring IoC Container幫我們做了轉化工作,可以參考org.springframework.data.redis.core.ZSetOperationsEditor)。

除了當快取,Redis還能幹啥

org.springframework.data.redis.support包中提供了一些以Redis作為後端儲存的元件,包括原子計數器和Java Collections的一些實現類。

@Service
public class ActivityService {
    RedisAtomicInteger counter;
    public ActivityService(RedisConnectionFactory connectionFactory) {
        counter = new RedisAtomicInteger(counterKey,
                connectionFactory, 1);
    }
}複製程式碼

以上程式碼建立了一個分散式的原子計數器。

III. 參考

#coding/spring

相關文章