当前位置: 首页 > news >正文

Spring Cache 实战指南

redis中常见的问题

前言

在本文中,我们将探讨 Redis 在缓存中的应用,并解决一些常见的缓存问题。为了简化理解,本文中的一些配置是直接写死的,实际项目中建议将这些配置写入配置文件,并通过配置文件读取。

一、为什么需要缓存?

在Web应用开发中,频繁的数据库查询和复杂的计算操作会显著影响系统性能。为了提升系统的响应速度和整体性能,缓存机制成为了不可或缺的一部分。Spring Cache通过抽象缓存层,使开发者能够通过简单的注解实现方法级别的缓存,从而有效减少重复计算和数据库访问,显著提升系统的响应速度。

前提

本文使用Redis作为缓存管理器(CacheManager),因此你需要确保正确引入并配置Redis。

引入与基本使用(此处由AI代写,非本文重点)

Spring Cache快速配置

Java配置类示例:

@Configuration
@EnableCaching
@Slf4j
public class RedisCachingAutoConfiguration {
    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public 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编写,经过测试可用。然而,在实际使用中,这些用法可能不符合某些场景需求,或者使用起来不够方便。以下是一些常见问题及解决方案:

  1. 自动生成的key格式为{cacheable.value}::{cacheable.key},为什么一定是"::"两个冒号?
    (查看源码org.springframework.data.redis.cache.CacheKeyPrefix
    如果需要为key统一加前缀,可以在RedisCacheConfiguration中设置。

  2. 批量删除时,@CacheEvict不够灵活。

    • 方案一:使用@CacheEvict并设置allEntriestrue,但这样会删除所有value相同的缓存,可能会误删不需要清除的数据。
    • 方案二:手动调用删除缓存。
    • 方案三:自定义批量删除缓存注解。
  3. 大部分场景下,使用某个固定属性值作为缓存时,增删改操作每次都要写key取某个值,非常繁琐。

    • 方案一:自定义KeyGenerator
  4. 高并发场景下如何确保数据的一致性和系统的稳定性?

    • 方案一:在单体架构中,可以在构建CacheManager时指定RedisCacheWriterlockingRedisCacheWriter,并在@CachePut@CacheEvict中指定带锁的CacheManager
    • 方案二:在集群环境中,可以在@CachePut@CacheEvict对应的方法上加分布式锁(如Redisson)。
  5. 如何防止缓存雪崩?

    • 定义多个缓存管理器,每个管理器有不同的过期时间。
    • 在方法上指定使用哪个缓存管理器。
  6. 如何防止缓存穿透?

    • 缓存框架中允许缓存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 {
    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public 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();
    }


    /**
     * 缓存的异常处理
     */
    @Bean
    public CacheErrorHandler errorHandler() {
        // 异常处理,当Redis发生异常时,打印日志,但是程序正常走
        log.info("初始化 -> [{}]", "Redis CacheErrorHandler");
        return new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
                log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
            }

            @Override
            public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
                log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
            }

            @Override
            public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
                log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
            }

            @Override
            public 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
    @Nonnull
    public 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;

    @Override
    public 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);
    // 业务代码
}

相关文章:

  • 华为机试牛客刷题之HJ58 输入n个整数,输出其中最小的k个
  • 掌握 Postman:高级 GET 请求技术与响应分析
  • Ubuntu22.04美化MacOS主题
  • 什么是正文化
  • 【CSS3】完整修仙功法
  • WordPress 代码高亮插件 io code highlight
  • 【C++】string类字符串详细解析
  • SCI英文论文Accepted后的第一步——Rights and Access
  • Jenkins 集成 SonarQube 代码静态检查使用说明
  • 【Rust】一文掌握 Rust 的详细用法(Rust 备忘清单)
  • python打包辅助工具
  • 【视频】OpenCV:色彩空间转换、灰度转伪彩
  • react自定义hook
  • 排序复习_代码纯享
  • batman-adv 优化:基于信号强度(RSSI)选择链路
  • SpringCloud配置中心:Config Server与配置刷新机制
  • 使用 Python 和 python-pptx 构建 Markdown 到 PowerPoint 转换器
  • 华为OD机试 - 核酸最快检测效率 - 动态规划、背包问题(Java 2024 E卷 200分)
  • 深入理解 HTML5 Web Workers:提升网页性能的关键技术解析
  • 基礎複分析習題3.複函數
  • 规范涉企案件审判执行工作,最高法今天发布通知
  • 魏晓栋已任上海崇明区委常委、组织部部长
  • 著名文学评论家、清华大学中文系教授蓝棣之逝世
  • 中国专家组赴缅开展地震灾害评估工作
  • 湖南永州公安全面推行“项目警官制”,为重点项目建设护航
  • 网上销售假冒片仔癀和安宫牛黄丸,两人被判刑