1. 程式人生 > >Spring boot快取初體驗

Spring boot快取初體驗

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設定。

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

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

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

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

每次呼叫需要快取功能的方法時,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}

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

這是預設使用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

這才發現,原來第二次修改的資料,預設使用的快取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

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

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

從資料庫中查詢學生: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格式序列化方式。

原始碼地址:https://github.com/lingEric/springboot-integration-he