1. 程式人生 > >SpringBoot25-spingboot資料訪問-資料快取Cache

SpringBoot25-spingboot資料訪問-資料快取Cache

    我們知道一個程式的瓶頸在於資料庫,我們也知道記憶體的速度是大大快於硬碟的速度的。當我們需要重複地獲取相同的資料的時候,我們一次又一次的請求資料庫或者遠端服務,導致大量的時間耗費在資料庫查詢或者遠端方法呼叫上,導致程式效能的惡化,這便是資料快取要解決的問題。

一,Spring 快取支援

     Spring定義了org.springframework.cache.CacheManager和org.springframework.cache.Cache介面用來統一不同的快取技術。其中CacheManager是Spring提供的各種快取技術抽象介面,Cache介面包含快取的各種操作(增加,刪除,獲得快取,我們一般不會直接和此介面打交道)

1,Spring支援的CacheManager

      針對不同的快取技術,需要實現不同的CacheManager,Spring定義瞭如下所示的CacheManager實現:

SimpleCacheManager:使用簡單的Collection來儲存快取,主要用來測試用途。

ConcurrentMapCacheManager:使用ConcurrentMap來儲存快取。

NoOpCacheManager:僅測試用途,不會實際儲存快取。

EhCacheCacheManager:使用EhCache作為快取技術。

GuavaCacheManager:使用Google Guava的GuavaCache作為快取技術。

HazelcastCacheManager:使用Hazelcast作為快取技術。

JCacheCacheManager:支援JCache標準的實現作為快取技術,如Apache Commons JCS

RedisCacheManager:使用Redis作為快取技術。

     在我們使用任意一個實現的CacheManager的時候,需要註冊實現CacheManager的Bean,例如:

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

     當然,每種快取技術都有很多的額外配置,但配置cacheManager是必不可少的

2,聲名式快取註解

      Spring提供了4個註解來宣告快取規則(又是使用註解式的AOP的一個生動例子)。這四個註解如下:

@Cacheable:在方法執行前spring先檢視快取中是否有資料,如果有資料,則直接返回快取資料;若沒有資料,呼叫方法並將方法返回值放進快取。

@CachePut:無論怎樣,都會將方法的返回值放到快取中。@CachePut的屬性與@Cacheable保持一致。

@CacheEvict:將一條或多條資料從快取中刪除。

@Caching:可以通過@Caching註解組合多個註解策略在一個方法上。

     @Cacheable,@Cacheable,@CacheEvict都有value屬性,指定的是要使用的快取名稱;key屬性指定的是資料在快取中的儲存的鍵。

3,開啟聲名式快取支援

     開啟聲名式快取支援十分簡單,只需在配置類上使用@EnableCaching註解即可,如下:

package com.jack.springboot8cache.config;

import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

/**
 * create by jack 2017/10/9
 */
@Configuration
@EnableCaching
public class AppConfig {
}


二,Spring Boot的支援

      在spring中使用快取技術的關鍵是配置CacheManager,而Spring Boot為我們自動配置了多個CacheManager的實現。

      Spring Boot的CacheManager的自動配置放置在org.springframework.boot.autoconfigure.cache包中,如下所示:


       通過上圖我們可以看出,Spring Boot為我們自動配置了EhCacheCacheConfiguration(使用EhCache),GenericCacheConfiguration(使用Collection),GuavaCacheConfiguration(使用Guava),HazelcastCacheConfiguration(使用Hazelcast),InfinispanCacheConfiguration(使用Infinispan),JCacheCacheConfiguration(使用JCache),NoOpCacheConfiguration(不使用儲存),RedisCacheConfiguration(使用Redis),SimpleCacheConfiguration(使用ConcurrentMap)。在不做任何額外配置的情況下,預設使用的是SimpleCacheConfiguration,即使用ConcurrentMapCacheManager。Spring boot支援以“spring.cache”為字首的屬性來配置快取:

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實現

spring.cache.guava.spec=#guava specs

       在Spring Boot環境下,使用快取技術只需在專案中匯入相關快取技術的依賴包,並在配置類上使用@EnableCaching開啟快取即可。

三,實戰

      下面將以Spring Boot預設的ConcurrentMapCacheManager作為快取技術,演示@Cacheable,@CachePut,@CacheEvit,最後使用EhCache,Guava來替換快取技術。

1,新建Spring Boot專案

      新建Spring Boot專案依賴為Cache,JPA,WEB,新增mysql驅動,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>

	<groupId>com.jack</groupId>
	<artifactId>springboot8cache</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>

	<name>springboot8cache</name>
	<description>Demo project for Spring Boot</description>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>1.5.7.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<!--mysql連線驅動-->
		<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>

		<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.39</version>
		</dependency>

		<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</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>

2,實體類
package com.jack.springboot8cache.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/**
 * create by jack 2017/10/3
 */
//@Entity註解指明這是一個和資料庫表對映的實體類
@Entity
public class Person {
    /**
     * 主鍵id
     * @Id註解指明這個屬性對映為資料庫的主鍵
     * @GeneratedValue定義主鍵生成的方式,下面採用的是mysql的自增屬性
     */
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Integer id;
    /**
     * 姓名
     */
    private String name;
    /**
     * 年齡
     */
    private Integer age;
    /**
     * 地址
     */
    private String address;

    public Person() {
        super();
    }

    public Person(Integer id, String name, Integer age, String address) {
        super();
        this.id = id;
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

3,實體類Repository
package com.jack.springboot8cache.dao;


import com.jack.springboot8cache.entity.Person;
import org.springframework.data.jpa.repository.JpaRepository;

/**
 * create by jack 2017/10/8
 * 實體類
 */
public interface PersonRepository extends JpaRepository<Person,Integer> {
}

4,業務服務

1)介面:

package com.jack.springboot8cache.service;

import com.jack.springboot8cache.entity.Person;

/**
 * create by jack 2017/10/9
 */
public interface DemoService {
    Person save(Person person);

    void remove(Integer id);

    Person findOne(Person person);
}

2)實現類
package com.jack.springboot8cache.impl;


import com.jack.springboot8cache.dao.PersonRepository;
import com.jack.springboot8cache.entity.Person;
import com.jack.springboot8cache.service.DemoService;
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;

/**
 * create by jack 2017/10/9
 */
@Service
public class DemoServiceImpl implements DemoService {
    @Autowired
    private PersonRepository personRepository;
    /**
     * 注意:如果沒有指定key,則方法引數作為key儲存到快取中
     */
    /**
     *@CachePut快取新增的或更新的資料到快取,其中快取的名稱為people,資料的key是person的id
     * @param person
     * @return
     */
    @CachePut(value = "people",key="person.id")
    @Override
    public Person save(Person person) {
        Person p = personRepository.save(person);
        System.out.println("為id,key為:"+p.getId()+"資料做了快取");
        return p;
    }

    /**
     * @CacheEvict從快取people中刪除key為id的資料
     * @param id
     */
    @CacheEvict(value = "people")
    @Override
    public void remove(Integer id) {
        System.out.println("刪除了id,key為"+id+"的資料快取");
        personRepository.delete(id);
    }

    /**
     * @Cacheable快取key為person的id資料到快取people中
     * @param person
     * @return
     */
    @Cacheable(value = "people",key = "person.id")
    @Override
    public Person findOne(Person person) {
        Person p = personRepository.findOne(person.getId());
        System.out.println("為id,key為:"+p.getId()+"資料做了快取");
        return p;
    }
}

    注意:如果沒有指定key,則方法引數作為key儲存到快取中

5,控制器

package com.jack.springboot8cache.controller;

import com.jack.springboot8cache.entity.Person;
import com.jack.springboot8cache.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * create by jack 2017/10/9
 */
@RestController
@RequestMapping("cache")
public class CacheController {
    @Autowired
    private DemoService demoService;

    @RequestMapping("/put")
    public Person put(Person person){
        return demoService.save(person);
    }

    @RequestMapping("/evit")
    public String evit(Integer id){
        demoService.remove(id);
        return "ok";
    }

    @RequestMapping("/cacheable")
    public Person cacheable(Person person){
        return demoService.findOne(person);
    }

}

6,開啟快取支援
package com.jack.springboot8cache;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class Springboot8cacheApplication {

	public static void main(String[] args) {
		SpringApplication.run(Springboot8cacheApplication.class, args);
	}
}

7,執行

     當我們對資料庫做了快取以後,資料的獲得將從快取中得到,而不是從資料庫中得到。當前的資料庫的資料情況如下所示:


    我們在每次執行測試情況下,都重啟了應用程式。

1)測試@Cacheable

      第一次訪問http://localhost:9090/cache/cacheable?id=85,第一次將呼叫方法查詢資料庫,並將資料放到快取people中。

此時控制檯輸出如下:

2017-10-09 23:29:05.877  INFO 2468 --- [nio-9090-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2017-10-09 23:29:05.878  INFO 2468 --- [nio-9090-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2017-10-09 23:29:05.939  INFO 2468 --- [nio-9090-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 61 ms
Hibernate: select person0_.id as id1_0_0_, person0_.address as address2_0_0_, person0_.age as age3_0_0_, person0_.name as name4_0_0_ from person person0_ where person0_.id=?
為id,key為:85資料做了快取

頁面輸出如下:


      再次訪問http://localhost:9090/cache/cacheable?id=85,此時控制檯沒有再次輸出hibernate的查詢語句,以及“為id,key為:85資料做了快取”字樣,表示沒有呼叫這個方法,頁面直接從資料快取中獲得資料,頁面輸出結果如下:


2)測試@CachePut

      訪問http://localhost:9090/cache/put?name=jack8&age=99&address=深圳深圳,此時控制檯輸出如下:

2017-10-09 23:37:10.823  INFO 1212 --- [nio-9090-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2017-10-09 23:37:10.824  INFO 1212 --- [nio-9090-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2017-10-09 23:37:10.893  INFO 1212 --- [nio-9090-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 69 ms
Hibernate: insert into person (address, age, name) values (?, ?, ?)
為id,key為:86資料做了快取

頁面輸出如下:


     我們再次訪問:http://localhost:9090/cache/cacheable?id=87,控制檯無輸出,從快取直接獲得資料,頁面顯示如上。

3)測試@CacheEvit

       訪問:http://localhost:9090/cache/cacheable?id=87,為id為87的資料做快取,再次訪問http://localhost:9090/cache/cacheable?id=87,確認資料已經從快取中獲取。

       訪問:http://localhost:9090/cache/evit?id=87,從快取中刪除key為87的快取資料:

刪除了id,key為87的資料快取
Hibernate: select person0_.id as id1_0_0_, person0_.address as address2_0_0_, person0_.age as age3_0_0_, person0_.name as name4_0_0_ from person person0_ where person0_.id=?
Hibernate: delete from person where id=?

     再次訪問:http://localhost:9090/cache/cacheable?id=87,觀察控制檯,資料已經從資料庫中刪除了,查詢快取,發現空指標異常了,查詢失敗

四,切換快取技術

      切換快取技術除了移入相關依賴包或者配置以外,使用方式和上面的實戰例子儲存一致。下面講解Spring Boot下EhCache和Guava作為快取技術的方式,其餘快取技術也是類似的方式。

1,EhCahce

      當我們需要使用EhCache作為快取技術的時候,我們只需要在pom.xml中新增EhCache的依賴即可:

<!-- https://mvnrepository.com/artifact/net.sf.ehcache/ehcache -->
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.4</version>
</dependency>

     EhCache所需的配置檔案ehcache.xml只需放在類路徑下,Spring Boot會自動掃描,例如:

<?xml version="1.0" encoding="UTF-8">

<ehcache>

    <cache name="people" maxElementsInMemory="1000"/>

</ehcache>

Spring Boot會給我們自動配置EhcacheCacheManager的Bean。

2,Guava

     當我們需要使用Guava作為快取技術的時候,我們只需要在pom.xml中增加Guava的依賴即可:

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>23.0</version>
</dependency>

Spring Boot會給我們自動配置GuavaCacheManager這個Bean。

3,Redis

       使用Redis,只需新增下面的依賴即可:

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>1.5.7.RELEASE</version>
</dependency>

Spring Boot將會為我們自動配置RedisCacheManager以及RedisTemplate的Bean。