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

我欲只争朝夕
发布于 2023-10-24 11:08
浏览
0收藏

介绍

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

GitHub地址

​https://github.com/erlieStar/spring-boot-cache-demo​

参考资料

​https://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/​


文章转载自公众号:Java识堂

分类
标签
已于2023-10-24 11:09:09修改
收藏
回复
举报
回复
    相关推荐