使用缓存必须注意的事项

时间:2022-07-23
本文章向大家介绍使用缓存必须注意的事项,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

为什么要使用cache

关系型数据库的数据量比较小,以mysql为例,单表的量尽量控制在千万级别。

关系型数据库在TPS上的瓶颈往往会比其他瓶颈更容易暴露出来,尤其对于大型web系统,由于每天大量的并发访问,对数据库的读写性能要求非常高;而传统的关系型数据库的处理能力确实捉襟见肘;以我们常用的MySQL数据库为例,常规情况下的TPS大概只有1500左右(各种极端场景下另当别论)。

下面是MySQL官方所给出的一份测试数据:

系统配置:

Sun V40z / 4x 2390MHZ / Solaris 10 / 8GB RAM

1m rows,Read Only,4 CPU

Connections

Trans/sec

1

382

2

677

4

1130

8

1479

32

1418

256

947

1024

224

详细压测报告:

https://www.percona.com/blog/files/presentations/UC2005-Advanced-Innodb-Optimization.pdf

对于一个PV上亿的网站,每一次请求涉及多次数据库交互,每天的读写请求量远远超过关系型数据库的处理能力,所以必须通过高效的缓存抵挡大部分的数据请求。

缓存类型

  • 本地缓存 本地缓存会减少网络层的交互,无论是本地内存还是磁盘,速度比较快。但对分布式系统来讲有一个缺点,当数据库更新时,没有一个简单有效的方法去更新本地缓存。

本地缓存适用两种场景:

  • 一、对缓存内容时效性要求不高,能接受一定的延迟,可以设置较短过期时间,被动失效更新保持数据的新鲜度。
  • 二、缓存的内容不会改变。比如订单号与uid的映射关系,一旦创建就不会发生改变。

注意问题:

  • 内存Cache数据条目上限控制,避免内存占用过多导致应用瘫痪。
  • 内存中的数据移出策略
  • 虽然实现简单,但潜在的坑比较多,最好选择一些成熟的开源框架
  • 分布式缓存 本地缓存的使用很容易让你的应用服务器带上“状态”,而且容易受内存大小的限制。 分布式缓存借助分布式的概念,集群化部署,独立运维,容量无上限,虽然会有网络传输的损耗,但这1~2ms的延迟相比其更多优势完成可以忽略。 优秀的分布式缓存系统有大家所熟知的Memcached、Redis。对比关系型数据库和缓存存储,其在读和写性能上的差距可谓天壤之别,redis单节点已经可以做到8W+ QPS。设计方案时尽量把读写压力从数据库转移到缓存上,有效保护脆弱的关系型数据库。
  • 客户端缓存 大部分的web应用、微服务应用都会尽量做到无状态,方便于线性扩容。有状态的后端存储:DB、NoSQL、分布式文件系统、CDN等。 另一个很重要的就是客户端缓存了,对客户端存储的合理使用,原本每天几千万甚至上亿的接口调用,一下就可能降到了几百万甚至更少,而且即便是用户更换浏览器,或者缓存丢失需要重新访问服务器,由于随机性比较强,请求分散,给服务器的压力也很小。另外再加上合理的缓存过期时间,就可以在数据准确和性能上做一个很好的折衷。

常用技术框架

  • Guave
  • Memcached
  • Redis

更多缓存框架:

http://www.oschina.net/project/tag/109/cacheserver

更新策略

  • 被动失效 缓存数据主要是服务读请求的,通常会设置一个过期时间,或者当数据库状态改变时,通过一个简单的delete操作,使数据失效掉;当下次再去读取时,如果发现数据过期了或者不存在了,那么就重新去数据库读取,然后更新到缓存中,这即是所谓的被动失效策略。 被动策略有一个很大的风险,从缓存失效到数据再次被预热到cache这段时间,所有的读请求会直接打到DB上,对于一个高访问量的系统,很容易被击垮。
  • 主动更新 主动更新,很容易理解,就是数据库存储发生变化时,会直接同步更新到Cache,主要是为了解决cache空窗期引发的问题。比如电商的卖家修改商品详情,具有读多写少特点。 但如果是读多写多,同样会带来另一个问题,就是并发更新。多台应用服务器同时访问一份数据是很正常的,这样就会存在一台服务器读取并修改了缓存数据,但是还没来得及写入的情况下,另一台服务器也读取并修改旧的数据,这时候,后写入的将会覆盖前面的,从而导致数据丢失。解决的方式主要有三种:
1、锁控制。这种方式一般在客户端实现(在服务端加锁是另外一种情况),

序列化

分布式缓存的本质就是将所有的业务数据对象序列化为字节数组,然后保存到自己的内存中。所使用的序列化方案也自然会成为影响系统性能的关键点之一

  • 序列化速度
  • 对象压缩比例
  • 支持的序列化数据类型范围
  • 反序列化的速度
  • 框架接入易用性

常见的序列化框架:

  • Java源生序列化
  • Hessian
  • Protobuf
  • Kryo

开发注意事项

  • 评估当前业务使用的空间大小。避免空间不足,导致热数据被置换出去,影响缓存命中率
  • 不要把缓存当DB使用,因为它会丢失
  • 最好设置过期时间,可以自己回收
  • key定义遵循一定规则,相同业务采用同一前缀
  • 缓存对象粒度。高内聚低耦合,考虑尽可能复用,不要一个小字段修改导整个大对象全部失效
方案一:
uid---> 发过的贴子内容列表

方案二:
uid--->发过的贴子tid列表
tid--->贴子内容

  • 另外缓存对象大小要控制,不要过大,占用过多带宽。之前遇到过一个业务团队,单key下挂了5M的大对象,每次用时,从缓存中取出,反序列化,然后取其中一小部分。后来随着业务并发量上升,把网卡打爆,进而影响其它正常业务访问。
  • 根据业务需求,选择合适的缓存框架,比如memcache只支持kv对存储,redis则支持较丰富的数据结构
  • 是否要引入多级缓存,本地内存--》非持久化缓存(如memcache)---》持久化缓存---》DB,要注意数据一致性问题
  • 提前考虑扩容问题

问题汇总

1、缓存穿透

我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就挂掉了。那这种问题有什么好办法解决呢?

有一个比较巧妙的做法是,可以将这个不存在的key预先设定一个值。比如,"NULL" ,在返回这个NULL值的时候,我们的应用就可以认为这是不存在的key。

缓存穿透如果被恶意攻击,造成的影响面很容易放大。比如文章详情页,查询一个不存在的tid,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成影响。

2、缓存集体失效

对于一些活动期间的数据通常会提前预热到缓存中,并设置一个过期时间,如果系统的并发量很高,恰巧缓存又失效了,此时会将压力转嫁给后面的DB,很容易击垮系统。

那如何解决这些问题呢?

其中的一个简单方案就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。还有一种方式,就是计算好缓存的过期时间。

3、DB和缓存不一致

当修改了数据库后,没有及时修改缓存,或者缓存服务器挂了。如果是因为网络问题引起的没有及时更新,可以通过重试机制来解决。而缓存服务器挂了,请求首先自然也就无法到达,从而直接访问到数据库。那么我们在修改数据库后,无法修改缓存,这时候可以将这条数据放到数据库中,同时启动一个异步任务定时去检测缓存服务器是否连接成功,一旦连接成功则从数据库中按顺序取出修改数据,依次进行缓存最新值的修改。

4、命中率较低,影响性能

  • 过期时间太短, 这种场景可以根据实际情况适当增大过期时间
  • 存在不合理缓存删除逻辑, 导致有效的缓存频繁被删除
  • 不合理的key规则设计, 每次缓存访问的key都在变化, 导致无法命中缓存和频繁的新缓存创建
  • key确实不存在,但是应用还是在频繁的访问, 这种应该从业务逻辑上杜绝

性能指标

  • 缓存空间的使用率
  • topN 命令的执行次数
  • 缓存的命中率
  • 缓存的接口平均RT,最大RT,最小RT
  • 缓存的QPS
  • 网络出口流量
  • 客户端连接数
  • key个数统计