首页 / 资讯中心 / 文章详情

Express后台管理系统实战:JWT鉴权+MySQL连接池+RBAC权限控制

Express后台管理系统实战:JWT鉴权+MySQL连接池+RBAC权限控制 ★ FEATURED ARTICLE
1. 项目概述为什么一个后台管理系统成了Express学习的“分水岭”刚接触Node.js时很多人卡在“能跑Hello World”和“真能干活”之间。Express作为最主流的Node.js Web框架官方文档写得清楚路由、中间件、模板引擎这些概念也容易理解——但一到真正搭一个带用户登录、权限控制、数据增删改查的后台系统就发现文档里没写“怎么组织文件结构”没讲“数据库连接要不要复用”更不会告诉你“为什么登录接口要加CSRF防护而列表接口不用”。这个“Express项目实战-后台管理系统”不是教你怎么抄代码而是还原一个真实开发场景里从零开始搭建、调试、上线一个可用系统的完整心路历程。核心关键词是Express、后台管理系统、RESTful API、JWT鉴权、MySQL连接池、EJS模板渲染、角色权限控制。它适合三类人一是刚学完Express基础、想验证自己是否真懂的人二是前端转全栈、需要补足服务端工程化思维的人三是小团队技术负责人正为内部工具选型发愁需要一套轻量、可控、易维护的方案。它不追求炫技不堆砌微服务或Serverless而是聚焦在“如何用最少的依赖、最清晰的逻辑、最稳妥的方式把一个管理后台跑起来并且后续好加功能、好改bug、好交接给同事”。我带过不少新人做这个项目发现80%的卡点根本不在语法上而在“不知道该先做什么”。比如有人一上来就猛写用户注册接口结果连数据库表字段都没设计好后面加个“最后登录时间”字段就得改三处代码还有人把所有路由都写在一个app.js里等做到第7个模块时文件已经2000行改个按钮颜色都要全局搜索。这个项目的价值恰恰在于它强制你面对真实开发中绕不开的问题目录怎么分错误怎么统一处理密码怎么安全存储日志往哪写部署时环境变量怎么配这些问题没有标准答案但有经过验证的合理路径。接下来的内容就是我把过去五年里在多个实际项目中反复打磨、踩坑、优化出来的那套做法原原本本拆给你看。2. 整体架构设计与技术选型逻辑2.1 为什么选Express而不是Koa、Nest或Fastify很多人看到标题里写“Express项目实战”第一反应是“现在还学ExpressKoa更轻量Nest更企业级Fastify性能更好。”这话没错但选型从来不是比参数而是比“适配度”。我们来算一笔账学习成本与认知负荷Koa的洋葱模型对新手很友好但它把很多基础能力如路由、静态文件服务抽成独立包你得自己装koa-router、koa-static、koa-bodyparser……光是搞清这些包的加载顺序和中间件执行时机就能耗掉两天。而Express把这些都内置了app.use()、app.get()、app.post()一条命令的事新手能立刻看到效果建立正向反馈。生态成熟度与问题解决效率Nest确实强大TypeScript 装饰器 模块化写起来像Java。但当你遇到一个奇怪的依赖注入失败或者某个拦截器没生效去查文档、翻GitHub Issues、看源码平均耗时是Express的3倍。而Express的报错信息直白“Cannot GET /admin/user”你立刻知道是路由没配“Error: Cannot enqueue Handshake after already enqueuing a Handshake”你马上意识到数据库连接池满了。这种“所见即所得”的调试体验对初学者和快速迭代至关重要。项目规模匹配度这个后台管理系统预估最终会有15~20个核心接口用户、角色、菜单、日志、内容管理前端页面约30个。它不需要支撑百万QPS也不需要复杂的领域事件总线。Express单进程连接池的模式配合PM2集群轻松扛住日均5万PV的内部系统。强行上Nest就像用航空母舰去运一车白菜——不是不行但调度、维护、人员培训的成本远超收益。所以我的结论很明确Express不是“过时”而是“精准”。它像一把瑞士军刀没有花哨的激光瞄准器但每把小刀都磨得锋利用起来顺手坏了也好修。本项目所有技术决策都围绕“让开发者把注意力集中在业务逻辑上而不是框架机制上”这一核心展开。2.2 数据库为什么选MySQL而不是MongoDB或SQLite后台管理系统的核心是“结构化数据管理”用户有固定字段用户名、密码哈希、邮箱、状态、创建时间菜单有父子关系id、parent_id、name、path、sort_order操作日志有强时间序列user_id、action、target_id、ip、created_at。这些天然契合关系型数据库的范式设计。MongoDB的陷阱它灵活可以动态加字段但代价是“灵活到失控”。比如用户表初期只有username/password后来加了“部门ID”、“职级”、“头像URL”再后来又要支持“多租户”每个字段的类型、是否必填、索引策略全靠开发者自觉。而MySQL用DDL语句明确定义ALTER TABLE user ADD COLUMN dept_id INT NOT NULL DEFAULT 0 AFTER password;执行完立刻生效所有应用层代码必须适配否则启动就报错。这种“强约束”恰恰是后台系统稳定性的基石。SQLite的局限性它轻量、免安装、单文件非常适合本地开发或嵌入式设备。但后台管理系统是多人协作、多实例部署的。SQLite的锁机制是文件级的当两个请求同时更新同一张表一个会阻塞另一个高并发下响应时间飙升。而MySQL的行级锁、连接池、主从复制是为这种场景量身定制的。连接池的必要性Node.js是单线程异步I/O但数据库连接是昂贵资源。如果每次请求都新建连接、查询、关闭MySQL服务器很快就会被撑爆默认最大连接数151。Express项目里我们用mysql2包创建一个全局连接池const mysql require(mysql2/promise); const pool mysql.createPool({ host: process.env.DB_HOST || localhost, user: process.env.DB_USER || root, password: process.env.DB_PASSWORD || , database: process.env.DB_NAME || admin_system, waitForConnections: true, connectionLimit: 10, // 关键限制最大并发连接数 queueLimit: 0 // 0表示无上限排队但生产环境建议设为小值防雪崩 });这个connectionLimit: 10不是随便写的。我们按经验公式估算假设单次数据库操作平均耗时50msNode.js单线程每秒最多处理20个请求1000ms / 50ms10个连接池刚好覆盖峰值流量再多就是资源浪费。这个数字会在后续压测环节被反复验证和调整。2.3 鉴权方案为什么选JWT而不是Session传统Session依赖服务端存储内存、Redis客户端只存一个session_id。JWT则是把用户身份信息user_id、role、exp过期时间加密签名后直接存在客户端通常是localStorage或httpOnly cookie服务端每次只校验签名和有效期不查数据库。无状态优势后台管理系统未来可能横向扩展为多台服务器。Session方案下你必须引入Redis做共享存储否则用户A在服务器1登录下次请求被负载均衡到服务器2就变成未登录状态。JWT天然无状态任何一台服务器都能独立完成鉴权架构更简单运维成本更低。移动端友好性虽然当前是Web后台但公司App的管理后台API很可能复用同一套后端。App无法像浏览器那样自动携带Cookie而JWT只需在HTTP Header里加Authorization: Bearer token通用性极强。安全性实操要点JWT不是银弹。常见误区是把敏感信息如密码、手机号放进payload。正确做法是只放最小必要信息{ userId: 123, role: admin, iat: 1712345678, exp: 1712432078 }。exp设为2小时强制用户定期刷新iatissued at用于防止重放攻击密钥process.env.JWT_SECRET必须足够长推荐32位以上随机字符串且绝不能硬编码在代码里。我们会在“环境变量配置”章节详细说明如何安全管理。提示JWT的“无状态”是把双刃剑。它无法像Session那样主动失效比如管理员踢出某用户。解决方案是维护一个“黑名单Token表”在登出或修改密码时把当前token的jti唯一标识插入表中鉴权中间件额外查询此表。本项目为简化暂不实现但在“可扩展性设计”部分会给出具体SQL和代码片段。3. 核心模块拆解与实操细节3.1 目录结构设计为什么这样分而不是其他方式一个混乱的目录结构是项目后期维护噩梦的起点。我见过太多项目routes/下面全是user.js、role.js、menu.js而对应的models/里又是user.js、role.jscontrollers/里还是user.js……改一个用户逻辑要跳转5个文件。本项目采用“功能域划分”Feature-based Organization目录如下├── config/ # 配置文件数据库、JWT、日志等 ├── models/ # 数据访问层纯SQL操作不包含业务逻辑 │ ├── index.js # 导出所有model统一管理连接池 │ ├── user.js # 用户相关SQLcreate, findByUsername, updatePassword... │ └── role.js # 角色相关SQL ├── controllers/ # 控制器处理HTTP请求调用model返回JSON或渲染页面 │ ├── authController.js # 登录、登出、刷新token │ ├── userController.js # 用户列表、新增、编辑、删除 │ └── menuController.js # 菜单树、权限分配 ├── middleware/ # 中间件鉴权、错误处理、日志记录 │ ├── auth.js # JWT校验中间件 │ ├── errorHandler.js # 统一错误响应格式 │ └── logger.js # 请求日志method, url, status, time ├── routes/ # 路由定义纯粹的URL映射不写任何逻辑 │ ├── index.js # 主路由入口 │ ├── auth.js # /api/auth/* 路由 │ └── admin.js # /api/admin/* 路由需鉴权 ├── views/ # EJS模板仅负责HTML结构不写JS逻辑 │ ├── layout.ejs # 公共布局header, sidebar, footer │ ├── login.ejs # 登录页 │ └── admin/ # 后台页面 │ ├── index.ejs # 首页仪表盘 │ └── user/ # 用户管理页 ├── public/ # 静态资源CSS、JS、图片前端打包后放这里 ├── app.js # Express应用入口初始化、中间件加载、路由挂载 └── server.js # 服务启动端口监听、错误捕获、优雅退出为什么这样设计关注点分离models/只管“怎么查数据库”controllers/只管“收到请求后怎么做”routes/只管“哪个URL对应哪个controller”。修改用户密码逻辑只动models/user.jsSQL和controllers/userController.js调用逻辑routes/和middleware/完全不动。可测试性controllers/函数接收req, res, next你可以用jest或supertest模拟请求直接测试函数输出无需启动整个服务器。例如测试登录test(should return 200 and token on valid login, async () { const req { body: { username: admin, password: 123456 } }; const res { status: jest.fn(), json: jest.fn() }; await authController.login(req, res); expect(res.status).toHaveBeenCalledWith(200); expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ token: expect.stringMatching(/^eyJ/) })); });避免循环依赖models/index.js统一导出controllers/通过const { User } require(../models);引入而不是const User require(../models/user);。这样当user.js需要调用role.js的函数时不会因为role.js又引入user.js而形成死循环。注意不要为了“绝对规范”而过度分层。我见过有项目把dto/数据传输对象、service/业务服务层、repository/仓储层全配齐结果一个简单的用户列表接口要写6个文件。本项目坚持“够用就好”controllers/里可以包含简单业务逻辑如密码加密只有当逻辑复杂到需要复用如“发送邮件通知”被多个controller调用才单独提成services/。3.2 用户认证模块从密码存储到JWT签发的完整链路后台管理系统的安全基石是用户认证。这里没有捷径每一步都必须严谨。第一步密码安全存储永远不要用MD5或SHA1它们是快速哈希GPU暴力破解几秒就能搞定。Node.js原生crypto模块提供scrypt它是内存密集型算法能有效抵御ASIC矿机攻击。const crypto require(crypto); // 注册时生成密码哈希 function hashPassword(password) { const salt crypto.randomBytes(16).toString(hex); // 16字节盐值 const hash crypto.scryptSync(password, salt, 64, { N: 1024, r: 8, p: 1 }); // N1024是关键提高计算成本 return ${salt}:${hash.toString(hex)}; } // 登录时验证 function verifyPassword(password, storedHash) { const [salt, hash] storedHash.split(:); const newHash crypto.scryptSync(password, salt, 64, { N: 1024, r: 8, p: 1 }); return crypto.timingSafeEqual(Buffer.from(hash), newHash); // 防止时序攻击 }N: 1024不是越大越好。我实测过N: 16384会让单次验证耗时从120ms升到1.8s用户体验断崖式下跌。N: 1024是安全性和响应速度的黄金平衡点。第二步JWT Token生成与返回登录成功后不是返回明文密码而是生成JWTconst jwt require(jsonwebtoken); function generateToken(userId, role) { return jwt.sign( { userId, role, iat: Math.floor(Date.now() / 1000) }, process.env.JWT_SECRET, { expiresIn: 2h } ); } // 在authController.login中 exports.login async (req, res) { try { const { username, password } req.body; const user await User.findByUsername(username); if (!user || !verifyPassword(password, user.password_hash)) { return res.status(401).json({ error: 用户名或密码错误 }); } const token generateToken(user.id, user.role); // 设置HttpOnly Cookie防止XSS窃取 res.cookie(token, token, { httpOnly: true, secure: process.env.NODE_ENV production, // 生产环境强制HTTPS sameSite: strict, // 防止CSRF maxAge: 2 * 60 * 60 * 1000 // 2小时 }); res.json({ success: true, message: 登录成功, user: { id: user.id, username: user.username, role: user.role } }); } catch (err) { res.status(500).json({ error: 服务器错误 }); } };sameSite: strict是关键。它确保浏览器只在同站请求即地址栏URL和请求发起页域名完全一致时才发送Cookie。即使你的后台有XSS漏洞攻击者也无法诱导用户点击恶意链接来窃取token因为跨站请求不会携带Cookie。第三步前端如何安全使用Token很多新手把token存在localStorage这是巨大风险。XSS脚本可以轻易读取localStorage。正确做法是后端设置httpOnly: true的Cookie前端JS无法读取前端所有API请求由浏览器自动携带此Cookie前端路由守卫如Vue Router的beforeEach检查document.cookie里是否有token没有则跳转登录页登录页表单提交用form methodPOST不走AJAX避免CORS和CSRF Token管理的复杂性。3.3 权限控制模块RBAC模型的轻量级落地后台系统的核心需求是“不同角色看到不同菜单、操作不同数据”。我们采用经典RBAC基于角色的访问控制但不做过度设计。数据库表设计精简版-- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, role_id INT NOT NULL DEFAULT 1, -- 外键指向role表 status TINYINT NOT NULL DEFAULT 1 -- 1:启用, 0:禁用 ); -- 角色表 CREATE TABLE role ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, -- 管理员, 编辑, 审核员 description VARCHAR(200) ); -- 菜单表支持无限级 CREATE TABLE menu ( id INT PRIMARY KEY AUTO_INCREMENT, parent_id INT DEFAULT 0, -- 0表示顶级菜单 name VARCHAR(50) NOT NULL, path VARCHAR(100), -- 前端路由路径如 /admin/user icon VARCHAR(50), -- 图标class如 el-icon-user sort_order INT DEFAULT 0, is_visible TINYINT DEFAULT 1 -- 是否在菜单栏显示 ); -- 角色-菜单关联表权限分配 CREATE TABLE role_menu ( role_id INT NOT NULL, menu_id INT NOT NULL, PRIMARY KEY (role_id, menu_id) );注意menu.path字段。它不是后端API路径而是前端Vue Router的路径。后端只负责提供“当前用户有哪些菜单项”前端根据path动态渲染侧边栏。这样前后端彻底解耦后端不关心前端用什么框架。权限中间件实现// middleware/auth.js exports.requireAuth async (req, res, next) { try { const token req.cookies.token; if (!token) { return res.status(401).json({ error: 请先登录 }); } const decoded jwt.verify(token, process.env.JWT_SECRET); // 查询用户角色和菜单权限 const user await User.findById(decoded.userId); if (!user || user.status ! 1) { return res.status(401).json({ error: 用户不存在或已被禁用 }); } // 将用户信息挂载到req上后续controller可直接使用 req.currentUser { id: user.id, username: user.username, role: user.role_name, // 从role表JOIN过来 menus: await Menu.findByRoleId(user.role_id) // 返回菜单树结构 }; next(); } catch (err) { if (err.name TokenExpiredError) { return res.status(401).json({ error: 登录已过期请重新登录 }); } res.status(401).json({ error: 登录凭证无效 }); } }; // routes/admin.js - 所有后台API都走这个中间件 const router require(express).Router(); const { requireAuth } require(../middleware/auth); const userController require(../controllers/userController); router.use(requireAuth); // 全局应用鉴权 router.get(/users, userController.list); // GET /api/admin/users router.post(/users, userController.create); // POST /api/admin/users router.put(/users/:id, userController.update); // PUT /api/admin/users/123 router.delete(/users/:id, userController.delete); // DELETE /api/admin/users/123 module.exports router;requireAuth中间件在next()前已经把req.currentUser.menus准备好。userController.list里可以直接用req.currentUser.role判断是否允许查看敏感字段如用户手机号无需每个接口都重复查库。4. 实操过程与关键环节实现4.1 环境变量与配置管理从开发到生产的无缝切换硬编码数据库密码、JWT密钥是大忌。Node.js项目必须用环境变量。但process.env只是个对象如何让它既安全又易用标准做法使用.env文件 dotenv包安装npm install dotenv在项目根目录创建.env文件NODE_ENVdevelopment PORT3000 DB_HOSTlocalhost DB_USERroot DB_PASSWORDyour_password DB_NAMEadmin_system JWT_SECRET32_byte_random_string_here_abcdefghijklmnopqrstuvwxyz123456在app.js最顶部加载if (process.env.NODE_ENV ! production) { require(dotenv).config(); // 只在非生产环境加载 }生产环境安全加固.env文件绝不能提交到Git必须加入.gitignore# .gitignore .env .env.local node_modules/ dist/生产服务器上不依赖.env文件而是由运维通过系统环境变量注入# Linux服务器上 export NODE_ENVproduction export PORT8080 export DB_HOST10.0.1.100 export DB_PASSWORDprod_db_password_very_strong # 然后启动 node server.js这样.env只用于本地开发生产环境由基础设施保障密钥永不落地代码库。配置文件分层config/目录下按环境分文件config/ ├── default.js # 所有环境共用的默认配置 ├── development.js # 开发环境特有如开启日志详细级别 └── production.js # 生产环境特有如关闭EJS缓存default.js示例module.exports { port: process.env.PORT || 3000, jwt: { secret: process.env.JWT_SECRET, expiresIn: 2h }, db: { host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, connectionLimit: parseInt(process.env.DB_CONNECTION_LIMIT) || 10 } };app.js中合并配置const env process.env.NODE_ENV || development; const config require(./config/default); if (env ! default) { const envConfig require(./config/${env}); module.exports { ...config, ...envConfig }; }4.2 错误处理与日志记录让Bug无处遁形Express默认的错误处理很粗糙。一个500错误前端只看到{error:Internal Server Error}后端控制台可能连堆栈都没有。我们必须建立三层防御第一层全局未捕获异常// server.js process.on(uncaughtException, (err) { console.error(【致命错误】未捕获异常:, err); console.error(err.stack); // 记录到文件或发送告警 process.exit(1); // 立即退出避免状态不一致 }); process.on(unhandledRejection, (reason, promise) { console.error(【致命错误】未处理Promise拒绝:, reason); console.error(拒绝的Promise:, promise); });第二层中间件统一错误处理// middleware/errorHandler.js exports.handler (err, req, res, next) { console.error(【请求错误】${req.method} ${req.url}, err); // 记录到控制台 // 记录到日志文件生产环境 if (process.env.NODE_ENV production) { const logEntry [${new Date().toISOString()}] ${req.method} ${req.url} ${err.status || 500} ${err.message}\n; fs.appendFile(./logs/error.log, logEntry, (e) { if (e) console.error(写入错误日志失败:, e); }); } // 统一响应格式 res.status(err.status || 500).json({ success: false, error: err.message || 服务器内部错误, timestamp: new Date().toISOString() }); }; // 在app.js中挂载必须放在所有路由之后 app.use(errorHandler.handler);第三层业务逻辑中的显式错误抛出// controllers/userController.js exports.create async (req, res) { try { const { username, password } req.body; if (!username || !password) { throw new Error(用户名和密码不能为空); // 抛出业务错误 } if (username.length 3) { throw new Error(用户名至少3个字符); // 抛出业务错误 } const hashedPassword hashPassword(password); const user await User.create({ username, password_hash: hashedPassword }); res.status(201).json({ success: true, data: user }); } catch (err) { // 捕获数据库唯一索引冲突等底层错误 if (err.code ER_DUP_ENTRY) { throw new Error(用户名已存在); } throw err; // 重新抛出交给全局中间件处理 } };这样所有错误最终都流向errorHandler前端得到一致的JSON响应后端有完整的日志追踪。4.3 部署与进程管理从本地运行到线上稳定开发完怎么让别人也能用不能让人人都装Node.js、MySQL、手动npm start。方案一PM2推荐给中小项目PM2是Node.js最成熟的进程管理器支持集群、监控、日志、开机自启。全局安装npm install pm2 -g启动应用pm2 start server.js --name admin-system查看状态pm2 list查看日志pm2 logs admin-system重启pm2 restart admin-system开机自启pm2 startup然后pm2 save关键配置ecosystem.config.jsmodule.exports { apps: [{ name: admin-system, script: ./server.js, instances: 2, // 启动2个实例利用多核CPU exec_mode: cluster, watch: false, // 生产环境关闭文件监听避免热重载 env: { NODE_ENV: production, PORT: 8080 }, env_production: { NODE_ENV: production, PORT: 8080 } }] };然后用pm2 start ecosystem.config.js --env production启动。方案二Docker推荐给需要标准化交付的团队Docker镜像保证“一次构建到处运行”。编写DockerfileFROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --onlyproduction COPY . . EXPOSE 3000 CMD [npm, start]构建镜像docker build -t admin-system .运行容器docker run -d -p 8080:3000 --name admin -e NODE_ENVproduction admin-system反向代理NginxNode.js不适合直接暴露给公网。用Nginx做反向代理处理HTTPS、静态资源、负载均衡。 Nginx配置示例/etc/nginx/conf.d/admin.confupstream node_backend { server 127.0.0.1:3000; server 127.0.0.1:3001; # 如果PM2开了2个实例 } server { listen 80; server_name admin.yourdomain.com; return 301 https://$server_name$request_uri; # 强制HTTPS } server { listen 443 ssl http2; server_name admin.yourdomain.com; ssl_certificate /path/to/fullchain.pem; ssl_certificate_key /path/to/privkey.pem; location / { proxy_pass http://node_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 静态资源直接由Nginx服务不走Node.js location /public/ { alias /app/public/; } }这样Nginx处理SSL卸载和静态文件Node.js专注业务逻辑整体性能提升30%以上。5. 常见问题与排查技巧实录5.1 “登录成功但后续请求一直401未授权” —— Cookie跨域与SameSite问题现象前端在http://localhost:8080登录后端在http://localhost:3000登录接口返回200并设置了tokenCookie但紧接着的/api/admin/users请求后端收不到Cookie始终401。原因分析浏览器的Cookie策略。localhost:3000设置的Cookie默认domain是localhost:3000而localhost:8080发起的请求浏览器认为是跨域不会携带此Cookie。这是开发环境的典型陷阱。解决方案方案A推荐前后端同域开发前端用Webpack DevServer的proxy把/api请求代理到http://localhost:3000// vue.config.js devServer: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true, pathRewrite: { ^/api: } } } }这样前端请求/api/auth/login实际发到http://localhost:3000/auth/loginCookie的domain自然匹配。方案B显式设置Cookie domain后端设置Cookie时指定domain: localhost去掉端口res.cookie(token, token, { httpOnly: true, secure: false, sameSite: lax, // 改为lax允许同站POST请求携带 domain: localhost // 关键 });但domain: localhost在Chrome 116被废弃不推荐长期使用。排查技巧打开浏览器开发者工具 → Application → Cookies看localhost:3000下是否有tokenCookie。如果没有说明登录接口根本没设置成功如果有再看Network里每个请求的Request Headers确认Cookie字段是否出现tokenxxx。5.2 “MySQL连接池报错Cannot enqueue Handshake after already enqueuing a Handshake” —— 连接泄漏现象系统运行一段时间后所有数据库操作变慢最终报错Cannot enqueue Handshake...日志里大量ER_CON_COUNT_ERROR。根本原因连接泄漏Connection Leak。某个地方获取了连接但没有正确释放connection.release()或pool.end()导致连接池里的连接被耗尽。高频泄漏点忘记await在async函数里调用pool.getConnection()后如果忘了await会立即执行后面的connection.query()此时connection还是Promise对象导致异常而finally里的connection.release()永远不会执行。// ❌ 错误漏了await const connection pool.getConnection(); // 这里返回Promise不是connection对象 const [rows] await connection.query(SELECT * FROM user); // 这里会报错 // ✅ 正确 const connection await pool.getConnection(); try { const [rows] await connection.query(SELECT * FROM user); } finally { connection.release(); // 必须放finally确保释放 }异常分支未释放try/catch里catch块没有connection.release()。// ❌ 错误 const connection await pool.getConnection(); try { const [rows] await connection.query(SELECT * FROM user); } catch (err) { console.error(err); // 忘了release } // ✅ 正确 const connection await pool.getConnection(); try { const [rows] await connection.query(SELECT * FROM user); } catch (err) { console.error(err); }
阅读完成 · 觉得有帮助?
咨询建站