性能常识 因为热 Key 和大 Key,Redis 终于被压崩了

爱学习的饲养员 · 2024年09月03日 · 最后由 爱学习的饲养员 回复于 2024年09月04日 · 3751 次阅读

今天分享一下在做压测时遇到的很有意思的性能问题以及对应的排查解决方案,这个性能问题的现象为:Redis 线上实例不可用,Redis 读写均超时。

性能问题排查过程

先来看一下问题代码(Go 语言实现),这段代码的含义为先从 Redis 当中读取数据,如果 Redis 里没有数据,则访问 DB 获取数据,获取到数据后再 Set Redis 缓存,便于下次访问直接从 Redis 获取数据,减轻数据库压力。

func GetInfoByCache(key string, expired int) interface{} {      
        strData, errCache := GetCacheData(ctx, key, expired)
        if errCache == nil {
            return strData
        }

        ret := GetInfoFromOther(params)  //访问数据库

        strJson, errJson := jsoniter.MarshalToString(ret[0].Interface())
        if errJson == nil {
            errSet := SetCacheData(ctx, key, strJson)
            if errSet != nil {
                ctx.Warning(errSet)
            }
        }
        return ret
}

//调用该方法,传参当中的Redis Key只有一个,为固定值
res:=GetInfoByCache("Redis_Key_Name",60)

问题产生原因

如果是熟悉编程的小伙伴,应该知道上述业务逻辑是运用 Redis 缓存很基本的操作,即使是在高并发情况下,Redis 实例一般也能扛住,那问题到底出在哪里呢?

有两个前置条件

第一个前置条件,调用 GetInfoByCache 方法时,使用到的 Redis Key 是一个大 Key。

  • 在 Redis 中,"大 Key"通常指的是存储在数据库中的一个占用相对较大内存空间的键值对。当一个键值对的值非常大时,它可能会被称为大 Key。这可能会对 Redis 服务器的内存占用过大、影响性能等负面影响,因此需要谨慎处理。
  • Redis 大 Key 不是指存储在 Redis 中的某个 Key(键)的大小超过一定的阈值,而是该 Key(键)所对应的 value(值)过大。

另外一个前置条件是,这个 Redis 的 Key 为固定值,在高并发条件下会成为一个热 Key。

  • Redis 的"热 Key"(Hot Key)指的是在一个 Redis 数据库中,某些特定的键(Key)被频繁地访问或者执行操作,导致这些键成为数据库中的热点。这通常是因为这些键存储了特别热门的数据,被大量的读取或写入操作所影响。

热 Key 可能对 Redis 的性能和稳定性产生负面影响,因为它们引起了数据库中的高并发访问。当某个 Key 变得热门时,可能会导致以下一些问题:

  • 性能瓶颈:大量的读取或写入操作集中在一个热 Key 上,导致该 Key 所在的分片或节点成为性能瓶颈,因为它承受了大量的请求负担。
  • 响应延迟:其他未热的 Key 可能因为竞争数据库资源而经历响应延迟,影响整体性能。
  • 内存占用:热 Key 可能占用大量内存,导致 Redis 实例的内存使用率升高。如果 Redis 的内存用尽,可能导致 LRU(最近最少使用)策略被触发,清除部分数据,进而影响业务。

在进行压测时,在 Redis 大 Key 和热 Key 的加持下,压测到达指定的 QPS 就会发生下面的性能问题:
首先读取 Redis 开始出现失败,读 Redis 失败必然会进行访问数据库,并写入 Redis,但写 Redis 又是写大 Key,写入超时失败,再次影响 Redis 读请求,越来越多的 Redis 读请求失败,最终造成 Redis 的实例都不可用。

解决方案

跟开发讨论后,制定了以下 6 种问题的解决方案,权衡成本和风险,最终采用了第 2 种方案:
将 Redis 的 Key 进行打散,这样就能解决 Redis 热 Key 的问题,Redis 读写请求可以落到不同的 Redis 分片上,进而解决了 Redis 实例不可用的性能问题。

共收到 2 条回复 时间 点赞

有个问题可以探讨一下,在 strJson, errJson := jsoniter.MarshalToString(ret[0].Interface()) 这一步,我猜测是获取到数据库里的数据并且序列化成 strJson 了对吧,后面就是把 strJson 写 redis,为何在这里不直接返回 strJson,然后异步去写 redis,这样改动量其实也挺小的,风险的话可能就是 redis 节点异常期间数据库的压力稍微大一点

zhoudian 回复

是的,也可以这样优化,但是会对数据库产生压力而且效果也不会太明显

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册