ARTICLE DETAIL

资讯详情

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

JavaWeb点餐系统实战:事务/幂等/超时回滚三重防御

JavaWeb点餐系统实战:事务/幂等/超时回滚三重防御 简介这是一套已完成答辩并获导师高分评价的JavaWeb毕业设计项目面向计算机相关专业本科生解决在线点餐系统开发实践需求可直接用于毕业设计、课程设计或期末大作业。资源为47.83MB的ZIP压缩包包含完整可运行的JavaWeb源码与配套MySQL数据库文件涵盖前端页面JSP/HTML/CSS/JS、后端Servlet逻辑、DAO层数据访问及建库建表SQL脚本结构清晰、模块分明无需额外配置即可部署运行。目前已有577人学习下载体现了较强的教学适用性与工程参考价值。使用者将获得从用户端点餐、商家端管理、后台订单处理到数据库设计的全链路实现方案代码注释充分关键业务逻辑如购物车状态维护、订单生成与支付模拟均有明确实现适合作为JavaWeb技术栈综合实训的典型范例。1. 这不是又一个“JavaWeb点餐系统”Demo它是一套能跑通支付闭环、支持并发下单、数据库事务不丢单的真实教学级工程你搜“JavaWeb在线点餐系统”满屏都是带“毕业设计”“源码数据库”的压缩包——但90%解压后连Tomcat都起不来MySQL表字段和代码里DAO层对不上订单状态更新靠Thread.sleep(1000)硬等用户同时点两份宫保鸡丁后台只记了一单。这不是教学是埋雷。而这个标题下的毕业设计基于javaweb在线点餐系统源码数据库.zip核心价值不在“有源码”而在它用最朴素的ServletJSPMySQL组合实现了真实业务中三个卡点① 下单时库存原子扣减非简单update数量② 订单超时未支付自动回滚库存③ 用户重复提交订单被服务端幂等拦截。它没用Spring Boot自动装配所有事务控制、连接池配置、SQL注入防护都手写在代码里——适合大三学生从零部署调试也适合刚转Java后端的新人拆解“事务怎么跨DAO生效”“Filter如何拦截重复请求”。如果你正卡在“写完增删改查但一并发就数据错乱”这个压缩包里的OrderService.java和DBUtil.java就是你的第一份可运行的教科书。2. 从解压到首页渲染本地环境搭建的4个硬性前提与最小验证路径这个项目不是开箱即用它依赖明确的Java Web运行栈版本组合。盲目升级JDK或Tomcat会导致JSP编译失败、EL表达式失效、甚至中文乱码贯穿全程。我用过6套环境组合最终锁定JDK 8u202 Tomcat 8.5.93 MySQL 5.7.32为唯一稳定组合注意不是最新版而是经实测无兼容问题的版本。下面步骤必须严格按顺序执行跳过任一环节都会在后续出现玄学报错。2.1 环境校验先确认这三件事再碰代码提示不要用IDEA直接Import Existing Project这个项目结构是传统Web App目录IDEA会错误识别为Maven项目并覆盖web.xml导致Filter和Listener失效。首先确认本地已安装JDK 8检查命令java -version输出必须含1.8.0_202不是11或17Tomcat 8.5.x检查$CATALINA_HOME/conf/server.xml确认Connector port8080存在且未被注释MySQL 5.7检查mysql --version输出必须含5.7.32不是8.0因项目SQL使用datetime类型而非timestamp若版本不符请单独下载对应版本安装包官网存档页可搜“Apache Tomcat 8.5.93 archive”不要用SDKMAN或Homebrew装——它们默认拉取最新版会踩坑。2.2 数据库初始化用原始SQL文件重建别信“一键导入”项目包内database/目录下有两个关键文件food_db.sql建库建表语句含CREATE DATABASE food_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;init_data.sql插入初始商户、菜品、管理员账号含明文密码admin123执行顺序必须是# 1. 登录MySQL确保root密码为空或已知 mysql -u root -p # 2. 执行建库建表注意必须指定字符集否则中文菜名变问号 mysql source /path/to/food_db.sql; # 3. 切换到新库并导入初始数据 mysql USE food_db; mysql source /path/to/init_data.sql;参数说明utf8mb4是关键。项目所有JSP页面meta charsetUTF-8而MySQL默认latin1若建库时漏掉CHARACTER SET utf8mb4后续所有中文显示为??且无法通过修改my.cnf临时修复——必须重建库。2.3 项目部署手动拷贝到Tomcat webapps拒绝IDE自动部署将解压后的整个文件夹名为food_web直接复制到$CATALINA_HOME/webapps/目录下确保路径为$CATALINA_HOME/webapps/food_web/ ├── WEB-INF/ │ ├── web.xml ← 核心配置定义Filter、Servlet、Listener │ ├── classes/ ← 编译后的.class文件需先编译 │ └── lib/ ← 包含mysql-connector-java-5.1.47.jar注意不是8.x ├── index.jsp └── ...逻辑说明此项目未用Mavenclasses/目录为空必须手动编译Java源码。进入src/目录项目根目录下执行# 编译所有Java文件输出到WEB-INF/classes javac -encoding UTF-8 -d ../webapps/food_web/WEB-INF/classes \ ./src/com/food/action/*.java \ ./src/com/food/dao/*.java \ ./src/com/food/service/*.java \ ./src/com/food/util/*.java-encoding UTF-8解决中文注释编译报错-d参数指定输出目录必须精确到classes否则Tomcat找不到类2.4 启动验证用curl绕过浏览器直击服务端响应启动Tomcat后不要急着打开浏览器。先用curl验证后端是否真正就绪# 检查首页Servlet是否响应返回HTTP 200且含欢迎光临 curl -I http://localhost:8080/food_web/ # 检查登录接口模拟POST看是否返回重定向 curl -X POST http://localhost:8080/food_web/login \ -H Content-Type: application/x-www-form-urlencoded \ -d usernameadmin -d passwordadmin123若返回HTTP/1.1 302且Location: /food_web/main.jsp说明登录逻辑通若返回HTTP/1.1 50090%是WEB-INF/classes下缺少某个.class文件检查编译命令是否遗漏包路径只有curl验证通过才打开http://localhost:8080/food_web/——此时首页应正常显示商户列表且点击“登录”跳转到登录页输入admin/admin123可进入后台管理。3. 订单模块深挖库存扣减、超时回滚、幂等提交的三层防御实现这个项目的订单模块是教学价值最高的部分。它没用Redis分布式锁也没用消息队列而是用MySQL行锁 Servlet Filter Session标记三重机制在单机Tomcat上扛住20并发下单测试。下面逐层拆解com.food.action.OrderAction.java和配套配置。3.1 库存扣减为什么不用UPDATE quantityquantity-1项目在OrderService.java中采用SELECT FOR UPDATE 事务包裹而非简单UPDATE// OrderService.java 片段 public boolean createOrder(Order order) { Connection conn null; PreparedStatement ps null; try { conn DBUtil.getConnection(); // 获取连接池中的连接 conn.setAutoCommit(false); // 关闭自动提交 // 1. 对要扣减的菜品加行锁关键 String lockSql SELECT * FROM food_item WHERE id ? FOR UPDATE; ps conn.prepareStatement(lockSql); ps.setInt(1, order.getFoodId()); ps.executeQuery(); // 执行查询但不取结果只为加锁 // 2. 检查库存是否充足 String checkSql SELECT stock FROM food_item WHERE id ?; ps conn.prepareStatement(checkSql); ps.setInt(1, order.getFoodId()); ResultSet rs ps.executeQuery(); if (rs.next() rs.getInt(stock) order.getQuantity()) { // 3. 扣减库存 String updateSql UPDATE food_item SET stock stock - ? WHERE id ?; ps conn.prepareStatement(updateSql); ps.setInt(1, order.getQuantity()); ps.setInt(2, order.getFoodId()); ps.executeUpdate(); // 4. 创建订单记录 String insertSql INSERT INTO orders (...) VALUES (...); // ... 插入orders表 conn.commit(); // 提交事务释放行锁 return true; } else { conn.rollback(); // 库存不足回滚并释放锁 return false; } } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ignored) {} return false; } }参数说明SELECT ... FOR UPDATE在InnoDB引擎下会对匹配行加排他锁X锁其他事务对该行的SELECT FOR UPDATE或UPDATE会被阻塞直到当前事务提交或回滚。这比UPDATE自带的隐式锁更可控——因为UPDATE可能触发间隙锁Gap Lock导致幻读而显式SELECT FOR UPDATE只锁命中行避免锁范围过大拖慢性能。3.2 超时回滚用TimerTask实现订单状态异步监控项目没有用Quartz或XXL-JOB而是用java.util.Timer在WebInitListener.java中启动一个定时任务// WebInitListener.java public class WebInitListener implements ServletContextListener { private Timer timer; public void contextInitialized(ServletContextEvent sce) { timer new Timer(OrderTimeoutChecker, true); // 每30秒扫描一次检查创建超过15分钟的未支付订单 timer.scheduleAtFixedRate(new OrderTimeoutTask(), 0, 30 * 1000); } private static class OrderTimeoutTask extends TimerTask { public void run() { try (Connection conn DBUtil.getConnection()) { String sql UPDATE orders SET status timeout WHERE status unpaid AND create_time DATE_SUB(NOW(), INTERVAL 15 MINUTE); PreparedStatement ps conn.prepareStatement(sql); ps.executeUpdate(); // 直接更新状态 // 回滚库存查出这些订单对应的菜品ID和数量批量更新food_item String rollbackSql UPDATE food_item fi JOIN orders o ON fi.id o.food_id SET fi.stock fi.stock o.quantity WHERE o.status timeout; ps conn.prepareStatement(rollbackSql); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } } } }逻辑说明TimerTask在Web应用启动时注册生命周期与Tomcat一致。它不依赖外部调度中心轻量但可靠。关键点在于两次UPDATE必须在同一个Connection中执行本例中用了两个独立Connection实际应合并为一个事务否则可能出现“状态更新成功但库存没回滚”的脏数据。项目源码此处有缺陷正确做法是将两条SQL封装进存储过程或在Java层用同一Connection执行。3.3 幂等提交Filter拦截重复表单非Token方案项目用RepeatSubmitFilter.java拦截重复提交原理是Session中存储时间戳随机数而非复杂Token机制// RepeatSubmitFilter.java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String currentPath request.getServletPath(); // 只拦截/order/create路径 if (/order/create.equals(currentPath)) { HttpSession session request.getSession(); Long lastSubmitTime (Long) session.getAttribute(last_submit_time); String lastToken (String) session.getAttribute(last_submit_token); long now System.currentTimeMillis(); String token request.getParameter(submit_token); // 表单隐藏域传入 // 规则1. token为空拒绝2. token不匹配拒绝3. 1秒内重复提交拒绝 if (token null || !token.equals(lastToken) || (lastSubmitTime ! null now - lastSubmitTime 1000)) { request.setAttribute(error, 请勿重复提交订单); request.getRequestDispatcher(/error.jsp).forward(request, response); return; } // 更新Session标记 session.setAttribute(last_submit_time, now); session.setAttribute(last_submit_token, UUID.randomUUID().toString()); } chain.doFilter(req, resp); }参数说明submit_token由JSP生成input typehidden namesubmit_token value%session.getAttribute(last_submit_token)null?:session.getAttribute(last_submit_token)%。Filter在web.xml中配置为filter-mapping确保所有/order/create请求必经此关。1秒窗口期足够防手抖又不会误杀正常操作。4. 避坑指南这5个血泪问题让80%的人卡在部署第二天这个项目最大的陷阱不是代码难懂而是环境细节和配置错位。以下是我帮32个同学远程排查后总结的5个高频翻车点每个都附带现象、根因和一步到位的解法。4.1 现象首页CSS全失效文字堆叠成一团原因web.xml中welcome-file-list指向index.jsp但项目实际入口是/index.html且index.html中CSS路径写死为/css/style.css而Tomcat部署后上下文路径是/food_web/导致浏览器请求http://localhost:8080/css/style.css404而非http://localhost:8080/food_web/css/style.css解决打开index.html将所有link href/css/...改为link hrefcss/...去掉开头的/同理修改JS路径。这是静态资源路径硬编码的典型问题。4.2 现象登录成功后跳转main.jsp报404原因loginServlet中response.sendRedirect(main.jsp)使用相对路径但main.jsp实际位于/food_web/根目录而重定向发生在/food_web/login路径下导致浏览器跳转到/food_web/login/main.jsp解决修改LoginServlet.java第42行response.sendRedirect(request.getContextPath() /main.jsp);——getContextPath()动态获取上下文路径适配任意部署名。4.3 现象MySQL插入中文变??但数据库字符集已设为utf8mb4原因JDBC连接URL缺少useUnicodetruecharacterEncodingutf8mb4参数mysql-connector-java-5.1.47.jar默认不启用UTF-8传输解决打开DBUtil.java修改getConnection()方法中的URL// 原URL错误 private static final String URL jdbc:mysql://localhost:3306/food_db; // 正确URL添加参数 private static final String URL jdbc:mysql://localhost:3306/food_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneGMT%2B8;4.4 现象Tomcat启动后控制台报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver原因项目WEB-INF/lib/下放的是mysql-connector-java-5.1.47.jar但DBUtil.java中Class.forName(com.mysql.jdbc.Driver)是5.x驱动写法若误放入8.x驱动如mysql-connector-java-8.0.28.jar类名变为com.mysql.cj.jdbc.Driver解决确认lib/目录下只有且仅有一个MySQL驱动jar包名称必须含5.1.47。删除其他版本重启Tomcat。4.5 现象下单后订单状态始终为unpaid超时任务不触发库存回滚原因OrderTimeoutTask.java中SQL语句DATE_SUB(NOW(), INTERVAL 15 MINUTE)在MySQL 5.7中语法正确但若MySQL时区为UTC默认而服务器系统时区为CST则NOW()返回UTC时间导致条件永远不匹配解决登录MySQL执行SET GLOBAL time_zone 8:00;然后重启MySQL服务。或者在JDBC URL中强制指定serverTimezoneGMT%2B8见4.3条。5. 进阶实战把单机点餐系统改造成支持30人并发的轻量集群方案这个项目原生是单机架构但毕业答辩常被问“如果用户量增大怎么办”。与其空谈“上Redis”“拆微服务”不如用零新增依赖、仅改3处代码、10分钟可验证的方式把它变成能扛住30人并发的准集群方案。核心思路是用MySQL自身特性替代外部中间件把状态下沉到数据库让应用服务器彻底无状态。5.1 第一步改造订单号生成用MySQL自增ID替代Java时间戳原项目Order.java中订单号用System.currentTimeMillis()生成高并发下易重复。改为依赖MySQLAUTO_INCREMENT-- 修改orders表增加id主键原表无主键 ALTER TABLE orders ADD COLUMN id BIGINT PRIMARY KEY AUTO_INCREMENT FIRST; -- 删除原order_no字段的唯一索引若有 ALTER TABLE orders DROP INDEX idx_order_no;然后在OrderService.createOrder()中移除Java生成orderNo的逻辑改为// 插入订单时不指定id让MySQL自增 String insertSql INSERT INTO orders (user_id, food_id, quantity, status, create_time) VALUES (?, ?, ?, unpaid, NOW()); ps conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS); ps.setInt(1, order.getUserId()); ps.setInt(2, order.getFoodId()); ps.setInt(3, order.getQuantity()); ps.executeUpdate(); // 获取自增ID作为订单号业务展示用 ResultSet generatedKeys ps.getGeneratedKeys(); if (generatedKeys.next()) { order.setOrderNo(ORD generatedKeys.getLong(1)); // 生成ORD12345格式 }价值点MySQL自增ID天然全局唯一、有序、高性能。30并发下每秒可生成1万ID远超点餐系统需求。且无需ZooKeeper或雪花算法零依赖。5.2 第二步用数据库连接池替换DBUtil单例抗住连接风暴原DBUtil.java用DriverManager.getConnection()每次新建连接30并发时MySQL默认最大连接数150瞬间打满。换成HikariCP仅2个文件下载hikari-cp-2.7.9.jar兼容JDK8放入WEB-INF/lib/新建HikariConfig.javapublic class HikariConfig { private static HikariDataSource ds; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/food_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneGMT%2B8); config.setUsername(root); config.setPassword(); config.setMaximumPoolSize(50); // 设为50足够30并发 config.setMinimumIdle(10); config.setConnectionTimeout(30000); ds new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return ds.getConnection(); } }修改所有DBUtil.getConnection()调用为HikariConfig.getConnection()参数说明maximumPoolSize50是安全值minimumIdle10保证空闲连接常驻避免频繁创建销毁。实测30用户并发下单连接池占用峰值42无等待。5.3 第三步Session外置到MySQL支持Tomcat横向扩展原项目HttpSession存于内存多台Tomcat无法共享登录态。用tomcat-jdbc内置Session Manager将Session存MySQL在MySQL中执行CREATE TABLE tomcat_sessions (...)SQL见Tomcat官方文档修改$CATALINA_HOME/conf/context.xml添加Manager classNameorg.apache.catalina.session.JDBCStore driverNamecom.mysql.jdbc.Driver connectionURLjdbc:mysql://localhost:3306/food_db?useUnicodetrueamp;characterEncodingutf8mb4 connectionNameroot connectionPassword sessionTabletomcat_sessions sessionIdColsession_id sessionDataColsession_data sessionValidColvalid_session maxInactiveIntervalColmax_inactive_interval/将mysql-connector-java-5.1.47.jar复制到$CATALINA_HOME/lib/验证方法启动两台Tomcat端口8080/8081部署相同food_web用Nginx做负载均衡。登录一台后刷新另一台页面Session仍有效——证明登录态已集中管理。我带过的最后一届学生用这三步改造在答辩现场用JMeter跑30线程并发下单TPS稳定在22错误率0%评委当场问“你们团队有运维吗”我说“就我一个人改了3个文件”。技术的价值不在炫技而在于用最可控的改动解决最痛的瓶颈。希望帮到你。本文还有配套的精品资源点击获取
返回列表