1. 为什么我劝你别再用MySQL 5.7跑CRMEB了
1.1 从一次卡顿排查说起
上个月朋友公司的CRMEB多商户系统出了个怪问题:每天晚上八点到十点的下单高峰期,订单列表接口偶尔会卡到四五秒才返回,后台按商户维度导出对账单时更是直接超时。他们的Java服务是微服务架构,中间件、缓存、对象存储都排查了个遍,最后把目光落在数据库上——一台跑了快四年的MySQL 5.7,单实例扛着几十个商户的订单、商品、会员、营销数据,慢查询日志里密密麻麻全是全表扫描和临时文件排序。
这其实不是个例。CRMEB这类多商户电商系统,业务表多、关联查询重、统计报表频繁,数据库版本长期停留在5.7甚至5.6,性能瓶颈迟早会从“偶发”变成“日常”。MySQL 8.0从2018年发布到现在,无论是优化器、事务处理、字符集还是窗口函数,都已经非常成熟,官方也早已将5.7列为接近EOL的状态。把CRMEB的数据库升级到MySQL 8,不是“追新”,而是切切实实解决生产痛点。
1.2 CRMEB在旧版MySQL下的三座大山
先说清楚CRMEB这套系统对数据库的依赖程度。CRMEB多商户版(Java)的底层是Spring Boot + MyBatis-Plus,业务库包含商品表、订单表、商户表、用户表、营销活动表、拼团秒杀表等。多商户模型天然决定了它的SQL特点:按merchant_id频繁过滤、跨表join统计、按时间段聚合分析。旧版MySQL在这些场景下有三个明显的吃力点:
-
优化器疲软:5.7的优化器对复杂关联查询的执行计划经常选错驱动表,导致本可以走索引的SQL变成临时表+文件排序。尤其是订单表和订单商品明细表这种一对多关系,数据量一上来,慢查询立刻冒头。
-
字符集和排序规则落伍:CRMEB早期建库很多直接用了utf8mb4 + utf8mb4_general_ci,这套组合在5.7里没问题,但utf8mb4_general_ci对中文和特殊字符的排序规则比较粗糙,而且5.7的utf8mb4只是“基本多文种平面”的utf8,像emoji里的生僻字、部分特殊符号依然无法完整存储。
-
缺乏窗口函数和CTE:营销报表里经常要算“每个商户本月订单排名”“每个用户最近一次下单时间”,在5.7里只能用临时变量嵌套子查询去模拟,SQL写出来又长又难维护,执行效率也差。
1.3 升级到底能带来什么
从5.7切到8.0,最直观的感受是三条:
- 排序更快:MySQL 8重构了优化器,引入了cost model的改进,像CRMEB这种多条件、多表join的查询,执行计划明显更合理,我实测同一个“按商户汇总近30天销售额”的SQL,在数据量相同的情况下,从5.7的2.1秒降到了0.6秒左右。
- 字符集更省心:8.0默认字符集就是utf8mb4,默认排序规则是utf8mb4_0900_ai_ci,对中文、生僻字、emoji的支持更加完整,而且校验规则更符合日常认知。
- 运维更省事:8.0支持原子DDL,ALTER TABLE改列、加索引不再像5.7那样存在“改一半失败,留下半成品表”的尴尬;数据字典统一化,information_schema的查询也快很多。
当然,升级不是无痛的。MySQL 8改了一些默认行为,比如认证插件从mysql_native_password换成了caching_sha2_password,sql_mode默认多了NO_ZERO_DATE和STRICT_TRANS_TABLES,这些都会让CRMEB的旧代码第一个运行周期就“爆雷”。但这些问题都有标准解法,下面我把整个升级链路从准备到回滚一步步拆开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 升级前必须做好的环境核对与备份
2.1 盘点当前环境的黄金三问
动手升级前,先别急着装新版本。有几个问题必须提前确认,否则很容易做到一半发现方案选错:
- 当前CRMEB的MySQL是多少小版本? 如果你的5.7还停留在5.7.20以下,建议先做一个5.7.x内部的原地升级(比如5.7.18升到5.7.31),再做5.7到8.0的跨大版本升级。官方给出的升级路径是5.7升8.0,中间不能从5.6直接跳,而5.6需要先升级到5.7,这是个容易忽略的点。
- 数据库服务器用什么文件系统? MySQL 8的数据字典文件格式和5.7不同,跨版本升级不能用复制ibd文件这种物理迁移方式,只能用逻辑导出导入,或者原地跑mysql_upgrade。如果你之前的“备份”是直接拷贝整个datadir,那这次要换成mysqldump或逻辑备份方式。
- 代码里有没有引入旧版专属的SQL写法? CRMEB的Mapper里偶尔会用到一些老套路,比如用@select注解写复杂的集合查询、用IFNULL而不是COALESCE、或者依赖字符串隐式转换比较。这些大部分在8.0没问题,但如果你项目里有自定义SQL引用了mysql.proc表或mysql.user表的字段,就需要整改。
2.2 一键备份与恢复演练
备份是最不能跳过的一步。我的习惯是在升级窗口前做“三份备份”:
- mysqldump全量逻辑备份,加--single-transaction、--routines、--triggers、--events参数,保证导出过程不锁表且包含存储过程、触发器、事件。
- binlog位置记录,因为升级过程中业务可能还在写入,所以要记录准确的master log file和pos,方便做增量补数。
- 配置文件备份,把my.cnf原样拷贝一份,同时记录线上生效的参数,用mysqldump --print-defaults导出一份当前生效参数快照。
备份不是备份完就完了,必须做恢复演练。见过太多人备份文件有几十个G,真到恢复时才发现文件损坏或者字符集乱码。建议在测试环境先建一个空实例,把备份文件完整恢复一遍,至少确认三件事:恢复后表数量一致、关键业务表的数据行数和生产一致、CRMEB后台能正常登录。
bash复制# 逻辑备份命令,注意调整密码和数据目录
mysqldump -uroot -p --single-transaction --routines --triggers --events \
--default-character-set=utf8mb4 --databases crmeb > crmeb_bak_$(date +%Y%m%d%H%M).sql
提示:如果数据量大,可以考虑用--tab=目录配合SELECT INTO OUTFILE的方式做快速导出,但由于CRMEB的库表结构包含外键和自增列,直接导入时容易遇到约束问题,我还是推荐标准mysqldump方案,配合并行导入来提速。
2.3 兼容性预检清单
在正式升级前,先把下面这五类项目过一遍,能避免大部分“升级后起不来”的尴尬:
| 预检项 | 说明 | 检查方法 |
|---|---|---|
| 存储引擎 | 8.0不再支持MyISAM作为系统表引擎,业务表也建议全量InnoDB | SELECT table_schema, table_name, engine FROM information_schema.tables WHERE engine <> 'InnoDB'; |
| 重复索引 | 8.0对冗余索引更严格,重复索引会导致写入性能下降 | 用pt-duplicate-key-checker跑一遍 |
| 外键和约束 | 外键列必须和引用列有相同字符集和排序规则 | 重点查order表、order_detail表、merchant表之间的关联 |
| SQL模式 | 8.0默认sql_mode包含STRICT_TRANS_TABLES,之前不严格的SQL可能报错 | 在5.7上先执行SET sql_mode='STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO'观察业务是否正常 |
| 用户与权限 | 8.0的权限表结构变化,部分老账号可能在升级后丢失密码插件 | 提前导出所有用户授权语句:pt-show-grants |
3. 数据迁移实操:从导出到导入的完整链路
3.1 参数调整:让mysqldump导出足够干净
很多人在导出时直接裸跑一条命令,结果导入时报错一堆,根本原因是导出时没带对参数。针对CRMEB这种包含存储过程、触发器和视图的系统,我推荐的导出命令长这样:
bash复制mysqldump -uroot -p \
--single-transaction \
--routines --triggers --events \
--set-gtid-purged=OFF \
--default-character-set=utf8mb4 \
--max_allowed_packet=1G \
--net_buffer_length=8192 \
--databases crmeb > crmeb_full.sql
几个参数的重点:
--set-gtid-purged=OFF:如果你的5.7没有开启GTID,导出时如果不加这个参数,导入8.0后会带着GTID信息,导致复制环境配置起来很别扭。--max_allowed_packet=1G:CRMEB的营销活动表、商品详情表经常存大量的JSON或长文本,如果packet设小了,恢复时会报“Got a packet bigger than 'max_allowed_packet'”。--default-character-set=utf8mb4:强制导出文件使用utf8mb4,防止乱码。
3.2 导入阶段的字符集与排序规则处理
导入前先按8.0的规范建好一个空库,再导入数据,比直接往旧库上覆盖要干净。建库语句推荐写成这样:
sql复制CREATE DATABASE `crmeb` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
导入时也要注意客户端连接的字符集:
bash复制mysql -uroot -p --default-character-set=utf8mb4 crmeb < crmeb_full.sql
字符集这一步最容易踩的坑是:CRMEB老库的表可能一部分是utf8mb4,一部分是utf8,还有一部分是latin1,导入到同一个库里后,join查询出现“Illegal mix of collations”报错。我在一次升级中就遇到商品表的name列是utf8mb4_general_ci,商户表的name列是utf8mb4_unicode_ci,关联查询直接报错。解决办法是先统一表字符集:
sql复制-- 把crmeb库下所有表统一为utf8mb4和utf8mb4_0900_ai_ci
SELECT CONCAT('ALTER TABLE `', table_name, '` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;')
FROM information_schema.tables
WHERE table_schema = 'crmeb';
把上面SQL的输出粘贴执行,然后再重新导入数据,就能避免大量collation冲突。
3.3 增量数据同步的兜底方案
生产环境升级不可能完全停服太久,我的习惯是“全量备份 + 停服窗口 + 增量追平”三步走:
- 凌晨低峰期做一次全量导出,这是基础数据快照。
- 停服维护窗口开始后,停止CRMEB的Java服务,避免新数据写入。
- 如果全量导出后到停服之间还有少量数据变化,用binlog解析工具(如mysqlbinlog)把这段时间的增量操作提取出来,在导入全量后补上。
不过对于大多数CRMEB项目,凌晨两三点停服半小时做升级完全可接受,只要全量导出时业务没有写操作(或写操作量级很小),其实不需要复杂的增量同步。所以我的建议是:维护窗口尽量选在低峰期,前台开启“系统维护中”页面,把升级时间控制在1小时以内。
4. MySQL 8落地后CRMEB必踩的五个坑
4.1 caching_sha2_password 让Java服务连不上
这是升级后第一只拦路虎。MySQL 8默认的认证插件是caching_sha2_password,但很多Java应用使用的数据库驱动版本偏老,比如mysql-connector-java 5.1.x,根本不认识这个插件,启动时直接抛Unable to load authentication plugin 'caching_sha2_password'。
解决思路有两个:
- 升级驱动到mysql-connector-java 8.0.x,并在JDBC URL里显式指定
allowPublicKeyRetrieval=true和useSSL=false(内网环境)。 - 如果不方便升级驱动,就在MySQL端把应用账号的认证插件改回旧版:
sql复制ALTER USER 'crmeb'@'%' IDENTIFIED WITH mysql_native_password BY 'your_password';
CRMEB的Java服务通常在配置文件里用了com.mysql.jdbc.Driver,这个类名对应老驱动。如果升级驱动,要改成com.mysql.cj.jdbc.Driver,同时URL里建议加上serverTimezone=Asia/Shanghai,否则日期字段会差8小时。这里我要说一句,新驱动 + 新认证插件才是正道,改回native_password只是应急,长期来看还是在延续旧债。
4.2 sql_mode 变化导致 group by 报错
CRMEB后台有不少统计SQL是按天、按商户group by的,例如“近30天订单量按天统计”。在5.7默认的sql_mode里,ONLY_FULL_GROUP_BY没有开得很严格,写法上只要select列里包含group by列,其他列即使不在group by里也可能蒙混过关。但8.0默认开启ONLY_FULL_GROUP_BY,这种SQL直接报错:Expression #2 of SELECT list is not in GROUP BY clause。
最正确的做法是把SQL改成合规写法,用ANY_VALUE或者把select列都放进group by:
sql复制-- 原写法(8.0下报错)
SELECT merchant_id, order_status, COUNT(*) FROM order WHERE create_time > '2024-01-01' GROUP BY merchant_id;
-- 修正写法
SELECT merchant_id, ANY_VALUE(order_status), COUNT(*) FROM order WHERE create_time > '2024-01-01' GROUP BY merchant_id;
但如果你暂时不想大改代码,也可以在my.cnf里把sql_mode的ONLY_FULL_GROUP_BY去掉:
ini复制[mysqld]
sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
我个人的建议是代码规范优先,去掉ONLY_FULL_GROUP_BY只是让老代码跑起来,新代码还是应该按标准来,因为一旦SQL写法依赖了非聚合列的非确定性取值,统计结果可能莫名其妙出错,这在财务对账场景里是很大的隐患。
4.3 大小写敏感:lower_case_table_names 必须在初始化前定
MySQL 8的lower_case_table_names参数在Linux上默认是0,也就是说表名区分大小写。而CRMEB在Linux服务器上老库可能设置的是1,表名大小写不敏感。升级到8.0后,如果你直接在同一个实例上恢复,遇到表名大小写不一致的情况,应用层会报找不到表。
需要特别注意的是:在MySQL 8里,lower_case_table_names必须在初始化实例时指定,中途修改会导致数据字典不一致,我踩过一次这个坑,改完后整个实例起不来,只能重新初始化再恢复数据。所以正确做法是:
- 如果新实例还没初始化,先在配置文件里写上
lower_case_table_names=1,再初始化数据目录,然后再导入数据。 - 如果是从5.7原地升级,原实例已经是0或1保持即可,但必须确认应用层的表名大小写风格是否统一。
CRMEB的表名都是小写下划线风格,如果你之前也是规范建库,一般不受影响。但如果你用了任何“驼峰表名”的自定义插件,这一步要特别留意。
4.4 时区问题导致时间字段偏差8小时
MySQL 8对时间类型的处理更严格,连接时区默认是服务器系统时区。如果你的Java服务部署在配置了UTC的Docker容器里,而MySQL服务器是Asia/Shanghai,那查出来的timestamp字段就很可能差8小时。
解决方案在JDBC URL里明确指定时区:
properties复制jdbc:mysql://localhost:3306/crmeb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
同时,MySQL端建议把全局时区也设为东八区:
sql复制SET GLOBAL time_zone = '+08:00';
SET time_zone = '+08:00';
这里要提醒一下,如果CRMEB里已有的timestamp数据是当初按错误时区写入的,那改完时区后历史数据的“实际时间”可能会偏移。升级后建议先跑几条核对SQL,把order表里的create_time、pay_time和业务真实时间比一比,确认没有偏差再开放服务。
4.5 老SQL语法在MySQL 8中的兼容处理
升级后另一个常见问题是一些老SQL运算符和函数在8.0里行为变化。比如:
- 隐式字符集转换:
WHERE name = '中文'这种字符串比较,如果列是utf8mb4而客户端连接字符集设置不对,8.0可能无法使用索引,甚至返回错误结果。 - 日期减法:在5.7里
DATE_SUB(NOW(), INTERVAL 1 DAY)没问题,但有些老代码写的是NOW() - 86400,这在8.0里会当成数字运算,而不是日期运算,直接导致日期错误。 GROUP BY中使用了WITH ROLLUP:8.0对WITH ROLLUP和ORDER BY的配合要求更严格,如果老代码习惯把ORDER BY写在WITH ROLLUP前面,需要调整为后面。
我的处理套路是:把CRMEB的项目源码里的所有XML Mapper扫描一遍,按关键词搜GROUP BY、DATE_SUB、IFNULL、NOW()、CURDATE()这些函数,逐个确认有没有上面提到的不规范写法。这一步虽然琐碎,但比上线后再看报错日志要省心得多。
5. 基于MySQL 8特性给CRMEB做的性能调优
5.1 调整连接池参数
升级到8.0后,CRMEB的Java服务连接池配置可以直接用MySQL 8的新特性来优化。比如druid连接池中,连接建立后可以通过connectionInitSqls设置一个初始化SQL,把当前连接的事务隔离级别、时区等一次配好:
yaml复制spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/crmeb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
initial-size: 10
min-idle: 10
max-active: 100
connection-init-sqls:
- "SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci"
这里给连接池加一个utf8mb4排序规则,能有效避开前面提到的collation冲突问题。同时,由于8.0的查询性能更好,连接池的maxActive可以比5.7时代稍微调大一点,因为长查询变短后,单个连接占用时间缩短,但并发峰值时需要的连接数会更多。
5.2 使用窗口函数优化订单统计SQL
MySQL 8最大的增量之一就是支持窗口函数。CRMEB后台有一个很常见的需求:“每个商户按销售额排名,取Top 10”。在5.7里,我只能用@rownum之类的临时变量,SQL写得又臭又长:
sql复制-- 5.7写法:临时变量实现排名
SET @row_number = 0;
SET @merchant_group = '';
SELECT merchant_id, total_amount, rownum
FROM (
SELECT merchant_id, total_amount,
@row_number := IF(@merchant_group = merchant_id, @row_number + 1, 1) AS rownum,
@merchant_group := merchant_id AS dummy
FROM (
SELECT merchant_id, SUM(pay_amount) AS total_amount
FROM order
WHERE pay_status = 1
GROUP BY merchant_id
ORDER BY merchant_id, total_amount DESC
) t1
) t2
WHERE rownum <= 10;
升级到8.0后,一条SQL搞定:
sql复制SELECT merchant_id, total_amount, rn
FROM (
SELECT merchant_id, SUM(pay_amount) AS total_amount,
ROW_NUMBER() OVER (PARTITION BY merchant_id ORDER BY SUM(pay_amount) DESC) AS rn
FROM `order`
WHERE pay_status = 1
GROUP BY merchant_id
) t
WHERE rn <= 10;
不只是排名,像“每个商户最近一单时间”“每个用户第三笔订单金额”这类场景,窗口函数都能让SQL逻辑更清晰,执行计划也更稳定。CRMEB的报表模块非常适合重构这些SQL。
5.3 不可见索引与多值索引怎么用在多商户场景
MySQL 8.0还新增了不可见索引(Invisible Indexes)和函数索引。在多商户场景下,一个很实用的技巧是:用不可见索引来验证“这个索引到底有没有用”。
比如你发现CRMEB的系统索引里有idx_merchant_id和idx_merchant_id_status两个索引,你想判断后者是否冗余,可以在8.0里直接把它改成不可见,而不需要删除:
sql复制ALTER TABLE `order` ALTER INDEX idx_merchant_id_status INVISIBLE;
然后观察一段时间的慢查询,确认没有SQL依赖这个索引,再真正删除。在5.7里,你只能删掉再重建,风险高得多。
对多商户系统来说,如果商品表里有attributes这种JSON字段,8.0的多值索引(Multi-Valued Index)还可以直接对JSON数组做索引,这在做“按商品属性筛选”时会很有用。不过CRMEB当前版本对大JSON字段的检索不多,这里先留个扩展思路,不建议一上来就无脑加。
6. 回滚预案:升级失败后30分钟内恢复
6.1 回滚的前提条件
不管准备多充分,生产环境升级必须先把回滚预案想好。我见过有人升级失败后手忙脚乱地找备份文件,结果发现备份文件没有同步到其他机器,服务器被初始化后只能干瞪眼。
回滚的前提条件,说三遍也不为过:
- 数据库备份文件已经复制到独立机器或对象存储,而不是放在数据库服务器本地磁盘。
- 应用代码和配置也做了环境快照,回滚时能恢复到升级前的JAR包和application.yml。
- 明确一个回滚决策时间点,比如“升级后30分钟内,如果CRMEB后台首页无法登录,或者核心接口错误率超过5%,立即回滚”。
6.2 一份可执行的回滚脚本
下面是一个我在类似项目中用过的回滚流程脚本,逻辑不复杂,但每一步都有明确目的:
bash复制#!/bin/bash
# 回滚到5.7环境:停服务 -> 恢复数据 -> 恢复配置 -> 启服务
# 1. 停掉Java服务
systemctl stop crmeb-server
# 2. 用之前导出的备份文件重建5.7环境
# 如果新环境是8.0,这里需要先清掉8.0的数据目录,用备份恢复
docker stop mysql-8 || true
docker rm mysql-8 || true
# 启动临时5.7容器
docker run -d --name mysql-57-rollback \
-e MYSQL_ROOT_PASSWORD=xxx \
-v /data/mysql57_backup:/var/lib/mysql \
-p 3306:3306 \
mysql:5.7.31
# 3. 等待数据库初始化完成
sleep 20
# 4. 恢复备份(备份文件路径根据实际情况改)
mysql -uroot -p -h 127.0.0.1 -P 3306 < crmeb_bak_before_upgrade.sql
# 5. 恢复旧配置并启动服务
cp /data/backup/application.yml /opt/crmeb/config/application.yml
systemctl start crmeb-server
# 6. 检查日志
tail -f /opt/crmeb/logs/crmeb.log
回滚脚本里最容易被忽略的是“旧配置文件”的恢复。如果你在升级MySQL时顺手把CRMEB的application.yml里的连接池、sql_mode相关配置也改了,回滚时只恢复数据库是不够的,必须连配置一起恢复,否则应用起来后还是按新连接串走。
注意:回滚操作最好提前演练一遍。我在测试环境试过一次,发现备份文件因为权限问题无法在容器里挂载,还好演练时发现了。如果等生产环境出问题才试,那场面就不好看了。
我在实际项目中还有一个小习惯:所有升级操作命令都写成一个大事务式的checklist文档,每执行完一步就打勾。以前总觉得自己记性好,后来发现人到半夜,大脑容易短路,文档比记忆可靠得多。这次给CRMEB升级,如果只让我说一句最核心的经验,那就是——先确认备份能恢复,再谈升级步骤;先写好回滚方案,再动生产环境。做到这两点,MySQL 8升级这条路基本就稳了。
