Spring Redis Cache @Cacheable 大并发下返回null

来源:互联网 发布:淘宝产品摄影报价 编辑:程序博客网 时间:2024/06/06 03:52

问题描述

最近我们用Spring Cache + redis来做缓存。在高并发下@Cacheable 注解返回的内容是null。查看了一下源代码,在使用注解获取缓存的时候,RedisCache的get方法会先去判断key是否存在,然后再去获取值。这了就有一个漏铜,当线程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对应的值    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是否存在。不能直接用获取的值根据是否是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;}

完整实现:

重写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函数。     * 父类的get方法,是先使用exists判断key是否存在,不存在返回null,存在再到redis缓存中去取值。这样会导致并发问题,     * 假如有一个请求调用了exists函数判断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管理器

@Configurationpublic 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);        //这里可以设置一个默认的过期时间 单位是秒        redisCacheManager.setDefaultExpiration(redisDefaultExpiration);        return redisCacheManager;    }    /**     * 显示声明缓存key生成器     *     * @return     */    @Bean    public KeyGenerator keyGenerator() {        return new SimpleKeyGenerator();    }}

源码:
https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-cache-redis 工程

原创粉丝点击