前端中的异步任务在高并发场景下的解决方案(javascript)

866 阅读6分钟

问题阐述

在我们前端开发过程中 进行ajax请求是很普通的场景,但是在某些极端场景下,页面可能会在短时间内发送大量ajax请求,这样的情况下会有一下坏处:

  1. 会有大量处于pendding状态的异步任务被挂起,浪费内存如果处于pedding状态的异步任务足够多的话,甚至会爆栈从而导致页面卡死。
  2. 这些请求里可能会有一些是接口相同,但是在不同的地方反复请求的情况,这样不仅浪费客户端资源也会浪费服务端资源。
  3. 影响浏览器性能,导致有些真正需要被执行的任务因为这些异步任务回调触发而造成页面卡顿的现象。

解决方案

根据上述情况,我想出了以下两种解决方案,欢迎还有方案的朋友一起讨论

  1. 请求缓存,利用缓存思想,实现一个有以下功能的方案:
  • 可以让 在一定时间的相同接口名的ajax请求只请求一次,在该时间内的其余请求都会返回第一次请求缓存起来的结果。
  • 可以设置最大缓存数量,超过该数量就会将最长时间未调用的接口缓存给清理掉
  • 可以设置最大缓存时间,超过改时间未被使用的接口缓存将会被清理掉
  1. 请求队列,利用队列的思想,实现一个请求队列,需要能支持以下功能:
  • 可以设置同时正在执行的异步任务的数量,在正在执行的任务数量超过预设值时,就会将之后添加进来的任务进行一个排队的操作,并且最先进队列的异步任务回调函数会排在队首,最先执行
  • 在一个异步任务执行完成之后,会从等待执行的异步任务队列中,从队首取出一个来执行,且在任一一个异步任务完成之后都会执行该操作

方案实现

方案1 接口缓存

在方案一种,我们首先需要实现一个lru-cache的操作,思路可以参考leetcode.146,直接上才艺!

class LruCache {
            constructor(max) {
                this.max = max//最大缓存长度
                this.cache = new Map()//用一个map来存储缓存
            }
            get(key) {
                if (this.cache.has(key)) {//如果该值存在 则将该值从队列拿出来放到最后一位
                    const val = this.cache.get(key)//1.取出来
                    this.cache.delete(key)//2.从现在的位置删除
                    this.cache.set(key, val)//3.放在队尾
                    return this.cache.get(key)
                }else return undefined
                
            }
            set(key, value) {
                //如果当前值存在 就把当前值删除
                if (this.cache.has(key)) {
                    this.cache.delete(key)
                }
                //然后将当前值放在队尾
                this.cache.set(key, value)
                //如果在进行set操作后 超过了预设长度 则把队首的删除
                //这也就是为什么会把重复get的值放在队尾
                //原则上来说 被删除掉的都是在一定时间范围内使用频率不高的缓存值
                if (this.cache.size > this.max) {
                    this.cache.delete(this.cache.keys().next().value)
                }
            }
        }

这样 我们就基本实现了一个lru-cahche。

接下来我们再基于上述例子,在实现一个定时过期的功能。

我的思路是 在数据set的时候同时进行一个settimeout操作, 并且在删除数据的时候进行一次cleartimeout,直接上代码!

        class LruCache {
            constructor({ max, time }) {
                this.max = max
                this.time = time
                this.cache = new Map()
            }
            has(key) {
                return this.cache.has(key)
            }
            get(key) {
                if (this.cache.has(key)) {
                    //valu是一个数组,该数组的第0个元素是set的时候的数据,
                    //第一个元素是settimeout返回的事件句柄,清除定时器时能用到
                    const val = this.cache.get(key)
                    clearTimeout(val[1])
                    this.cache.delete(key)
                    const timer = setTimeout(() => {
                        this.cache.delete(key)
                    }, this.time);
                    this.cache.set(key, [val[0], timer])
                    return this.cache.get(key)[0]
                } else return undefined
            }
            set(key, value) {
                if (this.cache.has(key)) {
                    const val = this.cache.get(key)
                    clearTimeout(val[1])
                    this.cache.delete(key)
                }
                const timer = setTimeout(() => {
                    this.cache.delete(key)
                }, this.time)
                //在set操作的时候一定要将settimeout返回的事件句柄保存起来
                //用于在适当的时机进行清除定时器操作
                this.cache.set(key, [value, timer])
                if (this.cache.size > this.max) {
                    const dk = this.cache.keys().next().value
                    clearTimeout(this.cache.get(dk)[1])
                    this.cache.delete(dk)
                }
            }
        }

基本思路和之前哒lru-cache一样 只是在每个set的地方加上了一个settimeout,并在delete的地方加上一个cleartimeout 接下来 我们来模拟一下真是使用场景:

 const cache = new LruCache({ max: 10, time: 1000 * 10 })
        function myAjax(url, type) {
            return new Promise(reslove => {
                if (cache.has(url)) {
                    reslove(cache.get(url))
                }
                // $.ajax({
                //     method: type,//get或post,情趣方式
                //     url,//接口地址,
                //     success: function (res) {
                //         cache.set(url, res)
                //         reslove(res)
                //     }
                // })
                //由于没有真实接口 这里我们用settimeout模拟
                setTimeout(() => {
                    const value = url + 123
                    cache.set(url, value)
                    reslove(value)
                }, 1000);
            })
        }
        myAjax('xxxxxxxxxxx', 'get').then(res => console.log(res))
        myAjax('asdasdsadsa', 'get').then(res => console.log(res))
        myAjax('asdasdsadsa', 'get').then(res => console.log(res))
        myAjax('qweqweqweqw', 'get').then(res => console.log(res))

现在 我们就实现了一个接口缓存的功能,且支持过期接口数据清理的功能

方案2 异步任务并队列

首先 我们需要实现一个队列,这个队列支持增加异步任务,但是同时执行的异步任务数量是有限的,需要等待当前正在执行的异步任务中,任意一个执行完毕之后,才会从等待执行的异步任务队列的队首取一个出来执行,上代码!

 class asyncTask {
            constructor(max) {
                this.max = max
                this.count = 0
                this.taskQueue = []
            }
            add(fn) {
                this.count++
                //总任务数  减     待执行任务数     大于  允许同时执行的任务数量
                //(couunt)  - (taskQueue.length)   >        max + 1
                //则说明现在增加的这个任务需要到队列中等待合适的时机再执行
                if (this.count - this.taskQueue.length > this.max + 1) {
                    //任务超了 排队,且return一个promise 用于在使用的时候接收.then
                    return new Promise(resolve => {
                        //resolve为执行时机,将resolve保存在taskQueue
                        //便于在 当前正在实行任务 的 数量 小于 最大任务数量(this.max)时,将fn作为参数执行后传给resolve,并执行resolve
                        //在合适的时机就像这样执行 👇👇👇
                        // const task = this.taskqueue.shift()
                        // task[1](task[0])
                        this.taskQueue.push([fn, resolve])
                    })
                } else {
                    //没超 直接执行
                    return this.exec(fn)
                }
            }
            exec(fn, re) {
                return new Promise(resolve => {
                    fn().then(res => {
                        this.count && this.count--
                        //如果当前还有在this.taskqueue中还有任务等着执行,并且当当前剩余任务数量小于正在执行的任务数量时
                        //就拿一个带执行任务出来执行
                        if (this.taskQueue.length && (this.count - this.taskQueue.length < this.max)) {
                            const task = this.taskQueue.shift()
                            // 这里采用递归调用exec,作用是:
                            // 便于当前异步任务执行完成时 也会进入上面的if判断,以便于执行队列中的下一个任务
                            this.exec(task[0], task[1])
                        }
                        re ? re(res) : resolve(res)

                    })
                })
            }
        }

接下来我们来尝试使用以下:

 const asynctasks = new asyncTask(3)
        asynctasks.add(() => new Promise(resolve => setTimeout(() => resolve(111), 5000))).then(res => console.log(res))
        asynctasks.add(() => new Promise(resolve => setTimeout(() => resolve(222), 1000))).then(res => console.log(res))
        asynctasks.add(() => new Promise(resolve => setTimeout(() => resolve(333), 1000))).then(res => console.log(res))
        asynctasks.add(() => new Promise(resolve => setTimeout(() => resolve(444), 2000))).then(res => console.log(res))
        asynctasks.add(() => new Promise(resolve => setTimeout(() => resolve(555), 4000))).then(res => console.log(res))
        asynctasks.add(() => new Promise(resolve => setTimeout(() => resolve(666), 3000))).then(res => console.log(res))
        asynctasks.add(() => new Promise(resolve => setTimeout(() => resolve(777), 1000))).then(res => console.log(res))
        asynctasks.add(() => new Promise(resolve => setTimeout(() => resolve(888), 1000))).then(res => console.log(res))
        asynctasks.add(() => new Promise(resolve => setTimeout(() => resolve(999), 1000))).then(res => console.log(res))

这样就实现了异步任务高并发场景下的控制了,当然,极端情况下,我们还可以把这两种方式结合使用,具体就是实现以下效果:

  1. 在设置的时间内执行的异步任务都会被缓存,如果在时间内再次请求相同接口则会返回上次请求的返回数据,而不会发送真实的请求
  2. 在进行异步请求的过程中,可以控制最多同时执行的异步任务的数量,如果超出数量限制 则会放入队列中等待执行

这就是我的方案了,欢迎有别的思路的朋友一起讨论哦