SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存

SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存

码农世界 2024-06-15 后端 82 次浏览 0个评论

文章目录

    • 1. 前言
    • 2. 缓存
      • 2.1 什么是缓存
      • 2.2 使用缓存的好处
      • 2.3 缓存的成本
      • 2.4 Spring Cache和Redis的优点
      • 3. Spring Cache基础知识
        • 3.1 Spring Cache的核心概念
        • 3.2 Spring Cache的注解
          • 3.2.1 SpEL表达式
          • 3.2.2 @Cacheable
          • 3.2.3 @CachePut
          • 3.2.4 @CacheEvict
          • 4. 实现查询数据缓存
            • 4.1 准备工作
            • 4.2 添加依赖
            • 4.3 修改配置文件
            • 4.4 配置缓存管理器
            • 4.5 使用Spring Cache注解
            • 4.6 测试
              • 4.6.1 查询测试
              • 4.6.2 更新、删除测试
              • 5. 总结

                1. 前言

                在现代应用程序中,查询缓存的使用已经变得越来越普遍。它不仅能够显著提高系统的性能,还能提升用户体验。缓存通过在内存中存储频繁访问的数据,减少对数据库或其他存储系统的访问,从而加快数据读取速度。在这篇文章中,我们将探讨缓存的基本概念、重要性以及如何使用Spring Cache和Redis实现查询数据缓存 。

                2. 缓存

                2.1 什么是缓存

                缓存是一种临时存储机制,用于在内存中保存频繁访问的数据。它可以是硬件(如CPU缓存)或软件(如应用程序缓存)。缓存的主要目的是通过减少数据访问的延迟,提高系统的响应速度。以下是缓存的一些关键特性:

                • 临时性:缓存中的数据通常是临时的,会在一段时间后失效或被替换。
                • 快速访问:由于缓存数据存储在内存中,访问速度非常快。
                • 空间有限:缓存的存储空间通常有限,因此需要有效的管理策略,如LRU(最近最少使用)策略。

                  2.2 使用缓存的好处

                  1. 提高性能:缓存可以显著减少数据读取的时间,因为内存访问速度比硬盘或网络存储快很多。
                  2. 减轻数据库负载:缓存可以减少数据库的查询次数,从而减轻数据库的负载,提升整体系统的稳定性和可扩展性。
                  3. 节省资源:通过减少对后端系统的访问,缓存可以帮助节省带宽和计算资源。
                  4. 提高用户体验:快速的数据访问可以显著提升用户体验,特别是在需要频繁读取数据的应用场景中。

                  2.3 缓存的成本

                  1. 内存消耗:缓存需要占用系统的内存资源,过多的缓存可能会影响其他应用程序的性能。
                  2. 数据一致性:缓存中的数据可能会与数据库中的数据不一致,尤其是在数据频繁更新的场景中。需要设计有效的缓存失效策略来保证数据的一致性。
                  3. 复杂性增加:引入缓存机制会增加系统的复杂性,需要处理缓存的管理、更新和失效等问题。
                  4. 维护成本:缓存系统需要定期监控和维护,以确保其高效运行。

                  2.4 Spring Cache和Redis的优点

                  为了实现高效的数据缓存,Spring Boot提供了Spring Cache模块,而Redis则是一个强大的缓存数据库。结合使用Spring Cache和Redis,能够充分发挥二者的优点,实现高效的数据缓存。

                  • Spring Cache的优点:
                    • 简化缓存操作:Spring Cache提供了一系列注解(如@Cacheable、@CachePut、@CacheEvict),简化了缓存的使用,使开发者能够专注于业务逻辑。
                    • 灵活的缓存管理:Spring Cache支持多种缓存提供者(如EhCache、Hazelcast、Redis等),可以根据具体需求选择合适的缓存实现。
                    • 透明的缓存机制:Spring Cache使得缓存操作对业务代码透明,开发者无需关心缓存的具体实现细节。
                    • Redis的优点:
                      • 高性能:由于数据存储在内存中,Redis的读写速度非常快,能够处理每秒数百万级别的请求。
                      • 丰富的数据结构:Redis支持多种数据结构,如字符串、哈希、列表、集合、有序集合等,能够满足不同场景下的数据存储需求。
                      • 持久化支持:虽然Redis主要用于内存存储,但它也提供了数据持久化的功能,可以将数据定期保存到磁盘,防止数据丢失。
                      • 分布式支持:Redis支持主从复制、哨兵模式和集群模式,能够实现高可用性和数据的水平扩展。
                      • 灵活的过期策略:Redis支持为每个键设置过期时间,自动删除过期数据,方便实现缓存失效策略。

                        3. Spring Cache基础知识

                        在Spring Boot中,Spring Cache提供了一套简洁且强大的缓存抽象机制,帮助开发者轻松地将缓存集成到应用程序中。以下是Spring Cache的一些核心概念和常用注解。

                        3.1 Spring Cache的核心概念

                        1. CacheManager

                          • 定义:CacheManager是Spring Cache的核心接口,负责管理多个缓存实例。它是缓存操作的入口点,提供了获取和操作缓存实例的方法。
                          • 实现:Spring提供了多种CacheManager实现,如ConcurrentMapCacheManager、EhCacheCacheManager、RedisCacheManager等。不同的实现适用于不同的缓存存储机制。
                          • Cache

                            • 定义:Cache是缓存的具体实现,负责存储和检索缓存数据。它提供了基本的缓存操作,如put、get、evict等。
                            • 实现:具体的Cache实现依赖于底层的缓存存储机制,如内存缓存、Redis缓存等。

                        3.2 Spring Cache的注解

                        3.2.1 SpEL表达式

                        因为Spring Cache使用SpEL表达式来动态生成缓存键,所以在学习Spring Cache的注解之前我们还要先简单了解一下SpEL表达式的语法,这部分可以先不看懂,在后面看注解的时候回来看即可。

                        SpEL表达式的语法类似于Java的表达式语法,支持以下几种操作:

                        1. 字面量:
                          • 数字:1, 2.5
                          • 字符串:'hello', "world"
                          • 布尔值:true, false
                          • 空值:null
                          • 属性和方法:
                            • 访问对象的属性:#user.name
                            • 调用对象的方法:#user.getName()
                            • 运算符:
                              • 算术运算:+, -, *, /, %
                              • 比较运算:==, !=, <, >, <=, >=
                              • 逻辑运算:&&, ||, !
                              • 集合和数组:
                                • 访问集合元素:#users[0]
                                • 集合操作:#users.size(), #users.isEmpty()
                                • 条件运算符:
                                  • 三元运算符:condition ? trueValue : falseValue
                                  • Elvis运算符:expression ?: defaultValue
                                  • 变量:
                                    • 定义和使用变量:#variableName

                        接下来进入Spring Cache注解的学习:

                        3.2.2 @Cacheable
                        • 作用:@Cacheable注解用于标注需要缓存的方法。当该方法被调用时,Spring Cache会先检查缓存中是否存在对应的数据。如果存在,则直接返回缓存数据;如果不存在,则执行方法并将结果存入缓存。
                        • 示例:
                          @RestController("/users")
                          @RequiredArgsConstructor
                          public class UserController {
                              private final UserService userService;
                              
                              @Cacheable(value = "user", key = "#id")
                              public User getUser(Long id) {
                                  // 获取用户的逻辑
                                  return userService.findById(id);
                              }
                          }
                          
                        • 参数:
                          • value:指定缓存的名称。
                          • key:指定缓存的键,可以使用SpEL表达式。
                            3.2.3 @CachePut
                            • 作用:@CachePut注解用于标注需要更新缓存的方法。即使缓存中已经存在数据,该方法仍然会执行,并将结果更新到缓存中。
                            • 示例:
                              @RestController("/users")
                              @RequiredArgsConstructor
                              public class UserController {
                                  private final UserService userService;
                                  
                                  @CachePut(value = "user", key = "#user.id")
                                  public User updateUser(User user) {
                                      // 更新用户的逻辑
                                      return userService.save(user);
                                  }
                              }
                              
                            • 参数:
                              • value:指定缓存的名称。
                              • key:指定缓存的键,可以使用SpEL表达式。
                                3.2.4 @CacheEvict
                                • 作用:@CacheEvict注解用于标注需要清除缓存的方法。当该方法被调用时,Spring Cache会清除对应的缓存数据。
                                • 示例:
                                  @RestController("/users")
                                  @RequiredArgsConstructor
                                  public class UserController {
                                      private final UserService userService;
                                      
                                      @CacheEvict(value = "user", key = "#id")
                                      public void deleteUser(Long id) {
                                          // 删除用户的逻辑
                                          userService.deleteById(id);
                                      }
                                  }
                                  
                                • 参数:
                                  • value:指定缓存的名称。
                                  • key:指定缓存的键,可以使用SpEL表达式。
                                  • allEntries:如果设置为true,则清除缓存中的所有数据。

                                    4. 实现查询数据缓存

                                    4.1 准备工作

                                    1. Redis安装与配置:

                                    这里可以自行查找文章进行安装和配置,网上优质文章很多👻。

                                    1. 创建Product实体类:
                                    @Data
                                    @AllArgsConstructor
                                    public class Product implements Serializable {
                                        private Long id;
                                        private String name;
                                        private Integer category;
                                        private String description;
                                        private Integer stock;
                                    }
                                    
                                    1. 创建枚举类ResultEnum:
                                    @Getter
                                    public enum ResultEnum {
                                        /* 成功状态码 */
                                        SUCCESS(1, "操作成功!"),
                                        /* 错误状态码 */
                                        FAIL(0, "操作失败!"),
                                        /* 参数错误:10001-19999 */
                                        PARAM_IS_INVALID(10001, "参数无效"),
                                        PARAM_IS_BLANK(10002, "参数为空"),
                                        PARAM_TYPE_BIND_ERROR(10003, "参数格式错误"),
                                        PARAM_NOT_COMPLETE(10004, "参数缺失"),
                                        /* 用户错误:20001-29999*/
                                        USER_NOT_LOGGED_IN(20001, "用户未登录,请先登录"),
                                        USER_LOGIN_ERROR(20002, "账号不存在或密码错误"),
                                        USER_ACCOUNT_FORBIDDEN(20003, "账号已被禁用"),
                                        USER_NOT_EXIST(20004, "用户不存在"),
                                        USER_HAS_EXISTED(20005, "用户已存在"),
                                        /* 系统错误:40001-49999 */
                                        FILE_MAX_SIZE_OVERFLOW(40003, "上传尺寸过大"),
                                        FILE_ACCEPT_NOT_SUPPORT(40004, "上传文件格式不支持"),
                                        /* 数据错误:50001-599999 */
                                        RESULT_DATA_NONE(50001, "数据未找到"),
                                        DATA_IS_WRONG(50002, "数据有误"),
                                        DATA_ALREADY_EXISTED(50003, "数据已存在"),
                                        AUTH_CODE_ERROR(50004, "验证码错误"),
                                        /* 权限错误:70001-79999 */
                                        PERMISSION_UNAUTHENTICATED(70001, "此操作需要登陆系统!"),
                                        PERMISSION_UNAUTHORIZED(70002, "权限不足,无权操作!"),
                                        PERMISSION_EXPIRE(70003, "登录状态过期!"),
                                        PERMISSION_TOKEN_EXPIRED(70004, "token已过期"),
                                        PERMISSION_LIMIT(70005, "访问次数受限制"),
                                        PERMISSION_TOKEN_INVALID(70006, "无效token"),
                                        PERMISSION_SIGNATURE_ERROR(70007, "签名失败");
                                        // 状态码
                                        int code;
                                        // 提示信息
                                        String message;
                                        ResultEnum(int code, String message) {
                                            this.code = code;
                                            this.message = message;
                                        }
                                        public int code() {
                                            return code;
                                        }
                                        public String message() {
                                            return message;
                                        }
                                        public void setCode(int code) {
                                            this.code = code;
                                        }
                                        public void setMessage(String message) {
                                            this.message = message;
                                        }
                                    }
                                    
                                    1. 创建统一返回结果封装类Result:

                                    相关文章可以看这里:Spring Boot3统一结果封装

                                    @Data
                                    @NoArgsConstructor
                                    public class Result implements Serializable {
                                        // 操作代码
                                        Integer code;
                                        // 提示信息
                                        String message;
                                        // 结果数据
                                        T data;
                                        public Result(ResultEnum resultCode) {
                                            this.code = resultCode.code();
                                            this.message = resultCode.message();
                                        }
                                        public Result(ResultEnum resultCode, T data) {
                                            this.code = resultCode.code();
                                            this.message = resultCode.message();
                                            this.data = data;
                                        }
                                        public Result(String message) {
                                            this.message = message;
                                        }
                                        //成功返回封装-无数据
                                        public static Result success() {
                                            return new Result(ResultEnum.SUCCESS);
                                        }
                                        //成功返回封装-带数据
                                        public static  Result success(T data) {
                                            return new Result(ResultEnum.SUCCESS, data);
                                        }
                                        //失败返回封装-使用默认提示信息
                                        public static Result error() {
                                            return new Result(ResultEnum.FAIL);
                                        }
                                        //失败返回封装-使用返回结果枚举提示信息
                                        public static Result error(ResultEnum resultCode) {
                                            return new Result(resultCode);
                                        }
                                        //失败返回封装-使用自定义提示信息
                                        public static Result error(String message) {
                                            return new Result(message);
                                        }
                                    }
                                    

                                    4.2 添加依赖

                                    
                                        org.springframework.boot
                                        spring-boot-starter-cache
                                    
                                    
                                        org.springframework.boot
                                        spring-boot-starter-data-redis
                                    
                                    

                                    4.3 修改配置文件

                                    spring:
                                      data:
                                        redis:
                                          # Redis服务器地址
                                          host: ${shijun.redis.host}
                                          # Redis服务器端口
                                          port: ${shijun.redis.port}
                                          # Redis服务器认证密码
                                          password: ${shijun.redis.password}
                                          # Redis数据库索引
                                          database: ${shijun.redis.database}
                                    

                                    4.4 配置缓存管理器

                                    /**
                                     * 配置类,用于设置缓存管理器及相关配置,以启用缓存功能
                                     *
                                     * @author shijun
                                     * @date 2024/06/13
                                     */
                                    @EnableCaching
                                    @Configuration
                                    public class CacheConfig extends CachingConfigurerSupport {
                                        /**
                                         * 配置Redis键的序列化方式
                                         *
                                         * @return StringRedisSerializer,用于序列化和反序列化Redis中的键
                                         */
                                        private RedisSerializer keySerializer() {
                                            return new StringRedisSerializer();
                                        }
                                        /**
                                         * 配置Redis值的序列化方式
                                         *
                                         * @return GenericJackson2JsonRedisSerializer,使用Jackson库以JSON格式序列化和反序列化Redis中的值
                                         */
                                        private RedisSerializer valueSerializer() {
                                            return new GenericJackson2JsonRedisSerializer();
                                        }
                                        /**
                                         * 缓存前缀,用于区分不同的缓存命名空间,一般以模块名或者服务名命名,这里暂时写cache
                                         */
                                        public static final String CACHE_PREFIX = "cache:";
                                        /**
                                         * 配置缓存管理器,使用Redis作为缓存后端
                                         *
                                         * @param redisConnectionFactory Redis连接工厂,用于创建Redis连接
                                         * @return RedisCacheManager,Redis缓存管理器实例
                                         */
                                        @Bean
                                        public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
                                            // 配置序列化,解决乱码的问题,设置缓存名称的前缀和缓存条目的默认过期时间
                                            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                                                    // 设置键的序列化器
                                                    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
                                                    // 设置值的序列化器
                                                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
                                                    // 设置缓存名称的前缀
                                                    .computePrefixWith(name -> CACHE_PREFIX + name + ":")
                                                    // 设置缓存条目的默认过期时间为300秒
                                                    .entryTtl(Duration.ofSeconds(300));
                                            // 创建非锁定的Redis缓存写入器
                                            RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(Objects.requireNonNull(redisConnectionFactory));
                                            // 返回Redis缓存管理器实例,使用上述配置
                                            return new RedisCacheManager(redisCacheWriter, config);
                                        }
                                    }
                                     
                                    

                                    分析:

                                    StringRedisSerializer :使用 StringRedisSerializer 将缓存的键序列化为字符串。因为Redis中的键通常是字符串类型,使用字符串序列化器可以确保键在Redis中以可读的形式存储,便于调试和管理。

                                    GenericJackson2JsonRedisSerializer :使用 GenericJackson2JsonRedisSerializer 将缓存的值序列化为JSON格式,可读性高并且便于人工排查数据。

                                    4.5 使用Spring Cache注解

                                    由于我们的缓存的数据源来自于数据库,而数据库的数据是会发生变化的,因此,如果当数据库中数据发生变化,而缓存却没有同步,此时就会有数据一致性问题存在,在一些并发场景会出现问题。

                                    这里采用Cache Aside Pattern 即旁路缓存模式:缓存调用者在更新完数据库后再去更新缓存,也称之为双写方案。

                                    • 读流程:

                                      SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存

                                      分析:

                                      1. 应用程序首先从缓存中查找数据。
                                      2. 如果缓存命中,则直接返回缓存中的数据。
                                      3. 如果缓存未命中,则从数据库中读取数据,并将读取到的数据写入缓存,以便
                                      4. 后续请求可以直接从缓存中获取。
                                      • 写流程

                                        SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存

                                        分析:

                                        1. 应用程序首先更新数据库中的数据。
                                        2. 然后使缓存中的对应数据失效
                                    @Slf4j
                                    @RestController("/products")
                                    public class ProductController {
                                        /**
                                         * 根据ID获取产品信息
                                         * 通过@Cacheable注解,当请求的产品ID在缓存中存在时,直接从缓存中获取产品信息,减少数据库查询
                                         *
                                         * @param id 产品ID
                                         * @return 返回查询结果,包含指定ID的产品信息
                                         */
                                        @GetMapping("/getProductById")
                                        @Cacheable(value = "productsCache", key = "#id")
                                        public Result getProductById(Long id) {
                                            // 当从数据库获取数据时会打印,如果是从缓存中查询并不会执行到这里。
                                            log.info("从数据库获取产品: id = {}", id);
                                            Product product = new Product(id, "product", 100, "课本", 10);
                                            return Result.success(product);
                                        }
                                        /**
                                         * 更新产品信息
                                         * 通过@CacheEvict注解,当更新产品时,清除缓存中对应产品的数据,确保获取到最新的数据
                                         * 设置allEntries为true,表示清除整个缓存中的所有产品数据
                                         *
                                         * @param product 产品对象,包含更新后的详细信息
                                         * @return 返回更新结果,成功更新时返回成功标志
                                         */
                                        @PutMapping("/updateProduct")
                                        @CacheEvict(value = "productsCache", key = "#product.id")
                                        public Result updateProduct(@RequestBody Product product) {
                                            // 更新操作
                                            return Result.success();
                                        }
                                        /**
                                         * 删除指定ID的产品
                                         * 通过@CacheEvict注解,当删除产品时,清除缓存中对应产品的数据
                                         *
                                         * @param id 待删除产品的ID
                                         * @return 返回删除结果,成功删除时返回成功标志
                                         */
                                        @DeleteMapping("/deleteProductById")
                                        @CacheEvict(value = "productsCache", key = "#id")
                                        public Result deleteProductById(Long id) {
                                            // 删除操作
                                            return Result.success();
                                        }
                                    }
                                    

                                    4.6 测试

                                    4.6.1 查询测试

                                    因为我们在查询接口上使用的@Cacheable接口,所以当执行查询操作时,第一次查询会从数据库中获取,因此会输出「从数据库获取产品: id = x」,此时查看Redis控制台会发现出现一个对应的缓存,之后的每次查询都会从Redis中查询(控制台不会输出「从数据库获取产品: id = x」),直到对应的缓存数据时间结束。

                                    1. 发送查询请求:
                                    SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存
                                    1. 查看Redis中的缓存数据:

                                    通过观察可以发现CacheConfig类中的序列化配置起作用了,Redis中的数据不再是一堆乱码,并且在右上角还有我们之前配置的缓存的过期时间(我们之前配置的300s)。

                                    SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存
                                    1. 查看控制台发现本次查询为从数据库查询:
                                    SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存
                                    1. 再次发送会发现数据成功的查询了:
                                    SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存
                                    1. 再次查询控制台发现并没有输出从数据库获取产品: id = 1,说明本次查询为从Redis缓存中获取数据。
                                    SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存
                                    4.6.2 更新、删除测试

                                    因为之前我们在更新和删除接口上使用的@CacheEvict注解,所以当执行更新或者删除操作时,会将Redis中对应的产品缓存数据删除。

                                    1. 分别发送更新请求和删除请求,然后再次查看Redis中的缓存数据:
                                    SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存 SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存
                                    1. 可以发现Redis当中对应的缓存数据被删除了,符合我们的设计:
                                    SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存

                                    5. 总结

                                    在本文中,我们详细介绍了如何在Spring Boot项目中使用Spring Cache和Redis实现数据缓存,并简单讲解了使用Cache Aside Pattern来解决数据一致性问题,希望对大家学习有所帮助。如有问题,大家可以私信或者在评论区询问😊。

                                    转载请注明来自码农世界,本文标题:《SpringBoot系列——使用Spring Cache和Redis实现查询数据缓存》

                                    百度分享代码,如果开启HTTPS请参考李洋个人博客
                                    每一天,每一秒,你所做的决定都会改变你的人生!

                                    发表评论

                                    快捷回复:

                                    评论列表 (暂无评论,82人围观)参与讨论

                                    还没有评论,来说两句吧...

                                    Top