1. 程式人生 > >Spring Boot學習(十):Spring Boot 與快取

Spring Boot學習(十):Spring Boot 與快取

快取,我們應該已經很熟悉了。那麼今天就來學習一下Spring Boot中怎麼使用快取。

1、說起快取,先來了解下JSR107

首先什麼是JSR?

        JSR是Java Specification Requests 的縮寫 ,Java規範請求,故名思議提交Java規範,大家一同遵守這個規範的話,會讓大家‘溝通’起來更加輕鬆。

什麼是JSR107?

        JSR107就是如何使用快取的規範。

JSR107都有哪些內容?

        核心API:

  • CachingProvider:定義了建立,配置,得到,管理和控制0個或多個CacheManager,一個應用在執行時可能訪問0個或者多個CachingProvider。
  • CacheManager:它定義了建立,配置,得到,管理和控制0個或多個有著唯一名字的Cache ,一個CacheManager被包含在單一的CachingProvider。

  •  Cache:Cache是一個Map型別的資料結構,用來儲存基於鍵的資料,很多方面都像java.util.Map資料型別。一個Cache 存在在單一的CacheManager。

  •  Entry:Entry是一個存在在Cache的鍵值對。

  • ExpiryPolicy:不是所有的資料都一直存在快取中不改變的,為快取的資料新增過期的策略會讓你的快取更加靈活和高效。

     相應的關係可以參考下圖:

2、Spring Boot的快取機制

2.1、建立專案

參考上一篇文章,Spring Boot整合mybatis,快速建立一個Spring Boot專案(除web, mysql, mybatis之外多新增一個cache模組),如下:

建立好之後,看一下pom檔案,會看到引入了cache模組:

專案結構:

2.2、完善專案結構並測試

這個專案我們不使用mapper配置檔案,我們基於mapper註解的方式進行訪問。

建立表user,並插入兩條資料:

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `age` int(4) NOT NULL,
  `name` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci

實體類User:

package com.example.cache.domain;

import org.springframework.stereotype.Component;

import java.io.Serializable;

/**
 * @author pavel
 * @date 2018/11/19 0019
 */
@Component
public class User implements Serializable {

    private static final long serialVersionUID = -1274433079373420955L;

    private Long id;
    private Integer age;
    private String name;

    public User() {

    }

    public User(Long id, Integer age, String name) {
        this.id = id;
        this.age = age;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

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

    public Integer getAge() {
        return age;
    }

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

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

mapper介面:

package com.example.cache.mapper;

import com.example.cache.domain.User;
import org.apache.ibatis.annotations.*;

/**
 * 基於註解的mapper配置
 * @author pavel
 * @date 2018/11/19 0019
 */
@Mapper
public interface UserMapper {

    @Select("select * from user where id = #{id}")
    User getUser(Long id);

    @Update("update user set name = #{name},age = #{age} where id = #{id}")
    void updateUser(User user);

    @Delete("delete from user where id = #{id}")
    void deleteUser(Long id);

    @Insert("insert into user(age,name) values(#{age},#{name}) ")
    void insertUser(User user);


}

service:

package com.example.cache.service;

import com.example.cache.domain.User;
import com.example.cache.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**
 * @author pavel
 * @date 2018/11/22 0022
 */
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    
    public User getUser(Long id) {
        System.out.println("查詢" + id + "號員工");
        return userMapper.getUser(id);
    }
}

啟動類上新增mapper的包掃描:

package com.example.cache;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.example.cache.mapper")
public class SpringBootCacheApplication {

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

主配置檔案中的資料庫連線配置:

### database ###
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_test?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=yjx941001
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

### 控制檯列印sql ###
logging.level.com.example.cache.mapper = debug

controller:

package com.example.cache.controller;

import com.example.cache.domain.User;
import com.example.cache.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author pavel
 * @date 2018/11/19 0019
 */
@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable("id") Long id) {
        User user = userService.getUser(id);
        System.out.println("查詢結果: " + user);
        return user;
    }
}

控制檯:

可以發現"查詢1號員工"字樣會列印兩次,說明第二次訪問再次呼叫了查詢方法,訪問資料庫,此時沒有任何快取機制。

2.3、加入快取機制

修改啟動類,新增@EnableCaching註解,開啟快取機制:

package com.example.cache;

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

@SpringBootApplication
@EnableCaching
@MapperScan("com.example.cache.mapper")
public class SpringBootCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootCacheApplication.class, args);
    }
}

修改service,給getUser方法加上@Cacheable註解(下面再詳細介紹這個註解的作用),如下:

package com.example.cache.service;

import com.example.cache.domain.User;
import com.example.cache.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**
 * @author pavel
 * @date 2018/11/22 0022
 */
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Cacheable(cacheNames = "user")
    public User getUser(Long id) {
        System.out.println("查詢" + id + "號員工");
        return userMapper.getUser(id);
    }
}

再次請求:

看控制檯輸出,第一次請求,打印出“查詢1號員工”字樣、查詢sql以及查詢結果,但第二次請求,只打印了查詢結果,並沒有“查詢1號員工”字樣以及查詢sql,說明我們配置的快取是生效的,第二次請求是直接從快取中獲取user物件。

3、工作原理及執行流程
    3.1、工作原理

       快取我們引入了cache模組, 還是之前講到過的Spring Boot自動配置原理,有一系類的xxxAutoConfiguration配置類,那麼肯定會有cache相關的自動配置,即:CacheAutoConfiguration,所以我們就進去看一下。

        看到會給容器匯入一個CacheConfigurationImportSelector選擇器,debug看一下:

這裡就是匯入的所有的快取配置。但不是都生效,又興趣的話可以每一個點進去看一下,看在什麼情況下那個配置才會生效。

可以在配置檔案中配置: debug=true

然後看到控制檯輸出中,預設是 SimpleCacheConfiguration會生效。所以我們就之間去看這個快取配置類:

如上圖,可以看出,這個配置類,就是給容器註冊了一個CacheManager:ConcurrentMapCacheManager

我們再看下這個ConcurrentMapCacheManager:

實現了CacheManager介面,那麼就有了關於Cache相關的操作方法,比如getCache()

所以說,ConcurrentMapCacheManager的作用是建立和獲取ConcurrentMapCache型別的快取元件。

我們繼續看,怎麼建立的Cache,如下,直接new ConcurrentMapCache();

再往下走,進到ConcurrentMapCache類,這個就是快取元件類,

類中有兩個方法,put(Object key, @Nullable Object value) 以及lookup(Object key)方法,如下:

可見,key的值就是1,

lookup()方法返回值為null,說明快取中沒有這個物件,再往下走,就進入到了getUser()方法進行查詢,然後再進到put()方法,將查詢結果以key-value的方式存到快取中,key是請求引數1,而value就是查詢結果-User物件:

進入到lookup()方法,可以看到,this.store中有值,key是1,value是一個User物件,正是上一步訪問後存入到快取中的User物件,所以,再次訪問就能夠直接從快取中獲取到值了。

快取的工作原理就是這個樣子了,可以自己debug一步一步的看。

3.2、執行流程

@Cacheable的執行流程:
        (1) 該註解時作用於方法之上的,在方法執行之前,會先去查詢Cache(快取元件),按照cacheNames/value指定的名字獲取,(cacheManager先獲取相應的快取),第一次獲取快取如果沒有Cache元件會自動建立。

        (2) 去Cache中查詢快取的內容,使用一個key,key預設值就是方法的引數。

                key是按照某種策略生成的;預設是使用keyGenerator介面的實現類SimpleKeyGenerator生成;

                SimpleKeyGenerator的生成key的預設策略(debug一步一步可以看到的):

                                       如果沒有引數: key = new SimpleKey()

                                       如果有一個引數:key = 引數的值

                                       如果有多個引數:key = new SimpleKey(params)
        (3) 沒有查到快取就呼叫目標方法 (也就是上面例子中的getUser()方法)
        (4) 將目標方法返回的結果放到快取中

4、@Cacheable註解詳解


註解@Cacheable的相關屬性:
               cacheNames/value: 指定快取元件的名字;將結果放到哪個快取元件中,可以用陣列的形式指定多個快取元件
               key:快取資料使用的key,  預設使用的是方法引數的值 id-方法返回值
               keyGenerator: key的生成器,可以自己指定key的生成器的元件id 

                                 key/keyGenerator:二選一使用
               cacheManager:指定快取管理器,  或者指定cacheResovler指定獲取解析器
               condition: 指定符合條件的情況下才快取  
               unless: 否定快取,當unless指定的條件為true時,方法的返回值不回被快取。 
               sync:是否使用非同步模式

接下來一個一個來嘗試:
cacheNames:我們在上面的例子已經使用到了,用value是一樣的效果,都是給快取元件指定一個名字。
key: 快取資料使用的key,不設定的話預設是使用方法引數,上面已經說過了。

         我們還可以自己設定key的值,利用SpEL表示式,那麼快取中可以寫的SpEL如下圖所示:

下面就來自己定義一個key的值,比如我想將key設定為:方法名[引數值],則如下拼接,根據上圖看到,#root.methodName就是方法名,#id就是引數值:

可以看到,生成的key就是我們自己設定的:方法名[引數值]

keyGenerator: 這個就是一個key生成器,自己寫一個就是,如下:

package com.example.cache.config;

import org.springframework.cache.interceptor.KeyGenerator;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * @author pavel
 * @date 2018/11/22 0022
 */
@Configuration
public class MyCacheConfig {

    @Bean("myKeyGenerator")
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                return method.getName() + "[" + Arrays.asList(params).toString() + "]";
            }
        };
    }
}

然後在方法上指定keyGenerator:

如上圖,生成的key為:getUser()[ [1] ],多一層[]是Arrays.asList()產生的。

cacheManager: 指定快取管理器,這個後面我們使用多個快取管理器時再討論。

condition: 指定符合條件的情況下才快取;比如我指定當引數id大於1的時候才快取:condition = "#id>1",如下:

可以看到,連續兩次請求,獲取id為1 的使用者,都會呼叫方法傳送sql語句查詢,第一次的查詢結果並沒有被快取,

可見,第一次請求的結果進行的快取。

unless: 否定快取,當unless指定的條件為true,方法的返回值就不會被快取。例如:unless = "#id == 2", 當引數id為2時就不進行快取。

控制檯兩次請求都呼叫方法併發送sql進行查詢。

只是第一次請求呼叫了方法併發送sql進行查詢。

sync:是否使用非同步模式。sync=true   

  看原始碼,預設是false,使用非同步模式就不支援unless了,這個就自己試一下吧。如下:

5、@Cacheput註解

作用:修改資料庫資料,並同步更新快取。  這就避免了更新了資料庫的資料(資料已加入快取)後再次查詢還是查到更新前的資料。

執行時機:

        1、先呼叫目標方法

        2、將目標方法的結果快取起來 

下面就來使用一下這個註解。給上面的UserService中增加一個updateUser方法,方法上使用@Cacheput註解,並返回修改後的User物件, 如下:

package com.example.cache.service;

import com.example.cache.domain.User;
import com.example.cache.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**
 * @author pavel
 * @date 2018/11/22 0022
 */
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Cacheable(cacheNames = "user")
    public User getUser(Long id) {
        System.out.println("查詢" + id + "號員工");
        return userMapper.getUser(id);
    }


    @CachePut
    public User updateUser(User user) {
        System.out.println("更新" + user.getId() + "號員工");
        userMapper.updateUser(user);
        return user;
    }
}

UserController中增加updateUser方法:

package com.example.cache.controller;

import com.example.cache.domain.User;
import com.example.cache.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author pavel
 * @date 2018/11/19 0019
 */
@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable("id") Long id) {
        User user = userService.getUser(id);
        System.out.println("查詢結果: " + user);
        return user;
    }

    @GetMapping("/user")
    public User updateUser(User user) {
        User u = userService.updateUser(user);
        System.out.println("更新後結果:" + u);
        return u;
    }
}

下面我們來測試一下,先說一下測試流程:

        2、再次查詢1號使用者,還是之前的結果

        4、再次查詢1號使用者,那麼,會不會查詢資料庫?返回的是更新前的使用者還是更新後的使用者?

啟動專案,按照以上測試流程逐步進行,控制檯輸出結果如下:

上體可以看到,步驟3確實是更新了資料庫,但是步驟4再次查詢時,沒有傳送查詢sql,但是查詢結果確實更新前的資料,這是 怎麼回事?難道@Cacheput沒起作用?

回頭看一下我們UserService中的兩個方法,查詢和更新,我們都沒有設定快取資料的key,所以都預設以引數為key,

那麼上面步驟1執行之後,快取中:key = id  value = User物件

              步驟3執行之後,快取中:key = 傳入的User物件 value = 返回的User物件

這下明白了吧,我們更新操作之後,返回的更新後的資料其實是存入到了快取中,但是存入的key同樣是一個User物件而不是id。

下面將updateUser()方法做下修改,設定key值為傳入User物件的id或者是返回User物件的id:

   @CachePut(cacheNames = "user", key = "#user.id")    // 或者key = "#result.id"
    public User updateUser(User user) {
        System.out.println("更新" + user.getId() + "號員工");
        userMapper.updateUser(user);
        return user;
    }

這樣修改之後,查詢和更新的方法,key值都是User物件的id了。

再次測試以上四個步驟,此時資料庫中id為1的資料是:【name=xinanxin, age=25】,將其修改為【name=curry, age=31】

控制檯輸出如下:

可以看到,步驟4查詢出來的就是修改後的user物件了。有興趣的話,可以debug一步步的看一下原始碼,是怎麼進行快取中資料更新的。

6、@CacheEvict註解

作用:快取清除。一般用在刪除的方法上,刪除資料後,進行快取清除

相關屬性:

        key: 指定要清除的資料

        allEntries: 預設是false,若設定為true,則清除該Cache中的所有快取資料。

        beforeInvocation: 預設是false, 代表快取的清除是在方法呼叫之後進行的,如果方法出現異常,則快取不會被清除,若設定為true,則代表快取的清除是在方法呼叫之前進行的,不論該方法的執行是否會出現異常,快取都會被清除。

 這個註解,案例就不詳細寫了啊,可以參考上面的查詢和修改,寫上一個delete方法測試。

7、@Caching註解

作用:定義複雜的快取規則。是一個組合註解,裡面可以包括以上介紹的三個註解。如下:

我們同樣來寫一個案例,使用一下這個註解.

在UserMapper介面增加方法,getUser=ByName()

    @Select("select * from user where name = #{name}")
    User getUserByName(String name);

在UserService中增加方法,getUserByUserName():

  @Caching(
            cacheable = {
                    @Cacheable(cacheNames = "user")
            },
            put = {
                    @CachePut(cacheNames = "user", key = "#result.id"),
                    @CachePut(cacheNames = "user", key = "#result.age")
            }
    )
    public User getUserByName(String name) {
        System.out.println("通過name查詢User");
        return userMapper.getUserByName(name);
    }

在這個方法上,加上了@Caching註解,裡面包含了caccheable和put,cacheable給快取中新增的資料key為name,value為User物件, put給快取新增的資料key是 id和 age,value為User物件。

等於說,呼叫了這個方法之後,再根據id,age去查詢,就不用查詢資料庫了,直接從快取中取。

但是根據name查詢,還是會發送sql進行資料庫查詢,因為使用了@CachePut註解,使用這個註解每次都會呼叫方法。

在UserController中增加getUserByUserName()

    @GetMapping("/user/find-by-name")
    public User getUserByName(@RequestParam("name") String name) {
        User user = userService.getUserByName(name);
        System.out.println("getUserByName()查詢結果: " + user);
        return user;
    }

下面來測試一下,測試流程:

重啟,然後依次執行上面步驟,控制檯輸出如下:

通過控制檯輸出可以看到,步驟1通過name查詢,傳送sql查詢,步驟2再通過id查詢(或者是自己寫的通過age查詢),都不會發送sql查資料庫,而是直接從快取中取的,步驟3再次通過name查詢時,還是傳送了sql查詢,說明加上@CachePut後,每次呼叫都會查詢資料庫。

8、@CacheConfig註解

這是全域性快取配置,作用在類上面,對整個類生效。看下這個註解都有哪些內容:

可以設定快取名稱,key生成器,快取管理器,以及快取解析器

比如在上面講@Caching註解的案例中,我們將快取資料都新增到了名為user的Cache中,所以我們配置了三次 cacheNames = "user",這樣很繁瑣,那麼可以在類上面使用@CacheConfig(cacheNames = "user") 來簡化,如下:

package com.example.cache.service;

import com.example.cache.domain.User;
import com.example.cache.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;

/**
 * @author pavel
 * @date 2018/11/22 0022
 */
@Service
@CacheConfig(cacheNames = "user")
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Cacheable()
    public User getUser(Long id) {
        System.out.println("查詢" + id + "號員工");
        return userMapper.getUser(id);
    }


    @CachePut(key = "#user.id")    // 或者key = "#result.id"
    public User updateUser(User user) {
        System.out.println("更新" + user.getId() + "號員工");
        userMapper.updateUser(user);
        return user;
    }

    @CacheEvict()
    public void deleteUser(Long id) {
        System.out.println("刪除"+ id + "號員工");
        userMapper.deleteUser(id);
    }

    @Caching(
            cacheable = {
                    @Cacheable()
            },
            put = {
                    @CachePut(key = "#result.id"),
                    @CachePut(key = "#result.age")
            }
    )
    public User getUserByName(String name) {
        System.out.println("通過name查詢User");
        return userMapper.getUserByName(name);
    }
}

這樣,效果是一樣的,可以自己嘗試一下。

好了,我關於Spring Boot的快取相關內容的學習就是如上這些了,以後再深入學習的再補充過來。

希望對剛研究這塊的小夥伴能有一點點幫助。