從零開發短視訊電商 快取Cache實戰Simple、Caffeine和Redis多快取管理器

2022-01-08 10:00:11

SpringBoot整合快取Cache

檔案:https://docs.spring.io/spring-boot/docs/2.3.12.RELEASE/reference/html/spring-boot-features.html

1.增加pom依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

支援以下幾種快取實現,預設自動根據程式碼依賴判斷使用哪種實現,也可以在組態檔中強制指定使用哪種快取實現。

當然也可以自實現 cacheManager,隨便參照simple或者redis如何實現的即可。

2.啟用快取功能

在設定類中新增@EnableCaching來啟用快取功能

@Configuration
@EnableCaching
public class CachingConfig {
}

預設情況下,如果我們沒有明確指定任何其他快取,會自動根據依賴環境判斷,如果上圖的依賴都沒有,它預設使用ConcurrentHashMap作為底層快取。即Simple的型別,參考程式碼SimpleCacheConfiguration.javaConcurrentMapCacheManager.java

也可以通過組態檔直接指定:

spring:
  cache:
    type: simple

常見快取操作

  • 使用註解@Cacheable等。
  • 使用快取管理器CacheManager

快取

@Cacheable(value = "user", key = "#id", unless="#result == null")
public User getUser(long id) {...}

cacheNames/value:指定快取元件的名字;將方法的返回結果放在哪個快取中,是陣列的方式,可以指定多個快取。必填

key:快取的 key,可以為空,如果指定要按照 SpEL 表示式編寫,如果不指定,則預設按照方法的所有引數進行組合。

keyGenerator:key的生成器;可以自己指定key的生成器的元件id,key/keyGenerator:二選一使用。

cacheManager:當自己設定了CacheManager後可以指定使用哪個快取管理器,預設使用的是Springboot自動設定的快取管理器;或者cacheResolver指定獲取解析器。

condition:快取的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行快取. condition="#userName.length()>2"

unless:否定快取;當unless指定的條件為true,方法的返回值就不會被快取;可以獲取到結果進行判斷。

sync:是否開啟同步功能,預設不開啟。開啟後unless屬性將不能使用。

getUser()將首先檢查快取是否存在,不存在則呼叫實際的方法,最後快取結果;存在則直接返回快取結果,跳過呼叫實際方法。

或者使用快取管理器。

@Service
public class UserService {
    @Autowired
    CacheManager cacheManager;
    public User getUser(long id) {
        if(cacheManager.containsKey(id)) {
            return cacheManager.get(id);
        }
        // lookup address, cache result, and return it
    }
}

清除快取

指定刪除一個或多個或所有值,以便可以再次將新值載入到快取中。

@CacheEvict(value="user", allEntries=true)
public User updateUser(long id) {...}

allEntries:是否刪除所有值。預設false,預設情況下,只刪除關聯鍵下的值。注意,不允許將這個引數設定為true並指定一個鍵。

beforeInvocation:預設false,是否應該在呼叫方法之前發生驅逐。將此屬性設定為true,將導致驅逐發生,而不考慮方法的結果(即,是否丟擲異常)。

​ 預設值為false,意味著快取回收操作將在被建議的方法被成功呼叫後發生(也就是說,只有在呼叫沒有丟擲異常的情況下)。

或者使用快取管理器

@Autowired
CacheManager cacheManager;
public void evictSingleCacheValue(String cacheName, String cacheKey) {
    cacheManager.getCache(cacheName).evict(cacheKey);
}
public void evictAllCacheValues(String cacheName) {
    cacheManager.getCache(cacheName).clear();
}
public void evictAllCaches() {
    cacheManager.getCacheNames().stream()
      .forEach(cacheName -> cacheManager.getCache(cacheName).clear());
}

更新快取

實際我們不用這個,高並行下會導致更新丟失問題或者鎖問題。這裡僅做介紹哈

@CachePut(value="addresses")
public String getAddress(Customer customer) {...}

組合快取

@Caching(evict = { 
  @CacheEvict("addresses"), 
  @CacheEvict(value="directory", key="#customer.name") })
public String getAddress(Customer customer) {...}

可以使用@Caching多個快取註解組合,使用它來實現我們自己的自定義快取邏輯。

類快取設定

使用@CacheConfig註解,我們可以在類級別將一些快取設定簡化到一個地方

@CacheConfig(cacheNames={"addresses"})
public class CustomerDataService {
    @Cacheable
    public String getAddress(Customer customer) {...}

SpEL上下文資料

Spring Cache提供了一些供我們使用的SpEL上下文資料,下表直接摘自Spring官方檔案:

名稱位置描述範例
methodNameroot物件當前被呼叫的方法名#root.methodname
methodroot物件當前被呼叫的方法#root.method.name
targetroot物件當前被呼叫的目標物件範例#root.target
targetClassroot物件當前被呼叫的目標物件的類#root.targetClass
argsroot物件當前被呼叫的方法的參數列#root.args[0]
cachesroot物件當前方法呼叫使用的快取列表#root.caches[0].name
Argument Name執行上下文當前被呼叫的方法的引數,如findArtisan(Artisan artisan),可以通過#artsian.id獲得引數#artsian.id
result執行上下文方法執行後的返回值(僅當方法執行後的判斷有效,如 unless cacheEvict的beforeInvocation=false)#result

注意:

1.當我們要使用root物件的屬性作為key時我們也可以將「#root」省略,因為Spring預設使用的就是root物件的屬性。 如

@Cacheable(key = "targetClass + methodName +#p0")

2.使用方法引數時我們可以直接使用「#引數名」或者「#p引數index」。 如:

@Cacheable(value="users", key="#id")
@Cacheable(value="users", key="#p0")

SpEL提供了多種運運算元

型別運運算元
關係<,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne
算術+,- ,* ,/,%,^
邏輯&&,||,!,and,or,not,between,instanceof
條件?: (ternary),?: (elvis)
正規表示式matches
其他型別?.,?[…],![…],1,$[…]

Cache實現之Redis快取管理器

檔案:https://spring.io/projects/spring-data-redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
需要引入commons-pool2作為連線池 必須!!!
<!--spring2.0整合redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

版本依賴如下

使用的JavaRedis使用者端是Lettuce因為 Spring Boot 預設使用它。

application.yaml檔案中設定屬性:

spring:
  cache:
    # 指定使用redis 
    type: redis
  redis:
    # reids的連線ip
    host: 127.0.0.1
    port: 6379
    password: laker123
    # Redis預設情況下有16個分片,這裡設定具體使用的分片,預設是0
    database: 0
    # 連線超時時間(毫秒)
    timeout: 10000ms
    #  redis client設定,使用lettuce
    lettuce:
      pool:
        # 連線池中的最小空閒連線 預設 0
        min-idle: 0
        # 連線池最大阻塞等待時間(使用負值表示沒有限制) 預設 -1
        max-wait: 1000ms
        # 連線池最大連線數(使用負值表示沒有限制) 預設 8
        max-active: 8
        # 連線池中的最大空閒連線 預設 8
        max-idle: 8

注意注意注意!!!,這裡redis預設的序列化為JDK序列化,所以上面的User實體類一定要實現序列化public class User implements Serializable,否則會報java.io.NotSerializableException異常。

@Cacheable(value = "user", key = "#id", unless="#result == null")
public User getUser(long id) {...}

getUser(1)其對應結果如下圖:

這裡我們後邊會修改其預設的序列化為json格式。

框架會自動生成一個RedisTemplate 範例 ,我們也可以直接使用RedisTemplate操作快取。

@Autowired
private RedisTemplate redisTemplate;
public void save(User user) {
    redisTemplate.opsForValue().set(user.getId(), user);
}
public User findById(Long id) {
    return (User)redisTemplate.opsForValue().get(id);
}

RedisTemplate 是執行緒安全的哦

預設情況下,Lettuce 會為我們管理序列化和反序列化,我們來自定義設定RedisTemplate

@Configuration
@EnableCaching
public class CachingConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setKeySerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        return redisTemplate;
    }
}

Jackson2JsonRedisSerializer和GenericJackson2JsonRedisSerializer的區別

  • 使用Jackson2JsonRedisSerializer需要指明序列化的類Class,可以使用Obejct.class

  • 使用GenericJacksonRedisSerializer比Jackson2JsonRedisSerializer效率低,佔用記憶體高。

  • GenericJacksonRedisSerializer反序列化帶泛型的陣列類會報轉換異常,解決辦法儲存以JSON字串儲存。

  • GenericJacksonRedisSerializer和Jackson2JsonRedisSerializer都是以JSON格式去儲存資料,都可以作為Redis的序列化方式。

來自:https://blog.csdn.net/bai_bug/article/details/81222519

redisTemplate.opsForValue().set(id,user);

迴歸Cache中RedisCacheManager自定義設定如下:

方式一 RedisCacheConfiguration

@Bean
public RedisCacheConfiguration cacheConfiguration() {
    return RedisCacheConfiguration.defaultCacheConfig()
      .entryTtl(Duration.ofMinutes(60))
      .disableCachingNullValues()
      .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}

方式二 RedisCacheManagerBuilderCustomizer

@Bean
public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
    return (builder) -> builder
      .withCacheConfiguration("itemCache",
        	RedisCacheConfiguration.defaultCacheConfig()
                              .entryTtl(Duration.ofMinutes(10)))
      .withCacheConfiguration("customerCache",
        	RedisCacheConfiguration.defaultCacheConfig()
                              .entryTtl(Duration.ofMinutes(5)));
}

分別為itemCachecustomerCache設定了 10 分鐘和 5 分鐘的 TTL 值。

方式三 CachingConfigurerSupport

擴充套件CachingConfigurerSupport類並覆蓋cacheManager () 方法。此方法返回一個 bean,它將成為我們應用程式的預設快取管理器:

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
  	@Override
	public CacheManager cacheManager() {...}
	@Override
	public CacheResolver cacheResolver() {...}
	@Override
	public KeyGenerator keyGenerator() {...}
	@Override
	public CacheErrorHandler errorHandler() {...}
}

設定多個快取管理器並動態切換

在某些情況下,我們可能需要在應用程式中使用多個快取管理器。

設定類中建立兩個快取管理器 bean。設定其中的一個 bean為@Primary

@Configuration
@EnableCaching
public class MultipleCacheManagerConfig {
    @Bean
    @Primary
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("customers", "orders");
        cacheManager.setCaffeine(Caffeine.newBuilder()
          .initialCapacity(200)
          .maximumSize(500)
          .weakKeys()
          .recordStats());
        return cacheManager;
    }
    @Bean
    public CacheManager alternateCacheManager() {
        return new ConcurrentMapCacheManager("customerOrders", "orderprice");
    }
}

現在,Spring Boot 將使用CaffeineCacheManager作為所有快取方法的預設值,直到我們為一個方法明確指定了AlternateCacheManager

@Cacheable(cacheNames = "customers")
public Customer getCustomerDetail(Integer customerId) {
    return customerDetailRepository.getCustomerDetail(customerId);
}
@Cacheable(cacheNames = "customerOrders", cacheManager = "alternateCacheManager")
public List<Order> getCustomerOrders(Integer customerId) {
    return customerDetailRepository.getCustomerOrders(customerId);
}

整體範例

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        redisTemplate.setKeySerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
        return redisTemplate;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1)) //設定所有快取有效時間 為 1個小時。
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer))
                .disableCachingNullValues(); // 禁用快取空值.
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(config).build();
        return cacheManager;
    }
}

Cache實現之Caffeine快取管理器

CaffeineCacheManagerspring-boot-starter-cache starter 提供。如果依賴存在Caffeine,它將由 Spring 自動設定, Caffeine是一個用 Java 8 編寫的快取庫。

增加依賴Caffeine
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

可以自己客製化設定,控制快取行為的主要設定,例如過期、快取大小限制等

@Bean
public Caffeine caffeineConfig() {
    return Caffeine.newBuilder()
               .expireAfterWrite(60, TimeUnit.MINUTES);
}
@Bean
public CacheManager cacheManager(Caffeine caffeine) {
    CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
    caffeineCacheManager.setCaffeine(caffeine);
    return caffeineCacheManager;
}

參考:

  • https://www.baeldung.com/spring-boot-redis-cache

  1. ↩︎