ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java停车场系统实战:并发锁、BigDecimal计费与SQL优化

Java停车场系统实战:并发锁、BigDecimal计费与SQL优化 简介本资源是一套面向Java初学者与课程设计学生的Web停车场管理系统完整开发套件聚焦B/S架构下的实际业务系统开发全流程。资源涵盖Java后端SpringServletJSP、MySQL数据库含建表SQL与示例数据、配套论文含开题、任务书、答辩PPT、部署与功能模块教学视频以及16张核心界面截图助学习者贯通需求分析、编码实现、数据库设计、文档撰写与演示汇报全环节。压缩包共16个文件含7张系统界面PNG截图、3份Word论文文档、2段MP4实操视频含环境部署与车位管理模块详解、1个ZIP源码包、1个DB数据库文件、1个SQL脚本及1个答辩PPTX整体大小40.51MB。已有1632人学习下载内容结构清晰、模块对应明确特别适合Java Web课程实训、毕业设计参考及智慧停车类项目快速复现与二次开发。1. 为什么一个“Web停车场管理系统”能成为Java初学者的通关副本——它不是Demo是真实业务闭环的最小切口你可能已经刷过几十个“Java Web学生管理系统”“图书借阅系统”但它们往往卡在登录页就没了下文没有车辆进出的真实时间戳逻辑没有车位状态的实时抢占与释放没有超时计费的阶梯规则更没有管理员面对300个车位同时被占时的并发焦虑。而这个“基于Web停车场管理系统”恰恰把「时间敏感型资源调度」塞进了最朴素的SSM或Spring Boot技术栈里——它用MySQL存车位状态、用Java定时任务算停车费、用JSP/Thymeleaf渲染实时空位数、用SQL脚本一键初始化200个带编号和类型固定/临时的车位。这不是玩具项目它是你第一次亲手把“数据库增删改查”变成“车主扫码进场→系统锁位→离场自动计费→财务导出日报”的完整链路。适合刚学完ServletJDBC、正卡在“怎么把书上代码变成能跑通的网站”的人也适合面试前想拿一个有真实业务逻辑、能讲清并发控制点、能现场演示SQL优化过程的项目背书的Java工程师。别被标题里的“论文视频”吓退——核心价值永远在源码和SQL里那才是你能抄、能改、能压测、能调优的实体。2. 从零搭起系统骨架选Spring Boot还是SSM为什么我坚持用MyBatis-Plus生成建表SQL2.1 技术栈决策为什么放弃Struts2也不直接上Spring Cloud停车场管理系统的本质是「单体高并发读写」高峰期每分钟可能有50辆车进出每辆车触发1次入场插入、1次出场更新计费计算、后台每秒轮询空位数。这种场景下Spring Boot MyBatis-Plus 的组合比传统SSMSpringSpringMVCMyBatis更省心——它自带内嵌Tomcat避免了XML配置地狱MyBatis-Plus的TableField(fill FieldFill.INSERT)能自动填充创建时间比手写拦截器少3个类更重要的是它的AutoGenerator能根据Java实体类反向生成建表SQL这直接解决了新手最头疼的问题“我写了Car实体类但不知道MySQL里字段类型该设VARCHAR(16)还是CHAR(16)主键要不要加索引外键约束怎么写才不报错”。而Spring Cloud对这个项目是过度设计不需要服务拆分车位数据必须强一致性不需要网关鉴权管理员登录用Session足矣ZooKeeper注册中心反而增加部署复杂度。我见过太多人用Spring Cloud搭了个单体停车场系统结果光配Nacos就耗掉两天最后连“入场按钮点击没反应”都查不出是Feign超时还是Hystrix熔断——这完全偏离了练手初衷。2.2 用MyBatis-Plus生成建表SQL三步搞定带索引和注释的MySQL脚本MyBatis-Plus的代码生成器不是摆设它能输出符合生产习惯的SQL。关键在于配置StrategyConfig时指定字段策略和数据库关键字处理// 生成器配置核心片段放在generator.java中 StrategyConfig strategy new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); // 数据库下划线转Java驼峰 strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setEntityLombokModel(true); // 生成Lombok注解 strategy.setRestControllerStyle(true); strategy.setInclude(parking_space, parking_record, user_info); // 只生成这三个表 strategy.setTablePrefix(t_); // 表名前缀统一为t_ strategy.setCapitalMode(true); // 开启大写命名适配MySQL严格模式生成后得到的ParkingSpace.java实体类会自动带上TableName(t_parking_space)和TableId(type IdType.ASSIGN_ID)。此时执行MybatisPlusGenerator的execute()方法它会输出schema.sql文件内容类似-- t_parking_space 车位表 CREATE TABLE t_parking_space ( id bigint NOT NULL COMMENT 主键ID, space_no varchar(10) NOT NULL COMMENT 车位编号如A001, space_type tinyint NOT NULL DEFAULT 1 COMMENT 车位类型1-固定2-临时, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1-空闲2-占用3-维修, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_space_no (space_no), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车位基础信息表;注意生成的SQL默认不含外键约束MyBatis-Plus为解耦默认关闭但停车场业务中parking_record表必须关联parking_space.id和user_info.id。你需要手动在生成的SQL末尾追加ALTER TABLE t_parking_record ADD CONSTRAINT fk_record_space FOREIGN KEY (space_id) REFERENCES t_parking_space(id); ALTER TABLE t_parking_record ADD CONSTRAINT fk_record_user FOREIGN KEY (user_id) REFERENCES t_user_info(id);2.3 数据库初始化为什么必须用SQL脚本而非Hibernate ddl-auto很多新手图省事在application.yml里写spring.jpa.hibernate.ddl-autocreate结果上线后发现所有历史数据没了。停车场系统要求数据零丢失——管理员昨天录入的200个固定车位、上周的收费记录绝不能因为重启应用就清空。所以必须用SQL脚本初始化init_data.sql插入初始车位A001-A100为固定车位B001-B100为临时车位设置status1空闲test_data.sql模拟10条已出场记录验证计费逻辑执行顺序必须是先schema.sql建表 → 再init_data.sql插基础数据 → 最后test_data.sql补测试数据。我一般把这三个SQL文件放在src/main/resources/sql/下启动时用SpringApplicationRunListener监听ApplicationStartedEvent事件触发执行——这样比PostConstruct更可靠能确保在任何Bean初始化前完成建库。3. 核心业务落地入场、出场、计费、空位统计四段代码决定系统是否“真能用”3.1 入场逻辑为什么用Redis锁而不是synchronized——解决“同一车位被抢注”问题当两辆车几乎同时扫同一个二维码进场时如果只用Java层synchronized(parkingSpaceId)由于请求打到不同服务器实例锁根本不起作用。必须用分布式锁。这里不用Redisson太重直接用Redis原生命令// ParkingService.java public boolean enterParking(String spaceNo, String carPlate) { String lockKey lock:space: spaceNo; String requestId UUID.randomUUID().toString(); // 尝试获取锁3秒过期防止死锁 Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, Duration.ofSeconds(3)); if (!locked) { throw new BusinessException(车位 spaceNo 正在被其他车辆占用请稍候); } try { // 1. 查车位状态 ParkingSpace space parkingSpaceMapper.selectOne( new QueryWrapperParkingSpace().eq(space_no, spaceNo)); if (space null || space.getStatus() ! 1) { // 非空闲状态 throw new BusinessException(车位 spaceNo 不可用); } // 2. 更新车位为占用 space.setStatus(2); parkingSpaceMapper.updateById(space); // 3. 记录入场记录 ParkingRecord record new ParkingRecord(); record.setSpaceId(space.getId()); record.setCarPlate(carPlate); record.setEnterTime(new Date()); record.setStatus(1); // 1-进行中 parkingRecordMapper.insert(record); return true; } finally { // Lua脚本保证删除锁的原子性 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; redisTemplate.execute(new DefaultRedisScript(script, Long.class), Collections.singletonList(lockKey), requestId); } }参数说明lockKey用space_no做粒度比锁整个表更细requestId防误删避免A线程锁过期后B线程删了A的锁Duration.ofSeconds(3)是经验值——入场操作通常1秒设3秒足够覆盖网络抖动。3.2 出场计费为什么用BigDecimal而不是double——避免0.10.20.30000000000000004停车场计费规则往往是“首小时5元之后每半小时3元”这种阶梯计费用double累加必然出现精度丢失。必须用BigDecimal// ParkingRecordService.java public BigDecimal calculateFee(Date enterTime, Date exitTime) { long minutes (exitTime.getTime() - enterTime.getTime()) / (1000 * 60); BigDecimal fee BigDecimal.ZERO; if (minutes 60) { fee new BigDecimal(5.00); } else { // 首小时5元超出部分按每30分钟3元计 long extraMinutes minutes - 60; long halfHours extraMinutes % 30 0 ? extraMinutes / 30 : extraMinutes / 30 1; // 向上取整 fee fee.add(new BigDecimal(5.00)) .add(new BigDecimal(3.00).multiply(BigDecimal.valueOf(halfHours))); } // 保留两位小数四舍五入 return fee.setScale(2, RoundingMode.HALF_UP); }血泪经验曾有个项目用double fee 5.0 (extraMinutes / 30.0) * 3.0结果用户交了19.999999999999996元财务对账时疯了。BigDecimal的setScale(2, RoundingMode.HALF_UP)是唯一安全解。3.3 实时空位统计为什么用MySQL COUNT(*)而不是缓存——数据一致性优先级高于性能有人提议“把空位数存在Redis里每次入场1、出场-1”但这是危险的——如果Redis宕机空位数就永久错乱。停车场系统里“显示还有12个空位”直接影响车主决策必须绝对准确。所以采用数据库实时COUNT// ParkingSpaceController.java GetMapping(/available-count) public ResultInteger getAvailableCount() { // 直接查数据库不走缓存 Integer count parkingSpaceMapper.selectCount( new QueryWrapperParkingSpace().eq(status, 1)); // status1为空闲 return Result.success(count); }性能实测MySQL 5.7 InnoDB引擎下对200行的parking_space表执行COUNT(*) WHERE status1平均耗时3msQPS轻松破2000。只有当车位数超10万时才需考虑Redis缓存定时刷新比如每5秒同步一次。3.4 管理员报表如何用一条SQL查出“昨日各时段入场车次”前端要画折线图展示“早高峰7-9点入场120辆”后端不能循环查12次。用MySQL的HOUR()函数分组-- 查询昨日每小时入场车辆数按小时聚合 SELECT HOUR(enter_time) as hour_of_day, COUNT(*) as car_count FROM t_parking_record WHERE DATE(enter_time) DATE_SUB(CURDATE(), INTERVAL 1 DAY) AND status 1 -- 仅统计已入场未出场的记录 GROUP BY HOUR(enter_time) ORDER BY hour_of_day;避坑提示DATE(enter_time) CURDATE()会忽略索引必须用enter_time 2024-05-20 00:00:00 AND enter_time 2024-05-21 00:00:00才能走enter_time索引。上面SQL中DATE_SUB(CURDATE(), INTERVAL 1 DAY)虽可读性好但实际生产环境建议用预计算日期字符串传参。4. 避坑指南那些让开发者凌晨三点还在抓头发的5个真实问题4.1 现象入场成功但页面显示“车位已被占用”原因前端提交的space_no带空格如A001 而数据库里存的是A001WHERE space_no ?匹配失败。解决在Controller层用RequestParam String spaceNo接收后立即执行spaceNo spaceNo.trim()同时在MySQL建表时给space_no字段加COLLATE utf8mb4_unicode_ci忽略大小写和空格差异。4.2 现象MySQL执行UPDATE t_parking_space SET status2 WHERE space_noA001返回0行影响原因space_no字段类型是CHAR(10)而MySQL的CHAR类型会右补空格。当存入A001时实际存储为A001 共10字符查询时A001不等于A001 。解决建表时将space_no类型改为VARCHAR(10)或在查询条件中用TRIM(space_no) A001但会失效索引不推荐。4.3 现象定时任务每天0点执行计费结算但某天凌晨1点才发现有3条记录没结算原因Scheduled(cron 0 0 0 * * ?)依赖服务器本地时间若服务器时区设为UTC而业务要求东八区时间则任务在UTC时间0点即北京时间8点执行。解决在application.yml中强制指定时区spring.jackson.time-zone: GMT8并在Scheduled上显式声明时区Scheduled(cron 0 0 0 * * ?, zone GMT8)。4.4 现象导出Excel报表时中文全是??号原因Tomcat服务器默认编码为ISO-8859-1而Spring Boot的Content-Type未指定charsetUTF-8。解决在Controller返回Excel时显式设置响应头response.setContentType(application/vnd.ms-excel;charsetUTF-8); response.setHeader(Content-Disposition, attachment;filename URLEncoder.encode(停车场日报.xls, UTF-8));4.5 现象用MyBatis-Plus的selectPage分页查询第1页正常第2页数据重复原因MySQL分页LIMIT 10,10在ORDER BY字段存在重复值时如多个记录update_time相同会导致排序不稳定第二页可能包含第一页的数据。解决在ORDER BY后追加主键id作为第二排序条件ORDER BY update_time DESC, id DESC。MyBatis-Plus中配置PageT时用new QueryWrapper().orderByDesc(update_time).orderByDesc(id)。5. 进阶技巧用慢SQL日志定位“为什么出场要等5秒”——三步揪出隐藏瓶颈5.1 开启MySQL慢查询日志找到真正拖慢系统的SQL很多开发者以为“系统慢是因为Java代码”其实90%的性能问题在SQL。先在MySQL配置文件my.cnf中开启慢日志# my.cnf slow_query_log ON slow_query_log_file /var/log/mysql/mysql-slow.log long_query_time 1 # 超过1秒的SQL记为慢SQL log_queries_not_using_indexes ON # 记录未走索引的查询重启MySQL后执行几次出场操作然后查看慢日志tail -100 /var/log/mysql/mysql-slow.log你会看到类似# Query_time: 4.231245 Lock_time: 0.000123 Rows_sent: 1 Rows_examined: 15000 SET timestamp1716234567; SELECT * FROM t_parking_record WHERE car_plate 粤B12345 AND status 1;关键指标解读Rows_examined: 15000表示这条SQL扫描了1.5万行才找到目标记录——显然car_plate字段没建索引5.2 给高频查询字段加索引一条命令提升10倍速度针对上面的慢SQL给car_plate和status联合建索引因为WHERE条件是car_plate ? AND status ?-- 在t_parking_record表上创建复合索引 ALTER TABLE t_parking_record ADD INDEX idx_carplate_status (car_plate, status);为什么不是单列索引单独给car_plate建索引MySQL在WHERE car_plate ? AND status ?时仍需回表过滤status而联合索引(car_plate, status)能直接定位到满足两个条件的行Rows_examined从15000降到1。5.3 验证索引效果用EXPLAIN看执行计划是否走索引执行EXPLAIN命令对比加索引前后EXPLAIN SELECT * FROM t_parking_record WHERE car_plate 粤B12345 AND status 1;加索引前type是ALL全表扫描加索引后type变为refkey显示idx_carplate_statusrows从15000降到1——这就是性能提升的铁证。5.4 延伸技巧用pt-query-digest分析慢日志自动生成优化建议如果慢SQL不止一条手动分析效率低。用Percona Toolkit的pt-query-digest# 安装Percona ToolkitUbuntu sudo apt-get install percona-toolkit # 分析慢日志输出TOP10慢SQL及优化建议 pt-query-digest /var/log/mysql/mysql-slow.log --limit 10它会告诉你“SELECT * FROM t_parking_record WHERE status 1这条SQL扫描了全表建议添加INDEX(status)”甚至给出ALTER TABLE命令——这比自己猜快10倍。我带过的实习生里80%的人第一次接触慢SQL优化时都以为要重写Java代码。直到他们亲眼看到EXPLAIN里rows从15000变成1才真正理解“数据库设计比Java语法重要十倍”。现在我每个新项目上线前必做三件事开慢日志、跑压力测试、用pt-query-digest扫一遍。这已经成了我的肌肉记忆——不是为了炫技而是不想再半夜被报警电话叫醒只因为忘了给car_plate加索引。希望帮到你。本文还有配套的精品资源点击获取
返回列表