redis中常見的問題
前言
在本文中,我們將探討 Redis 在緩存中的應用,并解決一些常見的緩存問題。為了簡化理解,本文中的一些配置是直接寫死的,實際項目中建議將這些配置寫入配置文件,并通過配置文件讀取。
一、為什么需要緩存?
在Web應用開發中,頻繁的數據庫查詢和復雜的計算操作會顯著影響系統性能。為了提升系統的響應速度和整體性能,緩存機制成為了不可或缺的一部分。Spring Cache通過抽象緩存層,使開發者能夠通過簡單的注解實現方法級別的緩存,從而有效減少重復計算和數據庫訪問,顯著提升系統的響應速度。
前提
本文使用Redis作為緩存管理器(CacheManager),因此你需要確保正確引入并配置Redis。
引入與基本使用(此處由AI代寫,非本文重點)
Spring Cache快速配置
Java配置類示例:
@Configuration
@EnableCaching
@Slf4j
public class RedisCachingAutoConfiguration {@Resourceprivate RedisConnectionFactory redisConnectionFactory;@Beanpublic CacheManager defaultCacheManager() {RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofHours(1));return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)).cacheDefaults(configuration).build();}
}
三、核心注解深度解析
1. @Cacheable:數據讀取緩存
@Cacheable(value = "users", key = "#userId", unless = "#result == null")
public User getUserById(Long userId) {return userRepository.findById(userId).orElse(null);
}
- value:指定緩存名稱(必填)
- key:支持SpEL表達式生成緩存鍵
- condition:方法執行前判斷(例如
userId > 1000
才緩存) - unless:方法執行后判斷(例如空結果不緩存)
屬性 | 執行時機 | 訪問變量 | 作用場景 |
---|---|---|---|
condition | 方法執行前判斷 | 只能訪問方法參數(如 #argName ) | 決定是否執行緩存邏輯(包括是否執行方法體) |
unless | 方法執行后判斷 | 可以訪問方法參數和返回值(如 #result ) | 決定是否將方法返回值存入緩存(不影響是否執行方法體) |
2. @CachePut:強制更新緩存
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {return userRepository.save(user);
}
適用場景:數據更新后同步緩存,確保后續讀取的是最新數據。
3. @CacheEvict:精準清除緩存
@CacheEvict(value = "users", key = "#userId", beforeInvocation = true)
public void deleteUser(Long userId) {userRepository.deleteById(userId);
}
- 刪除指定條目:通過key精準定位
- 清空整個緩存:
allEntries = true
- beforeInvocation:方法執行前清除(避免執行失敗導致臟數據)
4. @Caching:組合操作
@Caching(put = @CachePut(value = "users", key = "#user.id"),evict = @CacheEvict(value = "userList", allEntries = true)
)
public User updateUserProfile(User user) {// 業務邏輯
}
5. @CacheConfig:類級別配置
@Service
@CacheConfig(cacheNames = "products")
public class ProductService {// 類中方法默認使用products緩存
}
工程化實踐解決方案
前面的示例內容由AI編寫,經過測試可用。然而,在實際使用中,這些用法可能不符合某些場景需求,或者使用起來不夠方便。以下是一些常見問題及解決方案:
-
自動生成的key格式為
{cacheable.value}::{cacheable.key}
,為什么一定是"::"兩個冒號?
(查看源碼org.springframework.data.redis.cache.CacheKeyPrefix
)
如果需要為key統一加前綴,可以在RedisCacheConfiguration
中設置。 -
批量刪除時,
@CacheEvict
不夠靈活。- 方案一:使用
@CacheEvict
并設置allEntries
為true
,但這樣會刪除所有value
相同的緩存,可能會誤刪不需要清除的數據。 - 方案二:手動調用刪除緩存。
- 方案三:自定義批量刪除緩存注解。
- 方案一:使用
-
大部分場景下,使用某個固定屬性值作為緩存時,增刪改操作每次都要寫
key
取某個值,非常繁瑣。- 方案一:自定義
KeyGenerator
。
- 方案一:自定義
-
高并發場景下如何確保數據的一致性和系統的穩定性?
- 方案一:在單體架構中,可以在構建
CacheManager
時指定RedisCacheWriter
為lockingRedisCacheWriter
,并在@CachePut
和@CacheEvict
中指定帶鎖的CacheManager
。 - 方案二:在集群環境中,可以在
@CachePut
和@CacheEvict
對應的方法上加分布式鎖(如Redisson)。
- 方案一:在單體架構中,可以在構建
-
如何防止緩存雪崩?
- 定義多個緩存管理器,每個管理器有不同的過期時間。
- 在方法上指定使用哪個緩存管理器。
-
如何防止緩存穿透?
- 緩存框架中允許緩存
null
,未找到的數據可以直接緩存空值。
- 緩存框架中允許緩存
統一修改前綴與定義key序列化
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;import javax.annotation.Resource;
import java.time.Duration;/***redisCache配置** @author weiwenbin* @date 2025/03/11 下午5:15*/
@Configuration
@EnableCaching
@Slf4j
public class RedisCachingAutoConfiguration {@Resourceprivate RedisConnectionFactory redisConnectionFactory;@Beanpublic CacheManager defaultNoLockingCacheManager() {String keyPre = "hatzi";String directoryName = "cache";RedisCacheConfiguration configuration = getCacheConfiguration(Duration.ofHours(1), keyPre, directoryName);return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory)).cacheDefaults(configuration).build();}/*** 緩存的異常處理*/@Beanpublic CacheErrorHandler errorHandler() {// 異常處理,當Redis發生異常時,打印日志,但是程序正常走log.info("初始化 -> [{}]", "Redis CacheErrorHandler");return new CacheErrorHandler() {@Overridepublic void handleCacheGetError(RuntimeException e, Cache cache, Object key) {log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);}@Overridepublic void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);}@Overridepublic void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);}@Overridepublic void handleCacheClearError(RuntimeException e, Cache cache) {log.error("Redis occur handleCacheClearError:", e);}};}public static RedisCacheConfiguration getCacheConfiguration(Duration duration, String keyPre, String directoryName) {RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(duration);/*** 默認CacheKeyPrefix 中分隔符為"::" 我想改成":" 所以這樣寫* 20250315放棄serializeKeysWith是因為自定義批量刪除注解serializeKeysWith設置的前綴未生效*/configuration = configuration.computePrefixWith(cacheName -> {String pre = "";if (StrUtil.isNotBlank(keyPre)) {pre += keyPre + ":";}if (StrUtil.isNotBlank(directoryName)) {pre += directoryName + ":";}return pre + cacheName + ":";});return configuration;}
}
自定義KeyGenerator
自定義KeyGenerator
@Component
@Slf4j
public class PkKeyGenerator implements KeyGenerator {@Override@Nonnullpublic Object generate(@Nonnull Object target, @Nonnull Method method, Object... params) {if (params.length == 0) {log.info("PkKeyGenerator key defaultKey");return "defaultKey";}for (Object param : params) {if (param == null) {continue;}if (param instanceof PkKeyGeneratorInterface) {PkKeyGeneratorInterface pkKeyGenerator = (PkKeyGeneratorInterface) param;String key = pkKeyGenerator.cachePkVal();if (StrUtil.isBlank(key)) {return "defaultKey";}log.info("PkKeyGenerator key :{}", key);return key;}}log.info("PkKeyGenerator key defaultKey");return "defaultKey";}
}
自定義接口
public interface PkKeyGeneratorInterface {String cachePkVal();
}
入參實現接口
public class SysTenantQueryDTO implements PkKeyGeneratorInterface, Serializable {private static final long serialVersionUID = 1L;@ApiModelProperty(value = "id")private Long id;@Overridepublic String cachePkVal() {return id.toString();}
}
注解中使用
@Cacheable(value = "sysTenant", keyGenerator = "pkKeyGenerator")
public SysTenantVO getVOInfoBy(SysTenantQueryDTO queryDTO) {// 業務代碼
}
自定義注解批量刪除
工具類
public class CacheDataUtils {/*** 批量鍵清除方法* 該方法用于從指定的緩存中清除一批鍵對應的緩存對象* 主要解決批量清除緩存的需求,提高緩存管理的靈活性和效率** @param cacheManager 緩存管理器,用于管理緩存* @param cacheName 緩存名稱,用于指定需要操作的緩存* @param keys 需要清除的鍵集合,這些鍵對應的緩存對象將會被清除*/public static void batchEvict(CacheManager cacheManager, String cacheName, Collection<?> keys) {// 檢查傳入的鍵集合是否為空,如果為空則直接返回,避免不必要的操作if (CollUtil.isEmpty(keys)) {return;}// 獲取指定名稱的緩存對象Cache cache = cacheManager.getCache(cacheName);// 檢查緩存對象是否存在,如果存在則逐個清除傳入的鍵對應的緩存對象if (cache != null) {keys.forEach(cache::evict);}}
}
自定義注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BatchCacheEvict {/*** 目標緩存名稱** @return String[]*/String[] cacheNames() default {};/*** 緩存鍵(SpEL表達式)** @return String*/String key();/*** 指定CacheManager Bean名稱** @return String*/String cacheManager() default "";/*** 是否在方法執行前刪除* 建議后置刪除** @return boolean*/boolean beforeInvocation() default false;/*** 條件表達式(SpEL)** @return String*/String condition() default "";
}
切面編程
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.hatzi.core.enums.SystemResultEnum;
import com.hatzi.core.exception.BaseException;
import com.hatzi.sys.cache.annotation.BatchCacheEvict;
import com.hatzi.sys.cache.util.CacheDataUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.cache.CacheManager;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Component;import java.util.Collection;/*** 批量清除緩存切面類* 用于處理帶有 @BatchCacheEvict 注解的方法,進行緩存的批量清除操作** @author weiwenbin*/
@Aspect
@Component
@Slf4j
public class BatchCacheEvictAspect {// SpEL 解析器private final ExpressionParser parser = new SpelExpressionParser();// 參數名發現器(用于解析方法參數名)private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();/*** 處理批量清除緩存的操作** @param joinPoint 切入點* @param batchEvict 批量清除緩存注解* @return 方法執行結果* @throws Throwable 可能拋出的異常*/@Around("@annotation(batchEvict)")public Object handleBatchEvict(ProceedingJoinPoint joinPoint, BatchCacheEvict batchEvict) throws Throwable {// 條件判斷if (StrUtil.isNotBlank(batchEvict.condition()) && !isConditionPassed(joinPoint, batchEvict.condition())) {log.info("handleBatchEvict isConditionPassed is false");return joinPoint.proceed();}// 空值檢查if (ArrayUtil.isEmpty(batchEvict.cacheNames()) || StrUtil.isEmpty(batchEvict.key())) {log.info("handleBatchEvict cacheNames or key is empty");return joinPoint.proceed();}// 前置刪除if (batchEvict.beforeInvocation()) {evictCaches(joinPoint, batchEvict);}try {Object result = joinPoint.proceed();// 后置刪除if (!batchEvict.beforeInvocation()) {evictCaches(joinPoint, batchEvict);}return result;} catch (Exception ex) {log.error(ex.getMessage());throw ex;}}/*** 執行緩存的批量清除操作** @param joinPoint 切入點* @param batchEvict 批量清除緩存注解*/private void evictCaches(ProceedingJoinPoint joinPoint, BatchCacheEvict batchEvict) {// 創建 SpEL 上下文EvaluationContext context = createEvaluationContext(joinPoint);String cachedManagerName = batchEvict.cacheManager();String keyExpr = batchEvict.key();String[] cacheNames = batchEvict.cacheNames();//獲取緩存對象CacheManager cacheManager = getCacheManager(cachedManagerName);//解析key的值Object key = parser.parseExpression(keyExpr).getValue(context);if (!(key instanceof Collection)) {log.error("keyExpr 類型錯誤必須是Collection的子類");throw new BaseException(SystemResultEnum.INTERNAL_SERVER_ERROR);}for (String cacheName : cacheNames) {CacheDataUtils.batchEvict(cacheManager, cacheName, (Collection<?>) key);}}/*** 創建 SpEL 上下文** @param joinPoint 切入點* @return SpEL 上下文對象*/private EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint) {MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 構建 SpEL 上下文(支持方法參數名解析)return new MethodBasedEvaluationContext(joinPoint.getTarget(),signature.getMethod(),joinPoint.getArgs(),parameterNameDiscoverer);}/*** 獲取緩存管理器對象** @param cacheManagerName 緩存管理器名稱* @return 緩存管理器對象*/private CacheManager getCacheManager(String cacheManagerName) {return StrUtil.isBlank(cacheManagerName) ?SpringUtil.getBean(CacheManager.class) :SpringUtil.getBean(cacheManagerName, CacheManager.class);}/*** 判斷條件是否滿足** @param joinPoint 切入點* @param condition 條件表達式* @return 是否滿足條件*/private boolean isConditionPassed(ProceedingJoinPoint joinPoint, String condition) {return Boolean.TRUE.equals(parser.parseExpression(condition).getValue(createEvaluationContext(joinPoint), Boolean.class));}
}
使用
@Override
@Transactional(rollbackFor = {Exception.class})
@BatchCacheEvict(cacheNames = "sysTenant", key = "#idList")
public Boolean delByIds(List<Long> idList) {// 手動刪除// CacheDataUtils.batchEvict(SpringUtil.getBean("defaultCacheManager", CacheManager.class),"sysTenant", idList);// 業務代碼
}