Spring Boot中如何缓存数据库查询结果

缓存是一种技术,可以存储不经常变化的频繁查询数据,并减少请求的延迟。我们可以在软件应用程序的不同层使用这种技术。

在本文中,讨论在 Spring Boot 中使用 ConcurrentHash 和 Redis 缓存数据库结果

使用案例

  • 我们已经意识到(在检查和可视化跟踪之后,每当调用` /book_reviews/:isbn ` API时,都会进行查询以从 books 表中获取 book_reviews 。
  • 我们的应用程序在book_reivews API上获得了大量流量,并且调用来获取每本书的评论效率不高,因为 book_reviews 的更改并不频繁。
  • 因此,我们希望在应用程序级别本地缓存书评,以便减少数据库往返次数并减少数据库上的读取查询,从而可以为其他关键查询提供更好的延迟。


实体
我们将使用 BookReview 实体。

@Entity
@Table(name="BOOK_REVIEWS")
public class BookReview {
 
    @Id
    @GeneratedValue(strategy= GenerationType.SEQUENCE, generator = "book_reviews_reviews_id_seq")
    @SequenceGenerator(name = "book_reviews_reviews_id_seq", sequenceName = "book_reviews_reviews_id_seq", allocationSize = 1)
    private Long reviewsId;
    private String userId;
    private String isbn;
    private String bookRating;
}
确保将一些示例数据添加到 book_reviews 表中。

控制器
控制器是一个调用逻辑类的简单休息端点。
@RestController
@RequestMapping("/books")
public class BookReviewsController {
 
    @Autowired
    private BookReviewsWithSimpleCacheLogic bookReviewsLogic;
 
    @GetMapping("/reviews")
    @ResponseBody()
    public List<BookReview> getBookReviews(@RequestParam("isbn") String isbn){
        return bookReviewsLogic.getAllReviewsByIsbn(isbn);
    }
}

逻辑
逻辑类使用存储库通过 isbn 获取 book_reviews。
@Service
public class BookReviewsWithSimpleCacheLogic {
 
    @Autowired
    private BookRepository bookRepository;
 
    public List<BookReview> getAllReviewsByIsbn(String isbn){
        return bookRepository.findByIsbn(isbn);
    }
}

缓存 
Spring为其cache_abstraction提供了多种存储选项,我们可以在其中进行选择。如果我们没有定义一个,那么它会选择默认值,这是一个简单的并发哈希图。

Spring提供了@Cacheble、@CacheEvict和@CachePut等几种不同的注解来实现缓存。

  • @Cacheble:用它来存储定义缓存的方法的缓存结果,如果已经存在则返回。
  • @CachePut:调用时强制更新缓存
  • @CacheEvict:当调用带注释的方法时逐出缓存

询问

为了查询书评,我们的存储库正在对数据库进行以下查询。
select br1_0.reviews_id,br1_0.book_rating,br1_0.isbn,br1_0.user_id from book_reviews br1_0 where br1_0.isbn=?

缓存查询结果

我们使用@Cacheble注释添加缓存,并定义缓存键和缓存值。这里的值定义了缓存的名称,键用于在缓存中查询特定的键。

@Cacheable(value = "book_reviews", key = "isbn")
 public List<BookReview> getAllReviewsByIsbn(String isbn){
      System.out.println(
"cache doesn't exist, querying the db");
      return bookRepository.findByIsbn(isbn);
 }

当我们到达 book_reviews 端点时,由于 @Cacheable 注释,它将首先检查缓存是否已存在,如果不存在,则计算方法并将结果分配给缓存。

如果我们第一次到达端点,我们会看到以下查询被执行到数据库。
Executing SQL Query: select br1_0.reviews_id,br1_0.book_rating,br1_0.isbn,br1_0.user_id from book_reviews br1_0 where br1_0.isbn=?
但在后续请求中,我们将看不到任何日志,因为结果将从缓存中返回。

如果您想在 JPA 中记录 SQL 查询,您可以使用以下命令设置 application.properties:
spring.jpa.show-sql=true

您还可以添加 sql 拦截器并在应用程序代码中记录 SQL 查询。

逐出缓存
我们不能永远保留缓存,我们需要根据过期时间驱逐它。例如,我们可以将缓存保留5分钟,然后过期。
5 分钟后,缓存过期,在下一次调用中,它会被重新分配给数据库查询返回的值。
Spring缓存提供@CacheEvict注解来驱逐缓存。我们可以定义任何特定的键,我们希望将其从缓存中删除。

@CacheEvict(value = "book_reviews", key = "isbn")
public void evictCachesWithKey(String isbn){
    System.out.println(
"evicting cache");
}

我们需要调用evictCachesWithKey ()方法来移除缓存。
出于演示目的,我们将使用端点调用此方法并验证该方法是否按预期工作。

@GetMapping("/evict")
    @ResponseBody()
    public void evictCaches(@RequestParam(
"isbn") String isbn){
        bookReviewsLogic.evictCachesWithKey(isbn);
    }

如果我们到达端点,我们将看到相同的日志,并且在下一个请求中,我们将看到对数据库进行了书评查询,但没有从缓存返回。


自动清除缓存
到目前为止,为了逐出缓存,我们一直在手动调用 evict api。但理想的行为是根据定义的生存时间 (TTL) 自动调用它。
我们需要对逻辑进行一些更改,首先我们需要添加一个存储缓存键及其设置时间的哈希图。这将有助于确定缓存是否已超过定义的 TTL。

private final Map<String, LocalDateTime> cacheKeyWithStartTime = new HashMap<>();
 
@Cacheable(value = "book_reviews", key = "isbn")
public List<BookReview> getAllReviewsByIsbn(String isbn){
     System.out.println(
"cache doesn't exist, querying the db");
     cacheKeyWithStartTime.put(
"book_reviews:"+isbn, LocalDateTime.now());
     return bookRepository.findByIsbn(isbn);
}

其次,我们需要添加一个调度程序来定期运行逐出方法,以便它将删除所有超过定义的 TTL 的到期缓存。

我们还修改了缓存驱逐逻辑,我们不再使用@CacheEvict注释。相反,我们注入cacheManager,它将通过键为我们提供缓存,并且我们将通过引用哈希图来确认是否超过了TTL,因为它存储了设置的时间。

@Autowired
 private CacheManager cacheManager;
  
 private final Duration TTL = Duration.ofMinutes(1);
  
 @Scheduled(cron="0 * * * * *")
 public void scheduleCacheEviction(){
      System.out.println(
"Evicting Cache");
      evictCaches();
 }
 
 public void evictCaches(){
    cacheKeyWithStartTime.forEach((key, value) -> {
         String[] cacheKey = key.split(
":");
         Cache cache = cacheManager.getCache(cacheKey[0]);
 
         long actualDuration = Duration.between(value, LocalDateTime.now()).toMinutes();
          
         if(actualDuration >= TTL.toMinutes()){
             boolean isEvicted = cache.evictIfPresent(cacheKey[1]);
             System.out.println(cacheKey[1]+
" cache evicted? " + isEvicted);
          }
        });
    }
  }

使用Redis进行缓存
我们可以通过 Spring Boot 使用 Redis 进行缓存。

Redis实例
我有一个Mac实例,在Mac上,我们可以使用brew安装并启动Redis服务。

->> ~ $ brew install redis
Running `brew update --auto-update`...
 
->> ~ $  brew services start redis
==> Successfully started `redis` (label: homebrew.mxcl.redis)


在windows上安装redis

依赖性
我们需要在 pom.xml 中添加 redis-data 依赖项。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

缓存配置
缓存配置将为我们的 Redis 缓存管理器进行设置。在这里我们可以定义缓存的配置和 TTL。我们定义 TTL 为 1 分钟。

@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        cacheConfigurations.put("book_reviews", defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(1)));
 
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory)
                .cacheDefaults(defaultCacheConfig())
                .withInitialCacheConfigurations(cacheConfigurations);
 
        return builder.build();
    }
 
    private RedisCacheConfiguration defaultCacheConfig() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }
}

缓存逻辑
现在对于我们的缓存逻辑,我们只需要定义@Cacheble并添加缓存键和值。我们需要确保将 book_reviews 值添加到 Redis 缓存管理器中。

@Service
public class BookReviewsWithRedisLogic {
 
    @Autowired
    private BookRepository bookRepository;
 
    @Cacheable(value="book_reviews", key="isbn")
    public List<BookReview> getAllReviewsByIsbn(String isbn){
       return bookRepository.findByIsbn(isbn);
    }
}

我们不需要定义缓存逐出,因为它已经由 Redis 固有地处理。

结论
在本文中,我们学习如何使用简单的并发哈希图存储在 Spring Boot 中进行缓存。我们自动为缓存添加了 TTL 过期时间。我们还设置了 Redis 缓存管理器来使用它进行缓存。