品味Spring Cache设计之美(一)

iamwaiwai
发布于 2022-5-17 17:29
浏览
0收藏

最近负责教育类产品的架构工作,两位研发同学建议:“团队封装的Redis客户端可否适配Spring Cache,这样加缓存就会方便多了” 。

 

于是边查阅文档边实战,收获颇丰,写这篇文章,想和大家分享笔者学习的过程,一起品味Spring Cache设计之美。品味Spring Cache设计之美(一)-鸿蒙开发者社区1 硬编码


在学习Spring Cache之前,笔者经常会硬编码的方式使用缓存。

 

举个例子,为了提升用户信息的查询效率,我们对用户信息使用了缓存,示例代码如下:

@Autowire
  private UserMapper userMapper;
  @Autowire
  private StringCommand stringCommand;
  //查询用户
  public User getUserById(Long userId) {
   String cacheKey = "userId_" + userId;
   User user=stringCommand.get(cacheKey);
   if(user != null) {
    return user;
   }
   user = userMapper.getUserById(userId);
   if(user != null) {
    stringCommand.set(cacheKey,user);
    return user;
   }
   //修改用户
   public void updateUser(User user){
    userMapper.updateUser(user);
    String cacheKey = "userId_" + userId.getId();
    stringCommand.set(cacheKey , user);
   }
   //删除用户
   public void deleteUserById(Long userId){
     userMapper.deleteUserById(userId);
     String cacheKey = "userId_" + userId.getId();
     stringCommand.del(cacheKey);
   }
  }

相信很多同学都写过类似风格的代码,这种风格符合面向过程的编程思维,非常容易理解。但它也有一些缺点:

 

1.代码不够优雅。业务逻辑有四个典型动作:存储,读取,修改,删除。每次操作都需要定义缓存Key ,调用缓存命令的API,产生较多的重复代码

 

2.缓存操作和业务逻辑之间的代码耦合度高,对业务逻辑有较强的侵入性。

 

侵入性主要体现如下两点:

 

      ◆ 开发联调阶段,需要去掉缓存,只能注释或者临时删除缓存操作代码,也容易出错;
      ◆ 某些场景下,需要更换缓存组件,每个缓存组件有自己的API,更换成本颇高。

 

2 缓存抽象


首先需要明确一点:Spring Cache不是一个具体的缓存实现方案,而是一个对缓存使用的抽象(Cache Abstraction)。品味Spring Cache设计之美(一)-鸿蒙开发者社区2.1 Spring AOP


Spring AOP是基于代理模式(proxy-based)。

 

通常情况下,定义一个对象,调用它的方法的时候,方法是直接被调用的。


品味Spring Cache设计之美(一)-鸿蒙开发者社区将代码做一些调整,pojo对象的引用修改成代理类。

ProxyFactory factory = new ProxyFactory(new SimplePojo());
factory.addInterface(Pojo.class);
factory.addAdvice(new RetryAdvice());

Pojo pojo = (Pojo) factory.getProxy(); 
//this is a method call on the proxy!
pojo.foo();

 调用pojo的foo方法的时候,实际上是动态生成的代理类调用foo方法。

 

代理类在方法调用前可以获取方法的参数,当调用方法结束后,可以获取调用该方法的返回值,通过这种方式就可以实现缓存的逻辑。

 

2.2  缓存声明


缓存声明,也就是标识需要缓存的方法以及缓存策略

 

Spring Cache 提供了五个注解。

 

  ◆ @Cacheable:根据方法的请求参数对其结果进行缓存,下次同样的参数来执行该方法时可以直接从缓存中获取结果,而不需要再次执行该方法;
  ◆ @CachePut:根据方法的请求参数对其结果进行缓存,它每次都会触发真实方法的调用;
  ◆ @CacheEvict:根据一定的条件删除缓存;
  ◆ @Caching:组合多个缓存注解;
  ◆ @CacheConfig:类级别共享缓存相关的公共配置。


我们重点讲解:@Cacheable,@CachePut,@CacheEvict三个核心注解。

 

2.2.1 @Cacheable注解


@Cacheble注解表示这个方法有了缓存的功能。

@Cacheable(value="user_cache",key="#userId", unless="#result == null")
public User getUserById(Long userId) {
  User user = userMapper.getUserById(userId);
  return user;
}

上面的代码片段里,getUserById方法和缓存user_cache 关联起来,若方法返回的User对象不为空,则缓存起来。第二次相同参数userId调用该方法的时候,直接从缓存中获取数据,并返回。

 

▍ 缓存key的生成

 

我们都知道,缓存的本质是key-value存储模式,每一次方法的调用都需要生成相应的Key, 才能操作缓存。

 

通常情况下,@Cacheable有一个属性key可以直接定义缓存key,开发者可以使用SpEL语言定义key值。

 

若没有指定属性key,缓存抽象提供了 KeyGenerator来生成key ,默认的生成器代码见下图: 

品味Spring Cache设计之美(一)-鸿蒙开发者社区

它的算法也很容易理解:

 

  ◆ 如果没有参数,则直接返回SimpleKey.EMPTY
  ◆ 如果只有一个参数,则直接返回该参数;
  ◆ 若有多个参数,则返回包含多个参数的SimpleKey对象。


当然Spring Cache也考虑到需要自定义Key生成方式,需要我们实现org.springframework.cache.interceptor.KeyGenerator 接口。

Object generate(Object target, Method method, Object... params);

然后指定@Cacheable的keyGenerator属性。

@Cacheable(value="user_cache", keyGenerator="myKeyGenerator", unless="#result == null")
public User getUserById(Long userId) 

▍ 缓存条件

 

有的时候,方法执行的结果是否需要缓存,依赖于方法的参数或者方法执行后的返回值。

 

注解里可以通过condition属性,通过Spel表达式返回的结果是true 还是false 判断是否需要缓存。

@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)

上面的代码片段里,当参数的长度小于32,方法执行的结果才会缓存。

 

除了condition,unless属性也可以决定结果是否缓存,不过是在执行方法后。

@Cacheable(value="user_cache",key="#userId", unless="#result == null")
public User getUserById(Long userId) {

上面的代码片段里,当返回的结果为null则不缓存。

 

2.2.2 @CachePut注解


@CachePut注解作用于缓存需要被更新的场景,和 @Cacheable 非常相似,但被注解的方法每次都会被执行。

 

返回值是否会放入缓存,依赖于condition和unless,默认情况下结果会存储到缓存。

@CachePut(value = "user_cache", key="#user.id", unless = "#result != null")
public User updateUser(User user) {
    userMapper.updateUser(user);
    return user;
}

当调用updateUser方法时,每次方法都会被执行,但是因为unless属性每次都是true,所以并没有将结果缓存。当去掉unless属性,则结果会被缓存。

 

2.2.3 @CacheEvict注解


@CacheEvict 注解的方法在调用时会从缓存中移除已存储的数据。

@CacheEvict(value = "user_cache", key = "#id")
public void deleteUserById(Long id) {
    userMapper.deleteUserById(id);
}

当调用deleteUserById方法完成后,缓存key等于参数id的缓存会被删除,而且方法的返回的类型是Void ,这和@Cacheable明显不同。

 

2.3 缓存配置


Spring Cache是一个对缓存使用的抽象,它提供了多种存储集成。品味Spring Cache设计之美(一)-鸿蒙开发者社区要使用它们,需要简单地声明一个适当的CacheManager - 一个控制和管理Cache的实体。

 

我们以Spring Cache默认的缓存实现Simple例子,简单探索下CacheManager的机制。

 

CacheManager非常简单:

public interface CacheManager {
   @Nullable
   Cache getCache(String name);
   
   Collection<String> getCacheNames();
}

在CacheConfigurations配置类中,可以看到不同集成类型有不同的缓存配置类。品味Spring Cache设计之美(一)-鸿蒙开发者社区通过SpringBoot的自动装配机制,创建CacheManager的实现类ConcurrentMapCacheManager。品味Spring Cache设计之美(一)-鸿蒙开发者社区ConcurrentMapCacheManager的getCache方法,会创建ConcurrentCacheMap品味Spring Cache设计之美(一)-鸿蒙开发者社区ConcurrentCacheMap实现了org.springframework.cache.Cache接口。品味Spring Cache设计之美(一)-鸿蒙开发者社区从Spring Cache的Simple的实现,缓存配置需要实现两个接口:

 

  ◆ org.springframework.cache.CacheManager


  ◆ org.springframework.cache.Cache

已于2022-5-17 17:29:39修改
收藏
回复
举报
回复
    相关推荐