波普特廉价酒店-北京邮电大学软件工程课程设计

1. 前言

这是北京邮电大学2023年秋季-软件工程课设的作业,设计具有调度功能的波普特廉价酒店的空调管理系统

这也算是朝花夕拾,过了一个学期之后,我又重新审视了之前的内容,在这里生成一篇博客,希望能帮助到更多后人。

之所以重写一遍,一方面是之前的Python版本后端总是会出现问题(组员写的,有未知bug,也没有做任何异常处理,很容易导致程序崩掉),本人实在无法忍受这样的代码工作存在在我的仓库中,于是自己手动重写了一遍后端代码,使用语言为Golang。

github链接:https://github.com/SamuraiBUPT/bupt-hotel-management

有关本次任务的详细设计要求,可以参考我上传的课设PDF:

https://github.com/SamuraiBUPT/bupt-hotel-management/blob/main/help-docs/bupt-hotel-requirement.pdf

因此,我们有两套技术栈:

  • 一套是Vue + Element Plus + Python Flask + MySQL(有未知bug,至今没法定位,我也实在难以忍受屎山的痛苦)
  • 另一套是Vue + Element Plus + Gin + GORM (目前未发现任何问题,但是工程仅仅完成到了scheduler实现为止,有一些前端的接口没有完善,但是那些都是crud boy的小活了,自己动动手就能完成的,我懒得弄了,这边时间也比较紧)

这套重构之后的系统,我还是比较满意的,一方面没有python的未知bug,其次是利用了golang的协程,程序有了更好的性能。整体coding的过程比较爽。

写这份博客,有几个目的:

  • 拯救后面的学弟学妹于水火之中
  • 提供了一个良好的scheduler范本
  • 因为我时间不足,留下了一些CRUD的小活,也有一些超级简单的接口没有来得及写。核心的工作做完了,小活大家自己去做即可。

2. 数据库表设计

2.1 房间表

房间表:

  • room_id(pk),
  • client_id(varchar), // 客人的身份证号
  • checkin_time(datetime),
  • checkout_time(datetime),
  • state(int), // 有没有人入住
  • cost(float),
  • current_speed(string), // 当前风速,缺省为medium
  • current_tempera(float32) // 当前温度,缺省为24

2.2 操作表

操作表:

  • id(pk),
  • room_id(int),
  • op_time(datetime),
  • optype(int), // 因为有很多种操作,比如说有开关机、调风速、调温度,每一种都对应一个type
  • old(varchar),
  • new(varchar)

用户的操作主要有以下几种:

  • 开关空调
  • 调整温度
  • 调整风速

所以optype暂时定为1-3.

  • 当optype为1时,old_state和new_state分别为开和关,
  • 当optype为2时,old_state和new_state分别为之前的温度值、设定后的温度值。
  • 当optype为3时,old_state和new_state分别为风速的旧值和新值。比如说low, medium, high

操作表的核心作用就是作为一个reference。

2.3 详单表

详单表:

  • id(pk),
  • room_id(int),
  • query_time(datetime),
  • start_time(datetime),
  • end_time(datetime),
  • serve_time(float),
  • speed(varchar),
  • cost(float),
  • rate(float),
  • status(varchar)

详单表只用作一个记录,是结果表,不具备任何reference的意义。

为什么会有·serve_time·呢?难道不是 e n d − s t a r t end-start endstart就完事了吗?

并不是的。因为在运行过程中,会有调度系统,也就是空调资源会被抢占,会出现没有送风的情况。

比如说你的请求是第1秒来的(query_time),然后因为大家都在用,所以你还在waiting queue里面,你有可能在第3秒才开始被serve (start_time),在serve的过程中,因为你有可能被抢占,所以你时而在running queue里面,时而又被挤到了 waiting queue里面。这就导致了你的serve_time不等于end - start这么简单。比如说你是在第10秒结束的服务(end_time),但是实际上,从第3秒到第10秒之间,你可能只被serve了5秒(serve_time),剩下的时间都被抢占了。

这就是为什么会有这么多字段。

rate是费率,由课设pdf和老师的要求决定。

3. 核心:调度算法设计

有且仅有调整风速的时候,会被scheduler掌握。

这个是按照风速大小进行调度的。当一个新的风速设定到来之后,如果发生了时间片的抢占,那么先会记录这个操作入库(操作表)

然后这个操作的任务会进入scheduler的waiting_queue中。然后会依次处理里面的请求。这个是任务的waiting_queue,不是当前房间的waiting_queue

风速越大,优先级越高。比如说如果有high存在的话,有可能medium和low都会被挤占,甚至hunger(这是文档规定的),我们只负责实现与执行代码工程,不负责质疑老师的要求。

当task_waiting_queue里面有任务的时候,我们从中读取任务,然后进行调度。任务先出队,然后决定它在serving_queue中还是在waiting_queue中。

我们需要一个数据结构,来记录房间的serve进展。

Slots: room_id, start_time, speed即可。

之后就是校验时间、滚动这个slots中的房间位置,

3.1 调度算法

考虑到有一个scheduler,我们将让这个scheduler持有两个goroutine。

一个负责进行scheduler的step操作,每隔一秒钟step一次,进行调度算法的工作。

另一个负责处理来自handler的任务。面对到来的请求,我们在handler层就已经可以进行数据库IO操作。在经过数据库IO之后,再把信息数据更新给Scheduler,便于Scheduler进行Step。

如图所示:

架构图
这种设计出于两个考虑:

  • 如果是先把数据发送给scheduler,再由scheduler决定数据是否持久化到数据库中。整个过程比较复杂,比如说:
    • scheduler要还要涉及一堆判断逻辑,需要判断要把handler的信息中的哪些内容,更新到哪个表中,这是很复杂的判断逻辑
    • Task不好写,因为task的种类繁多,不可能全部覆盖完,代码量会非常恐怖。
    • scheduler本身还要进行schedule,schedule完了之后还要把schedule的内容给IO到数据库里面,加上之前的IO,他要分别进行两种不同的IO操作。
  • Scheduler仅仅是负责scheduler,它基于的信息来源就是自己的一套信息,它输出的地方就是数据库。所以其实是handler和scheduler都要对数据库有CRUD操作。scheduler除了schedule的逻辑之外,不负责执行任何除此之外的任务。

只有第一个管道,传输进来的信息,决定list中slot的滚动状态。scheduler内部的step仅仅负责进行滚动操作。

维护两个队列:waiting_queue和running_queue.

任何一个元素,在最初的时候都是在waiting queue当中。之后会被调度进入running queue。

整体调度的决策树是:

决策
也有一份基本的代码骨架:

if waiting_queue.Len() > 0 {
    if running_queue.Len() < s.ServingSize {
        // 可以进入
    } else {
        first_e_run := running_queue.Front()
        first_e_wait := waiting_queue.Front()
        if first_e_wait.Value.(*Slot).Speed >= first_e_run.Value.(*Slot).Speed {
            // 发生抢占
        }
        // 如果到这里了,说明waiting会阻塞。
    }
} else {
	// running queue内部循环
}

有关结算

结束的时候要进行结算,去详单里面结算。只有能够被结算的房间才会被持久化到数据库中,比如说仅仅是时间片被抢占出来了,不能被结算,因为query_time是请求到来的时间、start_time是开始服务的时间,中途可能被抢占出来,并不算做serve结束了,因此end_time和serve_time这两个字段没有办法填充数据,这个时候就不算做结算。

重申:只有彻底退出了serve任务的,才能够被结算。

不想进行数据库的设计工作,也就是不想设计接口,在我们这里全部使用嵌入式SQL语句。

3.2 协程划分

整个scheduler有三个协程

  • 监听协程:负责监听来自外部的任务
    • add
    • update 改变风速
    • delete 从队列中移除
  • step协程:每隔一段时间step一次,仅仅负责调度队列
  • 结算协程:这个协程负责进行调度结果的数据库入库操作

我们在上面说了,有可能一个房间仅仅是被抢占了。所以serve time 不等于end_time-start_time这么简单

详单表,仅仅是我们等级的一个结果。并不能成为一个可以被随时更新的表格。

所以我们应该有一个schedule_board。

我们从结算协程的角度,考虑如下情况:

  • running queue中被移除了一个:
    • 可能是被抢占了,这个时候要记录他的开始与结束时间,加入scheduler board,这个时候仅仅是加入而已,并不涉及结算
    • 可能是关空调了,也就是delete了,或者空调被update了,就要进行结算了。(因为要产生详单)计算费用,同时要把之前scheduler_board里面的所有记录内容全部进行结算。
      • 同时,要开启结算,首先是结算所有的内容,然后从数据库中移除相关记录

所以,scheduler board中要有如下字段:

  • room_id
  • speed
  • duration
  • cost

除此之外,不需要关心其他任何行为,包括:

  • running queue中添加了一个,waiting queue中添加、减少了一个

有关更多的信息,大家可以前往仓库自行查看,也欢迎fork、提交PR,共同完善这个仓库后续的代码。

如果您直接clone代码、直接拿这份代码新建自己的仓库,也是可以的(毕竟只是个课设而已,主要的作用在帮忙,不在于其他的)。当然,如果这份代码有帮助到你的话,您可以给我一颗免费的star,真的感激不尽~

  • 10
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值