简介:基于SSM框架的智能停车场管理系统是一套面向Java学习者与课程设计场景的完整项目源码,整合Spring、Spring MVC与MyBatis三大框架,实现车牌识别、自动计费、车位监控、报表统计等核心业务,可支撑毕业设计或停车管理类项目二次开发。压缩包共1324个文件,约21.83MB,其中包含119个Java源文件、146个JSP页面、358个JS脚本、145个CSS样式以及2个SQL数据库脚本,另有图片、字体等静态资源,目录结构清晰,覆盖后端逻辑、前端交互与数据库设计,便于直接导入IDE运行学习。已有118人学习浏览,适合通过项目实战理解SSM整合流程、权限控制及异常处理等知识点。资源附带的配置文件、建表语句和前端模板,能帮助读者快速搭建运行环境,还可根据需求扩展VIP客户管理、设备维护等功能模块,是巩固Java Web开发技能的实用素材。
1. 基于SSM框架的智能停车场,不止是增删改查
在Java Web开发里,SSM框架——Spring、SpringMVC、MyBatis的组合——早已是面试和简历上的常客。但当它落到“智能停车场系统”这种带实体业务的项目时,很多人写出来的东西仍停留在管理员对车位表的简单增删改查,外加一个尴尬的计时器。真正的智能停车场,核心不在于“管理”,而在于“状态流转”:一个车位从空闲到占用,一笔订单从进行到结算,一辆车从入场到出场,背后是一连串强一致性的操作。
这个标题的本质需求,是用SSM这套经典架构,把一个带状态机、计费规则、并发抢占的业务系统落地。它适合正在准备毕设、刚入行想攒项目经验的开发者,也适合工作了几年想用SSM快速搭一套管理后台的工程师。下面这套做法,是我在实际项目中验证过的方案——从框架整合到数据库设计,再到计费引擎和并发控制,每一步都有可以复现的命令和代码。
2. SSM框架的整合链路:从依赖到二开配置的完整拆解
2.1 SSM框架的职责边界与选型理由
SSM框架不是一个单独的技术,而是三层协作:Spring管理对象与事务,SpringMVC处理请求路由,MyBatis负责数据库访问。相比Spring Boot,SSM框架的整合过程更繁琐,但也正因如此,它能帮你理解Spring IoC容器、AOP事务、DispatcherServlet这些底层机制是如何串联起来的。如果你把SSM框架的整合流程吃透,再看Spring Boot的自动配置,本质就是把这些手动配置变成了约定。
在智能停车场场景下,SSM框架有一个明显的优势:MyBatis的SQL是自己掌控的,遇到复杂的计费统计、车位报表,可以直接写SQL,不需要受ORM生成的僵化查询限制。比如统计某个时间段的车位周转率,一条GROUP BY加上条件聚合就能完成,这在智能停车场这种重数据、重状态的业务里非常实用。
2.2 依赖引入与版本搭配
用Maven构建时,SSM框架的依赖版本搭配是关键。Spring的版本和SpringMVC必须严格一致,而MyBatis与Spring的桥接需要单独引入mybatis-spring包。下面是经过验证的依赖组合:
<properties> <spring.version>5.3.23</spring.version> <mybatis.version>3.5.10</mybatis.version> </properties> <dependencies> <!-- Spring核心与SpringMVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis及与Spring桥接 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <!-- 数据库驱动和连接池 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.15</version> </dependency> </dependencies>这段配置的核心在于mybatis-spring的2.0.7版本。它的主要作用是把MyBatis的SqlSessionFactory交给Spring容器管理,让Mapper接口可以自动代理注入到Service层,同时复用Spring的事务管理器。如果不引入这个桥接包,MyBatis和Spring就像两个独立的系统,事务无法统一控制。
2.3 Spring与MyBatis整合:SqlSessionFactory的3个必调参数
Spring与MyBatis整合的唯一入口就是SqlSessionFactoryBean,它有三个参数决定了框架的运行方式,任何智能停车场项目的SSM框架配置都绕不开:
<!-- spring-mybatis.xml --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/smart_parking?useSSL=false&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="root"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.parking.entity"/> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> <property name="logImpl" value="org.apache.ibatis.logging.stdout.StdOutImpl"/> </bean> </property> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.parking.mapper"/> </bean>参数说明如下:
mapperLocations指定Mapper XML文件的路径,这是智能停车场项目中所有SQL的存放位置。如果路径配错,项目启动时不会报错,但一旦调用Mapper方法就会抛出BindingException。mapUnderscoreToCamelCase这个属性我建议一定要设为true,它可以把数据库的create_time自动映射到实体类的createTime,省去大量resultMap手写。typeAliasesPackage让Mapper XML里可以直接写parameterType="ParkingOrder",而不必写全限定名。
MapperScannerConfigurer的作用是扫描com.parking.mapper包下的所有接口,为每个接口生成代理实现类并注册到Spring容器。这样Service层直接@Autowired注入Mapper接口即可,不用自己写实现类。
2.4 SpringMVC配置:三大件与JSON响应的坑
SpringMVC的本质是一个Servlet,它接管所有/路径的请求。配置上需要三个核心组件,同时在智能停车场系统中需要处理JSON数据交互:
<!-- spring-mvc.xml --> <context:component-scan base-package="com.parking.controller"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper"> <bean class="com.fasterxml.jackson.databind.ObjectMapper"> <property name="dateFormat"> <bean class="java.text.SimpleDateFormat"> <constructor-arg value="yyyy-MM-dd HH:mm:ss"/> </bean> </property> </bean> </property> </bean> </mvc:message-converters> </mvc:annotation-driven> <mvc:default-servlet-handler/>为什么JSON转换器里的日期格式单独配置?因为默认的Jackson会把Date序列化为时间戳数字,前端拿到1680000000000根本无法判断是哪一天。智能停车场的订单时间、入场时间、出场时间全部是Date类型,统一格式为yyyy-MM-dd HH:mm:ss后,前端展示和调试都会容易很多。
mvc:default-servlet-handler是用来放行静态资源的——CSS、JS、图片不经过SpringMVC。不配置它的话,你写好的CSS文件会全部404。
2.5 验证SSM框架整合成功的最小动作
框架整合后不要急着写业务代码,先做一次最小验证,确保整条链路是通的:
# 启动Tomcat后,在浏览器(或Postman)请求: GET http://localhost:8080/parking/api/ping # 期望返回: # {"status":200,"message":"SSM框架运行正常","time":"2024-01-15 10:30:00"}这个接口的Controller长这样:
@Controller @RequestMapping("/api") public class PingController { @Autowired private ParkingSpaceMapper spaceMapper; @ResponseBody @GetMapping("/ping") public Map<String, Object> ping() { Map<String, Object> result = new HashMap<>(); // 验证Spring容器正常 result.put("status", 200); result.put("message", "SSM框架运行正常"); // 验证MyBatis能查到数据库 int total = spaceMapper.countAll(); result.put("spaceCount", total); return result; } }这个接口同时验证了三件事:Spring容器能注入Mapper、SpringMVC能处理请求、MyBatis能连上数据库执行SQL。只要spaceCount返回一个数值,就说明SSM框架的整合链路已经完整打通,后面的业务代码可以放心往下写了。
3. 智能停车场系统的数据库设计与MVC分层实现
3.1 核心表结构与状态字段的设计要点
智能停车场系统的数据库设计是整个项目的基础。我见过很多半途而废的项目,问题大多出在表结构上——要么一张表全塞进去,要么耦合太深。停车场系统的核心表应该拆成四张:车位表、用户表、订单表、计费规则表。
-- 车位表:记录停车场所有车位的物理信息与实时状态 CREATE TABLE parking_space ( id BIGINT PRIMARY KEY AUTO_INCREMENT, space_no VARCHAR(20) NOT NULL COMMENT '车位编号,如A-001', area VARCHAR(50) NOT NULL COMMENT '区域:A区/B区/C区', type TINYINT NOT NULL DEFAULT 1 COMMENT '1-普通车位 2-充电车位 3-无障碍车位', status TINYINT NOT NULL DEFAULT 0 COMMENT '0-空闲 1-占用 2-预留', location_desc VARCHAR(255) COMMENT '位置描述:靠近电梯口等', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号,用于并发控制', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_space_no (space_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 停车订单表:记录一次停车行为的完整生命周期 CREATE TABLE parking_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '业务订单号,全局唯一', space_id BIGINT NOT NULL COMMENT '关联车位ID', plate_number VARCHAR(10) NOT NULL COMMENT '车牌号', entry_time DATETIME NOT NULL COMMENT '入场时间', exit_time DATETIME COMMENT '出场时间(未出场为NULL)', duration_minutes INT COMMENT '停车时长(分钟),出场时计算', fee DECIMAL(10,2) COMMENT '应收费用', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-进行中 2-已完成 3-已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), INDEX idx_plate_status (plate_number, status), INDEX idx_space_status (space_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 计费规则表:把收费方案做成可配置的数据 CREATE TABLE billing_rule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, rule_name VARCHAR(50) NOT NULL COMMENT '规则名称:如-白天时段', start_time TIME NOT NULL COMMENT '生效开始时间', end_time TIME NOT NULL COMMENT '生效结束时间', first_hour_fee DECIMAL(10,2) NOT NULL COMMENT '首小时费用', additional_hour_fee DECIMAL(10,2) NOT NULL COMMENT '超出首小时的每小时费用', daily_cap DECIMAL(10,2) COMMENT '每日封顶费用,NULL表示不封顶', enabled TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户表:管理员与普通用户的统一表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(255) NOT NULL COMMENT 'BCrypt加密存储', role TINYINT NOT NULL DEFAULT 1 COMMENT '1-普通用户 2-管理员', phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;设计要点在于:订单表用status字段标识状态,出场时间为NULL表示停车进行中,这是天然的状态机表达。space_id上的索引和status上的联合索引idx_space_status,是为了支撑“查询某车位的当前订单”这高频操作——每次车辆入场、出场都要先查这个,索引能让查询走覆盖索引,极大降低延迟。parking_space表加version字段则是用乐观锁解决多个入口同时对一个车位操作的情况,这在停车场的ETC通道和多闸口场景下很重要。
3.2 车辆入场——状态流转与事务控制
车辆入场是整个系统中最能体现SSM框架优势的业务场景,它同时涉及多个表的更新,需要事务保证一致性。入场逻辑如下:车位状态从空闲变为占用、生成一条进行中的订单、如果是包月用户还要校验有效期。
@Service @Transactional(rollbackFor = Exception.class) // 类级别声明事务 public class ParkingServiceImpl implements ParkingService { @Autowired private ParkingSpaceMapper spaceMapper; @Autowired private ParkingOrderMapper orderMapper; @Autowired private BillingRuleMapper billingRuleMapper; @Override public ParkingOrder entry(String plateNumber, Long spaceId) { // 1. 悲观锁锁定车位记录,防止多入口并发抢同一车位 ParkingSpace space = spaceMapper.selectByIdForUpdate(spaceId); if (space == null) { throw new BizException("车位不存在"); } if (space.getStatus() != 0) { throw new BizException("车位已被占用"); } // 2. 更新车位状态为占用 spaceMapper.updateStatus(spaceId, 1); // 3. 生成订单 ParkingOrder order = new ParkingOrder(); order.setOrderNo(generateOrderNo()); order.setSpaceId(spaceId); order.setPlateNumber(plateNumber); order.setEntryTime(new Date()); order.setStatus(1); orderMapper.insert(order); return orderMapper.selectById(order.getId()); } private String generateOrderNo() { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); return sdf.format(new Date()) + UUID.randomUUID().toString().substring(0, 4).toUpperCase(); } }@Transactional注解保证了这个方法里三个操作的一致性:只要车位更新失败或者订单插入失败,整个事务回滚,车位不会出现“被占用但无订单”的脏状态。selectByIdForUpdate是MyBatis里加了FOR UPDATE的查询,会在数据库层面锁住这条车位记录,直到事务结束才释放。这个操作在高并发停车场入口尤为重要——两个车同时扫描同一个车位时,只有第一个请求能拿到锁,第二个请求会阻塞到锁释放后才发现车位已被占用。
这里的逻辑在SSM框架下的数据流是:Controller接收前端请求,调用Service的entry方法;Service通过MyBatis的Mapper接口操作数据库;事务由Spring统一控制提交或回滚。这个三层结构在日志里可以清晰看到——如果入口报错,Controller层抛出异常后,事务管理器会执行回滚,MyBatis的SQL日志会显示对应数量的更新语句被回滚。
3.3 车辆出场——计费规则引擎与状态收尾
车辆出场比入场多了一步费用计算,计费规则需要支持动态配置。常见做法是:按时间段区分收费标准,比如白天(8:00-20:00)首小时5元,超出部分每小时3元;夜间(20:00-次日8:00)整段收取10元。这个规则不写死在Java代码里,而是放在billing_rule表中。
public BigDecimal calculateFee(Date entryTime, Date exitTime) { long minutes = (exitTime.getTime() - entryTime.getTime()) / (1000 * 60); if (minutes <= 30) { // 30分钟内免费 return BigDecimal.ZERO; } // 查询生效的计费规则(简化逻辑:按小时计费) List<BillingRule> rules = billingRuleMapper.selectEnabled(); if (rules.isEmpty()) { throw new BizException("未配置计费规则"); } // 按最长的规则计算 BillingRule rule = rules.get(0); double hours = Math.ceil(minutes / 60.0); BigDecimal fee = rule.getFirstHourFee(); if (hours > 1) { BigDecimal additional = rule.getAdditionalHourFee() .multiply(BigDecimal.valueOf(hours - 1)); fee = fee.add(additional); } // 判断是否超过每日封顶 if (rule.getDailyCap() != null && fee.compareTo(rule.getDailyCap()) > 0) { fee = rule.getDailyCap(); } return fee; }出场时调用这个计算逻辑,完整代码如下:
public ParkingOrder exit(Long orderId) { ParkingOrder order = orderMapper.selectById(orderId); if (order == null || order.getStatus() != 1) { throw new BizException("订单不存在或已完成"); } Date exitTime = new Date(); order.setExitTime(exitTime); // 计算时长(分钟) long durationMs = exitTime.getTime() - order.getEntryTime().getTime(); int durationMinutes = (int) (durationMs / (1000 * 60)); order.setDurationMinutes(durationMinutes); // 计算费用 BigDecimal fee = calculateFee(order.getEntryTime(), exitTime); order.setFee(fee); order.setStatus(2); // 更新订单并释放车位 orderMapper.updateByPrimaryKey(order); spaceMapper.updateStatus(order.getSpaceId(), 0); return order; }错误处理上,出场操作也有可能发生衔接问题——订单更新成功但车位释放失败。因此出场操作同样需要事务保护。还有一个关键细节:计费使用BigDecimal而不是double,这是金融计算的基本要求,double的浮点误差在费用计算中会导致对账不平。
3.4 MyBatis动态SQL在停车场查询中的应用
智能停车场系统的后台管理常常需要组合查询:按车牌号、时间范围、车位区域、状态多个条件筛选订单。这个需求用MyBatis的动态SQL来写,比Java里拼接SQL优雅得多,也更安全:
<!-- ParkingOrderMapper.xml --> <select id="searchOrders" resultType="ParkingOrder"> SELECT * FROM parking_order <where> <if test="plateNumber != null and plateNumber != ''"> AND plate_number LIKE CONCAT('%', #{plateNumber}, '%') </if> <if test="startTime != null"> AND entry_time >= #{startTime} </if> <if test="endTime != null"> AND exit_time <= #{endTime} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} </select><where>标签会自动处理条件拼接——如果所有条件都为空,它会生成不带WHERE的查询,同时自动去除第一个条件前面的AND,避免WHERE AND这种SQL语法错误,这是MyBatis动态SQL里最实用的一个细节。
分页用LIMIT #{offset}, #{pageSize},offset在Java侧计算:(pageNum - 1) * pageSize,这是SSM框架项目中最常见的分页写法。
4. SSM框架整合之下,智能停车场的三个核心问题怎么解
4.1 并发抢占车位——乐观锁与悲观锁的选择
停车场入口通常有多个闸机,两个车同时申请同一车位是真实存在的并发场景。处理方式有两种:悲观锁用SELECT ... FOR UPDATE直接锁行,简单直接,但并发量上来后锁等待会拖慢吞吐;乐观锁通过version字段实现无锁更新,更适合高并发读写。
<!-- ParkingSpaceMapper.xml 乐观锁更新 --> <update id="occupyByVersion"> UPDATE parking_space SET status = 1, version = version + 1 WHERE id = #{spaceId} AND status = 0 AND version = #{version} </update>调用时检查影响行数:
public boolean tryOccupySpace(Long spaceId, Integer version) { int rows = spaceMapper.occupyByVersion(spaceId, version); return rows == 1; // 影响1行表示成功;影响0行表示版本冲突,车位已被别人抢走 }如果tryOccupySpace返回false,前端会提示“车位刚刚被占用,请重新选择”,用户刷新页面就能看到最新车位状态。在只更新一个表的场景下,乐观锁比悲观锁更合适;但如果入场操作涉及车位、订单、用户多个表的强一致更新,还是要把@Transactional放在外层方法,让悲观锁和事务配合,保证原子性。
4.2 每小时收费计算——SQL与Java的边界划分
计费规则如果在SQL里写复杂逻辑,会让SQL变得冗长且难以调整;如果纯Java计算,又得把规则表数据全部加载到内存。合理边界是:规则数据由MyBatis查询加载,计算逻辑放在Service层。这样的话,计费规则的调整只需要改数据库不需要改代码,运维成本大幅降低。例如调整首小时费用,只需要执行一条UPDATE,而不需要重新编译部署。
-- 调整A类车位白天的首小时费用 UPDATE billing_rule SET first_hour_fee = 6.00 WHERE rule_name = '白天时段' AND enabled = 1;4.3 车牌识别对接——给SSM框架项目留好扩展口
智能停车场系统的“智能”很大程度体现在车牌识别上。主流做法是集成第三方车牌识别SDK或摄像头设备,识别结果通过HTTP回调发送给后端。在SSM框架中,只需要在Controller中预留一个回调接口:
@RestController @RequestMapping("/api/device") public class DeviceCallbackController { @Autowired private ParkingService parkingService; @PostMapping("/plate-recognized") @ResponseBody public Map<String, Object> plateRecognized(@RequestBody PlateRecognizeResult result) { // 1. 校验设备签名,防止非法请求 if (!verifyDeviceSign(result.getDeviceId(), result.getSign())) { throw new BizException("设备签名验证失败"); } // 2. 查询空闲车位(按区域优先策略) ParkingSpace space = parkingService.findAvailableSpace(result.getArea()); if (space == null) { return Result.error("当前区域无空闲车位"); } // 3. 直接入场 ParkingOrder order = parkingService.entry(result.getPlateNumber(), space.getId()); return Result.success(order); } }第三方回调是HTTP请求,天然契合SpringMVC的接口暴露能力。设备回调触发的入场和人工录入入场走的是同一个Service方法,事务和状态管理逻辑完全一致,下游改造不需要动核心代码。
4.4 系统安全性——密码加密与SQL注入防护
SSM框架项目经常被忽略的是安全性设计,而上线后的攻击大多集中在两个地方:SQL注入和未加密密码。MyBatis的#{}预编译机制本身就能有效防止SQL注入——所有传参都会被当作字符串字面量处理,而不是拼接到SQL里。但如果有人在Mapper XML里手写了${},就会把参数直接拼入SQL,这种行为要严格禁止。
密码存储要使用BCrypt加密,SpringSecurity自带的BCryptPasswordEncoder直接可以用:
@Service public class UserServiceImpl implements UserService { @Autowired private SysUserMapper userMapper; private BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); public void register(String username, String rawPassword) { SysUser user = new SysUser(); user.setUsername(username); // 加密后再存入数据库,天然带随机盐 user.setPassword(encoder.encode(rawPassword)); userMapper.insert(user); } public boolean login(String username, String rawPassword) { SysUser user = userMapper.selectByUsername(username); if (user == null) { return false; } // 校验密文与明文是否匹配 return encoder.matches(rawPassword, user.getPassword()); } }BCrypt的特点是无法逆向解密,每次加密结果都不同(自动加盐),即使两个用户密码相同,数据库里存的密文也不一样。这让撞库攻击变得几乎不可能成功。
5. SSM框架项目的性能体检:用数据说话
5.1 百万级订单的分页查询优化
当停车订单累积到百万级后,LIMIT offset, pageSize会出现明显的性能退化——MySQL要扫描并丢弃前offset行。这个场景的优化方案是延迟关联或游标分页:
-- 优化前(深分页时越来越慢) SELECT * FROM parking_order ORDER BY id DESC LIMIT 100000, 20; -- 优化后:先查ID,再回表取数据 SELECT o.* FROM parking_order o INNER JOIN (SELECT id FROM parking_order ORDER BY id DESC LIMIT 100000, 20) t ON o.id = t.id;这个优化的核心逻辑是:子查询只查id列,单列查询能走覆盖索引,不需要回表,扫描速度比查询整行快得多。拿到20个id后再和主表关联取全部字段,整个过程只需要读取20行完整数据。
5.2 慢SQL监控与执行计划分析
MyBatis的日志只能看到SQL和执行时间,但定位慢查询还得靠MySQL原生的慢查询日志:
-- 查看慢查询日志状态 SHOW VARIABLES LIKE 'slow_query_log'; -- 打开慢查询日志(临时生效) SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';long_query_time = 1表示执行时间超过1秒的SQL都会记录到slow.log。配合MyBatis日志里的SQL原文,可以直接把慢SQL拿到Navicat里执行EXPLAIN:
EXPLAIN SELECT * FROM parking_order WHERE plate_number = '京A88888' AND status = 1;关注type列的值——如果是ALL说明全表扫描,需要加索引;如果是ref或index则说明索引命中正常。额外的rows列是估算扫描行数,如果rows大于10000但实际只返回一行,说明索引选择性和过滤条件需要调整。
key_len可以用来判断联合索引的实际使用长度。SPA是一个高频词项。
5.3 连接池参数与数据库交互调优
Druid连接池的参数设定直接影响SSM框架在高并发下的表现。我一般把初始连接数设为5,最大活跃连接数设为20,这两个数字根据业务并发量调整:
spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 60000 test-while-idle: true validation-query: SELECT 1test-while-idle会在空闲连接回收时检测连接是否有效,validation-query用来发送一个轻量SQL确认连接没有断线。这两个参数虽然小,但在MySQL空闲8小时后自动断开连接的场景下非常关键——Spring容器里的连接池如果不做连接有效性检查,重启数据库后第一次请求会因为拿到断开的连接而报错。
5.4 用JConsole初始化一个热点接口的思路
做完数据库层优化,还可以用jvisualvm或JConsole,打开Tomcat进程后连接上去,先启动压力测试(比如用JMeter并发跑入场接口),然后观察诊断Tab下的CPU线程图。
如果看到大量线程阻塞在SocketInputStream上,说明瓶颈在数据库等待;如果阻塞在业务代码上,可能存在锁竞争或死循环。这个定位思路能帮你在低代码阶段就快速锁定问题所在。
数据驱动的好处在于,你可以精准地回答领导或面试官的质疑:“系统能撑多少并发?”然后报出一个基于实际压测的数字,而不是说“应该没问题”。优化到这一步后,SSM框架的这套智能停车场系统基本达到了生产可用水平——从框架整合、业务实现、并发控制到性能优化,全链路都是可以追溯的。
本文还有配套的精品资源,点击获取