Spring boot快取初體驗

  • 2019 年 10 月 3 日
  • 筆記

spring boot快取初體驗

1.項目搭建

使用MySQL作為資料庫,spring boot集成mybatis來操作資料庫,所以在使用springboot的cache組件時,需要先搭建一個簡單的ssm環境。

首先是項目依賴

<dependencies>      <dependency>          <groupId>org.springframework.boot</groupId>          <artifactId>spring-boot-starter-cache</artifactId>      </dependency>      <dependency>          <groupId>org.springframework.boot</groupId>          <artifactId>spring-boot-starter-web</artifactId>      </dependency>      <dependency>          <groupId>org.mybatis.spring.boot</groupId>          <artifactId>mybatis-spring-boot-starter</artifactId>          <version>1.3.2</version>      </dependency>      <dependency>          <groupId>org.springframework.boot</groupId>          <artifactId>spring-boot-starter-data-redis</artifactId>      </dependency>      <dependency>          <groupId>mysql</groupId>          <artifactId>mysql-connector-java</artifactId>          <version>5.1.48</version>          <scope>runtime</scope>      </dependency>      <dependency>          <groupId>org.springframework.boot</groupId>          <artifactId>spring-boot-starter-test</artifactId>          <scope>test</scope>      </dependency>  </dependencies>  

資料庫測試用的數據

CREATE TABLE `student`  (    `id` int(11) NOT NULL AUTO_INCREMENT,    `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,    `gender` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,    `age` int(11) NULL DEFAULT NULL,    PRIMARY KEY (`id`) USING BTREE  ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;    -- ----------------------------  -- Records of student  -- ----------------------------  INSERT INTO `student` VALUES (1, 'eric', 'male', 22);  INSERT INTO `student` VALUES (2, 'alice', 'female', 23);  INSERT INTO `student` VALUES (3, 'bob', 'male', 21);

對應的實體類程式碼如下:

public class Student {      private Integer id;      private String name;      private String gender;      private Integer age;      //省略構造函數,getter,setter,toString  }

對應的mapper:

public interface StudentMapper {      @Select("select * from student where id = #{id}")      Student getStudentById(Integer id);  }

對應的service:

@Service  public class StudentService {      @Autowired      private StudentMapper studentMapper;        public Student getStudentById(Integer id) {          return studentMapper.getStudentById(id);      }  }

對應的測試類:

@RunWith(SpringRunner.class)  @SpringBootTest  public class DemoCacheApplicationTests {      @Autowired      private StudentService studentService;        /**       * 測試mybatis是否正確配置       */      @Test      public void contextLoads() {          System.out.println(studentService.getStudentById(1));      }    }

運行上面的測試方法,成功列印

student{id=1, name='eric', gender='male', age=22}

項目的架子基本搭建成功了,接下來就是使用springboot提供的快取註解來測試一下。

在這之前,先了解一些背景知識。

首先是JSR107快取規範,Java Caching定義了5個核心介面,分別是CachingProvider, CacheManager, Cache, Entry
和 Expiry。

  • CachingProvider
    定義了創建、配置、獲取、管理和控制多個CacheManager。一個應用可以在運行期訪問多個CachingProvider。
  • CacheManager
    定義了創建、配置、獲取、管理和控制多個唯一命名的Cache,這些Cache存在於CacheManager的上下文中。一個CacheManager僅被一個CachingProvider所擁有。
  • Cache
    是一個類似Map的數據結構並臨時存儲以Key為索引的值。一個Cache僅被一個CacheManager所擁有。
  • Entry
    是一個存儲在Cache中的key-value對。
  • Expiry
    每一個存儲在Cache中的條目有一個定義的有效期。一旦超過這個時間,條目為過期的狀態。一旦過期,條目將不可訪問、更新和刪除。快取有效期可以通過ExpiryPolicy設置。

Snipaste_2019-09-23_21-58-01.png

Spring從3.1開始定義org.springframework.cache.Cache和org.springframework.cache.CacheManager介面來統一不同的快取技術,並支援使用JCache 【JSR-107】註解簡化我們開發。

我們先看一下Cache介面的基本結構:

Snipaste_2019-09-25_23-33-25.png

Cache介面為快取的組件規範各種快取的基本操作,spring提供了各種常用的xxxCache實現,比如:RedisCache,EhCacheCache,ConcurrentMapCache等等。

在當前添加的依賴下,可以找到這些Cache實現

Snipaste_2019-09-25_23-32-20.png

每次調用需要快取功能的方法時,Spring就會去檢查指定參數的目標方法是否已經被調用過,如果有就直接從快取中獲取,沒有則調用方法並將結果快取之後再返回給用戶,之後的數據都是直接從快取中獲取。

所以使用快取需要考慮以下幾個方面:

  • 確定方法是否需要快取
  • 確定方法快取的策略(比如key的設定,快取的數據是使用json格式還是Java序列化)
  • 快取和資料庫數據一致性如何保證
  • 每次從快取中讀取之前快取過的數據

首先並不是所有方法都需要快取,一般來講都是頻繁訪問並且不經常修改的數據才需要快取。

key的生成策略可以直接使用key屬性來指定,也可以指定keyGenerator

快取的數據默認情況下都是使用Java序列號的方式,我們可以將它存儲為json格式,看項目需要。

快取的一致性,這個比較複雜,本文不涉及到高並發情況下快取和資料庫一致的討論,只是保證在數據修改或刪除時,及時地更新快取中的數據。換句話說,就是數據在快取之後,如果之後調用了修改的方法,把數據修改了,需要CachePut註解及時地把快取里的數據也一併修改,或者,調用了刪除的方法,需要使用CacheEvict註解來刪除相應快取的數據。

至於每次都從快取中讀取已經快取過的數據,這個事情就交給Spring來自動處理吧。

Cache 快取介面,封裝快取的基本操作
CacheManager 快取管理器,管理各種快取組件,一個應用程式可以有多個快取管理器
@Cacheable 主要針對方法配置,能夠根據方法的請求參數對其結果進行快取
@CacheEvict 清空快取
@CachePut 保證方法被調用,又希望結果被快取,一般用於修改數據。
@EnableCaching 開啟基於註解的快取
keyGenerator 快取數據時key生成策略
serialize 快取數據時value序列化策略

@CachePut@Cacheable兩個註解的區別是什麼呢?

@CachePut:這個注釋可以確保方法被執行,同時方法的返回值也被記錄到快取中。

@Cacheable:當重複使用相同參數調用方法的時候,方法本身不會被調用執行,即方法本身被略過了,取而代之的是方法的結果直接從快取中找到並返回了。

​ 對於@CachePut這個註解,它的作用是什麼呢,每次方法都執行,那麼快取的意義是什麼呢?答案很簡單,同一個快取實例的相同的key的快取的數據,可以用@CachePut更新,而@Cacheable在取值的時候,是@CachePut更新後的值。但同時也要注意確保是同一個快取實例對象,並且key要保證一致!!!

@Cacheable,@CachePut,@CacheEvict註解的常用屬性如下:

屬性 作用 示例
value 快取的名稱,在 spring 配置文件中定義,必須指定至少一個 例如: @Cacheable(value=」mycache」) 或者 @Cacheable(value={」cache1」,」cache2」}
key 快取的 key,可以為空,如果指定要按照 SpEL 表達式編寫,如果不指定,則預設按照方法的所有參數進行組合 例如: @Cacheable(value=」testcache」,key=」#userName」)
condition 快取的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行快取/清除快取,在調用方法之前之後都能判斷 例如: @Cacheable(value=」testcache」,condition=」#userName.length()>2」)
allEntries (@CacheEvict ) 是否清空所有快取內容,預設為 false,如果指定為 true,則方法調用後將立即清空所有快取 例如: @CachEvict(value=」testcache」,allEntries=true)
beforeInvocation (@CacheEvict) 是否在方法執行前就清空,預設為 false,如果指定為 true,則在方法還沒有執行的時候就清空快取,預設情況下,如果方法執行拋出異常,則不會清空快取 例如: @CachEvict(value=」testcache」,beforeInvocation=true)
unless (@CachePut) (@Cacheable) 用於否決快取的,不像condition,該表達式只在方法執行之後判斷,此時可以拿到返回值result進行判斷。條件為true不會快取,fasle才快取 例如: @Cacheable(value=」testcache」,unless=」#result == null」)

Cache SpEL available metadata

名字 位置 描述 示例
methodName root object 當前被調用的方法名 #root.methodName
method root object 當前被調用的方法 #root.method.name
target root object 當前被調用的目標對象 #root.target
targetClass root object 當前被調用的目標對象類 #root.targetClass
args root object 當前被調用的方法的參數列表 #root.args[0]
caches root object 當前方法調用使用的快取列表(如@Cacheable(value={"cache1", "cache2"})),則有兩個cache #root.caches[0].name
argument name evaluation context 方法參數的名字. 可以直接 #參數名 ,也可以使用 #p0或#a0 的形式,0代表參數的索引; #iban 、 #a0 、 #p0
result evaluation context 方法執行後的返回值(僅當方法執行之後的判斷有效,如『unless』,』cache put』的表達式 』cache evict』的表達式beforeInvocation=false) #result

這個掌握就好,沒有必要去死記硬背,默認情況下的配置都是夠用的。

2.快取使用過程解析

首先需要引入spring-boot-starter-cache依賴

然後使用@EnableCaching開啟快取功能

然後就可以使用快取註解來支援了。

先看一下官方API裡面是怎麼說的吧:

@Target(value=TYPE)  @Retention(value=RUNTIME)  @Documented  @Import(value=CachingConfigurationSelector.class)  public @interface EnableCaching

Enables Spring’s annotation-driven cache management capability,To be used together with @Configuration classes as follows:

@Configuration  @EnableCaching  public class AppConfig {        @Bean      public MyService myService() {          // configure and return a class having @Cacheable methods          return new MyService();      }      @Bean      public CacheManager cacheManager() {          // configure and return an implementation of Spring's CacheManager SPI          SimpleCacheManager cacheManager = new SimpleCacheManager();          cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));          return cacheManager;      }  }

@EnableCaching is responsible for registering the necessary Spring components that power annotation-driven cache management, such as the CacheInterceptor and the proxy- or AspectJ-based advice that weaves the interceptor into the call stack when @Cacheable methods are invoked.

官方文檔的描述簡潔明了,我們只需要開啟快取,然後訂製CacheManager即可。

If the JSR-107 API and Spring’s JCache implementation are present, the necessary components to manage standard cache annotations are also registered. This creates the proxy- or AspectJ-based advice that weaves the interceptor into the call stack when methods annotated with CacheResult, CachePut, CacheRemove or CacheRemoveAll are invoked.

強大的spring同樣支援了JSR107快取註解!!!當然,本文還是主要以講解spring的快取註解為主。

For those that wish to establish a more direct relationship between @EnableCaching and the exact cache manager bean to be used, the CachingConfigurer callback interface may be implemented. Notice the @Override-annotated methods below:

如果想要明確地訂製你的CacheManager,可以像下面這樣使用

 @Configuration   @EnableCaching   public class AppConfig extends CachingConfigurerSupport {         @Bean       public MyService myService() {           // configure and return a class having @Cacheable methods           return new MyService();       }         @Bean       @Override       public CacheManager cacheManager() {           // configure and return an implementation of Spring's CacheManager SPI           SimpleCacheManager cacheManager = new SimpleCacheManager();           cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("default")));           return cacheManager;       }         @Bean       @Override       public KeyGenerator keyGenerator() {           // configure and return an implementation of Spring's KeyGenerator SPI           return new MyKeyGenerator();       }   }

This approach may be desirable simply because it is more explicit, or it may be necessary in order to distinguish between two CacheManager

因為一個應用環境下可以有多個CacheManager,這樣聲明CacheManager可以更加直觀。

Notice also the keyGenerator method in the example above. This allows for customizing the strategy for cache key generation, per Spring’s KeyGenerator SPI. Normally, @EnableCaching will configure Spring’s SimpleKeyGenerator for this purpose, but when implementing CachingConfigurer, a key generator must be provided explicitly. Return null or new SimpleKeyGenerator() from this method if no customization is necessary.

如果實現了CachingConfigurer介面,就需要明確定義keyGenerator

CachingConfigurer offers additional customization options: it is recommended to extend from CachingConfigurerSupport that provides a default implementation for all methods which can be useful if you do not need to customize everything. See CachingConfigurer Javadoc for further details.

可以通過繼承CachingConfigurerSupport來實現其它的訂製功能。CachingConfigurerSupport類的結構如下,可以只對你需要訂製的功能進行重寫,其它的一律默認返回null即可,如果返回null,那麼spring boot 的自動配置就會生效。

  /**   * An implementation of {@link CachingConfigurer} with empty methods allowing   * sub-classes to override only the methods they're interested in.   *   * @author Stephane Nicoll   * @since 4.1   * @see CachingConfigurer   */  public class CachingConfigurerSupport implements CachingConfigurer {        @Override      public CacheManager cacheManager() {          return null;      }        @Override      public KeyGenerator keyGenerator() {          return null;      }        @Override      public CacheResolver cacheResolver() {          return null;      }        @Override      public CacheErrorHandler errorHandler() {          return null;      }    }  

The mode() attribute controls how advice is applied: If the mode is AdviceMode.PROXY (the default), then the other attributes control the behavior of the proxying. Please note that proxy mode allows for interception of calls through the proxy only; local calls within the same class cannot get intercepted that way.

Note that if the mode() is set to AdviceMode.ASPECTJ, then the value of the proxyTargetClass() attribute will be ignored. Note also that in this case the spring-aspects module JAR must be present on the classpath, with compile-time weaving or load-time weaving applying the aspect to the affected classes. There is no proxy involved in such a scenario; local calls will be intercepted as well.

真是縱享絲滑。

3.實際上手

@CacheConfig註解可以定義當前類的所有使用到快取註解(@Cacheable,@CachePut,@CacheEvict)的通用配置,下面的示例程式碼實際只配置了當前類的快取名稱

@Service  @CacheConfig(cacheNames = "student")  public class StudentService {      @Autowired      private StudentMapper studentMapper;        @Cacheable      public Student getStudentById(Integer id) {          System.out.println("從資料庫中查詢學生:" + id);          return studentMapper.getStudentById(id);      }        @CachePut      public Student updateStudent(Student student) {          System.out.println("更新資料庫中的學生數據:" + student);          studentMapper.updateStudent(student);          return student;      }        @CacheEvict      public void deleteStudent(Integer id) {          System.out.println("刪除資料庫中的學生:"+id);          studentMapper.delStudent(id);      }  }  

上面只是簡單的使用這三個註解,更加詳細的屬性使用,請看後面的內容。我們先測試一下快取的使用效果。

測試類的程式碼如下:

@RunWith(SpringRunner.class)  @SpringBootTest  public class DemoCacheApplicationTests {      @Autowired      private StudentService studentService;        @Test      public void contextLoads() {          System.out.println(studentService.getStudentById(1));      }        @Test      public void testUpdate() {          studentService.updateStudent(new Student(1,"gotohell","female",23));      }        @Test      public void testDelete() {          studentService.deleteStudent(1);      }  }  

首先測試@Cacheable註解,第一次調用該方法,列印的日誌如下:

從資料庫中查詢學生:1  student{id=1, name='mmm', gender='male', age=21}

第二次調用該方法,列印的日誌如下:

student{id=1, name='mmm', gender='male', age=21}

說明快取已經生效了,沒有從資料庫中獲取學生數據。我們看一下快取裡面的內容,

Snipaste_2019-09-26_22-52-46.png

這是默認使用jdk序列化存儲的結果,我們可以選擇採用json格式存儲數據。另外,key的生成策略,默認是cache名稱前綴加上方法參數,我覺得這個默認情況下就已經夠用了,不需要再進行額外的訂製。

再來測試一下修改,

列印日誌如下:

更新資料庫中的學生數據:student{id=1, name='gotohell', gender='female', age=23}

查看資料庫中的數據,已經修改成功,redis的數據由於是序列化的,這裡就不截圖了,我們直接再調用一次查詢看它有沒有更新即可。

列印結果如下:

student{id=1, name='mmm', gender='male', age=21}

說明沒有更新快取中的數據,難道是@CachePut註解不起作用嗎?

查看一下redis

Snipaste_2019-09-26_23-01-43.png

這才發現,原來第二次修改的數據,默認使用的快取key是對象,為什麼呢,因為默認情況下,key的生成策略就是快取名稱student+方法的參數,而更新方法的參數就是學生對象,所以測試拿不到更新之後的數據,因為兩個key不一致。

那麼只要把更新方法的key指定為1不就可以了嗎

@CachePut(key = "#result.id")  public Student updateStudent(Student student) {      System.out.println("更新資料庫中的學生數據:" + student);      studentMapper.updateStudent(student);      return student;  }

重新指定的key就是這樣子,它支援spring的表達式,具體的使用規則,前面已經列出表格了。重新測試之後,列印日誌如下:

student{id=1, name='gotohell', gender='female', age=23}

獲取到了更新之後的數據,說明key起作用了。

再來測試一下刪除,列印日誌如下:

刪除資料庫中的學生:1

資料庫中的數據已經成功刪除了,快取中的數據也已經清空了。

Snipaste_2019-09-26_23-11-59.png

這個時候再去調用查詢,列印的日誌如下:

從資料庫中查詢學生:1  null

從列印的日誌來看,是查詢了資料庫的,因為快取裡面已經沒有了,但是資料庫中的數據也是刪除了的,所以返回了null

4.使用JSON來序列化對象

這個就需要我們來訂製CacheManager了,加入一個新的配置類

@Configuration  public class MyRedisConfig {      @Bean      public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {          //初始化一個RedisCacheWriter          RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);          //設置CacheManager的值序列化方式為json序列化          RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair                  .fromSerializer(new GenericJackson2JsonRedisSerializer());          RedisCacheConfiguration defaultCacheConfig=RedisCacheConfiguration.defaultCacheConfig()                  .serializeValuesWith(pair).entryTtl(Duration.ofHours(1));          //初始化RedisCacheManager          return new RedisCacheManager(redisCacheWriter, defaultCacheConfig);      }  }  

重新測試查詢方法,發現快取的值採用了JSON格式序列化方式。

Snipaste_2019-09-26_23-46-09.png

源碼地址:https://github.com/lingEric/springboot-integration-hello