端内请求池改造设计
背景
随着业务需求的扩展,大部分项目的网络请求变得复杂且无序,因为请求绝对孤立,没有设定优先级,也不存在什么依赖关系(只能通过业务嵌套的方式满足逻辑需求),因此,造成了请求挤兑(可能出现优先级反转),业务层逻辑负责度增加,代码臃肿,这对于快速迭代是极为不利的,前人会把坑越挖越大,之后来的人,修改成本也随之增加。
另外提一句,重构的意义不在于推翻旧的技术,建立新的技术,而是要将一些原本无序复杂的逻辑,变得有序简单。好的代码一定是学习成本很低的代码,学习成本过高,不利于代码迭代。
对于端内请求池的改造,也是秉承着大道至简的原则,希望可以通过高内聚的逻辑代码,对于业务代码进行简化。
设计思路
请求队列需要解决三个问题:
- 请求优先级
- 请求依赖问题
- 小盒子封装,避免大范围污染工程代码
请求优先级
优先级的设定是队列请求的基本逻辑规则,原则上,一个队列内出现空闲请求且无高优先级请求之后,才可以发起低优先级请求。如果所示:
图中只描述了吞吐量为1的情况,事实上如果增加吞吐量,会产生一个问题,这个我们之后再说。先看这个模型,业务层将请求交付给优先级处理,优先级处理将处理好的请求,插入到全局的缓存序列中,这里挖个坑,一会儿我们在把它填上。如果想要实现无限优先级序列,可以通过数字实现,发生优先级重叠的请求,可以按照FILO(先进后出)的原则进行请求,这样可以保证最新的请求先被执行。如果想实现有限优先级序列,需要提前设定好优先级,然后采用map对于请求进行优先级存储。
这是一个很简易的请求逻辑模型,之后我们需要处理一下几个问题:
- 吞吐量增加后,高优先级请求还没有插入缓存序列,此时递归请求池获取缓存序列中低优先级请求。此类问题如何解决?
- 全局序列可能导致请求挤压,当前页面无法及时获取数据。
对于问题1的解决,需要根据业务场景作为决策选择的主要依据,可以提供的方法,如果在有限优先级序列中,可以采用优先级锁的方式,等待上一个高优先级全部完成之后,再进行下一个优先级池的递归请求。
而问题2引出了下一个需要讨论的问题,分布式缓存序列。
分布式缓存序列
分布式缓存序列,通过将序列进行模块分发,从而解决模块请求积压的问题。如图所示:
如同线程与进程的关系,序列选择器本身类似于一个进程,内部的资源是序列共享的,但是交付到请求池的请求,是通过内部调度完成的,这符合面向对象的内聚原则,同时与业务实现了隔离。优先级处理器将tag与优先级信息一同传递给选择器,有选择器进行组合,当业务方也就是当前的请求方发生改变时,通知选择器进行重新调度,这需要有一个全局对象进行监听。
当然,在设计请求池时,最好设计成并发请求,这样可以保证请求的容错性,选择器发生调度时也可以及时的将新的请求插入到池子中。
最大吞吐量最好放在选择器中处理,这样可以保证请求池的功能单一。
到此优先级请求序列的基本思路介绍完了,接下来了介绍一下依赖型请求序列的设计思路。
请求依赖
理想状态下,请求链是有向且单一依赖的,但实际业务中,不可能存在如此理想的状态。很多请求是图形关系,多重依赖的。因此,如果想构建一个请求序列,需要对请求的图形依赖关系进行排序。
拓扑排序
对于拓扑排序的具体实现方法,这里就不做过多介绍了,不太了解的同行可以去网站上自行搜索。
这里需要讲到的是,采用拓扑排序,对于传入的依赖逻辑有几点要求:
- 数据需要有向型,也就是要写明依赖关系,如: A依赖B
- 避免双向依赖关系,如:A依赖B,B不能再依赖A
- 避免依赖图中出现环,如:A依赖B,B依赖C,C依赖A
前两种情况都比较容易规避,但是第三种情况,在业务关系依赖极为复杂的场景中,极为不易察觉,因此,在优先级处理器中,需要对环的异常情况进行抛出,以避免内部无限递归导致系统故障。
总结
优先级请求对于请求序列的控制,多数采用吞吐量,实现并发的高效性,因此对于吞吐量的控制,序列的选择在设计中应尽量做到容错性较好。
而请求依赖中,并不能采用吞吐量最大控制,因为会影响到并发的高效性,有可能一次可以把前十个依赖请求全部加在完,如果限制了吞吐量,分多次回调,这样是极为不合理的设计。
本文只阐述设计思路,具体设计类图还在整理,欢迎参与讨论。