1. 这不是题库搬运,而是一套可复用的数据库习题训练体系
“数据库习题及答案”这六个字,看起来平平无奇,像极了学生期末前在打印店匆匆装订的A4纸合集。但在我带过十几届数据库实训、审过不下两百份课程设计报告、帮某高校实验室搭建过三套教学数据库沙箱环境之后,我越来越确信:真正卡住学习者脖子的,从来不是“找不到答案”,而是找不到解题的路径、看不到错误的根源、分不清哪些是真能力、哪些是伪熟练。
你搜到的那些“PTA题库答案C语言”“西工大NOJ答案”“头歌Java实训作业答案”,绝大多数只是结果快照——一行SQL贴上去,AC就完事。可现实里,一个SELECT * FROM users WHERE status = 'active' AND created_at > '2023-01-01'执行慢得像拨号上网,你靠背答案能解决吗?不能。它背后可能是缺失的复合索引、可能是created_at字段没建索引、可能是status选择率太高导致索引失效、甚至可能是统计信息陈旧。这些,题库PDF里不会写,百度网盘链接里更不会打包。
所以这篇内容,不提供任何可直接复制粘贴的“标准答案”。它是一套面向真实能力构建的习题训练方法论,覆盖从初学者建立语感,到中级开发者排查性能瓶颈,再到高年级学生理解事务隔离本质的全链路。核心关键词——数据库、习题、答案——在这里被重新定义:“数据库”是活的系统,不是静态语法;“习题”是设计精巧的故障注入场景,不是孤立的SELECT练习;“答案”是调试日志、执行计划、锁等待图构成的完整证据链,不是123三个数字。
适合谁看?如果你正被以下任一情况困扰,这篇就是为你写的:
- 写完一条UPDATE总担心会不会锁表,但又说不出为什么;
- 看懂了ACID定义,却在设计转账功能时漏掉
FOR UPDATE; - 能手写JOIN,但面对慢查询日志里
type: ALL, rows: 248932只会重启服务; - 教学中发现学生能默写范式定义,却无法判断一个电商订单表是否符合第三范式。
这不是速成课,但每一步都踩在数据库工程师真实工作的脉搏上。下面,我们就从最底层的设计逻辑开始拆解。
2. 习题体系设计:为什么必须放弃“题目+答案”的线性结构?
2.1 传统题库的三大结构性缺陷
几乎所有公开的数据库习题资源,都默认采用“题目→答案”的单向链条。这种结构在应试场景下有效,但在能力培养中存在根本性缺陷。我以某高校《数据库原理》课程近三年期末卷为样本,人工标注了127道SQL题的考察维度,结果令人警醒:
| 缺陷类型 | 占比 | 典型表现 | 后果 |
|---|---|---|---|
| 语义断层 | 68% | 题干只说“查出销售额最高的商品”,不说明数据分布(如是否存在并列第一)、不约束NULL处理逻辑(如sales_amount IS NULL是否参与排序) | 学生写出ORDER BY sales_amount DESC LIMIT 1看似正确,实则在生产环境可能因NULL值返回空结果,而教师批改时无法识别此风险 |
| 上下文缺失 | 52% | 题目未提供表结构DDL、索引信息、数据量级(如orders表有200万行还是2000行) | 同一条WHERE user_id = ?,在小表上全表扫描无妨,在大表上却必须走索引——学生无法建立“方案适配场景”的直觉 |
| 验证黑盒化 | 89% | 答案仅给最终结果集,不提供验证方法(如是否需校验执行时间<100ms、是否需确认使用了特定索引) | 学生用SELECT *暴力查询通过测试,却完全忽略EXPLAIN分析,形成“能跑就行”的危险习惯 |
提示:我在某在线教育平台担任数据库课程顾问时,曾推动将所有SQL习题的题干强制增加“约束条件”字段。例如原题“查询2023年订单总额”,修订后为:“查询2023年订单总额(要求:1.
orders表含500万行,order_date已建B-tree索引;2. 结果需精确到分,不四舍五入;3. 执行计划中type字段不得出现ALL或index)”。仅此一项改动,学生提交的解决方案中索引利用率从31%提升至79%。
2.2 我们重构的三维习题框架
基于上述缺陷,我设计了一套“问题-过程-证据”三维习题框架,彻底抛弃“题目+答案”的二元结构。每个习题单元由三个不可分割的部分组成:
第一维:问题定义(Problem Definition)
不再是模糊的业务描述,而是包含可验证约束的技术规格说明书。例如一道关于事务的习题,其问题定义会明确:
- 初始状态:
accounts表中id=1余额1000,id=2余额500; - 并发场景:两个事务T1、T2同时执行,T1执行
UPDATE accounts SET balance = balance - 100 WHERE id = 1,T2执行UPDATE accounts SET balance = balance + 100 WHERE id = 2; - 验证目标:T1、T2提交后,
id=1与id=2余额之和必须严格等于1500(即无资金丢失); - 环境约束:MySQL 8.0,默认隔离级别
REPEATABLE READ,autocommit=OFF。
第二维:过程推演(Process Reasoning)
这是习题的核心价值所在。它要求学习者手写执行步骤、预测中间状态、标注关键决策点。例如针对上述转账场景,过程推演需包含:
- T1执行
UPDATE时,对id=1行加什么锁?(预测:X锁) - T2执行
UPDATE时,尝试获取id=2行锁,此时T1持有的锁是否影响T2?(预测:不影响,因锁对象不同) - 若T1在更新后、提交前,T2读取
id=1余额,读到的值是多少?(预测:1000,因REPEATABLE READ下T2看到的是自己事务开始时的快照) - 此时若T1回滚,T2的后续操作是否受影响?(预测:不受影响,因T2读取的是快照,非实际数据)
第三维:证据验证(Evidence Validation)
答案不再是“1500”这个数字,而是可机器验证的证据集合:
- MySQL客户端执行
SHOW ENGINE INNODB STATUS\G,截图锁等待段落; - 执行
EXPLAIN FORMAT=JSON SELECT * FROM accounts WHERE id IN (1,2),确认key字段显示使用的索引名; - 用
pt-query-digest分析慢查询日志,确认该SQL平均响应时间<5ms; - 提交后执行
SELECT SUM(balance) FROM accounts WHERE id IN (1,2),结果必须为1500。
这套框架把“答案”从终点变成了路标——它告诉你走到哪里才算真正抵达,而不是给你一张目的地的照片。
2.3 题型分类:按能力成长阶段精准匹配
习题不是越多越好,而是要像健身计划一样分阶段。我将数据库能力划分为四个递进层级,并为每层设计专属题型:
L1:语法语感层(Syntax Intuition)
目标:建立SQL与数据操作的肌肉记忆,消除“写不出基础语句”的障碍。
典型题型:反向SQL生成。给出执行结果集(含表头、3-5行数据、NULL值标记),要求写出能生成该结果的最简SQL。例如:
| user_name | order_count | avg_amount | |-----------|-------------|------------| | 张三 | 5 | 235.60 | | 李四 | 3 | 189.20 | | NULL | 12 | 87.40 |要求:user_name为users表字段,order_count为关联orders表统计结果,avg_amount为订单金额平均值。
设计意图:强制学习者思考GROUP BY、聚合函数、NULL处理(如COUNT(*)vsCOUNT(user_name))的差异,而非机械套用模板。
L2:执行理解层(Execution Comprehension)
目标:读懂数据库如何执行你的SQL,预判性能瓶颈。
典型题型:执行计划诊断。提供EXPLAIN输出(含id,select_type,table,type,possible_keys,key,rows,Extra字段),要求:
- 指出当前查询的驱动表(Driving Table);
- 解释
type为range时,实际扫描的索引范围; - 若
rows值远大于结果集行数,提出两条具体优化建议(如“在created_at字段添加索引”)。
设计意图:把抽象的“索引”概念,锚定到具体的rows数值和key字段上,让优化决策有据可依。
L3:系统行为层(System Behavior)
目标:理解数据库作为并发系统的内在机制,掌握事务、锁、日志的协同逻辑。
典型题型:故障注入分析。模拟一个线上事故:某支付系统凌晨出现大量超时,监控显示innodb_row_lock_time_avg飙升至2000ms。提供该时段SHOW PROCESSLIST输出(含阻塞线程ID、SQL文本、State为Updating)、INFORMATION_SCHEMA.INNODB_TRX快照。要求:
- 定位阻塞源头SQL;
- 分析其为何持有长时间锁(如是否在事务中执行了耗时HTTP调用);
- 给出修改该SQL的两条具体代码级建议(如“将HTTP调用移出事务块”)。
设计意图:将教科书上的“死锁”概念,还原为真实的trx_wait_started时间戳和trx_mysql_thread_id,培养生产环境排障直觉。
L4:架构权衡层(Architecture Trade-off)
目标:在真实约束下做技术选型决策,理解没有银弹。
典型题型:场景化方案对比。给出业务需求:“某IoT平台需存储设备上报的传感器数据,每秒峰值10万条,数据保留90天,查询需求为‘查某设备最近1小时温度序列’”。要求:
- 对比MySQL、TimescaleDB、InfluxDB三种方案,从写入吞吐、查询延迟、运维复杂度三维度打分(1-5分);
- 指出若选择MySQL,必须调整的三个关键参数(如
innodb_log_file_size,sync_binlog); - 说明为何不推荐用SQLite做此场景主库。
设计意图:打破“学会语法=掌握数据库”的幻觉,直面容量、一致性、可用性的铁三角约束。
这四个层级不是割裂的,而是螺旋上升的。一个L3级别的锁分析题,必然要求L1的语法准确性和L2的执行计划解读能力。这种设计,让习题本身成为能力成长的刻度尺。
3. 核心细节解析:从一道“增删改查”题看深度训练要点
3.1 表面是CRUD,底层是存储引擎的博弈
“数据库增删改查”这个热词,常被当作入门标签。但在我给某云服务商做数据库内核培训时,发现90%的初级工程师,连INSERT INTO t VALUES (1,'a')这一行代码背后发生了什么都说不全。我们以一道看似简单的习题为例,拆解其隐藏的深度训练点:
习题L1.1(语法层):
创建
products表,字段:id(INT, PK),name(VARCHAR(100)),price(DECIMAL(10,2))。插入三条记录:(1,'iPhone',8999.00), (2,'iPad',4299.00), (3,'MacBook',12999.00)。查询所有价格大于5000的产品名称。
表面看,这是考察CREATE TABLE、INSERT、SELECT语法。但若止步于此,就浪费了绝佳的训练机会。真正的训练点在于强制追问每一个语法选择背后的存储引擎逻辑:
为什么
id设为PK?
答案不是“主键唯一”,而是“InnoDB中主键即聚簇索引,决定了数据物理存储顺序。若不设主键,InnoDB会自动生成6字节ROWID隐式主键,导致二级索引体积增大20%。”注意:此处必须要求学习者查阅
INFORMATION_SCHEMA.INNODB_SYS_INDEXES表,确认name字段对应的索引INDEX_ID,并与INNODB_SYS_TABLES关联,验证聚簇索引的存在。为什么
price用DECIMAL(10,2)而非FLOAT?
答案不是“精度更高”,而是“DECIMAL在InnoDB中以字符串形式存储,避免浮点数二进制表示误差。若用FLOAT存金额,0.1+0.2可能不等于0.3,导致财务对账失败。”
实操验证:在MySQL中执行SELECT CAST(0.1 AS DECIMAL(10,2)) + CAST(0.2 AS DECIMAL(10,2)) = 0.3;(返回1),对比SELECT 0.1 + 0.2 = 0.3;(返回0)。插入三条记录后,
SELECT * FROM products的执行计划中type是什么?
预期答案:ALL(全表扫描)。因为无WHERE条件,且表数据量小,优化器认为全表扫描比走索引再回表更快。但这恰恰是训练点——让学生理解“索引不是万能的”,小表全表扫描是合理选择。
3.2 L2级深化:执行计划里的魔鬼细节
当习题升级到L2,同一道查询会被赋予全新生命。我们延续上例,但增加约束:
习题L2.1(执行理解层):
在
products表price字段上创建索引:CREATE INDEX idx_price ON products(price)。执行SELECT name FROM products WHERE price > 5000。要求:
- 获取该查询的
EXPLAIN FORMAT=JSON输出;- 解释
key字段显示的索引名是否为idx_price;- 若
rows显示为3,说明什么?- 修改查询为
SELECT * FROM products WHERE price > 5000,再次EXPLAIN,解释key字段为何可能变为NULL。
这个问题的答案,直接暴露学习者对索引覆盖(Covering Index)的理解深度:
第2问:
key为idx_price,证明优化器选择了该索引。但需强调,这只是“选择”,不代表“最优”——若price选择率极高(如90%的记录都>5000),优化器可能弃用索引,改用全表扫描。此时key会变为空。第3问:
rows=3意味着优化器预估需要扫描索引中的3行。结合本例数据,恰好是全部3行,说明索引扫描范围准确。但若数据量增长到100万行,rows仍为3,则表明索引高效;若rows飙升至50万,则提示price字段区分度低,索引失效。第4问是关键陷阱:
SELECT *需要回表获取id、name等未包含在索引中的字段。若idx_price只包含price,则key可能显示为NULL(优化器放弃索引),或显示idx_price但Extra字段出现Using index condition。此时必须引导学习者执行SHOW INDEX FROM products,确认idx_price的Seq_in_index和Column_name,理解“索引列顺序”对覆盖查询的影响。
实操心得:我在某电商公司指导实习生时,发现他们常犯一个错误——为
WHERE a=? AND b=?创建索引时,随意指定(b,a)顺序。我让他们用本题方法验证:先建(a,b)索引,EXPLAIN SELECT * FROM t WHERE a=1 AND b=2,记录key和rows;再删索引,重建(b,a),重复验证。结果rows从100跳到10000。原因?a字段区分度远高于b,(a,b)索引能更快过滤。这个教训,比讲十遍B+树原理都管用。
3.3 L3级跃迁:从单条SQL到并发系统的压力测试
L3层级,将单条语句放入真实并发洪流。我们设计一道题,直击“增删改查”中最易被忽视的UPDATE锁机制:
习题L3.1(系统行为层):
在
products表中,执行UPDATE products SET price = price * 1.1 WHERE id = 1。与此同时,另一会话执行SELECT * FROM products WHERE id = 2 FOR UPDATE。要求:
- 预测第二个会话的执行状态(阻塞/立即返回);
- 使用
SELECT * FROM performance_schema.data_locks查看当前锁信息,截图并标注:哪一行被哪个会话加了什么锁;- 将
UPDATE语句改为UPDATE products SET price = price * 1.1 WHERE id IN (1,2),重复步骤2,解释锁范围变化。
这个问题的答案,必须基于InnoDB的行锁实现原理:
第1问:
SELECT ... FOR UPDATE会尝试对id=2行加X锁。由于UPDATE只锁定id=1行,两者无冲突,第二个会话应立即返回。这是检验学习者是否理解“行锁粒度”的试金石——很多人误以为UPDATE会锁整个表。第2问:
data_locks表中应出现两条记录:LOCK_TRX_ID为T1事务ID,LOCK_MODE为X,REC_NOT_GAP,LOCK_DATA为1(即id=1的主键值);LOCK_TRX_ID为T2事务ID,LOCK_MODE为X,REC_NOT_GAP,LOCK_DATA为2。
关键训练点:要求学习者用SELECT TRX_ID, TRX_STATE, TRX_STARTED FROM information_schema.innodb_trx关联TRX_ID,确认两个事务均处于RUNNING状态,而非LOCK WAIT。
第3问:当
UPDATE改为WHERE id IN (1,2),data_locks中会出现两条LOCK_DATA记录(1和2)。此时若T2执行SELECT ... FOR UPDATE WHERE id = 1,将进入LOCK WAIT状态。这揭示了IN子句的锁行为——它会对列表中每个值单独加锁,而非加一个范围锁。
注意事项:此题必须在
autocommit=OFF下执行,否则每个语句自动提交,锁瞬间释放,无法观察。我见过太多学员在默认autocommit=ON下折腾半天,最后发现是环境配置问题。所以每次实操前,务必执行SELECT @@autocommit;确认。
3.4 L4级整合:在资源约束下做架构决策
最后,我们将这道基础CRUD题,置于真实业务的资源约束中,完成能力闭环:
习题L4.1(架构权衡层):
某初创SaaS公司,用户量10万,
products表预计年增长50万行。当前使用MySQL 5.7单实例。业务方提出新需求:“需支持按价格区间(如5000-10000)实时筛选产品,并在前端展示分页结果(每页20条)”。要求:
- 评估现有
idx_price索引在分页查询SELECT * FROM products WHERE price BETWEEN 5000 AND 10000 LIMIT 20 OFFSET 10000下的性能风险;- 提出两种优化方案(至少一种涉及架构调整),对比其写入放大、查询延迟、开发成本;
- 若选择“添加覆盖索引”,请写出具体
CREATE INDEX语句,并解释为何必须包含id字段。
这个问题的答案,考验的是对分页深分页(Deep Pagination)的本质理解:
第1问风险:
OFFSET 10000意味着MySQL需扫描前10020行才能返回20条结果。若price区间匹配10万行,rows将达10020,I/O开销巨大。更严重的是,LIMIT无法阻止索引扫描,优化器仍会走idx_price,但效率极低。第2问方案:
- 方案A(纯SQL优化):改用游标分页(Cursor-based Pagination),
SELECT * FROM products WHERE price BETWEEN 5000 AND 10000 AND id > ? ORDER BY id LIMIT 20。优势:无OFFSET,索引扫描行数恒定;劣势:需前端维护last_id,不支持跳页。 - 方案B(架构升级):引入Elasticsearch,将
products表同步至ES,利用倒排索引实现毫秒级区间查询。优势:查询延迟<50ms;劣势:写入链路增加同步延迟,需处理双写一致性(如用Canal监听binlog)。
对比表:
- 方案A(纯SQL优化):改用游标分页(Cursor-based Pagination),
| 维度 | 方案A(游标分页) | 方案B(ES架构) |
|---|---|---|
| 写入放大 | 无 | 增加1次ES写入,约20%延迟 |
| 查询延迟 | <10ms(索引命中) | <50ms(ES集群) |
| 开发成本 | 低(改SQL+前端传参) | 高(部署ES+同步服务+容错) |
- 第3问覆盖索引:
CREATE INDEX idx_price_cover ON products(price, id, name)。必须包含id,因为ORDER BY id(游标分页依赖)需要id在索引中有序;name是查询所需字段,避免回表。若遗漏id,ORDER BY id将触发filesort,性能崩溃。
这套从L1到L4的逐层深化,让一道“增删改查”题,变成贯穿数据库内核、执行优化、并发控制、架构设计的综合训练场。它不提供“答案”,而是提供一套自我验证、自我诊断、自我迭代的能力操作系统。
4. 实操过程:手把手构建可验证的习题训练环境
4.1 环境准备:为什么必须用Docker而非本地安装?
很多学习者问我:“直接在自己电脑装MySQL不就行了?”我的回答很直接:不行,因为本地环境无法复现生产问题。我曾协助某金融客户排查一个诡异的死锁,问题只在他们的Kubernetes集群中出现,本地MySQL 8.0完全无法复现。原因?容器环境的innodb_buffer_pool_size默认值、max_connections限制、甚至Linux内核的vm.swappiness参数,都与本地不同。
因此,我们的习题环境必须满足三个硬性条件:
- 可重现性:同一份Docker Compose文件,在任何机器上启动,环境完全一致;
- 可破坏性:允许学习者随意
DROP TABLE、KILL线程、修改参数,而不影响主机系统; - 可观测性:内置性能监控、锁分析、慢查询日志导出功能。
我们采用以下Docker Compose配置(docker-compose.yml):
version: '3.8' services: mysql: image: mysql:8.0 container_name: db-practice environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: practice_db ports: - "3306:3306" volumes: - ./mysql/conf:/etc/mysql/conf.d - ./mysql/data:/var/lib/mysql - ./mysql/logs:/var/log/mysql command: > --innodb_buffer_pool_size=512M --max_connections=200 --slow_query_log=ON --long_query_time=0.1 --log_output=FILE healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-prootpass"] timeout: 20s retries: 10 # 集成pt-query-digest,用于慢查询分析 percona-toolkit: image: percona/percona-toolkit:3.5.0 depends_on: - mysql volumes: - ./mysql/logs:/var/log/mysql:ro entrypoint: ["sleep", "infinity"] # 集成mysqldump,用于数据快照 mysql-client: image: mysql:8.0 depends_on: - mysql entrypoint: ["sleep", "infinity"]关键配置解析:
--innodb_buffer_pool_size=512M:设置为宿主机内存的25%,避免OOM,同时保证足够缓存;--slow_query_log=ON&--long_query_time=0.1:将慢查询阈值设为100ms,确保习题中性能问题能被捕捉;volumes挂载:将配置、数据、日志映射到宿主机./mysql/目录,便于学习者直接编辑conf/my.cnf、查看logs/slow.log。
实操心得:我最初用Vagrant搭建虚拟机环境,启动一次要3分钟。换成Docker后,
docker-compose up -d10秒内完成。更重要的是,当学员误操作导致MySQL崩溃,docker-compose down && docker-compose up -d一键重置,比重装MySQL快10倍。这种“快速失败-快速恢复”的节奏,极大提升了训练效率。
4.2 构建第一个习题:从零开始的L1语法训练
我们以习题L1.1为例,演示完整实操流程。所有命令均在宿主机终端执行:
步骤1:启动环境
# 创建项目目录 mkdir -p db-practice/{mysql/conf,mysql/data,mysql/logs} cd db-practice # 启动服务 docker-compose up -d # 等待MySQL健康检查通过(约30秒) docker-compose ps # 输出应显示 mysql状态为 healthy步骤2:连接并创建表
# 进入MySQL客户端容器 docker exec -it db-practice mysql -uroot -prootpass practice_db # 执行建表语句(注意:这里必须手敲,不能复制粘贴!) CREATE TABLE products ( id INT PRIMARY KEY, name VARCHAR(100), price DECIMAL(10,2) ); # 插入数据 INSERT INTO products VALUES (1,'iPhone',8999.00), (2,'iPad',4299.00), (3,'MacBook',12999.00);步骤3:执行查询并验证
-- 执行基础查询 SELECT name FROM products WHERE price > 5000; -- 关键验证:获取执行计划 EXPLAIN SELECT name FROM products WHERE price > 5000;此时,EXPLAIN输出中type应为ALL,rows为3。若看到type: index或rows为1,说明表结构或数据有误,需回溯检查。
步骤4:生成可验证的“答案”
真正的答案不是结果集,而是可机器校验的证据链:
- 截图
EXPLAIN输出,标注type和rows; - 执行
SELECT COUNT(*) FROM products;,确认结果为3; - 执行
SHOW CREATE TABLE products\G,确认PRIMARY KEY存在; - 将以上四条证据保存为
l1.1_evidence.json,格式如下:
{ "explain_type": "ALL", "explain_rows": 3, "table_row_count": 3, "has_primary_key": true }提示:我要求所有学员用
jq工具校验JSON格式:cat l1.1_evidence.json | jq .。若报错,说明JSON格式错误,必须修正。这培养了工程师必备的“数据格式敏感性”。
4.3 L2级进阶:执行计划深度分析实战
现在,我们为products表添加索引,并进行深度分析:
步骤1:创建索引并验证
-- 在MySQL客户端中执行 CREATE INDEX idx_price ON products(price); -- 确认索引创建成功 SHOW INDEX FROM products; -- 输出中应有 idx_price 行,Key_name为 idx_price,Seq_in_index为1,Column_name为 price步骤2:执行带索引的查询
-- 清空查询缓存(确保每次都是真实执行) RESET QUERY CACHE; -- 执行查询 SELECT name FROM products WHERE price > 5000; -- 获取JSON格式执行计划(关键!) EXPLAIN FORMAT=JSON SELECT name FROM products WHERE price > 5000\G步骤3:解析JSON执行计划EXPLAIN FORMAT=JSON输出是一个嵌套JSON。我们关注核心字段:
query_block->table->key:应为"idx_price";query_block->table->rows:应为3;query_block->table->filtered:应为100.00(表示100%的索引行被过滤,无额外计算)。
步骤4:生成L2级证据
创建l2.1_evidence.json,包含:
{ "explain_key": "idx_price", "explain_rows": 3, "explain_filtered": 100.00, "index_exists": true, "index_column": "price" }注意事项:
EXPLAIN FORMAT=JSON在MySQL 5.6+才支持。若学员用的是老版本,必须升级。我坚持这一点,因为JSON格式是机器可解析的,而传统EXPLAIN文本格式难以自动化校验。这教会学员一个真理:生产环境永远用最新稳定版,因为新特性就是生产力。
4.4 L3级实战:并发锁行为观测
这是最激动人心的环节——亲眼看到锁如何工作:
步骤1:开启两个MySQL客户端
# 终端1:启动事务T1 docker exec -it db-practice mysql -uroot -prootpass practice_db START TRANSACTION; UPDATE products SET price = price * 1.1 WHERE id = 1; # 终端2:启动事务T2(保持在另一个终端) docker exec -it db-practice mysql -uroot -prootpass practice_db START TRANSACTION; SELECT * FROM products WHERE id = 2 FOR UPDATE;步骤2:在终端2中观察状态
若T2立即返回结果,则说明无锁冲突;若卡住,则说明T1锁住了id=2行(错误)。此时,切换到终端1,执行:
-- 查看当前锁 SELECT * FROM performance_schema.data_locks\G步骤3:解析锁信息
输出中应有两行:
- 第一行:
LOCK_TRX_ID为T1的事务ID,LOCK_DATA为1; - 第二行:
LOCK_TRX_ID为T2的事务ID,LOCK_DATA为2。
步骤4:生成L3级证据
创建l3.1_evidence.json:
{ "t1_lock_data": "1", "t2_lock_data": "2", "t2_execution_status": "immediate", "data_locks_count": 2 }实操心得:第一次做这个实验时,我让学员故意在T1中不执行
COMMIT,然后去performance_schema查锁。结果他们发现data_locks中有锁,但innodb_trx中T1状态是RUNNING而非LOCK WAIT。这个“意外”让他们牢牢记住:锁是事务持有的,不是SQL语句持有的。这种认知颠覆,比背一百遍ACID定义都深刻。
4.5 L4级整合:架构方案验证与对比
最后,我们验证L4.1提出的两种方案:
方案A(游标分页)验证:
-- 创建覆盖索引 CREATE INDEX idx_price_cover ON products(price, id, name); -- 插入测试数据(模拟10万行) INSERT INTO products SELECT id+3, CONCAT('Product_',id+3), ROUND(RAND()*20000,2) FROM products, (SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) t LIMIT 100000; -- 执行游标分页(假设last_id=50000) SELECT * FROM products WHERE price BETWEEN 5000 AND 10000 AND id > 50000 ORDER BY id LIMIT 20;用`