Spring缓存总结及源码分析

/ Spring / 2655浏览

Spring为我们提供了几个注解来支持缓存。其核心注解包括:@Cacheable、@CachePut、@CacheEvict。

@Cacheable标记的方法在执行后Spring Cache将缓存其返回结果,而使用@CacheEvict标记的方法会在方法执行前或者执行后移除Spring Cache中的某些元素。

配置Redis缓存的支持

Maven依赖

在pom.xml中,增加:

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

Bean配置

具体配置如下:

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.CacheKeyPrefix;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.*;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

/**
 * redis 配置
 */
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(CacheProperties.class)
public class RedisConfig extends CachingConfigurerSupport {

   /**
    * Redis模板 默认序列化方式
    * @param factory
    * @return
    */
   @Bean
   public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

   @Bean
   public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
      return new RedisCacheManager(
         RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
         // 默认策略,未配置的 key 会使用这个
         this.getRedisCacheConfigurationWithTtl(30),
         // 指定 key 策略
         this.getRedisCacheConfigurationMap()
      );
   }

   /**
    * 指定不同key的缓存策略
    * @return
    */
   private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
      Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
      // 菜单json树缓存
      redisCacheConfigurationMap.put("string:menu-tree", this.getRedisCacheConfigurationWithTtl(600));
      // 数据字典缓存
      redisCacheConfigurationMap.put("string:data-dictionary", this.getRedisCacheConfigurationWithTtl(600));

      return redisCacheConfigurationMap;
   }

   /**
    * 默认key的缓存策略
    * @param minutes
    * @return
    */
   private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer minutes) {
      Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

      // 配置对象映射
      ObjectMapper om = new ObjectMapper();
      om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
      om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
      jackson2JsonRedisSerializer.setObjectMapper(om);

      // Redis 缓存设置
      RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();

      redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
            RedisSerializationContext
                  .SerializationPair
                  .fromSerializer(jackson2JsonRedisSerializer)
      ).entryTtl(Duration.ofMinutes(minutes));

      // 自定义前缀 (:一个冒号)
      redisCacheConfiguration = redisCacheConfiguration.computePrefixWith(myKeyPrefix());

      return redisCacheConfiguration;
   }

   /**
    * key 生成
    * @return
    */
   @Bean
   public KeyGenerator simpleKeyGenerator() {
      return (o, method, objects) -> {
         StringBuilder stringBuilder = new StringBuilder();
         stringBuilder.append(o.getClass().getSimpleName());
         stringBuilder.append(".");
         stringBuilder.append(method.getName());
         stringBuilder.append("[");
         for (Object obj : objects) {
            stringBuilder.append(obj.toString());
         }
         stringBuilder.append("]");
         String build = stringBuilder.toString();
         return build;
      };
   }

   /**
    * 缓存前缀(追加一个冒号 : )
    * @return
    */
   private CacheKeyPrefix myKeyPrefix(){
      return name -> name + ":";
   }
}

其中,主要是配置CacheManager,里面分别对不同的缓存key,配置不同的过期策略,比如某些 key 需要较长时间过期,某些 key 可能较短时间过期,那么就分别进行配置,加入到map中即可。

注解使用

@Cacheable

注解参数:

属性名必填描述
valueY缓存的命名空间
keyN指定一个唯一的key(在缓存命名空间中),使用SpEL表达式
conditionN限定条件,哪种情况使用缓存,使用SpEL表达式
unlessN限定条件,哪种情况下不使用缓存,使用SpEL表达式

下面是列举一些各种缓存的形式:

1.基本形式:

@Cacheable(value="cacheName", key"#id")
public ResultDTO method(int id); 

2.组合形式 :

@Cacheable(value="string:menu-tree-json",
        key = "'['+T(String).valueOf(#mode).concat('-').concat(#roleId)+']'")
@Override
public String getTreeJson(int mode,long roleId);

3.多个参数拼接 :

@Cacheable(value = "findByUsernameAndTypeCached", key = "#username + '_' + #type")
public User findByUsernameAndTypeCached(String username, Integer type); 

4.复杂参数(数组) :

@Cacheable(value = "findByUserIdsCached", key = "#userIds[0] + ''")
public User findByUserIdsCached(Integer[] userIds);

5.对象形式:

@Cacheable(value="cacheName", key"#user.id)
public ResultDTO method(User user); 

6.自定义key:

@Cacheable(value="gomeo2oCache", keyGenerator = "keyGenerator")
public ResultDTO method(User user); 

另外,Spring默认提供了EL表达式获取一些值:

属性描述示例
methodName当前方法名#root.methodName
method当前方法#root.method.name
target当前被调用的对象#root.target
targetClass当前被调用的对象的class#root.targetClass
args当前方法参数数组#root.args[0]
caches当前被调用的方法使用的Cache#root.caches[0].name

@CachePut

此注解和@Cacheable 使用上是一样的,区别是,@CachePut 注解声明后,每次都存储到缓存,而@Cacheable 则会先检查是否存在,如果不存在才进行缓存数。

@CacheEvict

缓存清除注解,标记在类或方法上执行都会触发缓存的清除操作。 @CacheEvict可以指定的属性有value、key、condition、allEntries和beforeInvocation。其中value、key和condition的语义与@Cacheable对应的属性类似。即value表示清除操作是发生在哪些Cache上的(对应Cache的名称);key表示需要清除的是哪个key,如未指定则会使用默认策略生成的key;condition表示清除操作发生的条件。下面我们来介绍一下新出现的两个属性allEntries和beforeInvocation。

缓存源码解读

缓存初始化

Spring的中央缓存管理器在配置文件中可以得知,位于 org.springframework.cache.CacheManager:

public interface CacheManager {
   @Nullable
   Cache getCache(String name);

   Collection<String> getCacheNames();
}

CacheManager是Spring缓存的中央管理器,它的成员方法很简单,一个是获取缓存对象Cache,一个是获取缓存名称的集合,集合就是String的列表,那么在看一下Cache的方法:

接口一共定义了9个方法,都是对缓存进行存、查、清除等操作,一目了然。

因此我们所有缓存实现都要实现CacheManager接口,这里我们以Redis缓存为例分析,在配置CacheManager Bean中我们返回的是RedisCacheManager,来看一下类图:

可以看到 RedisCacheManager 继承了 AbstractTransactionSupportingCacheManager ,而AbstractTransactionSupportingCacheManager 继承了 AbstractCacheManager ,最后 AbstractCacheManager 实现了 CacheManager 和 InitializingBean 接口。

我们已知道CacheManager是缓存管理器,主要作用是获得缓存,那么InitializingBean 接口主要作用是初始化Spring的Bean,里面只有一个方法:void afterPropertiesSet() throws Exception;,执行时机是BeanFactory在设置了提供的所有bean属性之后调用。

接下来看下 AbstractCacheManager ,它是抽象缓存管理器,主要作用是对缓存管理器和初始化进行了默认的实现,看一下此了的主要方法:

public abstract class AbstractCacheManager implements CacheManager, InitializingBean {

   private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);

   private volatile Set<String> cacheNames = Collections.emptySet();

   // Early cache initialization on startup

   @Override
   public void afterPropertiesSet() {
      initializeCaches();
   }
   
   public void initializeCaches() {
		Collection<? extends Cache> caches = loadCaches();

		synchronized (this.cacheMap) {
			this.cacheNames = Collections.emptySet();
			this.cacheMap.clear();
			Set<String> cacheNames = new LinkedHashSet<>(caches.size());
			for (Cache cache : caches) {
				String name = cache.getName();
				this.cacheMap.put(name, decorateCache(cache));
				cacheNames.add(name);
			}
			this.cacheNames = Collections.unmodifiableSet(cacheNames);
		}
	}
	
	protected abstract Collection<? extends Cache> loadCaches();
	
	@Override
	@Nullable
	public Cache getCache(String name) {
		Cache cache = this.cacheMap.get(name);
		if (cache != null) {
			return cache;
		}
		else {
			// Fully synchronize now for missing cache creation...
			synchronized (this.cacheMap) {
				cache = this.cacheMap.get(name);
				if (cache == null) {
					cache = getMissingCache(name);
					if (cache != null) {
						cache = decorateCache(cache);
						this.cacheMap.put(name, cache);
						updateCacheNames(name);
					}
				}
				return cache;
			}
		}
	}
	
	@Deprecated
	protected final void addCache(Cache cache) {
		String name = cache.getName();
		synchronized (this.cacheMap) {
			if (this.cacheMap.put(name, decorateCache(cache)) == null) {
				updateCacheNames(name);
			}
		}
	}
	
	// 省略部分源码......
}

首先是实现了 afterPropertiesSet 方法,调用了initializeCaches方法,作用是初始化静态配置文件中的缓存,可以看到此方法是线程安全的,使用了同步锁进行初始化,将loadCaches() 方法返回的集合添加到 cacheNames中,loadCaches()方法的实现在RedisCacheManager类中,可以看下方法:

// 加载所有配置的缓存到缓存集合中
@Override
protected Collection<RedisCache> loadCaches() {

   List<RedisCache> caches = new LinkedList<>();

   for (Map.Entry<String, RedisCacheConfiguration> entry : initialCacheConfiguration.entrySet()) {
      caches.add(createRedisCache(entry.getKey(), entry.getValue()));
   }

   return caches;
}

遍历的缓存集合是 initialCacheConfiguration,它是一个成员变量,在我们创建RedisCacheManager实例中,在构造器里赋予初始值。到这里也就清楚了,我们自定义的@Bean,CacheManager传入的缓存Map的作用了。

另外,AbstractCacheManager 抽象类的其它方法主要是 getCache 、和 addCache等,都是所用了同步锁进行对缓存进行获取和管理。

AbstractTransactionSupportingCacheManager 类实现了 AbstractCacheManager ,看下源码:

public abstract class AbstractTransactionSupportingCacheManager extends AbstractCacheManager {
   private boolean transactionAware = false;

   public void setTransactionAware(boolean transactionAware) {
      this.transactionAware = transactionAware;
   }

   public boolean isTransactionAware() {
      return this.transactionAware;
   }

   @Override
   protected Cache decorateCache(Cache cache) {
      return (isTransactionAware() ? new TransactionAwareCacheDecorator(cache) : cache);
   }
}

可以看出,它的核心功能就是对事务的支持,其默认情况下,是不支持事务的,因为 transactionAware 的默认值是 false,如果他们设置了为 true,那么将返回带有事务的 Cache对象。

缓存读写

接下来看下最底层的实现类 RedisCacheManager,即Redis缓存管理器,核心功能有2个:

这里看下是如何进行读写的,看下类图:

这个类提供了多种多样的构造器,供我们使用。另外很明显可以看出成员变量cacheWriter就是对缓存起到读写作用的,它的类型是 RedisCacheWriter接口:

里面主要定义了常用的缓存读写方法。其静态方法nonLockingRedisCacheWriter 和 lockingRedisCacheWriter,分别返回无锁和有锁的默认实现类 DefaultRedisCacheWriter:

此类主要有2个成员变量:

private final RedisConnectionFactory connectionFactory;
private final Duration sleepTime;

connectionFactory 是redis连接工厂,sleepTime即过期策略,默认的过期策略是 Duration.ZERO,也就是永久存储。

剩下的方法都是对缓存进行存取相关的,重点看下这个接口的实现接口:

public interface RedisConnection extends RedisCommands { // 省略... }

也就是RedisCommands,看下此接口源码,一眼看去,(๑ŐдŐ)b ,那么一大堆实现...

其实不用一一去具体看,都是对Redis各种数据类型、命令封装的接口,用到什么数据类型去使用里面的API就好了,可以参考Redis官网的API,这里面其实都是对官网Redis命令的封装而已,我们拿来就用即可。

public interface RedisCommands extends RedisKeyCommands, RedisStringCommands, RedisListCommands, RedisSetCommands,
      RedisZSetCommands, RedisHashCommands, RedisTxCommands, RedisPubSubCommands, RedisConnectionCommands,
      RedisServerCommands, RedisScriptingCommands, RedisGeoCommands, RedisHyperLogLogCommands {

   @Nullable
   Object execute(String command, byte[]... args);
}

比如进入RedisStringCommands这个接口:

有没有很熟悉,和Redis的命令几乎是对应的,^_^ .

好了,既然是命令接口,那么去找到相应的实现类,就可以观察我们缓存最终是在哪里进行执行的了。来看一下connection包:

可以看下这个包里面的两个子包,jedis 和 lettuce,也就是不同客户端的不同实现啦,由于我在用的是springboot2.x版本,其默认使用的是 lettuce 客户端实现的,所以我们可以去里面找到相应的实现类,LettuceStringCommands:

class LettuceStringCommands implements RedisStringCommands { //省略...... }

由于代码量过大,而且都是API,所以就不贴全部的源码了,看到这个类是实现了 RedisStringCommands 接口就明白了。

所以这里我设置了断点:

访问控制器,带有的注解,直接进入了断点:

往下调试 ..... 最后进入了CacheAspectSupport 类的 CachePutRequest 方法:

调试执行完成后(后面断点一直跳就不用理会了,重点部分调试完即可哈,不然没玩没了),可以看到数据已经保存到Redis中了:

缓存读的话,基本也是这个思路,后面有时间再补充吧。

另外,缓存写入时,SpringBoot2.x的默认前缀使用了两个 :: 冒号,在Redis客户端查看时很不方便,在RedisCacheConfiguration的构造方法可以看到:

public static RedisCacheConfiguration defaultCacheConfig() {
   // ...
   return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(),
         SerializationPair.fromSerializer(new StringRedisSerializer()),
         SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()), conversionService);
}

其中 CacheKeyPrefix.simple() 就是缓存前缀,点进去看一下:

static CacheKeyPrefix simple() {
   return name -> name + "::";
}

居然有两个 :: 有没有,多不方便看?? 所以如果想用一个 :的话,那么就自己实现了 CacheKeyPrefix 传入进去就可以了。如:

/**
 * 缓存前缀(追加一个冒号 : )
 * @return
 */
private CacheKeyPrefix myKeyPrefix(){
   return name - name + ":";
}

alt

配图:黑足猫