云音乐曲库缓存随着多年的实践、改善,结合曲库数据的特点,形成自有的一套缓存使用体系,并能够取得了很好的效果。
在工作过程中,有些喜欢刨根问底的同学会经常来讨论这样设计的原因,想在其它场景中借鉴一样的思路,在此想以文章进行历程介绍,让更多的同学了解背后设计的动机。
本篇文章较少形而上的理论知识,更多以实战为主,以解决问题作为切入点,讲述曲库缓存的设计之道

缓存基础介绍

缓存是系统设计中,用于提升底层系统访问能力的一种技术手段,它同样作用于云音乐的各个系统中,一种常用的缓存使用调用链路如下:

转化为时序图,如下图所示:

整个缓存的数据放入,是采用懒加载的方式,先取缓存,取到则返回,取不到则透过到下一层,拿到后会回写当前层的缓存,这是整个云音乐缓存使用的整体思路

在正式进入实战之前,介绍一些概念数据:

  • 一次简单的DB操作,耗时在 0.5~0.6ms
  • 一次简单缓存操作(非本机),耗时在 0.5~0.6ms
  • 一次简单的本机缓存操作,耗时在0.2~0.3 ms

云音乐曲库读是整个云音乐服务中接口调用量最高的几个之一,曲库读整体服务的rpc峰值调用qps能够达到 50w+ (双机房累加),通过多种缓存使用的尝试及调优,并最终从以下角度进行考虑并实践,得到较好的效果

曲库数据的特点

很多中间件、组件等设计,在考虑设计时,都会朝通用化方式去实现,而契合业务场景的特点,则更能将性能做到极致,曲库的缓存实现,是与曲库数据特性有着深度的联系,具体如下:

  • 读多写少
  • 可以读写分离
  • 数据变化秒级延迟用户不敏感
  • 热点数据集中
  • 通过List(列表)获取数据的场景很多,有大量 MultiGet 操作

有上述特点的业务场景,都可以参考曲库的缓存使用姿势。

实战场景1: 防穿透

什么是穿透?

缓存穿透是指缓存和数据库中都没有的数据,而不断有请求过来请求,导致请求不断向下到DB查询,会给底层带来极大的压力

这里有个简单而通用的思路

从缓存取不到的数据,在数据库中也没有取到,这时也可以在缓存中写入一个特殊值进行标记,缓存时间的设置可以视情况确定(如果主动清理可以设置长一点、否则短一点)

由于这种做法比较通用,故而在曲库封装的缓存代码中,将其通用化封装,即对于下面时序图,第四步进行设置:

实战场景2:扩容

扩容咋一看是比较简单的运维场景,但实际上也有很多考究,具体如下:

场景1:memcache容量不够

随着曲库数据量的逐步变大,memcache的占用量也越来越高,扩容memcache一个简单的做法,就是加机器节点,如果对于只有一组缓存(云音乐大部分都是这样),并且采用的是一致性哈希,需要在低峰期进行加节点(加的瞬间会有部分不命中);如果是普通哈希,则节点扩容风险非常高

曲库是一致性哈希,可以在低峰期加节点,但这里需要有一点需要注意,如果 MultiGet 的使用非常频繁,节点数的增加可能会导致性能下降

具体是什么原因呢?

我们使用Multiget一次性获取100个键对应的数据,系统最初只有一台Memcached服务器,随着数据量的增加,系统容量捉襟见肘,于是我们又增加了一台Memcached服务器,数据散列到两台服务器上,开始那100个键在两台服务器上各有50个,问题就在这里:原本只要访问一台服务器就能获取的数据,现在要访问两台服务器才能获取,服务器加的越多,需要访问的服务器就越多,所以问题不会改善,甚至还会恶化

从曲库的使用经验来看,底层节点控制在 4~6 个,性能与容量较为均衡

场景2:单组memcache性能达到上限

容量上解决后,还有就是memcache本身性能瓶颈,大部分场景下,我们系统的请求量达不到memcache的系统瓶颈。但假设memcache上限qps是20w,此时系统压力在 30w 该怎么做呢?

曲库侧的最佳实践,就是使用多组缓存,假设一组缓存的上限 qps,此时有两组一样数据内容的缓存,通过随机读的方式,是否上限能达到原来的两倍呢?

从这个思路出发,曲库进行了实践,实践的关键难点是:如何保障两组缓存数据是 一样 的呢?

这里设计了一个MemcacheManager的代理,所有的memcache操作均通过这个代理,而代理对于命令是:随机读、顺序写 的方式

关于代理

本文在此处使用的代理,是本地的一种动态代理实现,用于增强MemcacheManager的使用,具体如下:

这里简化下,假设,云音乐MemcacheManager的接口定义如下:

public interface MemcacheManager {public Object get(String key);public boolean set(String key, Object value);}

为了保持使用上一致,我们定义它的代理类 MemcachedSelect2ManagerImpl,但对于使用上进行增强,如下:

public class MemcachedSelect2ManagerImpl implements InvocationHandler, FactoryBean<MemcacheManager> {private final MemcachedSelector memcachedSelector = new MemcachedSelector();  // memcache选择器...@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {String methodName = method.getName(); // 根据方法名枚举,有限的方法名,如 Set/Get 等boolean syncWrite = (this.writeSync || this.forceWriteSync) && this.writeSyncMethods.contains(methodName); //根据设置以及方法名 判断是否需要顺序写if (syncWrite || syncMethods.contains(methodName)) {return call(method, args, this.memcachedSelector.getMemcacheds()); // 拿到全部的memcache进行顺序操作执行}return call(method, args, this.memcachedSelector.select()); // 如果不需要顺序写,则选一个memcache 进行读}
}

代理类大概上述实现

场景3:运维层面多组缓存如何扩容

上面说了一些理论层面的实践,实际过程中,使用多组缓存有个难点,从运维角度,如何加上新的分组呢?

如果直接添加一组空的缓存过去,则由于两组缓存瞬间是不对等的,如果此时就均匀访问,一定会引起很多超时,最初曲库是采用跑一遍曲库数据并设置Key-Value的方式,不过那样非常不灵活,过于大动干戈,后来通过在 场景2 中的代理类进行改造,通过设置缓存请求的比例来配置

如下,是曲库dg集群的缓存配置:

{"writeSync":true, // 表示同步写以下各组缓存"configs":[{"memcacheName":"music-rep2-g1-memcache_dg","servers":["music-memcache1.dg.163.org:11253"],"poolSize":16, // 连接数"opTimeout":1000, // 超时时间"rate":100,  // 读比例"codec":1,  // 序列化方式 1 标识 Hessian"hash":1 // 1表示使用一致性哈希},{"memcacheName":"music-rep2-memcache_dg3","servers":["music-rep-g3-memcache1.dg.163.org:11253"],"poolSize":16,  // 连接数"opTimeout":1000,  // 超时时间"rate":100, // 读比例"codec":1, // 序列化方式 1 标识 Hessian"hash":1 // 1表示一致性哈希}]
}

通过设置rate值来控制每组缓存的请求量,当设置为 0 时,则只写入,无读请求,作为预热;然后根据预热情况,逐步从0调整至100

同样的,为了更好的做均衡,将缓存的健康度也引入考虑,按照响应时间进行缓存分组的选择,这也是曲库缓存代码实现的一些细节技巧

场景4:容量够、性能不够,如何使用多级缓存、本地缓存

对于缓存的使用,更极端的一个问题是,如果memcache缓存的qps上线是20w,此时有 2000w 的qps请求,并且这些请求的接口或数据较为集中(2-3个接口数据占了 40% 请求量),有没有更好的方式呢?

这里的表现,就是容量足够、性能不能满足的情况,按照上述分组的方式,按照这样的比例,需要10个分组甚至更多,这样存放全量数据,也是非常浪费的。

针对这个问题,曲库采用了加上一级本地缓存的两级缓存策略处理,具体如下:

  • 在选择部署机器时,选择大内存的的云主机,并在云主机本身启动本地Memcache
  • 把核心高请求量的接口结果用本地Memcache进行缓存,同样适用懒加载方式

为什么使用本地部署Memcache 而不是JVM内做内存缓存呢?

  • Memcache进程与JVM进程隔离
  • Memcache独立部署,监控、干预等各环节较简单(包括淘汰策略)

以曲库读为例,每台机器的本地缓存请求量已经接近 0.6w,单机房200台机器,本地缓存拦截到的调用总qps可达到 120w,如果这些调用全部到上述的分组缓存中,则硬件成本、代理的顺序写代价也是很高的。

结合实战场景3的防击穿设置,其时序图如下:

什么样的数据比较适合使用本地缓存呢?

  • 调用量极高
  • 仍然是读多写少、变化少
  • 对象较小(因为本地缓存较小)

实战场景3:防止瞬间击穿

什么是瞬间击穿?

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发请求特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

这种的解决方式有多种,比如在请求数据库侧进行加锁、设置永不过期等(memcache最长30d)

曲库采用的是缓存穿刺的做法,具体是怎样的呢?

对于缓存中的 Key-Value ,将每个Value变成这样一个对象:

public static class HoleWrapper<T> implements Serializable {private long expire; // 对象的过期时间private T target; // 对象本身}

即每个在缓存中的对象,都带上自身的过期时间,这样在获取对象的时候,就知道缓存是否快过期了,如果能得到这个信息,结合业务特点 对于秒级延迟不敏感、热点数据集中,则可以这么进行设置,在曲库,我们称之为 穿刺 :

  • 通过 key 获取 HoleWrapper
  • 查看 HoleWrapper中的 expire 是否 快过期(快过期:可以定义5min、1h)
  • 如果是,当前线程将获取到的 HoleWrapper 的 expire 时间延长,并放入缓存(此操作耗时较少)
  • 当前线程向下穿透到下一层取数据,并将最新数据进行更新

时序图如下:

穿刺 体现在步骤3中,此处不能完全杜绝击穿的风险,但由于缓存操作远远快于DB操作,这样产生击穿的概率就下降了极多;有了穿刺,对于热点数据就能很好的做好防护,并且qps越高、越热点,越能体现优势。

实战场景4:缓存清理

缓存清理不是本篇内容的重点,不过曲库在实践中有个小技巧,在缓存 Key-Value 设计时一个遵循的原则:

  • 所有的缓存清理,由于曲库数据支持秒级延迟的特点,可以进行异步清理
  • 所有的缓存清理,由数据库变更(NDC消息)消息触发
  • 所有关联的Key,可以由单条NDC生成

能满足上述3个原则,曲库缓存的清理就能变得比较轻巧,可以采用异步监听NDC消息的方式进行清理

总结

以上,是曲库缓存使用的实践历程,涉及的细节较多,不同业务场景可以参考不同的考虑方式进行部分借鉴

后续曲库缓存的发展方向,是将元数据中额MetaData数据与状态数据分开,并将MetaData数据进行纯静态化处理,结合业务数据变化的特点,将状态部分数据的降级等引入考虑,进行更深度的缓存使用

特别注意

从时序图也可以看到,曲库缓存通过分级、穿刺等设计,增加了缓存操作的链路,所以,假设缓存命中率不够高,会将平均路径变长,进而会导致耗时增长,并非所有场景都适合直接使用,需要各业务场景结合各业务特点进行合理的采纳思维进行推进

作者:廖祥俐

看到这里的小伙伴,如果你喜欢这篇文章的话,别忘了转发、收藏、留言互动!

如果你想知道更多大厂同学亲授的干货,可以私信我!

还有新整理的海量资料,包含面经分享、模拟试题、视频干货…… 需要1V1的学习规划、职业规划也可以加V(weizhuanye234)

网易云音乐是怎么做曲库缓存的?设计动机大揭秘!相关推荐

  1. 网易云音乐PC端刷曲快捷键

    文章首发于szhshp的第三边境研究所(szhshp.org), 转载请注明 网易云音乐PC端刷曲快捷键 好吧我承认我特别懒 云音乐其实做的还不错,FM推荐的算法明显比虾米好. 虾米可以听的曲子都被我 ...

  2. 听歌识曲java_Android自定义View之继承扩展(仿网易云音乐听歌识曲)

    前言 上篇文章说到了自定义View的组合实战,链接:Android自定义View之组合实战(以支付宝页面为例) ,感兴趣的同学可以看看.今天要分享的是一个模仿网易云音乐听歌识曲界面的自定义View,实 ...

  3. Chrome插件:网易云音乐听歌识曲

    大家好,我是若川.持续组织了8个月源码共读活动,感兴趣的可以 点此加我微信ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步.同时极力推荐订阅我写的<学习源码整体架构系列& ...

  4. 【Vue知识点- No8.】网易云音乐案例(vant组件库的使用)

    No8.网易云音乐案例 知识点自测 知道reset.css和flexible.js的作用. 什么是组件库-例如bootstrap的作用. yarn命令的使用. 组件名字用name属性方式注册. 如何自 ...

  5. Axure教程(中级):网易云音乐听歌识曲效果模仿

    本教程给大家分享一下模仿[网易云音乐]APP的听歌识曲界面效果 做法如下,如有雷同,纯属默契: 一.页面布局 从左侧元件库拉入一个[椭圆形],尺寸为100*100,填充色为红色,再复制4个作为声波圆圈 ...

  6. python音乐的数据抓取与分析_python抓取网易云音乐热评做词图数据分析

    最近就有一部"怀旧"题材的电影,未播先火,那就是刘若英的处女作--<后来的我们>.青春,爱情,梦想,一直是"怀旧"题材的核心要素,虽然电影现在还未上 ...

  7. python爬虫网易云音乐评论最多的歌_Python爬虫44万条数据揭秘:如何成为网易云音乐评论区的网红段子手?...

    获取数据 其实逻辑并不复杂: 1.爬取歌单列表里的所有歌单url. 2.进入每篇歌单爬取所有歌曲url,去重. 3.进入每首歌曲首页爬取热评,汇总. 歌单列表是这样的: 翻页并观察它的url变化,注意 ...

  8. 网易云音乐的三点借鉴:功能、设计、运营

    近日,网易云音乐宣布用户数超过5500万,最近三个月用户总量增加近50%.看到这一新闻,我着实有点惊讶--这两年,音乐市场寒气未散.喧哗的表面下是功能设计.使用习惯.运营方式.盈利模式等产业链条上的一 ...

  9. 音乐无界限,听见好时光—网易云音乐Linux版震撼来袭!

    为了带来更好的音乐体验,实现对音乐高品质的追求,经过网易云音乐与深度科技团队长达半年多的联合开发,大家期待已久的网易云音乐正式登陆 Linux 平台! 网易云音乐是一款专注于发现与分享的音乐产品,依托 ...

最新文章

  1. 2019年备受关注的5个IT系统灾难恢复技术趋势
  2. java运行构建期间出错_构建和运行Java 8支持
  3. Python __str__() 方法
  4. Eclipse创建SpringMVC,Spring, Hibernate项目
  5. 怎样给 ActiveX 控件签名并打包发布
  6. php mysql 任务队列_PHP+MySQL实现消息队列步骤详解
  7. Docker Compose 引用环境变量
  8. mysql pdo 获取最后一条sql_一条sql语句的执行过程-mysql
  9. layui中弹出层的两种表达方式
  10. struts+spring+mybatis介绍
  11. Windows11安装JDK
  12. 小学多媒体计算机室管理计划,多媒体教室管理工作计划5篇
  13. 机械设计参考CAD零件图纸常用素材资料(300张)
  14. WEB攻防-ASP安全
  15. 上网认证页面html,如何设置网页认证上网
  16. Draco使用笔记(1)——图形解压缩
  17. 封装HDLC协议实现两个路由器互相通信
  18. 2011年中国程序员薪水,蛋疼。
  19. 人机测试计算机,人机测试
  20. Linux 显示行数 number

热门文章

  1. 双屏,ppt只在一个显示器上放映
  2. 阿里大牛都在读的10本Java实战书籍,Java开发进阶必备书单
  3. 【数据结构】使用Java实现顺序表类 SeqList
  4. pandas的read_excel 报错:OverflowError: date value out of range‘ 的解决办法
  5. c++获取剪切板的内容
  6. 明解C语言入门练习13
  7. Mac版本有道云笔记 同步失败问题
  8. 微信小程序——云函数操作数据库
  9. 2021年同等学力统考临近该怎么复习?
  10. case when then 中的条件里面加上 and or 的写法