Spring Boot + Redis實現快取
快取作為開發中提高服務效能相對有效的一種方式,在實際開發中得到廣泛使用。在Spring 3.1之前,如果想使用快取,相對是比較麻煩的,往往在業務程式碼中要摻雜快取的邏輯,比如判斷快取是否存在,存在則取快取,不存在在從DB中讀取,然後再講資料存入快取中,使用起來相當不方便。Spring 3.1引入了基於註釋的快取技術,它本質上不是一個具體的快取實現方案(例如EHCache、Redis、MemoryCache),而是一個對快取使用的抽象,通過在程式碼中新增少量註解,即能夠達到快取方法的返回物件的效果。也就是說底層具體快取實現對於開發人員來講是透明的,實現快取和具體業務程式碼的解耦。目前Spring註解支援的快取有java.util.concurrent.ConcurrentMap,Ehcache 2.x,Redis等。本文介紹如何通過Spring註解結合底層Redis實現快取,首先要引入如下兩個包:
spring-boot-starter-data-redis
spring-boot-starter-cache
redis.clients.jedis
1. 專案結構
| pom.xml | springboot-13-redis-cache.iml +---src | +---main | | +---java | | | \---com | | | \---zhuoli | | | \---service | | | \---springboot | | | \---redis | | | \---cache | | | | SpringBootRedisCacheApplicationContext.java | | | | | | | +---controller | | | | UserController.java | | | | | | | +---repository | | | | +---conf | | | | | DataSourceConfig.java | | | | | RedisCacheConfig.java | | | | | | | | | +---mapper | | | | | UserMapper.java | | | | | | | | | +---model | | | | | User.java | | | | | UserExample.java | | | | | | | | | \---service | | | | | UserRepository.java | | | | | | | | | \---impl | | | | UserRepositoryImpl.java | | | | | | | \---service | | | | UserControllerService.java | | | | | | | \---impl | | | UserControllerServiceImpl.java | | | | | \---resources | | | application.properties | | | | | +---autogen | | | generatorConfig_zhuoli.xml | | | | | \---base | | \---com | | \---zhuoli | | \---service | | \---springboot | | \---redis | | \---cache | | \---repository | | \---mapper | | UserMapper.xml | | | \---test | \---java
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> <groupId>com.zhuoli.service</groupId> <artifactId>springboot-13-redis-cache</artifactId> <version>1.0-SNAPSHOT</version> <!-- Spring Boot 啟動父依賴 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> </parent> <build> <plugins> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.5</version> <!--如果不配置configuration節點,配置檔名字必須為generatorConfig.xml--> <configuration> <!--可以自定義generatorConfig檔名--> <configurationFile>src/main/resources/autogen/generatorConfig_zhuoli.xml</configurationFile> <verbose>true</verbose> <overwrite>true</overwrite> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> <dependencies> <!-- Exclude Spring Boot's Default Logging --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</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>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- 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> </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> <!-- https://mvnrepository.com/artifact/redis.clients/jedis --> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.2</version> <scope>provided</scope> </dependency> </dependencies> </project>
3. RedisCache配置
@Configuration
public class RedisCacheConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("127.0.0.1", 6379);
return new JedisConnectionFactory(redisStandaloneConfiguration);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return new RedisCacheManager(
RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
this.getRedisCacheConfigurationWithTtl(600), // 預設策略,未配置的key會使用這個
this.getRedisCacheConfigurationMap() // 指定key策略
);
}
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
redisCacheConfigurationMap.put("user", this.getRedisCacheConfigurationWithTtl(10));
redisCacheConfigurationMap.put("other", this.getRedisCacheConfigurationWithTtl(18000));
return redisCacheConfigurationMap;
}
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(jackson2JsonRedisSerializer)
).entryTtl(Duration.ofSeconds(seconds));
return redisCacheConfiguration;
}
}
這裡分別講一下RedisCacheConfig中的幾個Bean配置
3.1 RedisConnectionFactory定義
RedisConnectionFactory主要用來定義Redis資料來源連線,最開始我的想法是,通過呼叫JedisConnectionFactory的setHostName等方法去指定資料來源連線的,如下:
@Bean
JedisConnectionFactory jedisConnectionFactory() {
JedisConnectionFactory jedisConFactory
= new JedisConnectionFactory();
jedisConFactory.setHostName("localhost");
jedisConFactory.setPort(6379);
return jedisConFactory;
}
但是檢視官方文件發現,Spring Boot 2.0後setHostName、setPort方法都已經過期了,Spring Boot 2.0後使用RedisStandaloneConfiguration替代檢視文件後,RedisConnectionFactory Bean定義如下:
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("127.0.0.1", 6379);
return new JedisConnectionFactory(redisStandaloneConfiguration);
}
3.2 定製RedisTemplate
自動配置的RedisTemplate並不能滿足大部分專案的需求,比如我們基本都需要設定特定的Serializer(RedisTemplate預設會使用JdkSerializationRedisSerializer)。
Redis底層中儲存的資料只是位元組,雖然Redis本身支援各種型別(List, Hash等),但在大多數情況下,這些指的是資料的儲存方式,而不是它所代表的內容(內容都是byte),使用者自己來決定資料如何被轉換成String或任何其他物件。使用者自定義型別和原始資料型別之間的互相轉換通過RedisSerializer介面(org.springframework.data.redis.serializer)來處理,顧名思義,它負責處理序列化/反序列化過程。包中多個實現可以開箱即用,比如:StringRedisSerializer和JdkSerializationRedisSerialize、用來處理JSON格式的資料的Jackson2JsonRedisSerializer或GenericJackson2JsonRedisSerializer。
上述RedisTemplate ValueSerializer序列化使用GenericJackson2JsonRedisSerializer而不是Jackson2JsonRedisSerializer,因為Jackson2JsonRedisSerializer需要為每一個需要序列化進Redis的類指定一個Jackson2JsonRedisSerializer,因為其建構函式中需要指定一個型別來做反序列化:
redis.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));
如果應用中有大量物件需要快取,這顯然是不合適的。而GenericJackson2JsonRedisSerializer直接把型別資訊序列化到了JSON格式中,讓一個例項可以操作多個物件的反序列化。
3.3 定製RedisCacheManager
有時候Spring Boot自動給我們配置的RedisCacheManager不能滿足我們應用的需求,比如我們要對快取的key進行設定宣告週期,這時候可以通過自定義CacheManager的方式實現。由於Spring Boot2.0之後,RedisCacheManager類發生了很多改變,原來的一堆方法比如setExpires、setUsePrefix、setDefaultExpiration都已經不存在了,通過檢視原始碼及文件,最終找到一種Spring Boot 2.*版本複合需求的RedisCache設定快取宣告週期的方法。其實RedisCacheManager還有很多其他的建構函式,可以按照具體需求選擇。上述配置實現cache名稱為user的快取生命週期為10S,名稱為other的快取生命週期為18000S,除此之外所有cache的宣告週期預設為600S。
4. 快取註解介紹
Spring針對快取提供的註解主要包含@Cacheable、@CachePut、@CacheEvict 三個,具體含義如下:
4.1 示例說明
@CachePut(value = “user”, key = “#user.id”,condition = “#user.username.length() < 10”) 只快取使用者名稱長度少於10的資料
@Cacheable(value = “user”, key = “#id”,condition = “#id < 10″) 只快取ID小於10的資料
@Cacheable(value=”user”,key=”#user.username.concat(##user.password)”) 快取後的redis key為 user::username::password(其中username和password為EL表示式)
@CacheEvict(value=”user”,allEntries=true,beforeInvocation=true) 加上beforeInvocation=true後,不管內部是否報錯,快取都將被清除
5. 快取使用
我在repository查詢方法上新增快取,實際開發中也可以把快取放在service層。repository層快取使用如下:
@Cacheable(value = "user", key = "#id")
@Override
public User getUserById(Long id) {
log.info("資料庫取資料");
return userMapper.selectByPrimaryKey(id);
}
@Cacheable註解表示當第一次呼叫getUserById方法時,會進行資料庫查詢,並將查詢到的結果User快取到Redis,redis的key為user::id,@Cacheable註解中,key屬性支援EL表示式,#id表示具體的引數id,比如方法請求id為6,則最終Redis快取的key為user::6
@CacheEvict(value = "user", key = "#id")
@Override
public int delUserById(Long id) {
UserExample example = new UserExample();
example.createCriteria().andIdEqualTo(id);
return userMapper.deleteByExample(example);
/*等價於
return userMapper.deleteByPrimaryKey(id);
*/
}
@CachePut(value = "user", key = "#user.id")
@Override
public User updateUser(User user) {
userMapper.updateByPrimaryKey(user);
return userMapper.selectByPrimaryKey(user.getId());
}
6. 開啟快取功能
@EnableCaching註解,開啟快取功能,如下:
@SpringBootApplication
@EnableCaching
@Import(value = {DataSourceConfig.class, RedisCacheConfig.class})
public class SpringBootRedisCacheApplicationContext {
public static void main(String[] args) {
SpringApplication.run(SpringBootRedisCacheApplicationContext.class, args);
}
}
篇幅原因,controller層、service層的程式碼這裡就不展示了,有興趣的同學可以到文章底部連結看一下示例程式碼。
7. 測試
之前在RedisCacheConfig中,我將名稱為user的快取宣告週期設定為10S,是為了測試宣告週期設定是否生效,根據測試結果顯示,宣告週期是生效的。正式進行測試之前,注意將user快取宣告週期設定為一個較大值。
7.1 @Cacheable![](https://github.com/GitHaoChen/zhuoli/raw/master/rediscachetest0.png)
![](https://github.com/GitHaoChen/zhuoli/raw/master/rediscachetest1.png)
7.2 @CachePut
7.3 @CacheEvict
預設情況下,RedisCache 不會快取任何null values,因為Redis會丟棄沒有value的keys。使用CacheEvict清楚快取後,會使用org.springframework.cache.support.NullValue作為佔位符儲存。