SpringBoot编程基础教程:缓存和性能优化

发布于:2023-10-24 ⋅ 阅读:(122) ⋅ 点赞:(0)

作者:禅与计算机程序设计艺术

1.背景介绍

  随着互联网web应用的发展,网站的访问量越来越大,对于后端的服务器性能要求也越来越高。而数据查询的速度直接影响到用户体验的好坏。为了解决这个问题,人们在日益复杂的web应用中加入了缓存机制来提升网站的访问速度。本文将以Spring Boot框架作为案例,从缓存实现、缓存分类及应用场景等方面入手,进行缓存相关技术的介绍和实践。通过对Spring Cache的介绍、使用及配置方法、Redis作为缓存服务器的搭建、应用集成Redis缓存等内容,能够让读者对Spring Boot下缓存的相关知识有全面的理解和掌握,并运用实际案例加深对缓存原理、应用场景和选型的理解。

2.核心概念与联系

    缓存是一种提升网站访问速度的方法,其核心理念就是利用空间换取时间,避免频繁访问数据库,减少资源消耗,因此在软件设计上也是非常重要的一环。缓存可以分为三种类型:

  • 客户端缓存(Client Side Caching):浏览器或其他客户端的本地磁盘存储方式。比如浏览器缓存、手机APP缓存等。客户端缓存实现起来比较简单,但是不能充分利用多核CPU,而且会占用用户设备的内存。

  • 反向代理缓存(Reverse Proxy Caching):在服务端增加一个缓存层,所有请求都先经过缓存层,缓存层会检查是否存在缓存副本,如果存在则直接返回响应,否则才去源站获取资源并将结果返回给客户端。反向代理缓存能够最大化地提升网站的访问速度,但同时也引入了一定的复杂性和风险。

  • 服务端缓存(Server Side Caching):在服务端保存资源副本,当下一次相同请求发生时,可以直接返回缓存副本,这样就避免了重复计算,缩短响应时间。虽然比起客户端缓存更省事,但由于需要维护缓存,因此更新缓存的周期、大小等都要考虑。

    本文主要介绍Spring Boot下基于Java中的Spring Cache模块来实现缓存,Spring Cache模块由Spring Framework提供支持。它提供了多种缓存注解如@Cacheable、@Caching、@CachePut、@CacheEvict、@CacheConfig、@CacheKeyGenerator等用于声明缓存规则。Spring Boot在整合Spring Cache的过程中还封装了很多便捷的方式,使开发者可以快速的完成缓存的配置和管理。

      一般来说,反向代理缓存与客户端缓存配合使用,提高静态资源的访问速度;而服务端缓存可以根据不同的业务场景选择使用,例如:查询热点数据可以使用服务端缓存,而查询冷数据可以使用客户端缓存,提高查询效率。总之,缓存策略需要根据实际情况制定。

3.核心算法原理和具体操作步骤以及数学模型公式详细讲解

  缓存通常是为了加快应用运行速度而使用的一种技术,其关键在于如何缓存有效的数据。实现缓存往往依赖于缓存算法,包括LRU算法(最近最少使用)、FIFO算法(先进先出)、LFU算法(最近最不常用)等。本节将以LRU算法为例,详细介绍LRU算法的原理及其在缓存中的应用。

LRU(Least Recently Used,最近最少使用)算法,是页面置换算法的一种。该算法选择最近最久未被访问到的页面予以淘汰。其基本思想是如果某页被访问,则把该页放至栈顶,如果再次访问该页,则将其移至栈顶。当某个页被访问时,若其在栈底,则栈底的页变为栈顶,即栈底的页被淘汰出栈。

下图展示了LRU算法的缓存工作原理。假设现有4个页面P1,P2,P3,P4,初始状态时,P1在栈顶,其它页面依次往下排列。

  1. 当访问页面P2时,将其移动至栈顶,栈结构如下所示:
  1. 当访问页面P1时,将其淘汰掉,栈结构如下所示:
  1. 当再次访问页面P1时,将其重新加载并移动至栈顶,栈结构如下所示:

从图中可见,访问顺序为P2->P1->P1,当再次访问页面P1时,将其重新加载并移动至栈顶。

在缓存的实际应用中,LRU算法可以用来控制缓存容量,达到缓存命中率最大化,且保证最新的缓存元素优先被淘汰。通过设置缓存超时时间,当缓存项超时时,会自动失效,从而保证缓存项的 freshness。

通过调整访问频率,可以将热点数据缓存到内存中,降低缓存的IO负载,提高缓存命中率;通过设置超时时间,可以保证热点数据的有效期,防止缓存数据过期;通过调整算法参数,可以调控缓存的分配比例,平衡缓存空间和命中率。

4.具体代码实例和详细解释说明

4.1 安装Redis

  1. 下载安装包:http://redis.io/download ,当前最新版本为4.0.11。
  2. 配置环境变量:添加 REDIS_HOME 指向 redis 目录,然后把 REDIS_HOME\redis-server.exe 复制到任意路径,如 D:\Programs\redis-server.exe 。
  3. 修改配置文件 redis.windows.conf,确保 port 为非默认端口 (默认为 6379),默认情况下无法远程访问 Redis 服务。
  4. 执行命令启动 Redis 服务器:D:\Programs\redis-server.exe redis.windows.conf

4.2 创建 Spring Boot 项目

  1. 使用 Spring Initializr 生成 Spring Boot 项目

  2. 添加 Redis starter 依赖:

         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>
  3. 添加 @EnableCaching 注解开启缓存功能:

     import org.springframework.cache.annotation.EnableCaching;
    
     @EnableCaching
     @SpringBootApplication
     public class DemoApplication {
    
         public static void main(String[] args) {
             SpringApplication.run(DemoApplication.class, args);
         }
    
     }
  4. 添加 CacheManager 配置类:

     import org.springframework.beans.factory.annotation.Value;
     import org.springframework.cache.CacheManager;
     import org.springframework.cache.annotation.EnableCaching;
     import org.springframework.context.annotation.Bean;
     import org.springframework.context.annotation.Configuration;
     import org.springframework.data.redis.cache.RedisCacheConfiguration;
     import org.springframework.data.redis.cache.RedisCacheManager;
     import org.springframework.data.redis.connection.RedisConnectionFactory;
     import org.springframework.data.redis.core.RedisTemplate;
     import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
     import org.springframework.data.redis.serializer.RedisSerializationContext;
     import org.springframework.data.redis.serializer.RedisSerializer;
    
     import java.time.Duration;
    
     @Configuration
     @EnableCaching
     public class CacheManagerConfig {
    
         @Value("${spring.redis.host}")
         private String host;
    
         @Value("${spring.redis.port}")
         private int port;
    
         @Value("${spring.redis.timeout}")
         private int timeout;
    
         @Bean
         public RedisConnectionFactory redisConnectionFactory() {
             return new LettuceConnectionFactory();
         }
    
         @Bean
         public RedisTemplate<Object, Object> redisTemplate() {
             RedisTemplate<Object, Object> template = new RedisTemplate<>();
             template.setConnectionFactory(redisConnectionFactory());
             // 设置序列化
             GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
             RedisSerializer keySerializer = RedisSerializer.string();
             RedisSerializer valueSerializer = serializer;
             RedisSerializationContext.Builder<Object, Object> builder = RedisSerializationContext
                    .newSerializationContext(keySerializer).value(valueSerializer);
             RedisSerializationContext<Object, Object> context = builder.build();
             template.setKeySerializer(keySerializer);
             template.setValueSerializer(valueSerializer);
             template.setHashKeySerializer(keySerializer);
             template.setHashValueSerializer(valueSerializer);
             template.afterPropertiesSet();
             return template;
         }
    
         /**
          * 初始化RedisCacheManager
          */
         @Bean
         public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
             // 配置序列化
             RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(30))
                    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()))
                    .serializeValuesWith(
                             RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
             RedisCacheManager manager = RedisCacheManager.builder(connectionFactory)
                    .cacheDefaults(config)
                    .transactionAware()
                    .build();
             return manager;
         }
    
     }
  5. 测试缓存效果:编写 Service 接口和 Service 实现类,分别调用 @Cacheable 和 @CacheEvict 注解来测试缓存效果。

     import com.example.demo.entity.User;
     import org.springframework.cache.annotation.CacheConfig;
     import org.springframework.cache.annotation.CacheEvict;
     import org.springframework.cache.annotation.Cacheable;
     import org.springframework.stereotype.Service;
    
     import javax.annotation.Resource;
     import java.util.List;
    
     @Service
     @CacheConfig(cacheNames = "users")// 设置缓存的名字
     public class UserService {
    
         @Resource
         private UserRepository userRepository;
    
         @Cacheable(key="'allUsers'") // 设置缓存的键
         public List<User> getAllUsers(){
             return this.userRepository.findAll();
         }
    
         @Cacheable(key="#id") // 设置缓存的键
         public User getUserById(Long id){
             return this.userRepository.findById(id).get();
         }
    
         @CacheEvict(allEntries=true)// 清空缓存
         public void saveUser(User user){
             this.userRepository.save(user);
         }
    
         @CacheEvict(key="#p0.id") // 根据ID清除缓存
         public void deleteUser(User user){
             this.userRepository.delete(user);
         }
    
     }
     import org.junit.jupiter.api.Test;
     import org.springframework.beans.factory.annotation.Autowired;
     import org.springframework.boot.test.context.SpringBootTest;
    
     import static org.assertj.core.api.Assertions.*;
    
     @SpringBootTest
     class DemoApplicationTests {
    
         @Autowired
         private UserService userService;
    
         @Test
         void testGetAllUsers() throws Exception{
             List<User> allUsers1 = userService.getAllUsers();
             assertThat(allUsers1.size()).isGreaterThanOrEqualTo(1);// 查询数据第一次查询不走缓存
             Thread.sleep(1000);// 等待1秒,使缓存过期
             List<User> allUsers2 = userService.getAllUsers();
             assertThat(allUsers2.size()).isGreaterThanOrEqualTo(1);// 查询数据第二次查询走缓存
             assertNotSame(allUsers1, allUsers2);// 判断两个对象地址不同
         }
    
         @Test
         void testGetUserById() throws Exception{
             Long userId = 1L;
             User user1 = userService.getUserById(userId);
             assertThat(user1).isNotNull();// 查询数据第一次查询不走缓存
             Thread.sleep(1000);// 等待1秒,使缓存过期
             User user2 = userService.getUserById(userId);
             assertThat(user2).isNotNull();// 查询数据第二次查询走缓存
             assertSame(user1, user2);// 判断两个对象地址相同
         }
    
         @Test
         void testSaveUser() throws Exception{
             User user = new User("Tom", 20);
             userService.saveUser(user);
             Thread.sleep(1000);// 等待1秒,使缓存过期
             User dbUser = userService.getUserById(user.getId());
             assertThat(dbUser).isNotNull();// 保存数据之后,立刻查出数据
             assertEquals(user.getName(), dbUser.getName());// 比较查询出的数据和预期数据是否一致
             assertEquals(user.getAge(), dbUser.getAge());
         }
    
         @Test
         void testDeleteUser() throws Exception{
             Long userId = 1L;
             User user = userService.getUserById(userId);
             assertThat(user).isNotNull();// 删除之前查询一次
             userService.deleteUser(user);
             Thread.sleep(1000);// 等待1秒,使缓存过期
             User dbUser = userService.getUserById(userId);
             assertThat(dbUser).isNull();// 删除之后,立刻查出数据为空
             Thread.sleep(1000);// 等待1秒,使缓存过期
             User nullUser = userService.getUserById(userId);
             assertThat(nullUser).isNull();// 查不到数据
         }
    
     }

5.未来发展趋势与挑战

  近年来,缓存已经成为许多应用程序的标配组件。由于其相对简单的使用,使得它能够快速的帮助优化网站的访问速度,提升用户体验。随着Web应用变得越来越复杂,涉及到更多的数据查询和复杂的业务逻辑,缓存技术已经成为优化web应用性能的关键瓶颈。

  Spring Boot 提供的缓存模块,使开发者能够轻松的集成缓存到自己的应用中。但同时也带来了一些潜在的问题和挑战。首先,缓存依赖于各种第三方库,版本兼容问题、不稳定性等问题可能导致应用不可用。另外,缓存的分布式部署也会增加复杂度,扩展性差。

  本文介绍了缓存相关的理论知识,并结合Spring Boot框架下的缓存模块,探讨了缓存的原理及其使用方法,通过实际案例加深了缓存的相关知识。但同时,也应该看到,缓存只是解决特定问题的工具,而并不是万能的。因此,在实践中,应该根据业务特点、系统资源、可用性等多种因素综合考虑,合理配置缓存方案,进一步提升系统性能。


网站公告

今日签到

点亮在社区的每一天
去签到