用注解解决业务逻辑和缓存逻辑的深度耦合

  • 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");  }