Springboot简单设计两级缓存

两级缓存相比单纯使用远程缓存,具有什么优势呢?

本地缓存基于本地环境的内存,访问速度非常快,对于一些变更频率低、实时性要求低的数据,可以放在本地缓存中,提升访问速度

使用本地缓存能够减少和Redis类的远程缓存间的数据交互,减少网络I/O开销,降低这一过程中在网络通信上的耗时

但是在设计中,还是要考虑一些问题的,例如数据一致性问题。首先,两级缓存与数据库的数据要保持一致,一旦数据发生了修改,在修改数据库的同时,本地缓存、远程缓存应该同步更新。

如果是分布式环境下,一级缓存之间也会存在一致性问题,当一个节点下的本地缓存修改后,需要通知其他节点也刷新本地缓存中的数据,否则会出现读取到过期数据的情况,这一问题可以通过类似于Redis中的发布/订阅功能解决。

此外,缓存的过期时间、过期策略以及多线程访问的问题也都需要考虑进去,不过我们今天暂时先不考虑这些问题,简单的在代码中实现两级缓存的管理。

图片中一级缓存找的图是Ehcache,但实际项目中我使用的是caffeine做一级缓存,redis做二级缓存原理都是一样,先引入相关依赖

java 复制代码
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>com.github.ben-manes.caffeine</groupId>
			<artifactId>caffeine</artifactId>
			<version>2.9.2</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-pool2</artifactId>
			<version>2.8.1</version>
		</dependency>

在yml中配置redis的相关信息

java 复制代码
  redis:
    host: localhost
    port: 6379
    password:
    timeout: 5000
#    lettuce:
#      pool:
#        max-active: 8
#        max-wait: -1ms
#        max-idle: 8
#        min-idle: 0

注释的lettuce都是默认值,实际要调整放开注释自行调整即可

枚举类型枚举

java 复制代码
public enum CacheType {

    /**
     * 存取
     */
    FULL,

    /**
     * 只存
     */
    PUT,

    /**
     * 删除
     */
    DEL
}

定义一个注解,用于添加在需要操作缓存的方法上,使用cacheName + key作为缓存的真正key,timeOut为可以设置的二级缓存Redis的过期时间,type是一个枚举类型的变量,表示操作缓存的类型

java 复制代码
import com.yx.light.element.jpa.enums.CacheType;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface L2Cache {

    /**
     * 缓存名字
     * @return
     */
    String cacheName();

    /**
     * 缓存key
     * @return
     */
    String key() default ""; //支持springEl表达式

    /**
     * redis缓存超时时间
     * @return
     */
    long timeOut() default 120;

    /**
     * 缓存类型
     * @return
     */
    CacheType type() default CacheType.FULL;
}

RedisTemplate配置类

java 复制代码
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    /**
     * 配置自定义redisTemplate
     *
     * @return
     */
    @Bean
    RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //设置值(value)的序列化采用Jackson2JsonRedisSerializer。
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        //设置键(key)的序列化采用StringRedisSerializer。
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

Caffeine配置类

java 复制代码
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                .initialCapacity(128)//初始大小
                .maximumSize(1024)//最大数量
                .expireAfterWrite(60, TimeUnit.SECONDS)//过期时间
                .build();
    }
}

El转换辅助工具类

java 复制代码
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.util.TreeMap;

public class ElParserUtil {

    private ElParserUtil() {
    }

    public static String parse(String elString, TreeMap<String, Object> map) {
        elString = String.format("#{%s}", elString);
        //创建表达式解析器
        ExpressionParser parser = new SpelExpressionParser();
        //通过evaluationContext.setVariable可以在上下文中设定变量。
        EvaluationContext context = new StandardEvaluationContext();
        map.entrySet().forEach(entry ->
                context.setVariable(entry.getKey(), entry.getValue())
        );

        //解析表达式
        Expression expression = parser.parseExpression(elString, new TemplateParserContext());
        //使用Expression.getValue()获取表达式的值,这里传入了Evaluation上下文
        String value = expression.getValue(context, String.class);
        return value;
    }
}

两级缓存切面,在切面中操作Cache来读写Caffeine的缓存,操作RedisTemplate读写Redis缓存。

java 复制代码
import com.github.benmanes.caffeine.cache.Cache;
import com.yx.light.element.jpa.annotations.L2Cache;
import com.yx.light.element.jpa.enums.CacheType;
import com.yx.light.element.jpa.utils.ElParserUtil;
import lombok.AllArgsConstructor;
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.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
@Aspect
@AllArgsConstructor
public class L2CacheAspect {

    private final Cache cache;
    private final RedisTemplate redisTemplate;

    @Pointcut("@annotation(com.yx.light.element.jpa.annotations.L2Cache)")
    public void cacheAspect() {
    }

    @Around("cacheAspect()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        //拼接解析springEl表达式的map
        String[] paramNames = signature.getParameterNames();
        Object[] args = point.getArgs();
        TreeMap<String, Object> treeMap = new TreeMap<>();
        for (int i = 0; i < paramNames.length; i++) {
            treeMap.put(paramNames[i], args[i]);
        }

        L2Cache annotation = method.getAnnotation(L2Cache.class);
        String elResult = null;
        if (treeMap.size() == 0) {
            elResult = StringUtils.isEmpty(annotation.key()) ? method.getName() : annotation.key();
        } else {
            elResult = ElParserUtil.parse(annotation.key(), treeMap);
        }
        String realKey = annotation.cacheName() + ":" + elResult;

        //强制更新
        if (annotation.type() == CacheType.PUT) {
            Object object = point.proceed();
            if (Objects.isNull(object)) {
                log.info("方法执行完毕无返回值,无需更新缓存");
                return object;
            }
            redisTemplate.opsForValue().set(realKey, object, annotation.timeOut(), TimeUnit.SECONDS);
            cache.put(realKey, object);
            return object;
        } else if (annotation.type() == CacheType.DEL) {
            redisTemplate.delete(realKey);
            cache.invalidate(realKey);
            return point.proceed();
        }

        //读写,查询Caffeine
        Object caffeineCache = cache.getIfPresent(realKey);
        if (Objects.nonNull(caffeineCache)) {
            log.info("从caffeine中获取数据");
            return caffeineCache;
        }

        //查询Redis
        Object redisCache = redisTemplate.opsForValue().get(realKey);
        if (Objects.nonNull(redisCache)) {
            log.info("从redis中获取数据");
            cache.put(realKey, redisCache);
            return redisCache;
        }

        log.info("从数据库中获取数据");
        Object object = point.proceed();
        if (Objects.nonNull(object)) {
            //写入Redis
            redisTemplate.opsForValue().set(realKey, object, annotation.timeOut(), TimeUnit.SECONDS);
            //写入Caffeine
            cache.put(realKey, object);
        }
        return object;
    }
}

改造service实现类的几个方法简单测试一下

java 复制代码
    @Override
    @L2Cache(cacheName = "GroupHeader", type = CacheType.FULL)
    public List<GroupHeader> findAllGroupHeader() {
        return groupHeaderRepository.findAll();
    }
	
    @Override
    @L2Cache(cacheName = "GroupHeader", key = "#groupHeader.groupCode", type = CacheType.PUT)
    public void editGroupHeader(GroupHeader groupHeader) {
        groupHeaderRepository.save(groupHeader);
    }

    @Override
    @L2Cache(cacheName = "GroupHeader", key = "#ids", type = CacheType.DEL)
    public void deleteGroupHeader(String ids) {
        String[] split = ids.split(",");
        for (String id : split) {
            groupHeaderRepository.deleteById(Long.parseLong(id));
        }
    }

连续调用两次查询接口看日志打印效果和redis客户端的查询

相关推荐
IT毕设实战小研5 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
一只爱撸猫的程序猿6 小时前
使用Spring AI配合MCP(Model Context Protocol)构建一个"智能代码审查助手"
spring boot·aigc·ai编程
甄超锋6 小时前
Java ArrayList的介绍及用法
java·windows·spring boot·python·spring·spring cloud·tomcat
武昌库里写JAVA8 小时前
JAVA面试汇总(四)JVM(一)
java·vue.js·spring boot·sql·学习
Pitayafruit9 小时前
Spring AI 进阶之路03:集成RAG构建高效知识库
spring boot·后端·llm
zru_960210 小时前
Spring Boot 单元测试:@SpyBean 使用教程
spring boot·单元测试·log4j
甄超锋10 小时前
Java Maven更换国内源
java·开发语言·spring boot·spring·spring cloud·tomcat·maven
还是鼠鼠11 小时前
tlias智能学习辅助系统--Maven 高级-私服介绍与资源上传下载
java·spring boot·后端·spring·maven
2301_7930868712 小时前
Redis 04 Reactor
数据库·redis·缓存
舒一笑15 小时前
Started TttttApplication in 0.257 seconds (没有 Web 依赖导致 JVM 正常退出)
jvm·spring boot·后端