【带并发限制的异步调度器Ⅱ】点餐排队系统Demo

要点:

  1. 店家可以设置任意张数的N人桌,根据自家容客量来设置,并且初始化时会给每张桌子一个编号。
    初始化桌子的数目以及能使用的人数:
    2人桌:3张, 4人桌:9张, 6人桌:6张, 8人桌:4张
const deskSettings = [
  { peopleNum: 2, maxExecutingNum: 3 },
  { peopleNum: 4, maxExecutingNum: 9 },
  { peopleNum: 6, maxExecutingNum: 6 },
  { peopleNum: 8, maxExecutingNum: 4 },
];
const scheduler = new Scheduler(deskSettings);

Scheduler内部初始化,根据N人桌来构造相应的任务管理器,因此会构造4个管理器(2人,4人,6人,8人),这4个管理器会放在taskMapper映射表中。当需要取用时,会从映射表中拿出来

//peopleNum表示几人桌,maxTaskNum表示这家店有多少个这样的桌子
const letterStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
class Scheduler {
  constructor(TaskSettings) {
    if ((TaskSettings||[]).length > 0) {
      this.taskMapper = {};
      for (const item of TaskSettings) {
        let { peopleNum, maxExecutingNum } = item;
        if (peopleNum <= 0) {
          continue;
        }
        let taskObj = {
          tasks: [],
          executingTasks: [],
          maxTaskNum: maxExecutingNum,
          idList: []
        };

        let letter = letterStr.substr(peopleNum-1,1);
        for (let i = 1; i <= maxExecutingNum; i++) {
          let idStr = letter + (i.toString().length<2 ? '0'+i : i);
          taskObj.idList.push(idStr);
        }
        this.taskMapper['peopleNum_'+peopleNum] = taskObj;
      }
    } else {
      this.tasks = []; //待执行的任务列表
      this.executingTasks = []; //正在执行的任务列表
      this.maxTaskNum = maxTaskNum;  //最大执行任务数
    }
  }
}

构造出来的taskMapper结构如下:tasks,待执行的任务列表;executingTasks,正在执行的任务列表,maxTaskNum,最大执行任务数,
idList,所有桌子的编号

{
  peopleNum_2: {
    tasks: [],
    executingTasks: [],
    maxTaskNum: 3,
    idList: [ 'B01', 'B02', 'B03' ]
  },
  peopleNum_4: {
    tasks: [],
    executingTasks: [],
    maxTaskNum: 9,
    idList: ['D01', 'D02', 'D03', 'D04', 'D05', 'D06', 'D07', 'D08', 'D09' ]
  },
  peopleNum_6: {
    tasks: [],
    executingTasks: [],
    maxTaskNum: 6,
    idList: [ 'F01', 'F02', 'F03', 'F04', 'F05', 'F06' ]
  },
  peopleNum_8: {
    tasks: [],
    executingTasks: [],
    maxTaskNum: 4,
    idList: [ 'H01', 'H02', 'H03', 'H04' ]
  }
}
  1. 由于每桌人的用餐时长不一样,因此为了模拟真实情况,会随机设定顾客的用餐时长:
//顾客随机用餐的时长:秒
const dinnerTimeArr = [10,20,30,40];
//从数组中随机取出元素,N不能大于数组的长度
function getRandomArrayElements(arr, N) {
  let indexArray = [];
  arr = arr || [];
  N = N || 0;
  if (arr.length === 0) {
    return [];
  }
  let max = arr.length - 1;
  let min = 0;
  for (let i = 0; i < N; i++) {
    let randomIndex = parseInt(Math.random() * (max - min + 1)) + min; //取随机数组索引值
    while (indexArray.includes(randomIndex)) {
      randomIndex = parseInt(Math.random() * (max - min + 1)) + min;
    }
    indexArray.push(randomIndex);
  }

  let randomArray = indexArray.map(index => arr[index]);
  return randomArray;
}

以及用eatDinnerPromise来表示顾客的用餐行为:

function eatDinnerPromise(peopleNum, deskNum) {
  return new Promise((resolve, reject) => {
    let dinnerTime = (getRandomArrayElements(dinnerTimeArr,1))[0];
    console.log(`我们是${peopleNum}人桌,桌子编号【${deskNum}】,【${moment().format('HH:mm:ss')}】开始用餐,将会进行${dinnerTime}秒。`);
    let ts = dinnerTime*1000;
    setTimeout(()=> {
      console.log(`我们用餐结束【${moment().format('HH:mm:ss')}】,桌子编号【${deskNum}】,用时:${dinnerTime}秒`);
      resolve();
    },ts);
  });
}
  1. 店家的桌子数据初始化完毕,顾客用餐行为也准备完毕,接下来就需要把这些代码串联起来。首先来了一桌顾客,先把他们加入系统,Scheduler中应有一个add方法,
add(promiseMaker, peopleNum) {
    let task = this.generateTask(promiseMaker, peopleNum);
    let taskObj = this.taskMapper['peopleNum_'+peopleNum];

    if (taskObj.executingTasks.length < taskObj.maxTaskNum) {
      this.run(task, peopleNum);
    } else {
      taskObj.tasks.push(task);
    }

    return task.id;
  }

而由于外部加进来的promise不一定符合我们的数据结构需求,就需要用内部方法generateTask来转换一下,

generateTask(promiseMaker, peopleNum){
    let taskObj = this.taskMapper['peopleNum_'+peopleNum];
    //从生成的数据中随机拿一个id编号出来
    let taskId = this.getTaskId(taskObj.idList);

    return {
      id: taskId,
      peopleNum: peopleNum,
      promise: promiseMaker
    };
  }

当需要从N人桌的编号池中拿一个编号出来时,会需要用到getTaskId方法,为了模拟真实情况,从编号池中拿取是采用的随机拿取,并且会暂时的从编号池移除此编号,当用餐结束再把编号归还。

getTaskId(idList){
    let taskId = (getRandomArrayElements(idList, 1))[0];
    idList.splice(idList.findIndex(id => id === taskId), 1);

    return taskId;
  }

以上讲的只是如何把外面promise封装成内部需要的任务结构,接下来就是运行任务,当正在执行的任务列表有空余时,会直接把任务加入此列表运行,使用的是run方法

run(task, peopleNum) {
    let taskObj = this.taskMapper['peopleNum_'+peopleNum];
    let arrayLength = taskObj.executingTasks.push(task);
    let index = arrayLength - 1;
    task.promise(peopleNum, task.id).then(() => {
      taskObj.executingTasks.splice(index, 1);
      //归还id编号
      taskObj.idList.push(task.id);
      if (taskObj.tasks.length > 0) {
        let newTask = taskObj.tasks.shift();
        newTask.id = this.getTaskId(taskObj.idList);
        this.run(newTask, newTask.peopleNum);
      }
    })
  }

这个任务结束之后,会做3件事:
(1)从正在执行的任务列表executingTasks中把自己移除
(2)归还任务(桌子)编号
(3)若待执行任务列表tasks中有任务,会从头部取出一个新任务,并从之前移除任务的编号池里拿出一个编号给新任务运行

  1. 为了了解排在自己前面还有多少桌,会用show来展示排队信息:
//获取现在排在前面的桌子数目
  show(peopleNum,deskId){
    let taskObj = this.taskMapper['people_Num'+peopleNum];
    let index = taskObj.tasks.findIndex(item => item.id === deskId);
    let executingTaskNum = taskObj.executingTasks.length;
    console.log(`您的${peopleNum}人桌编号为${deskId},有${executingTaskNum}在吃`);
    console.log(`排在您前面的还有${index}桌`);

    return {
      preTaskNum: index,
      executingTaskNum: executingTaskNum
    };
  }

以上就把Scheduler构造完毕了,接着用一些例子来测试下成果:

scheduler.add(eatDinnerPromise,2);
scheduler.add(eatDinnerPromise,2);
scheduler.add(eatDinnerPromise,2);
scheduler.add(eatDinnerPromise,2);
scheduler.add(eatDinnerPromise,2);

打印出来的结果:

我们是2人桌,桌子编号【B02】,【23:27:39】开始用餐,将会进行20秒。
我们是2人桌,桌子编号【B03】,【23:27:39】开始用餐,将会进行30秒。
我们是2人桌,桌子编号【B01】,【23:27:39】开始用餐,将会进行20秒。
我们用餐结束【23:27:59】,桌子编号【B02】,用时:20秒
我们是2人桌,桌子编号【B02】,【23:27:59】开始用餐,将会进行40秒。
我们用餐结束【23:27:59】,桌子编号【B01】,用时:20秒
我们是2人桌,桌子编号【B01】,【23:27:59】开始用餐,将会进行20秒。
我们用餐结束【23:28:09】,桌子编号【B03】,用时:30秒
我们用餐结束【23:28:19】,桌子编号【B01】,用时:20秒

完整的代码链接:点餐排队系统Demo
结语:
根据【带并发限制的异步调度器】可以做出一个能用于实际场景的Demo例子,这就是举一反三的实际运用。
当然Demo肯定不能用于实际的,只能是演示可行性,若真正需要投入实际场景使用,还需要更多的扩展,如:操作界面可视化,配置数据持久化,手动选择用餐桌子,打印排队信息,统计日均用餐数等。
如果练手这个项目,我会采用Egg+React来实践,到时候做出来会分享在github上,敬请期待。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容