背景
正在对某个接口做性能优化,通过pinpoint发现为了获取一次@Cacheable注解的数据,居然对redis发起了3次调用,分别是两次exists和一次get
源码分析
org.springframework.data.redis.cache.RedisCache
public RedisCacheElement get(final RedisCacheKey cacheKey) {
Assert.notNull(cacheKey, "CacheKey must not be null!");
Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.exists(cacheKey.getKeyBytes());
}
});
if (!exists) {
return null;
}
byte[] bytes = doLookup(cacheKey);
// safeguard if key gets deleted between EXISTS and GET calls.
if (bytes == null) {
return null;
}
return new RedisCacheElement(cacheKey, fromStoreValue(deserialize(bytes)));
}
private byte[] doLookup(Object key) {
RedisCacheKey cacheKey = key instanceof RedisCacheKey ? (RedisCacheKey) key : getRedisCacheKey(key);
return (byte[]) redisOperations.execute(new AbstractRedisCacheCallback<byte[]>(
new BinaryRedisCacheElement(new RedisCacheElement(cacheKey, null), cacheValueAccessor), cacheMetadata) {
@Override
public byte[] doInRedis(BinaryRedisCacheElement element, RedisConnection connection) throws DataAccessException {
return connection.get(element.getKeyBytes());
}
});
}
通过以上方法可以很清楚的看出一次exists和一次get命令,那么另一次exists是什么操作?
通过进入doInRedis追踪org.springframework.data.redis.cache.RedisCache.AbstractRedisCacheCallback#waitForLock方法可以发现这就是另一次exists,假如这个锁存在的话,该方法会无限等待WAIT_FOR_LOCK_TIMEOUT=300毫秒直至该锁被释放
protected boolean waitForLock(RedisConnection connection) {
boolean retry;
boolean foundLock = false;
do {
retry = false;
if (connection.exists(cacheMetadata.getCacheLockKey())) {
foundLock = true;
try {
Thread.sleep(WAIT_FOR_LOCK_TIMEOUT);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
retry = true;
}
} while (retry);
return foundLock;
}
另外可以看一下以下的lock方法,顺着方法向上可以追踪至org.springframework.data.redis.cache.RedisCache.RedisWriteThroughCallback#doInRedis,而这个类又只在org.springframework.data.redis.cache.RedisCache#get(java.lang.Object, java.util.concurrent.Callable<T>)方法中调用,这个方法在@Cacheable的boolean sync() default false中有说明,当开启sync时,@Cacheable将会加锁保证只有一个线程去数据库中加载缓存,其他线程同步等待
protected void lock(RedisConnection connection) {
waitForLock(connection);
connection.set(cacheMetadata.getCacheLockKey(), "locked".getBytes());
}
继续追踪这个key可以发现RedisCacheMetadata
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");
}
这个分布式锁key则是cacheName + "~lock"
总结
通过上面的waitForLock和源码中的lock方法就可以得知:spring data redis框架(源码版本1.8.11 RELEASE)为redis的get方法添加了锁机制,但是事实上只有在@Cacheable的sync=true时这个锁才能真正起作用,而且锁真正存在的情况特别少,因此这个多余的锁exists判断只会浪费性能
另外一次get就可以完成的业务被冗余成两次exists和一次get,并发量高的时候容易影响redis性能
解决
当对于性能要求特别高的时候,可以放弃采用@Cacheable注解的方式做缓存,而采用手动使用redisTemplate.ops.get的方式来获取缓存,但缺点是在原先@CacheEvict更新或者删除的方法也需要改成手动redisTemplate.delete的方法来删除缓存
另外可以看下更新版本的spring-data-redis是否可以避免这个问题