尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
JSP相册管理系统实战:图片上传、缩略图与分页鉴权
简介这是一套面向Java Web入门者与课程设计学习者的JSP相册管理系统源码围绕照片上传、分类管理、评论与后台维护等常见需求展开适合用来理解JSP页面、Servlet请求处理、JavaBean业务封装与MySQL数据访问之间的协作方式。压缩包共115个文件约3.53MB以20个jsp页面、22个java源文件及对应class、26张jpg图片、5个gif素材为主另含jar依赖、properties配置、sql建库脚本和web.xml部署描述能够较完整地呈现一个传统Java Web项目的目录组织与运行依赖。已有434人学习说明其作为入门级实战案例具有一定参考面。读者可从中梳理用户登录、照片上传、相册分类、评论管理等模块的实现脉络观察Servlet与Biz层的职责划分、JDBC连接管理及数据库表结构设计并借助源码对照理解JSP动态页面与后端数据交互的完整流程为课程设计、毕业设计选题和Java Web基础巩固提供可复用的参考。1. JSP 相册管理系统到底在解决什么问题传了两百张图第三天页面就白了某开发者给一个内部团队做的相册系统上线第三天收到反馈图片传到第 200 张后相册列表翻到第三页就一片空白。日志里没有异常数据库里照片记录也在。最后一查三件事叠在一起分页 SQL 的 offset 用页码直接乘、缩略图根本没生成、列表页每张图都把 5MB 原图怼进浏览器。这不是 JSP 的问题是分层没做对。基于 JSP 的相册管理系统说到底是「用户体系 相册分组 图片上传存储 缩略图与浏览分页」四件事的组合JSP 只负责最后那层模板渲染。它适合两类人一类是要交课程设计、需要一套能跑通全链路的完整增删改查的人另一类是拿它当练手项目想搞明白上传、存储、鉴权和分页这四道关怎么过的人。下面按我实际搭的顺序讲从骨架到上传到分页最后落到上线前排不掉的几个坑。2. 先把工程骨架立住JSP 加 Servlet 加 JDBC 的分层与选型先把一句话说清楚JSP 是视图层技术它擅长的事情是把后端准备好的数据渲染成 HTML不擅长的是接请求、校验参数、开数据库连接。很多教程里的写法是把 JDBC 代码直接塞进% %里跑一个 demo 没问题但只要需求多一条比如「相册要能设成私密」这堆脚本就没法改。2.1 为什么纯 JSP 写业务会在第三个需求上翻车在 JSP 里直接写业务最典型的样子是这样页面顶部% page importjava.sql.* %中间% Connection conn DriverManager.getConnection(...); %然后循环ResultSet拼img。这套写法有几个结构性问题。第一是连接管理失控。每个请求进来都新建一个连接用完忘了close()连接池很快打满表现出来是整站 500而且错误信息指向的地方和真正的泄漏点往往差着好几个文件。第二是参数校验无处安放。上传接口要在接收文件之前就判断登录状态、剩余空间、单文件大小这些逻辑写在 JSP 里就是一堆 if 嵌套改一次动全身。第三是渲染和查询耦合。列表页要同时查相册信息和照片数量纯 JSP 写法会变成嵌套循环里再开查询N1 查询就是这么来的。所以我一般会把工程切成四层servlet接请求和校验、service放业务规则、dao管 SQL、jsp只做渲染。这个切法不新鲜但它能让你在加「相册私密」这种需求时只改 service 一层加一个 filter 判断而不是去翻十几个 JSP 文件。分层不是为了好看是为了改需求时不用全局搜索getConnection。2.2 三张表的建表脚本与字段取舍相册系统的表不用多三张足够跑通闭环用户、相册、照片。照片表里存路径而不是存二进制这一点后面 3.4 节会展开讲先看结构。-- 用户表只保留登录和展示必需字段 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, pwd_hash CHAR(64) NOT NULL, -- SHA-256 十六进制不存明文 salt CHAR(16) NOT NULL, -- 每个用户独立盐值 create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 相册表owner_id 是后面做越权校验的关键外键 CREATE TABLE album ( id INT PRIMARY KEY AUTO_INCREMENT, owner_id INT NOT NULL, title VARCHAR(64) NOT NULL, is_private TINYINT NOT NULL DEFAULT 0, -- 0 公开 1 私密 cover_photo INT NULL, -- 封面图 id可为空 create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_owner (owner_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 照片表存相对路径、尺寸、大小不存二进制 CREATE TABLE photo ( id INT PRIMARY KEY AUTO_INCREMENT, album_id INT NOT NULL, file_name VARCHAR(128) NOT NULL, -- 原始文件名仅作展示 store_path VARCHAR(255) NOT NULL, -- 相对存储路径如 2024/06/ab12cd.jpg thumb_path VARCHAR(255) NOT NULL, -- 缩略图相对路径 byte_size INT NOT NULL, -- 字节数用于统计配额 width SMALLINT NOT NULL, height SMALLINT NOT NULL, deleted TINYINT NOT NULL DEFAULT 0, -- 软删除避免物理删除误伤 create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_album (album_id, id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个字段的取舍值得说一句。pwd_hash用CHAR(64)是因为 SHA-256 十六进制固定 64 位定长字段比VARCHAR省一点索引开销也防止有人存进去更长的串。salt单独一列而不是拼在 hash 里是为了将来换算法时不用动表结构。store_path存相对路径而非绝对路径这样换部署目录、换存储根盘符的时候不用批量改数据。deleted用软删除因为相册系统最常见的误操作是「删了才发现要恢复」物理删除之后只能靠备份而备份通常不是实时的。KEY idx_album (album_id, id)这个联合索引是给分页查询准备的。相册详情页的查询模式固定是「按 album_id 过滤按 id 倒序取一段」联合索引正好覆盖过滤和排序两个动作能省掉一次文件排序。2.3 目录结构与 web.xml 路由让上传请求落到正确的 Servlet工程结构直接决定你后面找 bug 的速度。我习惯的目录长这样album-system/ ├── src/main/java/ │ ├── servlet/ LoginServlet / AlbumServlet / PhotoUploadServlet / ThumbServlet │ ├── service/ AlbumService / PhotoService │ ├── dao/ AlbumDao / PhotoDao / UserDao │ ├── filter/ AuthFilter / EncodingFilter │ └── util/ DbUtil / ThumbUtil / PathUtil ├── src/main/webapp/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── views/ album_list.jsp / album_detail.jsp / login.jsp │ ├── static/ css / js │ └── META-INF/ └── upload/ 存储根目录部署时放在 webapp 之外两个点必须强调。views目录放在WEB-INF下面浏览器无法直接访问所有页面只能通过 Servlet 转发进来这样鉴权才有意义不然用户直接敲 URL 就能绕过登录。upload目录放在webapp之外是因为打包部署时webapp里的内容会被覆盖或清空用户上传的文件放在里面迟早丢一次。路由这块Servlet 3.0 之后可以直接用注解web.xml里只留欢迎页和错误页比全写在 XML 里好维护?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1 display-namealbum-system/display-name welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list !-- 统一 500 错误页避免异常堆栈直接吐给浏览器 -- error-page error-code500/error-code location/WEB-INF/views/error.jsp/location /error-page error-page error-code403/error-code location/WEB-INF/views/forbidden.jsp/location /error-page /web-app这里version3.1意味着容器至少要支持 Servlet 3.1MultipartConfig和WebServlet才能正常生效。如果部署到只支持 2.5 的老容器上注解会被完全忽略表现是访问接口 404而代码编译毫无问题这个坑在 5.1 节还会提到。欢迎页指向index.jsp只是为了让根路径不报 404真正的首页跳转放在index.jsp里用一行response.sendRedirect转到相册列表的 Servlet避免用户停留在根路径看到空白。分层的价值在这里就体现出来了AuthFilter拦/album/*和/photo/*EncodingFilter统一设 UTF-8两个 Filter 一挂后面所有 Servlet 都不用重复写这两件事。骨架立完再往上堆功能才不会越写越乱。3. 图片上传链路从 multipart 解析到缩略图落盘上传是整个系统里最容易出问题的一环因为它同时涉及 HTTP 协议解析、临时文件、磁盘 IO、图片解码四件事。任何一环参数没配对症状都是「上传失败」四个字但原因完全不同。3.1 Part 接口与 commons-fileupload 的选择依据Servlet 3.0 引入了Part接口容器自己解析multipart/form-data代码里直接request.getPart(file)就能拿到文件流。老一点的做法是引入commons-fileupload配合ServletFileUpload手动解析。两者选哪个看三个维度维度Servlet 3.0 Partcommons-fileupload依赖零依赖容器内置需要额外 jar临时文件控制靠MultipartConfig的location和阈值靠DiskFileItemFactory的阈值和目录进度监听不支持需自己包一层输入流支持ProgressListener超大文件中断由容器处理行为跟版本相关可精确控制代码量少注解配完直接用多解析器要手动初始化判断标准很直接如果只是普通相册上传单文件限制在十兆以内用Part就够了少一个依赖少一类冲突。如果要做「上传进度条」或者「断点续传」那Part就不够用得自己包ServletInputStream统计字节数这时候用commons-fileupload的监听器反而省事。我一般选Part然后在上传接口外面加一个前端的分片逻辑把大文件切成 2MB 一片服务端按片追加。这样进度条归前端管服务端只负责拼文件比在 Java 里搞监听器干净。3.2 上传 Servlet 的完整实现与四个必调参数下面是能直接跑的上传 Servlet去掉业务装饰之后核心就这些WebServlet(/photo/upload) MultipartConfig( fileSizeThreshold 1024 * 1024, // 超过 1MB 落临时文件避免大图占堆内存 maxFileSize 10L * 1024 * 1024, // 单文件 10MB 上限 maxRequestSize 60L * 1024 * 1024, // 单次请求 60MB支持一次传多张 location /data/album/tmp // 临时目录必须存在且可写 ) public class PhotoUploadServlet extends HttpServlet { private static final SetString ALLOW Set.of(image/jpeg, image/png, image/webp); private final PhotoService photoService new PhotoService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Integer uid (Integer) req.getSession().getAttribute(uid); if (uid null) { resp.sendError(401); return; } int albumId Integer.parseInt(req.getParameter(albumId)); // 关键一步确认相册归属不然后面所有校验都白做 if (!photoService.albumBelongsTo(albumId, uid)) { resp.sendError(403); return; } for (Part part : req.getParts()) { if (!file.equals(part.getName())) continue; String submitted PathUtil.baseName(part.getSubmittedFileName()); // 只取文件名剥掉路径 String ext PathUtil.safeExt(submitted); if (!ALLOW.contains(part.getContentType()) || ext null) { resp.sendError(400, unsupported type); return; } if (part.getSize() 10L * 1024 * 1024) { resp.sendError(413); return; } // 生成不可预测的文件名杜绝同名覆盖和路径注入 String storeName PathUtil.randomName(ext); // 如 a3f9c1e2.jpg String relPath PathUtil.dateShard(storeName); // 如 2024/06/12/a3f9c1e2.jpg Path dest PathUtil.resolveUnderRoot(relPath); // 拼到存储根目录下做越界检查 try (InputStream in part.getInputStream()) { Files.createDirectories(dest.getParent()); Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING); } photoService.saveWithThumb(albumId, submitted, relPath, dest.toFile()); part.delete(); // 删掉容器临时文件否则 /data/album/tmp 会越堆越多 } resp.sendRedirect(req.getContextPath() /album/detail?id albumId); } }四个参数逐个说。fileSizeThreshold设 1MB含义是小文件直接在内存里处理大文件落到location指定的临时目录避免几十兆的图片把堆撑爆。maxFileSize是单文件硬上限超了容器直接抛IllegalStateException注意这个异常要在 Filter 或error.jsp里接住否则用户看到的是容器默认错误页。maxRequestSize要比maxFileSize大因为它管的是一次请求的总大小一次传五张图就是五个maxFileSize之和再加表单开销设成一样大就会出现「单张能过、一次传五张就失败」的诡异现象。location必须提前创建好容器不会帮你建目录目录不存在时的报错信息通常只写「临时目录不可用」很难定位。代码里两个细节容易被忽略。getSubmittedFileName()返回的字符串在部分浏览器里带完整路径比如C:\Users\x\1.jpg如果不做baseName处理后面拼路径就会出错这也是路径穿越的一个入口5.1 节展开。part.delete()是必须的容器只在请求结束时清理临时文件但如果你的临时目录和系统临时目录不是同一个清理行为可能不符合预期主动删一次最稳。3.3 缩略图生成Thumbnailator 与原生 ImageIO 的差别不生成缩略图的相册系统在图片超过五十张之后就没法看了。列表页每张图都加载原图五兆一张一页二十张就是一百兆流量移动端打开直接白屏。缩略图必须在上传成功后同步生成不能等到列表页请求时再临时压缩那样第一次访问会卡到超时。原生ImageIO能干活但代码量不小读图、算缩放比例、getScaledInstance、画到BufferedImage、再写出去还得自己处理透明通道。Thumbnailator把这一串封装成链式调用我一般用它public static void makeThumb(File src, File dest, int maxSide) throws IOException { // 1. 先按 EXIF 方向信息校正手机竖拍的照片不校正会躺倒 BufferedImage raw ImageIO.read(src); if (raw null) throw new IOException(not an image: src.getName()); BufferedImage fixed ExifUtil.rotateIfNeeded(src, raw); // 2. 等比缩放到长边不超过 maxSide质量 0.82 是体积和观感的平衡点 Thumbnails.of(fixed) .size(maxSide, maxSide) .keepAspectRatio(true) .outputQuality(0.82f) .outputFormat(jpg) .toFile(dest); // 3. 体积兜底压缩后仍超过 200KB 就降质量重来一次 if (dest.length() 200 * 1024) { Thumbnails.of(fixed) .size(maxSide, maxSide) .keepAspectRatio(true) .outputQuality(0.65f) .outputFormat(jpg) .toFile(dest); } }size(maxSide, maxSide)配合keepAspectRatio(true)的实际效果是「长边缩到 maxSide短边按比例」所以传 320 出来的不会是一张被拉伸的方图。outputQuality(0.82f)是个经验值低于 0.7 在渐变色块上能看到明显色带高于 0.9 体积降不下来。outputFormat(jpg)是必须写的不写的话 Thumbnailator 会按输出文件名的扩展名猜一旦你把缩略图命名成.dat这种无扩展名形式就会写出格式错误的文件浏览器显示成裂图而且没有任何报错。EXIF 那一步是最容易被跳过的。手机拍的照片会在 EXIF 里记一个方向标记读取像素数据时不带旋转生成的缩略图在大图看是正的、缩略图是横的。修的办法是在读图之后按方向标记做一次仿射变换或者用支持自动校正的读图方式。这件事不修用户上传十张竖拍照片九张缩略图是躺着的反馈会很难看。3.4 存磁盘还是存 BLOB一次真实的取舍把图片直接存进数据库BLOB字段好处是备份一体化、迁移方便、事务一致。但它有硬伤单表体积膨胀极快一千张五兆的图就是五个 Gmysqldump导出会慢到离谱而且SELECT *的查询会把图片数据一起拖出来缓存全部失效。我做的取舍是数据库只存路径文件落磁盘路径按日期分片。分片这件事有实际意义同一目录下堆十万个文件很多文件系统在列目录时会明显变慢按年/月/日三层分片之后单个目录里的文件数控制在几千量级。表里store_path存2024/06/12/a3f9c1e2.jpg读的时候用PathUtil.resolveUnderRoot拼回去。只有一种情况我会考虑BLOB图片总量确定很小比如几百张头像、并且要求备份绝对原子。除此之外都走磁盘备份用rsync处理文件目录数据库单独导两条线各管各的反而更清楚。4. 相册浏览与分页SQL 分页、JSTL 渲染与图片访问控制到这一层数据已经在库里了剩下的是怎么把它稳稳当当地显示出来。分页和鉴权是两块最常出问题的地方。4.1 分页 SQL 的两种写法和 count 查询的坑分页有两种主流写法。一种是LIMIT offset, size另一种是游标式「上一页最后一行的 id 作为起点」。相册系统的场景是用户点页码跳转需要总数算总页数所以用LIMIT更合适。-- 列表查询走 idx_album(album_id, id) 覆盖过滤和排序 SELECT id, file_name, thumb_path, width, height, byte_size FROM photo WHERE album_id ? AND deleted 0 ORDER BY id DESC LIMIT ?, ?; -- 总数查询单独一条不要和列表查询混在一起 SELECT COUNT(*) FROM photo WHERE album_id ? AND deleted 0;LIMIT ?, ?的第一个问号是偏移量不是页码。这是新手最容易搞错的地方页码 3、每页 20 条偏移量应该是(3-1) * 20 40写成3 * 20 60就会漏掉整整一页。第一页偏移量是 0不是 20。这种错法在一个小数据量的库里几乎测不出来等到数据超过两页才开始显形前面第 1 章那个「翻到第三页空白」的例子就是这么来的。COUNT(*)那条查询在相册场景下值得单独优化。如果相册里有几万张照片每次翻页都COUNT一次代价不低。常见的处理是图片总数在相册表里冗余一个计数器上传加一、删除减一列表页直接读这个字段只有计数器缺失时才回落到COUNT。另一种做法是「下一页」按钮式分页压根不显示总页数查询时多取一条判断有没有下一页COUNT完全省掉。还有一个细节ORDER BY id DESC必须配合索引才快。如果排序字段和索引顺序不一致MySQL 会做一次 filesort数据量大了以后翻到后面几页会明显变慢因为偏移量越大需要跳过的行越多。真到了那个量级就该换成游标分页把上一页最后一条的 id 传给下一页WHERE album_id ? AND id ? ORDER BY id DESC LIMIT 20无论翻到第几页都是索引区间扫描代价恒定。4.2 用 JSTL 把缩略图墙渲染出来JSP 里千万不要再写% %循环。JSTL 的c:forEach加 EL 表达式已经够用而且不会在页面里引入 Java 代码模板文件交给前端改也不会崩。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfn urihttp://java.sun.com/jsp/jstl/functions % div classphoto-grid c:forEach items${page.list} varp varStatusst a classcell href${ctx}/photo/view?id${p.id} %-- 一定要走 thumb 接口不能直接拼文件路径否则鉴权形同虚设 --% img src${ctx}/photo/thumb?id${p.id} width${p.width} height${p.height} loadinglazy decodingasync alt${fn:escapeXml(p.fileName)} /a /c:forEach /div c:if test${empty page.list} p classempty这个相册还没有照片/p /c:ifloadinglazy是浏览器原生懒加载滚动到视口才发请求一页几十张缩略图不会一次性打满并发连接。decodingasync让图片解码不阻塞主线程滚动时更顺。width和height写出真实像素值浏览器能在图片加载前就占好位置避免滚动条跳动这个细节对观感影响比想象中大。fn:escapeXml不能省。文件名是用户可控的如果有人把文件命名成带尖括号的字符串不转义就会把页面结构冲掉。EL 表达式本身不会自动转义JSTL 只有在特定标签里才做这里必须显式加。4.3 别让 upload 目录裸奔图片访问的三种鉴权方式如果upload目录放在webapp下面用户拿到 URL 就能直接访问私密相册的「私密」两个字就是装饰。三种处理方式第一种是把目录放在webapp外所有图片请求走一个 Servlet 读文件写流。这种方式控制力最强每一张图都能校验归属代价是多一次 Java 层的 IO 转发。第二种是用容器级别的安全约束在web.xml里配security-constraint但配置粒度粗通常只按路径前缀管做不了「按图片归属逐张判断」。第三种是给图片生成带时效的签名 URL适合前后端分离的架构。单体 JSP 项目我一般用第一种代码不复杂WebServlet(/photo/thumb) public class ThumbServlet extends HttpServlet { private final PhotoDao photoDao new PhotoDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { Integer uid (Integer) req.getSession().getAttribute(uid); if (uid null) { resp.sendError(401); return; } int id; try { id Integer.parseInt(req.getParameter(id)); } catch (NumberFormatException e) { resp.sendError(400); return; } Photo p photoDao.findWithOwner(id); // 一次联表查出照片归属的相册主人 if (p null) { resp.sendError(404); return; } if (p.isPrivate() p.getOwnerId() ! uid) { resp.sendError(403); return; } Path file PathUtil.resolveUnderRoot(p.getThumbPath()); if (!Files.isReadable(file)) { resp.sendError(404); return; } resp.setContentType(image/jpeg); resp.setHeader(Cache-Control, private, max-age86400); // 私密图只允许浏览器缓存 try (InputStream in Files.newInputStream(file); OutputStream out resp.getOutputStream()) { in.transferTo(out); } } }几个参数值得注意。Cache-Control用private而不是public因为同一张图对不同用户的可见性不同中间代理缓存了就会串号private只允许终端浏览器缓存一天之内重复访问不打服务端。setContentType(image/jpeg)要和你生成缩略图时的输出格式对上如果缩略图是 webp 而这里声明 jpeg部分浏览器会显示异常。resolveUnderRoot里必须做一次规范化加前缀校验防止thumb_path被写成../../etc/passwd这种形式虽然这个字段是我们自己生成的但防御性写法能省掉将来的一次事故。transferTo比手写byte[]缓冲循环简洁JDK 9 之后自带内部会做缓冲。如果还在用更老的 JDK就老老实实写while ((n in.read(buf)) 0)那段。5. 上线前排查中文名、越权、连接泄漏与重复上传这一节的四条每一条我都真实遇到过症状千奇百怪但根因都很朴素。5.1 中文文件名存进去变成问号路径还能被穿越现象用户上传「我的旅行.jpg」列表里显示成???.jpg下载回来文件名是一串乱码。另一种表现是上传成功但文件落到意料之外的目录。原因两个问题。乱码是multipart请求头里文件名的编码和容器解析编码不一致容器默认按 ISO-8859-1 解析中文就变成了问号。路径穿越是getSubmittedFileName()在某些客户端上会返回带目录的路径如果直接拿去拼存储路径../就能跳出存储根目录。解决编码问题在容器配置里指定URIEncoding和file.encoding为 UTF-8请求侧在 Filter 里统一setCharacterEncoding(UTF-8)文件名只取最后一段并且用白名单校验扩展名存储名一律用随机串加白名单扩展名重新生成原始文件名只存在file_name字段里做展示用。这样即使原始名里带路径或特殊字符也不会影响落盘位置。5.2 换个相册 id 就能看别人的私密照片现象A 用户登录后把地址栏里的albumId12改成albumId13直接看到了别人的相册。原因查询只按album_id过滤没有校验当前登录用户是不是这个相册的主人。这类问题在功能开发阶段完全测不出来因为测试的时候大家用的都是同一个账号。解决所有涉及相册和照片的查询一律带上归属条件要么在 SQL 里加AND owner_id ?要么在 service 层查出来之后做一次归属比对。我倾向于两者都做SQL 层做过滤是性能上的必要service 层做断言是逻辑上的保险。另外album的is_private字段要在列表页和详情页分别判断一次私密相册不出现在公开列表里直接访问详情页返回 403。5.3 跑了半天就 500连接池全被占死现象系统刚启动正常访问量上来之后开始间歇性 500重启就好过一阵又犯。日志里能看到「等待连接超时」。原因DAO 层有分支提前returnConnection没进finally。比如查询抛异常走了 catchcatch 里直接返回 null连接就漏了。每次请求漏一个池子很快见底。解决所有 JDBC 资源用 try-with-resourcesConnection、PreparedStatement、ResultSet三层都包进去这样无论正常返回还是抛异常都会关。连接池参数上maximumPoolSize不要按「越大越好」配一般按数据库最大连接数除以应用实例数再留两成余量connectionTimeout设短一点比如 3 秒让问题快速暴露而不是慢慢堆积。另外开一下池子的泄漏检测超过阈值没归还的连接会打日志带堆栈定位起来比翻代码快得多。5.4 同一张图传了五遍磁盘一周就满现象用户反馈「我传了但是没看到」于是反复点上传磁盘占用涨得比照片数快好几倍。原因上传成功后页面跳转慢用户以为没成功就重试服务端没有做去重每次都生成新的随机文件名同一张图存了多份。解决前端按钮点击后立即置灰配合一个提交中的遮罩这是最低成本的做法。服务端做一次内容去重对文件流算一次摘要在album_id加摘要的联合唯一索引上做判重命中就直接返回已有记录。另外给每个用户或每个相册设一个总量配额上传前先查已用SUM(byte_size)超了就拒绝把磁盘写满这件事挡在业务层而不是等运维告警。6. 从能跑到能维护图片服务拆分的判断标准与压测方法单体 JSP 项目跑到什么程度该把图片这块拆出去我的判断标准是三条同时成立图片请求占全部请求的比例超过七成、单机磁盘使用率长期高于六成、缩略图接口的 P95 响应时间超过 300 毫秒。三条不齐就别拆拆了只会多一套部署和一份故障排查成本。拆的时候不需要一步到位先把存储路径抽成一个配置项让本地磁盘和对象存储可以用同一个接口切换这一步改动很小但收益最大阶段存储位置访问方式适用规模起步单机本地目录Servlet 读流转发万张以内单机部署过渡本地目录加 Nginx 静态映射鉴权后返回带签名的短链十万张单机双进程拆分对象存储直传加临时凭证十万张以上多实例过渡阶段那个「鉴权后返回短链」是最划算的一步。图片不再经过 Java 层转发由反向代理直接吐文件Java 只负责在用户点开相册时生成一批带时效的链接链接里带一个用密钥算出来的签名过期就失效。这样既保住了私密性又把 IO 压力从应用进程里挪走了。验证这套改动有没有效果别靠感觉用压测说话。我一般用一段最简单的并发脚本打缩略图接口观察两个数吞吐和错误率。# 200 并发、持续 60 秒打缩略图接口只看关键指标 ab -n 20000 -c 200 -k \ -H Cookie: JSESSIONID登录后的会话 \ http://127.0.0.1:8080/album/photo/thumb?id1001-k开启长连接不开启的话每次请求都重建 TCP测出来的是握手开销不是业务能力。-c是并发数别一上来就拉满从 50 开始每次翻倍找到错误率开始上升的那个点那才是这套配置的真实上限。压测期间同时看容器的线程池活跃数和连接池等待数如果线程数打满而 CPU 只用了三成说明瓶颈在 IO 等待那就该往「把 IO 挪出应用进程」这个方向走而不是加机器。最后说个我踩过的教训。早期做这类系统我总想着把功能堆全相册要有标签、要有回收站、要有分享链接结果核心的上传去重和缩略图校正一直拖着没做等用户量上来补这两件事的成本比一开始做高了十倍因为存量数据得清洗一遍。相册系统这种东西功能少一点不丢人上传不丢文件、翻页不空页、别人的照片看不到这三条守住剩下的都是锦上添花。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

JAVA第三周学习感悟

JAVA第三周学习感悟

加入未来软件工作室第三周主要学习类与对象、成员变量与方法、继承、抽象类与接口这些面向对象核心内容。从之前的数组过渡到面向对象编程,思维模式发生很大转变,难度也有所提升。本周学习内容回顾1.类与对象这是面向对象的入门内容,明白了类…

📅 2026/11/2 1:57:20
css盒子模型讲解

css盒子模型讲解

一、盒子模型解决什么问题控制盒子内容大小、内边距、边框、外边距区分:元素本身占多大空间、它和别的盒子之间留多大空隙解决尺寸计算混乱的痛点(标准盒 / IE 怪异盒两种计算规则)实现元素之间拉开距离、加边框、背景留白、模块排版盒子四层…

📅 2026/11/2 1:57:20
【机器视觉】必会算法之:CLIP(对比语言-图像预训练)

【机器视觉】必会算法之:CLIP(对比语言-图像预训练)

机器视觉之:CLIP1、引言2、CLIP(Contrastive Language-Image Pre-training)2.1 定义与解析2.2 核心原理2.2.1 双塔结构:图像编码器 文本编码器2.2.2 对比学习:把"配对"拉近、把"非配对"推远2.2.3 训练数据:4…

📅 2026/11/2 1:57:20
MORE NEWS

更多资讯

📰

WIZnet IO模块选型指南:工业自动化与物联网通信

1. WIZnet IO模块选型核心考量维度在工业自动化和物联网设备开发中,选择合适的WIZnet IO模块往往决定着整个项目的通信可靠性和长期维护成本。根据我过去五年在智能工厂项目中集成各类IO模块的经验,选型时需要重点评估以下六个硬指标:1.1 通信…

📰

XGP会员状态与购买故障分诊:授权票据、支付回调与资格校验

XGP 获取不了会员状态、点了购买转半天没反应、钱扣了权益没到账——这三句话我在过去两年里至少听过几十遍,而且绝大多数人把它们当成同一个问题,一上来就重装应用、换账号、反复点购买按钮,越折腾越乱。实际上这是三条完全不同的链路&#…

📰

Jev不该被问什么?技术协作中的提问边界与高效沟通指南

1. 从“Jev 不该被问什么”说起:一个被误读的提问边界第一次看到“Jev 不该被问什么”这个标题,我脑子里蹦出来的不是某个具体的人,而是一类场景。你在技术群、项目复盘会、开源社区或者线下技术沙龙里,总会遇到某个人——可能是团…

📰

Jev不该被问什么?人际沟通中的边界感与高情商提问指南

1. 从“Jev 不该被问什么”说起:一个被忽视的沟通边界问题第一次看到“Jev 不该被问什么”这个标题,我脑子里蹦出来的不是某个具体的人,而是一类场景。你在工作群里、在朋友聚会上、在某个兴趣社群里,总会遇到那种让人瞬间卡壳的问…

📰

大模型入门全路径:从机器学习到Transformer与微调部署

1. 从零起步学大模型,先搞清楚这条路的全貌很多人第一次接触大模型,脑子里冒出来的第一个问题往往是"我该从哪开始"。网上资料铺天盖地,一会儿是Transformer架构图,一会儿是PyTorch环境搭建,一会儿又跳到微调…

📰

加密资产价值投资:策略、模型与风险管理

1. 项目概述:当价值投资遇上加密资产传统金融领域的价值投资理念与新兴的加密货币市场看似两个平行世界——前者强调基本面分析和长期持有,后者以高波动性和投机属性著称。但最近两年,越来越多传统投资机构开始将部分资产配置到加密领域&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬