前言
二级缓存架构是用空间和复杂度换取极致的性能,下面是两种方案的对比:
![image]()
- 由图可以看出,二级缓存架构在网络开销, 访问速度上更具优势,但在部署难度和运维成本上更高。
- 二级缓存架构还有一个不得不面临的问题,在分布式环境下如何保证各个服务器之间的缓存同步。
![image]()
- 我们可以看出,如果8080服务器更新了数据,那么只有8080服务器知道更新缓存,8081服务器没有执行业务所以根本不知道数据已经改变,当后续请求如果访问到8081服务器,便会出现缓存与真实数据不一致问题。
如何解决不同节点之间的数据同步问题
高内聚,低耦合
- 出现这种问题的原因在于缓存的删除和服务器耦合太深,只有本机操作了数据才能更新自己的缓存。
- 解决方案便是使用消息队列。用消息队列让删除缓存和服务器解耦,让消息队列去通知服务器删除缓存。
![image]()
实战环节
1 2 3 4
| <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> </dependency>
|
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(); } }
|
业务应用
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; } 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); 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(); log.info("删除redis缓存,key = {}",key); redisCache.delete(key); log.info("通知其他节点删除缓存"); kafkaTemplate.send(TOPIC, key); }
|
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
| consumer: group-id: cache-group-${server.port}
|
总结
- Caffeine + Redis 二级缓存能够突破网络 IO 瓶颈(Caffeine 直接在 JVM 内存中读取,性能比 Redis还快),更高的可用性(如果
Redis 宕机,本地缓存还能支撑一段时间的只读操作,提供一定的降级能力)。 - 但同时也会带来一些列挑战,如数据一致性难题(当数据更新时,需要通知所有应用实例清除自己的本地缓存),内存压力(
每个实例都要存一份数据,如果缓存对象很大或很多,会显著增加 JVM 的堆内存压力,可能导致频繁 GC),预热问题(
节点重启后,本地缓存是空的,瞬间流量会全部打向 Redis 甚至数据库)。