ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Express + Sequelize + MySQL:从本地开发到Docker部署的实战指南

Express + Sequelize + MySQL:从本地开发到Docker部署的实战指南 从零搭一套 Express Sequelize MySQL 的 API 服务听起来是一件很成熟的事但真正动手时版本兼容、容器网络、SSL 连接、连接池配置这一类问题能把人磨到怀疑人生。我最近刚把一个内部项目从本地开发推到 Docker 部署链路不算复杂但涉及的点很全项目初始化、数据建模、接口编写、容器编排。整个过程里踩了不少坑有些问题在文档里查半天也未必能找到直接答案所以干脆把自己的实操路线整理成一篇能照着做的记录。这篇内容对刚接触 Node 全栈的开发者会比较友好如果你正准备从“能跑”迈向“能部署”或者已经被 Sequelize 关联查询、MySQL 容器启动失败这类问题卡住那这篇应该能帮你省下不少时间。1. 为什么是这套组合Express Sequelize MySQL 的取舍逻辑1.1 这套技术栈解决的核心问题项目一开始团队里有人提议用 Prisma有人想直接上 Fastify还有人建议把数据库换成 PostgreSQL。讨论半天最终定的还是 Express Sequelize MySQL。不是因为这套组合最时髦而是因为它在“快速交付”和“长期可维护”之间拿到了最好的平衡点。一个典型的业务 API 项目要解决的无非是这几件事接收 HTTP 请求、校验参数、读写数据库、返回统一格式的响应。Express 负责前端的请求处理Sequelize 负责把 JavaScript 对象映射成 SQLMySQL 负责真正的数据存储。这个分工清晰到每个环节都有独立的开源生态支撑。团队里新来的同事学过 Node 基础就能上手不像某些全栈框架需要同时理解约定、目录规范、插件机制学习成本直接拉满。1.2 Express 依然稳妥但要注意版本差异Express 4 是目前存量最大的版本文档、中间件、插件几乎全兼容。但我建议新项目直接看 Express 5它在 2024 年成为正式版最大的变化是 Promise 链路的错误处理异步中间件抛出的异常能直接交给错误处理函数不需要再包一层asyncHandler。// Express 4 时代常见的写法 app.get(/users, async (req, res, next) { try { const users await User.findAll(); res.json(users); } catch (err) { next(err); } }); // Express 5 可以直接这样写 app.get(/users, async (req, res) { const users await User.findAll(); res.json(users); });Express 5 的异步错误会自动进入错误处理中间件代码确实干净不少。不过要注意Express 5 的路由通配符写法改了原来app.get(*)这种写法要换成app.get(/*splat)网上很多老教程里的代码直接搬过来会报错。如果只是为了稳定上线用 Express 4 也完全没问题关键是全项目统一。1.3 Sequelize 的定位够用且可控的 ORMSequelize 经常被拿去和 Prisma 比。Prisma 的类型安全和优雅的客户端确实很吸引人但它有一个绕不开的问题生成客户端和迁移流程需要单独的 CLI 工具链团队协作时容易因为解析版本不一致出现本地能跑、部署就报错的情况。Sequelize 更“传统”它对 SQL 的抽象没有 Prisma 那么深但好处是模型、查询、事务都建立在大家都熟悉的 SQL 语义上。另外 Sequelize 的迁移Migration机制写得比较完整上线之后如果改了字段可以通过迁移脚本平滑变更适合有明确发布流程的业务系统。而 MySQL 搭配 Sequelize 的生态匹配度很高MySQL 8.0 的 JSON 字段、窗口函数、事务隔离级别Sequelize 都覆盖得不错。1.4 MySQL 才不是什么老古董数据库选型上MySQL 被很多新项目嫌弃“太传统”但在标准 Web 业务里它依然是最可靠的选择。MySQL 8.0 原生支持窗口函数、公共表达式处理报表类查询不比 PostgreSQL 差多少运维资料又多用 Docker 部署时踩坑也有大量前人的解决方案。相比之下 PostgreSQL 的功能确实更多但团队如果只有 MySQL 经验纯粹为了一个列表查询接口去切换数据库得不偿失。2. 环境准备阶段MySQL 安装与 Docker Desktop 那些坑2.1 本地直接安装 MySQL 的完整流程如果你的开发机是 Windows 或 macOS第一步是装 MySQL。这里有个很常见的误区直接去官网下载最新的 MySQL Installer结果被一堆组件搞晕。实际只需要 MySQL Server 和 MySQL Workbench图形化工具两个组件。Windows 下安装时安装类型建议选 Server only后续配置步骤里记得选 MySQL 8.0.x 版本字符集务必选择utf8mb4。为什么不用默认的utf8mb3因为utf8mb3就是旧版的utf8它不支持完整 emoji 存储也不支持某些生僻汉字。现在接口里随便一个用户昵称可能就带个表情符号用旧字符集存进去直接报Incorrect string value。macOS 用户可以用 Homebrew 安装这个比较快brew install mysql8.0 brew services start mysql8.0装完之后默认的 root 用户没有密码第一次进 MySQL 需要设置密码mysql -u root ALTER USER rootlocalhost IDENTIFIED BY 你的密码; FLUSH PRIVILEGES;这里有一个很多新手会踩的点本地开发时 Sequelize 连接 MySQLhost 用127.0.0.1而不是localhost。原因是 Node.js 里localhost可能被解析成 IPv6 的::1而 MySQL 默认监听 IPv4结果是连接直接拒绝。这个问题我至少见过三个同事问过报错信息类似ECONNREFUSED 127.0.0.1:3306折腾半天最后发现是 host 写错了。2.2 Docker Desktop 启动失败从 virtualization 到 WSL2很多项目最后要交给 Docker 部署所以开发环境索性直接用 Docker 跑 MySQL。这也省去了本地安装 MySQL 的麻烦。但在 Windows 上Docker Desktop 第一步就可能卡住最常见的就是启动时报错Virtualization support not detected。这个问题的根源是 Windows 的虚拟化功能没开。解决方法按顺序排查打开任务管理器在“性能”标签页确认“虚拟化”状态如果是“已禁用”需要进 BIOS/UEFI 开启 VT-x 或 AMD-V。确认“适用于 Linux 的 Windows 子系统”功能已启用PowerShell 管理员模式下执行确保 Hyper-V 平台已启用这一步在“启用或关闭 Windows 功能”里勾选。dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart改完必须重启电脑之后再启动 Docker Desktop 就正常了。如果还报错可以改用 WSL2 后端。Docker Desktop 的 Settings 里有 “Use the WSL 2 based engine” 选项大多数现代 Windows 项目推荐直接开启。2.3 用 Docker 跑 MySQL 8.0 的容器化开发环境开发阶段我把 MySQL 跑在容器里这样团队每个人的数据库版本、字符集配置完全一致再也不会出现“我本地能跑你本地不能跑”的甩锅现场。启动命令很简单docker run -d \ --name dev-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDrootpass \ -e MYSQL_DATABASEapi_demo \ -v mysql_data:/var/lib/mysql \ mysql:8.0注意-e MYSQL_DATABASEapi_demo这个参数它会在容器首次启动时自动创建一个空数据库省得后面再手动执行 CREATE DATABASE。数据卷mysql_data用来持久化数据否则容器一删除数据全没了。提示开发阶段的密码直接用 rootpass 没问题但项目接近上线前一定要换成强密码并且考虑单独建应用账号而不是让 Node 应用直接用 root 账号连接数据库。3. 项目初始化与 Sequelize 建模把表结构设计落地3.1 目录结构直接决定后期维护成本我见过很多人把所有代码堆在app.js里三五个接口还好加到二十个接口就没法看了。我的建议是项目一开始就按模块分目录按下面的结构搭。api-demo/ ├── src/ │ ├── config/ │ │ └── config.js │ ├── models/ │ │ ├── index.js │ │ ├── user.js │ │ └── post.js │ ├── routes/ │ │ ├── index.js │ │ ├── userRoutes.js │ │ └── postRoutes.js │ ├── controllers/ │ │ ├── userController.js │ │ └── postController.js │ ├── middlewares/ │ │ ├── errorHandler.js │ │ └── auth.js │ ├── app.js │ └── server.js ├── migrations/ ├── seeders/ ├── .env ├── .env.example ├── package.json └── Dockerfile这个结构把“路由”“控制器”“模型”拆开属于 Node 生态里经典的 MVC 演化版。Routes 只负责定义 URL 到处理函数的映射Controller 里放业务逻辑Model 里定义数据结构。这样后续加字段、加接口、改业务逻辑改动范围都限制在对应模块内不会牵一发动全身。3.2 初始化 Sequelize 的完整步骤先初始化 npm 项目并安装依赖npm init -y npm install express sequelize mysql2 dotenv npm install --save-dev sequelize-cli nodemon注意 mysql2 是 Sequelize 连接 MySQL 时必需的驱动默认的 mysql 包也存在兼容问题直接选mysql2是社区共识。接着在package.json里把 sequelize-cli 的脚本配上scripts: { dev: nodemon src/server.js, start: node src/server.js, db:migrate: sequelize-cli db:migrate, db:seed: sequelize-cli db:seed:all }然后初始化 Sequelize 骨架npx sequelize-cli init这个命令会生成config/config.json、models、migrations、seeders四个目录。但麻烦的地方在于默认生成的 config.json 是明文密码而且通常希望用环境变量区分本地和线上所以直接把 config.json 换成 JS 配置读取.env文件。// src/config/config.js require(dotenv).config(); module.exports { development: { username: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, host: process.env.DB_HOST, port: process.env.DB_PORT || 3306, dialect: mysql, timezone: 08:00 }, test: { // 和 development 类似可单独配置 }, production: { // 生产环境建议 use_env_variable 或这里同样读取 .env } };3.3 模型定义用户和文章的例子我用一个最典型的“用户-文章”模型来做示例因为文章列表、作者关联、按状态筛选这些场景几乎覆盖了大多业务 API 的基本需求。// src/models/user.js const { DataTypes } require(sequelize); module.exports (sequelize) { const User sequelize.define(User, { id: { type: DataTypes.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true }, username: { type: DataTypes.STRING(50), allowNull: false, unique: true }, email: { type: DataTypes.STRING(100), allowNull: false, unique: true, validate: { isEmail: true } }, status: { type: DataTypes.ENUM(active, inactive, banned), defaultValue: active }, lastLoginAt: { type: DataTypes.DATE, allowNull: true } }, { tableName: users, underscored: true }); User.associate (models) { User.hasMany(models.Post, { foreignKey: userId, as: posts }); }; return User; };// src/models/index.js const fs require(fs); const path require(path); const { Sequelize, DataTypes } require(sequelize); const basename path.basename(__filename); const env process.env.NODE_ENV || development; const config require(../config/config)[env]; const sequelize new Sequelize( config.database, config.username, config.password, config ); const models {}; fs.readdirSync(__dirname) .filter(file file.indexOf(.) ! 0 file ! basename file.endsWith(.js)) .forEach(file { const model require(path.join(__dirname, file))(sequelize, DataTypes); models[model.name] model; }); Object.values(models).forEach(model { if (model.associate) model.associate(models); }); models.sequelize sequelize; models.Sequelize Sequelize; module.exports models;这个index.js是 sequelize-cli 自动生成的模板够用。你的应用入口只需要const { User, Post } require(./models)就能拿到所有模型和关联关系。3.4 迁移脚本让表结构可演进模型定义只是 Sequelize 的“蓝图”要让表真正出现在数据库里需要跑迁移。迁移的意义在于任何一次表结构变更都有记录、可回滚这才是多人协作的底气来源。npx sequelize-cli model:generate --name Post --attributes title:string,content:text,userId:integer,status:string生成的文件在 migrations 目录里属于新生成的Post表骨架。实际开发中我会手动调整字段约束把 userId 加上索引把 content 的类型改成 TEXT 而不是默认的 STRINGuse strict; module.exports { async up(queryInterface, Sequelize) { await queryInterface.createTable(posts, { id: { type: Sequelize.INTEGER.UNSIGNED, autoIncrement: true, primaryKey: true }, title: { type: Sequelize.STRING(200), allowNull: false }, content: { type: Sequelize.TEXT, allowNull: false }, user_id: { type: Sequelize.INTEGER.UNSIGNED, allowNull: false, references: { model: users, key: id }, onDelete: CASCADE, onUpdate: CASCADE }, status: { type: Sequelize.ENUM(published, draft, archived), defaultValue: draft }, created_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.literal(CURRENT_TIMESTAMP) }, updated_at: { type: Sequelize.DATE, allowNull: false, defaultValue: Sequelize.literal(CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP) } }); }, async down(queryInterface, Sequelize) { await queryInterface.dropTable(posts); } };这里的关键点有两个。一个是表名字段默认会用下划线命名user_id、created_at因为模型里配了underscored: true这避免了 JS 的 camelCase 和 SQL 的下划线风格来回切换时的混乱。另一个是allowNull: false结合references建立外键约束Sequelize 的关联查询才能用到数据库级的外键保障。4. API 设计与中间件链路从 CRUD 到事务控制4.1 Express 应用入口的搭法// src/app.js const express require(express); const cors require(cors); const helmet require(helmet); const routes require(./routes); const errorHandler require(./middlewares/errorHandler); const app express(); app.use(helmet()); app.use(cors({ origin: process.env.CORS_ORIGIN ? process.env.CORS_ORIGIN.split(,) : * })); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(/api, routes); app.use(errorHandler); module.exports app;// src/server.js const app require(./app); const { sequelize } require(./models); const PORT process.env.PORT || 3000; async function start() { try { await sequelize.authenticate(); console.log(数据库连接成功); } catch (err) { console.error(数据库连接失败, err.message); process.exit(1); } app.listen(PORT, () { console.log(API 服务已启动http://localhost:${PORT}); }); } start();helmet 用来设置安全响应头属于上线必须的东西。CORS 在开发和部署阶段分别配置开发环境直接*部署环境用域名白名单。常有新手把 CORS 配置问题当成后端 bug 排查其实前端浏览器控制台的CORS policy报错已经写得很明白了回到服务端把origin配置好就行。4.2 错误处理中间件把异常信息控制在内部统一错误处理是 API 设计里最容易被忽略、但直接影响联调效率的部分。不统一的话MySQL 报错可能把 SQL 语句和字段名直接吐给前端既不安全也不友好。我会写一个中间件专门处理。// src/middlewares/errorHandler.js module.exports function errorHandler(err, req, res, next) { let statusCode err.status || 500; let message err.message || 服务器内部错误; // Sequelize 校验错误 if (err.name SequelizeValidationError) { statusCode 400; message err.errors.map(e e.message).join(); } // Sequelize 唯一约束冲突 if (err.name SequelizeUniqueConstraintError) { statusCode 409; message 数据冲突当前字段的值已存在; } // 参数校验失败来自 express-validator if (err.type validation) { statusCode 400; message err.message; } res.status(statusCode).json({ success: false, message, data: null }); if (statusCode 500) { console.error([${new Date().toISOString()}], err.stack); } };4.3 路由和控制器以文章接口为例Routes 里只写路径和控制器方法。// src/routes/postRoutes.js const express require(express); const router express.Router(); const postController require(../controllers/postController); router.get(/, postController.list); router.get(/:id, postController.getById); router.post(/, postController.create); router.put(/:id, postController.update); router.delete(/:id, postController.remove); module.exports router;Controller 里是实际业务逻辑。// src/controllers/postController.js const { Post, User } require(../models); exports.list async (req, res, next) { try { const page parseInt(req.query.page) || 1; const pageSize parseInt(req.query.pageSize) || 10; const status req.query.status; const where {}; if (status) where.status status; const { rows, count } await Post.findAndCountAll({ where, include: [ { model: User, as: user, attributes: [id, username] } ], order: [ [status, ASC], [createdAt, DESC] ], limit: pageSize, offset: (page - 1) * pageSize, distinct: true }); res.json({ success: true, data: rows, meta: { page, pageSize, total: count, totalPages: Math.ceil(count / pageSize) } }); } catch (err) { next(err); } };关于排序这里多说一句。很多人用 Sequelize 时order参数只会写一列我习惯加二级排序比如上面先按状态升序、再按创建时间降序。这样“已发布”的永远排在“草稿”前面同状态内又是最新优先。效果等同于 SQL 里的ORDER BY status ASC, created_at DESC。include的attributes一定要做投影只取前端真正需要的字段。如果不加这个限制联表查询会把用户密码哈希也带出来这是非常低级但常见的泄露姿势。4.4 Sequelize 事务处理订单与库存的那个经典场景事务在涉及金额、库存这类业务时是底线绝对不能省。比如下订单时既要创建订单记录又要扣减库存这两步必须在一个事务里任何一步失败都要整体回滚。const { sequelize, Order, Inventory, OrderItem } require(../models); exports.createOrder async (req, res, next) { const t await sequelize.transaction(); try { const { userId, items } req.body; // 1. 创建订单主表 const order await Order.create({ userId, status: pending, totalAmount: 0 }, { transaction: t }); let total 0; // 2. 逐个写入订单明细并扣减库存 for (const item of items) { const goods await Inventory.findOne({ where: { id: item.goodsId }, lock: t.LOCK.UPDATE, transaction: t }); if (!goods || goods.stock item.quantity) { throw new Error(商品 ${item.goodsId} 库存不足); } await Inventory.update( { stock: goods.stock - item.quantity }, { where: { id: goods.id }, transaction: t } ); await OrderItem.create({ orderId: order.id, goodsId: goods.id, price: goods.price, quantity: item.quantity }, { transaction: t }); total goods.price * item.quantity; } await order.update({ totalAmount: total }, { transaction: t }); await t.commit(); res.json({ success: true, data: order }); } catch (err) { await t.rollback(); next(err); } };这里lock: t.LOCK.UPDATE是关键它对应 SQL 的SELECT ... FOR UPDATE。普通查询在并发场景下会出现两个请求同时读到库存为 1、同时扣减成功的超卖问题。加锁之后同一行记录在一个事务成功提交前另一个事务会阻塞等待。作为对比如果这里是纯展示型接口不需要锁一旦涉及写操作就必须考虑并发安全。5. Docker 部署从本地容器到可上线镜像的完整流程5.1 多阶段构建的 Dockerfile 细节写 Dockerfile 的目标不只是“能跑”而是把体积、安全、可维护性都兼顾。Node 项目最经典的坑是用node:latest当基础镜像结果镜像超 1GB构建又慢又占空间。# build 阶段 FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . # 生产阶段 FROM node:20-alpine WORKDIR /app ENV NODE_ENVproduction RUN addgroup -g 1001 -S nodejs adduser -S nodejs -u 1001 COPY --frombuilder --chownnodejs:nodejs /app/node_modules ./node_modules COPY --frombuilder --chownnodejs:nodejs /app/src ./src COPY --frombuilder /app/package*.json ./ COPY --frombuilder /app/.env.example ./ USER nodejs EXPOSE 3000 CMD [node, src/server.js]npm ci和npm install的区别在于前者严格按照 package-lock.json 安装不会自动升级依赖版本构建结果具备可重复性。镜像里新建一个非 root 用户运行应用避免容器进程以 root 权限运行被攻破后直接控制宿主机这是安全基线要求。5.2 docker-compose一次拉起应用和数据库实际部署时用 docker-compose 编排最顺手。一个命令同时启动 MySQL 和 Node 应用而且数据库先启动并健康检查通过后应用才开始启动。这个顺序控制很重要我第一次上手时就因为没加健康检查Node 应用启动时数据库还没就绪导致 Sequelize 连接失败容器反复重启。version: 3.8 services: db: image: mysql:8.0 container_name: api_mysql restart: always environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: ${MYSQL_DATABASE} TZ: Asia/Shanghai ports: - 3306:3306 volumes: - db_data:/var/lib/mysql - ./init:/docker-entrypoint-initdb.d healthcheck: test: [CMD, mysqladmin, ping, -h, localhost, -u, root, -p${MYSQL_ROOT_PASSWORD}] interval: 10s timeout: 5s retries: 10 start_period: 30s networks: - api_net api: build: . container_name: api_node restart: always depends_on: db: condition: service_healthy ports: - 3000:3000 environment: NODE_ENV: production DB_HOST: db DB_PORT: 3306 DB_USER: root DB_PASSWORD: ${MYSQL_ROOT_PASSWORD} DB_NAME: ${MYSQL_DATABASE} networks: - api_net volumes: db_data: networks: api_net: driver: bridgedepends_on配合condition: service_healthy这个写法是 compose 规范支持的MySQL 容器只有通过 mysqladmin ping 的检测后才算就绪Node 应用才会启动。第一次启动时 MySQL 需要初始化数据这个阶段可能持续 20-30 秒所以健康检查的start_period也要放宽。5.3 环境变量管理与密钥安全docker-compose 文件里出现明文密码是最常见的部署失误。生产环境必须用宿主机上的.env文件给 compose 提供变量而且这个.env不能提交到 git。我的做法是项目里保留.env.example作为模板实际部署时复制一份.env填上真实密码和密钥。cp .env.example .env vi .env # 填入实际值 docker-compose up -d --build再说一个联通问题容器内 Node 应用访问 MySQL 的 host 应该写 services 名db而不是127.0.0.1。因为两个容器各有独立的网络栈127.0.0.1指向的是应用容器自身连不上 MySQL 容器。这个点我接手过一个交接项目部署文档里写的DB_HOST127.0.0.1容器里怎么都连不上改回db之后立即恢复。5.4 首次部署后必须做的验证部署不是docker-compose up -d就结束了。我会按顺序做三轮验证先登录 MySQL 容器确认表结构和数据卷持久化正常docker exec -it api_mysql mysql -uroot -p然后确认应用容器日志没有报错docker logs api_node --tail 100最后用一个真实的发布接口完整走一遍curl -X POST http://localhost:3000/api/posts \ -H Content-Type: application/json \ -d {title:测试,content:部署验证,userId:1}如果接口返回成功再从宿主机访问一次 MySQL 确认记录真的写进去。到这里本地到容器化的闭环才算真正打通。6. 我整理的问题排查清单SSL、401、连接池与更多6.1 MySQL SSL 连接错误要么全用证书要么明确关闭部署后最容易遇到的连接问题就是 MySQL 8.0 默认开启 SSL而 Sequelize 默认并没有配置 CA 证书两边握手直接报错类似ER_SSL_REFUSED或SSL connection error: error:1425...。本地开发时 MySQL 往往用非 SSL 模式跑所以整个联调阶段都正常一部署到 Docker 里就出错。解决方式有两种。如果是在内网环境部署安全性已经由网络隔离保证我建议在 Sequelize 配置里直接关闭 SSLdialectOptions: { ssl: false }如果是跨公网连接或对安全合规有要求应该在服务端开启 SSL 并配置证书和客户端 CA然后在 Sequelize 里这样配置dialectOptions: { ssl: { ca: fs.readFileSync(/path/to/ca.pem), rejectUnauthorized: true } }注意千万不要把rejectUnauthorized设成false来“绕过” SSL 证书校验。这个选项一旦关闭等于完全不验证服务器身份中间人攻击可以直接截获数据库明文流量。如果连接报错先确认证书链条是否完整而不是降低安全等级。6.2 接口调用 401API Key 的排查顺序项目中接入第三方服务时我遇到过一次反复报401 Unauthorized: incorrect api key provided的情况。排查顺序其实有套路先确认 API Key 本身没复制错再确认环境变量是否真的注入到运行环境里。最常见的坑是.env文件里配了API_KEYsk-xxx但启动命令是直接node src/server.js没有加载 dotenv导致 process.env.API_KEY 是 undefined请求发送时实际携带的 key 是空字符串。排查方法很简单// 在发起请求前打印 key 的前几位 console.log(API key 前6位, process.env.API_KEY?.slice(0, 6));如果打印出来是 undefined先检查require(dotenv).config()是否在你的入口文件里最先执行。如果 key 有值但请求还报 401再检查 key 复制时是不是带了引号比如.env里写成了API_KEYsk-abcdotenv 会把引号也读进去实际发送的 key 就变成了sk-abc带双引号字符。6.3 Sequelize 连接池耗尽一个不该背锅的 MySQL 限制项目在没有配置连接池的情况下一旦并发超过一定量日志里就会出现Connection pool exhausted或Timeout acquiring a connection的报错。Sequelize 默认连接池其实是 5业务稍微有点并发就会打满。我的配置如下const sequelize new Sequelize(config.database, config.username, config.password, { host: config.host, port: config.port, dialect: mysql, pool: { max: 20, min: 5, idle: 10000, acquire: 30000 } });max表示连接池最大连接数min是保持的最小连接数idle是连接空闲多少毫秒后释放acquire是获取连接的超时时间。改成 20 后我这里的并发压力缓解了。但注意不能无限往上加MySQL 自身有max_connections限制默认一般 151 或者更多应用连接池超过数据库上限会把 MySQL 直接打垮。生产环境里连接池数值应该和 MySQL 配置配合调整通常把 MySQL 的max_connections调大到 500应用层连接池 max 设为 100 是一个相对安全的组合。6.4 DNS 类问题ECONNREFUSED 和 ENOTFOUND 的不同指向容器部署后出现ENOTFOUND基本是 host 写错比如把db写成了mysqlDocker 内部 DNS 解析不到这个服务名。出现ECONNREFUSED则说明网络通了但端口没对上比如 MySQL 监听的是容器内 3306而应用配置成了 3307或者 MySQL 容器本身没有正常启动。docker logs api_mysql | tail -50这条命令能最快告诉你 MySQL 容器到底死在哪一步。如果是初始化脚本报错比如.env里 MySQL 密码包含特殊字符没有转义容器启动就会失败此时所有连接报错都只是表象根因在容器日志里。6.5 时区问题为什么接口里查出来的时间差 8 小时MySQL 8.0 的时区默认是 UTC而国内服务器基本都是东八区。如果 Sequelize 配置里的timezone没有写08:00查询出来的created_at会比实际时间早 8 个小时。我跑完第一个接口就发现了这个奇怪现象前端显示的文章创建时间永远和实际对不上。解决方法有两个。推荐在数据库连接层面统一时区timezone: 08:00同时在 docker-compose 的 MySQL 服务里设置environment: TZ: Asia/Shanghai这两个都配置好之后无论从哪个层面查数据时间都是正确的东八区。顺带提一句前端展示时仍然建议把时间转换为用户本地时区MySQL 和 Node 保证数据存的是绝对时间前端做最后一步展示格式化。这篇内容我后续会持续更新接下来大概率会补上 Sequelize 迁移在生产环境的执行策略以及接口自动化测试层的搭建过程。就个人实际体验而言这套技术栈虽然不够新潮但每一层都有大量可查的资料和成熟的解决方案对一个需要长期迭代的业务项目来说这种稳定感本身就是最大的效率。
返回列表