1. 概述
本文介紹Spring boot 如何使用redis做緩存,如何對redis緩存進行定製化配置(如key的有效期)以及spring boot 如何初始化redis做緩存。使用具體的代碼介紹了@Cacheable,@CacheEvict,@CachePut,@CacheConfig等註解及其屬性的用法。
2. spring boot集成redis
2.1. application.properties
配置application.properties,包含如下信息:
- 指定緩存的類型
- 配置redis的服務器信息
- 請不要配置spring.cache.cache-names值,原因後面再說
## 緩存
# spring.cache.cache-names=book1,book2
spring.cache.type=REDIS
# REDIS (RedisProperties)
spring.redis.database=0
spring.redis.host=192.168.188.7
spring.redis.password=
spring.redis.port=6379
spring.redis.pool.max-idle=8
spring.redis.pool.min-idle=0
spring.redis.pool.max-active=100
spring.redis.pool.max-wait=-1
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
2.2. 配置啓動類
- @EnableCaching: 啓動緩存
- 重新配置RedisCacheManager,使用新的配置的值
@SpringBootApplication
@EnableCaching // 啓動緩存
public class CacheApplication {
private static final Logger log = LoggerFactory.getLogger(CacheApplication.class);
public static void main(String[] args) {
log.info("Start CacheApplication.. ");
SpringApplication.run(CacheApplication.class, args);
}
/**
* 重新配置RedisCacheManager
* @param rd
*/
@Autowired
public void configRedisCacheManger(RedisCacheManager rd){
rd.setDefaultExpiration(100L);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
經過以上配置後,redis緩存管理對象已經生成。下面簡單介紹spring boot如何初始化redis緩存。
2.3. spring boot 如何初始化redis做緩存
緩存管理接口org.springframework.cache.CacheManager,spring boot就是通過此類實現緩存的管理。redis對應此接口的實現類是org.springframework.data.redis.cache.RedisCacheManager。下面介紹此類如何生成。
首先我們配置application.properties的spring.redis.* 屬性後@EnableCaching後,spring會執行RedisAutoConfiguration,初始化RedisTemplate和StringRedisTemplate
@Configuration
@ConditionalOnClass({ JedisConnection.class, RedisOperations.class, Jedis.class })
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfiguration {
/**
* Standard Redis configuration.
*/
@Configuration
protected static class RedisConfiguration {
….
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
然後RedisCacheConfiguration會將RedisAutoConfiguration生成的RedisTemplate注入方法生成RedisCacheManager 後。
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisTemplate.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
private final CacheProperties cacheProperties;
private final CacheManagerCustomizers customizerInvoker;
RedisCacheConfiguration(CacheProperties cacheProperties,
CacheManagerCustomizers customizerInvoker) {
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
}
@Bean
public RedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate);
cacheManager.setUsePrefix(true);
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
cacheManager.setCacheNames(cacheNames);
}
return this.customizerInvoker.customize(cacheManager);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
根據以上的分析,我們知道在spring已經幫我們生成一個RedisCacheManager並進行了配置。
最後我們再可以對這個RedisCacheManager進行二次配置,這裏只列出配置key的有效期
/**
* 重新配置RedisCacheManager
* @param rd
*/
@Autowired
public void configRedisCacheManger(RedisCacheManager rd){
rd.setDefaultExpiration(100L);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
注意:
請不要在applicaion.properties中配置: spring.cache.cache-names=book1,book2,否則會導致我們新的配置無法作用到這些配置的cache上。這是因爲RedisCacheConfiguration 初始化RedisCacheManager後,會立即調用RedisCacheConfiguration 的初始化cache,而此時configRedisCacheManger還沒有執行此方法,使得我們的配置無法啓作用。反之,如果不配置,則後創建cache,會使用我們的配置。
3. spring緩存註解的用法
上節已經介紹如何配置緩存,這節介紹如何使用緩存。
3.1 輔助類
下方會使用到的輔助類
Book:
public class Book implements Serializable {
private static final long serialVersionUID = 2629983876059197650L;
private String id;
private String name; // 書名
private Integer price; // 價格
private Date update; //
public Book(String id, String name, Integer price, Date update) {
super();
this.id = id;
this.name = name;
this.price = price;
this.update = update;
}
// set/get略
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
BookQry : 封裝請求類
public class BookQry {
private String id;
private String name; // 書名
// set/get略
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
AbstractService
抽象類:初始化repositoryBook 值,模擬數據庫數據。BookService 和BookService2都是繼承此類
public abstract class AbstractService {
protected static Map<String, Book> repositoryBook = new HashMap<>();
public AbstractService() {
super();
}
@PostConstruct
public void init() {
// 1
Book book1 = new Book("1", "name_1", 11, new Date());
repositoryBook.put(book1.getId(), book1);
// 2
Book book2 = new Book("2", "name_2", 11, new Date());
repositoryBook.put(book2.getId(), book2);
// 3
Book book3 = new Book("3", "name_3", 11, new Date());
repositoryBook.put(book3.getId(), book3);
// 4
Book book4 = new Book("4", "name_4", 11, new Date());
repositoryBook.put(book4.getId(), book4);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
3.2. @Cacheable
@Cacheable的屬性的意義
- cacheNames:指定緩存的名稱
- key:定義組成的key值,如果不定義,則使用全部的參數計算一個key值。可以使用spring El表達式
/**
* cacheNames 設置緩存的值
* key:指定緩存的key,這是指參數id值。 key可以使用spEl表達式
* @param id
* @return
*/
@Cacheable(cacheNames="book1", key="#id")
public Book queryBookCacheable(String id){
logger.info("queryBookCacheable,id={}",id);
return repositoryBook.get(id);
}
/**
* 這裏使用另一個緩存存儲緩存
*
* @param id
* @return
*/
@Cacheable(cacheNames="book2", key="#id")
public Book queryBookCacheable_2(String id){
logger.info("queryBookCacheable_2,id={}",id);
return repositoryBook.get(id);
}
/**
* 緩存的key也可以指定對象的成員變量
* @param qry
* @return
*/
@Cacheable(cacheNames="book1", key="#qry.id")
public Book queryBookCacheableByBookQry(BookQry qry){
logger.info("queryBookCacheableByBookQry,qry={}",qry);
String id = qry.getId();
Assert.notNull(id, "id can't be null!");
String name = qry.getName();
Book book = null;
if(id != null){
book = repositoryBook.get(id);
if(book != null && !(name != null && book.getName().equals(name))){
book = null;
}
}
return book;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- keyGenerator:定義key生成的類,和key的不能同時存在
/**
* 以上我們使用默認的keyGenerator,對應spring的SimpleKeyGenerator
* 如果你的使用很複雜,我們也可以自定義myKeyGenerator的生成key
*
* key和keyGenerator是互斥,如果同時制定會出異常
* The key and keyGenerator parameters are mutually exclusive and an operation specifying both will result in an exception.
*
* @param id
* @return
*/
@Cacheable(cacheNames="book3", keyGenerator="myKeyGenerator")
public Book queryBookCacheableUseMyKeyGenerator(String id){
logger.info("queryBookCacheableUseMyKeyGenerator,id={}",id);
return repositoryBook.get(id);
}
// 自定義緩存key的生成類實現如下:
@Component
public class MyKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
System.out.println("自定義緩存,使用第一參數作爲緩存key. params = " + Arrays.toString(params));
// 僅僅用於測試,實際不可能這麼寫
return params[0] + "0";
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- sync:如果設置sync=true:a. 如果緩存中沒有數據,多個線程同時訪問這個方法,則只有一個方法會執行到方法,其它方法需要等待; b. 如果緩存中已經有數據,則多個線程可以同時從緩存中獲取數據
/***
* 如果設置sync=true,
* 如果緩存中沒有數據,多個線程同時訪問這個方法,則只有一個方法會執行到方法,其它方法需要等待
* 如果緩存中已經有數據,則多個線程可以同時從緩存中獲取數據
* @param id
* @return
*/
@Cacheable(cacheNames="book3", sync=true)
public Book queryBookCacheableWithSync(String id) {
logger.info("begin ... queryBookCacheableByBookQry,id={}",id);
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
}
logger.info("end ... queryBookCacheableByBookQry,id={}",id);
return repositoryBook.get(id);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- condition和unless 只滿足特定條件才進行緩存:
- condition: 在執行方法前,condition的值爲true,則緩存數據
- unless :在執行方法後,判斷unless ,如果值爲true,則不緩存數據
- conditon和unless可以同時使用,則此時只緩存同時滿足兩者的記錄
/**
* 條件緩存:
* 只有滿足condition的請求纔可以進行緩存,如果不滿足條件,則跟方法沒有@Cacheable註解的方法一樣
* 如下面只有id < 3才進行緩存
*
*/
@Cacheable(cacheNames="book11", condition="T(java.lang.Integer).parseInt(#id) < 3 ")
public Book queryBookCacheableWithCondition(String id) {
logger.info("queryBookCacheableByBookQry,id={}",id);
return repositoryBook.get(id);
}
/**
* 條件緩存:
* 對不滿足unless的記錄,才進行緩存
* "unless expressions" are evaluated after the method has been called
* 如下面:只對不滿足返回 'T(java.lang.Integer).parseInt(#result.id) <3 ' 的記錄進行緩存
* @param id
* @return
*/
@Cacheable(cacheNames="book22", unless = "T(java.lang.Integer).parseInt(#result.id) <3 ")
public Book queryBookCacheableWithUnless(String id) {
logger.info("queryBookCacheableByBookQry,id={}",id);
return repositoryBook.get(id);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
3.3. @CacheEvict
刪除緩存
- allEntries = true: 清空緩存book1裏的所有值
- allEntries = false: 默認值,此時只刪除key對應的值
/**
* allEntries = true: 清空book1裏的所有緩存
*/
@CacheEvict(cacheNames="book1", allEntries=true)
public void clearBook1All(){
logger.info("clearAll");
}
/**
* 對符合key條件的記錄從緩存中book1移除
*/
@CacheEvict(cacheNames="book1", key="#id")
public void updateBook(String id, String name){
logger.info("updateBook");
Book book = repositoryBook.get(id);
if(book != null){
book.setName(name);
book.setUpdate(new Date());
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
3.4. @CachePut
每次執行都會執行方法,無論緩存裏是否有值,同時使用新的返回值的替換緩存中的值。這裏不同於@Cacheable:@Cacheable如果緩存沒有值,從則執行方法並緩存數據,如果緩存有值,則從緩存中獲取值
@CachePut(cacheNames="book1", key="#id")
public Book queryBookCachePut(String id){
logger.info("queryBookCachePut,id={}",id);
return repositoryBook.get(id);
}
- 1
- 2
- 3
- 4
- 5
3.5. @CacheConfig
@CacheConfig: 類級別的註解:如果我們在此註解中定義cacheNames,則此類中的所有方法上 @Cacheable的cacheNames默認都是此值。當然@Cacheable也可以重定義cacheNames的值
@Component
@CacheConfig(cacheNames="booksAll")
public class BookService2 extends AbstractService {
private static final Logger logger = LoggerFactory.getLogger(BookService2.class);
/**
* 此方法的@Cacheable沒有定義cacheNames,則使用類上的註解@CacheConfig裏的值 cacheNames
* @param id
* @return
*/
@Cacheable(key="#id")
public Book queryBookCacheable(String id){
logger.info("queryBookCacheable,id={}",id);
return repositoryBook.get(id);
}
/**
* 此方法的@Cacheable有定義cacheNames,則使用此值覆蓋類註解@CacheConfig裏的值cacheNames
*
* @param id
* @return
*/
@Cacheable(cacheNames="books_custom", key="#id")
public Book queryBookCacheable2(String id){
logger.info("queryBookCacheable2,id={}",id);
return repositoryBook.get(id);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
4. 測試
4.1. 準備 redis
可以通過docker安裝redis,非常方便。docker的用法見docker 安裝和常用命令
4.2. 測試類
如果要驗證以上各個方法,可以下載工程,並執行測試類CacheTest。由於比較簡單,這裏不在演示。