原博客创建于 2016-9-18 22:34:48, 更新于 2017-6-21 22:00:00
需求
来自一个很现实的需求,有个业务只能串行执行,原来的代码的逻辑是弄个全局锁变量,当新的请求来的时候检查变量,在锁时直接返回错误让对方做重试,非常暴力。
很直观的改进方案就是用队列,系统是个用redis实现的伪消息队列,来多少推多少,根本做不到控流,而在代码内加队列又要考虑不能对原来的代码有太大改动(150行严重缩进横跨3个系统的业务代码,怼不动),还好原代码就是Promise写的,做流程控制好歹比callback简单多了
思路
Promise
本身的特性就是“保证在未来某一时刻会完成”,因此可以透明地加入队列,只要在未来某一时刻取出并处理,就可以继续向后执行原有代码,不会改变原有结构。队列本身要有节流功能,即可以控制同一时间内在运行的
Promise
数量,参考bluebird
的map
函数中的concurrency(并发)字段。
实现
比较核心的代码简化起来就这一段:1
2
3
4
5
6
7
8
9function add(fn) {
return new Promise((resolve, reject) => {
this.queue.push(() =>
Promise.resolve()
.then(fn)
.then(resolve, reject)
);
});
}
可以看到我们是往queue
队列里加入了一个函数,这个函数包裹了原函数fn
,将它执行的同步或者异步的结果传给外层的Promise
,这样对外表现就还是一个Promise
,这个函数进入队列,等待轮到它执行的时机
之后就是在内部维护一个“正在运行的任务数量”,在fn
运行前后做加减和判断,就可以控制并行数了
完整代码:np-queue
运行起来的感觉类似这样
1 | const q = new Queue(); |
默认并发数是1,因此代码会相隔1秒依次输出1,2,3,4
放到业务代码上,原代码是delay().then(...)
,现在改为q.add(delay).then(...)
,业务逻辑仍旧能跟在后面,原代码改动比较小,也易于理解
再配合wrap()
方法我们能更透明地完成这件事,它直接给一个函数提供了一层包装:1
2
3const wrapFn = function () {
return queue.add(fn.bind(thisArg, ...arguments));
};
原代码为delay(a,b).then(...)
我们可以改为delay_wrap(a,b).then(...)
,参数都不用改,透明地加了一层控制并发的逻辑,感觉很好。