文档章节

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

谢思华
 谢思华
发布于 2018/09/20 16:51
字数 988
阅读 61
收藏 2

问题描述

最近我们用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;
}

完整实现(3步):

1、重写RedisCache的get方法

package com.test.config.redis;

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 xiesihua 2018-09-20
 */
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;
    }

    /**
     * 重写父类的get函数。
     * 父类的get方法,是先使用exists判断key是否存在,不存在返回null,存在再到redis缓存中去取值。这样会导致并发问题,
     * 假如有一个请求调用了exists函数判断key存在,但是在下一时刻这个缓存过期了,或者被删掉了。
     * 这时候再去缓存中获取值的时候返回的就是null了。
     * 可以先获取缓存的值,再去判断key是否存在。
     *
     * 处理方法:只是把源码中的代码挪了下顺序,先取值,再查
     * 注意:不能把查询的一部省掉,里面有一个flush操作。省掉后,会空指针的
     * @author xiesihua 2018-09-20
     *
     * @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;
    }

}

2、重写RedisCacheManager

package com.test.config.redis;

import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisOperations;

/**
 * 自定义的redis缓存管理器
 * @author xiesihua 2018-09-20
 */
public class CustomizedRedisCacheManager extends RedisCacheManager {

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

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

3、配置Redis管理器

@Configuration
public class RedisConfig {

    @Value("${spring.redis.cache.expiration}")
    private Long cacheDefaultExpiration;
    
    @Bean
    public CacheManager cacheManager(@Qualifier("cacheRedisTemplate") RedisTemplate redisTemplate) {
        //改成使用自定义的redis缓存管理器,xiesihua,2018-09-20
        RedisCacheManager redisCacheManager = new CustomizedRedisCacheManager(redisTemplate);
        redisCacheManager.setDefaultExpiration(cacheDefaultExpiration);
        return redisCacheManager;
    }
}

demo验证

【测试代码】

redis设置的默认过期时间为 30s

 

ok,开始验证啦~

【改源码之前,原redis配置,执行上面死循环,控制台的输出】

很明显,在高并发的情况下,会返回空指针。

 

再看下优化后的效果

【改源码之后,使用了前面贴的设置,执行上面死循环,控制台的输出】

验证通过啦!无限死循环,都不会报空了!

本文转载自:https://blog.csdn.net/xiaolyuh123/article/details/78613041

共有 人打赏支持
谢思华
粉丝 70
博文 216
码字总数 151992
作品 0
广州
程序员
私信 提问
扩展spring cache 支持缓存多租户及其自动过期

spring cache 的概念 Spring 支持基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如 EHCache 或者 OSCache),而是一个对缓存使用的抽象,通过在既有代...

冷冷gg
昨天
0
0
SpringBoot之Mybatis操作中使用Redis做缓存

上一博客学习了SpringBoot集成Redis,今天这篇博客学习下Mybatis操作中使用Redis做缓存。这里其实主要学习几个注解:@CachePut、@Cacheable、@CacheEvict、@CacheConfig。 一、基础知识 @Cac...

社会主义接班人
2018/08/07
0
0
Spring Redis Cache @Cacheable 大并发下返回null

问题描述 最近我们用Spring Cache + redis来做缓存。在高并发下@Cacheable 注解返回的内容是null。查看了一下源代码,在使用注解获取缓存的时候,RedisCache的get方法会先去判断key是否存在,...

xiaolyuh
2017/11/23
0
0
Spring Cache For Redis.

一、概述 缓存(Caching)可以存储经常会用到的信息,这样每次需要的时候,这些信息都是立即可用的。 常用的缓存数据库: Redis 使用内存存储(in-memory)的非关系数据库,字符串、列表、集...

jmcui
2018/02/04
0
0
搞懂分布式技术14:Spring Boot使用注解集成Redis缓存

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a724888/article/details/80785403 为了提高性能,减少数据库的压力,使用缓存是非常好的手段之一。本文,讲解...

你的猫大哥
2018/06/23
0
0

没有更多内容

加载失败,请刷新页面

加载更多

python中类方法和静态方法区别

面相对象程序设计中,类方法和静态方法是经常用到的两个术语。 逻辑上讲:类方法是只能由类名调用;静态方法可以由类名或对象名进行调用。 在C++中,静态方法与类方法逻辑上是等价的,只有一...

xiangyunyan
今天
9
0
Hibernate SQLite方言

以下代码有参考过github上国外某位大佬的,在发文的最新稳定版Hibernate上是可用的,有时间再仔细分析一下 import org.hibernate.dialect.Dialect;import org.hibernate.dialect.function.S...

CHONGCHEN
今天
4
0
CentOS 7 MariaDB搭建主从服务器

本文编写环境为CentOS7。确保关闭SELinux,关闭防火墙或者防打开指定端口。具体信息如下 #master[root@promote ~]# cat /etc/redhat-release CentOS Linux release 7.6.1810 (Core) [r...

白豆腐徐长卿
今天
11
0
介绍python中运算符优先级

下面这个表给出Python的运算符优先级,从最低的优先级(最松散地结合)到最高的优先级(最紧密地结合)。这意味着在一个表达式中,Python会首先计算表中较下面的运算符,然后在计算列在表上部...

问题终结者
今天
4
0
Spring Boot 2.x基础教程:快速入门

简介 在您第1次接触和学习Spring框架的时候,是否因为其繁杂的配置而退却了?在你第n次使用Spring框架的时候,是否觉得一堆反复黏贴的配置有一些厌烦?那么您就不妨来试试使用Spring Boot来让...

程序猿DD
昨天
15
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部