计算机毕业设计:基于springboot微服务架构的班车预约系统的设计与实现

项目环境搭建

主要搭建的技术:Java、MySQL、Redis、zookeeper、Dubbo、RocketMQ

引言

本项目是对学校的班车预约服务的架构进行了重构,均采用的比较热门的技术。

技术选型

  • Web层:sb-gateway网关,控制整个项目的请求分发与转接等。

  • 接口层:sb-api定义各个服务的接口,比如用户、班车、订单等。

  • 服务层:有着四大服务,分别如上图所示,用户、班车、订单和支付,并且其中均采用第三方中间件Zookeeper、Dubbo和RocketMQ等。

  • 存储层,实际上,MySQL作为存储,而Redis做为MySQL上一层的缓存层

架构路线

首先,一条请求从用户移动端或者PC端发出,经过前端的Nginx的代理转发到sb-gateway的网关,网关进一步的将请求进行路由分发到对应的Controller,从而执行相应的业务逻辑,其中对应的Controller中分为两步,第一步:找缓存,若存在,直接返回,若不存在。第二步:业务逻辑方法向Dubbo请求远程调用(RPC),得到结果,另外一边业务执行相应的底层如MySQL等,将结果返回给网关,并写入缓存等。

环境端口

Java、MySQL、Redis

zookeeper

  • 下载地址

  • 选择3.4.14版本下载即可,放进和刚才的myapps下。

  • 解压:tar -zxf zookeeper-3.4.14.tar.gz

  • 将配置文件cp -r zoo_sample.cfg zoo.cfg

  • 启动zookeeper./zkServer.sh start

Dubbo

  • 采用的是最新版本

  • 下载解压后修配置文件 dubbo-admin-server/src/main/resources/application.properties

  • 配置文件修改zookeeper地址,dubbo控制台端口默认8080,(参考端口图)

  • 可以修改为其他端口例如 server.port=9898,以免与其他服务端口冲突。(参考端口图)

  • 在主目录dubbo-admin-server目录下,执行 mvn clean package -Dmaven.test.skip=true

  • cd dubbo-admin-server/target java -jar dubbo-admin-0.1.jar 启动即可

  • 最新版本采用前后端分离,那么前端可以到dubbo-admin-ui目录下,可在config目录下找到index.js中修改端口,修改过后可以终端输入npm install

  • 安装依赖过后,可以npm run dev启动

RocketMQ

订单服务

OrderController

getNoTakeOrdersById

未乘坐订单

if (redisUtils.hasKey(key)) {// redis是否有该缓存Object obj = redisUtils.get(key);NoTakeBusResponse response = (NoTakeBusResponse) obj;for (NoTakeDto noTakeDto : response.getNoTakeDtos()) {// 如果场次发车时间-当前时间是大0的,那么说明已经发车了String beginTime = noTakeDto.getBeginTime();if (beginTime.compareTo(DateUtil.getHours()) <= -1) {// 删掉当前缓存redisUtils.del(key);// 重新获取最新的数据response = orderService.getNoTakeOrdersById(request);// 写缓存redisUtils.set(key, response, RedisConstants.NO_TAKE_OREDERS_EXPIRE.getTime());return new ResponseUtil().setData(response);}}log.warn("getNoTakeOrdersById->redis\n");return new ResponseUtil().setData(obj);
}

getNoPayOrdersById

获取未支付订单接口

// 从redis中是否有缓存
if (redisUtils.hasKey(key)) {// 有就获取Object obj = redisUtils.get(key);log.warn("getNoPayOrdersById->redis\n");return new ResponseUtil().setData(obj);
}

getEvaluateOrdersById

根据评价状态获取用户订单接口 略

addOrder

添加订单接口

String countKey = RedisConstants.COUNT_DETAIL_EXPIRE.getKey() + request.getCountId();
// 座位缓存失效
if (redisUtils.hasKey(countKey)) {redisUtils.del(countKey);
}
String noPayKey = RedisConstants.NO_PAY_ORDERS_EXPIRE.getKey() + userId;
// 未支付列表缓存失效
if (redisUtils.hasKey(noPayKey)) {redisUtils.del(noPayKey);
}

selectOrderById

根据订单id获取详情订单 略

updateOrderStatus

更改订单状态

// 删掉订单详情缓存
if (redisUtils.hasKey(selectOrderKey)) {redisUtils.del(selectOrderKey);
}
// 删除未乘坐缓存
if (redisUtils.hasKey(noTakeKey)) {redisUtils.del(noTakeKey);
}
// 删除未支付缓存
if (redisUtils.hasKey(noPayKey)) {redisUtils.del(noPayKey);
}
// 删除评价缓存
if (redisUtils.hasKey(evaluateKey)) {redisUtils.del(evaluateKey);
}
OrderServiceImpl
getNoTakeOrdersByIdIPage orderIPage = new  Page<>(request.getCurrentPage(), request.getPageSize());
QueryWrapper queryWrapper = new QueryWrapper<>();
// 获取系统年月日
// 比如5.30
String day = DateUtil.getDay();
// 比如20:00
String hours = DateUtil.getHours();
System.out.println("当前日期:" + day);
System.out.println("当前时间:" + hours);
queryWrapper.eq("user_id", request.getUserId()) // 用户id.eq("order_status", "1")// 1:已经支付.ge("sc.begin_date", day) // 比如订单日期大于等于今天.ge("sc.begin_time", hours) // 订单时间大于等于当前时间.orderByAsc("sc.begin_time") // 排序.orderByDesc("so.order_time"); // 排序getEvaluateOrdersByIdIPage orderIPage = new Page<>(request.getCurrentPage(), request.getPageSize());
QueryWrapper queryWrapper = new QueryWrapper<>();
// 获取系统年月日
String day = DateUtil.getDay();
String hours = DateUtil.getHours();
System.out.println("当前日期:" + day);
System.out.println("当前时间:" + hours);
queryWrapper.eq("user_id", request.getUserId()) // 用户di.eq("order_status", "1") // 状态为1// 两种情况:// 1. 符合当天日期,并且订单场次发车时间小于当前日期// 2. 订单场次的发车日期小于当前日期.and(o -> o.eq("sc.begin_date", day).lt("sc.begin_time", hours).or().lt("sc.begin_date", day)).eq("evaluate_status", request.getEvaluateStatus()) // 评价状态.orderByDesc("sc.begin_time") // 排序.orderByDesc("so.order_time"); // 排序getNoPayOrdersByIdIPage noPayDtoIPage = new Page<>(request.getCurrentPage(), request.getPageSize());
QueryWrapper queryWrapper = new QueryWrapper<>();
// 获取系统年月日
String day = DateUtil.getDay();
String hours = DateUtil.getHours();
System.out.println("当前日期:" + day);
System.out.println("当前时间:" + hours);
queryWrapper.eq("so.user_id", request.getUserId()) // 用户id.eq("so.order_status", "0") // 未支付状态.ge("sc.begin_date", day) // 比如,订单场次日期大于当前日期.ge("sc.begin_time", hours)// 订单场次时间大于当前日期.orderByDesc("sc.begin_time") // 排序.orderByDesc("so.order_time"); // 未支付addOrderpublic AddOrderResponse addOrder(AddOrderRequest request) {// 判断座位,如果重复,直接退出,否则更新场次的座位信息AddOrderResponse response = new AddOrderResponse();// 全局orderIdLong orderId = UUIDUtils.flakesUUID();// 1。 判断座位,如果重复,直接退出,否则下一步// 2。 更新座位,如果没有异常,这是写操作// 3。 计算总金额,如果没有异常// 4。 添加订单,如果异常,这是写操作try {// 1。 判断座位,如果重复,直接退出,否则下一步tag = MqTags.ORDER_SEATS_CANCEL.getTag();boolean repeatSeats = busService.repeatSeats(request.getSeatsIds(), request.getCountId());if (repeatSeats) {// b:true 说明重复response.setCode(SbCode.SELECTED_SEATS.getCode());response.setMsg(SbCode.SELECTED_SEATS.getMessage());return response;}
//            CastException.cast(SbCode.SYSTEM_ERROR);// 2。 更新座位,如果没有异常,这是写操作// 用tags来过滤消息tag = MqTags.ORDER_ADD_SEATS_CANCLE.getTag();boolean addSeats = busService.addSeats(request.getSeatsIds(), request.getCountId());if (!addSeats) {response.setCode(SbCode.DB_EXCEPTION.getCode());response.setMsg(SbCode.DB_EXCEPTION.getMessage());return response;}// 模拟系统异常
//            CastException.cast(SbCode.SYSTEM_ERROR);// 3。 计算总金额,如果没有异常tag = MqTags.ORDER_CALC_MONEY_CANCLE.getTag();String seatIds = request.getSeatsIds();Integer seatNumber = seatIds.split(",").length;Double countPrice = request.getCountPrice();Double totalPrice = getTotalPrice(seatNumber, countPrice);
​
//            CastException.cast(SbCode.SYSTEM_ERROR);// 4。 添加订单,如果异常,这是写操作Order order = orderConvertver.res2Order(request);order.setOrderPrice(totalPrice);order.setEvaluateStatus("0"); // 未评价order.setOrderStatus("0"); // 未支付order.setUuid(orderId); // 唯一idtag = MqTags.ORDER_ADD_CANCLE.getTag();int insert = orderMapper.insert(order);// 插入 不判断了
//            CastException.cast(SbCode.SYSTEM_ERROR);// 这里就不读了,耗时
//            QueryWrapper wrapper = new QueryWrapper<>();
//            wrapper.eq("so.uuid", order.getUuid());
//            OrderDto orderDto = orderMapper.selectOrderById(wrapper);response.setCode(SbCode.SUCCESS.getCode());response.setMsg(SbCode.SUCCESS.getMessage());response.setOrderId(orderId);
//            response.setOrderDto(orderDto);// 这里放redis 未支付缓存,时间前端给定redisUtils.set(RedisConstants.ORDER_CANCLE_EXPIRE.getKey() + orderId, orderId, request.getExpireTime());return response;} catch (Exception e) {// 以上操作如果程序都不发生异常的话, 是不会执行这里的代码的// 也就是说不会发送回退消息的。// 目的是在高并发的情况下,程序内部发生异常,依然高可用
//            e.printStackTrace();log.error("订单业务发生异常");// 发消息,将座位退回,将订单退回MQDto mqDto = new MQDto();mqDto.setOrderId(orderId);mqDto.setCountId(request.getCountId());mqDto.setSeatsIds(request.getSeatsIds());try {String key = RedisConstants.ORDER_EXCEPTION_CANCLE_EXPIRE.getKey() + Convert.toStr(orderId);sendCancelOrder(topic,tag, key, JSON.toJSONString(mqDto));log.warn("订单回退消息发送成功..." + mqDto);} catch (Exception ex) {ex.printStackTrace();}response.setCode(SbCode.SYSTEM_ERROR.getCode());response.setMsg(SbCode.SYSTEM_ERROR.getMessage());return response;}
}

selectOrderById

省略

updateOrderStatus

public OrderResponse updateOrderStatus(OrderRequest request) {OrderResponse response = new OrderResponse();try {// 获取orderDtoQueryWrapper wrapper = new QueryWrapper<>();wrapper.eq("so.uuid", request.getUuid());OrderDto orderDto = orderMapper.selectOrderById(wrapper);// 1, 检查状态是否为2if (request.getOrderStatus().equals("2")) {// 说明关闭订单,回退座位busService.filterRepeatSeats(orderDto.getSeatsIds(), orderDto.getCountId());redisUtils.del(RedisConstants.COUNT_DETAIL_EXPIRE.getKey()+ orderDto.getCountId());// 清除场次详情的缓存}if (request.getOrderStatus().equals("1")) {// 说明已经支付,删掉5分钟的订单缓存redisUtils.del(RedisConstants.ORDER_CANCLE_EXPIRE.getKey() + request.getUuid());}Order order = orderConvertver.res2Order(request);// 更新状态orderMapper.updateById(order);// 暂时就不获取了response.setCode(SbCode.SUCCESS.getCode());response.setMsg(SbCode.SUCCESS.getMessage());redisUtils.del(RedisConstants.NO_PAY_ORDERS_EXPIRE.getKey()+order.getUserId());redisUtils.del(RedisConstants.SELECT_ORDER_EXPIRE.getKey() + request.getUuid());} catch (Exception e) {log.error("updateOrderStatus", e);response.setCode(SbCode.DB_EXCEPTION.getCode());response.setMsg(SbCode.DB_EXCEPTION.getMessage());return response;}return response;
}

deleteOrderById

省略

sendCancelOrder

发送订单回退消息

后边会单独介绍消息队列

班车服务

BusController

getCount

这部分缓存,优化的时候采用了Redis的list,发现我用Jmeter测试并发的时候,发现了Redis中出现了非常多的异常数据,我当时还没有找到问题,暂时先采取以下方案,以下方案在并发时候,并没有出现数据异常。

if (redisUtils.hasKey(key)) {// 如果缓存存在Object obj = redisUtils.get(key);log.warn("getCount->redis\n");// 返回数据return new ResponseUtil().setData(obj);
}
​
// 写
if (!redisUtils.hasKey(key)) {// 如果缓存不存在,就写,注意与数据库数据一致性redisUtils.set(key, response, RedisConstants.COUNTS_EXPIRE.getTime());
}getCountDetailById// 和上面一样
if (redisUtils.hasKey(key)) {Object obj = redisUtils.get(key);log.warn("getCountDetailById->redis\n");return new ResponseUtil().setData(obj);
}
// 这里不判断了,上面已经判断过了
redisUtils.set(key, response, RedisConstants.COUNT_DETAIL_EXPIRE.getTime());

BusServiceImpl

getBus

这里基本没调用过,它是获取班车人物信息的。所以分页查询即可

// MyBatis plus的分页查询
IPage busIPage = new Page<>(request.getCurrentPage(), request.getPageSize());
busIPage = busMapper.selectPage(busIPage, null);

getCount

获取场次列表

// 分页插件,这里自定义分页查询
IPage countIPage = new Page<>(request.getCurrentPage(), request.getPageSize());
QueryWrapper queryWrapper = new QueryWrapper<>();
// 获取时间
String currHours = DateUtil.getHours();
String day = DateUtil.getDay();
System.out.println("当前时间:"+currHours);
System.out.println("当前日期:"+day);
// 判断条件
// 1. 找出符合当前天(比如,5.30)
// 2. 找出大于等于当前时间的场次(比如,数据库8点有一场,目前时间为7点,它就符合)
// 3. 找出状态为getBusStatus的场次,一般是还未发车的场次,(比如0,1)
queryWrapper.eq("begin_date", day).ge("begin_time", currHours).eq("bus_status", request.getBusStatus()).orderByAsc("begin_time");// 时间
countIPage = countMapper.selectCounts(countIPage, queryWrapper);

自定义分页CountMapper接口

public interface CountMapper extends BaseMapper {/**** @param page* @param wrapper* @return*/IPage selectCounts(IPage page, @Param(Constants.WRAPPER) Wrapper wrapper);
​/**** @param wrapper* @return*/CountDetailDto selectCountDetailById(@Param(Constants.WRAPPER) Wrapper wrapper);
}

xml

getCountDetailById

查询场次详情信息

// 自定义查询,涉及到联表查询
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("sc.uuid", request.getCountId());

xml

repeatSeats

判断座位是否已重复

public boolean repeatSeats(String seats, Long coundId) {// 查查数据库, 找到座位字段boolean b = false; // false:不重复,true:重复try {Count count = countMapper.selectById(coundId);// 比如,selectedSeats 是1,2// dbSeats:"",// dbSeats:"1,2,3",// dbSeats: "4,5"// 前端传来的selectedSeats, 前端判断是否为空,要不然后端也判断一下得了if (seats.equals("")) {return true;}if (count.getSelectedSeats().equals("")) {return false;}String[] ss = seats.split(",");String[] cs = count.getSelectedSeats().split(",");// 这里考虑并发问题HashSet hashSet = new HashSet<>(Arrays.asList(cs)); for (String s : ss) {if (hashSet.contains(s)) return true;}} catch (Exception e) {e.printStackTrace();log.error("selectedSeats", e);return true; // 异常就算是重复}return b;
}addSeatsif (!StrUtil.isEmpty(selectedSeats)) {// 这里可以优化,字符串拼接,这样的方式爆内存// StringBuffer newSelectedSeats = selectedSeats + "," + newSelectedSeats; 
}

filterRepeatSeats

回退座位

Count count = countMapper.selectById(coundId);
String[] ss = seats.split(",");
String[] cs = count.getSelectedSeats().split(",");
// 并发问题,注意
HashSet hashSet = new HashSet<>(Arrays.asList(cs)); 
for (String s : ss) {if (hashSet.contains(s)) {hashSet.remove(s);}
}
if (hashSet.isEmpty()) {count.setSelectedSeats("");
}
// 考虑了并发
StringBuffer sb = new StringBuffer();
for (String s : hashSet) {sb.append(s);sb.append(",");
}
// 上面的方案可以用String的replace的方法替换,遍历要回退的座位,依次替换即可
count.setSelectedSeats(sb.toString());
countMapper.updateById(count);

看一下String的replace的源码(1.8),感觉遍历还挺多,但的确不用自己去写了

public String replace(char oldChar, char newChar) {if (oldChar != newChar) {int len = value.length;int i = -1;char[] val = value; /* avoid getfield opcode */while (++i < len) {if (val[i] == oldChar) {break;}}if (i < len) {char buf[] = new char[len];for (int j = 0; j < i; j++) {buf[j] = val[j];}while (i < len) {char c = val[i];buf[i] = (c == oldChar) ? newChar : c;i++;}return new String(buf, true);}}return this;
}

BusSchedule定时器

这里可以使用redis的延时队列或者RocketMQ的消息队列

schedulChangeBusStatus

/*** 每天上午7点到晚上21点,每隔30分钟执行一次*/
@Scheduled(cron = "0 0/30 7-21 * * ?")
private void schedulChangeBusStatus() {log.warn("schedulChangeBusStatus执行");busService.schedulChangeBusStatus();
}

看一下业务逻辑

public void schedulChangeBusStatus() {// 获取时间String currTime = DateUtil.getHours();// 获取日期String day = DateUtil.getDay();log.warn("schedulChangeBusStatus->目前时间:" + currTime);log.warn("schedulChangeBusStatus->目前时间:" + day);System.out.println("目前时间:"+ currTime);System.out.println("目前时间:"+ day);QueryWrapper queryWrapper = new QueryWrapper<>();// 先取出beingtime和now相等的表或者end_time和now相等到表// 1. 取出当天// 2. 取出开始时间或者结束时间符合当前时间queryWrapper.eq("begin_date", day) // 取出当天.and(o -> o.eq("begin_time", currTime) // 当前时间.or().eq("end_time", currTime));List counts = countMapper.selectList(queryWrapper);log.warn("schedulChangeBusStatus->查询到的:" + counts.toString());// 开始作妖for (Count count : counts) {String busStatus = count.getBusStatus();String beginTime = count.getBeginTime();String endTime = count.getEndTime();if (currTime.equals(beginTime)) {if (busStatus.equals("0")) { // 沙河空闲count.setBusStatus("2"); // 沙河->清水河}if (busStatus.equals("1")) { // 清水河空闲count.setBusStatus("3"); // 清水河->沙河}count.setSelectedSeats(""); // 清空座位}if (currTime.equals(endTime)) {if (busStatus.equals("2")) { // 沙河->清水河count.setBusStatus("1"); // 清水河空闲}if (busStatus.equals("3")) { // 清水河->沙河count.setBusStatus("0"); // 沙河空闲}}System.out.println("修改的:" + count);log.warn("schedulChangeBusStatus->修改的:" + count);// 写入数据库countMapper.updateById(count);}// 删缓存,这里非常重要...不删后果很严重String key1 = RedisConstants.COUNTS_EXPIRE + "0";String key2 = RedisConstants.COUNTS_EXPIRE + "1";if (redisUtils.hasKey(key1)) {redisUtils.del(key1);}if (redisUtils.hasKey(key2)) {redisUtils.del(key2);}
}

这里每隔30分钟扫库进行一次io,这里可以有优化... 假如我使用Redis的延迟队列

  1. 在用定时器添加场次的时候,可以将这些场次存入Redis的zset的队列中,元素为场次ID,元素对应的score是出发时间,这样就有17个(定时器每天凌晨添加17个)

  2. 还是用定时器轮询,采用每隔半小时轮询一次,我们取出队列中当前时间大于队列中权重的时间的场次ID,开始进行业务逻辑判断(更改场次状态)

  3. 更改过后,删除或者更改该场次的score的时间(结束时间)

  4. 以此类推,如果是结束时间的话,直接删除该场次id和score。

addCounts

这个项目,没有后台,因此场次需要定时器添加即可

/*** 每天凌晨0点2分执行*/
@Scheduled(cron = "0 2 0 * * ? ")
private void addCounts(){log.warn("addCounts执行");busService.addCounts();
}具体的业务逻辑:public void addCounts() {// 获取日期String day = DateUtil.getDay();// 获取前17个场次QueryWrapper wrapper = new QueryWrapper<>();wrapper.last("limit 17");List counts = countMapper.selectList(wrapper);// 开始修改 这里可以用java8 的特性, 还不是很熟悉,后期优化一下for (Count count : counts) {// 更改日期count.setBeginDate(day);// 更改uuidcount.setUuid(UUIDUtils.flakesUUID());// 清空座位count.setSelectedSeats("");// 将走位状态清零count.setSeatStatus("0");// 插入countMapper.insert(count);}
​// 删缓存,不删后果依然很严重String key1 = RedisConstants.COUNTS_EXPIRE + "0";String key2 = RedisConstants.COUNTS_EXPIRE + "1";if (redisUtils.hasKey(key1)) {redisUtils.del(key1);}if (redisUtils.hasKey(key2)) {redisUtils.del(key2);}
}

​完整项目源码资源详见:https://download.csdn.net/download/zy_dreamer/87952332


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部