继上一篇场景之后,事情还没有完。
我有一堆struct obj对象(数量级可能为千级), 客户端需要频繁拉取这些信息中的一部分去显示(比如,当切换标签页时)。
由于这一操作可能会很频繁,而struct obj对象并不算小,如果每一次都重新拉取全部数据,有点让人不舒服,而且对流量也是一种很大的浪费。因此就琢磨着怎么去优化整个过程,以使在此过程中使数据传输量最小。
第一反应,肯定就是对整个列表加一个version字段,每当有obj对象改变时,version加1,当客户端进行拉取时,拿着他本地存储的version值向服务端要列表,如果服务端的version没有比客户端version更新,则直接返回空即可(ps.之所以最开始想到这个办法,是因为有先例 :D)。
但是仔细分析一下整个场景,就会发现这个方法并不太适用这些场景。
一方面,所有obj对象共享着同一个version字段,就意味着只要有任何一个obj对象变动,version字段都会自增。因此理论上obj对象越多,version就变动越频繁,当obj多到一定程度,version机制基本上就形同虚设。
另一方面,由于客户端只需要拉取所有obj对象中的一部分,所有的obj对象共享同一个version也显得有些太过浪费。因此这个方案很容易就被放弃了。
另一种解决方案是,客户端在本地实现一个cache模块,当cache为空时向服务器拉取所需信息。此后再切换标签, 直接从本地cache模块取数据。只有当通过本地cache进行操作(如:通过某按钮根据cache中的obj对象的状态,向服务器发送请求)失败时,再清空本地cache重新从服务端获取。
然而,这种方案有另外一个坏处,就是用户每隔一段时间必然要出现一次错误。对用户来讲似乎不太友好。
经过仔细思考之后,对方案一和方案二进行了综合。来同时去除两种方案的所带来的弊端。
首先将版本号与每一个obj对象进行关联,这样就可以精确指示出某一个对象是否比客户端新。
再将拉取列表命令拆分成两条命令,pull_list和pull_info。
在客户端需要拉取列表时(比如切换标签页), 直接调用pull_list,而pull_list返回一个item列表。item结构体如下:
struct item {
int id;
int version;
};
可以看出这个item极其简单,就算是频繁拉取,数据量也还可以接受。
客户端依然会在本地维护一个cache, 而这个cache的数据结构可能类似于std::unordered_map<int, struct obj>一样的数据结构。
当客户端收到pull_list的回应之后,拿着item列表去cache中查询,确认cache中是否存在这些数据。如果数据存在,则检查服务端返回obj对象的version字段是否比cache中新,如果较新则从cache中删除些obj对象。
然后拿着cache不存在或失效的id列表使用pull_info命令向服务器请求数据。由于在相对短时间段内不可能所有obj对象都会有状态改变。因此,在客户端频繁拉取信息时,cache必然有极高的命中率,这也意味着会极大的节省数据流量开销。
此外,此方案还存在一个问题,那就是列表是随时动态变化的,而其中的obj对象还会随着某些特殊的操作或时间的流逝而消亡。
那么客户端如何才能感知这些变化, 并从cache中删除这些已经不存在的obj对象呢。
遗憾的是,客户端似乎真的没有办法来感知这些变化。
为了解决这个问题,我们做了一个约定,假设客户端一次拉取的列表为N,当客户端cache中obj对象的个数大于2N时,就意味着cache中至少有一半对象可能都已经消亡了(ps.只是有可能)。这时侯直接把cache清空,然后从新开始cache。
BTW, 其实约定为2N实为无奈之举,按我最初的想法,应该是用一个类似weak table的数据结构来cache。这样当内存不够时,可以由GC来自动释放这些内存,来保证客户端依然能够顺利流畅的运行。在Java中可以用WeakHashTable来代替,可遗憾的是在C#中并无此类型数据结构。