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

问题描述

最近我们用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验证

【测试代码】

4db6c578ea07b62166eb1a33e836392fcba.jpg

91f2893de0978d1460dafdf15519bde1c17.jpg

redis设置的默认过期时间为 30s53b1304a47b1ae12b2f593df4d6958dcddc.jpg

 

ok,开始验证啦~

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

2b62b772540474c7cdac884dd656a8e7c85.jpg

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

 

再看下优化后的效果

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

f87147f469309cd8a341f2d6ecbe5210775.jpg

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

转载于:https://my.oschina.net/xsh1208/blog/2088252

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot是一个开发框架,它简化了使用Spring框架进行Java应用程序开发的过程。Redis是一个内存数据结构存储系统,它可以用作缓存和数据库。@CacheableSpring框架的注解之一,它可以用于缓存方法的返回值。 要在Spring Boot中使用Redis和@Cacheable来实现缓存,首先需要配置Redis连接。可以通过在`application.properties`或`application.yml`文件中添加以下配置来完成: ```yaml spring.redis.host=127.0.0.1 spring.redis.port=6379 ``` 接下来,在需要缓存的方法上使用`@Cacheable`注解。例如,假设我们有一个名为`getUserById`的方法,用于根据用户ID获取用户信息: ```java @Service public class UserService { @Cacheable(value = "users", key = "#id") public User getUserById(Long id) { // 从数据库或其他数据源获取用户信息 return userRepository.findById(id); } } ``` 在上述示例中,`@Cacheable`注解用于将方法的返回值缓存起来。其中,`value`属性指定了缓存的名称,`key`属性指定了缓存的键。在这个例子中,缓存的名称为"users",缓存的键为方法的参数id。 最后,需要在Spring Boot应用程序的启动类上添加`@EnableCaching`注解来启用缓存功能: ```java @SpringBootApplication @EnableCaching public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } ``` 以上就是使用Spring Boot、Redis和@Cacheable实现缓存的基本步骤。通过配置Redis连接,使用`@Cacheable`注解来标记需要缓存的方法,并在启动类上添加`@EnableCaching`注解来启用缓存功能,可以轻松地实现缓存功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值