ARTICLE DETAIL

资讯详情

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

MySQL数据库设计实战:从表结构、索引到事务与线上排查

MySQL数据库设计实战:从表结构、索引到事务与线上排查 开场先聊点实在的。做了这么多年数据库相关的项目我最大的感受是一个系统的性能上限、维护成本甚至团队协作的顺畅程度往往在数据库设计的那一刻就已经决定了。网上关于 MySQL 的文章铺天盖地但大多数只教你怎么安装、怎么跑通一条 SELECT真正到了生产环境和复杂业务里表结构怎么拆、索引怎么建、事务怎么控制、线上问题怎么排查这些才是拉开差距的地方。这篇文章我就结合自己的实战经验把 MySQL 数据库设计和落地过程中最核心的东西拆开讲透。从表设计的基本原则到索引背后的 B 树原理再到事务隔离级别、存储过程的使用边界最后落到多语言连接、前后端分离项目里的真实场景和常见坑。既有原理层面的为什么也有可以直接抄作业的步骤和参数。适合刚接触数据库的后端开发、正在做课设或者毕业设计的同学也适合已经写了一段时间 SQL 但总觉得差点意思、想系统补一遍底层的工程师。内容不绕弯子直接进入正题。1. 表设计先想清楚范式、主键与字段类型怎么选很多人建表就是照抄需求文档字段堆上去就行。结果数据量一到百万级查询慢、更新乱、冗余到处都是。表设计是整个数据库的地基地基歪了后面所有优化都是补丁。1.1 范式与反范式什么时候该“设计得不规范”范式是关系数据库设计的基础理论我简单说一下应用层面的理解。第一范式要求字段原子性也就是每个字段只能存一个值别在一条记录里用逗号拼一堆标签第二范式要求非主键字段完全依赖主键不能只依赖主键的一部分这个主要针对联合主键的场景第三范式要求非主键字段之间不能有传递依赖比如订单表里存了客户编号就不要再存客户所在的城市因为城市是通过客户编号间接决定而不是直接依赖订单主键。规范化的最大好处是消除冗余更新一致。举一个最典型的例子如果客户地址散落在订单表、发货表、回访表里客户搬家之后你要改多少个地方改漏一个后面统计报表就是错的。遵循范式的表结构客户信息只存一份订单表只存客户 ID要查地址就 JOIN 一下。但实战里不能教条。我见过太多人“为了范式而范式”把好好的查询拆成七八张表 JOIN性能一塌糊涂。反范式设计的核心场景有两个一个是报表统计一个是读多写少的高并发查询。订单表就是一个典型。订单表里冗余一个商品名称、冗余一个商家名称从范式角度看是违反第三范式的因为商品名称和商家名称都可以通过商品 ID、商家 ID 查出来。但在交易系统的查询链路里订单列表页需要展示这些信息如果每次都 JOIN 商品表和商家表热门商品一上架瞬间流量就能把库打挂。所以我的习惯是核心交易表尽量按范式设计保证写入和更新的严谨性查询展示层和统计分析层可以专门建冗余宽表把需要 JOIN 的字段提前灌进去。这里最关键的是要设计好数据同步机制冗余字段更新了宽表也得跟着更新用事件通知或者定时任务都行但一定得有不然数据就对不上了。1.2 主键的选型比想象中更关键主键选型是表设计里最容易被低估的决策。很多人随手选个自增 ID 就完事或者看到网上说 UUID 好就全用 UUID其实都有代价。自增主键的好处是写入顺序性好新记录的主键值比之前的大B 树的叶子节点始终是在末尾追加页分裂发生的概率低写入性能非常稳定。但它有一个明显的短板分库分表或者数据迁移的时候多个库的自增 ID 会冲突需要额外处理步长和起始值。另外自增主键会暴露业务量竞争对手看你的订单 ID 就知道你一天有多少单。UUID 做主键是新手最容易踩的坑。UUID 是随机字符串作为主键时索引页的插入顺序是随机的频繁触发页分裂写入性能会明显下降而且 UUID 是字符串相比 bigint 动辄多出好几倍的存储空间。二级索引的叶子节点存的是主键值主键越大每个二级索引的存储空间和查询开销也越大。我测过一张千万级的表用 UUID 做主键比用 bigint 做主键二级索引的体积能大出 40% 以上。比较推荐的方案是采用雪花 ID 或是类似的分布式 ID 算法。雪花 ID 是一个 64 位的 bigint包含时间戳、机器 ID 和序列号既保证了全局唯一性又保持了大致的时间顺序。在分库分表场景下也能用而且用 bigint 存可以让索引体积最小。还有一个原则要提醒不要把业务字段当主键。身份证号、手机号、订单号这些字段虽然唯一但可能会变比如手机号注销后重新发放一旦变了所有关联表都要跟着改。主键要选一个和业务无关、永远不变化的逻辑字段或者干脆就用自增 ID另建唯一索引约束业务唯一性。1.3 字段类型、默认值与字符集细节决定大批量导入的成败字段类型选错后果是延迟的。刚开始数据量小看不出来等上了生产磁盘空间、查询效率、数据导入速度全都会出问题。数值类型方面能用 smallint 就别用 int能用 int 就别用 bigint。状态值、开关量用 tinyint 就够存储空间越小索引页能放下更多条目扫描速度越快。金额字段千万不要用 float 和 double会有精度丢失一定要用 decimal比如 decimal(10,2)。布尔值在 MySQL 里没有真正的 Boolean 类型一般用 tinyint(1) 表示0 和 1 就够了。字符串字段的坑主要在 varchar 和 TEXT 的选择。varchar 需要指定长度比如 varchar(255)它按实际内容长度存储varchar(255) 和 varchar(50) 存储相同内容占用的空间是一样的但 varchar(255) 可能让索引键变长因为索引排序需要按最大长度预留。TEXT 类型不能有默认值而且只能加到前缀索引也就是说你不能对 TEXT 字段建普通索引只能建索引的前 N 个字符查询条件很难命中完整索引。能用 varchar 解决的绝对不要用 TEXT。时间字段建议直接用 datetime不要用 timestamp。timestamp 的范围只到 2038 年虽然还有年头但很多系统要存历史数据选 datetime 范围更宽。另外 timestamp 会受时区影响如果你的服务器时区设置变了历史数据读出来就会偏移。datetime 不会。在建表时给时间字段设置 DEFAULT CURRENT_TIMESTAMP这样插入数据的时候就不用手动填时间了。字符集这块MySQL 8.0 之前的默认字符集是 latin1 和 utf8mb3很多老库建出来中文乱码就是这么来的。新表一律用 utf8mb4这是 UTF-8 的完整实现能存 emoji 和生僻字。排序规则上MySQL 8.0 默认是 utf8mb4_0900_ai_ci5.7 时代常用的是 utf8mb4_general_ci。ai 表示不区分重音ci 表示不区分大小写。如果业务要求排序时区分大小写要选 utf8mb4_bin。这个细节直接关系到你 ORDER BY 的结果对不对后面排查部分我会再展开讲。还有一个容易被忽略的字段默认值。比如“mysql 设置默认值为 0”这个话题很多业务状态字段确实应该默认 0比如订单状态默认 0 表示待支付删除标记默认 0 表示未删除。但要注意默认 0 和 NULL 在查询语义上是完全不同的。NULL 在 WHERE 条件里用 IS NULL 判断索引也不太好命中统计 COUNT 的时候会被排除。我的建议是能设置默认值就设置默认值禁止字段默认为 NULL查询条件写起来也干净。2. 索引是慢查询的止痛药但别乱吃索引可能是 MySQL 实战里最值得深挖的技术点了。一条 SQL 从 10 秒优化到 0.01 秒的差距往往就是一个索引的事情。但索引也不是越多越好每一个索引都意味着写入时要额外维护一份数据结构表写性能会下降磁盘占用会上升。2.1 B 树和聚簇索引搞懂底层才知道索引为什么这么快MySQL InnoDB 引擎的索引底层是 B 树。要理解它为什么适合数据库存储先要知道磁盘 I/O 的一个基本特征一次磁盘读取是按页来读的InnoDB 的页大小默认是 16KB。也就是说不管你要读多少数据至少要以 16KB 为单位从磁盘搬到内存。B 树的设计就是围绕这个特征展开的。它是一个多路平衡搜索树每个节点存储很多个索引项树的高度通常只有三层左右。假设每个索引项用 8 字节的主键加 6 字节的指针一个 16KB 的页大概能存 1000 多个索引项三层 B 树就能存储超过十亿条记录。这意味着查询最多只需要三次磁盘 I/O就能从十亿条数据里定位到目标记录所在的叶子页。这就是索引快的最根本原因。InnoDB 的索引结构还要区分聚簇索引和二级索引。聚簇索引的叶子节点直接存储整行的数据所以 InnoDB 表数据本身就是按主键顺序组织的。这也是为什么主键最好是递增的——如果主键是随机生成的插入新记录的时候经常要移动已有数据页分裂频繁性能断崖式下降。二级索引也就是普通索引的叶子节点存储的是索引列的值加上主键值。查询时先通过二级索引找到主键再回聚簇索引查一遍完整数据这个过程叫回表。回表意味着额外的磁盘 I/O这也是为什么很多 DBA 会强调覆盖索引的概念。用一个生活化的类比聚簇索引就像一本按拼音排好序的字典正文你想查“mysql”这个字翻到对应页码直接就能看到完整释义二级索引就像字典前面的偏旁部首检字表你先在检字表里找到这个字在第几页再翻到那一页去查正文。每翻一次页就是一次 I/O能不翻就不翻。2.2 联合索引的最左前缀原则一次索引命中靠的是结构联合索引是实战中用得最多的索引类型也是最容易用错的。很多人建了复合索引结果查询根本没有命中EXPLAIN 出来还是全表扫描。联合索引的底层结构要先想清楚。比如建立联合索引 (a, b, c)B 树会先按 a 排序a 相同的情况下按 b 排序b 相同的情况下按 c 排序。这就决定了索引的匹配规则查询条件必须从最左边开始连续匹配。你可以只用 a 作为条件查询也可以用 a 和 b 一起查但不能跳过 a 直接用 b 或者 c 查否则这个索引就失效了。这里有一个很多人困惑的点范围查询放在联合索引哪里比较合适假设要建一个查询条件包含等值和范围查询的索引比如 where status 1 and create_time 2024-01-01联合索引应该是 (status, create_time) 而不是 (create_time, status)。因为等值条件放在前面可以精确定位到一段连续的范围如果把范围条件放在前面后面的等值条件就没法走索引了。还有一点是 MySQL 5.6 之后引入的索引下推优化简写是 ICP。在没有 ICP 的时候二级索引查出主键后就要回表回表之后再去过滤 create_time 之类的条件。有了 ICPMySQL 会在二级索引内部就把 create_time 的条件过滤掉减少回表次数。实测下来对于过滤度比较高的查询ICP 能减少将近一半的回表次数。联合索引的字段顺序还要考虑区分度。区分度高就是字段取值的分布越分散越好比如用户 ID、手机号的区分度就很高状态字段只有几个取值的区分度就低。区分度高的字段放前面可以更快缩小索引扫描范围。2.3 慢查询排查中常见的索引失效场景索引失效是慢查询排查中最头疼的问题。我整理几个高频场景开发时能避开就避开。对索引列使用函数。比如 WHERE DATE(create_time) 2024-01-01MySQL 需要对每一行执行 DATE 函数之后才能比较索引自然就失效了。正确写法是 WHERE create_time 2024-01-01 AND create_time 2024-01-02这段区间直接走索引定位。隐式类型转换。比如你给字段 user_id 的类型是 varchar但查询条件写的是 WHERE user_id 12345MySQL 会把字段值转成数字再比较相当于对索引列做了隐式函数操作索引失效。这个坑在接口入参是数字类型的场景特别常见。解决办法就是确保 JDBC 或者 ORM 框架传入的类型和字段类型一致或者查询条件显式写成字符串。前导模糊查询。WHERE name LIKE %mysql% 没法用索引因为 B 树是按前缀排序的不知道你要从哪个字符开始查。但 WHERE name LIKE mysql% 是可以走索引的。全文搜索或者模糊匹配需求建议用专门的全文索引或者搜索引擎不要在核心表上做这种查询。OR 连接条件。WHERE a 1 OR b 2如果 a 和 b 上都有单列索引MySQL 8.0 的优化器可能会走 index merge但更多情况下会退化成全表扫描。最好的办法是把 OR 拆成两个查询 UNION 起来或者确认两个字段都建了索引再交给优化器。在排查慢查询的时候我一般会先用 EXPLAIN 看执行计划。主要关注 type 列、key 列和 rows 列。type 列从好到坏依次是 system、const、eq_ref、ref、range、index、ALL。看到 ALL 基本就是全表扫描必须优化。key 列显示实际命中的索引名称如果是 NULL 就说明没走索引。rows 列是估算扫描的行数行数相差几个数量级就是性能瓶颈所在。开发阶段还有一个容易被忽略的点不要为了“可能以后用得上”而提前建索引。每个索引都会拖慢 INSERT 和 UPDATE索引越多写放大越严重。我的原则是先按核心查询场景建索引上线后通过慢查询日志和实际业务流量再逐步补充。索引的功能是服务查询的没有查询需求的索引就是负债。3. 事务隔离级别与存储过程从概念到落地事务是关系型数据库和 NoSQL 拉开差距的核心能力。很多业务场景比如转账、下单、库存扣减任何一个步骤失败整个操作都要回滚没有事务根本无法保证数据一致。3.1 四大隔离级别背后的脏读、不可重复读和幻读事务有四大特性ACID。原子性保证操作要么全部成功要么全部失败一致性保证事务开始前和结束后数据都符合业务规则隔离性保证并发事务互不干扰持久性保证提交后数据不会丢失。InnoDB 通过 redo log 保证持久性和原子性通过 undo log 和锁机制实现隔离性这些都不需要业务去操心业务要关注的是隔离级别。SQL 标准定义了四种隔离级别读未提交、读已提交、可重复读和串行化。隔离级别越低并发能力越强出现的问题也越多。读未提交是最宽松的隔离级别一个事务能读到另一个事务未提交的数据这叫脏读。比如事务 A 给账户扣了 100 元还没提交事务 B 就查到了扣完之后的余额结果 A 回滚了B 读到的数据就是错的。这种级别基本只在理论研究里出现实战中没人用。读已提交解决了脏读但会出现不可重复读。就是同一个事务里两次读取同一行数据结果不一样。因为其他事务在你两次读取之间提交了修改。很多业务系统比如 Oracle 默认就是读已提交对于大多数查询场景是够用的。可重复读是 MySQL InnoDB 的默认隔离级别也是我个人用得最多的。它保证了一个事务里多次读取同一行数据结果完全一致不管别的事务有没有提交修改。但标准的可重复读仍然存在幻读问题就是说你查一个范围的记录第一次查出来 10 条第二次查出来 11 条多出来的那条是别的事务插入的就像幻觉一样。串行化是最严格的隔离级别所有事务一个一个排队执行读写互相阻塞。能完全避免幻读但并发能力极低生产环境基本不用。理解这几个问题之后选隔离级别才有依据。只有一个原则不用的场景别乱调MySQL 默认的可重复读已经经过了大量生产验证如果没搞懂现状不要轻易改成读已提交。3.2 MVCC 和间隙锁InnoDB 怎么在可重复读下摆平幻读MySQL 默认的可重复读隔离级别实际是通过 MVCC 多版本并发控制来实现的。InnoDB 的每行记录隐藏了事务 ID 字段修改记录时会生成新的版本旧版本保留在 undo log 里形成版本链。事务开启后创建自己的 ReadView 快照查询时只能看到比快照版本更早的数据。这就是快照读也是普通 SELECT 语句默认的执行方式。在快照读下可重复读是天然满足的因为你读到的始终是自己快照时刻的数据版本。幻读问题主要出现在当前读的场景。当前读是指带锁的查询和 DML 语句比如 SELECT ... FOR UPDATE、UPDATE、DELETE它们必须读取最新版本并在数据上加锁。当前读遇到范围查询时InnoDB 会用间隙锁锁定扫描范围内的间隙防止其他事务在这个间隙插入新记录这就从机制上解决了幻读。间隙锁是理解排查死锁的关键。举个例子表里有一个 id 从 1 到 100 的间隙事务 A 执行了 SELECT * FROM orders WHERE id 10 AND id 20 FOR UPDATE会把 10 到 20 之间所有不存在的 id 间隙全部锁住事务 B 想插入一条 id15 的记录就必须等 A 提交。如果事务 C 也持有了另一个范围的锁并且又申请 A 的锁范围就可能循环等待MySQL 检测到死锁会回滚其中一个事务。实战中我遇到的大部分死锁都是间隙锁互相冲突导致的。排查手法是执行 SHOW ENGINE INNODB STATUS 查看最新一条死锁信息里面会列出两个事务持有的锁和等待的锁。解决思路上可以缩小锁范围、调整索引让锁落在更小的范围上或者让并发事务对相同资源的访问顺序保持一致。MVCC 和间隙锁的组合让 InnoDB 在可重复读下既能保证很高并发又不出现幻读这也是为什么 MySQL 敢把它设为默认隔离级别。3.3 存储过程适合批处理不适合核心业务存储过程这个话题争议很大。我的观点是它有明确的适用场景但绝不该当万能工具。存储过程最大的优势是减少网络交互。一段复杂的批量逻辑如果写在应用代码里可能需要发几十条 SQL 来回执行封装成存储过程一次调用全部搞定。另外它可以把敏感的数据操作封装在数据库内部只暴露一个调用接口避免应用层知道底层表结构。数据归档、报表预处理、定时统计这类批处理任务存储过程非常合适。但它的问题也很致命。首先调试困难没有断点日志也不好打。其次版本管理很麻烦存储过程散落在数据库里不像代码有 Git 可以 diff、review团队协作时 DDL 的变化很容易互相覆盖。另外数据库的计算能力扩展性远不如应用服务器你可以在 CPU 不够时加应用实例但存储过程写多了数据库 CPU 先被打满。所以我的实践原则是核心业务链路比如订单、支付、库存扣减逻辑必须放在应用层代码里用事务和明确的业务逻辑控制非核心的批处理、统计、归档、清洗任务可以用存储过程提升效率。如果决定用存储过程有一个语法细节必须注意存储过程内部语句用分号结束但 MySQL 客户端解析时也用分号所以创建存储过程前要用 DELIMITER 修改分隔符执行完再改回来。这个坑新手几乎必踩。举个简单的数据归档例子把一年前的订单转移到订单归档表。存储过程逻辑就是开启事务、分批 SELECT 旧数据插入归档表、原表删除、提交配合事件调度器可以做到每天自动执行。核心代码大概长这样DELIMITER // CREATE PROCEDURE archive_old_orders() BEGIN DECLARE done INT DEFAULT 0; DECLARE batch_size INT DEFAULT 1000; REPEAT START TRANSACTION; INSERT INTO order_archive SELECT * FROM orders WHERE create_time DATE_SUB(NOW(), INTERVAL 1 YEAR) LIMIT batch_size; DELETE FROM orders WHERE create_time DATE_SUB(NOW(), INTERVAL 1 YEAR) LIMIT batch_size; COMMIT; SET done ROW_COUNT(); UNTIL done 0 END REPEAT; END // DELIMITER ;使用存储过程前一定要想清楚后续维护谁来负责如果团队里没人写过存储过程尽量用应用层定时任务代替可读性和可维护性会好很多。4. 多语言连接与前后端分离项目里的 MySQL 实战姿势数据库设计得再好最终要落到具体项目的接入方式上。这一节聊一聊不同语言和技术栈连 MySQL 的常见姿势以及前后端分离项目里数据库侧的完整落地流程。4.1 JDBC、连接池与 C 连接 MySQL 的方式对比Java 后端连接 MySQL 是最成熟的一套方案。基本链路是 JDBC 连接池 ORM 框架比如 MyBatis 或者 JPA。连接池我推荐 HikariCPSpring Boot 2.x 之后的默认连接池就是它性能非常好几乎没有配置成本。连接池的核心参数是 maximumPoolSize 和 minimumIdle。连接池开得太小高并发下请求要排队等连接开得太大MySQL 那边数百个空闲连接都在占内存反而影响性能。有一个经验公式可以供参考最大连接数基本等于 ((CPU核心数*2) 磁盘数)。四核八线程的机器配置 8 到 10 个连接是合理的。当然这只是起点最终要靠压测和监控来微调。我见过太多项目把 maximumPoolSize 配到 200数据库服务器连接数轻松被打满应用层却还没出现问题这里的浪费其实没有必要。连接超时参数也要关注。connectionTimeout 表示请求连接池时等待的毫秒数一般设 30000。idleTimeout 是连接空闲多久被回收默认 10 分钟。如果应用有定时任务晚上空闲时间比较长空闲连接会被回收掉早上一上班瞬间高并发连接池需要重新建连接会有一小段冷启动延迟。针对这种场景可以设置 minimumIdle 保持几个常驻连接。C 连接 MySQL 的路径会稍微辛苦一点常见方案有 MySQL 官方提供的 mysql-connector-c 和 MySQL C API。C API 是底层的 C 接口需要手动处理 MYSQL 结构体和结果的释放代码写起来繁琐但性能直接。mysql-connector-c 封装了面向对象的接口和 JDBC 的 API 风格类似适合 C 项目使用。C 连接 MySQL 最常见的坑是编译链接问题需要安装 libmysqlclient-dev编译时加上 -lmysqlclient。第二个坑是字符集默认连接字符集可能不是 utf8mb4中文字段读出来就是乱码连接建立后要立即执行 SET NAMES utf8mb4或者在连接参数里显式指定 charsetutf8mb4。还有一点MySQL 8.0 默认认证插件是 caching_sha2_password老版本 C API 如果不支持会导致认证失败需要升级连接库或者修改用户的认证插件为 mysql_native_password。日常开发和调试的话命令行 mysql 客户端和 Navicat 我都用。命令行适合快速执行脚本和排查问题Navicat 的可视化能力强很多可以直接看表结构、编辑数据、反向生成 ER 图还能看会话和锁状态。对于不会整天敲 SQL 的同学Navicat 能省不少事。4.2 前后端分离项目里 MySQL 的落地流程前后端分离架构下后端项目连接 MySQL 的完整流程可以总结成一条链路需求分析到 ER 图再到表结构 DDL接着是 ORM 映射然后是 Service 层事务控制最后是接口层面的接入和慢查询监控。需求分析阶段要明确核心实体和它们的关系。是一对多、多对多、还是自关联比如用户和订单是一对多订单和商品是多对多需要中间表。这个阶段先别急着写 DDL用纸笔或者工具把 ER 图画出来和产品对一遍确认字段含义后再动手。DDL 阶段把第 1 节讲的设计原则用上。字段类型、默认值、字符集、主键策略在这个阶段一次到位。表结构的管理强烈建议用版本化迁移工具比如 Flyway 或者 Liquibase。它们能自动记录哪些脚本执行过、哪些没有执行新同事入职跑一下就能得到完整的最新库结构再也不用靠人肉同步。ORM 映射阶段用 MyBatis 的话要注意 SQL 注入风险。核心原则是能用 #{} 预编译占位符就不要用 ${} 字符串拼接。${} 是直接把用户输入拼进 SQL等于把数据库门钥匙交出去了哪怕只是内部管理系统也不该留下这种隐患。事务控制的边界是一个高频问题。事务应该开在 Service 层不要开在 Controller 层。Controller 负责接收参数、校验请求Service 层聚合业务逻辑数据库操作放在一个事务方法里。事务里不要做外部调用比如发短信、调用支付网关因为外部接口的延迟不可控会让数据库连接长时间占用。碰到这种场景先把数据库操作提交再异步调用外部服务。接口接入阶段如果是增删改操作还要考虑接口的幂等性。用户多点几次提交按钮可能造成重复下单。常规做法是前端防重、后端用唯一索引兜底或者用分布式锁。数据库这边的最后一道防线就是唯一索引业务上能加的唯一约束要尽量加上这是数据质量的托底。监控阶段是上线后最重要的一环。打开慢查询日志把执行时间超过 1 秒的 SQL 全部捞出来逐个分析执行计划。我在项目中通常会加一个定时任务每天把慢日志汇总成表格发到群里谁写的 SQL 慢了一目了然。长期坚持下去整个团队的 SQL 质量都会明显提升。5. 线上常见问题实录SSL 连接、排序乱码与版本选择最后这部分是老本行分享几个我在线上真实踩过、排查过的问题。这些问题都很典型网上资料零散我把排查思路和最终解法整理出来给各位参考。5.1 MySQL 8.0 的 SSL 连接错误一次典型的客户端握手失败MySQL 8.0 默认开启了 SSL 加密连接这本来是安全增强却成了很多团队的噩梦。最常见的报错是类似 SSL connection error: unknown error number 或者 Public Key Retrieval is not allowed 的信息。先说这个问题的原理。MySQL 8.0 在设计上要求客户端在连接时完成 SSL 握手并且 caching_sha2_password 认证插件需要在首次连接时获取服务器的公钥来加密传输密码。如果你的 JDBC 驱动版本太老或者连接串里没有明确配置是否使用 SSL就会在握手阶段失败报出各种奇怪的错误。解决方式分几步。第一步是升级 JDBC 驱动到 8.x 版本最好和 MySQL 服务器大版本保持一致。第二步是在 JDBC 连接串里显式配置参数。本地开发和测试环境加 useSSLfalse 关掉 SSL 加密同时加 allowPublicKeyRetrievaltrue允许客户端从服务器获取公钥。生产环境的数据库一般在内网隔离的网络环境同样可以保持默认 SSL 关闭前提是确保应用和数据库之间没有经过公网传输。如果你是非 Java 技术栈比如 Python 和 C处理方式类似。Python 用 PyMySQL 连接 MySQL 8.0 时如果遇到认证失败检查连接参数里的 ssl 配置和 auth_plugin。C 的 mysql-connector-c 则需要在连接选项里设置 ssl_mode 为 disabled或者升级到支持 caching_sha2_password 的新版本。排查这类问题我一般分三步先看客户端驱动版本再看连接串参数最后看 MySQL 用户表的 plugin 字段。SELECT user, host, plugin FROM mysql.user如果发现用户的 plugin 是 caching_sha2_password 而驱动不支持可以临时把插件改回 mysql_native_password 验证问题然后决定是换驱动还是改认证方式。注意改插件会影响密码要有预案。5.2 ORDER BY 排序乱码与默认值 0 的坑排序问题在数据量小的时候完全看不出来数据量大了、排序条件复杂了各种诡异问题就冒出来了。第一个是中文排序问题。MySQL 默认的 utf8mb4_general_ci 排序规则对中文字符的排序是按 Unicode 码点来的不是按拼音。也就是说 “周” 排在 “张” 前面因为 “周” 的 Unicode 编码小于 “张”。这在很多业务场景下是不对的比如城市列表按拼音排序应该 “北京” 在 “成都” 前面实际上按码点排出来的结果可能不是你要的。解决中文按拼音排序一种简单粗暴的办法是使用 ORDER BY CONVERT(name USING gbk)因为 GBK 编码是按拼音顺序排列汉字的转换后排序就能得到拼音序。代价是性能差大数据量下不建议直接这么干。更好的方案是在表中增加一个拼音列或者使用了 collation 为 utf8mb4_zh_0900_as_cs 的字段这个排序规则专门解决中文排序问题。如果用的是 MySQL 5.7 则没有这个排序规则建议把排序逻辑放到应用层处理或者提前算好拼音字段。第二个问题是按照某个字段排序时NULL 值的表现。MySQL 默认 NULL 在升序排列时排在最前面降序时排在最后面。很多时候业务希望 NULL 当成最小值或者排到最后就需要用 ORDER BY IFNULL(sort_field, 9999)或者直接用 ORDER BY field IS NULL, field 的写法先排除空值再排序。还有字段默认值为 0 的坑。很多状态字段设计默认 0这在业务上没问题。但当你要对状态字段排序时比如 ORDER BY status所有默认 0 的记录会集中在一起如果后续要对 status 建立索引做排序区分度太低大概率走不了索引。另一个场景是统计查询里用 COUNT 或者 SUM 对默认 0 的字段做聚合如果把默认 0 和业务真实值混在一起统计结果会偏离预期。设计阶段就要明确默认 0 到底是“无效占位”还是“真实业务状态”两者要拉开语义必要时用 NULL 来区分。5.3 MySQL 5.7 还是 8.0版本选择的现实考量版本选择是每个团队都要面对的决策。网上搜“MySQL 5.7.44 官方为什么之后 5.7.43 呢”这类问题的人很多说明大家确实被版本绕晕过。先说结论新项目直接用 MySQL 8.0老项目如果跑得好好的别为了升级而升级。MySQL 8.0 带来的核心变化有几个。默认字符集是 utf8mb4新建表不需要每次指定字符集。认证插件换成了 caching_sha2_password安全性更高。优化器引入了哈希连接等值关联大表的效率提升明显。窗口函数和公共表表达式 CTE 让复杂查询写起来简单很多比如排名、同比环比、分组 TopN以前要用变量写一堆恶心的 SQL现在一个窗口函数搞定。MySQL 5.7 最后的版本是 5.7.44这个版本属于安全更新和 bug 修复功能上没有新的东西了。如果系统必须跑在 5.7 上一定要用 5.7.44 而不是更早的版本因为里面包含了不少安全漏洞的修复。也正因为 5.7 生命周期已经结束官方不再提供新的功能更新所以新项目没必要再从 5.7 起步。老项目从 5.7 升级到 8.0 要过的关主要在三处。第一认证插件的变化很多老客户端连不上需要上面说的 SSL 和插件处理。第二GROUP BY 的行为更严格以前允许的按别名或者 select 列表外字段分组8.0 会直接报错ONLY_FULL_GROUP_BY 默认开启。第三整数类型不再显示宽度比如 int(11) 在 8.0 里就变成 int不影响存储但影响一些显示逻辑和旧代码的注解。另一个现实问题是 MySQL 已经发布了 9.x 创新版本但生产环境我依然推荐 8.0.x 的稳定分支。9.x 比如 HeatWave 相关的功能组要看具体的场景需求常规业务用不到反而要承受更快的版本迭代和维护压力。安装方式上Linux 服务器推荐用官方 apt 源或者 yum 源直接安装版本、依赖、配置文件都帮你处理好了。Windows 开发环境下载 zip 包解压后执行 mysqld --initialize-insecure 初始化再用 net start mysql 启动服务。生产环境不要下载乱七八糟网站上的绿色版和集成环境数据库这种底层组件版本和补丁的正规性必须保证。最后说点个人体会在我接触过的所有项目里凡是后期数据和性能出问题的往前追根溯源几乎都能追溯到设计阶段的一个草率决定主键选型随手用了 UUID、字段类型图省事全选 varchar、字符集用了默认的 latin1、建索引凭感觉每个字段都建一遍。这些问题在开发环境里全都跑得好好的直到数据量涨上来、并发上来才开始集中爆发。所以我现在做项目有个习惯建表之前先在文档里写下这个表未来三年的数据量预期、主要的查询场景、写入频率然后才动手写 DDL。这么做的性价比极高可能只花掉半小时却能避免后面无数个熬夜排查慢查询的夜晚。而你有没有真正理解 MySQL也往往就体现在这些没人提醒的细节点上。希望这篇文章里的经验能让你少踩几个坑。
返回列表