SpringBoot 實戰 (十一) | 整合數據緩存 Cache

微信公衆號:一個優秀的廢人
如有問題或建議,請後臺留言,我會盡力解決你的問題。

前言

如題,今天介紹 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);
    }
}

測試

測試前,先看一眼數據庫當前的數據,如下:

原始數據庫數據

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 第一次測試結果 :

postman 第一次測試結果

再次訪問 http://localhost:8080/student/able/2 結果如下圖。但控制檯無 SQL 語句打印,也無爲id、key 爲2的數據做了緩存這句話輸出。

說明 @Cacheable 確實做了數據緩存,第二次的測試結果是從數據緩存中獲取的,並沒有直接查數據庫。

postman 第二次測試結果

2、測試 @CachePut

如下圖,postman 訪問 http://localhost:8080/student/put 插入數據:
postman 測試 @CachePut

下面是控制檯打印出了 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的數據做了緩存

插入數據返回的結果:

postman 第一次測試結果

數據庫中的結果:

新增的數據

訪問 http://localhost:8080/student/able/4 Postman 結果如下圖。控制檯無輸出,驗證了 @CachePut 確實做了緩存,下圖數據是從緩存中獲取的。

@Cacheable 測試結果

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

postman 訪問 http://localhost:8080/student/evit/3

從緩存中刪除 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 數據緩存的教程。

如果本文對你哪怕有一丁點幫助,請幫忙點好看。

你的好看是我堅持寫作的動力。

另外,關注之後在發送 1024 可領取免費學習資料。資料內容詳情請看這篇舊文:Python、C++、Java、Linux、Go、前端、算法資料分享

一個優秀的廢人

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章