首页 > 数据库 >Redis中的热点Key问题及解决方案

Redis中的热点Key问题及解决方案

时间:2025-01-19 11:57:49浏览次数:3  
标签:缓存 cacheConfigProperties 解决方案 Redis private Key 热点

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

相关文章

  • 大型集团SRM供应商管理如何设计?制造集团SRM供应商关系管理项目解决方案,项目功能需求清
    采购方业务设计采购需求明确前期采购计划,涵盖战略采购项目。建立工业品电子目录,便于快速查找与选择。运营采购商城电子目录,提升采购效率与透明度。采购询源发起询源需求,收集供应商信息。实施询报价流程,确保价格合理性与竞争力。规范化招投标流程,保证公平公正。引入电子竞价......
  • 查询缓存击穿解决方案
    查询缓存击穿解决方案在现代分布式系统中,缓存技术是提高系统性能和降低数据库负载的重要手段。然而,当缓存中的数据过期或不可用时,可能会发生缓存击穿(CacheBreakdown)。缓存击穿会导致大量的请求直接访问数据库,从而给数据库带来较大的压力,甚至可能导致数据库崩溃。为了应对......
  • Redis 入门教程:什么是 Redis?如何开始使用?
    Redis入门教程:什么是Redis?如何开始使用?Redis是一个开源的内存数据结构存储系统,广泛用于缓存、消息队列、实时数据处理等场景。它不仅速度快,而且支持多种数据结构(如字符串、哈希、列表、集合等),因此非常适合处理大量实时数据。今天,我们将带你一起快速了解Redis,并教你如何上......
  • Redis 深度解析:从基础到进阶,全面掌握高效缓存技术
    Redis深度解析:从基础到进阶,全面掌握高效缓存技术引言:Redis作为现代开发中不可或缺的技术之一Redis(RemoteDictionaryServer)作为一种开源的高性能键值数据库,在实际开发中发挥着至关重要的作用。它以其极高的读写性能、丰富的数据结构、持久化机制以及支持多种编程语言的客......
  • 【鱼皮大佬API开放平台项目】Spring Cloud Gateway HTTPS 配置问题解决方案总结
    问题背景项目架构为前后端分离的微服务架构:前端部署在8000端口API网关部署在9000端口后端服务包括:api-backend(9001端口)api-interface(9002端口)初始状态:前端已配置HTTPS(端口8000)后端服务未配置HTTPS通过Nginx进行反向代理遇到的问题第一阶段:400Ba......
  • JS上传文件夹的三种解决方案
    要求:免费,开源,技术支持技术:百度webuploader,分块,切片,断点续传,秒传,MD5验证,纯JS实现,支持第三方软件集成前端:vue2,vue3,vue-cli,html5,webuploader后端:asp.net,.netmvc,.netcore,asp,jsp,java,springboot,php,数据库:MySQL,Oracle,SQLServer,达梦,人大金仓,国产数据库平......
  • 在 .NET Core中如何使用 Redis 创建分布式锁
    在.NETCoreWebApi中使用Redis创建分布式锁可以通过StackExchange.Redis库来实现。分布式锁用于确保在分布式系统中,同一时间只有一个进程可以执行某段代码。1.场景描述在支付系统中,可能会出现以下并发问题:用户同时发起多次支付请求,导致重复扣款。多个请求同时处理同......
  • Redis实训:社交关注关系存储任务
    一、实验目的1. 理解Redis的安装、配置及基本操作。2. 掌握Redis的不同数据类型及相应操作方法。3. 学习使用Java客户端连接Redis,并进行数据操作。4. 实践使用Redis存储社交关注关系的功能。二、实验环境准备1. JAVA环境准备:确保JavaDevelopmentKit(JDK)已......
  • 【linux合集】redis集群部署
    集群式部署redis介绍:三台机器001、002、003然后再三台机器上面都部署redis_6379、redis_6380做主从,然后三台机器上面做集群1、安装/解压redismkdir-p/data/applications/wgethttps://download.redis.io/releases/redis-5.0.14.tar.gztar-xzf./redis-5.0.14.tar.gzcd......
  • 自主研发驱动程序不仅能为硬件设备提供定制化支持,提升性能和稳定性,还能够增强企业的技
    自主研发驱动程序是指由企业或组织自行设计、开发并实现的硬件设备驱动程序。驱动程序(DeviceDriver)是操作系统和硬件之间的桥梁,用于控制硬件设备的功能和提供操作系统与硬件设备间的通信接口。自主研发驱动程序通常是为了满足特定硬件或操作环境的需求,提升硬件的性能、兼容性和稳......