前提
使用緩存的好處有:
- 在一個系統中,數據庫中經常會有一些不怎麼變動的數據,舉個栗子:省市信息,不會經常變動。還有一些經過數據庫耗時計算得到的結果,也可以存入緩存。使用緩存的主要好處就是減少數據庫操作,減輕了數據庫壓力,提升系統性能。
- 由於用戶請求和數據庫之間增加了緩存這一層,而緩存數據處於內存中,相比較而言數據庫是讀取磁盤文件,速度自然比緩存慢。使用緩存大大提高了系統對請求的響應速度,提升用戶感知。
- 緩存是支持高併發的主要策略
壞處有:
- 緩存是以空間換時間的策略,大量使用會佔用大量內存
- 高併發情況下存在緩存和數據庫數據不一致
- 內存容量小,而且比硬盤貴
下面以SpringBoot2.1.6.RELEASE爲例,學習緩存的使用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
註解
SpringBoot 提供幾個註解實現緩存功能, 通過AOP方式實現
序號 | 註解名 | 含義 |
---|---|---|
1 | @EnableCaching | 開啓緩存註解 |
2 | @Cacheable | 如果能從緩存拿到數據,直接用緩存的 如果拿不到,執行方法,並將結果進行緩存 |
3 | @CachePut | 執行方法,並將結果存入緩存,緩存已存在則更新 |
4 | @CacheEvict | 執行方法後觸發刪除緩存 |
5 | @Caching | 用於組合多種緩存策略 |
6 | @CacheConfig | 設置類級別緩存通用屬性 |
Cacheable提供以下幾個屬性更細粒度控制緩存
序號 | 屬性名 | 含義 |
---|---|---|
1 | value/cacheNames | 緩存名字 |
2 | key | 緩存的key生成規則,spel表達式 |
3 | keyGenerator | 自定義key生成策略 |
4 | cacheManager | 自定義緩存管理器,默認ConcurrentMapCacheManager |
5 | cacheResolver | 自定義緩存解析器, 默認SimpleCacheResolver |
6 | condition | 緩存條件,傳入參數經過condition 判斷爲true緩存,spel表達式 |
7 | unless | 和condition類似,不過unless是在方法執行完畢後對結果進行判斷,是否加入緩存, spel表達式(常用#result) |
8 | sync | 應用多線程環境下,多個線程key相同,只會有一個線程對緩存讀寫,其他阻塞 |
StringBoot @Cacheable key針對方法參數默認生成策略爲:
- 無參數,key爲
SimpleKey.EMPTY
- 單個參數,key就是參數
- 多個參數,key是
SimpleKey
實例,參數注入SimpleKey的params屬性
key和keyGenerator只能指定一個,互斥
例子
@SpringBootApplication
@EnableCaching
public class SpringbootCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootCacheApplication.class, args);
}
}
@Data
public class User implements Serializable {
private String name;
private String password;
private Integer age;
}
@RestController
public class BaseController {
private static final Logger logger = LoggerFactory.getLogger(BaseController.class);
@GetMapping("/test")
@Cacheable(value = "cache-test")
public Object test() {
logger.info("進入test");
User user = new User();
user.setName("張三");
user.setAge(12);
user.setPassword("11111");
users.add(user);
return user;
}
@GetMapping("/cacheEvict")
@CacheEvict(value = "cache-test")
public Object cacheEvict() {
logger.info("----進入CacheEvict------");
return new ArrayList<>();
}
@GetMapping("/cachePut")
@CachePut(value = "cache-test")
public Object cachePut() {
logger.info("----進入cachePut------");
User user = new User();
user.setName("王五");
user.setAge(19);
user.setPassword("119811");
return user;
}
@GetMapping("/testKey")
// 將參數name和key拼接作爲key
@Cacheable(value = "cache-test", key = "#name + #age")
public Object cacheKey(String name, Integer age) {
logger.info("-----進入cacheKey------");
User user = new User();
user.setName(name);
user.setAge(age);
user.setPassword("11111");
return user;
}
@GetMapping("/testCondition")
// 當age > 10,將結果加入緩存
@Cacheable(value = "cache-test", condition = "#age > 10")
public Object cacheCondition(String name, Integer age) {
logger.info("----進入cacheCondition-----");
User user = new User();
user.setName(name);
user.setAge(age);
user.setPassword("11111");
return user;
}
@GetMapping("/testUnless")
// 如果方法執行結果User.age > 10就不緩存
@Cacheable(value = "cache-test", unless = "#result.age > 10")
public Object cacheUnless(String name, Integer age) {
logger.info("-----進入cacheUnless------");
User user = new User();
user.setName(name);
user.setAge(age);
user.setPassword("11111");
return user;
}
@GetMapping("/testSync")
// 當sync = true,三個線程key相同,只有一個對緩存讀寫,讀寫完後其他線程從緩存取,所以只會打印一次-----進入cacheSync----
// 當sync = false時,三個線程key相同,判斷緩存爲空,同時進入方法,會打印三次-----進入cacheSync----
@Cacheable(value = "cache-test", sync = true)
public Object cacheSync(String name, Integer age) {
logger.info("----進入cacheSync----");
User user = new User();
user.setName(name);
user.setAge(age);
user.setPassword("11111");
return user;
}
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootCacheApplicationTests {
private static final Logger logger = LoggerFactory.getLogger(SpringbootCacheApplicationTests.class);
@Test
public void test() {
// 訪問url,根據開啓端口,需要修改端口號
String requestUrl = "http://localhost:8088/testSync?name=1&age=2";
RestTemplate rt = new RestTemplate();
new Thread(() -> {
ResponseEntity<User> forEntity = rt.getForEntity(requestUrl, User.class);
logger.info("Thread1:{}",forEntity);
}).start();
new Thread(() -> {
ResponseEntity<User> forEntity = rt.getForEntity(requestUrl, User.class);
logger.info("Thread2:{}",forEntity);
}).start();
new Thread(() -> {
ResponseEntity<User> forEntity = rt.getForEntity(requestUrl, User.class);
logger.info("Thread3:{}",forEntity);
}).start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
默認支持
SpringBoot 內置對一些緩存的默認支持,SpringBoot也是按照以下順序對緩存的提供者進行檢測
- Generic
- JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
- EhCache 2.x
- Hazelcast
- Infinispan
- Couchbase
- Redis
- Caffeine
- Simple
ConcurrenMap
- ConcurrentMap屬於上面順序的第9位Simple,是SpringBoot對緩存的默認實現
- 實現類是
org.springframework.cache.concurrent.ConcurrentMapCache
Ehcache
-
pom依賴
<dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> <version>2.10.6</version> </dependency>
-
以ehcache 2.10.6爲例,默認需要將配置文件
ehcache.xml
放在resource目錄下<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"> <!-- 磁盤緩存位置 --> <diskStore path="java.io.tmpdir/ehcache"/> <!-- maxEntriesLocalHeap 當前緩存在堆內存上所能保存的最大元素數量 --> <!-- eternal 是否永遠不過期 --> <!-- timeToIdleSeconds 對象空閒時,指對象在多長時間沒有被訪問就會失效。只對eternal爲false的有效。默認值爲0。(單位:秒) --> <!-- timeToLiveSeconds 對象存活時間,指對象從創建到失效所需要的時間。只對eternal爲false的有效。默認值爲0,表示一直可以訪問。(單位:秒)--> <!-- maxEntriesLocalDisk 在磁盤上所能保存的元素的最大數量--> <!-- diskExpiryThreadIntervalSeconds 對象檢測線程運行時間間隔。標識對象狀態的線程多長時間運行一次。默認是120秒。(單位:秒)--> <!-- memoryStoreEvictionPolicy 如果內存中數據超過內存限制,向磁盤緩存定時的策略,默認值爲LRU --> <!-- overflowToDisk 如果內存中數據超過內存限制,是否要緩存到磁盤上 --> <!-- 默認緩存 --> <defaultCache maxEntriesLocalHeap="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" maxEntriesLocalDisk="10000000" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU"> <persistence strategy="localTempSwap"/> </defaultCache> <!-- 自定義緩存 --> <cache name="cache-test" maxElementsInMemory="1000" eternal="false" timeToIdleSeconds="5" timeToLiveSeconds="5" overflowToDisk="false" memoryStoreEvictionPolicy="LRU"/> </ehcache>
-
也可以自己配置文件名和路徑,需要配置
spring.cache.ehcache.config=classpath:config/another-config.xml
-
注意配置文件中cache name需要和
@cacheable
value名字保持一致 -
ehcache比ConcurrenMap的優勢有:
- 可以配置緩存過期時間
- 持久化緩存數據到硬盤
- 等等
Redis
-
pom依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
SpringBoot支持Redis配置
# Redis服務器地址 spring.redis.host=127.0.0.1 # Redis服務器連接端口 spring.redis.port=6379 # Redis服務器連接密碼 spring.redis.password= # 連接池最大連接數(使用負值表示沒有限制) spring.redis.jedis.pool.max-active=20 # 連接池最大阻塞等待時間(使用負值表示沒有限制) spring.redis.jedis.pool.max-wait=-1 # 連接池中的最大空閒連接 spring.redis.jedis.pool.max-idle=10 # 連接池中的最小空閒連接 spring.redis.jedis.pool.min-idle=0 # 連接超時時間(毫秒) spring.redis.timeout=1000
-
默認緩存到Redis的類需要實現
Serializable
接口 -
更細粒度的配置可以通過
@Configuration
@Bean
配置RedisCacheManager
和RedisCacheConfiguration
實現@Configuration public class RedisConfig { // 自定義redis中key生成策略 @Bean public KeyGenerator simpleKeyGenerator() { return (o, method, objects) -> { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(o.getClass().getSimpleName()); stringBuilder.append("."); stringBuilder.append(method.getName()); stringBuilder.append("["); for (Object obj : objects) { stringBuilder.append(obj.toString()); } stringBuilder.append("]"); return stringBuilder.toString(); }; } // 配置redisCacheManager @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("redis-test", this.getRedisCacheConfigurationWithTtl(3000)); redisCacheConfigurationMap.put("redis-test-1", this.getRedisCacheConfigurationWithTtl(18000)); return redisCacheConfigurationMap; } // 使用jackson配置redis 序列化策略 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; }
參考
- http://blog.sina.com.cn/s/blog_4adc4b090102vh1s.html
- https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/integration.html#cache
- https://docs.spring.io/spring-boot/docs/2.3.0.RELEASE/reference/html/spring-boot-features.html#boot-features-caching
- https://www.cnblogs.com/powerwu/articles/9481113.html