news 2026/9/20 7:10:53

JSP+SQL Server抽奖系统实战:事务控制与连接池优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+SQL Server抽奖系统实战:事务控制与连接池优化

简介:本资源是一份完整的本科毕业设计论文,面向计算机专业学生及Web开发初学者,聚焦JSP技术在企业级抽奖场景中的工程化落地,解决客户关系管理与营销活动数字化中的抽奖功能模块设计难题。论文涵盖B/S架构设计、SQL Server数据库建模、用户权限分离(普通用户/管理员)、个人中心、抽奖核心逻辑、留言交互等完整模块,并深入分析JSP动态页面开发、会话管理、安全性机制(如SQL注入防护)及系统可扩展性设计。资源为单文件docx格式,共1个文件,大小949KB,内容结构规范,含任务书、摘要、系统需求分析、详细设计、关键技术实现、参考文献等标准章节,附有真实开发环境配置说明与典型界面描述。目前已有104人学习下载,适合用于课程设计参考、毕设开题借鉴、JSP项目实战复盘及B/S系统安全实践学习。

1. 这不是炫技的Demo,而是一个能跑在真实教学机房里的JSP抽奖系统

很多同学拿到“基于JSP的抽奖系统”这个毕设题目时,第一反应是:不就是点个按钮弹个中奖号码?但真正部署到学院机房服务器上、面对30人同时刷新、数据库连接池撑不住、页面跳转后奖品状态错乱——这些才是毕业答辩前一周凌晨三点的真实战场。本系统不是用Servlet硬编码+前端Math.random()糊弄出来的“伪抽奖”,而是以B/S架构为约束、SQL Server为数据底座、MyEclipse为开发环境的完整闭环:从用户登录态校验、抽奖资格实时判定、中奖记录原子写入,到后台管理界面动态配置奖品池与中奖概率,全部走标准JDBC事务流程。适合计算机专业大四学生复现,也适合作为Web开发入门项目理解JSP生命周期与MVC分层边界——尤其当你发现Tomcat日志里反复出现SQLException: Connection reset时,你会明白为什么必须配maxActive=20而不是默认的100。


2. 搭建可稳定运行的JSP+SQL Server开发环境:避开MyEclipse常见陷阱

2.1 为什么选SQL Server 2008 R2而非更新版本?

虽然SQL Server 2022功能更全,但高校机房普遍使用Windows Server 2008/2012,其兼容性验证最充分的是SQL Server 2008 R2(注意:不是Express版)。该版本对JDBC驱动支持成熟,且MyEclipse 10.x内置的SQL Server向导能直接识别其master数据库结构。若强行安装2019或2022,在MyEclipse中新建Database Connection时会报错[Microsoft][SQLServer JDBC Driver]This driver is not configured for integrated security——本质是Kerberos认证链断裂,而2008 R2默认使用SQL Server Authentication模式,只需配置sa账户密码即可直连。

提示:安装SQL Server 2008 R2时务必勾选“SQL Server Management Studio”,否则无法执行后续的建库脚本;安装路径避免含中文或空格,例如D:\SQLServer2008R2\D:\Program Files\Microsoft SQL Server\更稳妥。

2.2 MyEclipse中配置SQL Server JDBC驱动的三步实操

MyEclipse 2014或2015版本对SQL Server驱动支持较弱,不能直接通过“Add Library”导入sqljdbc4.jar。必须手动完成以下操作:

2.2.1 下载并放置驱动文件

从微软官网下载sqljdbc_4.0.2206.100_enu.exe(对应Java 7),解压后将sqljdbc4.jar复制到项目WEB-INF/lib/目录下。不要放在MyEclipse安装目录的plugins/子文件夹中——这是初学者最高频错误,会导致编译时找不到类,但运行时报ClassNotFoundException

2.2.2 在web.xml中声明数据库连接参数
<!-- WEB-INF/web.xml --> <context-param> <param-name>dbDriver</param-name> <param-value>com.microsoft.sqlserver.jdbc.SQLServerDriver</param-value> </context-param> <context-param> <param-name>dbUrl</param-name> <param-value>jdbc:sqlserver://localhost:1433;databaseName=lotteryDB;integratedSecurity=false;</param-value> </context-param> <context-param> <param-name>dbUser</param-name> <param-value>sa</param-value> </context-param> <context-param> <param-name>dbPassword</param-name> <param-value>YourStrong@Pass123</param-value> </context-param>

注意integratedSecurity=false必须显式声明,否则驱动尝试Windows身份验证失败;databaseName值需与后续创建的数据库名完全一致(大小写敏感)。

2.2.3 编写DBUtil工具类封装连接池
// src/com/utils/DBUtil.java public class DBUtil { private static DataSource dataSource; static { BasicDataSource ds = new BasicDataSource(); ds.setDriverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); ds.setUrl("jdbc:sqlserver://localhost:1433;databaseName=lotteryDB;"); ds.setUsername("sa"); ds.setPassword("YourStrong@Pass123"); ds.setInitialSize(5); // 初始连接数 ds.setMaxActive(20); // 最大活跃连接数(关键!) ds.setMaxIdle(10); // 最大空闲连接数 ds.setMinIdle(2); // 最小空闲连接数 ds.setMaxWait(5000); // 获取连接最大等待时间(毫秒) ds.setTimeBetweenEvictionRunsMillis(30000); // 空闲连接检测间隔 dataSource = ds; } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }

setMaxActive=20是核心参数:机房服务器内存通常≤4GB,若设为100,30人并发时连接池耗尽,后续请求直接阻塞在getConnection()调用上,表现为页面白屏无响应。该值需根据SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process=1查询结果动态调整。

2.3 创建抽奖专用数据库与表结构

在SQL Server Management Studio中执行以下建库脚本,必须按顺序执行

-- 步骤1:创建数据库 CREATE DATABASE lotteryDB ON PRIMARY ( NAME = 'lotteryDB_data', FILENAME = 'D:\SQLServer2008R2\lotteryDB.mdf', SIZE = 10MB, MAXSIZE = 100MB ) LOG ON ( NAME = 'lotteryDB_log', FILENAME = 'D:\SQLServer2008R2\lotteryDB.ldf', SIZE = 5MB, MAXSIZE = 50MB ); GO -- 步骤2:切换到新库并建表 USE lotteryDB; GO -- 用户表(简化版,仅存学号和姓名) CREATE TABLE users ( id INT IDENTITY(1,1) PRIMARY KEY, student_id VARCHAR(12) NOT NULL UNIQUE, name NVARCHAR(20) NOT NULL, created_time DATETIME DEFAULT GETDATE() ); -- 奖品表(支持权重配置) CREATE TABLE prizes ( id INT IDENTITY(1,1) PRIMARY KEY, prize_name NVARCHAR(50) NOT NULL, prize_level TINYINT NOT NULL CHECK (prize_level IN (1,2,3)), -- 1=一等奖,2=二等奖,3=参与奖 weight INT NOT NULL DEFAULT 1, -- 权重值,用于概率计算 total_count INT NOT NULL DEFAULT 0, remain_count INT NOT NULL DEFAULT 0 ); -- 中奖记录表(关键!必须带事务控制) CREATE TABLE lottery_records ( id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NOT NULL, prize_id INT NOT NULL, draw_time DATETIME DEFAULT GETDATE(), ip_address VARCHAR(15), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (prize_id) REFERENCES prizes(id) ); GO -- 步骤3:插入初始奖品数据(示例) INSERT INTO prizes (prize_name, prize_level, weight, total_count, remain_count) VALUES ('华为Mate60', 1, 1, 5, 5), ('小米手环8', 2, 3, 20, 20), ('定制笔记本', 3, 10, 100, 100);

prizes.weight字段是实现非等概率抽奖的核心:抽奖逻辑将按权重累加生成区间(如一等奖权重1→[0,1),二等奖权重3→[1,4),三等奖权重10→[4,14)),再用RAND()*14生成随机数匹配区间。此设计比ORDER BY NEWID()更可控,且便于后台调整权重而不影响历史数据。


3. 实现真正的“一次一抽”业务逻辑:JSP页面与Servlet协同机制

3.1 抽奖页面(index.jsp)的关键防护点

单纯用<form action="DrawServlet" method="post">提交存在严重风险:用户点击后F5刷新会导致重复抽奖。必须采用Post-Redirect-Get(PRG)模式,并在JSP中嵌入防重提交令牌:

<!-- index.jsp --> <%@ page import="java.util.UUID" %> <% String token = UUID.randomUUID().toString(); session.setAttribute("draw_token", token); %> <form id="drawForm" action="DrawServlet" method="post"> <input type="hidden" name="token" value="<%=token%>" /> <button type="button" onclick="submitOnce()">立即抽奖</button> </form> <script> function submitOnce() { const btn = document.querySelector('button'); btn.disabled = true; btn.textContent = '抽奖中...'; document.getElementById('drawForm').submit(); } </script>

session.setAttribute("draw_token", token)确保每次页面加载生成唯一令牌;<input type="hidden">将其传给Servlet;JavaScript禁用按钮防止客户端重复点击——这三重防护缺一不可。

3.2 DrawServlet中的事务控制与原子操作

// src/com/servlet/DrawServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String clientToken = request.getParameter("token"); String sessionToken = (String) request.getSession().getAttribute("draw_token"); // 步骤1:校验令牌有效性(防CSRF+重复提交) if (clientToken == null || !clientToken.equals(sessionToken)) { request.setAttribute("error", "非法请求,请重新进入抽奖页面"); request.getRequestDispatcher("error.jsp").forward(request, response); return; } Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关键:开启事务 // 步骤2:检查用户是否已抽过奖(业务规则) String checkSql = "SELECT COUNT(*) FROM lottery_records WHERE user_id = ?"; ps = conn.prepareStatement(checkSql); ps.setInt(1, getCurrentUserId(request)); // 从session获取当前用户ID rs = ps.executeQuery(); if (rs.next() && rs.getInt(1) > 0) { throw new RuntimeException("您已参与过抽奖,不可重复抽取"); } // 步骤3:按权重抽取奖品(核心算法) int totalWeight = 0; List<Prize> prizes = new ArrayList<>(); String weightSql = "SELECT id, prize_name, weight, remain_count FROM prizes WHERE remain_count > 0"; ps = conn.prepareStatement(weightSql); rs = ps.executeQuery(); while (rs.next()) { Prize p = new Prize(); p.setId(rs.getInt("id")); p.setWeight(rs.getInt("weight")); p.setRemainCount(rs.getInt("remain_count")); prizes.add(p); totalWeight += rs.getInt("weight"); } if (prizes.isEmpty()) { throw new RuntimeException("所有奖品已抽完"); } Random rand = new Random(); int randomValue = rand.nextInt(totalWeight); int currentSum = 0; Prize selectedPrize = null; for (Prize p : prizes) { currentSum += p.getWeight(); if (randomValue < currentSum) { selectedPrize = p; break; } } // 步骤4:原子更新奖品剩余数量 + 插入中奖记录 String updateSql = "UPDATE prizes SET remain_count = remain_count - 1 WHERE id = ?"; ps = conn.prepareStatement(updateSql); ps.setInt(1, selectedPrize.getId()); ps.executeUpdate(); String insertSql = "INSERT INTO lottery_records (user_id, prize_id, ip_address) VALUES (?, ?, ?)"; ps = conn.prepareStatement(insertSql); ps.setInt(1, getCurrentUserId(request)); ps.setInt(2, selectedPrize.getId()); ps.setString(3, request.getRemoteAddr()); ps.executeUpdate(); conn.commit(); // 提交事务 request.setAttribute("prize", selectedPrize); request.getRequestDispatcher("result.jsp").forward(request, response); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } request.setAttribute("error", e.getMessage()); request.getRequestDispatcher("error.jsp").forward(request, response); } finally { // 关闭资源(省略具体close代码,实际需在finally块中) } }

conn.setAutoCommit(false)开启事务是底线要求;throw new RuntimeException(...)触发回滚;getCurrentUserId(request)需从session中安全提取用户标识(如session.getAttribute("student_id")),绝不能从URL参数或表单字段读取——这是SQL注入高危点。

3.3 后台管理页面(admin.jsp)的权限隔离实现

管理员页面不能仅靠<% if(session.getAttribute("role").equals("admin")){ %>简单判断,必须配合Filter拦截:

// src/com/filter/AdminFilter.java public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; Object role = request.getSession().getAttribute("role"); if (role == null || !"admin".equals(role.toString())) { response.sendRedirect(request.getContextPath() + "/login.jsp?msg=无权访问管理员页面"); return; } chain.doFilter(req, resp); }

并在web.xml中注册:

<filter> <filter-name>AdminFilter</filter-name> <filter-class>com.filter.AdminFilter</filter-class> </filter> <filter-mapping> <filter-name>AdminFilter</filter-name> <url-pattern>/admin/*</url-pattern> </filter-mapping>

/admin/*路径映射确保所有后台请求(包括/admin/updatePrize.jsp)都经过角色校验,避免绕过JSP脚本直接访问URL。


4. 解决MyEclipse调试中最常卡住的三个问题:连接池、编码、页面跳转

4.1 Tomcat启动后JSP编译失败:定位class文件位置

当MyEclipse控制台显示org.apache.jasper.JasperException: Unable to compile class for JSP,不要盲目重启。先确认JSP编译后的Java类存放路径:

  1. 打开MyEclipse菜单Window → Preferences → MyEclipse → Servers → Tomcat → Tomcat n.x → Paths
  2. 查看Working directory字段值(如D:\myeclipse-workspace\.metadata\.plugins\org.eclipse.core.resources\.projects\lotterySystem\org.eclipse.jst.server.tomcat.runtime\work\Catalina\localhost\lotterySystem
  3. 进入该路径下的org\apache\jsp\子目录,找到对应JSP生成的.java文件(如index_jsp.java

若该目录为空,说明JSP未被正确部署:检查项目Properties → MyEclipse → Project Facets → Dynamic Web Module版本是否为2.5(JSP 2.0对应),不能选3.0或3.1——MyEclipse 2014对高版本支持不完善。

4.2 中文乱码的终极解决方案:三层编码统一

JSP页面中文乱码有三个源头,必须同步修正:

层级配置位置必须设置的值作用
JSP页面层index.jsp顶部<%@ page contentType="text/html;charset=UTF-8" %>告诉浏览器用UTF-8解析HTML
HTTP传输层web.xml中添加filter<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>+<init-param><param-name>encoding</param-name><param-value>UTF-8</param-value></init-param>强制request/response编码为UTF-8
数据库连接层JDBC URL追加参数jdbc:sqlserver://localhost:1433;databaseName=lotteryDB;characterEncoding=UTF-8;让SQL Server驱动用UTF-8编码传输数据

注意:SQL Server JDBC驱动的characterEncoding=UTF-8参数在4.0及以上版本才生效,若用旧版驱动需改用useUnicode=true&characterEncoding=utf8(MySQL风格,但SQL Server不支持,故必须升级驱动)。

4.3 页面跳转后Session丢失:检查Cookie路径配置

在机房服务器上,response.sendRedirect("result.jsp")session.getAttribute("prize")返回null,大概率是Cookie路径不匹配。在web.xml中强制指定Session Cookie路径:

<session-config> <cookie-config> <path>/</path> <!-- 关键:设为根路径,否则默认为应用上下文路径 --> <http-only>true</http-only> <secure>false</secure> </cookie-config> </session-config>

<path>/</path>确保Cookie在所有子路径(如/lotterySystem/result.jsp)下均有效;<secure>false</secure>因机房环境通常无HTTPS,设为false避免Cookie被浏览器丢弃。


5. 抽奖结果可视化与数据验证:用SQL Server Profiler抓取真实执行链路

5.1 用SQL Server Profiler验证事务原子性

当怀疑“抽中一等奖后奖品库存没减”时,不能只查prizes.remain_count,要用Profiler捕获真实SQL执行流:

  1. 打开SQL Server Management Studio → 工具 → SQL Server Profiler
  2. 新建跟踪 → 选择模板TSQL_SPs(捕获存储过程)→ 在“事件选择”页勾选:
    • SQL:BatchStarting(SQL批处理开始)
    • SQL:BatchCompleted(SQL批处理完成)
    • RPC:Completed(远程过程调用完成)
    • Transaction:SQLTransaction(事务事件)
  3. 设置筛选器:ApplicationName包含MyEclipseDatabaseName等于lotteryDB
  4. 点击“运行”,在浏览器中执行一次抽奖,观察Profiler中是否出现:
    • BEGIN TRANUPDATE prizes...INSERT lottery_records...COMMIT TRAN
    • 若看到ROLLBACK TRAN或缺失COMMIT,说明事务未正常结束,需检查Servlet中conn.commit()是否被异常跳过

5.2 查询中奖分布验证权重算法准确性

执行以下SQL,对比理论权重比与实际中奖比:

-- 统计各奖项实际中奖次数 SELECT p.prize_name, p.weight, COUNT(r.id) as actual_count, CAST(COUNT(r.id) AS FLOAT) * 100 / (SELECT COUNT(*) FROM lottery_records) as percentage FROM prizes p LEFT JOIN lottery_records r ON p.id = r.prize_id GROUP BY p.id, p.prize_name, p.weight ORDER BY p.prize_level; -- 理论权重占比(供对照) SELECT prize_name, weight, CAST(weight AS FLOAT) * 100 / (SELECT SUM(weight) FROM prizes) as theoretical_percentage FROM prizes;

actual_counttheoretical_percentage偏差超过±15%,说明随机数生成器被干扰(如多线程共享Random实例),应改为ThreadLocal<Random>SecureRandom

5.3 优化高频查询:为lottery_records表添加复合索引

lottery_records表在后台统计时会被频繁JOIN查询,但默认只有主键索引。执行以下命令提升查询速度:

-- 为user_id+draw_time组合创建索引(支持“某用户历史记录”查询) CREATE NONCLUSTERED INDEX IX_lottery_records_user_time ON lottery_records(user_id, draw_time) INCLUDE (prize_id, ip_address); -- 为prize_id创建索引(支持“某奖品被抽多少次”统计) CREATE NONCLUSTERED INDEX IX_lottery_records_prize ON lottery_records(prize_id);

INCLUDE子句将prize_idip_address包含在索引叶节点中,避免回表查询,使SELECT prize_id, ip_address FROM lottery_records WHERE user_id=123直接走索引。

提示:索引不是越多越好。lottery_records表每新增一条记录,上述两个索引各需更新一次。若机房服务器磁盘I/O较慢,建议仅保留IX_lottery_records_user_time,用prize_id字段的统计需求可通过定时任务汇总到prizes.stat_count字段中缓存。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 17:39:41

Python列表批量删除与去重的高效实现方案

1. 从实际需求出发&#xff1a;Python列表批量删除与去重的场景分析在日常数据处理中&#xff0c;我们经常会遇到这样的需求&#xff1a;从一个包含重复元素的列表中&#xff0c;既要删除指定的多个值&#xff0c;又要确保结果列表中的元素唯一。这种"批量删除去重"的…

作者头像 李华
网站建设 2026/9/19 23:58:12

NKR智能气体涡轮流量计:从Modbus接入到温压补偿与K系数修正

简介&#xff1a;NKR系列智能气体涡轮流量计选型/使用说明书是一份面向燃气计量、工业气体流量监测工程师与运维人员的完整技术文档&#xff0c;主要解决NKR系列流量计选型、安装、参数设置与维护问题。文档按GB/T32201-2015标准编制&#xff0c;涵盖技术性能指标、工作原理与结…

作者头像 李华
网站建设 2026/9/20 9:35:10

3条命令跑通OpenResearch:orx up研究智能体仪表盘完整走查

3条命令跑通OpenResearch&#xff1a;orx up研究智能体仪表盘完整走查 【免费下载链接】OpenResearch Turn your coding agents into research agents 项目地址: https://gitcode.com/GitHub_Trending/op/OpenResearch OpenResearch 是一个本地优先的研究智能体工作台&a…

作者头像 李华
网站建设 2026/9/20 13:39:35

数据分类分级实战:从标准解读到策略即代码

简介&#xff1a;本资源是一份面向数据安全从业者、企业合规人员及数字化转型技术负责人的专业培训课件&#xff0c;聚焦《数据安全法》落地背景下的数据分类分级核心能力构建。内容系统解读国家政策要求、国内外标准&#xff08;含NIST SP 800-60与国标GB/T 38667—2020&#…

作者头像 李华