前提
使用缓存的好处有:
- 在一个系统中,数据库中经常会有一些不怎么变动的数据,举个栗子:省市信息,不会经常变动。还有一些经过数据库耗时计算得到的结果,也可以存入缓存。使用缓存的主要好处就是减少数据库操作,减轻了数据库压力,提升系统性能。
- 由于用户请求和数据库之间增加了缓存这一层,而缓存数据处于内存中,相比较而言数据库是读取磁盘文件,速度自然比缓存慢。使用缓存大大提高了系统对请求的响应速度,提升用户感知。
- 缓存是支持高并发的主要策略
坏处有:
- 缓存是以空间换时间的策略,大量使用会占用大量内存
- 高并发情况下存在缓存和数据库数据不一致
- 内存容量小,而且比硬盘贵
下面以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