日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學(xué)無先后,達者為師

網(wǎng)站首頁 編程語言 正文

Spring Redis Cache @Cacheable 大并發(fā)下返回null

作者:譜寫 更新時間: 2022-03-14 編程語言

問題描述

最近我們用Spring Cache + redis來做緩存。在高并發(fā)下@Cacheable 注解返回的內(nèi)容是null。查看了一下源代碼,在使用注解獲取緩存的時候,RedisCache的get方法會先去判斷key是否存在,然后再去獲取值。這了就有一個漏銅,當(dāng)線程1判斷了key是存在的,緊接著這個時候這個key過期了,這時線程1再去獲取值的時候返回的是null。

RedisCache的get方法源碼:

public RedisCacheElement get(final RedisCacheKey cacheKey) {

    Assert.notNull(cacheKey, "CacheKey must not be null!");

    // 判斷Key是否存在
    Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {

        @Override
        public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
            return connection.exists(cacheKey.getKeyBytes());
        }
    });

    if (!exists.booleanValue()) {
        return null;
    }
    
    // 獲取key對應(yīng)的值
    return new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
}

// 獲取值
protected Object lookup(Object key) {

    RedisCacheKey cacheKey = key instanceof RedisCacheKey ? (RedisCacheKey) key : getRedisCacheKey(key);

    byte[] bytes = (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());
        }
    });

    return bytes == null ? null : cacheValueAccessor.deserializeIfNecessary(bytes);
}

解決方案

這個流程有問題,解決方案就是把這個流程倒過來,先去獲取值,然后去判斷這個key是否存在。不能直接用獲取的值根據(jù)是否是NULL判斷是否有值,因為Reids可能緩存NULL值。

重寫RedisCache的get方法:

public RedisCacheElement get(final RedisCacheKey cacheKey) {

    Assert.notNull(cacheKey, "CacheKey must not be null!");

    RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
    Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {

        @Override
        public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
            return connection.exists(cacheKey.getKeyBytes());
        }
    });

    if (!exists.booleanValue()) {
        return null;
    }

    return redisCacheElement;
}

完整實現(xiàn):

重寫RedisCache的get方法

package com.xiaolyuh.redis.cache;

import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheElement;
import org.springframework.data.redis.cache.RedisCacheKey;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.util.Assert;

/**
 * 自定義的redis緩存
 *
 * @author yuhao.wang
 */
public class CustomizedRedisCache extends RedisCache {

    private final RedisOperations redisOperations;

    private final byte[] prefix;

    public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration) {
        super(name, prefix, redisOperations, expiration);
        this.redisOperations = redisOperations;
        this.prefix = prefix;
    }

    public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration, boolean allowNullValues) {
        super(name, prefix, redisOperations, expiration, allowNullValues);
        this.redisOperations = redisOperations;
        this.prefix = prefix;
    }

    /**
     * 重寫父類的get函數(shù)。
     * 父類的get方法,是先使用exists判斷key是否存在,不存在返回null,存在再到redis緩存中去取值。這樣會導(dǎo)致并發(fā)問題,
     * 假如有一個請求調(diào)用了exists函數(shù)判斷key存在,但是在下一時刻這個緩存過期了,或者被刪掉了。
     * 這時候再去緩存中獲取值的時候返回的就是null了。
     * 可以先獲取緩存的值,再去判斷key是否存在。
     *
     * @param cacheKey
     * @return
     */
    @Override
    public RedisCacheElement get(final RedisCacheKey cacheKey) {

        Assert.notNull(cacheKey, "CacheKey must not be null!");

        RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
        Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {

            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.exists(cacheKey.getKeyBytes());
            }
        });

        if (!exists.booleanValue()) {
            return null;
        }

        return redisCacheElement;
    }


    /**
     * 獲取RedisCacheKey
     *
     * @param key
     * @return
     */
    private RedisCacheKey getRedisCacheKey(Object key) {
        return new RedisCacheKey(key).usePrefix(this.prefix)
                .withKeySerializer(redisOperations.getKeySerializer());
    }
}

重寫RedisCacheManager

package com.xiaolyuh.redis.cache;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.Collection;

/**
 * 自定義的redis緩存管理器
 * @author yuhao.wang 
 */
public class CustomizedRedisCacheManager extends RedisCacheManager {

    private static final Logger logger = LoggerFactory.getLogger(CustomizedRedisCacheManager.class);

    public CustomizedRedisCacheManager(RedisOperations redisOperations) {
        super(redisOperations);
    }

    public CustomizedRedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
        super(redisOperations, cacheNames);
    }

    @Override
    protected Cache getMissingCache(String name) {
        long expiration = computeExpiration(name);
        return new CustomizedRedisCache(
                name,
                (this.isUsePrefix() ? this.getCachePrefix().prefix(name) : null),
                this.getRedisOperations(),
                expiration);
    }
}

配置Redis管理器

@Configuration
public class RedisConfig {

    // redis緩存的有效時間單位是秒
    @Value("${redis.default.expiration:3600}")
    private long redisDefaultExpiration;

    @Bean
    public RedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
        RedisCacheManager redisCacheManager = new CustomizedRedisCacheManager(redisTemplate);
        redisCacheManager.setUsePrefix(true);
        //這里可以設(shè)置一個默認(rèn)的過期時間 單位是秒
        redisCacheManager.setDefaultExpiration(redisDefaultExpiration);

        return redisCacheManager;
    }

    /**
     * 顯示聲明緩存key生成器
     *
     * @return
     */
    @Bean
    public KeyGenerator keyGenerator() {

        return new SimpleKeyGenerator();
    }

}

?

?

?

?

?

原文鏈接:https://blog.csdn.net/baidu_37366055/article/details/109640640

欄目分類
最近更新