SpringBoot自习室预约系统实战:高并发防超卖与黑名单机制
2026/9/17 18:26:14 网站建设 项目流程

接到这个需求的时候,我第一反应是:这不就是“图书馆占座克星”吗。高校的自习室资源就那么点,一到期末考试季就上演“书在人不在”的占座大战,甚至还有人用一瓶水占一个下午的位置。管理老师头疼,学生也窝火。基于SpringBoot实现一套自习室预约系统,核心要解决的就是座位资源分配的问题:把“抢座”变成“约座”,让每一张椅子在哪个时间段属于谁都清清楚楚。这篇文章我打算换个写法,不搞那种从零到一逐行敲代码的保姆级教程,而是一个项目做完之后的完整复盘。聊清楚整体方案、核心设计思路、并发场景怎么处理、黑名单机制怎么落地,再把我实际操作中踩过的坑原原本本说出来。适合正在做毕业设计的朋友、接校园信息化项目的团队,以及想通过一个完整案例把SpringBoot实战串起来的开发者。

1. 需求梳理和技术选型:先想清楚这系统到底要管什么

很多初学SpringBoot的朋友容易一上来就建工程、写Controller,结果写到一半发现业务逻辑乱了。我接到这个项目的第一件事,其实是坐下来把需求一条条列清楚,把“系统边界”划出来。

1.1 核心需求拆解:自习室预约要解决的四个问题

自习室预约系统表面上是个CRUD,但真要落地,它其实要回答四个核心问题:

第一,座位资源怎么建模。一个自习室里有几十上百个座位,每个座位有位置编号、是否靠窗、是否靠插座、是否属于安静区等属性。这些属性会影响用户的选择偏好,也会影响管理员后续的统计和分析。所以座位不能简单当成一个字符串来存,它需要有自己的实体和状态。

第二,预约流程怎么设计。用户是当天预约还是提前几天预约?预约之后需不需要签到?如果人没来,座位什么时候释放?这些问题直接决定了业务表的字段设计和状态机的流转。我见过不少新手把预约表设计成只有“预约中”和“已完成”两个状态,结果用户取消、超时未签到这些情况完全没法处理。

第三,高并发怎么应对。热门自习室在晚上七点开放第二天预约时,瞬间可能涌进来几百上千个请求。如果按照普通的数据库操作来处理,很容易出现“座位超卖”——两个人同时约到了同一个座位。这是整个系统里最考验技术功底的地方。

第四,违约怎么治理。预约了不来、签到后中途离开不签退,这些行为不治理的话,系统很快就失去公信力。所以需要一套信用分或黑名单机制,把违约用户暂时关进“小黑屋”。

想清楚了这四个问题,再去选技术和设计表结构,思路就清晰了。

1.2 为什么选SpringBoot + MyBatis-Plus + MySQL这套组合

技术选型没有绝对的“最好”,只有“最合适”。我这次选的是SpringBoot 2.7 + MyBatis-Plus + MySQL 8.0 + Redis,前端配了一个Vue3的管理后台。这套组合在校园类管理系统中属于非常成熟的方案,教程多、社区活跃、出了问题很容易搜到答案。

SpringBoot在这个项目里解决的最大痛点是“配置地狱”。你回想一下SSM时代,光是一个Spring的XML配置文件就能写几百行,各种Bean定义、事务配置、数据源配置,新人光是搞定环境就要一周。SpringBoot通过自动装配把这些全都约定好了,我只需要在application.yml里写几行核心参数就能把项目跑起来。这并不是说SpringBoot比SSM“高级”,而是它在“快速交付”这件事上确实省了大量时间。

MyBatis-Plus则是把“单表操作”的重复劳动降到了最低。座位表、预约表这种简单的增删改查,直接用BaseMapper提供的方法就能完成,不需要写一堆XML映射文件。更实用的是它的分页插件和条件构造器,查询今天所有空闲座位、分页展示预约记录,几行代码就能搞定。

Redis在这个项目里扮演的角色很重。它不只是用来存验证码和Token,更重要的是承担了座位状态缓存和分布式锁这两个关键任务。关于这部分我会在第三章详细展开。

1.3 项目结构规划:分包方式决定你后续开发效率

项目结构看着是小事,但一个清晰的分包方案能让你少走很多弯路。我是这样设计的:

com.library.reservation ├── config -- 配置类(Redis、MyBatis-Plus、WebMvc、定时任务) ├── controller -- 接口层(用户端、管理员端分开) ├── service -- 业务层(重点,所有业务逻辑都在这里) ├── mapper -- 数据访问层 ├── entity -- 实体类(数据库表对应) ├── dto -- 前端交互数据对象 ├── vo -- 视图返回对象 ├── common -- 通用类(统一返回结果、状态码、异常处理) ├── utils -- 工具类(JWT、日期处理、座位编号生成等) ├── task -- 定时任务(释放超时座位、清理过期违约记录) └── enums -- 枚举(预约状态、座位状态、用户角色等)

这里面最想提醒大家的是:controller层一定不要写业务逻辑。你可能会觉得“我就查一下数据,直接在controller里写完算了”,但如果后面业务越来越复杂,这种写法会让代码膨胀得特别快。我在这个项目中踩过一次教训:最初把“取消预约”的代码直接写在controller里,后来要加“取消后是否需要扣除信用分”的需求时,不得不把所有调用点一个个翻出来改。后来统一收敛到service层,再改逻辑就只需要动一个地方了。

2. 数据库设计:一张好表胜过十次后期“打补丁”

数据库是业务的地基,这一块我花的时间最多。设计得好的表,后面写业务代码会非常顺畅;设计得烂的话,各种字段不够用、状态对不上、查询奇慢的问题都会冒出来。我直接把这套系统的表结构核心部分拿出来讲讲,你可以直接参考。

2.1 核心表结构:用户、座位、预约三张表的字段设计

第一张是用户表。我这里不只是存用户名和密码,还额外设计了角色、学号/工号、信用分这几个关键字段。

CREATE TABLE tb_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(255) NOT NULL COMMENT 'BCrypt加密后的密码', real_name VARCHAR(50) COMMENT '真实姓名', student_no VARCHAR(20) COMMENT '学号/工号', role TINYINT DEFAULT 1 COMMENT '角色 1-普通用户 2-管理员', credit_score INT DEFAULT 100 COMMENT '信用分,初始100', status TINYINT DEFAULT 1 COMMENT '账号状态 1-正常 0-禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_student_no (student_no) ) COMMENT '用户表';

第二张是座位表。这里有一个细节:我单独设计了一个seat_status字段来标记“当前状态”,而不是通过预约表临时计算。虽然这意味着多了一个需要维护的字段,但在查询“哪些座位是空闲的”这个高频操作上,直接查一个索引字段远比联表查预约表快得多。

CREATE TABLE tb_seat ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL COMMENT '自习室ID', seat_no VARCHAR(20) NOT NULL COMMENT '座位编号,如A-01', seat_status TINYINT DEFAULT 0 COMMENT '座位状态 0-空闲 1-已预约 2-使用中 3-维护中', is_window TINYINT DEFAULT 0 COMMENT '是否靠窗 1-是 0-否', has_power TINYINT DEFAULT 0 COMMENT '是否有插座 1-是 0-否', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_room_seat (room_id, seat_no) ) COMMENT '座位表';

第三张是预约表,这是整个系统的核心。我设计了预约开始时间、预约结束时间、实际签到时间、实际签退时间、状态、违约标记等字段。状态我用了TINYINT而不是字符串,因为字符串的存储空间更大,而且要写枚举去维护它。

CREATE TABLE tb_reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '用户ID', seat_id BIGINT NOT NULL COMMENT '座位ID', reserve_date DATE NOT NULL COMMENT '预约日期', start_time TIME NOT NULL COMMENT '预约开始时间', end_time TIME NOT NULL COMMENT '预约结束时间', checkin_time DATETIME DEFAULT NULL COMMENT '实际签到时间', checkout_time DATETIME DEFAULT NULL COMMENT '实际签退时间', status TINYINT DEFAULT 1 COMMENT '状态 1-已预约(待签到) 2-已签到(使用中) 3-已完成 4-已取消 5-超时未签到 6-违约', is_violation TINYINT DEFAULT 0 COMMENT '是否违约 1-是 0-否', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_date (user_id, reserve_date), INDEX idx_seat_time (seat_id, start_time, reserve_date), INDEX idx_status_time (status, reserve_date, start_time) ) COMMENT '预约表';

这里要多说一句那个复合索引。一开始我只给预约表加了单列索引,等数据量到了几万条之后,查询某个座位在某个时间段是否被占用变得特别慢。后来按照“等值查询在前,范围查询在后”的原则,建了(status, reserve_date, start_time)这个联合索引,查询性能明显改善。做系统设计时,提前考虑索引比事后优化要省力很多。

2.2 状态机设计:预约状态流转的完整闭环

预约系统的核心难点之一就是状态的流转要严谨。我画过一张状态流转图(文字版描述给大家):

  • 用户提交预约,生成记录,状态为“已预约(1)”。
  • 到达预约开始时间前30分钟到开始后15分钟内,用户可签到,状态变为“已签到(2)”。
  • 使用完毕后用户主动签退,状态变为“已完成(3)”。
  • 如果用户想要取消,且距离预约开始时间超过2小时,状态变为“已取消(4)”,座位立即释放。
  • 如果到了开始时间后15分钟用户仍未签到,系统自动置为“超时未签到(5)”,座位释放,用户记一次违约。
  • 如果是“已签到”状态,但用户在预约结束时间前就离开了且没有签退,由管理员标记为“违约(6)”。

这套状态机是经过和图书馆老师反复确认后定的。最关键的是“可取消”和“超时释放”的时间窗口,定得太宽会有人反复预约占着位置,定得太窄用户体验又很差。我这边最终定的方案是:预约开始前2小时可免费取消,预约开始后15分钟未签到自动释放。

2.3 管理员端的统计需求:一张视图搞定日报数据

管理员除了管座位,还特别想看每天的使用率、预约人数、违约人数。这不用单独写复杂的统计代码,我直接在MySQL里建了一张统计视图:

CREATE VIEW v_daily_reservation AS SELECT reserve_date, COUNT(*) AS total_count, SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) AS completed_count, SUM(CASE WHEN status = 5 THEN 1 ELSE 0 END) AS timeout_count, SUM(CASE WHEN is_violation = 1 THEN 1 ELSE 0 END) AS violation_count, COUNT(DISTINCT user_id) AS active_users FROM tb_reservation GROUP BY reserve_date;

这样管理员端的“今日概况”就只需要查这一张视图,轻松很多。不过要注意,视图对于复杂聚合还是有性能压力的,数据量超过百万级之后建议改成夜间的统计任务写入汇总表。

3. 预约功能的核心实现:并发场景下的“防超卖”方案

预约系统最核心、也最容易被问到的一个问题是:两个人同时提交了同一个座位的预约,怎么办?如果直接把查询和插入分开做,就一定会出现并发问题。这也是面试官非常喜欢问的一道题——“你怎么防止座位超卖?”

3.1 为什么不建议只用select然后再insert

最简单直观的做法是:先查一下这个座位这个时段是否已经被预约,如果没有就插入一条预约记录。但这里有一个经典的竞态条件:

用户A和用户B同时发起预约请求,两个请求同时执行select查询,发现座位都是空闲的,然后同时执行insert。结果就是同一张座位在同一个时间段被预约了两次。

有的同学会说,“我可以给这段代码加个synchronized”。这在单机部署下确实有效,但一旦系统部署到多台服务器上(前面有负载均衡),synchronized就锁不住了——两张服务器各跑各的线程,根本不互斥。而且就算单机,用synchronized锁住整个方法,性能也会很差,因为所有请求都在排队等同一把锁。

3.2 数据库唯一约束兜底

这是我的第一道防线,也是最底层的一道防线。我给预约表加了一个联合唯一索引:

ALTER TABLE tb_reservation ADD UNIQUE KEY uk_user_slot (seat_id, reserve_date, start_time);

这意味着同一天同一张座位的同一个开始时间,在数据库层面只允许一条记录存在。有了这个约束,即使业务代码出现并发问题,数据库也会直接报错而不是产生脏数据。

不过光有唯一索引还不够。如果系统真的并发请求很高,数据库会抛DuplicateKeyException,虽然数据一致性保住了,但用户会看到一个五千错误页面,这体验肯定不行。所以还需要在业务层面再做一层控制。

3.3 Redis分布式锁:把并发挡在业务代码之外

我采用的方式是Redis分布式锁 + 数据库唯一约束双保险。为什么选Redis?因为Redis是单线程模型,setnx(set if not exists)这类操作天然是原子的,多个服务实例同时执行也不会有竞态。这里我封装了一个简单的工具类,核心逻辑就是利用Redis的setnx和过期时间:

public boolean tryLock(String key, String requestId, long expireTime) { // 利用setnx + expire 组合,保证原子性 Boolean result = redisTemplate.opsForValue() .setIfAbsent(key, requestId, expireTime, TimeUnit.SECONDS); return Boolean.TRUE.equals(result); }

在预约座位的核心业务方法中,我先用座位ID和时间段拼接一个分布式锁的key:

String lockKey = "seat:lock:" + seatId + ":" + reserveDate + ":" + startTime; boolean locked = lockService.tryLock(lockKey, userId.toString(), 10); if (!locked) { throw new BizException("该座位该时段正在被其他用户预约,请稍后重试"); } try { // 核心预约逻辑 } finally { lockService.unlock(lockKey, userId.toString()); }

这个方案做下来,并发测试的结果就非常稳定了。我用JMeter模拟了50个线程同时抢同一个座位,数据库最终只插入了1条预约记录,其余49个请求全部返回“手慢了,座位被抢走啦”的提示。

3.4 Redis缓存座位状态:查询性能优化的实践

除了防并发,Redis还承担了一个特别重要的作用:座位状态缓存。

用户在挑选座位时,前端会频繁请求“某自习室的座位占用情况”。如果每一次请求都去MySQL里查,数据库压力会非常大。我的方案是:座位状态先放到Redis里,定时或异步同步回MySQL。

具体实现思路:

Key: seat:status:{roomId} Hash结构: field=seatId, value=座位状态

用户查询时直接查Redis的Hash,只有管理员修改座位(比如设置为维护中)时才同步更新缓存和数据库。预约成功后,Redis中的座位状态立即更新为“已预约”,同时发送一条异步消息去更新数据库,保证最终一致性。

这里要特别注意一个坑:Redis缓存和数据库之间的一致性维护。我的策略是“先更新数据库,再删除缓存”。因为更新数据库成功后如果删缓存失败,最多就是下次查询发现缓存里面有旧数据,可以通过设置短期过期时间来自动修正。如果反着来,“先删缓存,再更新数据库”,一旦更新数据库失败,缓存里就什么都没有了,所有请求全部穿透到数据库,数据库直接就垮了。

4. 签到、超时释放和黑名单:这些“不起眼”的逻辑才是体验关键

预约功能做完了之后,很多新手以为项目就结束了。但我在实际测试过程中发现,真正影响用户体验和自习室秩序的,其实是签到、超时释放、违约处理这些细节逻辑。这几块没做好,用户根本没法信任这个系统。

4.1 签到机制:二维码+地理围栏

签到的方式我参考了现在很多图书馆系统的做法:生成一个动态二维码,用户在自习室门口的扫码设备上扫一下完成入座签到。二维码里加密存储预约记录ID和一个时间戳,后端校验时间戳是否在签到时间窗口内。

为什么不用纯定位?因为大多数校园室内定位不准,GPS漂移很严重。但是为了做一个兜底,我会在签到接口里做一个“地理位置校验”——前端调用地图SDK获取用户当前位置,后端判断该位置距离自习室是否超过500米。超过就直接拒绝签到。

签到完成时,做两件事:

// 更新预约状态为已签到 reservation.setStatus(2); reservation.setCheckinTime(new Date()); reservationService.updateById(reservation); // 更新座位状态为使用中 seat.setSeatStatus(2); seatService.updateById(seat);

顺带清理Redis中的座位缓存,让其他用户看到这个座位已被占用。

这里有个很容易被忽略的细节:签到的接口一定要做“幂等处理”。二维码可能因为网络问题被重复扫描,或者用户手抖点了两次签到,如果每次请求都执行更新,虽然最终状态还是“已签到”,但会出现“重复签退”或“重复更新耗时”这类小问题。我的做法是在签到接口入口处用分布式锁锁住预约ID,第二次进来直接返回成功。

4.2 定时任务:超时未签到的自动释放

这是整个系统里“自动化”程度最高的一个模块。用户预约了早上8:00-10:00的座位,如果到8:15还没有签到,系统就应该自动释放该座位,并把一次违约记到用户头上。

我基于SpringBoot自带的@Scheduled定时任务实现,每1分钟执行一次扫描:

@Scheduled(cron = "0 * * * * ?") public void releaseTimeoutReservations() { // 查询所有状态为“已预约”且开始时间已超过15分钟的记录 List<Reservation> timeoutList = reservationMapper.selectList( new LambdaQueryWrapper<Reservation>() .eq(Reservation::getStatus, 1) .lt(Reservation::getStartTime, DateUtil.minutesAgo(15)) .le(Reservation::getReserveDate, DateUtil.today()) ); for (Reservation r : timeoutList) { // 更新预约状态为超时未签到 // 更新座位状态为空闲 // 扣减用户信用分 } }

这里要提醒大家一个分布式部署场景下的坑:如果这个服务被部署到多台服务器上,@Scheduled任务会在每台机器上同时执行,导致同一个预约被重复处理。生产环境我建议大家引入xxl-job这类分布式任务调度平台,或者至少在任务执行逻辑里加一个分布式锁,保证同一时刻只有一个实例在跑。

还有一点,时间判断不要用数据库的NOW(),尽量用应用服务器的时间统一计算。因为如果数据库服务器和应用服务器不在同一台机器上,时钟不一致会导致漏判或误判。

4.3 信用分与黑名单:让规则真正“硬”起来

系统上线之前我和管理员老师讨论了很久:违约了到底怎么处理?光提示没用,得有实际约束力。最后定了这套规则:

  • 用户初始信用分100分。
  • 超时未签到一次,扣20分。
  • 预约后取消,不扣分(鼓励大家不来的话主动取消)。
  • 签退超时或中途离场未签退被管理员标记,扣10分。
  • 信用分低于60分时,禁止预约未来7天的座位(相当于进入黑名单)。
  • 信用分低于40分时,永久限制预约,需要找管理员申诉恢复。

我通过一个信用分变更记录表来追踪每次扣分的原因,确保用户可以查询自己的扣分明细,避免了“莫名其妙被拉黑”的投诉。同时,在黑名单判定时用定时任务每天凌晨检查一次所有用户的信用分,统一更新预约权限状态,而不是每次预约请求都实时计算,这样可以减少不必要的性能开销。

4.4 主动签退与“爽约释放”的异步消息通知

用户签退时,系统除了更新状态,还会触发一个异步消息通知:如果该座位在接下来的一段时间有其他人预约,就提前给下一位用户推送“座位已释放,请准备入座”。

这个我是用Spring的事件机制实现的,核心是@Async和@EventListener两个注解。发一个预约状态变更事件,监听器异步处理通知逻辑,这样不影响主流程的响应速度。GitHub上很多人用消息队列(比如RabbitMQ)来做这件事,但对于这个小项目,Spring自带的事件机制完全够用,不需要为了引入MQ而引入MQ。

5. 部署上线与常见问题排查:那些文档里不会写的实战教训

代码写完之后,真正的考验才刚刚开始。部署到服务器上、真实用户跑起来之后,各种奇怪的问题就会冒出来。我接下来说的这些问题,都是我实际踩过的坑,可能需要花上几天才能排查出来,希望你们不用再走一遍。

5.1 IDEA创建SpringBoot项目遇到的版本陷阱

现在大家练手基本都是用IDEA来创建SpringBoot项目,但最近Spring官方对初始化向导做了调整,默认生成的SpringBoot版本可能会比较高。如果你用的是JDK 1.8环境,大概率会遇到启动直接报错的情况,提示类似“Unsupported class file major version”或“无法加载主类”这类问题。

我遇到过最典型的一个场景是:IDEA默认识别到机器上装了JDK 21,生成项目时SpringBoot版本选到了3.x。SpringBoot 3.x最低要求JDK 17,如果你的目标是做毕业设计或者在企业里维护老项目,绝大多数机器装的还是JDK 1.8。这种情况下我建议直接在pom.xml里把SpringBoot版本改回2.7.x,然后在Project Structure里把SDK切回1.8,同时确认Maven的Java Compiler版本也改成了1.8。版本不是越高越好,适合生产环境才是硬道理。

5.2 Linux服务器部署:Java环境与项目打包的踩坑记录

本地开发环境一切正常,一部署到Linux服务器就出各种幺蛾子。最常见的是这几个:

第一是Java环境变量问题。很多人习惯直接在/etc/profile里写JAVA_HOME,但有些发行版默认安装的是OpenJDK的头文件路径,导致命令行里java能找到,Tomcat却找不到。稳妥的做法是设置完环境变量之后执行source /etc/profile,并且用echo $JAVA_HOME确认路径真实存在。

第二是打包问题。前后端分离的项目中,前端同学构建出dist目录后,我需要手动把静态文件复制到SpringBoot的resources/static目录下,再执行mvn clean package。这个过程很容易出现“前端改了新的接口,后端打的包还是旧的”。我在项目里干脆写了一个脚本,构建前端后自动复制到后端再打包,彻底杜绝人工操作的疏漏。

第三是MySQL时区问题。SpringBoot连接MySQL时,如果application.yml里的连接串没有配置serverTimezone,在高版本MySQL/驱动下经常会出现“The server time zone value is unrecognized”的报错。我在连接串里加了serverTimezone=Asia/Shanghai解决。哪怕你本机的MySQL没报错,生产环境的机器大概率会出这个问题,建议一开始就加上。

5.3 数据库连接池参数调优:让系统扛得住抢座高峰

预约系统是有明显的流量尖峰的,比如每周日晚八点开放下一周座位,那一瞬间的并发量可能是平时的几十倍。如果数据库连接池配置不对,服务直接假死都时有发生。

我用的HikariCP是SpringBoot默认的连接池,配置经验如下:

spring: datasource: hikari: maximum-pool-size: 30 minimum-idle: 10 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000

这里有几个参数要说一下。maximum-pool-size不建议盲目设置得很大,很多新手以为连接池越大越好,其实连接数过多反而会增加数据库的线程切换开销,甚至把数据库拖垮。一般一个MySQL实例的并发能力在几百左右,应用层的连接池重点在于“快速获取连接”,而不是长时间持有连接。另外max-lifetime一定要小于MySQL的wait_timeout,否则连接会被MySQL服务端提前关闭,而应用还不知道,拿到一个已失效的连接直接报错。

5.4 前端联调阶段经常出现的跨域和接口问题

这个项目前端用的Vue,开发环境下它默认跑在8080端口,后端在8081,跨域问题就会冒出来。最省事的方案是后端加一个全局CORS配置:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

这个配置在开发阶段很方便,但上线后建议还是用Nginx做反向代理,把前后端域名统一起来。因为生产环境开通带凭据的跨域请求如果配置不当,会有潜在的CSRF风险,Nginx统一入口能把这个面收敛住。

另外一个联调时的高频问题就是前端传参格式和后端的DTO对不上。比如前端传了一个JSON数组,后端却用@RequestParam去接,那肯定接不到。我的统一规范是:GET请求用请求参数,POST请求用@RequestBody接收JSON对象。这样可以在controller层就减少很多误解。

5.5 线上问题速查表:直接抄作业的排查手册

我最后整理了一份自己在实际排查问题时使用的速查表,希望对你有用。

现象可能原因排查方式
项目启动时报端口被占用其他进程占用了8080端口netstat -ano查看占用端口的PID,然后结束进程,或在配置里换端口
登录后访问接口一直401JWT密钥配置不一致或Token过期检查JWT的签名密钥是否一致,确认token是否在有效期之内
座位状态显示与数据库不一致Redis缓存未及时更新检查“先更新数据库,再删除缓存”的顺序,确认异步更新是否执行
预约提交后页面一直转圈分布式锁超时时间太短或业务处理慢调大锁过期时间,或检查业务代码中是否存在慢SQL拖慢接口
每日定时任务执行了多次项目在多实例部署且未加分布式锁引入xxl-job,或在任务逻辑中加Redis锁
签到接口偶现异常超时二维码校验逻辑中调用了慢查询给预约记录表的主键索引检查一下,确认是否走了全表扫描

5.6 这套系统后续还能怎么扩展

如果你做完这个基础版本还觉得不过瘾,有几个方向可以继续深入。

一个是引入消息队列,把预约通知、签到通知、违约通知全部异步化,提高系统的整体吞吐量。

一个是用Netty或WebSocket做实时座位状态推送。现在用户预约之后要一直刷新页面才能看到座位是否被别人占了,体验并不好。如果服务端能主动推送座位变更的WebSocket消息,系统会专业很多。

一个是引入监控体系。用Spring Boot Actuator暴露健康检查接口,配合Prometheus和Grafana做一个简单的监控面板,看看JVM内存、接口耗时、数据库连接池占用情况。这一步做完,整个项目的“工程化”味道就出来了,写在简历上也更有亮点。

最后再多说一句实际体会:做这类管理系统,技术永远是第二位的,第一位是你对业务场景的理解深度。如果当初我没有去自习室实地坐了一下午,观察用户的真实行为,我根本不会想到设计“2小时前可免费取消”这个规则,也不会想到信用分恢复需要“连续守约一周才能加回来”这种细节。技术方案在网上搜一搜到处都是,但能把业务细节打磨到让人用着舒服,才是真正拉开差距的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询