Caffeine+Redis二级缓存架构+Kafka异步删除缓存简单实战
SryYa Three

前言


二级缓存架构是用空间和复杂度换取极致的性能,下面是两种方案的对比:
image


  • 由图可以看出,二级缓存架构在网络开销访问速度上更具优势,但在部署难度和运维成本上更高。
  • 二级缓存架构还有一个不得不面临的问题,在分布式环境下如何保证各个服务器之间的缓存同步。

image

  • 我们可以看出,如果8080服务器更新了数据,那么只有8080服务器知道更新缓存,8081服务器没有执行业务所以根本不知道数据已经改变,当后续请求如果访问到8081服务器,便会出现缓存与真实数据不一致问题。

如何解决不同节点之间的数据同步问题

高内聚,低耦合

  • 出现这种问题的原因在于缓存的删除和服务器耦合太深,只有本机操作了数据才能更新自己的缓存。
  • 解决方案便是使用消息队列。用消息队列让删除缓存和服务器解耦,让消息队列去通知服务器删除缓存。

image


实战环节

  • 引入Caffeine依赖
1
2
3
4
<dependency>  
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
  • 配置Caffenie缓存
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration  
public class CaffeineConfig {
@Bean
public Cache<String, Object> caffeineCache() {
return Caffeine.newBuilder()
//设置大小
.maximumSize(10000)
//设置数据过期时间
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
}
}

业务应用

  • UserServiceImpl
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
48
49
50
51
52
private static final String TOPIC = "cache-clear-topic";  

private static final String CACHE_PREFIX = "user:"

public User getById(Integer id) {
String key = CACHE_PREFIX + id;

//查本地缓存
User user = (User) caffeineCache.getIfPresent(key);
if (user != null) {
log.info("命中Caffeine缓存,user:{}",user);
return user;
}

//查Redis(本文使用的Redis进行了封装)
user = redisCache.get(key, User.class);
if (user != null) {
log.info("命中Redis缓存,user:{}",user);
//放入本地缓存
caffeineCache.put(key, user);
return user;
}

//查数据库
user = userMapper.selectById(id);
if (user != null) {
log.info("命中MySQL,user:{}",user);
//存入Redis
redisCache.set(key, user, 30);
//存入本地缓存
caffeineCache.put(key, user);
}

return user;
}

public void updateUser(User user) {

//更新数据库
log.info("更新数据库,最新数据:{}",user);
userMapper.updateById(user);

String key = CACHE_PREFIX + user.getId();

//删除Redis
log.info("删除redis缓存,key = {}",key);
redisCache.delete(key);

//通知其他节点清理本地缓存
log.info("通知其他节点删除缓存");
kafkaTemplate.send(TOPIC, key);
}
  • Consumer
1
2
3
4
5
6
@KafkaListener(topics = "cache-clear-topic")  
public void clearLocalCache(String key) {
log.info("本地缓存已删除");
//删除本地缓存
caffeineCache.invalidate(key);
}

注意: 我们要使不同的服务器处于不同的组,如果8080服务器和8081服务器处于一个组内会因为Kafka的轮询策略(
简单来说就是对一个组只分发一次消息)导致异步删除失败。
所以我们应该在Yaml配置文件中使用如下配置

1
2
3
#让组Id和服务端口绑定
consumer:
group-id: cache-group-${server.port}

总结

  • Caffeine + Redis 二级缓存能够突破网络 IO 瓶颈(Caffeine 直接在 JVM 内存中读取,性能比 Redis还快),更高的可用性(如果
    Redis 宕机,本地缓存还能支撑一段时间的只读操作,提供一定的降级能力)。
  • 但同时也会带来一些列挑战,如数据一致性难题(当数据更新时,需要通知所有应用实例清除自己的本地缓存),内存压力(
    每个实例都要存一份数据,如果缓存对象很大或很多,会显著增加 JVM 的堆内存压力,可能导致频繁 GC),预热问题(
    节点重启后,本地缓存是空的,瞬间流量会全部打向 Redis 甚至数据库)。
由 Hexo 驱动 & 主题 Keep
总字数 28.1k 访客数 访问量