基于SpringBoot实现高性能缓存组件

1. 简介

为了体现我们的实力,首先我们要有造轮子的能力。这意味着我们不仅要熟练掌握现有的技术栈和框架,还要具备深厚的技术功底。通过自主设计和实现关键组件,如高性能缓存系统,我们能够深入理解技术背后的原理,掌握核心技术的精髓,从而在面对复杂问题时能够提出独到见解和解决方案。

本篇文章将带大家实现一个简化版但功能类似Spring Cache的缓存组件。虽然不会像Spring Cache那样全面和复杂,但我们将通过动手实践,掌握缓存机制的核心原理。

2. 实战案例

2.1 环境准备

既然是基于redis实现,那么我们通过引入以下依赖来简化环境配置

xml 复制代码
<dependency>  
<groupId>org.springframework.boot</groupId>  
<artifactId>spring-boot-starter-data-redis</artifactId></dependency>

通过data-redis,我们需要做的就是配置redis服务器信息即可。

复制代码
spring:
  redis:
    timeout: 10000
    connectTimeout: 20000
    host: 127.0.0.1
    password: xxxooo
    lettuce:
      pool:
        maxActive: 8
        maxIdle: 100
        minIdle: 10
        maxWait: -1

通过以上的配置,在项目中我们就可以直接通过SpringBoot自动配置的StringRedisTemplate来实现各种操作。

2.2 自定义注解

这里我们仿照spring-cache定义如下两个注解(@Cacheable 触发缓存, @CacheEvict 删除缓存)。

**@Cacheable:**设置缓存或读取缓存;如果缓存中存在直接返回,如果不存在则将方法返回值设置到缓存中。

**@CacheEvict:**将当前指定key从缓存中删除。

触发缓存:

java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
  // 缓存Key;
  String key() default "" ;
  // 缓存名称(类似分组)
  String name() default "" ;
}

删除缓存

java 复制代码
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheEvict {
  // 缓存Key
  String key() default "" ;
  // 缓存名称(类似分组)
  String name() default "" ;
}

以上2个注解基本相同,分别完成读写,清除缓存操作。

注:为了灵活,我们需要让上面注解中的key支持SpEL表达式。

2.3 定义切面

该切面用来处理带有上面注解的类或方法,如:方法上有**@Cacheable**注解,那么先从缓存中读取内容,如果缓存中不存在则将当前方法返回值写入缓存中。

java 复制代码
@Component
@Aspect
public class CacheAspect {

  private static final Logger logger = LoggerFactory.getLogger(CacheAspect.class) ;

  private final StringRedisTemplate stringRedisTemplate ;
  public CacheAspect(StringRedisTemplate stringRedisTemplate) {
    this.stringRedisTemplate = stringRedisTemplate ;
  }

  private DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer() ;

  @Pointcut("@annotation(com.pack.redis.cache.Cacheable)")
  private void cacheable() {}
  @Pointcut("@annotation(com.pack.redis.cache.CacheEvict)")
  private void cacheevict() {}

  @Around("cacheable() || cacheevict()") 
  public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
    Object ret = null ;
    Method method = ((MethodSignature) pjp.getSignature()).getMethod() ;
    Object[] args = pjp.getArgs() ;

	// SPEL 表达式解析
    SpelExpressionParser parser = new SpelExpressionParser() ;
    StandardEvaluationContext context = new StandardEvaluationContext() ;
    // 获取参数名称
    String[] parameterNames = discoverer.getParameterNames(method) ;
    for (int i = 0, len = parameterNames.length; i < len; i++) {
      context.setVariable(parameterNames[i], args[i]) ;
    }
    // 类上的注解不做处理了;只处理方法上
    Cacheable cacheable = method.getAnnotation(Cacheable.class) ;
    if (cacheable != null) {
      // 假设都配置了name和key
      String name = cacheable.name() ;
      String expression = cacheable.key() ;
      Object value = parser.parseExpression(expression).getValue(context) ;
      String cacheKey = name + ":" + value;
      boolean hasKey = this.stringRedisTemplate.hasKey(cacheKey) ;
      if (!hasKey) {
        ret = pjp.proceed() ;
        this.stringRedisTemplate.opsForValue().set(cacheKey, new ObjectMapper().writeValueAsString(ret)) ;
        logger.info("写缓存【{}】,数据:{}", cacheKey, ret) ;
        return ret ; 
      }
      logger.info("从缓存【{}】获取", cacheKey);
      String result = this.stringRedisTemplate.opsForValue().get(cacheKey) ;
      return new ObjectMapper().readValue(result, method.getReturnType()) ;
    }
    CacheEvict cacheevict = method.getAnnotation(CacheEvict.class) ;
    if (cacheevict != null) {
      String name = cacheevict.name() ;
      String expression = cacheevict.key() ;
      Object value = parser.parseExpression(expression).getValue(context) ;
      String cacheKey = name + ":" + value ;
      this.stringRedisTemplate.delete(cacheKey) ;
    }
    return pjp.proceed() ;
  }

}

上面的环绕通知通过或(||)表达式的方式拦截2个注解,实现比较的简单主要是结合了SpEL表达式。上面的代码我们假设是你key进行了配置,如果没有配置,我们可以通过如下方式去生成key

java 复制代码
private String getKey(Class<?> targetType, Method method) {
  StringBuilder builder = new StringBuilder();
  builder.append(targetType.getSimpleName());
  builder.append('#').append(method.getName()).append('(');
  Class<?>[] types = method.getParameterTypes() ;
  for (Class<?> clazz : types) {
    builder.append(clazz.getSimpleName()).append(",") ;
  }
  if (method.getParameterTypes().length > 0) {
    builder.deleteCharAt(builder.length() - 1);
  }
  return (builder.append(')').toString()).replaceAll("[^a-zA-Z0-9]", "") ;
}

如果你没有配置key,可以通过当前执行的类及方法生成唯一的key。

2.4 测试

这里我是通过JPA实现CRUD操作

java 复制代码
@Service
public class UserService {

  private final UserRepository userRepository ;
  public UserService(UserRepository userRepository) {
    this.userRepository = userRepository ;
  }

  @Cacheable(name = "user", key = "#id")
  public User queryById(Long id) {
    return this.userRepository.findById(id).orElse(null) ;
  }

  @CacheEvict(name = "user", key = "#user.id")
  public void clearUserById(User user) {
    this.userRepository.deleteById(user.getId()) ;
  }
}

这里的clearUserById方法是有意将方法参数设置为User对象,就是为了方便演示SpEL表达式的使用。

测试接口

java 复制代码
@GetMapping("/{id}")
public User getUser(@PathVariable("id") Long id) {
  return this.userService.queryById(id) ;
}

@DeleteMapping("/{id}")
public void removeUser(@PathVariable("id") Long id) {
  User user = new User() ;
  user.setId(id) ;
  this.userService.clearUserById(user) ;
}

到此,一个简单的缓存组件就实现了。这里待完善及优化的还是非常多的,比如更新缓存,并发量大时缓存那里是不是应该加锁,不然肯定会有很多的线程都执行查询再存入缓存;我们这里是不是还可以借助本地缓存做多级缓存的优化呢?可以参考下面这篇文章。

\[Redis结合Caffeine实现二级缓存:提高应用程序性能\]

相关推荐
城管不管6 小时前
ReAct、Plan-and-Execute、Reflection 三大智能 Agent 范式核心区别
java·人工智能·算法·spring·ai·动态规划
梅孔立11 小时前
两种免费的翻译API调用方式详解 - Edge官方API与个人缓存服务
前端·缓存·edge
gwf21611 小时前
SSD读写速度深度解析:顺序读写vs随机读写、IOPS、延迟,你的硬盘性能到底怎么看?
git·嵌入式硬件·缓存·github·智能硬件
paopaokaka_luck13 小时前
基于springboot3+vue3的智能文库平台(AI智能搜索、AI智能汇总、实时在线状态展示、多格式文档预览与富文本编辑、Echarts图形化分析)
前端·网络·spring boot·网络协议·echarts
番茄炒鸡蛋加糖13 小时前
缓存三大问题
缓存
zzzll111114 小时前
Spring Boot 入门指南:从零开始构建 Java Web 应用
java·前端·spring boot
江晓鱼未暖18 小时前
十七、Redis 核心原理与架构详解
大数据·数据库·数据仓库·redis·缓存·架构
小罗水18 小时前
第13章 Redis 缓存、幂等锁与任务状态
数据库·redis·缓存
热心市民lcj19 小时前
Spring Boot 整合 Caffeine 本地缓存实战
spring boot·后端·缓存
zzzll111119 小时前
Spring Cloud 微服务:从入门到实践
spring·spring cloud·微服务