SpringBoot 实战 (十一) | 整合数据缓存 Cache

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
简介: 如题,今天介绍 SpringBoot 的数据缓存。做过开发的都知道程序的瓶颈在于数据库,我们也知道内存的速度是大大快于硬盘的,当需要重复获取相同数据时,一次又一次的请求数据库或者远程服务,导致大量时间耗费在数据库查询或远程方法调用上,导致性能的恶化,这便是数据缓存要解决的问题。

Spring 的缓存支持


Spring 定义了org.springframework.cache.CacheManager 和  org.springframework.cache.Cache 接口用于统一不同的缓存技术。其中,CacheManager 是 Spring 提供的各种缓存技术的抽象接口,Cache 接口则是包含了缓存的各种操作(增加,删除,获取缓存,一般不会直接和此接口打交道)。


1、Spring 支持的 CacheManager


针对不同的缓存技术,实现了不同的 CacheManager ,Spring 定义了下表所示的 CacheManager:

CacheManager 描述
SimpleCacheManager 使用简单的 Collection 来存储缓存,主要用于测试
ConcurrentMapCacheManager 使用 ConcurrentMap 来存储缓存
NoOpCacheManager 仅测试用途,不会实际缓存数据
EhCacheCacheManager 使用 EhCache 作为缓存技术
GuavaCacheManager 使用 Google Guava 的 GuavaCache 作为缓存技术
HazelcastCacheManager 使用 Hazelcast 作为缓存技术
JCacheCacheManager 支持 JCache(JSR-107) 标准的实现作为缓存技术,如 ApacheCommonsJCS
RedisCacheManager 使用 Redis 作为缓存技术


在使用以上任意一个实现的 CacheManager 的时候,需注册实现的 CacheManager 的 Bean,如:


@Bean
public EhCacheCacheManager cacheManager(CacheManager 
 ehCacheCacheManager){
    return new  EhCacheCacheManager(ehCacheCacheManager);
}


注意,每种缓存技术都有很多的额外配置,但配置 cacheManager 是必不可少的。


2、声明式缓存注解


Spring 提供了 4 个注解来声明缓存规则(又是使用注解式的 AOP 的一个例子)。4 个注解如下表示:


注解 解释
@Cacheable 在方法执行前 Spring 先查看缓存中是否有数据,若有,则直接返回缓存数据;若无数据,调用方法将方法返回值放入缓存中
@CachePut 无论怎样,都会将方法的返回值放到缓存中。
@CacheEvict 将一条或多条数据从缓存中删除
@Caching 可以通过 @Caching 注解组合多个注解策略在一个方法上


@Cacheable、@CachePut、@CacheEvict 都有 value 属性,指定的是要使用的缓存名称;key 属性指定的是数据在缓存中存储的键。


3、开启声明式缓存支持


开启声明式缓存很简单,只需在配置类上使用 @EnableCaching 注解即可,例如:


@Configuration
@EnableCaching
public class AppConfig{
}


SpringBoot 的支持


在 Spring 中使用缓存技术的关键是配置 CacheManager ,而 SpringBoot 为我们配置了多个 CacheManager 的实现。


它的自动配置放在 org.springframework.boot.autoconfigure.cache 包中。


在不做任何配置的情况下,默认使用的是 SimpleCacheConfiguration  ,即使用 ConcurrentMapCacheManager。SpringBoot 支持以前缀来配置缓存。例如:


spring.cache.type= # 可选 generic、ehcache、hazelcast、infinispan、jcache、redis、guava、simple、none
spring.cache.cache-names= # 程序启动时创建的缓存名称
spring.cache.ehcache.config= # ehcache 配置文件的地址
spring.cache.hazelcast.config= # hazelcast配置文件的地址
spring.cache.infinispan.config= # infinispan配置文件的地址
spring.cache.jcache.config= # jcache配置文件的地址
spring.cache.jcache.provider= # 当多个 jcache 实现在类路径的时候,指定 jcache 实现
# 等等。。。


在 SpringBoot 环境下,使用缓存技术只需要在项目中导入相关缓存技术的依赖包,并在配置类中使用 @EnableCaching 开启缓存支持即可。


代码实现


本文将以 SpringBoot 默认的 ConcurrentMapCacheManager 作为缓存技术,演示 @Cacheable、@CachePut、@CacheEvict。


1、准备工作


   IDEA

   JDK 1.8

   SpringBoot 2.1.3


2、Pom.xml 文件依赖


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.nasus</groupId>
    <artifactId>cache</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>cache</name>
    <description>cache Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- cache 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <!-- JPA 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- web 启动类 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mysql 数据库连接类 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- lombok 依赖,简化实体 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 单元测试类 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>


注释很清楚,无需多言。不会就谷歌一下。


3、Application.yaml 文件配置

spring:
  # 数据库相关
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true
    username: root
    password: 123456
  # jpa 相关
  jpa:
    hibernate:
      ddl-auto: update   # ddl-auto: 设为 create 表示每次都重新建表
    show-sql: true


4、实体类


package com.nasus.cache.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class Student {
    @Id
    @GeneratedValue
    private Integer id;
    private String name;
    private Integer age;
}


5、dao 层


package com.nasus.cache.repository;
import com.nasus.cache.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface StudentRepository extends JpaRepository<Student,Integer> {
}


6、service 层


package com.nasus.cache.service;
import com.nasus.cache.entity.Student;
public interface StudentService {
    public Student saveStudent(Student student);
    public void deleteStudentById(Integer id);
    public Student findStudentById(Integer id);
}


实现类:


package com.nasus.cache.service.impl;
import com.nasus.cache.entity.Student;
import com.nasus.cache.repository.StudentRepository;
import com.nasus.cache.service.StudentService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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 StudentServiceImpl implements StudentService {
    // 使用 slf4j 作为日志框架
    private static final Logger LOGGER = LoggerFactory.getLogger(StudentServiceImpl.class);
    @Autowired
    private StudentRepository studentRepository;
    @Override
    @CachePut(value = "student",key = "#student.id")
    // @CachePut 缓存新增的或更新的数据到缓存,其中缓存名称为 student 数据的 key 是 student 的 id
    public Student saveStudent(Student student) {
        Student s = studentRepository.save(student);
        LOGGER.info("为id、key 为{}的数据做了缓存", s.getId());
        return s;
    }
    @Override
    @CacheEvict(value = "student")
    // @CacheEvict 从缓存 student 中删除 key 为 id 的数据
    public void deleteStudentById(Integer id) {
        LOGGER.info("删除了id、key 为{}的数据缓存", id);
        //studentRepository.deleteById(id);
    }
    @Override
    @Cacheable(value = "student",key = "#id")
    // @Cacheable 缓存 key 为 id 的数据到缓存 student 中
    public Student findStudentById(Integer id) {
        Student s = studentRepository.findById(id).get();
        LOGGER.info("为id、key 为{}的数据做了缓存", id);
        return  s;
    }
}


代码讲解看注释,很详细。


7、controller 层

package com.nasus.cache.controller;
import com.nasus.cache.entity.Student;
import com.nasus.cache.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.Mapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/student")
public class StudentController {
    @Autowired
    private StudentService studentService;
    @PostMapping("/put")
    public Student saveStudent(@RequestBody Student student){
        return studentService.saveStudent(student);
    }
    @DeleteMapping("/evit/{id}")
    public void deleteStudentById(@PathVariable("id") Integer id){
        studentService.deleteStudentById(id);
    }
    @GetMapping("/able/{id}")
    public Student findStudentById(@PathVariable("id") Integer id){
        return studentService.findStudentById(id);
    }
}


8、application 开启缓存功能


package com.nasus.cache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@EnableCaching // 开启缓存功能
@SpringBootApplication
public class CacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheApplication.class, args);
    }
}


测试


测试前,先看一眼数据库当前的数据,如下:


640.png


1、测试 @Cacheable


访问 http://localhost:8080/student/able/2 控制台打印出了 SQL 查询语句,以及指定日志。说明这一次程序是直接查询数据库得到的结果。


2019-02-21 22:54:54.651  INFO 1564 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 11 ms
Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?
2019-02-21 22:54:59.725  INFO 1564 --- [nio-8080-exec-1] c.n.c.service.impl.StudentServiceImpl    : 为id、key 为2的数据做了缓存


postman 第一次测试结果 :


640.png


再次访问 http://localhost:8080/student/able/2 结果如下图。但控制台无 SQL 语句打印,也无为id、key 为2的数据做了缓存这句话输出。


说明 @Cacheable 确实做了数据缓存,第二次的测试结果是从数据缓存中获取的,并没有直接查数据库。


640.png


2、测试 @CachePut


如下图,postman 访问 http://localhost:8080/student/put 插入数据:


640.png


下面是控制台打印出了 SQL Insert 插入语句,以及指定日志。说明程序做了缓存。


Hibernate: insert into student (age, name, id) values (?, ?, ?)
2019-02-21 23:12:03.688  INFO 1564 --- [nio-8080-exec-8] c.n.c.service.impl.StudentServiceImpl    : 为id、key 为4的数据做了缓存


插入数据返回的结果:


640.png


数据库中的结果:


640.png


访问 http://localhost:8080/student/able/4 Postman 结果如下图。控制台无输出,验证了 @CachePut 确实做了缓存,下图数据是从缓存中获取的。


640.png


3、测试 @CacheEvict


postman 访问 http://localhost:8080/student/able/3 为 id = 3 的数据做缓存。


postman 再次访问 http://localhost:8080/student/able/3 确认数据是从缓存中获取的。


postman 访问 http://localhost:8080/student/evit/3


640.png


从缓存中删除 key 为 3 的缓存数据:


Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?
2019-02-21 23:26:08.516  INFO 8612 --- [nio-8080-exec-2] c.n.c.service.impl.StudentServiceImpl    : 为id、key 为3的数据做了缓存
2019-02-21 23:27:01.508  INFO 8612 --- [nio-8080-exec-4] c.n.c.service.impl.StudentServiceImpl    : 删除了id、key 为3的数据缓存


再次 postman 访问 http://localhost:8080/student/able/3 观察后台,重新做了数据缓存:


Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?
2019-02-21 23:27:12.320  INFO 8612 --- [nio-8080-exec-5] c.n.c.service.impl.StudentServiceImpl    : 为id、key 为3的数据做了缓存


这一套测试流程下来,证明了 @CacheEvict  确实删除了数据缓存。


源码下载


https://github.com/turoDog/Demo/tree/master/springboot_cache_demo


切换缓存技术


切换缓存技术除了在 pom 文件加入相关依赖包配置以外,使用方式与上面的代码演示一样。


1、切换 EhCache


在 pom 中添加 Encache 依赖:


<!-- EhCache 依赖 -->
<dependency>
     <groupId>net.sf.ehcache</groupId>
     <artifactId>ehcache</artifactId>
</dependency>


Ehcache 所需配置文件 ehcache.xml 只需放在类路径(resource 目录)下,SpringBoot 会自动扫描,如:


<?xml version="1.0" encoding="UTF-8">
<ehcache>
    <cache name="student" maxElementsInMmory="1000">
<ehcache>


SpringBoot 会自动配置 EhcacheManager 的 Bean。


2、切换 Guava


只需在 pom 中加入 Guava 依赖即可:


<!-- GuavaCache 依赖 -->
<dependency>
     <groupId>com.google.guava</groupId>
     <artifactId>guava</artifactId>
     <version>18.0</version>
</dependency>


SpringBoot 会自动配置 GuavaCacheManager 的 Bean。


3、切换 RedisCache


与 Guava 一样,只需在 pom 加入依赖即可:


<!-- cache 依赖 -->
<dependency>         
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-redis</artifactId>
</dependency>


SpringBoot 会自动配置 RedisCacheManager 以及 RedisTemplate 的 Bean。


此外,切换其他缓存技术的方式也是类似。这里不做赘述。


后语


以上为 SpringBoot 数据缓存的教程。


如果本文对你哪怕有一丁点帮助,请帮忙点好看。


你的好看是我坚持写作的动力。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
229 2
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
40 5
|
1月前
|
SQL 前端开发 关系型数据库
SpringBoot使用mysql查询昨天、今天、过去一周、过去半年、过去一年数据
SpringBoot使用mysql查询昨天、今天、过去一周、过去半年、过去一年数据
60 9
|
2月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
76 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
1月前
|
存储 easyexcel Java
SpringBoot+EasyExcel轻松实现300万数据快速导出!
本文介绍了在项目开发中使用Apache POI进行数据导入导出的常见问题及解决方案。首先比较了HSSFWorkbook、XSSFWorkbook和SXSSFWorkbook三种传统POI版本的优缺点,然后根据数据量大小推荐了合适的使用场景。接着重点介绍了如何使用EasyExcel处理超百万数据的导入导出,包括分批查询、分批写入Excel、分批插入数据库等技术细节。通过测试,300万数据的导出用时约2分15秒,导入用时约91秒,展示了高效的数据处理能力。最后总结了公司现有做法的不足,并提出了改进方向。
|
1月前
|
存储 缓存 算法
分布式缓存有哪些常用的数据分片算法?
【10月更文挑战第25天】在实际应用中,需要根据具体的业务需求、数据特征以及系统的可扩展性要求等因素综合考虑,选择合适的数据分片算法,以实现分布式缓存的高效运行和数据的合理分布。
|
2月前
|
自然语言处理 Java API
Spring Boot 接入大模型实战:通义千问赋能智能应用快速构建
【10月更文挑战第23天】在人工智能(AI)技术飞速发展的今天,大模型如通义千问(阿里云推出的生成式对话引擎)等已成为推动智能应用创新的重要力量。然而,对于许多开发者而言,如何高效、便捷地接入这些大模型并构建出功能丰富的智能应用仍是一个挑战。
232 6
|
2月前
|
easyexcel Java UED
SpringBoot中大量数据导出方案:使用EasyExcel并行导出多个excel文件并压缩zip后下载
在SpringBoot环境中,为了优化大量数据的Excel导出体验,可采用异步方式处理。具体做法是将数据拆分后利用`CompletableFuture`与`ThreadPoolTaskExecutor`并行导出,并使用EasyExcel生成多个Excel文件,最终将其压缩成ZIP文件供下载。此方案提升了导出效率,改善了用户体验。代码示例展示了如何实现这一过程,包括多线程处理、模板导出及资源清理等关键步骤。
|
2月前
|
缓存 监控 前端开发
处理页面缓存中数据不一致的问题
【10月更文挑战第9天】
55 2
|
2月前
|
Web App开发 JavaScript Java
elasticsearch学习五:springboot整合 rest 操作elasticsearch的 实际案例操作,编写搜索的前后端,爬取京东数据到elasticsearch中。
这篇文章是关于如何使用Spring Boot整合Elasticsearch,并通过REST客户端操作Elasticsearch,实现一个简单的搜索前后端,以及如何爬取京东数据到Elasticsearch的案例教程。
223 0
elasticsearch学习五:springboot整合 rest 操作elasticsearch的 实际案例操作,编写搜索的前后端,爬取京东数据到elasticsearch中。