ARTICLE DETAIL

资讯详情

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

新建数据库的正确姿势:从选型、字符集到锁与死锁的完整避坑指南

新建数据库的正确姿势:从选型、字符集到锁与死锁的完整避坑指南 干数据库这行这么久每次接手新项目我做的第一件事几乎都是同一个动作——新建数据库。很多人觉得这不过是敲一条CREATE DATABASE两秒钟完事但实际上我见过太多后续问题字符集乱码、权限失控、连接超时、表结构改不动、并发一高就死锁追根溯源全是在这一步埋下的雷。这篇东西我想用一个老开发的实际视角把新建数据库这条链路完整过一遍从选型、MySQL 8.4 从零搭库、SQLite 轻量建库到连接池、结构维护、锁与死锁再延伸到时序库、国产库和向量库的建库差异。无论你是刚入行的后端新人还是一个人扛全栈项目的老手照着走一遍能省下不少本不该踩的坑。1. 建库之前先想明白选型错误是最大的返工1.1 业务形态决定数据库形态很多人上来就是mysql -e CREATE DATABASE...根本没想过为什么用它。选型如果不做后期返工的成本远比你想象的高——换库不只是导入导出SQL语法、索引策略、事务隔离、备份方案全要重来一遍。我见过一个项目用 SQLite 存核心业务数据并发一上来直接整库被锁也见过有人拿 MongoDB 硬存强事务订单数据结果自己做了一堆补偿逻辑还经常对不上账。所以建库之前先别急着敲命令先回答一个问题这库要承载什么业务场景推荐形态代表产品核心交易、强事务、报表关系型数据库MySQL、PostgreSQL、Oracle本地工具、嵌入式、临时统计嵌入式关系型SQLite物联网采集、海量日志、监控指标时序数据库TDengine、InfluxDB结构灵活、文档型数据文档型数据库MongoDBAI 语义检索、相似度推荐向量数据库Milvus、pgvector选型的基本原则可以概括为三板斧一是数据有没有强事务要求有就优先关系型二是数据形态是否固定字段经常变就考虑文档型三是数据的核心访问模式是什么按时间范围扫描为主就选时序库按向量相似度检索为主就选向量库。还有一个容易忽略的因素是团队熟悉度某个库再先进团队没人会调优出了问题连日志都看不懂那它在你的项目里就是负资产。1.2 字符集和排序规则最容易被忽略的一步建库时字符集没选对是翻车率最高的细节没有之一。MySQL 里utf8和utf8mb4的差别不是宣传层面的事utf8在 MySQL 底层是utf8mb3最多支持 3 字节编码很多生僻字和 emoji 根本存不进去。我处理过一起线上事故用户昵称里带了一个 emoji 表情INSERT 直接报Incorrect string value查了很久才发现是建库时用了utf8。从那以后凡是可能接受用户输入、可能出表情符号的库我一律用utf8mb4排序规则默认utf8mb4_0900_ai_ci老一点的 MySQL 5.7 则用utf8mb4_unicode_ci。PostgreSQL 的做法不一样它的 UTF8 编码本身就支持 4 字节字符建库时更要注意的是LC_COLLATE和LC_CTYPE这两个参数影响排序和字符串比较一旦建完库再改非常麻烦。达梦、人大金仓这类国产库通常还需要确认是否选择 GB18030 字符集要看最终交付环境的监管要求。我的建议是新库一律 UTF8 系别给自己留隐患。1.3 版本与发行版不要随手选最新数据库版本真不是越新越好。比如 MySQL 8.0 之后默认认证插件是caching_sha2_password很多老版本的客户端、老项目的连接串根本没适配连上去就报错。关于 MySQL 8.4.11 LTS我的观点很明确新项目优先用 LTS 版本它是长期支持版本bug 修复和安全性更新有保障不至于过了几个月就被迫大版本升级。另外还要提一句发行版的问题。MySQL 除了 Oracle 的官方版还有 Percona Server、MariaDB 这两个常见的分支。很多人选 MariaDB 是因为它可以用一些 MySQL 没有的存储引擎但如果你对 MySQL 官方路由、权限模型、工具链生态有依赖换分支之前一定要做完整兼容性测试。我见过因为测试不充分开发环境用 MySQL、生产环境用 MariaDB结果某个 SQL 执行计划完全不同的案例。选型这种事一致性比炫技重要。2. 从零实操以 MySQL 8.4.11 LTS 为例完成一次正经建库2.1 下载解压与初始化配置这里我以 Windows 环境为例讲一遍 MySQL 8.4.11 LTS 的手工部署流程因为很多人是从官网下载 ZIP 包而不是安装 MSI 安装包。先把 ZIP 解压到不含中文和空格的目录比如D:\mysql-8.4.11-winx64然后新建my.ini内容如下[mysqld] basedirD:/mysql-8.4.11-winx64 datadirD:/mysql-8.4.11-winx64/data character-set-serverutf8mb4 collation-serverutf8mb4_0900_ai_ci port3306 default-authentication-plugincaching_sha2_password配置文件的路径在 Windows 下正斜杠和反斜杠都能用但我建议统一用正斜杠避免转义问题。接下来以管理员身份打开命令行进入bin目录执行初始化命令mysqld --initialize-insecure这里有个细节值得说明--initialize-insecure会生成一个 root 空密码账号方便第一次进入而--initialize会生成一个随机密码写在data目录的日志文件里很多新手找了半天找不到特别容易卡住。我第一次部署时用的--initialize结果日志文件路径记错了找了快半小时才找到密码后来干脆统一用--initialize-insecure登录进去的第一件事就是改密码。然后启动服务mysqld --install net start mysql mysql -uroot如果不想注册成系统服务直接mysqld --console前台运行也可以适合临时测试环境。登录成功后第一件事是给 root 设置一个强密码ALTER USER rootlocalhost IDENTIFIED BY YourStrongPssw0rd;2.2 建库建表与权限最小化基础建库和建表语句如下CREATE DATABASE IF NOT EXISTS mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; USE mall; CREATE TABLE user_info ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, user_name VARCHAR(64) NOT NULL COMMENT 用户名, email VARCHAR(128) DEFAULT NULL COMMENT 邮箱, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_email (email) ) ENGINEInnoDB COMMENT用户信息表;有几个点是我常年坚持的主键用BIGINT UNSIGNED AUTO_INCREMENT而不是INT因为线上表数据量涨起来之后 INT 上限真的不够用所有字段都写 COMMENT就是因为半年后你自己都会忘掉某个字段的含义ENGINEInnoDB必须显式指定虽然 MySQL 8.0 默认就是 InnoDB但写成明确约束能避免别人误改全局默认引擎VARCHAR的推荐计算方式不是拍脑袋而是根据字符集编码长度和实际业务长度上限综合估算。接下来是建用户。我见过太多项目开发同学直接拿 root 账号给应用连接这是非常危险的做法。正确的姿势是单独建一个应用账号只给这个库的最小权限CREATE USER mall_app% IDENTIFIED BY AppPssw0rd2024; GRANT SELECT, INSERT, UPDATE, DELETE ON mall.* TO mall_app%; FLUSH PRIVILEGES;%表示允许从任意主机连接如果应用和数据库在同一台机器或同一个内网网段建议限制成具体 IP 或网段比如mall_app192.168.1.%这样被拖库时至少还能卡一道网络边界。应用账号不给CREATE、ALTER、DROP权限防止应用被注入后直接改表结构。2.3 连接测试与连不上的高频原因建好库之后最常遇到的就是连接失败我把高频错误整理成一张表报错现象真实原因Host xxx is not allowed to connect账号没有对应来源主机授权Authentication plugin caching_sha2_password cannot be loaded客户端太老不支持新版认证插件Cant connect to MySQL server on ... (10061)3306 端口未监听或防火墙拦截Access denied for user xxxlocalhost密码错误或 host 限定不匹配先说认证插件这个问题。如果你的 Navicat 版本比较老或者程序里用的 MySQL JDBC 驱动低于 8.0连接时就会碰到caching_sha2_password cannot be loaded。解决办法有两个升级客户端驱动或者把这个账号的认证插件改回老版ALTER USER mall_app% IDENTIFIED WITH mysql_native_password BY AppPssw0rd2024;但我更推荐升级驱动因为mysql_native_password迟早会在后续版本里被移除老办法只是过渡。端口和防火墙的问题也很经典。MySQL 默认监听 3306如果netstat -ano | findstr 3306看不到监听说明实例没起来或者配置的 port 不对。如果监听正常但远程连不上八成是 Windows 防火墙弹窗时点了取消去防火墙入站规则放行 3306 即可。这个环节我每次都会让项目组写进部署文档不然换一台服务器又要重新踩一遍。3. 另一种新建数据库只有一个文件的 SQLite 玩法3.1 文件即数据库建库零成本SQLite 可能是被误解最多的数据库。很多人觉得它不是正经数据库实际上它在全球的部署量可能比任何数据库都大——手机、浏览器、嵌入式设备里全是它。SQLite 的新建数据库极其简单它的库就是一个文件连库文件都不存在的时候你连进去写第一条数据它就自动创建了。命令行里是这样玩的sqlite3 shop.db进入交互界面之后建表、插入、查询CREATE TABLE stock ( id INTEGER PRIMARY KEY AUTOINCREMENT, sku TEXT NOT NULL, qty INTEGER DEFAULT 0 ); INSERT INTO stock(sku, qty) VALUES (A-1001, 20); INSERT INTO stock(sku, qty) VALUES (A-1002, 5); SELECT * FROM stock; .quit你做完这些操作之后去看当前目录下多了一个shop.db文件这个文件就是完整数据库。它支持事务ACID 满足、支持标准 SQL 的大部分语法、支持索引还能加密。如果你的场景是本地桌面工具、离线缓存、单机小系统SQLite 比部署一个 MySQL 服务不知道省心多少。3.2 SQLite 管理工具选哪个很多人搜SQLite 数据库用哪个管理打开我直接给结论Windows 下用 DB Browser for SQLite简称 DB4S就够了。这是开源跨平台的工具免安装版本都有双击就能用。它支持可视化建表、执行 SQL、导入 CSV、查看数据对小项目来说绰绰有余。用 DB4S 新建数据库也很顺手菜单里点新建数据库选个路径建表界面可以直接勾选字段类型、主键、自增不用手写 SQL。对新手特别友好的一点是它会把图形化操作对应的 SQL 生成的 SQL 也展示在底部窗口你点一下按钮就能看到底层语句学 SQL 的过程变得很自然。3.3 SQLite 的坑自增主键和并发写SQLite 有个容易踩的坑是INTEGER PRIMARY KEY AUTOINCREMENT的行为和 MySQL 不完全一样。MySQL 是全局自增删掉最大行之后新插入的行 id 不会复用那个已删除的值除非你手动重置SQLite 的INTEGER PRIMARY KEY如果没有 AUTOINCREMENT新行可能会复用被删除的最大 rowid。如果你用 id 做业务顺序展示这个差别会产生奇怪的现象。并发写是另一个坑。SQLite 的写锁是数据库级别的也就是说同一时刻只允许一个写事务其他写操作会被阻塞直到超时。我见过有人拿它做小型 Web 应用的后台结果几个后台员工同时提交表单系统就时不时报database is locked。SQLite 适合读多写极少、写入并发不高的场景一旦有多人同时写就该考虑换 MySQL 或 PostgreSQL。4. 建库之后马上要面对的连接池与工具链4.1 连接池不是可选项是必选项库建好了工程代码也连上了接下来要解决的问题是连接复用。每次建立数据库连接都要经历 TCP 握手、认证、权限校验这一套流程即使在内网也要几十毫秒在高并发下如果每来一个请求就创建一个连接数据库服务和应用服务器都得被拖垮。连接池的作用就是提前创建一批连接放在池子里请求来了直接取用完归还。以 Spring Boot 默认的 HikariCP 为例生产环境我通常这样配置参数含义常用建议值initialSize初始化连接数510maxActive最大活跃连接数3050按 QPS 和单查询耗时估算minIdle最小空闲连接数与 initialSize 保持一致maxWait获取连接最大等待毫秒10003000maxAge连接最大存活时间30 分钟左右防止被数据库侧回收这里想纠正一个常见误区连接池不是越大越好。每个连接在数据库端都要消耗内存和线程资源maxActive 设置过大反而会让数据库频繁切换上下文。估算方式我一般是假设单接口平均查询 20ms目标 QPS 是 1000那么并发需要的连接数逻辑上是 20 个再留 30% 到 50% 的余量maxActive 设在 30 左右比较合适。如果单查询耗时几十毫秒甚至上百毫秒优先去优化 SQL而不是把连接数加到几百。4.2 常见连接工具链命令行、Navicat、DataGrip日常管理和排查我常用的工具是三件套命令行、Navicat/DataGrip、数据库插件。命令行永远最可靠它不会因为版本不兼容连不上图形工具适合看数据、改结构和导数据IDEA 里的 Database 插件则方便开发时顺手查一下数据。这里特别提一个习惯用 IDEA 导出数据库脚本。右键对应的表或库Export Schema会生成一份 SQL 脚本把它放到 Git 里管理。这样做的好处是任何新环境都能一键建出相同结构的库而且表结构的变更记录全部可追溯。我见过不少项目表结构全靠人肉记忆环境一换建库脚本都不知道在哪全靠 DBA 手工补最后环境和环境之间跑出不一致排查起来非常痛苦。4.3 数据库同步软件与主从复制的取舍项目规模上去之后单库扛不住就会用到数据库同步。这里先澄清一个概念数据库同步软件和主从复制不是一回事。主从复制是 MySQL 原生能力把主库的 binlog 传输到从库重放解决读写分离和容灾数据库同步软件则更多指跨库、跨平台的数据搬移工具比如把 Oracle 数据实时同步到 MySQL或者把生产库的一部分表同步到分析库。用这些工具之前一定要想清楚两个问题一是同步延迟是否可接受二是主键冲突怎么办。如果是跨库同步两边 id 策略不一致很容易出现主键冲突通常需要额外加映射字段。另一个坑是字符集源库是 GBK目标库是 utf8mb4中文不乱码就谢天谢地了。我的经验是任何同步方案上线前先在测试环境跑一周对账源端和目标端的行数、关键字段的分布确认无差异再放到生产。5. 建好库后立刻要做的事SQL 增删改查与结构维护5.1 基本增删改查细节决定命运增删改查谁都会写但生产事故往往出在最基础的操作上。我处理过的最常见事故是 DELETE 和 UPDATE 不带 WHERE一条命令下去整张表的数据没了。所以我的团队有个铁律生产环境执行 UPDATE 和 DELETE 之前必须先 SELECT 出相同 WHERE 条件的记录数并肉眼确认再在事务里执行执行完第一时间检查ROW_COUNT()是否符合预期。SELECT COUNT(*) FROM user_info WHERE status 0; UPDATE user_info SET status 1 WHERE status 0;还有一个容易被忽略的细节WHERE 条件是否走索引。SELECT * FROM user_info WHERE email ab.com如果 email 上没有索引数据量到百万级之后这个查询就是全表扫描。建索引不是越多越好因为索引影响写入性能但高频查询条件字段必须要索引。一个简单的验证方法是用EXPLAIN看执行计划的 type 和 rowsall 就是全表扫描ref 或 const 通常是正常命中索引。5.2 MySQL 加唯一约束但数据已经有重复怎么办开发过程中经常会遇到这种需求业务要求某个字段不能重复但表里已经存在重复数据。直接执行ALTER TABLE user_info ADD UNIQUE KEY uk_email (email);一定会报Duplicate entry xxx for key uk_email。正确的步骤是先定位重复数据SELECT email, COUNT(*) c FROM user_info GROUP BY email HAVING c 1;然后和业务确认保留规则。常见的规则是保留最小 id 那一条删除其余DELETE u1 FROM user_info u1 JOIN user_info u2 ON u1.email u2.email AND u1.id u2.id;清理之后再建唯一索引就不会报错了。这套先查重、再去重、再建约束的顺序适用于大部分数据库PostgreSQL 里写法略有差异但思路一致。5.3 Excel 导入数据库的正确姿势把 Excel 数据导入数据库听起来很简单实际上坑很多。最常见的问题是表头映射不对、日期格式识别错误、空值处理不当。我的推荐流程是先另存为 CSV再导入而不是直接连 Excel 文件操作因为 CSV 的字段类型是文本可控性比 Excel 里那些00变数字、日期变成序列号的问题少得多。LOAD DATA INFILE /tmp/user_import.csv INTO TABLE user_info FIELDS TERMINATED BY , OPTIONALLY ENCLOSED BY LINES TERMINATED BY \n IGNORE 1 ROWS (user_name, email, status);如果数据量不大用图形工具导入也没问题。但有一点必须注意导入前先备份目标表。我就见过有人导入时没注意主键重复把线上正常数据覆盖掉的案例。数据量在万行级以上不要一条条 INSERT用批量导入或事务分批提交速度完全不在一个量级。5.4 修改数据库结构小表随意大表要慎重MySQL 数据库修改结构这个词热度一直很高因为它确实容易出问题。给一个数据量几百万行的表直接执行ALTER TABLE ADD COLUMN在 MySQL 8.0 之前相当危险即使 8.0 支持在线 DDL也会产生很大的主从延迟和 IO 压力。正确的做法是评估表大小如果是几十万行以内、业务低峰期直接改问题不大如果是千万级大表用 gh-ost 或 pt-online-schema-change 这类在线变更工具。我之前处理过一个订单表2 亿行业务要求加一个索引。如果直接ALTER TABLE预估要锁十几分钟直接停了线上业务。后来用 gh-ost 在从库上做全量同步再切换主从全程对业务零感知。这件事给我的教训是表结构变更不是开发问题是运维问题一定要有时间预估和回滚方案。6. 并发环境下的隐藏考点锁与死锁6.1 InnoDB 的锁到底有几种并发一上来新建数据库时没考虑清楚的问题都会爆发。很多人以为 InnoDB 是行锁就可以高枕无忧了实际上 InnoDB 的锁机制远比想象复杂因为它还有间隙锁、意向锁、Next-Key Lock 这些概念。锁类型加锁范围常见触发场景表锁整张表MyISAM 写操作、DDL 变更行锁命中索引的单条记录UPDATE/DELETE 带索引条件间隙锁索引区间锁住不存在的记录可重复读隔离级别下的范围查询意向锁表级别的意向标记事务给行加锁前自动添加MVCC多版本并发控制是 InnoDB 另一个法宝它让普通读不加锁就能读到一致快照大大降低了读写互斥。但要注意并不是所有读都不加锁SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE仍然会加锁。很多新手写报表统计时用了 FOR UPDATE结果并发一高就大面积阻塞这种问题排查起来非常隐蔽。6.2 一个典型的死锁案例与排查链路死锁是什么简单说就是两个事务互相持有对方需要的资源谁都不让谁。最经典的案例是两个事务同时更新两条记录但顺序相反事务 A先 UPDATE id1 的行再 UPDATE id2 的行 事务 B先 UPDATE id2 的行再 UPDATE id1 的行。这两个事务并发执行时A 持有 id1 的锁等 id2B 持有 id2 的锁等 id1就死循环了。InnoDB 有死锁检测机制检测到后会回滚其中一个事务所以你在应用日志里看到的通常是一条Deadlock found when trying to get lock; try restarting transaction。排查死锁的标准动作是执行SHOW ENGINE INNODB STATUS\G找到LATEST DETECTED DEADLOCK这一段里面会记录两个事务执行的 SQL、涉及的表和行甚至持有的锁类型。根据这些信息能还原出事务执行的先后顺序然后针对性地调整代码。我的经验是死锁预防比死锁排查更重要三条铁律一是所有事务内部统一加锁顺序比如先更新主表再更新子表二是事务体量要小尽量别在一个事务里做多轮查询再更新三是更新条件尽量命中相同索引避免把大范围的行都锁住。7. 不一样的新建数据库时序、文档、国产与向量库7.1 TDengine面向时序数据的建库与 C 绑定写入物联网和监控场景的数据是典型的时序数据特点是写多读少、按时间聚合、数据量巨大。用普通关系型数据库存几千亿条设备上报数据成本高到离谱这时候用 TDengine 这类时序库是更合理的选择。TDengine 的建库语句里有一些专门针对时序场景的参数CREATE DATABASE iot KEEP 365 DURATION 10 BUFFER 16 WAL_LEVEL 2; USE iot; CREATE TABLE device_tb (ts TIMESTAMP, temp FLOAT, humi FLOAT) TAGS(device_id BINARY(16));KEEP表示数据保留天数DURATION表示数据文件分片的跨度这两个参数直接影响存储占用和查询性能。如果做的是边缘采集这种长时间不上云的场景KEEP设置就得放宽。程序写入方面C 绑定写入 TDengine 时官方推荐用参数化预编译接口taos_stmt_prepare。理由和 SQL 预编译一样避免了每次拼 SQL 字符串的开销和注入风险同时支持批量绑定。核心流程是connect 建立连接初始化 stmt 对象prepare 一条带问号占位符的 INSERT 语句然后反复 bind 参数并执行。这种方式在写入大量点位数据时吞吐量远高于逐条拼接 SQL。7.2 达梦、人大金仓这类国产库的建库差异信创场景下达梦DM和人大金仓KingbaseES越来越多出现在项目里。它们通常在语法上兼容 Oracle 或 PostgreSQL所以如果你只会 MySQL第一次建库反而会有点别扭。以人大金仓为例官方提供了 Docker 镜像拉取后可以快速起一个单机实例docker pull kingbase/v8 docker run -d -p 54321:54321 kingbase/v8起完容器后用内置工具登录创建数据库实例。这类国产库有两个常见坑一是对大小写敏感的处理和 MySQL/PostgreSQL 不同双引号和未加引号的对象名会被不同处理建表时容易混淆二是授权机制很多默认账号权限模型是兼容 Oracle 的创建业务账号时需要额外指定表空间和权限。如果你第一次接触建议先把官方文档里的初始化流程完整跑一遍再动手设计库表。7.3 MongoDB 和向量数据库不能照搬关系型思维MongoDB 的核心概念不是表而是数据库Database、集合Collection、文档Document。新建数据库在 MongoDB 里是最没有仪式感的操作——你甚至不需要先创建直接切到库名并插入一条文档库和集合就在这一瞬间实际落盘了use blog db.posts.insertOne({ title: 新建数据库这件事, tags: [database, mongodb], views: 1024 }) show dbs这种设计对开发者确实友好但也容易让团队忽视数据模型设计。没有外键、没有严格 schema如果不在建集合时通过 validator 约束字段结构一年后这个库里的文档格式可能五花八门。我的建议是 MongoDB 集合创建时就加上 JSON Schema 校验别图一时方便。向量数据库则是这两年 AI 项目里绕不开的话题。它和传统数据库解决的完全不是一类问题数据是 embedding 向量查询方式是找最相似的向量。新建一个向量集合时最重要的是一开始就定好向量维度因为维度通常对应 embedding 模型比如很多开源模型输出 768 维OpenAI 的 text-embedding-3-small 是 1536 维。维度定错了后面换模型就得重建整个集合代价非常大。另外还要考虑距离度量方式欧式距离和余弦相似度适用于不同场景这决定了查询语义是否符合业务预期。回到我自己的习惯上来现在不管新建什么数据库我事后都会做三件事把建库脚本提交进版本控制应用账号和 DBA 账号严格分离只给最小权限在沙箱环境跑一轮并发读写看一眼锁等待和慢查询是否异常。新建数据库这件事拉开差距的从来不是那一条CREATE DATABASE而是你为之后所有操作提前铺好的路。如果你正准备给新系统搭环境不妨把上面这条链路完整走一遍省下来的运维时间会非常可观。
返回列表