spring data redis自带锁机制

背景

正在对某个接口做性能优化,通过pinpoint发现为了获取一次@Cacheable注解的数据,居然对redis发起了3次调用,分别是两次exists和一次get


image.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() {

            @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(
                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)方法中调用,这个方法在@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是否可以避免这个问题

你可能感兴趣的:(spring data redis自带锁机制)