移动应使用缓存板块设计方案的讨论

作者 : 开心源码 本文共3644个字,预计阅读时间需要10分钟 发布时间: 2022-05-12 共90人阅读

在大多数人眼中,移动应使用开发者做的事情很简单,无非就是:

获取数据(网络请求、本地读取) ===> 绘制界面 ===> 展现数据

或者者是:

绘制界面 ===> 获取使用户输入 ===> 将数据发送服务器

固然,在少量简单的应使用中,大量重复的这种工作;但其实当应使用涉及到少量特定的场景的时候,对数据只做简单的解决是远远不够的。

假设有这样一个业务场景:

有这样一组数据,数据具备以下特点:

  • 数据量特别大
  • 组装数据相当繁琐
  • 这组数据会在不同场合被使用到很屡次。

假设有n处会用到这组数据,按照我们之前的思路,我们需要去数据库读取n次这组数据;但显然,这样频繁的数据读取势必会对性能造成影响。

再假设开发过程中,遇到了大文件读取的问题,且这个文件有可能会在打开之后,连续的屡次被访问,那么按照正常的流程,就会屡次去数据库读取文件,打开文件,这样就会有一个问题,就是每次打开一个大文件时,使用户都需要等待少量时间,即便打开的文件是刚刚已经打开过的,这样势必给使用户一个不好的体验。

那么此时,基于以上问题,缓存 的作使用就表现出来了。

概念详情

提到缓存,大多数人可能第一印象中就会想到Cache,其实Cache应该是一个大的概念,从它字面意思来了解,是存储的意思。在大多数缓存的三方库中,像YYCache、PINCache、TMCache也都区分开了Memory Cache 和 Disk Cache。
那么,缓存到底是什么一个概念,个人了解,

缓存首先应该是存在于内存中的少量数据的集合,这个数据集合存在的意义就在于处理频繁的数据读取的性能消耗问题 或者 处理大文件打开速度偏慢的问题 。

基于以上概念,我们即可以把频繁用的数据 或者 会被屡次打开的大文件 缓存到内存中,当数据用者用数据时,只要去内存中读取数据或者文件,这样速度显然会提升很多。但,其实有利也有弊,缓存势必会造成程序占使用内存的增长,这里就会涉及缓存数据升级的策略 和 缓存数据大小的问题。这个问题我们后边小节会有继续探讨。

方案来源

在计算机存储结构中,也有一个缓存;尽管和我们这里的缓存概念不同,但其实我们也可以参考一下,下面我们来看一下计算机的存储结构;

计算机存储结构.png

在整个存储结构中,一共涉及了CPU中的寄存器、缓存、内存、辅存四个板块的存储器;基于存储器的特性,按照考核存储器性能的指标来比照:

速度 :寄存器 > 缓存 > 主存 > 辅存
容量 :寄存器 < 缓存 < 主存 < 辅存

为了兼顾速度与容量,计算机的存储结构分为两个层次,分别是:

1.缓存-主存层次
2.主存-辅存层次

缓存-主存层次主要处理CPU和主存速度不匹配的问题。因为缓存的速度比主存高,主要讲CPU近期要使用的信息调入缓存,CPU即可以直接从缓存中获取信息,从而提高访问速度。但因为缓存容量较小,因而需要不断的奖不断地将主存的内容调入缓存,将缓存原来的信息替换掉。

主存-辅存层次主要处理了存储系统容量问题。辅存的速度比主存速度低,而且不能喝CPU交换信息,但是容量要比主存大的多,可以存放更多使用不到信息。当CPU需要这些信息时,再将其调入主存,供CPU访问。

从CPU的角度来看,缓存-主存这一层次的速度近于缓存,高于主存;容量却接近主存;主存-辅存层次这一层次从整体分析速度近于主存,而容量又近于辅存;这样就处理了速度、容量、成本三者的矛盾。

参照上述计算机存储的处理方案,我们可以大致的去整理思路,做出一套类似的缓存方案;

方案详情

上面也详情了少量对于缓存概念的了解,但是假如要真正的去做一个缓存板块的时候,只考虑内存的缓存是不够的,而需要在内存缓存的基础上添加磁盘存储的功能。
那么,基于这个思路去考虑,缓存板块的设计应该包含以下部分:

1.内存缓存及数据的升级策略
2.磁盘存储及数据清除策略
3.磁盘<=>内存数据交互策略

首先,内存板块,我们需要先确认一个存储结构,例如数组、链表等;可根据业务场景选择不同的缓存算法,例如YYCache用的LRU算法;为方便实现相关算法这里可以选使用链表;

其次,磁盘存储板块,根据文件读写以及数据库书写的性能比照;当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快少量。基于 SQLite 的这种体现,磁盘缓存最好是把 SQLite 和文件存储结合起来:key-value 元数据保存在 SQLite 中,而 value 数据则根据大小不同选择 SQLite 或者文件存储。参照

对于磁盘-内存数据交互的策略,可以参考下图:

数据读取流程.png

首先,数据用者用数据时,会调使用缓存板块API查找数据,缓存板块内部会先在内存中查找,假如有则直接返回,并且根据相应的算法操作当前数据,假如没有则去磁盘查找,磁盘有则返回数据,并且把数据加载到内存中,没有的话则返回无数据结果;

问题点

因为缓存用的一个特点,会有多个数据用者同时操作数据,对数据进行读写,而多个数据用者很有可能是不同线程所持有的对象,那么就会有一个问题,多线程在同时操作同一数据的时候,造成资源竞争问题,并极有可能导致程序直接崩溃;

对于这个问题,常见的有两种处理方案:

  • 加锁
    既然是资源竞争问题,那么很简单的即可以联想到加锁,的确,加锁可以是解决这种问题的一种处理方式。或者者说我们可以将属性用 atomic 关键字来修饰;
    但其实这两种都有肯定的缺点的;atomic关键字可以保证读的安全,加锁也可以是先读写安全,但是大规模用锁很可能会导致出现各种不可预测的问题,锁竞争,优先级反转,死锁等,会让整个APP复杂性增大,问题难以排查,并不是一个好的处理方案;

  • 数据不可变
    数据不可变指的并不是指缓存数据完全不可变,而是指对于我们存储的对象单位的属性的不可变。在这里,可以给所有数据对象增加一个标示:如 change,标示的作使用就是这个对象是不是已经改变。那么,当数据用者对数据进行修改的时候,不是直接对对象进行修改,而是生成一个新的对象,去替换老的对象;

这样做的优点就是:即便同时有多个数据用者读写数据,也不会造成资源争夺问题导致程序崩溃,但这样同时也有一点小瑕疵,就是当我们缓存数据升级时,但是,数据用者并不知道数据升级,用的依然是老数据;这里就涉及到数据升级后通知用者的策略了。

  • push

push 的方式就是 cache 层把升级 push 给上层,cache对整个对象升级替换掉时,发送广播通知上层,这里发通知的粒度可以按需求斟酌,上层监听自己关心的通知,假如发现自己持有的对象升级了,就要升级自己的数据,但这里的升级数据也是件挺麻烦的事。

举个例子,加入有一个列表HYViewController,存着一个数组 objs,保存着 obj 数据对象。而后这时 cache 层通知这个 HYViewController,某个 obj 对象有属性变了,这时这个 HYViewController 要怎么解决呢?有两个选择:

  • 遍历 objs 数组,取到obj对象,假如升级的是这个 obj 对象,就把这个对象替换升级。
  • 什么都不论,只需有数据升级的通知过来,所有数据都重新往 cache 层读一遍,重新组装数据,界面一律刷新。

第一种是精细化的做法,优点是不影响性能,缺点是蛋疼,工作量增多,还容易漏升级,需要清楚知道当前板块持有了哪些数据,有哪些需要升级。第二种是简单粗暴的做法,优点是省事省心,一律大刷一遍就行了,缺点是在少量复杂页面需要组装数据,会对性能造成较大影响。

  • pull

pull 的方式是指上层在特定时机自己去判断数据有没有升级。

首先所有数据对象都会有一个属性,暂时命名为 change,在 cache 层升级替换数据对象前,先把旧对象的 change 属性设为 YES,表示这个旧对象已经从 cache 里被抛弃了,属于脏数据,需要升级。而后上层在合适的时候自行去判断自己持有的对象的 change 属性能否为 YES,若是则重新在 cache 里取最新数据。具体步骤如下:

1.将要修改对象的change标示修改为YES
2.初始化新的对象,将需要修改的值赋值给新的对象
3.而后将缓存中对象替换为新对象

实际上这样做发生了多线程读写 change 属性,是有线程安全问题的,但由于 change 属性读取不频繁,可以直接给这个属性的读写加锁,不会像对所有属性加锁那样引发各种问题,处理对这个 change 属性读写的线程安全问题。

这里主要的问题是上层应该在什么时机去 pull 数据升级。可以在每次界面显示 -viewWillAppear 或者使用户操作后去检查,例如使用户点个赞,即可以触发一次检查,去升级赞的数据,在这适当的地方做检查其实基本可以处理差不多的问题,剩下的就是同个界面联动的问题,例如:在商品介绍里面收藏了这个商品,在push过来的上级界面也需要展现收藏的标示,也可以结合上面 push 的方式去做通知。

push 和 pull 两种是可以结合在一起使用的,pull 的方式弥补了 push 后数据一律重新读取大刷导致的性能低下问题,push 弥补了 pull 升级时机的问题,实际用中配合少量事前制定的规则或者框架一起用效果更佳。

说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 移动应使用缓存板块设计方案的讨论

发表回复