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 工程
阅读全文
0 0
- Spring Redis Cache @Cacheable 大并发下返回null
- Spring-Redis Cacheable中文翻译
- Spring缓存Cache,Cacheable,CachePut使用
- Spring Cacheable注解不缓存null值
- spring cache相关注解介绍 @Cacheable、@CachePut、@CacheEvict
- spring cache相关注解介绍 @Cacheable、@CachePut、@CacheEvict
- 使用spring自带的Cacheable注解处理Redis缓存
- Spring Cache集成redis
- Spring Cache注解+Redis
- Spring Cache集成redis
- Spring cache +redis
- Spring Cache注解+Redis
- PHP使用redis防止大并发下二次写入
- spring boot 整合 redis,使用@Cacheable,@CacheEvict,@CachePut,jedisPool操作redis数据库
- spring @Cacheable的用法
- spring redis cache使用思考
- Spring Cache + Redis配置备忘
- 基spring cache 整合redis
- 【JavaSE_学习笔记】File类
- 原生Android开发—Jar包生成
- CentOS下MySQL忘记root密码解决方法【转载】
- 非nmake方式Visual Studio C++ 2010 编译libevent
- 初学机器学习的你,是否掌握了这样的Linux技巧?
- Spring Redis Cache @Cacheable 大并发下返回null
- pandas.Series.quantile
- PAT考试乙级1037(C语言实现)
- CF——Codeforces 891 C Envy
- 利用tensorflow训练简单的DNN
- 算法练习-1、放苹果(分治算法)
- phantomJS+nodeJS+nginx完美解决前后端分离SEO问题
- MongoDB入门学习(1)创建删除
- unity mac 破解