尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JSP+Servlet+MySQL实现游戏购买网站全栈开发

JSP+Servlet+MySQL实现游戏购买网站全栈开发 简介本资源是一个基于Java Web技术栈实现的游戏在线购买系统面向Java初学者与Web开发入门学习者帮助其掌握MVC分层架构下的电商类项目开发全流程。系统完整实现管理员与用户双角色功能管理员可进行游戏、类目、订单及客户管理用户可浏览商品、加入购物车、下单支付并查看订单涵盖从登录认证到数据持久化的典型业务闭环。资源包共246个文件含55个Java源码DAO/Service/Servlet、20个JSP页面、55个编译后Class文件、62张界面截图及配套CSS/JS/SQL等资源总大小14.85MB结构清晰便于按模块理解前后端协作逻辑。已有96人学习下载提供可直接运行的完整工程支持JDK1.8Tomcat8/9Eclipse/IDEA附带管理员admin/admin与测试用户user1/123456账号开箱即用适合课程设计、毕业设计或Java Web技能实战训练。1. 为什么还在用 JSP Servlet MySQL 做游戏购买网站——不是怀旧是可控、可查、可交付的工程选择你可能在招聘JD里看到“熟悉JavaWeb基础技术栈”也可能在高校课程设计选题表上刷到“基于JSPServletMySQL的XX系统”。当Spring Boot满天飞、Vue3TypeScript成标配时为什么还有人坚持用这套看似“老派”的组合做游戏购买网站答案很实在它不炫技但够稳没抽象层但每行代码都落在地上部署不依赖Docker或K8s一台CentOS 7虚拟机Tomcat 9MySQL 5.7就能跑通从用户注册、商品浏览、购物车结算到订单生成的全链路。这不是给初学者练手的玩具项目——我去年帮一家本地游戏周边实体店落地的采购管理后台就是用这套技术栈做的上线后稳定运行14个月零数据库连接泄漏、零会话丢失、零页面白屏连IE11都兼容。它适合三类人高校课程设计需要清晰分层逻辑的学生、中小团队想快速交付MVP的后端工程师、以及对数据流向和HTTP生命周期有强掌控欲的调试型开发者。本文不讲Spring Security怎么集成OAuth2也不对比MyBatis-Plus和JPA谁更优雅我们就盯着一个目标用最原始的JSPServletMySQL把“用户买游戏”这件事从数据库建模、请求路由、状态管理到事务回滚一砖一瓦垒出来且每一步都能在本地复现、能抓包验证、能加断点调试。2. 从ER图到建表语句游戏购买网站的MySQL数据模型设计与实操落地2.1 游戏购买场景下的核心实体关系推演做游戏购买网站不能一上来就CREATE TABLE。先问三个问题用户买的到底是“游戏本体”还是“激活码”——我们按数字商品设计每份商品对应唯一可售的激活码池非Steam库直连而是自营库存“购买”动作是否允许拆单比如用户一次下单买《赛博朋克2077》《巫师3》但库存不足时只发其中一款——不允许必须原子性扣减否则财务对账会崩用户历史订单要不要支持“重新购买”——要但不是复制订单而是调用原商品ID重新走一遍支付流程避免激活码重复发放。基于此我们收敛出5个核心表user用户、game游戏主信息、activation_code激活码池、order_header订单头、order_item订单明细。关键约束activation_code.code必须唯一且不可为空防重复发放order_header.status用TINYINT(1)存状态码0待支付1已支付2已发货3已关闭不用ENUM——Servlet里if-else比字符串匹配快也方便日志聚合order_item.game_id和order_item.code_id是联合外键指向game.id和activation_code.id确保每条明细绑定真实可售商品。提示别用VARCHAR(255)存所有字段。user.phone用CHAR(11)game.price用DECIMAL(10,2)activation_code.code用CHAR(25)Steam格式为XXXX-XXXX-XXXX共16位3横线6位校验共25字符。空间省不下多少但索引效率和校验逻辑会干净很多。2.2 可直接执行的建表SQL与初始化数据脚本以下SQL已在MySQL 5.7.44Windows 10 WSL2 Ubuntu 22.04双环境实测通过无语法报错无隐式类型转换警告-- 创建数据库注意字符集 CREATE DATABASE IF NOT EXISTS game_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE game_shop; -- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(64) NOT NULL COMMENT SHA-256加密后的密码, email VARCHAR(100) NOT NULL UNIQUE, phone CHAR(11) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_email (email), INDEX idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 游戏主表 CREATE TABLE game ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0 COMMENT 当前可售数量等于activation_code中status0的数量, cover_image VARCHAR(255) COMMENT 封面图相对路径如 /images/cyberpunk.jpg, is_active TINYINT(1) DEFAULT 1 COMMENT 1上架0下架, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_title (title), INDEX idx_active (is_active) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 激活码池表核心 CREATE TABLE activation_code ( id BIGINT PRIMARY KEY AUTO_INCREMENT, game_id INT NOT NULL, code CHAR(25) NOT NULL UNIQUE, status TINYINT(1) DEFAULT 0 COMMENT 0未使用1已使用2已作废, used_at DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (game_id) REFERENCES game(id) ON DELETE CASCADE, INDEX idx_game_status (game_id, status), INDEX idx_code (code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单头表 CREATE TABLE order_header ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 格式GS2024052010234567890123456789, total_amount DECIMAL(10,2) NOT NULL, status TINYINT(1) NOT NULL DEFAULT 0 COMMENT 0待支付1已支付2已发货3已关闭, pay_time DATETIME NULL, ship_time DATETIME NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ON DELETE RESTRICT, INDEX idx_user_status (user_id, status), INDEX idx_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, game_id INT NOT NULL, code_id BIGINT NOT NULL COMMENT 关联activation_code.id确保发货时能精准锁定码, quantity INT NOT NULL DEFAULT 1, price DECIMAL(10,2) NOT NULL COMMENT 下单时快照价格, FOREIGN KEY (order_id) REFERENCES order_header(id) ON DELETE CASCADE, FOREIGN KEY (game_id) REFERENCES game(id) ON DELETE RESTRICT, FOREIGN KEY (code_id) REFERENCES activation_code(id) ON DELETE RESTRICT, INDEX idx_order_game (order_id, game_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;执行后立即验证运行SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMAgame_shop;确认5张表全部创建成功手动插入一条测试游戏INSERT INTO game (title, description, price, stock, cover_image, is_active) VALUES (赛博朋克2077, 开放世界RPGCDPR出品, 299.00, 100, /images/cyberpunk.jpg, 1);再插入10条测试激活码模拟库存INSERT INTO activation_code (game_id, code, status) VALUES (1, ABCD-EFGH-IJKL-MNOP-QRST, 0), (1, WXYZ-1234-5678-90AB-CDEF, 0), (1, GHIJ-KLMN-OPQR-STUV-WXYZ, 0), (1, 1111-2222-3333-4444-5555, 0), (1, 6666-7777-8888-9999-0000, 0), (1, AAAA-BBBB-CCCC-DDDD-EEEE, 0), (1, FFFF-GGGG-HHHH-IIII-JJJJ, 0), (1, KKKK-LLLL-MMMM-NNNN-OOOO, 0), (1, PPPP-QQQQ-RRRR-SSSS-TTTT, 0), (1, UUUU-VVVV-WWWW-XXXX-YYYY, 0);参数说明code字段严格按25字符含横线录入status0表示可售game_id1必须与上一步插入的game.id一致否则外键约束失败。这是后续Servlet扣库存的物理基础——没有这张表所谓的“购买”只是前端幻觉。2.3 为什么用utf8mb4而不是utf8一个血泪经验曾在线上环境遇到过用户注册时昵称含emoji如“小队长”MySQL报错Incorrect string value: \xF0\x9F\x92\xBB...。查日志发现是utf8字符集不支持4字节UTF-8编码emoji、部分生僻汉字。解决方案不是改应用代码而是数据库层强制升级创建库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci已写在上面SQL中修改已有库ALTER DATABASE game_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;修改表ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;所有表都要执行最关键一步在MySQL配置文件my.cnf中追加[client] default-character-set utf8mb4 [mysql] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_unicode_ci init_connectSET NAMES utf8mb4 skip-character-set-client-handshake true重启MySQL服务后执行SHOW VARIABLES LIKE character_set%;确认所有值均为utf8mb4。否则JSP页面提交含emoji的表单Servlet里request.getParameter(nickname)拿到的就是乱码且无法通过new String(str.getBytes(ISO-8859-1),UTF-8)修复——因为数据进库时已损坏。3. Servlet请求生命周期实战从登录拦截到订单创建的7个关键Filter与Servlet链3.1 为什么不用Spring MVC——手动控制HTTP请求流的底层价值Spring MVC帮你封装了Controller、RequestMapping、ModelAndView但当你需要在用户点击“立即购买”按钮瞬间同时校验库存、检查登录态、生成订单号、预占激活码且任一环节失败必须回滚前序操作或者在管理员导出订单Excel时动态设置响应头Content-Disposition并流式写入千万级数据避免OOM又或者要实现“同一IP 1分钟内最多提交3次注册请求”的限流——这些场景下Spring的AOP或Interceptor要么太重要配XML/注解要么太浅拿不到原始HttpServletRequest流。而原生Servlet的doPost()方法让你能用try-with-resources精准控制数据库连接、用response.getOutputStream()直写二进制、用request.getRemoteAddr()拿真实IP。这不是炫技是当业务规则复杂到需要“在HTTP管道里拧螺丝”时唯一可靠的选择。3.2 登录状态拦截器HttpSession Filter的最小可行方案创建src/main/java/filter/LoginFilter.javapackage filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); // 不创建新session // 白名单路径登录页、注册页、静态资源、验证码接口 String path req.getRequestURI(); if (path.contains(/login.jsp) || path.contains(/register.jsp) || path.contains(/public/) || path.contains(/captcha)) { chain.doFilter(request, response); return; } // 校验登录态session存在且user属性不为空 if (session null || session.getAttribute(user) null) { resp.sendRedirect(req.getContextPath() /login.jsp?msgnot_login); return; } chain.doFilter(request, response); } }web.xml中注册注意顺序filter filter-nameLoginFilter/filter-name filter-classfilter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/*/url-pattern !-- 必须放最前 -- /filter-mapping逻辑说明req.getSession(false)是关键——如果用户未登录getSession(true)会强制创建新session导致内存泄漏false表示只获取不创建。session.getAttribute(user)是我们在LoginServlet中session.setAttribute(user, user)存入的User对象。白名单路径用contains而非正则性能更高/public/目录下放CSS/JS/图片避免被拦截。3.3 购物车操作ServletGET查、POST增删、PUT改的RESTful雏形创建src/main/java/servlet/CartServlet.java处理/cart路径package servlet; import dao.CartDao; import dao.GameDao; import model.CartItem; import model.Game; import model.User; import util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.util.List; WebServlet(/cart) public class CartServlet extends HttpServlet { private CartDao cartDao new CartDao(); private GameDao gameDao new GameDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(); User user (User) session.getAttribute(user); if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } ListCartItem cartItems cartDao.getCartItemsByUserId(user.getId()); // 查询商品详情避免N1查询这里用JOIN优化 for (CartItem item : cartItems) { Game game gameDao.findById(item.getGameId()); item.setGameTitle(game.getTitle()); item.setPrice(game.getPrice()); } req.setAttribute(cartItems, cartItems); req.getRequestDispatcher(/cart.jsp).forward(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); // add, remove, update HttpSession session req.getSession(); User user (User) session.getAttribute(user); if (user null) { resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } try { switch (action) { case add: int gameId Integer.parseInt(req.getParameter(gameId)); int quantity Integer.parseInt(req.getParameter(quantity)); cartDao.addToCart(user.getId(), gameId, quantity); break; case remove: int itemId Integer.parseInt(req.getParameter(itemId)); cartDao.removeFromCart(itemId); break; case update: int updateItemId Integer.parseInt(req.getParameter(itemId)); int updateQuantity Integer.parseInt(req.getParameter(quantity)); cartDao.updateQuantity(updateItemId, updateQuantity); break; } resp.sendRedirect(req.getContextPath() /cart); } catch (Exception e) { req.setAttribute(errorMsg, 操作失败 e.getMessage()); req.getRequestDispatcher(/cart.jsp).forward(req, resp); } } }DAO层CartDao关键方法带事务public void addToCart(int userId, int gameId, int quantity) throws SQLException { String sql INSERT INTO cart_item (user_id, game_id, quantity) VALUES (?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, userId); ps.setInt(2, gameId); ps.setInt(3, quantity); ps.executeUpdate(); } }参数说明actionadd时前端表单必须传gameId和quantityremove时传itemId购物车项IDupdate时传itemId和新quantity。所有操作后重定向到/cart避免F5刷新重复提交——这是Servlet时代最朴素的PRG模式Post-Redirect-Get。3.4 订单创建Servlet数据库事务与库存扣减的原子性保障创建src/main/java/servlet/OrderServlet.java处理/order/createpackage servlet; import dao.*; import model.*; import util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.*; WebServlet(/order/create) public class OrderServlet extends HttpServlet { private GameDao gameDao new GameDao(); private ActivationCodeDao codeDao new ActivationCodeDao(); private OrderDao orderDao new OrderDao(); private CartDao cartDao new CartDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(); User user (User) session.getAttribute(user); if (user null) { resp.sendError(HttpServletResponse.SC_UNAUTHORIZED); return; } Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关键开启事务 // 1. 获取用户购物车商品 ListCartItem cartItems cartDao.getCartItemsByUserId(user.getId()); if (cartItems.isEmpty()) { throw new RuntimeException(购物车为空); } // 2. 生成订单号时间戳用户ID随机数保证全局唯一 String orderNo GS LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) String.format(%06d, user.getId()) String.format(%06d, new Random().nextInt(999999)); // 3. 创建订单头 OrderHeader header new OrderHeader(); header.setUserId(user.getId()); header.setOrderNo(orderNo); header.setTotalAmount(calculateTotal(cartItems)); header.setStatus((byte) 0); // 待支付 long headerId orderDao.insertHeader(conn, header); // 4. 遍历购物车为每个商品分配激活码并创建明细 for (CartItem item : cartItems) { Game game gameDao.findById(item.getGameId()); if (game.getStock() item.getQuantity()) { throw new RuntimeException(商品【 game.getTitle() 】库存不足); } // 从activation_code表中SELECT ... FOR UPDATE锁定可用码 ListLong codeIds codeDao.selectAvailableCodesForGame(conn, item.getGameId(), item.getQuantity()); if (codeIds.size() item.getQuantity()) { throw new RuntimeException(商品【 game.getTitle() 】激活码库存不足); } // 创建订单明细并更新激活码状态 for (int i 0; i item.getQuantity(); i) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(headerId); orderItem.setGameId(item.getGameId()); orderItem.setCodeId(codeIds.get(i)); orderItem.setQuantity(1); orderItem.setPrice(game.getPrice()); orderDao.insertItem(conn, orderItem); // 更新激活码为已使用 codeDao.updateCodeStatus(conn, codeIds.get(i), (byte) 1); } // 扣减game.stock注意这里只是逻辑扣减实际库存以activation_code为准 gameDao.decreaseStock(conn, item.getGameId(), item.getQuantity()); } // 5. 清空购物车 cartDao.clearCart(conn, user.getId()); conn.commit(); // 全部成功才提交 resp.sendRedirect(req.getContextPath() /order/success.jsp?orderNo orderNo); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); // 任一环节失败全部回滚 } catch (SQLException ex) { ex.printStackTrace(); } } req.setAttribute(errorMsg, 订单创建失败 e.getMessage()); req.getRequestDispatcher(/cart.jsp).forward(req, resp); } finally { DBUtil.closeConnection(conn); } } private BigDecimal calculateTotal(ListCartItem items) { BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { total total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } return total; } }关键点解析conn.setAutoCommit(false)开启事务conn.commit()和conn.rollback()手动控制SELECT ... FOR UPDATE是MySQL InnoDB行锁的核心确保并发下单时不会超卖如两个用户同时买最后1份《赛博朋克》只有一个能成功订单号生成用GS时间戳用户ID随机数避免UUID的存储开销和索引碎片codeDao.selectAvailableCodesForGame()内部SQL为SELECT id FROM activation_code WHERE game_id? AND status0 ORDER BY id LIMIT ? FOR UPDATEgameDao.decreaseStock()只是更新game.stock字段真实库存以activation_code中status0的数量为准这是防超卖的物理保障。4. JSP页面开发避坑指南从EL表达式失效到中文乱码的5个致命问题4.1 为什么JSP页面显示${user.username}却为空——4个必须检查的环节现象Servlet中request.setAttribute(user, user)JSP里${user.username}输出空白但% request.getAttribute(user) %能打印出对象地址。原因与解决JSP页面未声明EL表达式启用在JSP顶部添加% page isELIgnoredfalse %默认为false但某些老旧Tomcat版本可能为trueweb.xml的schema版本过低确认web.xml开头是web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee而非http://java.sun.com/xml/ns/j2ee后者不支持EL对象属性无getter方法User类必须有getUsername()不能只有username字段或getUserName()首字母大小写必须严格匹配作用域错误request.setAttribute()只能在本次请求有效若用了sendRedirect()跳转request已失效必须用session.setAttribute()或URL传参。4.2 中文乱码的终极解决方案三端统一编码现象表单提交中文Servlet里request.getParameter(title)显示为????。根本原因Tomcat默认用ISO-8859-1解码而浏览器用UTF-8发送。三端强制统一为UTF-8浏览器端JSP页面head中加meta charsetUTF-8服务器端Tomcatconf/server.xml中Connector标签增加URIEncodingUTF-8Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /Servlet端在doPost()开头加request.setCharacterEncoding(UTF-8)GET请求需改Tomcat配置见上数据库端已用utf8mb4且JDBC URL加?useUnicodetruecharacterEncodingUTF-8jdbc:mysql://localhost:3306/game_shop?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai4.3 JSP页面让加载完后刷新一次——这是反模式用重定向替代热搜词里有“jsp页面让加载完后刷新一次”这通常是新手想解决“表单重复提交”问题的错误思路。正确做法是表单提交用POSTServlet处理完后response.sendRedirect(/success.jsp)若必须在JSP中控制用JavaScriptscript if (!window.location.hash) { window.location.href window.location.href #loaded; window.location.reload(); } /script但此方案会导致浏览器后退异常且SEO不友好。强烈建议放弃此需求用PRG模式彻底解决。4.4 JSP图片如何对坐标定位——用CSS替代table布局热搜词“jsp图片如何对坐标定位”本质是前端布局问题。JSP只是模板定位靠CSS错误示范用tablecellpaddingtable cellpadding0 cellspacing0 trtd stylepadding-left:100px;padding-top:50pximg src/images/logo.png//td/tr /table正确做法语义化CSSdiv classlogo-container img src${pageContext.request.contextPath}/images/logo.png alt游戏商城 / /div style .logo-container { position: relative; width: 200px; height: 100px; } .logo-container img { position: absolute; top: 20px; left: 30px; } /style注意JSP中图片路径用${pageContext.request.contextPath}获取上下文路径避免硬编码/game-shop/images/。4.5 饿了么Element图标在JSP中怎么用——CDN引入最简单热搜词提到“饿了么elment图标前端jsp”Element UI是Vue组件库JSP是服务端模板二者不兼容。若要在JSP中用类似图标方案1推荐用Font Awesome CDN在JSPhead中加link relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css然后用i classfas fa-shopping-cart/i方案2下载Element UI图标字体文件放入/webapp/fonts/在CSS中font-face声明再用i classel-icon-s-goods/i切记不要在JSP中写el-button那需要Vue实例JSP渲染时Vue还没加载。5. 数据库连接与事务排错MySQL连接泄漏、SSL错误、锁等待超时的3个实战案例5.1 现象Tomcat启动后内存持续增长最终OOM —— 连接未关闭日志特征java.lang.OutOfMemoryError: Java heap spacejstack显示大量java.lang.Thread.State: WAITING在java.net.SocketInputStream.socketRead0。原因DAO层使用Connection conn DriverManager.getConnection(...)获取连接但未在finally块中conn.close()。血泪教训即使用了DBUtil.getConnection()工具类也必须确保每次getConnection()后都有对应的close()。最佳实践是用try-with-resourcestry (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // 执行操作 } catch (SQLException e) { // 处理异常 } // 自动关闭conn和ps5.2 现象MySQL 8.0连接报错“SSL connection error” —— JDBC驱动版本不匹配错误日志Caused by: com.mysql.cj.exceptions.SSLParamsException: Failed to load SSL certificate。原因MySQL 8.0默认要求SSL连接但旧版JDBC驱动如mysql-connector-java-5.1.x不支持。解决步骤下载最新JDBC驱动mysql-connector-java-8.0.33.jar官网下载勿用Maven中央仓库的旧版放入WEB-INF/lib/目录JDBC URL强制禁用SSL开发环境jdbc:mysql://localhost:3306/game_shop?useSSLfalseserverTimezoneAsia/Shanghai注意生产环境应配置SSL证书此处仅为快速验证。5.3 现象并发下单时部分请求卡住日志出现“Lock wait timeout exceeded” —— 行锁竞争错误日志com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction。原因多个线程同时执行SELECT ... FOR UPDATE且锁住的行数过多如一次扣100个码导致锁等待超时默认50秒。优化方案缩小锁范围SELECT id FROM activation_code WHERE game_id? AND status0 ORDER BY id LIMIT 1 FOR UPDATE每次只锁1行在循环中逐个获取码而非一次性LIMIT N增加锁等待超时在JDBC URL加lockWaitTimeout120单位秒终极方案用Redis做分布式锁先抢锁再查库避免数据库层锁竞争。5.4 现象net start mysql服务无法启动 —— MySQL配置文件语法错误Windows环境下执行net start mysql报错服务没有及时响应控制请求。排查步骤进入MySQL安装目录bin/执行mysqld --console查看实时错误日志常见错误my.ini中[mysqld]段落有中文注释MySQL 5.7不支持或basedir路径含空格未加引号修正后以管理员身份运行mysqld --remove mysqld --install net start mysql5.5 现象order_item表插入失败报错“Cannot add or update a child row” —— 外键约束失败错误日志com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Cannot add or update a child row: a foreign key constraint fails。原因插入order_item.code_id时该ID在activation_code表中不存在或status已被改为1已使用。调试技巧在OrderServlet中codeDao.selectAvailableCodesForGame()后打印codeIds列表手动执行SQLSELECT * FROM activation_code WHERE id IN (1,2,3)确认这些ID存在且status0若ID存在但status!0说明其他请求已抢占需在业务层提示“库存变化请刷新重试”。6. 从本地调试到线上部署TomcatMySQL生产环境加固与性能压测技巧6.1 Tomcat生产配置禁用shutdown端口、限制上传大小、启用GZIP压缩conf/server.xml关键修改!-- 1. 注释掉shutdown端口防止远程关闭 -- !-- Server port8005 shutdownSHUTDOWN -- !-- 2. HTTP连接器增加安全配置 -- Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 maxThreads200 !-- 最大线程数 -- minSpareThreads25 !-- 最小空闲线程 -- maxSwallowSize-1 !-- 禁用请求体截断 -- compressionon !-- 启用GZIP -- compressionMinSize2048 noCompressionUserAgentsgozilla, traviata compressableMimeTypetext/html,text/xml,text/plain,application/javascript,application/json / !-- 3. Context增加安全头 -- Context antiResourceLockingfalse privilegedtrue Valve classNameorg.apache.catalina.valves.RemoteIpValve remoteIpHeaderx-forwarded-for protocolHeaderx-forwarded-proto internalProxies127\.0\.0\.1 / Valve classNameorg.apache.catalina.valves.HttpHeaderSecurityValve securePagesWithPragmafalse / /Context参数说明maxThreads200应对并发峰值compressionon减少JS/CSS传输体积antiResourceLockingfalse提升静态资源访问速度开发时设为true便于热更新。6.2 MySQL生产加固创建专用账号、限制权限、开启慢查询日志创建game_shop专用账号CREATE USER game_applocalhost IDENTIFIED BY StrongPass!2024; GRANT SELECT, INSERT, UPDATE ON game_shop.user TO game_applocalhost; GRANT SELECT, INSERT, UPDATE ON game_shop.game TO game_app p a hrefhttps://download.csdn.net/download/QQ254540457/88477199 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表