接口数据做缓存,响应飞快似天神

概述

在商业中 "现金为王",而在互联网整个软件世界中,有一个与之相近的说法是"缓存为王"。

本文我们重点说明缓存方向:将方法的返回值缓存起来,下次再调用该方法时,如果方法的参数与之前调用时的参数相同,则会直接返回缓存中的结果,而不会再执行方法体。这样可以提高方法的执行效率

优点

  • 提高性能:缓存可以将方法的结果存储在内存中,后续对相同参数的方法调用可以直接从缓存中获取结果,避免重复计算,提高方法的执行效率。
  • 减轻数据库压力:对于需要频繁访问数据库的方法,可以将结果缓存在内存中,减轻数据库的压力,提高数据库的响应速度。
  • 简化代码逻辑:通过使用缓存,可以避免编写复杂的手动缓存代码或条件判断逻辑,使代码更简洁、可读性更好。

缺点

  • 内存占用:缓存的数据存储在内存中,因此如果缓存的数据量过大,会消耗大量的内存资源。在使用缓存时需要合理安排系统的内存和缓存容量。
  • 数据一致性:使用缓存后,需要注意维护数据的一致性。当数据发生变化时,需要及时更新缓存,以避免脏数据的问题。可通过合理设计缓存策略和使用缓存失效机制来解决这个问题。
  • 缓存失效:缓存的有效期限需要合理设置。如果缓存的有效期太长,可能导致数据更新不及时;如果缓存的有效期太短,可能增加重复执行代码的次数。需要根据具体业务场景来选择合适的缓存有效期。
  • 缓存击穿:当某个缓存条目在缓存失效时,同时有大量的并发请求到达时,可能会导致缓存击穿问题,即大量请求直接击穿到数据库。可以通过加锁或使用分布式锁等机制来避免缓存击穿。

注解介绍

Spring缓存机制通过 @EnableCaching开启,配合 @Cacheable、 @CachePut、 @CacheEvict等注解,为Java应用提供了一种声明式管理缓存的方式。这些注解使得缓存配置变得简洁明了,允许开发者轻松实现数据的自动缓存、更新和清除,从而优化应用性能,减少不必要的计算和数据访问开销。

1. 启用缓存支持

@EnableCaching 注解用于在Spring配置类上启用缓存管理功能。

  • 注解属性介绍

无属性。

  • 注解业务案例
java 复制代码
1.  @Configuration
    
2.  @EnableCaching
    
3.  public class CacheConfig {
    
4.      // 配置其他Bean`
    
5.  }
    

2. 缓存结果

@Cacheable 注解用于声明一个方法的结果是可缓存的。

  • 注解属性介绍

  • valuecacheNames: 指定缓存名称,可以是单个或多个。

  • key: 指定缓存键的SpEL表达式。

  • condition: 指定缓存的条件SpEL表达式。

  • unless: 指定不缓存的条件SpEL表达式。

  • 注解业务案例 单一缓存名称和键:

java 复制代码
1.  @Cacheable("books")
    
2.  public Book findBookById(String id) {
    
3.      // 业务逻辑
    
4.  }

多个缓存名称和条件:

java 复制代码
1.  @Cacheable(value = {"books", "archivedBooks"}, condition = "#id.length() > 10")
2.  public Book findBookWithComplexKey(String id) {
3.      // 业务逻辑
4.  }
    
6.  @Service
7.  public class MyService {
    
9.      /**
    
10.       `* 一个使用 @Cacheable 所有属性的案例。`
    
11.       `*` 
    
12.       `* @param id 用户ID`
    
13.       `* @return 返回用户对象`
    
14.       `*/
    
15.      @Cacheable(
    
16.          value = "users",          // 缓存名称`
    
17.          key = "#id",              // 缓存键,使用SpEL表达式`
    
18.          condition = "#id.length() > 3",  // 缓存条件,只有当ID长度大于3时才缓存`
    
19.          unless = "#result == null" // 除非条件,如果结果为null,则不缓存`
    
20.      )
21.      public User findUserById(String id) {
    
22.          // 模拟数据库查询操作,这里假设ID长度小于3时没有结果`
    
23.          if (id == null || id.length() <= 3) {
    
24.              return null;
    
25.          }
    
26.          return performDatabaseQuery(id);
    
27.      }
    

29.      private User performDatabaseQuery(String id) {
    
30.          // 模拟数据库查询逻辑`
    
31.          return new User(id, "Name based on ID");
    
32.      }
    
33.  }

3. 更新缓存

@CachePut 注解用于在方法执行后更新缓存。

  • 注解属性介绍

@Cacheable相同。

  • 注解业务案例
java 复制代码
 1.  @CachePut("books")

 2.  public Book updateBookDetails(String id, Book details) {

 3.  // 业务逻辑

 4.  }

 5.  

 6.  @Service

 7.  public class MyService {

 8.  

 9.  /**

 10. * 使用 @CachePut 所有属性的案例。

 11. *

 12. * @param user 用户对象,包含ID

 13. * @return 更新后的用户对象

 14. */

 15. @CachePut(

 16. 	value = "users", // 缓存名称

 17. 	key = "#user.id", // 缓存键,使用SpEL表达式

 18. 	condition = "#user.age > 18" // 条件,只有当用户年龄大于18时才更新缓存

 19. )

 20. public User updateUserProfile(User user) {

 21. // 模拟更新用户信息的业务逻辑

 22. return performUpdate(user);

 23. }

 24. 

 25. private User performUpdate(User user) {

 26. // 模拟更新逻辑

 27. user.setName("Updated Name");

 28. return user;

 29. }

 30. }

4. 清除缓存

@CacheEvict 注解用于在方法执行后清除缓存。

注解属性介绍

  • valuecacheNames: 指定缓存名称,可以是单个或多个。

  • allEntries: 清除所有缓存项。

  • condition: 指定清除缓存的条件SpEL表达式。

注解业务案例

清除特定缓存名称的条目:

java 复制代码
1.  @CacheEvict("books")
    
2.  public void deleteBook(String id) {
    
3.      // 业务逻辑
    
4.  }

清除所有缓存名称的所有条目:

java 复制代码
1.  @CacheEvict(allEntries = true)
    
2.  public void clearAllCaches() {
    
3.      // 业务逻辑
    
4.  }

5. 组合缓存操作

@Caching 注解用于组合多个缓存操作。

  • 注解属性介绍

value: 包含多个缓存操作的数组。

  • 注解业务案例
java 复制代码
1.  @Caching(
    
2.      cacheable = {@Cacheable("books")},
    
3.      put = {@CachePut("books")},
    
4.      evict = {@CacheEvict("archivedBooks")}
    
5.  )
    
6.  public Book processBook(String id, Book details) {
    
7.      // 业务逻辑
    
8.  }
    

代码演示

redis依赖安装

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

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.21</version>
        </dependency>
  • redis配置文件
yml 复制代码
spring:
  redis:
    # 连接地址
    host: "127.0.0.1"
    # 端口
    port: 6379
    # 数据库
    database: 1
    password: helloworld
    # 连接超时
    connect-timeout: 5s
    # 读超时
    timeout: 5s

    # Lettuce 客户端的配置
    lettuce:
      # 连接池配置
      pool:
        # 最小空闲连接
        min-idle: 0
        # 最大空闲连接
        max-idle: 8
        # 最大活跃连接
        max-active: 8
        # 从连接池获取连接 最大超时时间,小于等于0则表示不会超时
        max-wait: -1ms

接口缓存配置类

框架给我们提供了 @Cacheable 注解用于缓存方法返回内容。但是 @Cacheable 注解 不能 定义缓存 有效期。这样的话在一些需要自定义缓存有效期的场景就不太实用。

框架给我们提供的 RedisCacheManager 类,是 准对这一块的 就配置类, 我们可以根据此类,自己实现 缓存有效期 的功能。

java 复制代码
package com.xuzhongkj.configrations;


import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCache;
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.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import javax.validation.constraints.NotNull;
import java.time.Duration;
import java.util.Objects;


@Configuration
@EnableCaching
public class CustomRedisCacheManager {

    private String keyPreFix = "whero";

    private RedisSerializer<String> keySerializer() {
        return new StringRedisSerializer();
    }

    private RedisSerializer<Object> valueSerializer() {
        // fastjson 中的类
        return new GenericFastJsonRedisSerializer();
    }


    /*
     * @description 配置自定义 缓存管理器: RedisCacheManager
     **/
    @Bean
    public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
        // 缓存注解 配置:
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                //设置 key 为String
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
                //设置 value 为自动转Json的Object
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
                // 使用 prefixCacheNameWith 需要注意系统自动拼接的双":"问题
                .computePrefixWith(cacheName -> keyPreFix + ":" + cacheName + ":");


        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(Objects.requireNonNull(redisTemplate.getConnectionFactory()));

        RedisCacheManager redisCacheManager = new CustomRedisCacheManagerToolsClass(redisCacheWriter, config);
        return redisCacheManager;
    }
}


/**
 * @Title: 自定义redis缓存管理器, 为了实现 缓存有效期的 动态性
 */
class CustomRedisCacheManagerToolsClass extends RedisCacheManager {

    public static final String SEPARATOR = "#";

    public CustomRedisCacheManagerToolsClass(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
    }

    @Override
    protected RedisCache createRedisCache(@NotNull(message = "缓存名称不能为空") String name, RedisCacheConfiguration cacheConfiguration) {
        // 举例说明: name = "bookCourse:getCourseId#7D"
        if (name.contains(SEPARATOR)) {
            String[] spelStr = name.split(SEPARATOR);
            String key = spelStr[0];
            String valueStr = spelStr[1];

            int length = valueStr.length();

            if (length >= 2) {
                String cycleTimeStr = valueStr.substring(0, length - 1);

                if (cycleTimeStr.matches("\\d+")) {
                    long cycleTime = Long.parseLong(cycleTimeStr);

                    String cycleUnit = valueStr.substring(length - 1, length);

                    if (cycleUnit.equals("D")) {//表示天
                        return super.createRedisCache(key, cacheConfiguration.entryTtl(Duration.ofDays(cycleTime)));
                    }
                    if (cycleUnit.equals("H")) {//表示小时
                        return super.createRedisCache(key, cacheConfiguration.entryTtl(Duration.ofHours(cycleTime)));
                    }
                    if (cycleUnit.equals("M")) {//表示分钟
                        return super.createRedisCache(key, cacheConfiguration.entryTtl(Duration.ofMinutes(cycleTime)));
                    }
                    if (cycleUnit.equals("S")) {//表示秒
                        return super.createRedisCache(key, cacheConfiguration.entryTtl(Duration.ofSeconds(cycleTime)));
                    } else {
                        // 都不是则使用默认配置
                        return super.createRedisCache(name, cacheConfiguration);
                    }
                }
            }
        }
        return super.createRedisCache(name, cacheConfiguration);
    }
}

这里面简单对 RedisCacheConfiguration 缓存配置做一下说明:

  • serializeKeysWith():设置 Redis 的 key 的序列化规则。
  • erializeValuesWith():设置 Redis 的 value 的序列化规则。
  • computePrefixWith():计算 Redis 的 key 前缀。
  • cacheConfiguration.entryTtl(): 设置 @Cacheable 注解缓存的有效期。

测试使用

service层

java 复制代码
package com.xuzhongkj.services.impls;

import com.xuzhongkj.pojo.Userswtt;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class WttService {

    @Cacheable(cacheNames = {"bookCourse:getCourseId#7M"}, key = "#name")
    public Userswtt getName (String name) {
        System.out.println("-------  创建 -----------"+name);
        Userswtt u = new Userswtt();
        u.setOpenid("qwejlksdfjkdf");
        u.setAge(11);
        u.setAvatar("https:/asdfj;askdf");
        u.setUid(123L);
        return  u;
    }

    @CachePut(cacheNames = {"bookCourse:getCourseId#7M"}, key = "#name")
    public Userswtt EdtName (String name) {
        System.out.println("---------- 更新 --------"+name);
        Userswtt u = new Userswtt();
        u.setOpenid("qwejlksdfjkdf");
        u.setAge(22);
        u.setAvatar("https:/asdfj;askdf");
        u.setUid(123L);
        return  u;
    }

    @CacheEvict(cacheNames = {"bookCourse:getCourseId#7M"}, key = "#name")
    public Userswtt DelName (String name) {
        System.out.println("--------- 删除 ---------"+name);
        Userswtt u = new Userswtt();
        return  u;
    }
}

controller 层

java 复制代码
@RestController
@Slf4j
@RequestMapping("/test")
public class WttTest {

    @Autowired
    private WttService wttService;

    @GetMapping("/wtt")
    public Userswtt a1() {
            Userswtt name = wttService.getName("testResObj");
            return name;
    }

    @GetMapping("/wtt2")
    public Userswtt a2() {
            Userswtt name = wttService.EdtName("testResObj");
            return name;
    }

    @GetMapping("/wtt3")
    public Userswtt a3() {
            return wttService.DelName("testResObj");
    }

redis中键值对的存储情况:

  • key:

whero:bookCourse:getCourseId:testResObj

  • value

{"@type":"com.xuzhongkj.pojo.Userswtt","age":11,"avatar":"https:/asdfj;askdf","openid":"qwejlksdfjkdf","uid":123}

相关推荐
eternal__day13 分钟前
数据结十大排序之(选排,希尔,插排,堆排)
java·数据结构·算法·推荐算法
我焦虑的编程日记15 分钟前
【期末复习】JavaEE(上)
java·java-ee
o不ok!18 分钟前
java中File类
java·开发语言
小小小小关同学20 分钟前
【并发容器】ConcurrentLinkedQueue:优雅地实现非阻塞式线程安全队列
java·开发语言·安全
熬夜的猪30 分钟前
Redis 最佳实践
java·redis·后端
Qzer_40732 分钟前
在JVM(Java虚拟机)中,PC寄存器(Program Counter Register)扮演着至关重要的角色。
java·开发语言·jvm
yuanbenshidiaos36 分钟前
多线程----互斥
java·jvm·redis
岁岁岁平安38 分钟前
spring学习(spring-bean实例化(静态工厂))
java·学习·spring·bean·静态工厂模式·factory_method
老马啸西风42 分钟前
分布式链路追踪-02-Dapper 论文介绍
java
涡能增压发动积44 分钟前
Java不能搞大模型应用开发?SpringAI拯救你
java·人工智能·后端