一、配置
1.配置redis
默认是有配置redis的情况会使用redis作为缓存。
2.配置Cache
@Configuration
// 使用redis作为cache的缓存,前提是要配置redis
@ConditionalOnBean(RedisConfiguration.class)
public class CacheConfiguration {
@Resource
private RedisConnectionFactory factory;
@Resource
private RedisCacheProperties redisCacheProperties;
@Bean
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
};
}
@Bean
public CacheResolver cacheResolver() {
return new SimpleCacheResolver(cacheManager());
}
@Bean
public CacheErrorHandler errorHandler() {
// 用于捕获从Cache中进行CRUD时的异常的回调处理器。
return new SimpleCacheErrorHandler();
}
@Bean
public CacheManager cacheManager() {
final Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
//POJO无public的属性或方法时,不报错
om.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// null值字段不显示
om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 序列化JSON串时,在值上打印出对象类型
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
// 解决jackson2无法反序列化LocalDateTime的问题
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 过期时间
.entryTtl(Duration.ofMillis(redisCacheProperties.getExpireDefaultMillis()))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
final RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.builder(factory)
.cacheDefaults(config);
// 为每个key配置不同的过期时间
final Map<String, Long> expireMap = redisCacheProperties.getExpireMapMillis();
if (CollUtil.isNotEmpty(expireMap)) {
final ConcurrentHashMap<String, RedisCacheConfiguration> configMap = new ConcurrentHashMap<>();
final Set<String> cacheNames = expireMap.keySet();
expireMap.entrySet().forEach(e ->
configMap.put(e.getKey(), config.entryTtl(Duration.ofMillis(e.getValue())))
);
builder.initialCacheNames(cacheNames)
.withInitialCacheConfigurations(configMap);
}
return builder.build();
}
}
需要用到的配置类:
@Getter
@Setter
@NoArgsConstructor
@Component
@ConfigurationProperties(prefix = "redis-cache")
public class RedisCacheProperties {
/**
* 缓存默认过期时间,单位毫秒
*/
private long expireDefaultMillis = 86400000;
/**
* 缓存针对不同的key设置不同过期时间,单位毫秒
*/
private Map<String, Long> expireMapMillis;
}
yaml对应的配置:
##cache缓存设置
redis-cache:
# 默认超时时间,单位毫秒
expire-default-millis: 86400000
# 自定义超时时间map,单位毫秒
expire-map-millis:
mapTestCache: 6000
3.将Cache配置放入starter
将这些配置放入新的maven工程里面(即starter工程),如果需要此功能只需要引入这个starter工程即可。
starter工程中增加如下两个文件:
- 增加注解定义,在项目的启动类中增加此注解即可扫描到starter工程的包,并注入到spring:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(RootConfiguration.class)
public @interface EnableXXConfiguration {
}
- 增加类即上面的@Import(RootConfiguration.class),并增加包的扫描位置:
@Configuration
@ComponentScan("com.xx.xxx")
public class RootConfiguration {
}
二、使用
使用前启动类需要加注解:@EnableCaching
使用注解:
- @Cacheable:触发缓存写入。
- @CacheEvict:触发缓存清除。
- @CachePut:更新缓存(不会影响到方法的运行)。
- @Caching:重新组合要应用于方法的多个缓存操作。
- @CacheConfig:设置类级别上共享的一些常见缓存设置。
示例代码:
@Service
@Slf4j
public class CacheTestService {
@Cacheable(cacheNames = "mapTestCache", key = "#mapId")
public MapEntity findMapEntity(String mapId) {
log.info("进入数据库查询。。。");
return new MapEntity()
.setMapName("测试地图cache")
.setMapDescription("")
.setDel(1)
.setUpdateTime(System.currentTimeMillis());
}
@CachePut(cacheNames = "mapTestCache", key = "#map.mapId")
public MapEntity updateEntity(MapEntity map) {
log.info("进入数据库更新。。。");
return map;
}
@CacheEvict(cacheNames = "mapTestCache", key = "#mapId")
public void deleteEntity(String mapId) {
log.info("进入数据库删除。。。");
}
}
测试:
@Test
public void testCacheFind() {
final MapEntity mapEntity = cacheTestService.findMapEntity("110");
log.info("查出来数据:{}", JSONUtil.toJsonStr(mapEntity));
Assert.assertTrue(true);
}
@Test
public void testCacheUpdate() {
final MapEntity mapEntity = new MapEntity().setMapName("test更新。。").setMapId("110");
cacheTestService.updateEntity(mapEntity);
Assert.assertTrue(true);
}
@Test
public void testCacheDelete() {
cacheTestService.deleteEntity("110");
Assert.assertTrue(true);
}
三、Spring-Cache的不足
1)读模式
- 缓存穿透:查询一个null数据。解决:缓存空数据:cache-null-values=true;
- 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁,sync=true,只有@Cacheable有;
- 缓存雪崩:大量的key同时过期。解决:加随机时间。
2)写模式
如何保证缓存数据库一致性:
- 读写加锁,有序进行;(写少的情况,不然一直在等待)
- 引入canal,感知到MySQL的更新去更新数据库;
- 读多写多,直接去数据库查询就行;
总结
- 常规数据(读多写少,即时性,一致性要求不高的数据):完全可以使用Spring-Cache;写模式(只要缓存的数据有过期时间就足够)
- 特殊数据:特殊设计;