我知道这个问题已经有一段时间了,但是我最近又对这个话题做了一些研究,所以我想在这里分享一下这个“半散列”密钥是如何通过部分 spring 源代码生成的。
首先,Spring利用AOP来解析@Cacheable, @CacheEvict or @CachePut等注解。通知类是来自Spring-context依赖的CacheInterceptor,它是CacheAspectSupport的子类(也来自Spring-context)。为了便于解释,我将使用@Cacheable 作为示例,在这里查看部分源代码。
当注解为@Cacheable 的方法被调用时,AOP 会将其从CacheAspectSupport 类路由到此方法protected Collection<? extends Cache> getCaches(CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver),并在其中尝试解析此@Cacheable 注解。反过来,它会导致在实现 CacheManager 中调用此方法public Cache getCache(String name)。对于这个解释,实现 CacheManage 将是RedisCacheManager(来自 Spring-data-redis 依赖项)。
如果缓存没有被命中,它将继续创建缓存。下面是来自RedisCacheManager的关键方法:
protected Cache getMissingCache(String name) {
return this.dynamic ? createCache(name) : null;
}
@SuppressWarnings("unchecked")
protected RedisCache createCache(String cacheName) {
long expiration = computeExpiration(cacheName);
return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName) : null), redisOperations, expiration,
cacheNullValues);
}
本质上,它将实例化一个RedisCache 对象。为此,它需要 4 个参数,即 cacheName、prefix(这是回答这个问题的关键参数)、redisOperation(也就是配置的 redisTemplate)、expiration(默认为 0)和 cacheNullValues(默认为 false)。下面的构造函数展示了更多关于 RedisCache 的细节。
/**
* Constructs a new {@link RedisCache} instance.
*
* @param name cache name
* @param prefix must not be {@literal null} or empty.
* @param redisOperations
* @param expiration
* @param allowNullValues
* @since 1.8
*/
public RedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations,
long expiration, boolean allowNullValues) {
super(allowNullValues);
Assert.hasText(name, "CacheName must not be null or empty!");
RedisSerializer<?> serializer = redisOperations.getValueSerializer() != null ? redisOperations.getValueSerializer()
: (RedisSerializer<?>) new JdkSerializationRedisSerializer();
this.cacheMetadata = new RedisCacheMetadata(name, prefix);
this.cacheMetadata.setDefaultExpiration(expiration);
this.redisOperations = redisOperations;
this.cacheValueAccessor = new CacheValueAccessor(serializer);
if (allowNullValues) {
if (redisOperations.getValueSerializer() instanceof StringRedisSerializer
|| redisOperations.getValueSerializer() instanceof GenericToStringSerializer
|| redisOperations.getValueSerializer() instanceof JacksonJsonRedisSerializer
|| redisOperations.getValueSerializer() instanceof Jackson2JsonRedisSerializer) {
throw new IllegalArgumentException(String.format(
"Redis does not allow keys with null value ¯\\_(ツ)_/¯. "
+ "The chosen %s does not support generic type handling and therefore cannot be used with allowNullValues enabled. "
+ "Please use a different RedisSerializer or disable null value support.",
ClassUtils.getShortName(redisOperations.getValueSerializer().getClass())));
}
}
}
那么prefix在这个RedisCache中有什么用呢? --> 如构造函数about所示,在this.cacheMetadata = new RedisCacheMetadata(name, prefix);这个语句中使用,下面RedisCacheMetadata的构造函数显示更多细节:
/**
* @param cacheName must not be {@literal null} or empty.
* @param keyPrefix can be {@literal null}.
*/
public RedisCacheMetadata(String cacheName, byte[] keyPrefix) {
Assert.hasText(cacheName, "CacheName must not be null or empty!");
this.cacheName = cacheName;
this.keyPrefix = keyPrefix;
StringRedisSerializer stringSerializer = new StringRedisSerializer();
// name of the set holding the keys
this.setOfKnownKeys = usesKeyPrefix() ? new byte[] {} : stringSerializer.serialize(cacheName + "~keys");
this.cacheLockName = stringSerializer.serialize(cacheName + "~lock");
}
此时,我们知道一些前缀参数已设置为RedisCacheMetadata,但是这个前缀究竟是如何用于构成Redis中的键的(例如,\xac\xed\x00\x05t\x00\tvc:你提到的501381)?
基本上,CacheInterceptor 随后会向前调用上述RedisCache 对象的private RedisCacheKey getRedisCacheKey(Object key) 方法,该方法通过利用前缀 中的前缀 返回RedisCacheKey 的实例RedisCacheMetadata 和来自 RedisOperation 的 keySerializer。
private RedisCacheKey getRedisCacheKey(Object key) {
return new RedisCacheKey(key).usePrefix(this.cacheMetadata.getKeyPrefix())
.withKeySerializer(redisOperations.getKeySerializer());
}
到此为止,CacheInterceptor 的“pre”advice 就完成了,它会继续执行@Cacheable 注释的实际方法。并且在完成实际方法的执行后,它会做CacheInterceptor 的“post”advice,本质上是把结果放到RedisCache。下面是将结果放入redis缓存的方法:
public void put(final Object key, final Object value) {
put(new RedisCacheElement(getRedisCacheKey(key), toStoreValue(value))
.expireAfter(cacheMetadata.getDefaultExpiration()));
}
/**
* Add the element by adding {@link RedisCacheElement#get()} at {@link RedisCacheElement#getKeyBytes()}. If the cache
* previously contained a mapping for this {@link RedisCacheElement#getKeyBytes()}, the old value is replaced by
* {@link RedisCacheElement#get()}.
*
* @param element must not be {@literal null}.
* @since 1.5
*/
public void put(RedisCacheElement element) {
Assert.notNull(element, "Element must not be null!");
redisOperations
.execute(new RedisCachePutCallback(new BinaryRedisCacheElement(element, cacheValueAccessor), cacheMetadata));
}
在RedisCachePutCallback对象内部,它的回调方法doInRedis()实际上调用了一个方法来形成redis中的实际key,方法名是getKeyBytes(),来自RedisCacheKey实例。下面展示了这个方法的细节:
/**
* Get the {@link Byte} representation of the given key element using prefix if available.
*/
public byte[] getKeyBytes() {
byte[] rawKey = serializeKeyElement();
if (!hasPrefix()) {
return rawKey;
}
byte[] prefixedKey = Arrays.copyOf(prefix, prefix.length + rawKey.length);
System.arraycopy(rawKey, 0, prefixedKey, prefix.length, rawKey.length);
return prefixedKey;
}
正如我们在 getKeyBytes 方法中看到的那样,它同时使用原始密钥(在您的情况下为 vc:501381)和前缀密钥(在您的情况下为 \xac\xed\x00\x05t\x00\t)。