基于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实现二级缓存:提高应用程序性能]]

相关推荐
ac-er888810 分钟前
PHP常用缓存技术
开发语言·缓存·php
bug菌¹25 分钟前
滚雪球学SpringCloud[1.3]:SpringCloud环境搭建
后端·spring·spring cloud
小大力1 小时前
简单的spring缓存 Cacheable学习
java·redis·缓存
OEC小胖胖1 小时前
Spring MVC系统学习(一)——初识Spring MVC框架
java·后端·学习·spring·mvc
超级小的大杯柠檬水1 小时前
Spring Boot文件上传
java·spring boot·后端
code.song1 小时前
教师工作量|基于springBoot的教师工作量管理系统设计与实现(附项目源码+论文+数据库)
数据库·spring boot·后端
Mao.O2 小时前
在线聊天室项目(Vue3 + SpringBoot)
spring boot·websocket·vue3·在线聊天室
一只编程菜鸟2 小时前
SpringCloud Alibaba之Seata处理分布式事务
分布式·spring·spring cloud
coder what2 小时前
基于springoot新能源充电系统的设计与实现
java·spring boot·后端·新能源充电系统
csstmg2 小时前
vue2+keep-alive h5端 从首页进入客户列表-客户列表更新,从客户列表进入客户详情再返回,客户列表需要缓存筛选条件以及滚动位置
前端·缓存