17 | 高性能缓存架构

17 | 高性能缓存架构

朗读人:黄洲君    10′33′′ | 4.84M

虽然我们可以通过各种手段来提升存储系统的性能,但在某些复杂的业务场景下,单纯依靠存储系统的性能提升不够的,典型的场景有:

  • 需要经过复杂运算后得出的数据,存储系统无能为力

例如,一个论坛需要在首页展示当前有多少用户同时在线,如果使用 MySQL 来存储当前用户状态,则每次获取这个总数都要“count(*)”大量数据,这样的操作无论怎么优化 MySQL,性能都不会太高。如果要实时展示用户同时在线数,则 MySQL 性能无法支撑。

  • 读多写少的数据,存储系统有心无力

绝大部分在线业务都是读多写少。例如,微博、淘宝、微信这类互联网业务,读业务占了整体业务量的 90% 以上。以微博为例:一个明星发一条微博,可能几千万人来浏览。如果使用 MySQL 来存储微博,用户写微博只有一条 insert 语句,但每个用户浏览时都要 select 一次,即使有索引,几千万条 select 语句对 MySQL 数据库的压力也会非常大。

缓存就是为了弥补存储系统在这些复杂业务场景下的不足,其基本原理是将可能重复使用的数据放到内存中,一次生成、多次使用,避免每次使用都去访问存储系统。

缓存能够带来性能的大幅提升,以 Memcache 为例,单台 Memcache 服务器简单的 key-value 查询能够达到 TPS 50000 以上,其基本的架构是:

http://pic001.cnblogs.com/img/dudu/200809/2008092816494460.png

缓存虽然能够大大减轻存储系统的压力,但同时也给架构引入了更多复杂性。架构设计时如果没有针对缓存的复杂性进行处理,某些场景下甚至会导致整个系统崩溃。今天,我来逐一分析缓存的架构设计要点。

缓存穿透

缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况:

1. 存储数据不存在

第一种情况是被访问的数据确实不存在。一般情况下,如果存储系统中没有某个数据,则不会在缓存中存储相应的数据,这样就导致用户查询的时候,在缓存中找不到对应的数据,每次都要去存储系统中再查询一遍,然后返回数据不存在。缓存在这个场景中并没有起到分担存储系统访问压力的作用。

通常情况下,业务上读取不存在的数据的请求量并不会太大,但如果出现一些异常情况,例如被黑客攻击,故意大量访问某些读取不存在数据的业务,有可能会将存储系统拖垮。

这种情况的解决办法比较简单,如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继续访问存储系统。

2. 缓存数据生成耗费大量时间或者资源

第二种情况是存储系统中存在数据,但生成缓存数据需要耗费较长时间或者耗费大量资源。如果刚好在业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。

典型的就是电商的商品分页,假设我们在某个电商平台上选择“手机”这个类别查看,由于数据巨大,不能把所有数据都缓存起来,只能按照分页来进行缓存,由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。通常情况下这样实现是基本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题。

具体的场景有:

  • 分页缓存的有效期设置为 1 天,因为设置太长时间的话,缓存不能反应真实的数据。

  • 通常情况下,用户不会从第 1 页到最后 1 页全部看完,一般用户访问集中在前 10 页,因此第 10 页以后的缓存过期失效的可能性很大。

  • 竞争对手每周来爬取数据,爬虫会将所有分类的所有数据全部遍历,从第 1 页到最后 1 页全部都会读取,此时很多分页缓存可能都失效了。

  • 由于很多分页都没有缓存数据,从数据库中生成缓存数据又非常耗费性能(order by limit 操作),因此爬虫会将整个数据库全部拖慢。

这种情况并没有太好的解决方案,因为爬虫会遍历所有的数据,而且什么时候来爬取也是不确定的,可能是每天都来,也可能是每周,也可能是一个月来一次,我们也不可能为了应对爬虫而将所有数据永久缓存。通常的应对方案要么就是识别爬虫然后禁止访问,但这可能会影响 SEO 和推广;要么就是做好监控,发现问题后及时处理,因为爬虫不是攻击,不会进行暴力破坏,对系统的影响是逐步的,监控发现问题后有时间进行处理。

缓存雪崩

缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步骤耗时几十毫秒甚至上百毫秒。而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道另外有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。

缓存雪崩的常见解决方法有两种:更新锁机制后台更新机制

1. 更新锁

对缓存更新操作进行加锁保护,保证只有一个线程能够进行缓存更新,未能获取更新锁的线程要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

对于采用分布式集群的业务系统,由于存在几十上百台服务器,即使单台服务器只有一个线程更新缓存,但几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存,同样存在雪崩的问题。因此分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如 ZooKeeper。

2. 后台更新

由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存本身的有效期设置为永久,后台线程定时更新缓存。

后台定时机制需要考虑一种特殊的场景,当缓存系统内存不够时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程本身又不会去更新缓存,因此业务上看到的现象就是数据丢了。解决的方式有两种:

  • 后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1 秒或者 100 毫秒读取一次),如果发现缓存被“踢了”就立刻更新缓存,这种方式实现简单,但读取时间间隔不能设置太长,因为如果缓存被踢了,缓存读取间隔时间又太长,这段时间内业务访问都拿不到真正的数据而是一个空的缓存值,用户体验一般。

  • 业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。可能会出现多个业务线程都发送了缓存更新消息,但其实对后台线程没有影响,后台线程收到消息后更新缓存前可以判断缓存是否存在,存在就不执行更新操作。这种方式实现依赖消息队列,复杂度会高一些,但缓存更新更及时,用户体验更好。

后台更新既适应单机多线程的场景,也适合分布式集群的场景,相比更新锁机制要简单一些。

后台更新机制还适合业务刚上线的时候进行缓存预热。缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。

缓存热点

虽然缓存系统本身的性能比较高,但对于一些特别热点的数据,如果大部分甚至所有的业务请求都命中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。例如,某明星微博发布“我们”来宣告恋爱了,短时间内上千万的用户都会来围观。

缓存热点的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力。以微博为例,对于粉丝数超过 100 万的明星,每条微博都可以生成 100 份缓存,缓存的数据是一样的,通过在缓存的 key 里面加上编号进行区分,每次读缓存时都随机读取其中某份缓存。

缓存副本设计有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。

实现方式

由于缓存的各种访问策略和存储的访问策略是相关的,因此上面的各种缓存设计方案通常情况下都是集成在存储访问方案中,可以采用“程序代码实现”的中间层方式,也可以采用独立的中间件来实现。

小结

今天我为你讲了高性能架构设计中缓存设计需要注意的几个关键点,这些关键点本身在技术上都不复杂,但可能对业务产生很大的影响,轻则系统响应变慢,重则全站宕机,架构师在设计架构的时候要特别注意这些细节,希望这些设计关键点和技术方案对你有所帮助。

这就是今天的全部内容,留一道思考题给你吧,分享一下你所在的业务发生过哪些因为缓存导致的线上问题?采取了什么样的解决方案?效果如何?

欢迎你把答案写到留言区,和我一起讨论。相信经过深度思考的回答,也会让你对知识的理解更加深刻。(编辑乱入:精彩的留言有机会获得丰厚福利哦!)

版权归极客邦科技所有,未经许可不得转载

精选留言

  • bluefantasy
    我们的系统就出现过类似的问题,开始的时候没有缓存,每次做活动访问量大的时候就会导致反应特别慢。后来通过加redis缓存解决了问题。

    对于缓存雪崩问题,我们采取了双key策略:要缓存的key过期时间是t,key1没有过期时间。每次缓存读取不到key时就返回key1的内容,然后触发一个事件。这个事件会同时更新key和key1。
    2018-06-05
    作者回复

    很有创意👍👍

    2018-06-05

  • loveluckystar
    讲一个头两天发生的事情,我们的一个业务背后是es做db,之前是通过redis做缓存,缓存一段时间后失效再从es读取,是业务访问加载缓存的方式。有一天线上es集群机器单台出现问题,返回慢,由于分布式的缘故,渐渐拖满了所有请求,缓存失效来查询es发生了超时,加载失败,于是下次访问还是直接访问es。最终缓存全部失效,qps翻了好多倍,直接雪崩,es集群彻底没有响应了。。。之后我们只好先下线这个缓存加载功能,让集群活过来,最终改造缓存加载方式,用后台进程去更新缓存,而不用业务访问加载。
    2018-06-05
    作者回复

    现学现用的案例,很赞👍👍

    2018-06-05

  • 三月沙@wecatch
    好的缓存方案应该从这几个方面入手设计:
    1.什么数据应该缓存
    2.什么时机触发缓存和以及触发方式是什么
    3.缓存的层次和粒度( 网关缓存如 nginx,本地缓存如单机文件,分布式缓存如redis cluster,进程内缓存如全局变量)
    4.缓存的命名规则和失效规则
    5.缓存的监控指标和故障应对方案
    6.可视化缓存数据如 redis 具体 key 内容和大小
    2018-06-05
    作者回复

    确实,细节不少,可以写本书了😃

    2018-06-05

  • mapping
    计算机界两大难题:命名和缓存过期。没用缓存的时候,想着怎么用缓存提升性能,用了缓存又担心数据更新不及时。技术上希望所有的请求都能命中缓存,业务上又恨不得数据实时最新。所以就会引入各种缓存过期策略,如设置过期时间,按规则删除,打版本。这些应该在前期设计缓存系统时规划好,我们最早是将 sql md5 作 key 查询结果存入缓存,结果业务系统数据不一致,要清除缓存简直是噩梦,只能祭出绝招重启 memcache,后面改成按规则删除,在 key 中加上业务和用户的前缀,可以很方便删除某个业务或某个用户的缓存。以上过期策略在前端浏览器也是这样,最简单就是 web 服务器设置静态资源缓存过期时间,如果业务频繁发新版本,过期时间不宜设置太长,但其实每次变动的文件很少,这种策略会导致大部分缓存命中率不高。按规则删除,早期很多网站上会有诊断助手类的东西,页面加载错误点下诊断助手就帮你清除缓存,原理就是对静态文件逐一带上 no-cache 请求头发送 ajax 请求强制覆盖缓存(跟 DevTools 中 disable cache 原理一样)。打版本其实就相当于让浏览器请求一个新版本文件,对于老版本文件就让它在缓存中自生自灭。
    2018-06-05
    作者回复

    你们的缓存设计有点复杂,还不如调整业务,越复杂的方案越容易出错,参考架构设计原则

    2018-06-05

  • 王磊
    经常我说到缓存的时候,面试官问我,数据库自身不是有缓存吗,标准答案是怎么回击他?
    2018-06-05
    作者回复

    我对mysql比较熟,以下仅限mysql:
    1. mysql第一种缓存叫sql语句结果缓存,但条件比较苛刻,程序员不可控,我们的dba线上都关闭这个功能,具体实现可以查一下
    2. mysql第二种缓存是innodb buffer pool,缓存的是磁盘上的分页数据,不是sql的查询结果,sql的执行过程省不了。而mc,redis这些实际上都是缓存sql的结果,两种缓存方式,性能差很远。

    因此,可控性,性能是数据库缓存和独立缓存的主要区别

    2018-06-06

  • 公号-Java大后端
    上缓存架构的时候,结合以前的实际经历,会有几个值得注意的地方:

    1 哪些数据才真正的需要缓存?缓存也并非银弹。既然允许数据缓存,那么在你是可以接受在一定时间区间内的数据不一致性的。(当然可以做到最终一致性)

    2 确定好1后,就需要会数据类型进行分类,比如业务数据缓存,http缓存等

    3 根据数据类型及访问特点的不同选择不同缓存类型的技术方案。

    请问华仔,热点数据存在相当的突发性,临时的扩容似乎也来不及,能否从缓存架构角度如何避免类似微博宕机的事件?

    2018-06-05
    作者回复

    1. 限流
    2. 容器化+动态化
    3. 业务降级,例如限制评论

    2018-06-05

  • 倔强小小🐤
    我们系统是做美术馆的3d展示的,后台配置的数据有点多,画框数据有几m,单个模型数据有几百k,最早直接mysql直接读取,后来用redis但是感觉效果不理想,又引入ehcache,发现不经过网络传输性能很好,经过网络传输后网络一般还是卡的很,后来又在前端用local storage进行缓存,后端通过rabbitmq进行消息通知前端清除本地缓存,缓存设置有效期都是一天,请问下我们这种情况有更好的方案吗
    2018-06-05
    作者回复

    前后端分离,在node上缓存和渲染试试

    2018-06-05

  • 刘磊
    对缓存的key也需要进行valid,避免无效的key查询缓存
    2018-06-05
  • 醇梨子
    华仔,请教一下,针对这种高并发缓存架构设计中,缓存和存储系统一致性问题怎么保证?比如说商品浏览人数,需要存库,然后又需要放缓存,需要频繁更新数据库。
    2018-06-17
    作者回复

    没法保证,这类数据允许一定的不一致,一定范围内的对用户也没有影响,不要只从技术的角度考虑问题,结合业务考虑技术

    2018-06-19

  • jacy
    商品列表中,像商品描述等信息,缓存更新不及时影响不大,但某些重要数据,如价格,需要及时更新的数据,有没什么好的办法做到刷新。对于价格这种关键数据,不缓存,直接从数据库查询,是否可行。或者在用户查看商品详情时再去数据库查询价格,但可能出现列表中的价格和详情页中的价格不一致。
    2018-06-05
    作者回复

    通常有几种做法:
    1. 同步刷新缓存:当更新了某些信息后,立刻让缓存失效。
    这种做法的优点是用户体验好,缺点是修改一个数据可能需要让很多缓存失效
    2. 适当容忍不一致:例如某东的商品就是这样,我查询的时候显示有货,下单的时候提示我没货了
    3. 关键信息不缓存:库存,价格等不缓存,因为这类信息查询简单,效率高,关系数据库查询性能也很高

    2018-06-06

  • 鹅米豆发
    1、最早也是采用后台用数据库,前台用关系型数据库+被动缓存的模式。结果是经常的性能抖动,且缓存一致性问题很难解决。后来我们的多数系统,都采用了前后台分离的模式——后台原始数据仍然是关系型数据库,前台使用缓存作为数据源,两者之间数据实时同步+定时同步+人工触发结合。
    这个模式,基本根除了穿透,雪崩,不一致,性能抖动这些。但带来了新的问题,比如数据丢失且不可恢复。我们的做法是,让缓存具备相对可靠的持久化机制+运维体系。
    2、遇到过几次热点问题,感觉这个更加棘手些。第一种情况,单Key数据结构本身过大,单个分片出现热点,单次访问的复杂度变大。这个相对容易,可以对key进行拆分,使用hashtag机制分片。第二种情况,数据分片普遍不均衡,较少遇到,遇到就比较棘手。第三种情况,数据分片均衡,但访问不均衡,可以增加副本数量。
    2018-06-05
    作者回复

    缓存持久化是一个不错的方法

    2018-06-05

  • 🔰夏天的味道
    线上遇到的一个错误:业务查询的结果序列化后放到redis,下次从redis取出来时报错。原来是结果类虽然实现了Serializable接口,但是没有重写serialVersionUID,导致不能成功反序列化。
    2018-06-06
  • 紫clz
    刚好前段时间就有一个问题。
    产品反馈:后台更新的配置不生效,需要过几小时到半天不等的时间才能生效。
    从数据库确认持久化数据没问题,因此怀疑是缓存问题。

    我们的缓存设计是:业务key加一个缓存版本号做key,每次更新数据对应缓存的版本号就+1。
    前台定期扫描缓存版本号来实现缓存的更新。

    于是我查到了线上最新的版本号去缓存服务器查询,见鬼,数据也是正常的。排查了几天没有结果,无奈上线一个版本打了debug日志,终于找到原因了。

    我们的服务是双中心的,两边各存有一份缓存版本号,不知道什么原因,导致一边的缓存版本号比另外一个集群的多了3。由于我们的更新策略不是删掉无效的缓存,而是更新缓存的key,因此旧版本的缓存数据实际上还在缓存服务器上。更要命的是缓存服务器做了双中心同步(为啥同步暂且不提),这样就导致实际上一边集群读取的实际上是另外一个集群几个版本前留下的缓存数据。需要等待这个数据过期后,才能正常从数据库加载数据。
    2018-08-23
    作者回复

    这种做法会经常出现一些线上小问题

    2018-08-26

  • 100kg
    你好,我想请教下,如果两台mysql互为主从,其中一台主键自增步长设置为2,另一个为1,这样做会影响性能吗?比如索引的连贯性之类的
    2018-06-06
    作者回复

    不会,索性是对已经存在的值建立索性数据结构,值的连续性和大小对数据结构本身没有影响,值的数量才会影响索性的大小和性能

    2018-06-06

  • 星火燎原
    如果用redis做分布式锁 从业务上设置超时时间为1s 但是有一些逻辑单元确实需要执行比如3s才能释放掉锁。那么这种“正常的异常情况”应该怎么解决呢?
    2018-06-05
    作者回复

    那为何不直接设置为3秒超时?

    2018-06-06

  • 家榆
    我是实现了个框架:https://github.com/qiujiayu/AutoLoadCache,用于解决一下问题:
    1. 缓存操作与业务代码耦合问题;
    2. 缓存穿透问题;
    3. 异步在缓存快要过期时,异步刷新缓存;
    4. 使用“拿来主义机制”,降低回源并发量;
    2018-07-05
  • 王磊
    关于缓存雪崩的定义,文中的例子是说单条缓存在生成的时间间隔内有大量相同请求查询存储系统,除此之外,记得还有一个更典型的场景,是大量缓存同时过期,导致大量不同请求去查询存储系统(数据库)。请点评。
    2018-06-05
    作者回复

    一般不会出现这种情况,但缓存服务器宕机的场景和你说的类似,我们一般会要求线上进行模拟测试,假如一台缓存服务器宕机,系统是否能够顶住,如果顶不住,就要增加缓存服务器,或者优化某些key的设计

    2018-06-06

  • 武坤
    在 缓存雪崩的2.后台更新中“缓存系统内存不够时,会‘踢掉’一些缓存数据”,既然是内存不够那么总会有不在缓存里面的数据。后面的两种解决方式怎么能解决这个问题呢?
    2018-06-05
    作者回复

    一定程度上缓解,不能完全解决

    2018-06-05

  • cqc
    1个问题:关于后台更新,既然缓存服务器内存不足,需要剔除数据,那么后台更新再次触发查询,是否又会导致其他一些缓存数据被剔除,这感觉像是陷入一个循环了
    1个思路:我们之前的web项目,对于缓存热点数据,为了减少服务器的压力,在客户端引入了缓存:CDN加local storage,感觉对服务器端压力分散还是很有效果的。
    2018-06-05
    作者回复

    1.是的,所以加内存才是根本解决方式
    2. 这是分级缓存策略

    2018-06-05

  • 姜泮昌
    目前我们还没用上缓存技术,所有请求都直接访问了数据库,导致数据库集群压力大,cpu、内存、IO常常过高,请问项目初用缓存,比如redis ,有什么指导原则或技巧吗?是缓存热点sql还是热点功能还是根据其他原则缓存呢?
    2018-06-05
    作者回复

    数据库压力大就可以考虑缓存了

    2018-06-05

  • 一叶
    笔记:
    书籍:
    《高性能Mysql》
    《unix编程艺术》:宁花机器一分,不花程序员一秒

    提升性能:
    先单机,有压力后优先考虑sql优化、db参数调优,还有硬件性能(32核/16G/SSD)优化,不行还可以再考虑业务逻辑优化、缓存。不要一上来就读写分离、集群等,能单库搞定的就毫不犹豫的单库。

    ---

    主从读写分离
    适用于单机无法应付所有请求,且读比写多时,读写分离还可以分别针对读写节点建索引来优化。
    对实时性要求不高:刚写入就读会有延迟,同步数据特别大时,延迟可能达到分钟级(可用缓存解决:2-8原则,挑选占访问量80%的前20%来缓存)。
    TODO主从还能设置自增长key不一样?

    分库分表(甚用,增加很多复杂度)
    几千万或上亿
    分库时机:单机性能瓶颈,1业务不复杂,但整体数据量影响到数据库性能;2业务复杂,需要分系统由不同团队开发,使用分库减少团队耦合。(分库导致不能join和事务(有方案但性能太低用了跟没分库差别不大,用最终一致性/事件驱动))
    分表时机:单表数据量大拖慢了sql性能,做垂直(将常用和不常用字段分开)或水平拆分(id分段、hash路由、添加路由表等)提高速度。(那么join、count、分页排序等就变得复杂)
    TODO环状hash 一致性hash?

    nosql
    (nosql——not noly sql 本质上是牺牲ACID中的某个或某几个属性,以解决关系数据库某些复杂的问题)
    关系数据库:强大的sql功能和ACID属性,发展了几十年技术相当成熟 Mysql / Postgresql
    k-v存储:解决关系数据库无法存储数据结构的问题 Redis / Memcache (redis不太适合key的value特别大,这种情况会导致整个redis变慢,这种场景mc更好->参考IO模型 redis单Reactor单进程读写大value会阻塞所有业务 持久化也会)
    文档数据库:解决关系数据库强schema约束,查询不存在的列会报错,扩充很麻烦还会长时间锁表 MongoDB
    列式数据库:解决关系数据库处理大数据分析或统计时IO高的问题,关系数据库即使只处理某列也会把整行查询到内存中 HBase / Greenplum
    全文搜索引擎:解决关系数据库全文搜索like扫描全表性能问题 ElasticSearch / solr
    LevelDB 内存型?
    时序数据库?:实时计算统计实时监控 influxDB
    OLAP OLTP HTAP?

    缓存(千万千万不要设计复杂的缓存,到时候各种不一致问题烦死你)
    cdn、nginx缓存、网关缓存、数据层缓存redis、db本身也有缓存(sql结果缓存、读取的磁盘分页缓存)
    缓存穿透:1本身无数据(添加默认值缓存/布隆过滤器[整型自增key?]) 2未生成缓存(识别爬虫并禁止 但可能影响seo)
    缓存雪崩:缓存实效后大家都在更新缓存导致系统性能急剧下降(1消息队列通知后台更新、2使用分布式更新锁)
    缓存热点:大部分业务都会命中的同一份缓存,比如1000w+粉丝的微博消息,复制多分缓存副本,key里面加副本编号将请求分散,且设置过期范围,而不是所有副本固定同一过期时间。
    缓存框架看一下设计思路:echcache、网友分享https://github.com/qiujiayu/AutoLoadCache


    2018-09-23
    作者回复

    很用心,赞👍

    2018-09-26

  • 秋天来了
    对于前面提到的双key机制,可以改成这样的,还是一个key,但是他的value附加一个有效时间,如果你读这个key中的数据,根据有效时间判断,他是已经失效了,这次,先把这个结果返回,然后通知去更新缓存,这样可以节省一个key1。
    2018-08-14
    作者回复

    这样做的性能代价有点大,每个key读取的时候都要进行这个解析,而缓存的访问量又很大

    2018-08-15

  • 涛哥迷妹
    大神们的智慧也提供一个互相交流学习的机会赞一个👍
    2018-07-22
    作者回复

    读者卧虎藏龙👍👍

    2018-07-24

  • Only U
    我来说说我做过的一个项目设计的缓存策略吧,因为需要实时查询所有open状态的基础订单列表信息,所以有个后台进程每分钟刷新一次缓存。但因为open态order太多,导致redis序列化与反序列化过程太久而连接中断,后来我采用的一个方案就是根据每次查询必选的组合查询条件分别分组然后以单个查询值作为key放到缓存中,这样既达到了按照查询条件过滤的情况,又缩减了每次存取数据的字节数。可能比较简单吧,感觉也没有什么特别的。哈哈😄
    2018-06-11
    作者回复

    我们用过另外一种方式:将查询条件组合成字符串再计算md5,作为缓存的key,优点是简单灵活,缺点是浪费一部分缓存

    2018-06-12

  • bigticket
    为应对读写不平衡,高并发读取的业务场景,加入缓存机制;缓存又涉及到缓存穿透、缓存雪崩等问题,需要设计合理的过期时间,分页读取等
    2018-06-08
  • 万岁爷
    要是能写些demo就完美了。demo可以课件形式,放到网盘
    2018-06-06
    作者回复

    你可以自己写,不难

    2018-06-07

  • 大光头
    我们遇到的问题是代码bug导致缓存没有过期时间,缓存很快满了,缓存命中率降低,造成对业务很大的压力
    2018-06-06
    作者回复

    做好监控就可以及时发现

    2018-06-06

  • 何磊
    遇到过一次缓存失效导致缓存穿透,很多请求的压力直接到了db。为了处理这种情况,我采用的方案就是:key永不过期,后台有个进程定时更新所有缓存。
    文中提到的结合消息队列来更新更具有时效性,非常棒,看到评论中的有一个双key机制,设计很巧妙,不过成本太高了。相当于成本翻倍。
    2018-06-06
    作者回复

    除非特殊场景,一般我还是建议尽量用简单直观甚至粗暴的方案😂

    2018-06-06

  • june peng
    你好,在缓存雪崩后台更新策略里,比如1000个同时访问一个失效的缓存key,如果给这个key加读写锁,这样保证只有一个访问存储系统,其它999个人虽然慢点,但是至少能保证业务不会挂。如果用消息队列,就是前台只拿缓存key里的数据(不能访问存储系统),如果key不存在就发给消息队列更新,如果启多个进程去接受这些消息,依然不能避免后台击穿存储系统,难道只有启用一个进程?这样又太慢,可否进一步说明这种方案的复杂点在哪?谢谢
    2018-06-06
    作者回复

    你的分析很对👍
    具体在实现的时候,后台更新线程既不能只有一个,也不能和业务线程一样多,一般8~32个就差不多了,因为缓存更新并不会非常频繁。
    假如8个线程后台更新也可能导致缓存雪崩,那就要做更多事情了,例如:后台线程更新前先读取一下缓存,存在就不更新。

    2018-06-06

  • 王磊
    '后台更新既适应单机多线程的场景,也适合分布式集群的场景,相比更新锁机制要简单一些.' - 没觉得简单啊,因为如文中所说,后台更新也可能会出现多个业务线程都发送了缓存更新消息,这种情况我理解也需要有分布式锁来避免多个相同请求查询存储系统,否则在判断的时候,因为缓存在构建还没有准备好。
    2018-06-05
    作者回复

    通常情况下,业务线程数量要远远大于后台更新线程数量。假设20台64核机器,每台机器256线程,业务线程就是5120个,后台缓存更新线程数量一般8~32就足够了。

    如果缓存设计是只能一个线程更新,那确实也只能用锁了

    2018-06-06

  • sunny
    之前系统用阿里云memcached.有一段时间缓存总是失效。后来发现随着业务的增长,缓存的值越来越多。同一个value下的值过多会导致缓存系统存储失效。进而导致了文中提到的缓存血崩的情况。后来限制了缓存内容大小。解决了问题。
    2018-06-05
  • byte
    业务上通过Redis集群缓存网络数据,分布在北上广三地,3个数据中心,集群规模是80台物理机共1000个左右实例1主3从。线上出现过跨地域集群数据频繁全量同步打爆交换机的问题,导致整个服务不可用。通过排查发现是网络延时导致频繁全量同步以及服务器电源过热导致从服务器频繁重启。解决方案是跨地数据同步通过kafka试下,再接入各地Redis集群,电源问题通过更换硬件解决。
    2018-06-05
    作者回复

    缓存集群间一般不要跨数据中心同步,存储可以用跨数据中心同步

    2018-06-05

  • Snway
    订单设置的缓存有效期是永久的,但再一次上线后,更新缓存的后台线程出问题,导致用户查看不了最新订单数据,引起大量客诉!
    2018-06-05
    作者回复

    重启缓存服务器😂

    2018-06-05

  • 劉阳河
    缓存的用法还是不清楚,目前只是把基础数据放在redis中,但是基础数据并没有很大的性能问题,所以我感觉用处不大,而有性能问题查询列表因为有经常性的修改,所以没有存在redis。我现在的理解是讲读多修改少即时性要求不高的热点数据存储到redis,那对于上述查询列表怎么使用redis呢
    2018-10-15
    作者回复

    就算是修改频率是一分钟,缓存在这一分钟也是有很大作用的,因为一分钟可能就是几千上万次读操作了,所以不要以为要一天都不修改的数据才能用缓存

    2018-10-15

  • hello
    华仔,请教一下文中提到缓存热点问题,通过多副本方式解决,给每个副本的key设置一个编号,再随机读取,那么随机到的副本过期了要重新生成缓存吗?不生成怎么返回数据呢?
    2018-10-09
    作者回复

    所有缓存过期都要重新生成呀,如果正好读到一个过期的副本,可以继续用,缓存本来就不要求强一致性的

    2018-10-10

  • greatcl
    对于文中后台更新的例子有个疑问,如果当缓存系统内存不够时,一部分缓存被踢掉,后台去频繁读取缓存,发现被踢掉之后就立刻更新缓存。那这个时候因为内存不够,不是会有其它缓存被踢掉然后产生问题吗?
    2018-09-29
    作者回复

    1. 缓存被踢是正常的
    2. 不是每个缓存被踢都会引起问题,缓存有算法的,例如LRU

    2018-09-29

  • 黄哲
    有个榜单的计算过程比较复杂,大概要算3秒多,用户反馈榜单数据不对,由于对业务不熟悉,不知道计算过程这么复杂,就直接删了缓存,重新计算榜单,结果很悲剧,整个业务线挂了,一个典型的缓存雪崩案例。
    不过用了另一种解决的方式,将榜单计算的结果存在一张榜单表,缓存失效时直接读取这张榜单表,也算是一种思路吧。
    2018-09-25
    作者回复

    相当于双保险

    2018-09-26

  • 杨陆伟
    将查询条件组合成字符串再计算md5,作为缓存的key。======//请问这种方式有什么作用?例如查询出的商品变多或变少了,这个key马上就失效了
    2018-09-17
    作者回复

    查询结果不会影响key呢,至于说缓存不实时,这个没法避免,电商的商品缓存问题不大

    2018-09-19

  • laurencezl
    后台更新策略怎么选择哪些数据是需要缓存的?缓存的数据不是依赖查询么?
    2018-08-28
    作者回复

    是你选择的,程序员设计的时候需要确定哪类缓存要后台更新

    2018-08-28

  • 正是那朵玫瑰
    引用“我们用过另外一种方式:将查询条件组合成字符串再计算md5,作为缓存的key,优点是简单灵活,缺点是浪费一部分缓存”
    — — — — — — — — — — — — — — — — —
    没有太明白,为什么会浪费一部分缓存呢?
    2018-08-27
    作者回复

    用户输入的查询条件千变万化,排列组合太多了,很多的查询只会出现一下,缓存其实没有必要

    2018-08-27

  • lawlielt
    我们也是双key,一个存数据不过期,另一个存过期时间。过期时间的key用作多线程锁。
    2018-08-22
  • 文竹
    还没怎么深入接触过Redis,业务规模小,主要用来保存session,还没出现过问题。

    缓存穿透重在缓存无数据或失效,缓存雪崩重在缓存穿透后引起的崩溃,缓存热点重在热点数据。
    2018-08-19
  • 周威
    我们是cdn缓存通用url数据,rocksdb 缓存持久化实体数据,redis 缓存易变业务数据,空数据缓存防止穿透,每次数据库返回同时异步写rocks 作为降级雪崩网络延迟等意外情况的数据,设置开关可以开启本地缓存,同时启动定时任务去尝试读取最新db。由于分布式,真正开启降级方案由于每个机器本地不一样,会出现状态不一致,没有方案似乎
    2018-08-09
    作者回复

    你们的缓存方案很完善,但太复杂,容易出小问题,通常建议宁愿多花点机器成本,而不是设计这么复杂的缓存方案,《unix编程艺术》中有个原则:宁花机器一分,不花程序员一秒

    2018-08-09

  • yisshengyouni
    如果发现某个业务的缓存可能存在不一致问题,如何批量失效这些缓存呢,因为不能拿到所有的key。
    我们现在的做法是对每个业务加上一个前缀,如果不能确定哪些key出现不一致,就升级这个前缀,不知道还有没有好的方式?
    2018-08-05
    作者回复

    没有太好的办法,一种是缓存服务器重启,一种是将key前缀配置在文件中,全部失效的时候换一下配置就可以

    2018-08-06

  • 走神儿
    我们的缓存系统设计为三种,一种行级,就是数据库一条记录,表级,数据库表的查询结果,库级,连表查询结果,三种策略都是根据业务查询来缓存的。失效策略为更新A表,则A表的行级,表级,所有库级都失效;这样下来其实库级命中最低,但我们的代码规范是不到万不得已不能使用 join
    2018-08-02
    作者回复

    看起来性能高,实际上付出的复杂度代价更高,缓存管理会比较麻烦,一般不建议这么复杂的设计,很容易出各种数据不一致的问题

    2018-08-02

  • blacknccccc
    对于像淘宝商品列表筛选项特别多,组合起来会更多,这样在后台做更新缓存怎么处理,难道是把每一种组合的分页数据都缓存下来吗
    2018-07-22
    作者回复

    淘宝的具体实现没有研究,我们有类似的案例,针对常用的分类会统一缓存,缓存会主动更新;不常用的根据查询条件计算md5作为key 进行缓存,缓存时间不长,例如60分钟,防止短时间内大量访问压垮存储,例如爬虫

    2018-07-22

  • 涛哥迷妹
    华哥,请问下redis由于是单线程模型他的setnx分布式锁在高并发场景下会不会有性能问题?另外他的队列结构在redis多节点集群环境下是不是会有数据不一致问题?
    2018-07-22
    作者回复

    可以测试验证一下,我没有详细研究过

    2018-07-24

  • 涛哥迷妹
    看评论也能科普不少好东西啊
    2018-07-22
    作者回复

    必须的,我也能学到很多😀

    2018-07-22

  • bubble
    我们用过另外一种方式:将查询条件组合成字符串再计算md5,作为缓存的key,优点是简单灵活,缺点是浪费一部分缓存

    这个有创意
    2018-07-21
    作者回复

    黑猫白猫理论😄

    2018-07-22

  • bubble
    我们的系统就出现过类似的问题,开始的时候没有缓存,每次做活动访问量大的时候就会导致反应特别慢。后来通过加redis缓存解决了问题。

    对于缓存雪崩问题,我们采取了双key策略:要缓存的key过期时间是t,key1没有过期时间。每次缓存读取不到key时就返回key1的内容,然后触发一个事件。这个事件会同时更新key和key1。

    对以上疑问,既然是雪崩了,多少个key也没用吧!雪崩在此作何理解??
    2018-07-21
    作者回复

    雪崩是缓存读不到,大家都去读存储,你的场景是读取不到key就返回key1,因此不会雪崩

    2018-07-22

  • krugle
    我们的缓存都是下载业务代码里面,有数据不一致的地方要到处翻代码,有什么好的方法解决这个吗,百度了好多没结果
    2018-07-16
    作者回复

    缓存框架,由框架统一管理缓存,看看Ehcache

    2018-07-17

  • Simple_Zhang
    华哥,请问如何保证缓存和数据库的一致性,例如用户修改了一项配置,是先更新缓存还是先更新库?如何保证缓存和库都更新一致呢😊
    2018-07-04
    作者回复

    先更新库好些,因为更新库成功后即使更新缓存失败,缓存也有过期时间。

    如果要保证一致,更新库前先删除缓存,然后更新库,再更新缓存,但即使这样,也可能出现缓存和库不一致,因此要做到绝对一致是很复杂的,需要用到zk这类协调软件,一般不建议这么做,没必要

    2018-07-04

  • 梦里
    做游戏开发,数据先写入redis,用一个线程去读,然后写入mysql。遇到redis满了的情况,就是读的速度赶不上写的。很崩溃
    2018-07-04
    作者回复

    读的时候可以一次读取多个,进行批量操作,性能会好很多

    2018-07-04

  • 郭柱
    华哥,我现在来阿里云工作了,有机会希望能拜访华哥,和你分享一下之前我在上家公司做的一个系统。
    之前做过一个销售规则系统,这个系统服务于产品查询和销售。查询TPS峰值过万,查定比在100到200之间。规则系统的规则放到缓存中,同时支持规则实时发布到生产系统,性能测试指标是50ms就可以加载到生产系统。上线当晚,我就不停的在做规则修改,几分钟后查询系统变慢,开始大面积报警,当晚查询系统也在上线,后来过了一段时间发现是我们拖慢了整个业务链条,因为我们是新系统被迫下线改造,后来把缓存系统设计为AB缓存,每次加载新规则就加载到备用缓存中假设当前为B缓存,然后从日志中取1000生产查询请求,去查询最新更新的缓存,当100%成功后,将生产请求切换到B缓存,实际时间也就是秒级切换完成,这样保障了销售规则系统正常运转下去的,目前也还是这个架构。这种不考虑成本的方案是不是只有在国企才会用到?
    2018-06-27
    作者回复

    互联网用得更多啊,现在内存不值钱了,缓存大胆用😂

    2018-06-28

  • 孙振超
    在对qps有高要求的系统通常都会采用缓存,正所谓解决了一个问题之后就会有一个新问题出现,引入缓存在提升qps后最大的问题在于缓存db数据一致性问题。使用缓存的通常做法是先读缓存,缓存没有命中,就查询db,而后将查询到的数据添加到缓存中。在这种使用方法下导致缓存db不一致的主要情况是db和缓存没有同步更新,比如先更新db后更新缓存可能会出现db更新成功而缓存更新失败的情况,如果是先更新缓存后更新db则可能会出现缓存更新成功而db更新失败的情况。而解决这个问题可能要引入消息中间件或者利用分布式锁来保证,这样又给系统带来了复杂性。
    2018-06-24
  • 想飞的鱼
    我 off业务中涉及到很多金融各个分类的数据(股票,基金,债券,期货,宏观...),每个分类又有不同的业务类别数据,例如,股票有基础行情数据,还有各个上市公司的基础信息数据,财务数据......,由于业务场景,我们做缓存用了主动后台程序缓存,可以缓解数据库压力。但是这样造成了部分及时的行情数据无法及时更新到缓存中,后面我们部分业务改成了近期查库,及时查缓存,从一定程度解决问题。
    但是感觉现在开发业务复杂度变大了,需要主动缓存,业务程序查历史缓存,最新数据,进行合并。
    我们深知还需要继续学习,寻找更好的解决方案。感谢老师的分享。
    2018-06-22
    作者回复

    希望对你有帮助😊

    2018-06-23

  • 孙晓明
    我们的一个系统缓存是这样的:在数据库中定义了视图,将视图的结果缓存,永不过期;如果数据发生变动,先更新视图,然后再将视图结果更新到缓存中。后来也有用存储过程定时统计复杂的数据,并将统计结果存储到临时表中,程序访问时读取临时表的数据。
    2018-06-22
  • zwfec
    使用 update +1 的系统挂过,其实就是一个信息页统计访问数,修改了解决方法,使用js写入内存,再定时写入库
    2018-06-11
  • 大雁小鱼
    分布式缓存的设计关键点是什么?
    2018-06-10
  • echo_陈
    关于缓存穿透,比如,对用户缓存,因为每个用户的访问时间是均匀分布的……也不会同时缓存失效……这时候缓存穿透问题不大吧
    2018-06-08
    作者回复

    这种是正常的,只有短时间内大量数据都没法命中缓存才算缓存穿透

    2018-06-09

  • Mrsong721
    在以往的开发中遇到过这样的问题,某个数据做了缓存,key为k1,在后面的业务中发现也需要这个缓存数据,但是查询的key不是key1,而是key2,这种情况有什么好的解决方案么,曾经用过的方案是把key1与key2的对应关系也做了缓存,根据对应关系去缓存中查询想要的数据。类似的业务需求,有好的解决方案么?
    2018-06-07
    作者回复

    没明白,直接查key1为何不行?

    2018-06-07

  • 正直D令狐勇
    老师这里把缓存击穿和缓存雪崩合并在一起讲了。
    2018-06-06
    作者回复

    你是说爬虫的案例是缓存雪崩?我理解不是的,雪崩是由于缓存不存在且高并发访问,穿透是由于缓存不存在或生成缓存需要耗费大量资源

    2018-06-06

  • 顾海
    恶意攻击导致的缓存穿透,如果你的key不是非常多,可以使用布隆过滤器
    2018-06-06
    作者回复

    实际应用比较难把所有key列出来

    2018-06-06

  • MarksGui
    老师请教一下,比如数据库连表查询的分页结果,这样的数据怎么设计存入缓存了? 比如就用一个key,然后存储数据库查询结果的json吗?这个一直比较困惑。不知道key的设计怎么比较好。 感谢
    2018-06-05
    作者回复

    一般分页缓存,缓存id列表,不要缓存所有数据

    2018-06-06

  • 天天平安
    请问 阿里hybiddb for mysql,hybiddb for postgresql和maxcompute这三个产品有什么区别,分别在哪些情况选择对应的数据库合适?
    2018-06-05
  • qpm
    公司业务部署在某公司云上,有一次memcache故障,导致返回的数据总是为null。刚好系统设计是数据都先加载到缓存的,于是处理逻辑就认为系统里都没有数据,导致出现很大的数据问题。后来我们重新编写逻辑,不完全信任缓存进程,把一部分使用量较高的,数据不大的数据用JVM本地进行缓存。
    2018-06-05
    作者回复

    这里主要的问题是缓存使用不当,正常缓存返回null后应该去存储查

    2018-06-05

  • 十七
    现在项目重度依赖redis做统计计算,在请求量非常大的情况下,达不到低延时要求,不知道行业内有没有性能更好的实现方案
    2018-06-05
    作者回复

    之前看到有证券公司将依赖redis+lua的计算改为go去计算,redis只做存储

    2018-06-05

  • 李唐
    曾经设计过某安卓游戏平台,前台采用redis+solr+mysql多级缓存方案,考虑过缓存穿透,后来被新来的架构师否决了。个人感觉,业务架构重于技术架构
    2018-06-05
    作者回复

    否决的理由是什么?如果评估缓存穿透的可能性很低,不做确实也可以

    2018-06-05

  • yushing
    1、文本提到可以监控爬虫,发现问题后及时处理,请问具体是怎么处理呢?
    2、分页数据有很多排序规则,而且可能在某个时间点要上架新商品,就会有实时性的要求,请问这样使用分页缓存真的合适么?
    2018-06-05
    作者回复

    1. 监控数据库的各项指标,发现逐步变慢后看看是不是爬虫,只要系统还撑得住就让它爬,撑不住就不让它爬
    2. 缓存是为了解决性能问题,实时性要求很高就不能用缓存了,或者要做缓存及时更新机制

    2018-06-05

  • yushing
    业务系统中有使用reids存储商品list列表,每次添加、更新商品时,跟该商品有关的列表缓存都要跟着刷新;后来改成列表缓存中只保存商品id,查询时再关联商品信息,但这样只是在更新商品时不用更新商品列表缓存,在商品有添加、删除操作时,列表缓存更新的问题依然存在,请问对列表缓存的更新有什么好的方式呢?
    2018-06-05
    作者回复

    我觉得这样设计没什么问题呀,除非你们的list很大,如果list很大,分段缓存就可以

    2018-06-05

  • 啊哈
    如果采用商品分页缓存,怎么实现更新呢,商品对实时性还是有要求的吧?比如更改价格,或者新上架的商品,怎么能比较实时的展现?还有商品页还是很复杂的,除了筛选还有很多的排序等等,这些场景怎么应用缓存吗?
    2018-06-05
    作者回复

    缓存常见的列表操作,没法缓存所有的列表,因为搜索的条件太复杂了

    2018-06-05

  • byte
    想请教下对于流式音视频数据,类似优酷和爱奇艺这种网站后台的缓存系统如何设计比较合理?有没有推荐的方案或者书籍?谢谢!
    2018-06-05
    作者回复

    如果是媒体内容的缓存,那是CDN流式缓存的范畴,普通的后台缓存没什么特别的

    2018-06-05

  • 李志博
    最近有一个redis 存储数据过大的问题,字符串key ,大小2.5mb导致压测的时候发现带宽不够了,我优化的方式类似于后台更新的方式,启动的时候查一次,放到一个concurrent hash map 上,然后写个定时器每隔半小时更新一次,业务线程直接读map 里的数据,如果读不到,说明是新增的数据,我搞了个jdk 自带sync开头的queue ,先通过offer 往queue 里添加一个new object 触发一个异步线程更新缓存,同时业务线程在根据id 查单条,我还利用开关的观察者机制,留了个后门,可以通过人工方式触发缓存更新
    2018-06-05
    作者回复

    最好优化缓存设计,这么大的缓存,带宽都很浪费

    2018-06-05

  • haydenliu
    老师,关于缓存过期,我想问下,哪些场景需要加过期时间,那些不需要。另外,关于分布式锁,我想问下,基于zk的实现,和基于redis的实现,在阿里内部哪个用的比较多?为什么?
    2018-06-05
    作者回复

    1. 缓存的有效期和业务强相关
    2. 都有用,但哪个更多我没法统计,redis的简单,zk的功能强大,可靠性高

    2018-06-05

  • 钱浩亮
    老师我想问一下,redis有更新锁吗。再高并发的情况下,可以用吗
    2018-06-05
    作者回复

    redis的SETNX可以当锁来用,至于高并发是否可用,要看你期望达到多高的并发了,毕竟这是跨网络的访问,而且是单机支持,具体性能你要实测

    2018-06-05

  • 浪子恒心
    朗读者声音不错,很清晰!
    2018-06-05
  • 探索无止境
    文中提到了缓存穿透存储数据不存在的情况,存储空对象,但是如果黑客恶意发起连续的攻击,而且key不相同,那么也会让缓存服务器的内存被迅速占满,这个方案感觉还是有些问题的
    2018-06-05
    作者回复

    确实没办法😃😃

    2018-06-05