缓存就是数据交换的缓冲区(称作Cache),目的就是提高我们的接口性能,特别是那些需要大量CPU计算和I/O获取的数据。
使用缓存带来的问题缓存虽然能够提高应用程序的性能,但也会带来一些问题。比如:缓存失效,缓存击穿,缓存雪崩,数据一致性问题
缓存雪崩缓存失效为什么会带来问题呢?试想一下,单个的缓存失效其实并不会引发多大的问题,问题在于当大量的Key同时失效时,在高并发的情况下,大量的请求同时到数据库层,会给数据库层带来压力,从而引发其他的问题。
解决方案 优化过期时间既然是同时失效,那么我们只需要在Key的失效时间上再加上一个随机时间就好了,也就是失效时间 + 随机时间。go-zero 上已经有相关的代码,我简单摘抄出来看下
// A Unstable is used to generate random value around the mean value base on given deviation.
type Unstable struct {
deviation float64
r *rand.Rand
lock *sync.Mutex
}
// AroundDuration returns a random duration with given base and deviation.
func (u Unstable) AroundDuration(base time.Duration) time.Duration {
u.lock.Lock()
val := time.Duration((1 + u.deviation - 2*u.deviation*u.r.Float64()) * float64(base))
u.lock.Unlock()
return val
}
优化缓存
采用多级缓存,不同级别缓存设置的超时时间不同,及时某个级别缓存都过期,也有其他级别缓存兜底。代码如下,完整代码见:cache_redis.go
func (r *RedisCacheClient) Get(ctx context.Context, key string, fetch fetchFunc) (result []byte, err error) {
var byteValue []byte
fullKey := getFullKey(r.prefix, key)
fullKeyByte, _ := json.Marshal(fullKey)
if val, err := r.localCache.Get(fullKeyByte); err == nil {
r.status.IncrementLocalCacheHit()
return val, nil
}
r.status.IncrementLocalCacheMiss()
startTime := time.Now()
byteValue, err = r.client.Get(fullKey).Bytes()
elapsed := time.Since(startTime).Milliseconds()
for _, p := range r.plugins {
p.OnGetRequestEnd(ctx, cmdGet, elapsed, fullKey, err)
}
// 数据源拉取原始数据
........
}
缓存击穿
对于某些key设置了过期时间,但是其是热点数据,如果某个key失效,可能大量的请求打过来,缓存未命中,然后去数据库访问,此时数据库访问量会急剧增加。
解决方案 多级缓存+singleflight我们可以设置多级缓存,每一级缓存失效时间不一样,某个级别缓存过期,也有其他级别缓存兜底。而且再加上singleflight 限制,就可以做每一个服务实例只有一个请求最终到数据库源上,大大减轻了数据源压力
缓存穿透缓存穿透是指查询的数据在数据库是没有的,那么在缓存中自然也没有,所以,在缓存中查不到就会去数据库取查询,这样的请求一多,那么我们的数据库的压力自然会增大。
解决方案 设置Null值- 约定:对于返回为Null的依然缓存,对于抛出异常的返回不进行缓存,注意不要把抛异常的也给缓存了。采用这种手段的会增加我们缓存的维护成本,需要在插入缓存的时候删除这个空缓存,当然我们可以通过设置较短的超时时间来解决这个问题。
- 小数据用BitMap,大数据可以用布隆过滤器
我们通常说的数据一致性指的是在程序运行过程中本地缓存、分布式缓存、mysql数据库三者之间的数据一致性
本地缓存与DB保持一致 解决方案 MQ 方案- 应用实例1收到请求,更新 db,同时更新应用自己的本地缓存.
- 应用实例1 发送更新 mq 广播消息.
- 应用 实例2 和应用实例3 收到消息,查询 db,更新本地缓存.
- 这个时候应用实例1,2,3与 DB 数据就保持一致
- 更新 db 数据
- 监听 mysql binlog, 并写入到MQ
- 启动一个数据处理应用,消费 MQ 数据并进行数据加工
- 将加工后的数据写入 redis
- 查询 redis 数据返回
先进行缓存清除,再执行 update sql,最后(延迟 N 秒)再执行缓存清除。
上述中(延迟 N 秒)的时间要大于一次写 *** 作的时间,一般为 3-5 秒。
1.更新 db 数据
2.启动一个定时任务定时将 db 数据同步到 redis
热key是服务端的常见问题,指一段时间内某个key的访问量远远超过其他的key,导致大量访问流量落在某一个redis实例中;或者是带宽使用率集中在特定的key
以被请求频率来定义是否是热key,没有固定经验值。某个key被高频访问导致系统稳定性变差,都可以定义为热key。
可能造成的问题
- 占用大量的CPU资源,影响其他请求并导致整体性能降低。
- 集群架构下,产生访问倾斜,即某个数据分片被大量访问,而其他数据分片处于空闲状态,可能引起该数据分片的连接数被耗尽,新的连接建立请求被拒绝等问题。
- 在抢购或秒杀场景下,可能因商品对应库存Key的请求量过大,超出Redis处理能力造成超卖。
- 热Key的请求压力数量超出Redis的承受能力易造成缓存击穿,即大量请求将被直接指向后端的存储层,导致存储访问量激增甚至宕机,从而影响其他业务。
提供单独的热 key 检测的接入 sdk,应用系统引入该 sdk 后,热 key 检测系统自动计
算是否热 key 并推送相关结果给应用系统,应用系统根据业务实际情况进行相应处理。
改写 Redis SDK,记录每个请求,定时把收集到的数据上报,然后由一个统一的服务进行聚合计算。
解决方案 利用本地缓存在你发现热 key 以后,把热 key 加载到系统的内存中。针对这种热 key 请求,会直接从内存中取,而不会走到 redis 层。
- 优点:内存访问和 redis 访问的速度不在一个量级,基于本地缓存,接口性能非常好, 可以
大大增加单实例的 QPS。 - 缺点:受应用内存限制,容量有限,数据量非常大的时候,占用太多内存,不太适合。部分热点数据,需要提前预知。热点数据自动检测有一定的延迟,系统短时间内承受的风险比较大。
大key是指当redis的字符串类型占用内存过大或非字符串类型元素数量过多
生产环境中,综合衡量运维和环境的情况,给大key定义参考值如下:
- string类型的key超过10KB
- hash/set/zset/list等数据结构中元素个数大于5k/整体占用内存大于10MB
- 客户端执行命令的时长变慢。
- Redis内存达到maxmemory参数定义的上限引发 *** 作阻塞或重要的Key被逐出,甚至引发内存溢出(Out Of Memory)。
- 集群架构下,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡。
- 对大Key执行读请求,会使Redis实例的带宽使用率被占满,导致自身服务变慢,同时易波及相关的服务。
- 对大Key执行删除 *** 作,易造成主库较长时间的阻塞,进而可能引发同步中断或主从切换。
我们可以通过在Redis 客户端上实时统计出大Key,直接计算出Key对应的Value值大小就可以,例如
// b 为序列化之后的数据
b, err := utils.Serialize(value, c.getSerializer())
if err != nil {
return err
}
// var b []byte
// 长度
reqSize = len(b)
// 10KB
bigKey := 1024 * 10
if reqSize > bigKey {
}
- 优点:对性能几乎无影响。
- 缺点:返回的Key序列化长度并不等同于它在内存空间中的真实长度,因此不够准确,仅可作为参考。
-
对Redis的RDB备份文件进行定制化的分析,帮助您发现实例中的大Key,掌握Key在内存中的占用和分布
-
Redis提供了bigkeys参数能够使redis-cli以遍历的方式分析Redis实例中的所有Key,并返回Key的整体统计信息与每个数据类型中Top1的大Key,bigkeys仅能分析并输入六种数据类型(STRING、LIST、HASH、SET、ZSET、STREAM),命令示例为
redis-cli -h 127.0.0.1 -p 6379 --bigkeys
-
优点:可对历史备份数据进行分析,对线上服务无影响。
-
缺点:时效性差,RDB文件较大时耗时较长。
- 业务拆分,将key的含义更细粒度化,避免大key出现。
- 数据结构上拆分。如果大key是个大json,可以通过mset的方式,将这个key的内容打散到各个实例中,减小大key对数据量倾斜的影响;如果是大list,可以拆成list_1,list_2,list_N;其他数据结构同理。(可以考虑增加单独key存储大key被拆分的个数或元数据信息)
- 对于长文本,更建议使用文档型数据库例如MongoDB等。
- 对一致性要求不高的场景,尝试使用客户端缓存。(只解决了redis的阻塞问题,但机器或局域网的带宽问题没有改善)
- 对大key的压缩。相当于用cpu资源来降低网络io,其中google提出的snappy算法较常用。
- 对于hash等数据结构,需要注意业务是否可以引入定期清理无效field的机制。
- Hash 结构不建议使用,没有办法对具体的Key做过期时间设置,只能再额外开发功能去做,增加开发成本
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)