高性能缓存架构 ############## .. note:: 基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。 .. note:: 没法保证缓存和存储系统一致性,这类数据允许一定的不一致,一定范围内的对用户也没有影响,不要只从技术的角度考虑问题,结合业务考虑技术 .. note:: 缓存集群间一般不要跨数据中心同步,存储可以用跨数据中心同步 缓存穿透 ======== .. note:: 缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。 有两种情况:: 1. 存储数据不存在 如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中 这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统 2. 缓存数据生成耗费大量时间或者资源 存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源 如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况 典型的就是电商的商品分页:: 假设我们在某个电商平台上选择 “手机” 这个类别查看 由于数据巨大,不能把所有数据都缓存起来,只能按照分页来进行缓存 由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存 通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题 具体的场景有:: 1. 分页缓存的有效期设置为 1 天,因为设置太长时间的话,缓存不能反应真实的数据 2. 通常情况下,用户不会从第 1 页到最后 1 页全部看完,一般用户访问集中在前 10 页 因此第 10 页以后的缓存过期失效的可能性很大 3. 竞争对手每周来爬取数据,爬虫会将所有分类的所有数据全部遍历 从第 1 页到最后 1 页全部都会读取,此时很多分页缓存可能都失效了 4. 由于很多分页都没有缓存数据,从数据库中生成缓存数据又非常耗费性能(order by limit 操作) 因此爬虫会将整个数据库全部拖慢。 通常的应对方案:: 要么就是识别爬虫然后禁止访问,但这可能会影响 SEO 和推广; 要么就是做好监控,发现问题后及时处理, 因为爬虫不是攻击,不会进行暴力破坏,对系统的影响是逐步的,监控发现问题后有时间进行处理。 缓存雪崩 ======== .. note:: 缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。 当缓存过期被清除后,业务系统需要重新生成缓存:: 因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。 而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。 由于旧的缓存已经被清除,新的缓存还未生成, 并且处理这些请求的线程都不知道另外有一个线程正在生成缓存, 因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。 这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。 产生的原因:: 1. 大量 key 同时过期在实际情况中反而比较少见 因为不同的 key 生成时间、过期时间本来就是不同的, 除非缓存不够被踢出,自然过期的场景下不会出现大量 key 同时过期, 2. 真正雪崩比较常见的是一两个热点 key 过期后引起系统性能急剧下降。 缓存雪崩的常见解决方法有两种:: 1. 更新锁机制 2. 后台更新机制 1. 更新锁:: 对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新, 未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。 对于采用分布式集群的业务系统,由于存在几十上百台服务器, 即使单台服务器只有一个线程更新缓存, 但几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存,同样存在雪崩的问题。 因此分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如 ZooKeeper。 2. 后台更新:: 由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。 问题: 后台定时机制需要考虑一种特殊的场景 当缓存系统内存不够时,会 “踢掉” 一些缓存数据, 从缓存被 “踢掉” 到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值, 而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了 后台更新解决的方式有两种:: 1. 后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1 秒或者 100 毫秒读取一次), 如果发现缓存被 “踢了” 就立刻更新缓存, 这种方式实现简单,但读取时间间隔不能设置太长,因为如果缓存被踢了,缓存读取间隔时间又太长, 这段时间内业务访问都拿不到真正的数据而是一个空的缓存值,用户体验一般。 2. 业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。 可能会出现多个业务线程都发送了缓存更新消息,但其实对后台线程没有影响, 后台线程收到消息后更新缓存前可以判断缓存是否存在,存在就不执行更新操作。 这种方式实现依赖消息队列,复杂度会高一些,但缓存更新更及时,用户体验更好。 后台更新具体在实现:: 后台更新线程既不能只有一个,也不能和业务线程一样多,一般 8~32 个就差不多了,因为缓存更新并不会非常频繁。 假如 8 个线程后台更新也可能导致缓存雪崩,那就要做更多事情了, 例如:后台线程更新前先读取一下缓存,存在就不更新。 因为业务线程数量要远远大于后台更新线程数量。 假设 20 台 64 核机器,每台机器 256 线程,业务线程就是 5120 个,后台缓存更新线程数量一般 8~32 就足够了。 如果缓存设计是只能一个线程更新,那确实也只能用锁了 .. note:: 后台更新机制还适合业务刚上线的时候进行缓存预热。缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。 缓存热点 ======== .. note:: 缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。 以微博为例:: 对于粉丝数超过 100 万的明星,每条微博都可以生成 100 份缓存, 缓存的数据是一样的,通过在缓存的 key 里面加上编号进行区分,每次读缓存时都随机读取其中某份缓存。 .. note:: 缓存副本设计有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。 思考 ==== 对于缓存雪崩问题,我们采取了双 key 策略:: 要缓存的 key 过期时间是 t,key1 没有过期时间。 每次缓存读取不到 key 时就返回 key1 的内容,然后触发一个事件。 这个事件会同时更新 key 和 key1。 数据库自带的缓存是什么样的:: 作者回复:我对 mysql 比较熟,以下仅限 mysql: 1. mysql 第一种缓存叫 sql 语句结果缓存 但条件比较苛刻,程序员不可控,我们的 dba 线上都关闭这个功能 2. mysql 第二种缓存是 innodb buffer pool 缓存的是磁盘上的分页数据,不是 sql 的查询结果,sql 的执行过程省不了 而mc,redis 这些实际上都是缓存 sql 的结果,两种缓存方式,性能差很远。 因此,可控性,性能是数据库缓存和独立缓存的主要区别 好的缓存方案应该从这几个方面入手设计:: 1. 什么数据应该缓存 2. 什么时机触发缓存和以及触发方式是什么 3. 缓存的层次和粒度 网关缓存如 nginx 本地缓存如单机文件 分布式缓存如 redis cluster 进程内缓存如全局变量 4. 缓存的命名规则和失效规则 5. 缓存的监控指标和故障应对方案 6. 可视化缓存数据如 redis 具体 key 内容和大小 突发性请求量大于处理速度时解决方法:: 1. 限流 2. 容器化 + 动态化 3. 业务降级,例如限制评论 某些重要数据,如价格,需要及时更新的数据,有没什么好的办法做到刷新:: 1. 同步刷新缓存 当更新了某些信息后,立刻让缓存失效。 这种做法的优点是用户体验好,缺点是修改一个数据可能需要让很多缓存失效 2. 适当容忍不一致 例如某东的商品就是这样,我查询的时候显示有货,下单的时候提示我没货了 3. 关键信息不缓存 库存,价格等不缓存,因为这类信息查询简单,效率高,关系数据库查询性能也很高 像淘宝商品列表筛选项特别多,组合起来会更多,这样在后台做更新缓存怎么处理:: 1. 针对常用的分类会统一缓存,缓存会主动更新; 如: 电脑的前10页 2. 不常用的根据查询条件计算 md5 作为 key 进行缓存,缓存时间不长,例如 60 分钟 防止短时间内大量访问压垮存储,例如爬虫 如何保证缓存和数据库的一致性:: 先更新库好些,因为更新库成功后即使更新缓存失败,缓存也有过期时间。 如果要保证一致,更新库前先删除缓存,然后更新库,再更新缓存, 但即使这样,也可能出现缓存和库不一致, 因此要做到绝对一致是很复杂的,需要用到 zk 这类协调软件,一般不建议这么做,没必要 查询条件太多时,把所有确定的 Key 都放在布隆过滤器,先判断 Key 是否存在,来避免缓存穿透:: 这个方案有以下几个落地的问题: 1. 要求每次写的时候都更新布隆过滤器,读的时候判断布隆过滤 对于缓存这种高性能计算场景,这个影响还是比较大的 2. 布隆过滤器也只能放在缓存中,不能放在各个应用程序内 布隆过滤器本身的读写需要做到高可用高性能,全局互斥 3. 布隆过滤器不能删除数据,缓存过期后布隆过滤器会失效 所以布隆过滤器一般用在爬虫 URL、事件数据(带时间戳,只增不删)这种场景 我来说说我做过的一个项目设计的缓存策略吧,因为需要实时查询所有 open 状态的基础订单列表信息,所以有个后台进程每分钟刷新一次缓存。但因为 open 态 order 太多,导致 redis 序列化与反序列化过程太久而连接中断,后来我采用的一个方案就是根据每次查询必选的组合查询条件分别分组然后以单个查询值作为 key 放到缓存中,这样既达到了按照查询条件过滤的情况,又缩减了每次存取数据的字节数。可能比较简单吧,感觉也没有什么特别的。哈哈😄 作者回复:我们用过另外一种方式:将查询条件组合成字符串再计算 md5,作为缓存的 key,优点是简单灵活,缺点是浪费一部分缓存