背景
正在对某个接口做性能优化,通过pinpoint发现为了获取一次@Cacheable注解的数据,居然对redis发起了3次调用,分别是两次exists和一次get
![](https://img.haomeiwen.com/i8100269/12c0d57402969784.png)
源码分析
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
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;
}
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 set添加了锁机制,即通过程序保证了读写不能并发。对于某一组prefix,假设有线程正在对其中的某个key进行set操作,那么对于这组prefix的所有key都将无法进行其余任何操作。
但个人认为这是破坏redis单线程优势,无论是什么命令,达到redis都会变成单线程执行,程序只需要按命令达到redis的时间顺序执行即可,加锁是多此一举。
另外一次get就可以完成的业务被冗余成两次exists和一次get,并发量高的时候容易影响redis性能
网友评论