1. 项目概述:从“会输入命令”到“真正懂数据库”
1.1 核心需求解析
这年头,嘴上说自己会 MySQL 的人很多,但真到线上出问题,能淡定排查的没几个。很多初学者停留在“会用 Navicat 点点点”或者“背了几条 SELECT 语法”的阶段,一旦遇到字符集乱码、事务回滚不生效、SQL 执行突然变慢、主键冲突导致写入失败,就完全懵了。所谓“MySQL 数据库基础”,绝不只是 CREATE TABLE 和 SELECT FROM 这么简单。它应该覆盖数据库的架构认知、存储引擎选型、核心语法实操、事务与锁的机制理解、备份恢复策略,以及日常运维中最常踩的性能坑。
我见过太多开发者,写了两年业务代码,却答不上来 InnoDB 和 MyISAM 到底差在哪,也说不清为什么 count(*) 在千万级表上会慢得离谱。这就是基础不牢。这篇文章不是给你罗列文档,而是把我实际踩过的坑、验证过的方案、排查问题的思路,全部摊开来讲。适合刚接触数据库的初学者,也适合写过一些 SQL 但想补全底层认知的开发者和运维新人。
1.2 清楚了“有什么”,才知道“用什么”
MySQL 本质上是一个关系型数据库管理系统,它最核心的价值就四个字:稳定存、快速取。但“稳定”和“快速”背后,是一整套协同工作的组件。
从宏观架构上,MySQL 可以拆成三层来看:
- 客户端层:就是你发 SQL 的地方,包括命令行 mysql 客户端、各种 GUI 工具、应用程序的 JDBC/PDO 驱动等。它们负责把请求发出去,并把结果集展示出来。
- 服务端核心层:负责连接管理、权限校验、SQL 解析、优化器生成执行计划,以及最终调用存储引擎接口。
- 存储引擎层:真正把数据落到磁盘、维护索引、控制事务提交和回滚的地方。
对业务开发来说,你 90% 时间打交道的是第二层和第三层。SQL 写得好不好,取决于你对优化器的理解;数据稳不稳,取决于你对存储引擎的理解。
我第一次接手一个老项目时,发现他们的订单表用的是 MyISAM 引擎。单量小的时候没毛病,等后来数据到了百万级,频繁出现“锁表”——一个慢查询把整张表的读写全部卡住,前端接口直接超时报警。这就是典型的存储引擎选型失误。
所以聊“基础”,第一块硬骨头就是存储引擎。InnoDB 和 MyISAM 的选择,早在建表那一刻就决定了你后续的并发能力和数据安全性。这也是我建议所有初学者把它放在第一课的原因。
2. 存储引擎与表结构设计的底层逻辑
2.1 InnoDB 与 MyISAM 的选型对比
用生活里的例子来打比方。MyISAM 就像一间门上只有一把挂锁的资料室,查资料的人互相不干扰,但只要有人要改资料,就得把整间屋子锁起来,其他人只能在门口等着。InnoDB 则像一间有独立文件柜的办公室,每个人可以同时操作不同的柜子,互不打扰。只有在操作同一个柜子时,才需要短暂协调。
落实到技术参数上,两者的核心差异可以整理成一张表:
| 对比维度 | InnoDB | MyISAM |
|---|---|---|
| 事务支持 | 支持 ACID 事务,可回滚可提交 | 不支持事务,变更直接生效 |
| 锁粒度 | 行级锁,并发写入能力强 | 表级锁,写入时整表阻塞 |
| 崩溃恢复 | 依靠 Redo Log 和 Undo Log 自动恢复 | 依赖修复工具,恢复能力弱 |
| 外键约束 | 支持 | 不支持 |
| 全文索引 | 8.0 之前不支持,8.0 已支持 | 原生支持 |
| 物理文件 | .ibd 数据文件 + ibdata 共享表空间 | .MYD 数据文件 + .MYI 索引文件 |
| 适用场景 | 绝大多数 OLTP 业务系统 | 只读报表、数据仓库的历史明细表 |
这里想特别强调一下我的选型建议:从 5.7 版本开始,除非你有非常明确的理由,否则一律用 InnoDB。我之前在某项目中做过一次对比测试,同样的写密集场景,MyISAM 的 QPS 在并发数超过 20 时就开始雪崩,而 InnoDB 在并发 200 时依然能保持平稳。这就是行锁和表锁的本质差距。
2.2 字符集与排序规则不是小事
另一个基础但容易翻车的是字符集设置。默认的 utf8mb4 是绝对推荐的选择,它不是简单的“UTF-8 的别名”,而是能完整覆盖 BMP 字符之外的四字节字符——比如 emoji 表情和一些生僻字。如果误用了 utf8mb3(旧版叫 utf8),遇到 emoji 入库就会报错,或者出现乱码。
我做过一次线上迁移,源库是 utf8,目标库是 utf8mb4。直接迁移后,所有四字节字符全部显示为问号。原因就是迁移工具按源库字符集读取,转存时又按目标库写入,两边对“字符组”的定义不一致。解决方案其实很简单:先确认库、表、字段三级的字符集,做到全链路统一。
还有排序规则。utf8mb4_general_ci 是早期默认,校验速度快但对中英文混排的排序规则比较粗糙;utf8mb4_unicode_ci 在排序准确性上更好,强烈推荐新项目使用。如果你发现 ORDER BY 排序结果和你预期不一致,先检查排序规则,别急着优化索引。
2.3 建表时的字段类型选择与常见误区
基础建表谁都会,但字段类型的选型,直接决定一张表能承载多少数据、查询有多快。我在代码评审时经常看到以下三个问题:
第一,用 VARCHAR 存日期和时间。这会带来两个隐蔽问题:一是无法直接用日期函数做范围聚合,每次都要 CAST,索引彻底失效;二是排序是按字典序排的,不是按时间先后排的。正确做法是日期用 DATE、时间用 DATETIME 或 TIMESTAMP。
第二,小数用 FLOAT 或 DOUBLE。二进制浮点数的精度误差在累加运算中会不断放大,账务类系统尤其致命。金额、税率等精度敏感字段,一律用 DECIMAL,比如 DECIMAL(10,2)。
第三,主键用随机字符串。InnoDB 的主键是聚簇索引,物理上按主键顺序组织数据。如果主键是 UUID 这类随机值,每次插入都会导致页分裂和索引重排,写入性能会断崖式下降。建议用自增 BIGINT 做主键,或者纯粹用于分布式场景时,也要保证生成的 ID 是趋势递增的。
给大家一个经过验证的建表示例:
CREATE TABLE `user_account` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` VARCHAR(64) NOT NULL COMMENT '登录名', `email` VARCHAR(128) DEFAULT NULL COMMENT '邮箱', `balance` DECIMAL(12,2) NOT NULL DEFAULT '0.00' COMMENT '账户余额', `status` TINYINT NOT NULL DEFAULT '1' COMMENT '状态:1-正常 0-禁用', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户账户表';注意最后一行,ENGINE、CHARSET、COLLATE 三个参数在创建表时明确指定。很多初学者习惯用默认设置,但默认不一定适合你的业务,尤其是字符集,线上库一旦建成就很难改。
3. 核心操作与索引优化的实操要点
3.1 索引不是越多越好,而是越准越好
索引是 MySQL 性能的基础设施,也是初学者最容易走极端的地方。一种极端是不建索引,全表扫描,500 万行的表一条 SQL 跑十几秒;另一种极端是每个字段都加索引,导致写入变慢、索引文件膨胀。
我先给一个判断标准:如果一个查询条件能在索引中被精确定位到小于等于 5% 的行,那这个索引就是值得建的;如果它只能帮你把扫描范围缩小到 30%,那性价比就很低。
比如说WHERE status = 1 AND created_at > '2024-01-01'这样的查询,status 字段的区分度很低(总共就那么几个值),建单独的索引帮助不大。正确做法是建联合索引(status, created_at),让 MySQL 先按 status 过滤,再按 created_at 排序。这里我补充一个细节:最左前缀原则。联合索引的生效顺序是从左到右的。
我用一个实际案例来演示。一张订单表,包含 order_no、shop_id、customer_id、pay_time 四个字段。常见的查询有两种:
-- 查询某个店铺某段时间的订单 SELECT * FROM orders WHERE shop_id = 123 AND pay_time BETWEEN '2024-01-01' AND '2024-01-31'; -- 查询某个客户的订单 SELECT * FROM orders WHERE customer_id = 456 ORDER BY pay_time DESC;如果分别建(shop_id, pay_time)和(customer_id, pay_time)两个联合索引,两条查询都能精准走索引,不会出现一个索引吃遍天、另一个字段白白浪费索引维护成本的情况。
3.2 务必弄懂的 EXPLAIN 执行计划
索引建得好不好,不用靠猜,EXPLAIN 就是你的体检报告。我第一次真正理解它时,是在排查一个接口超时问题。SQL 很简单,但每次全表扫描,加了索引也没用。最后发现是 WHERE 条件里写了DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-06-01',函数套在了索引列上,优化器直接放弃走索引。
EXPLAIN 输出的核心字段,在日常排查中重点看这几个:
- type:从好到坏依次是 system > const > eq_ref > ref > range > index > ALL。看到 ALL 就要警惕,说明全表扫描。
- key:实际用到的索引名。NULL 说明没走索引。
- rows:预估扫描行数,越小越好。这个数字和实际值偏差过大时,通常是统计信息过期。
- Extra:出现 Using filesort 或 Using temporary 时,说明 MySQL 绕过索引做了额外排序或建临时表,是对性能的双重打击。
这里分享一个我屡次用来救场的操作:当一条慢 SQL 怎么优化都不理想时,通过改写 SQL 结构来改变执行计划。比如把 OR 改写成 UNION ALL 两条独立查询,让每条查询各自走索引,最后合并结果。同样的逻辑,性能可能是天壤之别。
3.3 四个必踩的 SQL 写法坑
我把从业以来在代码评审中高频出现的四个坑整理出来,它们都是可以用很简单的改写避免的:
第一,SELECT *。在生产环境里,除非你确认这是极小表且只需要全列展示,否则不要写星号。原因不仅是多传输了不需要的字段,更严重的是它让数据库无法使用覆盖索引。所谓覆盖索引,就是查询的字段全部包含在某一个索引中,MySQL 直接读索引就能返回结果,连数据行都不用回表。比如你只查 id 和 username,而 (username, id) 正好是一个联合索引,那这一次查询的 IO 成本会小到可以忽略。SELECT * 直接把这个可能性抹杀了。
第二,隐式类型转换。比如 phone 字段是 VARCHAR 类型,但 SQL 里写 WHERE phone = 13800138000,数字不加引号。MySQL 会把索引列转成数字再去匹配,函数作用在索引列上,索引失效。记住一个原则:字段是什么类型,查询条件就给它什么类型。
第三,LIKE 前置通配符。WHERE name LIKE '%张%'也是索引杀手,因为索引是按前缀排序的,前置通配符让 MySQL 无从定位。如果业务确实需要这种模糊匹配,要么考虑全文索引(MySQL 8.0 的 ngram 解析器对中文支持不错),要么单独引入搜索引擎,或者至少接受全表扫描并控制表体量。
第四,ORDER BY RAND()。这是我在某个推荐系统需求里见过最离谱的写法,直接对 20 万行的表做随机排序,一次性生成全表随机序列。正确做法是客户端生成随机 ID 列表,再按 ID 去取数据,或者用 JOIN JOIN 子查询限定范围。
这些坑看似基础,但在真实业务中带来的后果是实打实的——慢查询日志里躺着的那几条“全家桶”SQL,往往就是从这里长出来的。
4. 事务、锁与隔离级别的机制拆解
4.1 事务的 ACID 到底是靠什么实现的
事务是数据库和文件系统的本质区别。你说往文件里写一行数据,写到一半断电了,文件就烂了;但数据库说这条数据提交成功了,它就能保证整条写入了。这背后的支撑,是 InnoDB 的日志体系。
InnoDB 的 ACID 并不是一个抽象概念,而是由具体组件组成的:
- 原子性靠 Undo Log 实现。事务执行过程中,旧数据先写进 Undo Log,如果事务回滚,就按 Undo Log 里的记录把数据恢复回去。
- 持久性靠 Redo Log 实现。事务提交时,先把变更记录写入 Redo Log 的磁盘文件(顺序写,很快),再异步把数据页刷入磁盘。即使数据库崩溃,重启时可以用 Redo Log 重放,保证提交过的事务不丢。
- 隔离性靠锁和 MVCC(多版本并发控制)共同实现。
- 一致性是前三者的整体目标,由应用层的约束和事务的原子性共同保证。
有一次我帮朋友排查一个库存扣减异常,发现扣款成功后库存依然没变。看了代码发现事务方法里调用了另一个系统,那个系统超时抛了异常,但异常被 catch 吞掉了,事务没识别到失败,直接提交了。这个问题的本质不是数据库的锅,而是事务边界里混入了不可控的外部调用。这里的经验是:事务内不要调用远程接口,不要做耗时操作。
4.2 四种隔离级别与各自的代价
SQL 标准定义了四种隔离级别,分别对应三个异常现象的取舍。我把它们整理成一张表:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现机制 | 默认级别 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 不加锁直接读最新版本 | 否 |
| READ COMMITTED | 不可能 | 可能 | 可能 | 每次快照读都取最新已提交版本 | 部分数据库默认 |
| REPEATABLE READ | 不可能 | 不可能 | 可能(InnoDB 通过间隙锁基本解决) | 首次快照读后在事务内复用快照 | MySQL 默认 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 读写全部加锁,串行化 | 否 |
这里有个容易混淆的点:MySQL 的 REPEATABLE READ 其实提供了快照读,所以普通 SELECT 在事务内是稳定一致的。但如果是当前读(加了 FOR UPDATE 或 LOCK IN SHARE MODE),且范围内有间隙没有索引,仍然可能产生幻读。InnoDB 通过间隙锁(Gap Lock)和Next-Key Lock在默认隔离级别下把幻读问题压到了很低的发生概率,但并没有彻底消灭。彻底消灭只有 SERIALIZABLE。
我建议业务系统保持默认的 REPEATABLE READ,不要随意改成 READ COMMITTED。虽然 READ COMMITTED 能减少部分锁竞争,但某些对账逻辑和状态机流转,依赖的是可重复读的快照一致性。
4.3 行锁升级到表锁的三种典型场景
行锁是 InnoDB 的看家本领,但它在三种场景下会悄悄升级成表锁,导致并发能力骤降:
第一种,没有索引的更新条件。UPDATE t SET status=1 WHERE name='xx',如果 name 没索引,InnoDB 找不到具体行,只能锁住全表。记住:行锁是基于索引的,索引没命中,行锁就是空谈。
第二种,升锁竞争。两个事务分别更新不同行,但恰好这些行落在同一个索引间隙里,就可能因为间隙锁互相阻塞。这是 REPEATABLE READ 下的特有现象,难排查,通常表现为“两个互不相干的事务就是跑不完”。
第三种,外键约束检查。子表插入或更新时,需要对外键对应的父表行加共享锁。如果父表主键上并发操作多,子表写入会被大面积阻塞。
我在实践中给团队的硬性要求是:所有 UPDATE/DELETE 语句的 WHERE 条件必须能明确命中索引,否则直接挡在代码评审阶段。这是避免线上大面积锁表的最有效手段。
5. 备份恢复与日常权限管理实战
5.1 逻辑备份与物理备份的正确用法
备份这件事,平时没人觉得重要,等数据没了才知道值多少钱。MySQL 备份方案分两大类:逻辑备份用 mysqldump 导出 SQL 文本,物理备份用工具直接拷贝数据文件。
对于中小型系统,我推荐每周一次全量 mysqldump 加每日 binlog 增量。mysqldump 的标准命令是:
mysqldump -h 127.0.0.1 -uroot -p \ --single-transaction \ --routines --triggers --events \ --set-gtid-purged=OFF \ mydb > mydb_$(date +%F).sql--single-transaction参数对 InnoDB 至关重要。它会让 mysqldump 基于事务开启一次一致的快照备份,而不是对所有表加 READ LOCK。这样就可以在业务写入不停滞的情况下完成备份。
恢复时的命令也很简单:
mysql -h 127.0.0.1 -uroot -p mydb < mydb_2024-06-01.sql但真正的重点在于演练。我见过太多团队,备份脚本跑了一年,从未做过恢复演练。等到真出故障时,发现备份文件因为磁盘空间不足早就写了一半就中断了,或者恢复时因为字符集不一致乱码。我的经验是:每季度至少做一次“从备份恢复一个全新环境”的演练,确认恢复后的数据完整性。
5.2 利用 binlog 找回误删的数据
MySQL 的 binlog 是二进制日志,记录所有变更操作。开启方式是在 my.cnf 配置:
server-id = 1 log-bin = /var/log/mysql/mysql-bin binlog_format = ROW expire_logs_days = 7binlog_format = ROW这里特别说一句,它会记录每一行变更前后的具体值,而不是像 STATEMENT 模式只记录一条 SQL。ROW 模式对数据恢复非常友好,我们可以直接用 mysqlbinlog 解析出误操作前的数据。
实际恢复场景中,某个开发者误执行了不带 WHERE 的 UPDATE,把整张表的状态字段全改错了。恢复步骤如下:
- 先定位误操作时间点前后的 binlog 文件:
mysqlbinlog --start-datetime="2024-06-01 09:00:00" \ --stop-datetime="2024-06-01 10:00:00" \ /var/log/mysql/mysql-bin.000128 > /tmp/recover.sql在解析出的 SQL 中找到对应的误操作语句,确认 start-position 和 stop-position。
用反向逻辑构造补偿语句。如果是 UPDATE 改错,就根据 binlog 里记录的旧值,把数据反更新回去。
这里有个我在实操中反复栽过跟头的细节:binlog 解析出来的 SQL 不要直接导入生产环境。先导入到一张临时表或者全新库里去比对数据,确认无误后再做二次变更。
5.3 用户权限的最小化分配
权限管理看似和性能无关,实则和安全直接相关。我看到过很多公司的生产库,全部应用连的是 root 账号。一旦某个应用的配置泄露,等于把整个数据库拱手送人。
正确的分级是:
- DBA 使用 root 或具备 ALL PRIVILEGES 的账号,日常运维和变更。
- 业务应用使用只具备 SELECT/INSERT/UPDATE/DELETE 权限的专用账号。
- 只读分析系统使用 SELECT 权限账号。
- 日常创建账号时指定 IP 白名单,避免账号在任何机器上都能登录。
创建业务账号的规范示例:
CREATE USER 'app_user'@'10.0.0.%' IDENTIFIED BY '强密码'; GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'10.0.0.%'; FLUSH PRIVILEGES;注意这里10.0.0.%限定了只允许内网网段连接。我在某次安全扫描后就强制把所有应用账号的 host 从%改成了具体的网段,安全收益非常明显。
6. 常见问题与慢查询排查技巧实录
6.1 超实用问题速查表
在带新人和处理工单的过程中,我把最常出现的问题整理成了一张排查表。当你遇到异常时,按图索骥,90% 的坑都能快速定位。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 连接数打满,客户端报 too many connections | 应用连接池配置过大或慢查询堆积 | SHOW STATUS LIKE 'Threads_connected';SHOW PROCESSLIST | 调小连接池上限;优化慢 SQL;设置 max_connections 合理值 |
| SQL 执行突然变慢,explain 显示走索引但扫描行数暴涨 | 统计信息过期,优化器选错执行计划 | ANALYZE TABLE 表名;刷新统计信息 | 定期维护统计信息;必要时用 FORCE INDEX 强制走索引 |
| 插入数据超时,大量事务阻塞 | 行锁等待或间隙锁死锁 | SHOW ENGINE INNODB STATUS\G查看 LATEST DETECTED DEADLOCK | 排序业务事务的加锁顺序;缩短事务持续时间 |
| 中文乱码 | 客户端、连接、库表字符集不一致 | SHOW VARIABLES LIKE 'character_set_%' | 统一设为 utf8mb4,重连客户端 |
| 数据库磁盘被占满,写入直接失败 | 日志膨胀或 binlog 未清理 | du -sh /var/lib/mysql/*逐层定位 | 清理过期 binlog;开启日志自动清理;扩展磁盘 |
| 主从同步中断,Slave_SQL_Running 为 No | 从库执行了冲突 DDL 或数据不一致 | SHOW SLAVE STATUS\G查看 Last_Error | 跳过该错误或重新初始化从库对应数据 |
这张表里的每一条,我都实际见过并且处理过。特别是第一条“连接数打满”,在电商大促场景尤其常见。有一次某应用的默认连接池上限是 200,两个服务一扩容,直接把这个数字吃满,连带其他应用连接失败。排查起来其实不复杂,SHOW PROCESSLIST一眼就能看到是哪些 SQL 占着连接不释放。
6.2 慢查询日志的开启与解读
慢查询日志是我最依赖的诊断入口之一。开启方式有两种,一种是持久化到配置文件,一种是运行时动态设置。推荐配置文件方式,重启也不会丢:
slow_query_log = 1 slow_query_log_file = /var/log/mysql/slow.log long_query_time = 1 log_queries_not_using_indexes = 1long_query_time = 1表示超过 1 秒的 SQL 会记录下来。log_queries_not_using_indexes会额外记录所有没走索引的查询,帮助你发现那些“虽然快但隐患大”的语句。
拿到慢查询日志后,先看两个指标:Rows_examined(扫描行数)和Rows_sent(返回行数)。两者的比值是判断是否全表扫描最直观的依据。曾经有一条报表 SQL,扫描了 800 万行只返回 20 行,一看就是索引缺失的问题。补上联合索引后,总耗时从 12 秒降到 30 毫秒,数据量完全没变,这就是索引的威力。
6.3 避免死锁的几个实战习惯
死锁在 InnoDB 高频事务系统里并不罕见,但绝大多数是可以避免的。
最常见的是两个事务以相反顺序更新同一组记录。比如事务 A 先更新订单 id=1 再更新 id=2,事务 B 先更新订单 id=2 再更新 id=1,两个事务互相持有对方的锁且都在等对方的锁,死锁就发生了。MySQL 检测到死锁后,会自动回滚其中一个事务,并抛出 ERROR 1213。
我的团队在代码规范里立了一条死规则:同一事务内,所有涉及多行更新的操作,必须按固定的业务维度排序加锁。比如统一按主键 ID 升序更新,这样就永远不会出现循环等待。
另一个容易忽略的细节是间隙锁死锁。两个事务同时向某个范围内插入数据,即使插入的不是同一行,也可能因为间隙锁的冲突而死锁。在业务允许的情况下,可以把隔离级别调成 READ COMMITTED(如果你能确认业务不依赖可重复读快照)。但这条改动必须有充分的测试论证,不要随意在生产环境变换隔离级别。
7. 实操心得:从入门到形成肌肉记忆
最后聊点方法论。MySQL 基础这件事,背概念是没有用的,必须落到命令行和代码里。我自己带新人时,会要求他们做三件事:
第一,用命令行操作,不要依赖 GUI。不是 GUI 不好,而是命令行会逼着你理解每一条 SQL 的真实行为和输出结果。EXPLAIN、SHOW PROFILE、PROCESSLIST 这些运维命令,GUI 大多包装得太好,反而不利于形成直觉。
第二,养成先 EXPLAIN 后上线的好习惯。新写的查询,无论多简单,先在测试环境 EXPLAIN 一把,确认 type 不是 ALL,key 不为 NULL,再提交代码评审。这个习惯帮我挡掉过至少十次线上慢查询事故。
第三,给每次故障做复盘文档。把误操作、死锁、锁等待、备份恢复失败这些事件记录成文档,每个季度更新一次。技术能力就是在一次次的复盘和修补中牢固起来的。
MySQL 基础说深不深,说浅不浅,但它是一栋大楼的地基。你在上面盖的是 100 行代码的小工具,还是百万级并发的交易系统,考验的都是地基的扎实程度。别急着冲进复杂的架构,先把每一条 SQL 的走法看清楚,把 InnoDB 的每一条日志弄明白——这些功夫永远不会白费。