網(wǎng)站首頁 編程語言 正文
問題描述
最近我們用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
相關(guān)推薦
- 2022-04-02 Android?WebView如何判斷是否滾動到底部_Android
- 2022-12-23 Kubernetes調(diào)度管理優(yōu)先級和搶占機制詳解_云其它
- 2023-01-18 Python函數(shù)的參數(shù)列表解析_python
- 2022-09-03 Matplotlib中文亂碼的兩種詳細(xì)解決方案_python
- 2022-08-20 python使用tkinter模塊實現(xiàn)文件選擇功能_python
- 2023-03-18 Flutter封裝組動畫混合動畫AnimatedGroup示例詳解_Android
- 2022-04-27 python?數(shù)據(jù)挖掘算法的過程詳解_python
- 2023-06-13 C語言中如何通過指針參數(shù)返回值_C 語言
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支