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客户端的查询

相关推荐
XMYX-02 小时前
Spring Boot + Prometheus 实现应用监控(基于 Actuator 和 Micrometer)
spring boot·后端·prometheus
@yanyu6663 小时前
springboot实现查询学生
java·spring boot·后端
酷爱码4 小时前
Spring Boot项目中JSON解析库的深度解析与应用实践
spring boot·后端·json
java干货5 小时前
虚拟线程与消息队列:Spring Boot 3.5 中异步架构的演进与选择
spring boot·后端·架构
不凡的凡6 小时前
鸿蒙图片缓存(一)
缓存
武昌库里写JAVA8 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
小白杨树树8 小时前
【WebSocket】SpringBoot项目中使用WebSocket
spring boot·websocket·网络协议
潘yi.9 小时前
Redis哨兵模式
数据库·redis·缓存