惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
D
Docker
腾讯CDC
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏
The Cloudflare Blog
Martin Fowler
Martin Fowler
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
博客园 - 三生石上(FineUI控件)
Recent Announcements
Recent Announcements
F
Fortinet All Blogs
IT之家
IT之家
WordPress大学
WordPress大学
M
MIT News - Artificial intelligence
爱范儿
爱范儿
Microsoft Azure Blog
Microsoft Azure Blog
Vercel News
Vercel News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
小众软件
小众软件
N
Netflix TechBlog - Medium
T
Tailwind CSS Blog
Engineering at Meta
Engineering at Meta
博客园 - 【当耐特】

祈雨的笔记

安全多方计算MPC spark原理解析 kueue执行源码分析 spark on k8s执行源码分析 spark-operator源码解析 系统压测遇到的缓存击穿问题 我的世界PC与安卓联机 蚂蚁金服流量投放平台的AIG改造 G1大对象致Old区占用率高 日志打印导致接口响应率下跌分析 Groovy加载类导致OOM分析 ERROR日志打印导致CPU满载 记OceanBase死锁超时 应用发版期间服务响应超时 Ark Serverless初探 系统优化复盘一二三 The user specified as a definer does not exist Kong网关初探 API网关选型调研 CPU火焰图常用工具 配置中心选型调研 root操作Nginx导致用户组错误 基于Proxifier使用代理 FastJSON字段智能匹配踩坑 Nacos初探 记一次Nginx服务器CPU满荷载故障 基于券系统分库分表的思考 limit不参与SQL成本计算致索引失效 Linux常用性能监控命令 golang低版本http2偶现400
springboot(27)自定义缓存读写机制CachingConfigurerSupport
祈雨的笔记 · 2019-04-14 · via 祈雨的笔记

概述

缓存在springboot项目中很常见,分布式项目中最常见的缓存机制就是通过redis缓存mybatis的查询数据,如下示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

@Bean
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisSerializationContext.SerializationPair serializationPair =
RedisSerializationContext.SerializationPair.fromSerializer(getRedisSerializer());
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(30))
.serializeValuesWith(serializationPair);
return RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}

private RedisSerializer<Object> getRedisSerializer(){
return new GenericFastJsonRedisSerializer();
}

}
1
2
3
4
5
public interface UserMapper {

@Cacheable(cacheNames = "User:Id",key="#p0")
public User findById(@Param("id") Integer id);
}

上述代码的作用,是在调用findById方法时优先查询redis中的缓存数据。如果redis对应缓存不存在,则请求mysql查询数据,并将数据缓存到redis中,设置缓存的过期时间为30秒。

问题

示例代码简单明了,但是有两个问题:

  1. 当redis连接出现异常时,调用findById方法会抛出异常影响到正常的业务流程;
  2. 扩展性差,不能实现多层缓存,无法灵活切换多种缓存中间件(在@Cacheable中指定cacheManager只能实现一个方法固定使用一种缓存机制);

CacheErrorHandler

缓存仅仅是为了业务更快地查询而存在的,如果因为缓存操作失败导致正常的业务流程失败,有点得不偿失了。因此需要开发者自定义CacheErrorHandler处理缓存读写的异常。

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



public class IgnoreExceptionCacheErrorHandler implements CacheErrorHandler {

private static final Logger log = LoggerFactory.getLogger(IgnoreExceptionCacheErrorHandler.class);

@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
log.error(exception.getMessage(), exception);
}

@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
log.error(exception.getMessage(), exception);
}

@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
log.error(exception.getMessage(), exception);
}

@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
log.error(exception.getMessage(), exception);
}
}
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
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {





@Override
public CacheErrorHandler errorHandler() {
return new IgnoreExceptionCacheErrorHandler();
}

@Bean
public CacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisSerializationContext.SerializationPair serializationPair =
RedisSerializationContext.SerializationPair.fromSerializer(getRedisSerializer());
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(30))
.serializeValuesWith(serializationPair);
return RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}

private RedisSerializer<Object> getRedisSerializer(){
return new GenericFastJsonRedisSerializer();
}

}

缓存读写发生了异常,如果是读取redis异常,上述代码会导致调用findById读取缓存的值为空,从而继续从mysql读取数据,对业务没有影响。但是如果请求量很大就会出现缓存雪崩的问题,大量的查询请求发送到mysql导致mysql负载过大而阻塞甚至宕机,建议使用多层缓存兜底。

如果缓存写发生了异常,就可能导致mysql的数据和redis缓存的数据不一致的问题。为了解决该问题,需要继续扩展CacheErrorHandlerhandleCachePutErrorhandleCacheEvictError方法,思路就是将redis写操作失败的key保存下来,通过重试任务删除这些key对应的redis缓存解决mysql数据与redis缓存数据不一致的问题。

CacheResolver

开发者可以通过自定义CacheResolver实现动态选择CacheManager,如下通过代码实现对findById调用时使用多种缓存机制:优先从堆内存读取缓存,堆内存缓存不存在时再从redis读取缓存,redis缓存不存在时最后从mysql读取数据,并将读取到的数据依次写到redis和堆内存中。

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
45
46
47
public class CustomCacheResolver implements CacheResolver, InitializingBean {

@Nullable
private List<CacheManager> cacheManagerList;

public CustomCacheResolver(){
}
public CustomCacheResolver(List<CacheManager> cacheManagerList){
this.cacheManagerList = cacheManagerList;
}

public void setCacheManagerList(@Nullable List<CacheManager> cacheManagerList) {
this.cacheManagerList = cacheManagerList;
}
public List<CacheManager> getCacheManagerList() {
return cacheManagerList;
}

@Override
public void afterPropertiesSet() {
Assert.notNull(this.cacheManagerList, "CacheManager is required");
}

@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
Collection<String> cacheNames = getCacheNames(context);
if (cacheNames == null) {
return Collections.emptyList();
}
Collection<Cache> result = new ArrayList<>();
for(CacheManager cacheManager : getCacheManagerList()){
for (String cacheName : cacheNames) {
Cache cache = cacheManager.getCache(cacheName);
if (cache == null) {
throw new IllegalArgumentException("Cannot find cache named '" +
cacheName + "' for " + context.getOperation());
}
result.add(cache);
}
}
return result;
}

private Collection<String> getCacheNames(CacheOperationInvocationContext<?> context){
return context.getOperation().getCacheNames();
}
}
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
45
46
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

@Autowired
private RedisConnectionFactory connectionFactory;

@Override
public CacheResolver cacheResolver() {

CacheManager guavaCacheManager = new GuavaCacheManager();
CacheManager redisCacheManager = redisCacheManager();
List<CacheManager> list = new ArrayList<>();

list.add(concurrentMapCacheManager);

list.add(redisCacheManager);
return new CustomCacheResolver(list);
}





@Override
public CacheErrorHandler errorHandler() {
return new IgnoreExceptionCacheErrorHandler();
}

@Bean
public CacheManager redisCacheManager() {
RedisSerializationContext.SerializationPair serializationPair =
RedisSerializationContext.SerializationPair.fromSerializer(getRedisSerializer());
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(30))
.serializeValuesWith(serializationPair);
return RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}

private RedisSerializer<Object> getRedisSerializer(){
return new GenericFastJsonRedisSerializer();
}

}

通过自定义CacheResolver开发者可以实现更多的自定义功能,例如热点缓存自动升降级的场景:

项目大多数情况下只使用redis做缓存,当某些场景下个别数据成为了热数据,通过例如storm实时统计出热数据后,项目将这些热数据缓存到堆内存,缓解网络和redis的负载压力。

这种场景完全可以通过自定义CacheResolver来实现,storm实时统计出热数据,自定义的CacheResolver在调用resolveCaches选择CacheManager前,先判断此次读写的缓存key是否是热数据。如果是热数据则使用堆内存的CacheManager,否则使用redis的CacheManager