­

用註解解決業務邏輯和快取邏輯的深度耦合

  • 2019 年 10 月 5 日
  • 筆記

介紹

spring3.1引入了基於註解的快取技術,即spring cache模組,它不是一個具體的快取實現方案,而是一個對快取使用的抽象。你可以類比為JDBC,定義了一系列快取操作的介面,由具體的快取來實現,如Ehcache,Redis等。

演示一下我們一般是怎麼操作快取的

實體類

@Data  @NoArgsConstructor  public class Account {    private int id;    private String name;    private String password;      public Account(String name) {      this.name = name;    }  }  

快取類,這個簡單用ConcurrentHashMap來演示一下

public class Cache<T> {      private Map<String, T> cache = new ConcurrentHashMap<String,T>();      public T getValue(Object key) {      return cache.get(key);    }      public void addOrUpdateCache(String key,T value) {      cache.put(key, value);    }      // 根據 key 來刪除快取中的一條記錄    public void evictCache(String key) {      if(cache.containsKey(key)) {        cache.remove(key);      }    }      // 清空快取中的所有記錄    public void evictCache() {      cache.clear();    }  }  

服務類

public class AccountService {      private Cache<Account> cache = new Cache<>();      public Account getAccountByName(String name) {      Account result = cache.getValue(name);      // 如果在快取中,則直接返回快取的結果      if (result != null) {        System.out.println("get from cache " + name);        return result;      }      result = getFromDB(name);      // 將資料庫查詢的結果更新到快取中      if (result != null) {        cache.addOrUpdateCache(name, result);      }      return result;    }      private Account getFromDB(String name) {      System.out.println("get from db " + name);      return new Account(name);    }  }  

測試類

public class Main {      public static void main(String[] args) {        /**       * get from db aaa       * get from cache aaa       */      AccountService s = new AccountService();      s.getAccountByName("aaa");      s.getAccountByName("aaa");    }  }  

結果和我們預想的一樣,第一次從資料庫中拿,第二次就從快取中拿

這樣寫有什麼問題呢?

1.快取程式碼和業務程式碼耦合度太高 2.目前快取存儲這塊寫的比較死,不能靈活的切換為第三方模組,當然你可以再抽象一層。

我們用Spring Cache改造一下上面的程式碼

在Spring Boot中使用Spring Chache

1.在pom文件中加入依賴

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

2.在配置類中加@EnableCaching註解

@SpringBootApplication  @EnableCaching  public class CacheDemoApplication {      public static void main(String[] args) {          SpringApplication.run(CacheDemoApplication.class, args);      }  }  

以@Enable開頭的這種類型的註解在Spring Boot和Spring Cloud項目中還是經常出現的,這種類型的註解是一個開關註解,對於調試還是非常方便的。比如調試中你不想用快取,你就可以把這個註解注釋掉。這樣快取相關的註解都不能使用了。

服務類改成如下形式

@Service  public class AccountService {      @Cacheable(value = "cache", key = "#name")    public Account getAccountByName(String name) {      return getFromDB(name);    }      @Cacheable(value = "cache1", key = "#name")    public Account getAccountByNameCache1(String name) {      return getFromDB(name);    }      @Cacheable(value = "cache2", key = "#name")    public Account getAccountByNameCache2(String name) {      return getFromDB(name);    }      private Account getFromDB(String name) {      System.out.println("get from db " + name);      return new Account(name);    }  }  

現在你只看getAccountByName方法

@Test  public void test0() {        /**       * get from db 1       * get from db 2       */      accountService.getAccountByName("1");      accountService.getAccountByName("1");      accountService.getAccountByName("2");      accountService.getAccountByName("2");  }  

對於不同的key,第一次走資料庫,第二次走快取。完全符合我們的預期,而我們只用了一個註解。

spring cache的整體設計就類似下面這個map

Map<String, Map<String, String>> cacheManager;  

最外層的map是快取的名字(因為有可能有多個快取),裡面的map是快取的key和快取的value。

最外層的map對應spring cache的CacheManager介面(管理多個快取),實現類有EhCacheCacheManager和ConcurrentMapCacheManager等

裡面的map對應spring cache的Cache介面(定義快取的具體操作,如put和get等),實現類有EhCacheCache和ConcurrentMapCache等

Spring cache默認使用的是ConcurrentMapCacheManager,即把快取數據放在ConcurrentHashMap中。所以如果你想使用第三方快取只要注入對應的CacheManager實現類和Cache實現類就行,或者你自己寫實現類

接著來說上面用到的註解

@Cacheable(value = "cache", key = "#name")  

@Cacheable的value值為cache的名字,key為快取的key(使用SpEL表達式),快取的key有多種指定方式,我這裡只按照name進行了快取。實際的key有多個組合方式,還能設定key的快取條件。更多使用方式看最後的參考資料。

所以現在你明白@Cacheable的作用了把,如果能從快取中取到值,就從快取中取,否則就從資料庫中取,如果取到值,再把值放到快取中。背後的原理就是Spring Aop我就不深入解釋了。

當初我為了驗證我的想法,對getAccountByName寫了好幾個,就是@Cacheable中的value不同。

用來測試@Cacheable的value值是指定快取的名字

@Test  public void test1() {      /**       * get from db 1       * get from db 1       */      accountService.getAccountByNameCache1("1");      accountService.getAccountByNameCache2("1");  }  

列印所有快取的名字

@Autowired  CacheManager cacheManager;    @Test  public void test3() {        /**       * get from db 1       * get from db 1       * cache2       * cache1       */      accountService.getAccountByNameCache1("1");      accountService.getAccountByNameCache2("1");      Collection<String> collection = cacheManager.getCacheNames();      collection.forEach(item ->{          System.out.println(item);      });  }  

我再演示一下其他註解的使用

@Service  @CacheConfig(cacheNames = "cache")  public class AccountService1 {      @Cacheable(key = "#name")    public Account getAccountByName(String name) {      return getFromDB(name);    }      @CachePut(key = "#account.getName()")    public Account updateAccount(Account account) {      return account;    }      @CacheEvict(key = "#name")    public void deleteAccount(String name) {      }      private Account getFromDB(String name) {      System.out.println("get from db " + name);      return new Account(name);    }  }  

在前面的示例中,我們用到了@Cacheable註解,每次都得指明value屬性,即快取的名字。如果不想每次指定快取的名字,就可以用@CacheConfig註解在類上統一指定一個快取的名字。

@CachePut能夠根據方法的請求參數對其結果進行快取,和 @Cacheable 不同的是,它每次都會觸發真實方法的調用,所以這個註解經常用在更新操作上

@CacheEvict根據條件對快取清空,所以一般用在刪除方法上

@Test  public void test6() {      /**       * get from db aaa       * get from db aaa       */      accountService1.getAccountByName("aaa");      accountService1.deleteAccount("aaa");      accountService1.getAccountByName("aaa");  }