Redis作为常用的缓存解决方案,其性能和稳定性至关重要。然而,在高并发场景下,Redis可能会遇到热点Key问题,即大量请求集中在同一个Key上,导致缓存击穿,影响数据库服务,甚至拖垮整个应用。本文将详细解析热点Key问题的原理、如何发现热点Key以及如何通过多级缓存策略解决这一问题。
一、热点Key问题的原理
热点Key问题通常发生在读多写少的场景,如热点新闻、热点评论、明星直播等。在这些场景下,大量请求会集中访问Redis中的同一个Key,这不仅会增加Redis的负载,还可能导致缓存击穿,使得请求直接涌向数据库,最终影响应用服务的正常运行。
Redis单节点的查询性能一般在2万QPS,因此对单个固定Key的查询不能超过这个数值。在服务端读取数据并进行分片切分时,如果对某个Key的访问量超过了该节点Server的承受极限,热点Key问题就会出现。
二、如何发现热点Key
发现热点Key的方法有多种:
-
业务经验预估:通过分析历史活动数据,预估可能的热点Key。
-
业务侧监控收集:在操作Redis前添加代码进行数据统计并异步上报。
-
使用Redis自带命令:
-
monitor命令:实时抓取Redis服务器接收的命令,通过代码统计热Key。
-
hotkeys参数:Redis 4.0.3提供此功能,通过scan + object freq实现。
-
-
客户端收集:在操作Redis前添加统计Redis键值查询频次的逻辑。
-
代理层收集:利用代理服务在请求Redis前统一收集Redis热Key数据。
三、热点Key的危害
-
流量集中超网卡上限:热点Key请求过多,超过主机网卡流量上限,影响其他服务。
-
打垮缓存分片服务:热点Key查询超阈值会占用大量CPU资源,降低整体性能,严重时导致缓存分片服务崩溃。
-
集群访问倾斜:在集群架构下,某个数据分片被大量访问,其他分片空闲,可能导致该分片连接数耗尽,新连接请求被拒。
-
DB击穿与业务雪崩:热Key请求超Redis承受能力致缓存击穿,缓存失效时大量请求直抵DB层,DB性能弱,易引发雪崩,影响业务。
四、解决方案
(一)多级缓存策略
多级缓存策略是解决热点Key问题的有效方法。通过在客户端浏览器、就近CDN、Redis等缓存框架以及服务器本地进行缓存,形成二级、三级等多级缓存,目的是尽量缩短用户访问链路长度。
(二)本地缓存
使用本地缓存,如利用Ehcache、GuavaCache、Caffeine等,甚至是一个HashMap都可以。在发现热点Key以后,把热点Key加载到系统的JVM中,针对这种热点Key请求,会直接从本地存中取,而不会直接请求Redis。
本地缓存天然的将同一个Key的大量请求,根据网络层的负载均衡,均匀分散到了不同的机器节点上,避免了对固定Key全部打到单个Redis节点的情况,并且减少了1次网络交互。当然,使用本地缓存不可避免的遇到的问题就是,对于要求缓存强一致性的业务来说,需要花费更多的精力在保证分布式缓存一致性上,会增加系统的复杂度。
(三)热Key备份
该方案旨在缓解Redis单点热Key查询压力,具体做法是在多个Redis节点上备份热Key,避免固定Key总是访问同一节点。通过在初始化时为Key拼接0-2N之间的随机尾缀,便生成的备份Key分散在各个节点上。在有热Key请求时,随机选取一个备份Key所在的节点进行访问取值,这样读写操作就不会集中于单个节点,从而有效减轻了单个Redis节点的负担,提升系统应对热Key问题的能力。
(四)热Key拆分
将热Key拆分成多个带后缀名的Key,分散存储到多个实例中。客户端请求时按规则算出固定Key,使多次请求分散到不同节点。以“某抖音热搜”为例,拆分成多个带编号后缀的Key存储在不同节点,用户查询时根据用户ID算出下标访问对应节点。
虽用户可能只能获取部分数据,比如抖音中对于热点相关视频,可将其分散存储在不同节点并推送给不同用户,待热点降温后再汇总数据,挑选优质内容重新推送未收到的用户。此方法可缓解热Key集中访问压力,提升系统性能和用户体验。
(五)核心业务隔离
Redis单点查询性能有局限,当热点Key查询量超节点性能阈值,会致使缓存分片服务崩溃,该节点上所有业务的Redis读写均无法使用。
为避免热点Key问题波及核心业务,应提前做好核心与非核心业务的Redis隔离,至少要将存在热点Key的Redis集群与核心业务隔离开,如此可保障核心业务不受热点Key引发的问题影响,确保核心业务的稳定性和可用性,提升系统整体的可靠性和容错能力。
五、手写多级缓存框架
为了实现多级缓存策略,我们可以手写一个简单的多级缓存框架。以下是一个基于Spring Boot和JSR107规范的多级缓存框架实现示例。
(一)自定义配置
通过自定义配置类,我们可以灵活地配置Caffeine和Redis的缓存策略。
@Data
public class CaffeineConfigProp {
private Duration expireAfterAccess;
private Duration expireAfterWrite;
private Duration refreshAfterWrite;
private int initialCapacity;
private long maximumSize;
private CaffeineStrength keyStrength;
private CaffeineStrength valueStrength;
}
@Data
public class RedisConfigProp {
private Duration defaultExpiration = Duration.ZERO;
private Duration defaultNullValuesExpiration = null;
private Map<String, Duration> expires = new HashMap<>();
private String topic = "cache:redis:caffeine:topic";
}
@ConfigurationProperties(prefix="spring.cache.multi")
public class CacheConfigProperties {
private Set<String> cacheNames = new HashSet<>();
private boolean cacheNullValues = true;
private boolean dynamic = true;
private String cachePrefix;
private RedisConfigProp redis = new RedisConfigProp();
private CaffeineConfigProp caffeine = new CaffeineConfigProp();
}
(二)CacheManager实现
实现CacheManager接口,通过配置生成多级缓存的Bean。
@Slf4j
public class RedisCaffeineCacheManager implements CacheManager {
private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>();
private CacheConfigProperties cacheConfigProperties;
private RedisTemplate<Object, Object> stringKeyRedisTemplate;
private boolean dynamic;
private Set<String> cacheNames;
public RedisCaffeineCacheManager(CacheConfigProperties cacheConfigProperties,
RedisTemplate<Object, Object> stringKeyRedisTemplate) {
super();
this.cacheConfigProperties = cacheConfigProperties;
this.stringKeyRedisTemplate = stringKeyRedisTemplate;
this.dynamic = cacheConfigProperties.isDynamic();
this.cacheNames = cacheConfigProperties.getCacheNames();
}
@Override
public Cache getCache(String name) {
Cache cache = cacheMap.get(name);
if (cache != null) {
return cache;
}
if (!dynamic && !cacheNames.contains(name)) {
return null;
}
cache = new RedisCaffeineCache(name, stringKeyRedisTemplate, caffeineCache(), cacheConfigProperties);
Cache oldCache = cacheMap.putIfAbsent(name, cache);
return oldCache == null ? cache : oldCache;
}
// 生成caffeine缓存实例
public com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache() {
Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();
doIfPresent(cacheConfigProperties.getCaffeine().getExpireAfterAccess(), cacheBuilder::expireAfterAccess);
doIfPresent(cacheConfigProperties.getCaffeine().getExpireAfterWrite(), cacheBuilder::expireAfterWrite);
doIfPresent(cacheConfigProperties.getCaffeine().getRefreshAfterWrite(), cacheBuilder::refreshAfterWrite);
if (cacheConfigProperties.getCaffeine().getInitialCapacity() > 0) {
cacheBuilder.initialCapacity(cacheConfigProperties.getCaffeine().getInitialCapacity());
}
if (cacheConfigProperties.getCaffeine().getMaximumSize() > 0) {
cacheBuilder.maximumSize(cacheConfigProperties.getCaffeine().getMaximumSize());
}
return cacheBuilder.build();
}
@Override
public Collection<String> getCacheNames() {
return this.cacheNames;
}
public void clearLocal(String cacheName, Object key) {
Cache cache = cacheMap.get(cacheName);
if (cache == null) {
return;
}
RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache;
redisCaffeineCache.clearLocal(key);
}
}
(三)Cache实现
实现Cache接口,定义如何具体操作缓存数据。
public class RedisCaffeineCache extends AbstractValueAdaptingCache {
protected RedisCaffeineCache(boolean allowNullValues) {
super(allowNullValues);
}
@Override
protected Object lookup(Object o) {
// 在Caffeine缓存中查找
Object caffeineValue = caffeineCache.getIfPresent(o);
if (caffeineValue != null) {
return caffeineValue;
}
// 在Redis缓存中查找
Object redisValue = stringKeyRedisTemplate.opsForValue().get(o);
if (redisValue != null) {
// 将Redis中的值同步到Caffeine缓存中
caffeineCache.put(o, redisValue);
return redisValue;
}
return null;
}
// 后续等继承方法省略
}
(四)配置类
通过配置类将RedisCaffeineCacheManager注册为Bean。
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@EnableConfigurationProperties(CacheConfigProperties.class)
public class MultilevelCacheAutoConfiguration {
@Bean
@ConditionalOnBean(RedisTemplate.class)
public RedisCaffeineCacheManager cacheManager(CacheConfigProperties cacheConfigProperties,
@Qualifier("stringKeyRedisTemplate") RedisTemplate<Object, Object> stringKeyRedisTemplate) {
return new RedisCaffeineCacheManager(cacheConfigProperties, stringKeyRedisTemplate);
}
@Bean
@ConditionalOnMissingBean(name = "stringKeyRedisTemplate")
public RedisTemplate<Object, Object> stringKeyRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
@Bean
public RedisMessageListenerContainer cacheMessageListenerContainer(CacheConfigProperties cacheConfigProperties,
@Qualifier("stringKeyRedisTemplate") RedisTemplate<Object, Object> stringKeyRedisTemplate,
RedisCaffeineCacheManager redisCaffeineCacheManager) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(stringKeyRedisTemplate.getConnectionFactory());
CacheMessageListener cacheMessageListener = new CacheMessageListener(redisCaffeineCacheManager);
redisMessageListenerContainer.addMessageListener(cacheMessageListener,
new ChannelTopic(cacheConfigProperties.getRedis().getTopic()));
return redisMessageListenerContainer;
}
}
通过以上步骤,我们可以实现一个简单的多级缓存框架,该框架能够自动发现热点Key并将热点缓存访问请求前置在应用层本地缓存,从而有效解决热点Key问题。同时,通过Spring Boot Actuator的监控功能,我们可以实时监控缓存的运行状态和性能指标,确保系统的稳定性和可靠性。
标签:缓存,cacheConfigProperties,解决方案,Redis,private,Key,热点 From: https://blog.csdn.net/m0_73355421/article/details/145241091