简介:这份资源是基于JavaWeb的小型云盘系统完整项目,定位为仿照百度网盘核心功能的教学型毕业设计,适合Java初学者、数据库课程设计及求职者作为练习参考。系统前端采用Bootstrap构建界面,后台基于Servlet实现,包含文件上传、下载、分享等模块,对应提供了数据库SQL文件,可直接导入运行。压缩包共204个文件,容量约4.59MB,主要包含Java源码与编译类文件、JSP页面、JS脚本、CSS样式、数据库脚本及少量图片素材,整体结构清晰,便于按功能模块对照学习。目前已有387人浏览学习。开箱即用的源码与数据库脚本可帮助读者理解文件管理、用户登录、前后端交互等核心流程,也可为扩展云盘功能或撰写毕业设计文档提供基础。
1. 这个小型云盘系统到底解决什么问题:先别急着跑,想清楚再动手
做 javaweb 课程设计或者想自己私有化一个网盘的人,看到“基于 javaweb 的仿照百度网盘的小型云盘系统源码+数据库”应该是高兴的,因为这意味着不用从零开始写。但真正难的不是跑起来,而是搞清楚这套源码的存储结构、文件流和分享逻辑,让它在你的机器上稳定工作。这个系统解决的是文件的上传、下载、目录管理和分享链接生成,适合中小型课设,也适合刚接触 javaweb 文件操作的人作底稿。我接手的项目里,简化版网盘是最容易“看起来能跑、一传大文件就崩”的,这篇文章就顺着源码落地讲清楚。
2. 跑通项目前的一切准备:识别架构、对齐环境、把数据库脚本导入 MySQL
2.1 怎么判断手里的源码是 Servlet + JSP 还是 Spring 家族:看 web.xml 和 pom.xml
拿到这种“源码+数据库”的包,第一步不要急着在 IDEA 里点 Tomcat,先解压看目录。根目录如果有 pom.xml,说明是 Maven 工程;但 Maven 工程不一定是 Spring,可能只是用依赖管理老一套的 Servlet 项目。打开src/main/webapp/WEB-INF/web.xml,看有没有DispatcherServlet或者ContextLoaderListener的配置,有就是 Spring MVC;没有但有一堆<servlet>映射,就是传统 Servlet + JSP。
很多课程设计级别的网盘源码走的是后者,原因是文件上传下载这种场景用 Servlet 的原生 API 反而直观。早年老师喜欢讲三层架构:Servlet 做控制层、Service 做业务、DAO 做数据库操作。识别只是为了后续改代码不迷路,因为后面调文件保存路径、改连接池参数,不同架构改的位置完全不一样。
老项目往往没有 Maven,而是直接把 lib 包扔在 WEB-INF 下。用 IDEA 导入这种工程时,选中 web 目录右键做 Artifact,而不是新建 Module,否则一堆 jar 包不生效。下面的 web.xml 片段是一个典型的老式网盘入口配置:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> <display-name>NetDisk</display-name> <servlet> <servlet-name>FileServlet</servlet-name> <servlet-class>com.netdisk.web.FileServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>FileServlet</servlet-name> <url-pattern>/file/*</url-pattern> </servlet-mapping> </web-app>这段配置说明所有带/file/前缀的请求都交给FileServlet处理。如果源码里用的是@WebServlet("/file/*")注解,那 web.xml 里可能只有欢迎页和 session 超时配置。判断这个有什么用?用途是知道后面改上传下载接口时,应该写注解还是改 XML,以及上服务器时能不能用 Servlet 3.0 的异步支持。
2.2 本地开发环境怎么配:用 IDEA 运行 javaweb 项目的最小配置
这个题材的源码大概率是 JDK 1.8 + Tomcat 8.5 + MySQL 5.7 世代产物,太新反而容易翻车。JDK 11 以上跑老代码,javax.servlet包还要配--add-modules,纯属给自己找事。先对齐三件套:
java -version mvn -v mysql --version版本没问题后再处理 IDEA 中的 Tomcat 配置。运行配置里 Artifact 要选war exploded,而不是war,后者每次改 JSP 都要重新打一次包,调试效率极低。Deployment 的 Application context 建议设成/netdisk,这样本地访问地址是http://localhost:8080/netdisk/,和源码里可能出现的前缀保持一致。
很多网盘源码会把数据库连接写在 JDBC 工具类里,比如:
private static final String URL = "jdbc:mysql://localhost:3306/netdisk?useUnicode=true&characterEncoding=utf8";这种写法现在跑 MySQL 8.0 会报时区错误。要么把 mysql-connector-java 换了,要么在 URL 后面加serverTimezone=Asia/Shanghai。比这更隐蔽的是 Tomcat 连接器编码,老项目里的 JSP 大多是 GBK 写的,而数据库建库又用了 utf8,两边一冲突,页面中文正常,上传文件名乱码。我一般会在 server.xml 里把连接器强制到 UTF-8:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />Tomcat 8.5 默认 URIEncoding 就是 UTF-8,但写上去可以防手滑回退。要注意的是,这只解决 URL 里的中文参数,不解决 multipart 表单里的文件名编码,那个要看 Servlet 里有没有req.setCharacterEncoding("UTF-8")。
2.3 把数据库脚本导入进去:netdisk.sql 脚本的执行顺序和编码坑
数据库是这套源码的重头戏,名字通常直接叫netdisk.sql。导入前先别直接 source,我习惯先建好库再导,因为很多脚本里不带CREATE DATABASE:
mysql -uroot -p -e "CREATE DATABASE netdisk DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" mysql -uroot -p netdisk < netdisk.sql导完确认表是否完整:
mysql -uroot -p -e "USE netdisk; SHOW TABLES;"正常会看到user、folder、file_info、share_code这类核心表。如果没有,说明脚本前半段有报错被你忽略了。用mysql -uroot -p netdisk --force < netdisk.sql再导一次,把错误打全。
编码坑在网盘项目里特别明显:文件名允许带表情符号,比如“课程资料📚.zip”。utf8字符集存不了 emoji,必须utf8mb4。老脚本如果用的是DEFAULT CHARSET=utf8,在你导入后建出的表全是 utf8,上传服务一跑就报Incorrect string value。这时候不要改表的字符集,你只需要在建库时用utf8mb4,并且导入后对核心表执行一次转换:
ALTER TABLE file_info CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;另外,脚本里如果有DROP TABLE IF EXISTS,注意执行顺序。有的源码会先建 file_info 再建 user,原因是 file_info 要引用 user_id,但实际上如果没加外键约束,顺序无所谓。我见过最坑的是一个脚本里把初始管理员密码写成了明文,导完库别人能直接用admin/admin123登录。安全做法是导完库立刻改成 MD5 或 BCrypt 加密后的值,别等部署上线才想起这回事。
数据库连接池的配置也要顺手检查。老源码多用 C3P0 或 DBCP,连接串里写死密码是常事。本地跑无所谓,可一旦打包发给别人,谁都看得到明文数据库密码。落地时我一般会抽一个jdbc.properties,不让密码出现在 Java 类里。
3. 数据库设计拆解:用户、文件、分享链接三张核心表是怎么撑起一套网盘的
3.1 从普通文件表到支持秒传:file_info 表为什么必须有 md5 和 file_size
网盘系统的数据库设计关键点不是“存文件”,而是“存文件元数据”。普通文件表只需要文件名和路径,但要做秒传就必须有file_md5和file_size。秒传的逻辑很简单:用户上传前,前端或后端先计算文件的 MD5,然后去表里查是不是已经有人传过同样内容的文件。如果存在,直接把当前用户和这个 MD5 关联起来,不需要真正复制文件。
这也是为什么file_info表里一定要有user_id和file_md5两个字段。没有user_id你不知道文件属于谁,没有file_md5你只能按文件名比对,但同名文件内容可能完全不一样。按文件名比对会误判,按大小比对也不可靠,只有 MD5 + 文件大小双条件命中,才能判定为同一份文件。
还有一个字段不能省:parent_id。网盘要模拟目录结构,文件表和目录表常常是同一张表,用is_dir区分。根目录的parent_id设为'0',子目录指向父目录 ID。这种设计把“目录”也当“文件”存,增删改查都统一走 file_info 一张表,省掉另一张 folder 表。代价是查目录树时要用递归或者多次查询,不过数据量不大的网盘完全够用。
3.2 建表语句与字段说明:一份可以直接抄进 MySQL 的 SQL
下面这份建表语句是我按常见课设网盘的通用设计整理的,主键、索引、字符集都考虑到了,可以直接抄:
CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(64) NOT NULL, `password` varchar(64) NOT NULL, `salt` varchar(32) DEFAULT NULL, `total_capacity` bigint(20) DEFAULT 1073741824, `used_capacity` bigint(20) DEFAULT 0, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `idx_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `file_info` ( `id` varchar(32) NOT NULL, `user_id` int(11) NOT NULL, `parent_id` varchar(32) DEFAULT '0', `file_name` varchar(255) NOT NULL, `file_path` varchar(512) NOT NULL, `file_size` bigint(20) NOT NULL DEFAULT 0, `file_md5` varchar(32) DEFAULT NULL, `is_dir` tinyint(1) DEFAULT 0, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user_parent` (`user_id`,`parent_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `share_info` ( `id` int(11) NOT NULL AUTO_INCREMENT, `file_id` varchar(32) NOT NULL, `user_id` int(11) NOT NULL, `share_code` varchar(12) NOT NULL, `expire_time` datetime DEFAULT NULL, `download_count` int(11) DEFAULT 0, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `idx_code` (`share_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;user表里的total_capacity默认设成 1GB,单位是字节,1073741824 = 1GB。used_capacity每次文件上传成功后累加file_size,删除时减少。这里要注意用bigint而不是int,因为一个用户存几个大文件轻松超过 2GB,int最大值是21亿字节,约等于 2GB,不够。
file_info的id用了varchar(32),不是自增。这是刻意的:客户端在上传前后端展示层就会生成 UUID,这样分片上传合并时,同一文件可以直接用这个 ID 写库,避免并发插入时自增主键返回来回查询。parent_id也用 varchar,和id类型一致,不然查询会隐式转换,索引失效。
share_info的share_code设置了唯一索引,这是为了防止两条分享记录生成同一个短码。虽然 UUID 片段碰撞概率低,但唯一约束能兜底,一旦冲突,重试一次就好。
3.3 不用外键和自增主键的取舍:数据量小也别偷懒的边界
很多初学者看到user_id就下意识加FOREIGN KEY。在课设里没问题,但真实网盘项目最好别加。原因是删除目录或文件时,如果是递归删除,外键会逼迫你先处理子表再删主表,每删一个文件都要检查关联表,批量删除几十万条记录时性能会烂掉。网盘里的file_info和user的关系是“弱关联”,用户删除时,文件本身可以保留在服务器上一段时间做备份清理,不需要强一致性。
自增主键int在文件表上也存在隐患。网盘的file_info直接对应真实文件元数据,如果文件数量到亿级,int就满了。用 UUID 字符串虽然占空间,但胜在全局唯一、客户端可生成,水平拆分时不用改主键策略。这个对课设来说有点过度设计,但把“为什么”写在注释里,面试时能多说两句。
删改逻辑也有讲究。删除文件不是 DELETE 一条记录就完事,还要看有没有其他用户引用了同一个file_md5。如果只有当前用户引用,可以直接删物理文件;如果还有别人引用,只能删关联记录,物理文件不动。这个判断在 DAO 层的伪代码是:
SELECT COUNT(*) FROM file_info WHERE file_md5 = ? AND file_path = ?;查出来等于 1,说明只有当前记录在引用,物理文件可以删;大于 1 就只删元数据。这个逻辑是网盘系统“秒传”功能的双刃剑,处理不好会出现文件删了别人也丢的情况。我从一开始就在 DAO 层写好这个判断,后面少了很多线上问题。
4. 上传、下载、分享链接:核心 JavaWeb 代码这样写才留得住用户
4.1 上传接口实现:Servlet 3.0 的 Part 与你真正需要处理的分片问题
传统 javaweb 网盘的上传接口,优先用 Servlet 3.0 的Part接口,不需要再引入 commons-fileupload。代码里要加@MultipartConfig注解,否则request.getPart拿不到东西。这个注解里有两个参数必须调:maxFileSize限制单文件大小,maxRequestSize限制整个请求体大小。不设置的话,Tomcat 对 multipart 默认只允许 10MB,传大文件会直接打回。
@WebServlet("/upload") @MultipartConfig( maxFileSize = 1024 * 1024 * 2048, // 单文件 2GB,给足空间 maxRequestSize = 1024 * 1024 * 4096, // 整个请求 4GB,为分片预留 fileSizeThreshold = 1024 * 1024 // 超过 1MB 写临时文件,避免内存爆 ) public class UploadServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); Part filePart = req.getPart("file"); String submittedName = filePart.getSubmittedFileName(); if (submittedName == null || submittedName.trim().isEmpty()) { resp.getWriter().write("缺少文件名"); return; } // 先算 MD5,用于秒传判断,这里用 Commons Codec String md5 = DigestUtils.md5Hex(filePart.getInputStream()); File storageDir = new File("/data/netdisk/files"); if (!storageDir.exists()) storageDir.mkdirs(); File temp = new File(storageDir, md5 + ".tmp"); try (InputStream in = filePart.getInputStream(); FileOutputStream out = new FileOutputStream(temp)) { byte[] buffer = new byte[8192]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } } // 如果文件已存在,则不需要覆盖,走秒传逻辑 File dest = new File(storageDir, md5); if (!dest.exists()) { temp.renameTo(dest); } else { temp.delete(); } // 这里插入 file_info 记录,方法略 resp.getWriter().write("OK:" + md5); } }@MultipartConfig里的fileSizeThreshold很关键。当文件大小超过这个阈值,文件内容会写入临时目录而不是留在内存,否则 JVM 内存直接被大文件塞满。上传完成后临时文件要清理,renameTo在跨盘符时会失败,所以storageDir和临时文件必须在同一个磁盘路径下。
分片上传不是必须在 Servlet 端拆,更常见的做法是前端把大文件切成多个 5MB 的分片,每个分片单独请求这个接口,后端按分片序号写临时文件,全部传完后再合并。这个源码如果只提供单文件上传,你接到手后传超过 2GB 的文件还是会失败。要支持分片,需要加一个chunkIndex和totalChunks参数,这属于进阶改造,后面第 6 章讲思路。
4.2 下载接口实现:OutputStream 写文件流时最容易忽略的 Content-Type
下载是网盘的核心,也是最容易出乱码的地方。先看一段基础下载接口代码:
@WebServlet("/download") public class DownloadServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String fileId = req.getParameter("fileId"); // 查库得到 fileInfo,这里省略 DAO 层 String fileName = "课程资料.zip"; String filePath = "/data/netdisk/files/" + fileMd5; File file = new File(filePath); if (!file.exists()) { resp.sendError(404, "文件不存在"); return; } resp.setContentType("application/octet-stream"); // 关键:URLEncoder 处理中文文件名,并把 + 还原为 %20 String encodeName = URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20"); resp.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodeName); resp.setContentLengthLong(file.length()); resp.setHeader("Accept-Ranges", "bytes"); try (InputStream in = new FileInputStream(file); OutputStream out = resp.getOutputStream()) { byte[] buffer = new byte[8192]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } } } }先说乱码。老项目里常见写法是:
resp.setHeader("Content-Disposition", "attachment; filename=" + fileName);这行代码在 Chrome 和 Firefox 上会把中文文件名变成一堆下划线。正确做法是用 RFC 2231 格式,把文件名编码后放进filename*。filename里也保留一份 ASCII 兜底,但旧浏览器可以不管。
再说断点续传。Content-Disposition设置完后,浏览器下载时如果没设置Accept-Ranges: bytes,就不会显示“下载速度/剩余时间”这种进度交互,而且下载到一半断了只能重新来。要支持真正的断点续传,还得解析Range头,这里不是几百行能讲完的,但至少把Accept-Ranges加进去,不会破坏现有逻辑。
resp.setContentLengthLong必须写在getOutputStream()之前,而且文件长度必须准确。如果文件在下载过程中被删除,长度不对,浏览器会提示“下载失败”。如果查库时拿到file_size,不要用它,直接用file.length(),因为物理文件可能被清理过,元数据和实际不一致。
4.3 生成分享链接:从 UUID 到短码,校验逻辑放在哪一层
分享功能是网盘的“门面”,也让文件能跨用户流动。我的实现习惯是,点击分享时生成一个 8 位短码,而不是整个 UUID。UUID 太长,不适合做链接参数。短码算法:
public static String generateShareCode() { String uuid = UUID.randomUUID().toString().replace("-", ""); // 取 8 位,碰撞后通过唯一索引兜底 return uuid.substring(0, 8); }代码很短,但要注意一点:8 位十六进制只有 42 亿种组合,文件多时可能碰撞。稳妥做法是生成后查一次share_info,如果share_code已存在就重新生成。因为表上已经建了唯一索引idx_code,即使并发插入撞了,MySQL 也会抛异常,你在 Service 层捕获DuplicateKeyException再重试一次即可。
校验分享链接的逻辑我放在 Servlet 的 doGet 里,不放在 JSP 页面。流程是:接收code参数,查share_info表,先判断expire_time是否过期或state是否为 0,没异常就把file_id取出来,去file_info查元数据,最后跳转到下载接口。查询 SQL 长这样:
SELECT file_id, user_id, expire_time, download_count FROM share_info WHERE share_code = ? AND state = 1;对了,上面建表时我故意漏了state字段。真实项目里分享链接一定需要“取消分享”功能,逻辑上可以删行,但更好的做法是加一个TINYINT state DEFAULT 1,取消时置 0。删行会导致历史链接彻底失效,想恢复就没了。加字段后,分享链接的校验条件才会更完整。download_count可以用来做“限免次数”或统计热度,这个字段在课设里往往被忽略,但面试时提到它,会显得你考虑过真实运营场景。
5. 避坑指南:JavaWeb 网盘从“能跑”到“好用”的 5 个翻车点
5.1 上传大文件超时:Tomcat 默认配置不是给你传 1GB 用的
现象:传一个 300MB 的文件,进度到 60% 直接断掉,控制台报Connection reset或者后端日志里出现SocketTimeoutException。原因:Tomcat 连接器的connectionTimeout默认是 20 秒,上传时客户端和服务端之间长时间没有额外请求,超过了 20 秒就被断开了。另外 Tomcat 默认的maxPostSize是 2MB,multipart 表单超过 2MB,超出部分会被丢弃。
解决:在 Tomcat 的conf/server.xml的 Connector 上调大超时,并放开提交大小限制:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="300000" maxPostSize="-1" maxSwallowSize="-1" URIEncoding="UTF-8" />maxPostSize="-1"表示不限制 POST 请求体大小;maxSwallowSize="-1"允许吞掉上传出错后的残余数据包,否则服务端在读取异常时还会被客户端数据卡住。改完要重启 Tomcat,修改server.xml后不会热加载。
这里顺便说一个血泪经验:本地 IDEA 里跑 Tomcat 默认用的是安装目录的临时配置,经常改完没生效。一定要看启动日志里 Tomcat 的实际路径是不是你改的那份。IDEA 里如果配置了Deployment使用的是 “Server without artifacts”,它可能直接用内置的配置。
5.2 文件名中文乱码:HTTP 头带中文文件名是编码重灾区
现象:上传一个“毕业论文终版.pdf”,下载到本地变成“____.pdf”或者“毕业论文终版%PDF.pdf”。前端 JSP 页面显示正常,但下载就是乱码。原因:HTTP 头的Content-Disposition参数默认只支持 ASCII,你把中文字节直接塞进去,浏览器按 ISO-8859-1 解码,就变成了下划线。
解决:下载接口里正确编码文件名,代码在第 4 章给过。这里再补充一个注意点:URLEncoder.encode会把空格变成+,而浏览器解析filename*时会把+当作字面加号,所以必须replaceAll("\\+", "%20")。空格不算罕见,文件名里带空格的人特别多。
另外,上传时的req.setCharacterEncoding("UTF-8")必须在读取任何参数之前调用,放在doPost第一行。对于 multipart 请求,单靠setCharacterEncoding不够,还要保证 Tomcat 连接器用 UTF-8 解码 URI。两个都做到,乱码才彻底消失。
5.3 刷新页面重复提交表单:PRG 模式在网盘里不能只用一次
现象:上传一个文件后,页面 URL 还是/upload,按 F5 刷新,文件又传了一遍,数据库里出现两条同 MD5 但 ID 不同的记录。原因:表单 POST 提交到 Servlet 后,Servlet 直接 forward 到了结果 JSP。浏览器刷新时重新提交上次的 POST 请求,上传动作重复执行。
解决:用 Post/Redirect/Get 模式,上传成功后重定向到列表页,而不是转发:
// 上传成功后: resp.sendRedirect(req.getContextPath() + "/file/list?parentId=" + parentId);这里有一个细节:重定向是 GET 请求,如果列表页需要展示父目录 ID,要把parentId作为查询参数带上。直接sendRedirect("/list")会导致用户回到根目录,体验很怪。这个坑在网盘里比普通博客表单更隐蔽,因为用户不会只传一个文件,连续上传时刷新频率更高。
5.4 把下载路径写成项目内路径:重启后文件全没了的血泪经验
现象:本地跑好好的,上传几个文件后重启 Tomcat,登录发现文件列表还在,但点击下载 404。去webapp/upload/目录看,里面是空的。原因:代码里用的是request.getServletContext().getRealPath("/upload")获取存储路径,这个路径指向 Tomcat 部署目录的upload文件夹。IDEA 里重启项目会重新解压 war,旧目录被覆盖清理了。
解决:任何网盘系统,文件物理存储路径必须放在 Tomcat 之外,比如/data/netdisk/files。我习惯在web.xml里配置一个全局参数:
<context-param> <param-name>file.storage.path</param-name> <param-value>/data/netdisk/files</param-value> </context-param>然后在 Servlet 里读取:
String basePath = getServletContext().getInitParameter("file.storage.path");这个参数既可以用 Linux 绝对路径,也可以用 Windows 的D:/netdisk/files。好处是部署新版本、清理 Tomcat 缓存都不影响真实文件。修改代码后重新部署,文件依旧在。如果数据库里的file_path字段存的是相对路径,也要改成绝对路径或统一前缀。
5.5 数据库连接池配置过小:并发一高就卡死的隐藏瓶颈
现象:本地两个人同时上传大文件,一个传完,另一个页面一直转圈,数据库报Connection is not available, request timed out after 30000ms。原因:老项目用 DBCP 或 C3P0 时默认maxTotal=10或更小,上传过程中虽然不常查库,但文件元数据写入和秒传查询会在瞬间并发占用连接,连接池耗尽后其他请求只能排队。
解决:把连接池最大连接数调大,并设置合理的等待超时。如果是 DBCP2,配置文件里:
maxTotal=30 maxIdle=10 minIdle=5 maxWaitMillis=10000同时,上传接口里的文件流操作要放在数据库连接操作之外。常见错误写法是先在 Servlet 里从连接池拿一个连接,然后边读文件边写库,导致连接持有时间长到分钟级。正确做法是:先完成文件流落盘,再开启连接写元数据,数据库连接占用时间压到毫秒级。这样连接池不用太大也能扛住并发。
遇到上传并发卡死时,不要只加连接池参数,先看是不是代码里try (Connection conn = dataSource.getConnection())把连接包在了 IO 操作外面。如果是,调连接池等于给水箱加水,但水管还是堵的。
6. 部署到服务器后,我会先做的三个小改进:从课设项目变成能自己用的网盘
6.1 文件存储目录的外部化与备份策略
部署到 Linux 服务器后,第一件事是把文件目录完全独立出来,然后写个定时备份脚本。常见做法是把物理文件目录和 MySQL 数据库都按日期打快照:
#!/bin/bash # /opt/backup/netdisk_backup.sh BACKUP_ROOT=/backup TODAY=$(date +%Y%m%d) mkdir -p $BACKUP_ROOT/$TODAY cp -r /data/netdisk/files $BACKUP_ROOT/$TODAY/ mysqldump -uroot -p netdisk --single-transaction --quick > $BACKUP_ROOT/$TODAY/netdisk.sql tar -czf $BACKUP_ROOT/netdisk_$TODAY.tar.gz -C $BACKUP_ROOT/$TODAY . rm -rf $BACKUP_ROOT/$TODAY这个脚本放在 cron 里每天凌晨跑一次。数据库单表数据量不大时,mysqldump足够,不要用快照工具,容易把 binlog 弄乱。备份文件保留 7 天,超过就删,避免磁盘被撑满。我自己部署网盘的第一条习惯就是“文件可丢,元数据不可丢”,所以mysqldump里用了--single-transaction,备份过程中不影响线上写入。
6.2 用一张 file_user_link 表把分享做成可撤销
如果你要做的不是课设答辩,而是给自己和朋友长期用,分享功能必须支持“取消分享”。原版设计往往只有share_info,取消就是删除记录,导致已发出的链接瞬间失效。但真正好用的网盘允许用户把链接发给别人后,自己还能看到“已分享”列表,并可以单独撤销某个链接。
我的做法是在share_info表上加state字段,默认 1,撤销时更新为 0:
ALTER TABLE share_info ADD COLUMN state tinyint(1) NOT NULL DEFAULT 1;校验链接时把state = 1作为查询条件。这个方法最简单,不需要改原有代码结构,只改 SQL 和判断逻辑。如果你还想做一个“转存”功能,让别人把分享文件保存到自己网盘,那需要一个user_file_ref表记录“用户与文件多对多”的关系,否则很难判断一个文件被多少人引用。
6.3 断点续传与秒传的前端配合思路
源码层面如果只做了简单上传,传到一半断网就得重新来,这在真实网盘里不可接受。改造思路是前端用 File API 把文件切成 5MB 的块,每个块上传时先传 MD5,后端查file_info,如果已有相同 MD5,就跳过该块。全部块传完后,后端将临时块按顺序合并成完整文件,再写文件元数据。
前端配合时不推荐直接用原生 XMLHttpRequest 一坨梭哈,建议用File.slice()配合并发控制,控制并发数在 2 到 3 个,否则服务器 IO 很容易被拖死。这个改造看起来复杂,但核心仍然是后端那两张表:file_info记录最终文件的 MD5,临时块记录在另一张upload_chunk表。有了它,用户上传时刷新页面不用重新传,秒传也能真正落到用户界面上。
我自己凡是部署网盘,第一件事一定是把存储路径挪到 Tomcat 外面,第二件事是给分享表加 state 字段。这两个改动做完,整个系统的稳定性才算是真正从“能跑”走向“能用”。后续遇到上传超时、并发卡死,也都能从前面的检查列表里快速定位。这套基于 javaweb 的仿百度网盘小型系统,价值不在于它有多完整,而在于你愿意为它补上真实场景里缺失的那块短板。希望帮到你。
本文还有配套的精品资源,点击获取