品味Spring Cache设计之美(二)
3 入门例子
首先我们先创建一个工程spring-cache-demo。caffeine和Redisson分别是本地内存和分布式缓存Redis框架中的佼佼者,我们分别演示如何集成它们。
3.1 集成caffeine
3.1.1 maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.7.0</version>
</dependency>
3.1.2 Caffeine缓存配置
我们先创建一个缓存配置类MyCacheConfig。
@Configuration
@EnableCaching
public class MyCacheConfig {
@Bean
public Caffeine caffeineConfig() {
return
Caffeine.newBuilder()
.maximumSize(10000).
expireAfterWrite(60, TimeUnit.MINUTES);
}
@Bean
public CacheManager cacheManager(Caffeine caffeine) {
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
caffeineCacheManager.setCaffeine(caffeine);
return caffeineCacheManager;
}
}
首先创建了一个Caffeine对象,该对象标识本地缓存的最大数量是10000条,每个缓存数据在写入60分钟后失效。
另外,MyCacheConfig类上我们添加了注解:@EnableCaching。
3.1.3 业务代码
根据缓存声明这一节,我们很容易写出如下代码。
@Cacheable(value = "user_cache", unless = "#result == null")
public User getUserById(Long id) {
return userMapper.getUserById(id);
}
@CachePut(value = "user_cache", key = "#user.id", unless = "#result == null")
public User updateUser(User user) {
userMapper.updateUser(user);
return user;
}
@CacheEvict(value = "user_cache", key = "#id")
public void deleteUserById(Long id) {
userMapper.deleteUserById(id);
}
这段代码与硬编码里的代码片段明显精简很多。
当我们在Controller层调用 getUserById方法时,调试的时候,配置mybatis日志级别为DEBUG,方便监控方法是否会缓存。
第一次调用会查询数据库,打印相关日志:
Preparing: select * FROM user t where t.id = ?
Parameters: 1(Long)
Total: 1
第二次调用查询方法的时候,数据库SQL日志就没有出现了, 也就说明缓存生效了。
3.2 集成Redisson
3.2.1 maven依赖
<dependency>
<groupId>org.Redisson</groupId>
<artifactId>Redisson</artifactId>
<version>3.12.0</version>
</dependency>
3.2.2 Redisson缓存配置
@Bean(destroyMethod = "shutdown")
public RedissonClient Redisson() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6201").setPassword("ts112GpO_ay");
return Redisson.create(config);
}
@Bean
CacheManager cacheManager(RedissonClient RedissonClient) {
Map<String, CacheConfig> config = new HashMap<String, CacheConfig>();
// create "user_cache" spring cache with ttl = 24 minutes and maxIdleTime = 12 minutes
config.put("user_cache",
new CacheConfig(
24 * 60 * 1000,
12 * 60 * 1000));
return new RedissonSpringCacheManager(RedissonClient, config);
}
可以看到,从Caffeine切换到Redisson,只需要修改缓存配置类,定义CacheManager 对象即可。而业务代码并不需要改动。
Controller层调用 getUserById方法,用户ID为1的时候,可以从Redis Desktop Manager里看到:用户信息已被缓存,user_cache缓存存储是Hash数据结构。因为Redisson默认的编解码是FstCodec, 可以看到key的名称是:\xF6\x01。
在缓存配置代码里,可以修改编解码器。
public RedissonClient Redisson() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6201").setPassword("ts112GpO_ay");
config.setCodec(new JsonJacksonCodec());
return Redisson.create(config);
}
再次调用 getUserById方法 ,控制台就变成:可以观察到:缓存key已经变成了:["java.lang.Long",1],改变序列化后key和value已发生了变化。
3.3 从列表缓存再次理解缓存抽象
列表缓存在业务中经常会遇到。通常有两种实现形式:
1.整体列表缓存;
2.按照每个条目缓存,通过redis,memcached的聚合查询方法批量获取列表,若缓存没有命中,则从数据库重新加载,并放入缓存里。
那么Spring cache整合Redisson如何缓存列表数据呢?
@Cacheable(value = "user_cache")
public List<User> getUserList(List<Long> idList) {
return userMapper.getUserByIds(idList);
}
执行getUserList方法,参数id列表为:[1,3] 。执行完成之后,控制台里可以看到:列表整体直接被缓存起来,用户列表缓存和用户条目缓存并没有共享,他们是平行的关系。
这种情况下,缓存的颗粒度控制也没有那么细致。
类似这样的思考,很多开发者也向Spring Framework研发团队提过。
官方的回答也很明确:对于缓存抽象来讲,它并不关心方法返回的数据类型,假如是集合,那么也就意味着需要把集合数据在缓存中保存起来。
还有一位开发者,定义了一个@CollectionCacheable注解,并做出了原型,扩展了Spring Cache的列表缓存功能。
@Cacheable("myCache")
public String findById(String id) {
//access DB backend return item
}
@CollectionCacheable("myCache")
public Map<String, String> findByIds(Collection<String> ids) {
//access DB backend,return map of id to item
}
官方也未采纳,因为缓存抽象并不想引入太多的复杂性。
写到这里,相信大家对缓存抽象有了更进一步的理解。当我们想实现更复杂的缓存功能时,需要对Spring Cache做一定程度的扩展。