尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Java车位租赁管理系统:并发控制与状态机设计实战

发布时间:2026/9/29 17:59:51

资讯中心
01
ARTICLE

Java车位租赁管理系统:并发控制与状态机设计实战

Java车位租赁管理系统:并发控制与状态机设计实战
简介这是一套面向高校计算机专业学生与Java初学者、课程设计或毕业设计开发者的车位租赁管理系统完整项目资料围绕停车位信息管理、租赁合同、用户与订单处理等业务场景提供从需求分析到代码落地的整套参考方案。压缩包共406个文件约13.29MB以63个java源文件、38个jsp页面、38个xml配置、131个js脚本及66个class编译文件为主另含jar依赖、css样式、properties配置与sql建库脚本覆盖前端交互、后端控制与数据库持久化各层。项目按控制器、实体、示例查询等模块组织包含用户、合同、车位列表、申请与结算等业务控制器结构清晰便于对照理解分层设计与接口调用逻辑。目前已有168人学习下载适合需要快速搭建课程设计框架、参考完整工程目录与数据库脚本的读者也可作为答辩PPT与项目报告撰写的素材来源。1. 车位租赁管理系统从一份 Java 课程设计到能跑通的真实业务闭环很多同学拿到「基于 Java 的车位租赁管理系统」这个题目时第一反应是去搜一套现成源码改改界面、换换配色然后写报告交差。但真正做过一遍的人都知道这类系统最难的从来不是页面好不好看而是车位状态在「空闲—锁定—已租—释放」之间流转时数据一致性怎么保证。我见过太多版本前端点一下「立即租赁」按钮后台直接 update 一条记录就完事结果两个人同时抢同一个车位数据库里出现两条有效租约答辩时被老师一句「并发怎么处理」问得哑口无言。这个系统本质上是一个带时间维度的资源占用管理问题车位是有限资源租约是有起止时间的占用凭证用户、订单、缴费、续租、退租都围绕这条时间线展开。它适合计算机专业做课程设计或毕业设计的同学也适合想练手 Java Web 全栈的初级开发者。一套能拿得出手的实现至少要把数据库表关系、状态机、并发控制这三件事讲清楚而不是堆一堆 CRUD 接口。下面我按自己带学生做这类项目的实际路径把选型、建表、核心逻辑和踩过的坑一次讲透。2. 技术选型与数据库设计为什么这套组合最适合课程设计落地2.1 后端选 Spring Boot MyBatis 而不是原生 Servlet 的理由课程设计常见的另一条路是 JSP Servlet JDBC优点是「看起来像自己写的」缺点是代码量巨大且容易在事务和连接池上翻车。我一般推荐 Spring Boot 2.7.x 搭配 MyBatis原因很实际内置 Tomcat 省去配置Transactional注解直接解决租约写入的事务问题MyBatis 的 XML 映射对课程设计里那些多表关联查询比如查某用户所有有效租约对应的车位编号比 JPA 更直观。依赖清单里必须有的是spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java、druid-spring-boot-starter连接池方便监控 SQL、lombok。前端如果不想折腾脚手架用 Thymeleaf 服务端渲染就够省去跨域和 token 的麻烦想显得现代一点就 Vue3 Axios但要注意课程设计答辩时老师更关心业务逻辑不是前端工程化。!-- pom.xml 关键依赖版本按 Spring Boot 父工程管理即可 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.20/version /dependency这段依赖里Druid 的作用不只是连接池它自带 SQL 监控页面调试阶段打开stat-view-servlet就能看到每条租约查询的执行时间和慢 SQL比盲猜快得多。MyBatis 版本不要追最新2.3.x 和 Spring Boot 2.7 配合最稳3.x 需要 Spring Boot 3 和 JDK17课程设计环境未必跟得上。2.2 车位、租约、用户三张核心表的字段与索引设计表设计是这类系统的地基。我见过把车位状态直接冗余在车位表里、租约表只存一条记录的写法退租时状态回滚全靠代码手动改一旦漏改就出现「车位显示已租但没有任何有效租约」的脏数据。正确做法是车位表只存物理属性和一个「当前是否可租」的冗余标志真正的占用关系全部由租约表的时间区间决定冗余标志通过定时任务或触发器与租约表对齐。-- 车位表物理信息 冗余状态 CREATE TABLE parking_space ( id BIGINT PRIMARY KEY AUTO_INCREMENT, space_no VARCHAR(20) NOT NULL UNIQUE COMMENT 车位编号如 A-001, location VARCHAR(100) COMMENT 所在区域, type TINYINT DEFAULT 1 COMMENT 1普通 2充电 3无障碍, status TINYINT DEFAULT 0 COMMENT 0空闲 1已租 2维护, hourly_rate DECIMAL(8,2) DEFAULT 5.00, INDEX idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 租约表时间区间是核心 CREATE TABLE lease ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, space_id BIGINT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 1 COMMENT 1生效 2已退租 3已过期, total_fee DECIMAL(10,2), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_space_time (space_id, start_time, end_time), INDEX idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;idx_space_time这个联合索引是并发校验的关键后面判断「某车位在指定时间段是否已被占用」的查询会直接命中它。字段类型上金额用DECIMAL而不是FLOAT这是血泪经验浮点数算租金会出现 0.30000000000000004 这种结果对账时很难看。status用TINYINT而不是字符串省空间且比较快。2.3 用状态机约束租约流转避免脏数据租约不是随便改状态的。一个租约从创建到结束合法路径只有生效 → 已退租或生效 → 已过期。任何「已退租」的记录不允许再改回生效这是业务底线。我一般会在 Service 层写一个简单的状态校验方法而不是依赖数据库约束因为数据库约束报错信息对用户不友好。public void terminateLease(Long leaseId) { Lease lease leaseMapper.selectById(leaseId); if (lease null) { throw new BizException(租约不存在); } // 只有生效中的租约才能退租 if (lease.getStatus() ! LeaseStatus.ACTIVE.getCode()) { throw new BizException(当前租约状态不允许退租); } lease.setStatus(LeaseStatus.TERMINATED.getCode()); leaseMapper.updateById(lease); // 退租后释放车位注意这里要判断是否还有其他有效租约 releaseSpaceIfNoActiveLease(lease.getSpaceId()); }releaseSpaceIfNoActiveLease这个方法不能简单地把车位状态改成空闲必须先查该车位是否还存在其他生效中的租约比如同一车位被分时段租给不同人确认没有才释放。这个细节是区分「能跑」和「能答辩」的分水岭。3. 核心业务实现租赁、续租与并发抢占的代码落地3.1 租赁下单接口时间冲突检测的 SQL 与事务边界租赁的核心是「在用户选定的时间段内这个车位没有被别人占用」。判断逻辑用一句 SQL 就能表达存在一条生效租约其时间区间与请求区间有重叠。重叠的判定条件是existing.start_time request.end_time AND existing.end_time request.start_time注意是严格小于和大于边界相接不算冲突。Transactional(rollbackFor Exception.class) public Lease createLease(Long userId, Long spaceId, Date start, Date end) { // 1. 校验时间合法性 if (!start.before(end)) { throw new BizException(开始时间必须早于结束时间); } // 2. 冲突检测这里用 select ... for update 锁住相关行 int conflict leaseMapper.countConflict(spaceId, start, end); if (conflict 0) { throw new BizException(该时间段车位已被占用); } // 3. 写入租约 Lease lease new Lease(); lease.setUserId(userId); lease.setSpaceId(spaceId); lease.setStartTime(start); lease.setEndTime(end); lease.setStatus(LeaseStatus.ACTIVE.getCode()); lease.setTotalFee(calcFee(spaceId, start, end)); leaseMapper.insert(lease); // 4. 更新车位冗余状态 parkingSpaceMapper.markRented(spaceId); return lease; }对应的 Mapper XML 里countConflict要写成带FOR UPDATE的查询把该车位所有可能冲突的租约行锁住防止两个线程同时通过检测。事务边界放在 Service 方法上保证「检测—写入—更新车位」三步要么全成要么全败。select idcountConflict resultTypeint SELECT COUNT(*) FROM lease WHERE space_id #{spaceId} AND status 1 AND start_time lt; #{end} AND end_time gt; #{start} FOR UPDATE /select参数说明#{spaceId}是车位主键#{start}和#{end}是请求的起止时间。FOR UPDATE在 InnoDB 下会锁住扫描到的索引记录配合idx_space_time索引锁范围可控。如果不用FOR UPDATE两个并发请求可能都查到 0 条冲突然后各自插入这就是典型的幻读场景。3.2 续租与费用计算按小时还是按天参数怎么定费用计算方式直接影响用户体验和答辩时的业务合理性。我一般设计成「按小时计费不足一小时按一小时算单日封顶」。这样既灵活又不会出现租 23 小时比租 24 小时还贵的荒谬情况。核心参数有三个hourly_rate车位单价、daily_cap日封顶可设为 hourly_rate 的 8 到 10 倍、min_unit最小计费单位默认 1 小时。public BigDecimal calcFee(Long spaceId, Date start, Date end) { ParkingSpace space parkingSpaceMapper.selectById(spaceId); long millis end.getTime() - start.getTime(); // 向上取整到小时 long hours (long) Math.ceil(millis / 3600000.0); BigDecimal fee space.getHourlyRate().multiply(BigDecimal.valueOf(hours)); // 日封顶按跨越的自然日数计算上限 long days (long) Math.ceil(millis / 86400000.0); BigDecimal cap space.getHourlyRate() .multiply(BigDecimal.valueOf(8)) .multiply(BigDecimal.valueOf(days)); return fee.min(cap); }续租的逻辑是在原租约的end_time基础上延长但延长前必须重新做一次冲突检测因为原租约结束后的时间段可能已经被别人订走。续租不是简单 updateend_time而是「先检测新区间通过后更新」。这里有个容易忽略的点续租检测时要排除自己这条租约否则会和自己冲突。3.3 用乐观锁还是悲观锁并发抢占车位的两种方案对比并发抢占是这类系统最值得讲的点。悲观锁就是上面用的SELECT ... FOR UPDATE简单直接缺点是锁持有时间长高并发下容易排队。乐观锁的做法是在车位表加version字段更新时带版本号失败就重试。方案实现方式适用场景缺点悲观锁FOR UPDATE锁行冲突概率高、并发量中等锁等待可能死锁乐观锁version 字段 CAS冲突概率低、读多写少失败重试逻辑要写好唯一索引对 (space_id, 时间段) 建约束固定时段租赁时间段灵活时无法建课程设计里我推荐悲观锁因为实现简单、逻辑清晰答辩时好讲。乐观锁的重试逻辑如果写不好反而会出现「重试三次都失败」的尴尬。唯一索引方案在时间段不固定时基本不可用因为 MySQL 没法对区间建唯一约束。注意无论用哪种锁冲突检测和写入必须在同一个事务里否则锁的意义就没了。4. 避坑与排查那些让系统「看起来能跑」的隐蔽问题4.1 现象两个用户同时租到同一车位数据库出现重叠租约原因冲突检测 SQL 没有加FOR UPDATE或者事务隔离级别是 READ COMMITTED 且检测和插入之间没有锁保护两个线程都读到「无冲突」然后各自插入。解决在检测查询上加FOR UPDATE并确认 Service 方法有Transactional同时把 MySQL 隔离级别保持默认的 REPEATABLE READ。4.2 现象退租后车位状态没变回空闲新用户无法租赁原因退租逻辑只改了租约状态忘了同步车位表的冗余status字段或者同步时没判断该车位是否还有其他生效租约。解决退租后调用releaseSpaceIfNoActiveLease先SELECT COUNT(*) FROM lease WHERE space_id? AND status1为 0 才把车位置为空闲。4.3 现象费用计算出现 0.30000000000000004 这类小数原因金额字段用了FLOAT或DOUBLEJava 端用了double做运算。解决数据库用DECIMAL(10,2)Java 端统一用BigDecimal且BigDecimal的除法必须指定精度和舍入模式否则会抛ArithmeticException。4.4 现象续租时提示「时间冲突」但明明是自己这条租约原因续租的冲突检测没有排除当前租约 ID。解决在countConflict的 SQL 里加AND id ! #{excludeLeaseId}续租时把当前租约 ID 传进去。4.5 现象Druid 连接池报wait millis 60000接口全部超时原因某个事务里做了远程调用或大量循环查询连接被长时间占用或者FOR UPDATE锁等待导致连接堆积。解决检查慢 SQL 日志把非数据库操作移出事务给FOR UPDATE查询加合理的索引缩小锁范围连接池maxActive不要设太小课程设计环境设 20 足够。5. 答辩加分项把「能跑」变成「讲得清」的三个技巧第一个技巧是准备一张状态流转图手画或 PPT 都行把车位和租约的状态变化画出来答辩时老师一看就知道你理解业务。第二个技巧是主动讲并发场景哪怕你的实现只是悲观锁也要说清楚「为什么不用乐观锁」「锁的粒度是什么」这比被动回答强得多。第三个技巧是留一个可演示的边界用例比如「租 23 小时 59 分」和「租 24 小时」的费用对比现场跑给老师看证明你的计费逻辑经得起推敲。// 边界用例验证日封顶逻辑 Test public void testDailyCap() { Date start parseTime(2024-01-01 00:00:00); Date end23h parseTime(2024-01-01 23:59:00); Date end24h parseTime(2024-01-02 00:00:00); BigDecimal fee23 leaseService.calcFee(1L, start, end23h); BigDecimal fee24 leaseService.calcFee(1L, start, end24h); // 23小时按24小时封顶算24小时也按封顶算两者应相等 assertEquals(fee23, fee24); }这个测试用例的价值在于它逼你把「不足一天怎么算」这个模糊地带想清楚。我自己的习惯是每写完一个计费方法先写三个边界测试最小单位、跨天、封顶临界点。这三个过了基本不会在答辩现场翻车。数据库脚本和源码结构按这个思路整理报告里的「系统设计」章节自然就有内容可写不用硬凑。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。