——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-
引
传智播客中有一个面试题目项目就是这个银行业务调度系统,今天也来实现一把。
需求分析
- 这家银行有6个业务窗口,其中4个是普通窗口,1个快速窗口,1个VIP窗口,银行的客户比例为普通:快速:VIP=6:3:1,各个类型用户安排到对应窗口,快速和VIP窗口在空闲时也可以服务普通用户,用户业务办理时间是随机的,有最小值,但快速用户永远是最小值。模拟无需GUI,通过log显示结果
- 假定最小业务办理时间为30秒,最大600秒
- 快速和VIP窗口仅在普通窗口全忙时才接收普通用户
- 用户通过取号器来排队,窗口就绪后由号码排队系统分配客户
第一轮设计
- 通过一个模拟执行程序执行,执行程序 生成一个银行(6个窗口),一个用户模拟(取号),为了方便,采用100倍速模拟。
- 业务窗口有3种类型,行为上并无不同,仅仅标记的用户偏向不同,采用一个enum业务类型来标识。
- 用户有明确的业务目的,也就视为:有特定的业务办理时间(假定银行窗口业务员素质相同),有特定的业务类型(普通/快速/VIP),进入银行就有自己的排队号码(未进入设定为-1)。
- 业务类型有最小和最大办理时间,普通和VIP为(30,600),快速为(30,30)
- 银行取号系统负责给用户分配排队号码,并给空闲的窗口分配客户。
第二轮设计
- 窗口应该知道当前服务的客户,窗口应该属于银行,窗口应该有准备就绪开始,计划停止(服务完当前客户)的方法,以通知取号系统。
- 取号系统应该在没有排队用户时阻塞,取号系统应该为每个窗口尽快分配用户,按:用户采用3个阻塞队列,快速/VIP窗口轮询特有队列和普通队列。普通窗口直接阻塞取普通队列
实现
代码如下:
业务类型
/**
* 银行业务分类
* @author lz
*
*/
public enum BusinessType {
/**
* 普通业务 耗时30秒到10分钟
*/
Normal(30,600),
/**
* 快速业务 耗时30秒
*/
Quick(30,30),
/**
* VIP业务 耗时30秒到10分钟
*/
VIP(30,600);
/**
* 最小和最大耗时
*/
public int minTime,maxTime;
private BusinessType(int minTime, int maxTime) {
this.minTime = minTime;
this.maxTime = maxTime;
}
}
客户
/**
* 银行客户
* @author lz
*
*/
public class BankUser {
/**
* 来办业务的类型
*/
public final BusinessType biz;
/**
* 业务耗时
*/
public final int bizTime;
/**
* 排队号码
*/
public String queueNum;
@Override
public String toString() {
return "BankUser [biz=" + biz + ", bizTime=" + bizTime + ", queueNum="
+ queueNum + "]";
}
public String shortInfo() {
return queueNum+"("+bizTime+")";
}
public BankUser(BusinessType biz, int bizTime) {
super();
this.biz = biz;
this.bizTime = bizTime;
}
/**
* 进入银行排队
* @param bank 银行
*/
public void enQueue(Bank bank){
this.queueNum=bank.offerNum(this);
Logger.getLogger("BankQueue").info("新客户:"+this);
}
}
银行,包括窗口和调度系统
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import static cc.sisel.util.Quic.*;
/**
* 银行抽象,包括窗口和业务调度系统 默认构造包含6个窗口 其中一个VIP一个快速,其他普通
*
* @author lz
*
*/
public class Bank implements Runnable {
final BankUserHandler banksys;
List<BusinessWindow> servWindows;
public Bank(BankUserHandler banksys, List<BusinessWindow> servWindows) {
super();
this.banksys = banksys;
this.servWindows = servWindows;
}
public Bank() {
super();
this.banksys = new BankUserHandler();
this.servWindows = new ArrayList<Bank.BusinessWindow>(6);
this.servWindows.add(new BusinessWindow(BusinessType.VIP));
this.servWindows.add(new BusinessWindow(BusinessType.Quick));
this.servWindows.add(new BusinessWindow(BusinessType.Normal));
this.servWindows.add(new BusinessWindow(BusinessType.Normal));
this.servWindows.add(new BusinessWindow(BusinessType.Normal));
this.servWindows.add(new BusinessWindow(BusinessType.Normal));
}
@Override
public void run() {
ExecutorService exes = Executors.newFixedThreadPool(servWindows.size());
for (BusinessWindow businessWindow : servWindows) {
exes.execute(businessWindow);
businessWindow.readyToService();
}
// 演示用,控制台输出客户状态
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(
(() -> sp("等待客户:" + this.banksys.watingSum() + " 今日客户总数:"
+ this.banksys.cameUserSum())), 1, 5, TimeUnit.SECONDS);
}
/**
* 取号
*
* @param bankUser
* 取号的用户
* @return 号码条
*/
public String offerNum(BankUser bankUser) {
return this.banksys.accept(bankUser);
}
/**
* 业务调度系统 负责分派客户到窗口 并监视银行中等待客户的队列 记录今日来访的客户计数 生成号码条
*
* @author lz
*
*/
/**
* @author lz
*
*/
public class BankUserHandler {
BlockingQueue<BankUser> normalQueue = new LinkedBlockingQueue<BankUser>(),
quickQueue = new LinkedBlockingQueue<BankUser>(),
vipQueue = new LinkedBlockingQueue<BankUser>();
private int vipn = 0, quickn = 0, normaln = 0;
public int watingSum() {
return this.normalQueue.size() + this.quickQueue.size()
+ this.vipQueue.size();
}
public int cameUserSum() {
return this.normaln + this.quickn + this.vipn;
}
/**
* 生成号码条
*
* @param bankUser
* 用户
* @return 号码条
*/
private String accept(BankUser bankUser) {
switch (bankUser.biz) {
case VIP:
this.vipQueue.offer(bankUser);
return "V" + vipn++;
case Quick:
this.quickQueue.offer(bankUser);
return "Q" + quickn++;
case Normal:
this.normalQueue.offer(bankUser);
return "N" + normaln++;
}
return null;
}
/**
* 为窗口分派用户,若当前没有则等待1秒
*
* @param businessWindow
* 待分配的窗口
* @return 客户
*/
public BankUser offerUser(BusinessWindow businessWindow) {
BankUser offer = null;
while (offer == null) {
switch (businessWindow.offerType) {
case VIP:
return this.offerVIP();
case Quick:
return this.offerQuick();
case Normal:
return this.offerNormal();
}
ts(10);
}
return offer;
}
/**
* 为快速窗口查找匹配的客户
*
* @return 合适的客户
*/
private BankUser offerQuick() {
if (!this.quickQueue.isEmpty()) {
try {
return this.quickQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
} else if (!this.normalQueue.isEmpty()) {
try {
return this.normalQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
return null;
}
/**
* 为VIP窗口查找匹配的客户
*
* @return 合适的客户
*/
private BankUser offerVIP() {
if (!this.vipQueue.isEmpty()) {
try {
return this.vipQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
} else if (!this.normalQueue.isEmpty()) {
try {
return this.normalQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
return null;
}
/**
* 为普通窗口查找匹配的客户
* 这个方法是阻塞的
* @return 合适的客户
*/
private BankUser offerNormal() {
try {
return this.normalQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
}
/**
* 服务窗口
* @author lz
*
*/
public class BusinessWindow implements Runnable {
/**
* 窗口序号
*/
public final int serial;
/**
* 是否接续服务
*/
private boolean keepWorking;
/**
* 当前服务客户
*/
private BankUser currentServing;
/**
* 窗口服务类型
*/
public final BusinessType offerType;
public BusinessWindow(BusinessType offerType) {
super();
this.offerType = offerType;
this.serial = Bank.this.servWindows.size();
}
@Override
public String toString() {
return "Window[" + offerType.toString().charAt(0) + serial + "]";
}
public String shortInfo() {
return "W[" + offerType.toString().charAt(0) + serial + "]";
}
/**
* 就绪,可以服务
*/
public void readyToService() {
this.keepWorking = true;
}
/**
* 停止,服务完成当前用户就停止
*/
public void scheduleStop() {
this.keepWorking = false;
}
/**
* @return 是否正在服务
*/
public boolean isBusy() {
return currentServing != null;
}
/**
* 服务过程
* 服务完成会使当前客户离开
*/
private void handle() {
if (currentServing != null) {
// Logger.getLogger("BankQueue").info(this.shortInfo() +
// " 开始服务:" + currentServing.shortInfo() + " @" +Instant.now());
// sp(this + " 开始服务:" + currentServing + " @" +Instant.now());
ts(currentServing.bizTime * 10);
Logger.getLogger("BankQueue").info(this.shortInfo() +
" 服务完毕:" + currentServing.shortInfo() + " @" +Instant.now());
sp(this + " 服务完毕:" + currentServing + " @" + Instant.now());
this.currentServing = null;
}
}
/**
* 取一个用户
*/
private void next() {
this.currentServing = Bank.this.banksys.offerUser(this);
}
@Override
public void run() {
while (true) {
if (this.keepWorking) {
if(this.currentServing == null){
this.next();
}
this.handle();
} else {
ts(1000);
}
}
}
}
}
模拟器,包括用户生成器
/**
* 模拟器
* @author lz
*
*/
public class Simulator {
public static void main(String[] args) {
Logger.getLogger("BankQueue").setLevel(Level.ALL);
Bank bank = new Bank();
new Thread(bank).start();
BankUserGenerator gen=new BankUserGenerator();
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(
(() -> gen.randomGen().enQueue(bank)),
100,
300,
TimeUnit.MILLISECONDS);
}
}
/**
* 模拟银行用户生成器
* @author lz
*
*/
class BankUserGenerator{
Random r=new Random();
BankUser randomGen(){
BusinessType biz;
int rnum=r.nextInt(10);
if(rnum>8){
biz=BusinessType.VIP;
}else if(rnum>5){
biz=BusinessType.Quick;
}else{
biz=BusinessType.Normal;
}
return new BankUser(biz, biz.minTime+r.nextInt(biz.maxTime-biz.minTime+1));
}
}
分析
这里的快速队列和VIP队列采用了轮询的方式,效率偏低,如果可以,用标记多头阻塞队列是理想的,但是这个容器难度颇大,就没有实现。
而且客户的取号系统基本没有在分配中用上,实际应该维持一个映射,系统应该只对号码操作。
窗口的开关功能虽然实现了,但是没有模拟。
扩展讨论
一个用户在办理业务的时候可能多次往返窗口,这样重新拿号势必麻烦,而银行业务人员有时又需要传递用户,序列操作,最后,这个系统中可以加入用户等待时间统计,预估一个等待时间,提高客户体验,银行的窗口分配策略也可以动态修改就更好了。
这些需求,初步分析,可以通过内部号码生成与插队,动态代理调度系统并传递数据的方式完成,而统计就比较简单了,实现的方式很多。