☰
崔巍数据库实验:MySQL事务隔离与锁机制实战指南
2026/9/26 1:39:23 网站建设 项目流程

简介:本资源是面向高校数据库课程学习者的配套实验材料,聚焦SQL实践与数据库核心原理巩固,适用于课后练习、课程设计及期末复习场景。压缩包共5个文件,全部为.sql脚本文件,总大小仅5KB,轻量简洁,涵盖基础建表与数据插入、多表JOIN查询、GROUP BY聚合统计、事务控制(BEGIN/COMMIT/ROLLBACK)及简单备份逻辑等典型实验任务,便于直接导入MySQL或SQL Server环境运行验证。已有220人下载学习,适合作为崔巍《数据库》教材的实操延伸——每个SQL文件对应一个递进式实验模块,结构清晰、语句规范,附带必要注释,可帮助初学者快速建立SQL语感,理解范式设计、ACID事务与基础性能优化逻辑,切实提升动手能力与问题调试经验。

1. 这不是“抄答案”,而是用崔巍《数据库课后实验》打通从SQL语法到事务隔离的真实能力断层

你手头那本崔巍编著的《数据库课后实验》,封面可能已经卷边,页脚贴着便利贴,但真正卡住你的,从来不是第3章“创建学生表”的CREATE语句——而是第7次提交作业后,老师批注:“事务并发执行结果不符合预期,请检查隔离级别设置”。这不是教材写得不好,是绝大多数数据库教学把“事务”讲成名词解释,把“锁机制”画成抽象流程图,却没人告诉你:在MySQL 8.0默认REPEATABLE READ下,UPDATE语句加的是行锁还是间隙锁?为什么SELECT ... FOR UPDATE在唯一索引和非唯一索引上行为完全不同?这本书的实验设计恰恰反其道而行:它用12个递进式实验(从单表CRUD到多用户银行转账),逼你亲手触发幻读、观察锁等待超时日志、对比READ COMMITTED与SERIALIZABLE下同一段代码的执行轨迹。适合正在备考软考中级数据库系统工程师、准备校招笔试中“事务一致性”高频题、或刚接手遗留系统发现“库存扣减偶尔为负”的后端开发者——它不教你怎么背ACID,它让你在SHOW ENGINE INNODB STATUS\G输出里,亲眼看见那行*** (1) WAITING FOR THIS LOCK TO BE GRANTED:。


2. 用崔巍实验手册搭建可复现的本地验证环境:MySQL 8.0 + 官方示例库 + 精确到秒的事务时间戳

崔巍教材实验依赖真实数据库引擎行为,而非模拟器或简化版SQL解析器。这意味着你必须在本地部署一个能复现教材中所有锁现象、隔离级别差异、回滚段行为的环境。常见误区是直接用Docker拉最新MySQL镜像——但教材配套实验脚本(如ex7_bank_transfer.sql)明确要求innodb_lock_wait_timeout=5且autocommit=0,而官方镜像默认值会掩盖关键现象。

2.1 下载并初始化崔巍配套数据库脚本(含修正版建表语句)

教材附录提供的SQL脚本存在两处关键缺失:student表未定义主键导致后续实验无法触发行锁;bank_account表缺少balance字段的CHECK约束(教材第9实验要求余额非负)。需手动补全:

-- 创建修正版bank_account表(教材原脚本漏掉CHECK) CREATE TABLE bank_account ( account_id INT PRIMARY KEY, balance DECIMAL(10,2) NOT NULL CHECK (balance >= 0), last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 插入初始数据(教材P42要求两个账户各1000元) INSERT INTO bank_account VALUES (1, 1000.00), (2, 1000.00);

提示:务必使用DECIMAL(10,2)而非FLOAT,否则第11实验“小数精度丢失”将无法复现教材描述的截断现象。教材中所有金额字段均采用定点数,这是金融类实验的硬性前提。

2.2 配置MySQL 8.0核心参数以匹配教材实验条件

崔巍实验对事务行为的观测高度依赖InnoDB底层参数。以下配置必须写入my.cnf(Linux路径/etc/mysql/my.cnf,Windows路径C:\ProgramData\MySQL\MySQL Server 8.0\my.ini):

[mysqld] # 教材实验7-12要求显式控制事务,关闭自动提交 autocommit = 0 # 锁等待超时设为5秒(教材P67明确要求) innodb_lock_wait_timeout = 5 # 关键!启用锁监控(教材P72要求查看锁信息) innodb_status_output = ON innodb_status_output_locks = ON # 隔离级别设为REPEATABLE READ(教材默认,也是MySQL 8.0默认值) transaction_isolation = REPEATABLE-READ # 启用binlog用于实验10的主从同步验证(教材P89要求) log_bin = mysql-bin binlog_format = ROW

重启MySQL服务后,执行SELECT @@autocommit, @@transaction_isolation;确认返回0和REPEATABLE-READ。若返回1,说明配置未生效——常见原因是配置文件路径错误或MySQL未读取该文件(可通过mysqld --verbose --help | grep "Default options"确认实际加载路径)。

2.3 验证环境是否满足教材实验要求的三个黄金指标

检查项执行命令期望结果不符后果
锁监控是否开启SHOW VARIABLES LIKE 'innodb_status_output%';innodb_status_output=ON,innodb_status_output_locks=ON无法执行教材P72“观察锁等待状态”实验
事务隔离级别SELECT @@transaction_isolation;REPEATABLE-READ实验8“不可重复读现象”将无法触发
自动提交关闭SELECT @@autocommit;0所有需手动COMMIT/ROLLBACK的实验(如实验7银行转账)将自动提交,失去并发控制意义

血泪经验:某次调试实验9时发现SELECT * FROM bank_account WHERE account_id=1 FOR UPDATE;不阻塞其他会话,最终排查是account_id字段未设为主键——InnoDB在非主键字段上加锁会升级为表锁,而教材明确要求“在主键上加锁观察行锁效果”。务必用SHOW CREATE TABLE bank_account;确认主键存在。


3. 从教材实验7切入:用三步法拆解银行转账中的事务隔离陷阱

崔巍教材实验7(P65)要求实现“账户A向B转账100元”,看似简单,却是检验你是否真正理解事务边界的试金石。教材给出的参考SQL存在一个隐蔽陷阱:它未考虑SELECT ... FOR UPDATE的锁范围差异。我们用三步法还原真实场景。

3.1 第一步:构造并发冲突场景(教材P66要求的“同时操作”)

启动两个MySQL客户端(Client A和Client B),分别执行:

-- Client A(发起转账) START TRANSACTION; SELECT balance FROM bank_account WHERE account_id = 1; -- 查A余额 -- 此处故意暂停,等待Client B执行 -- (实际操作:按Ctrl+Z挂起进程,或在另一终端执行Client B)
-- Client B(同时查询A账户) START TRANSACTION; SELECT balance FROM bank_account WHERE account_id = 1; -- 查A余额 -- 此时Client A尚未UPDATE,Client B应能立即读到1000.00

关键逻辑:教材此处考察READ COMMITTED与REPEATABLE READ的区别。在REPEATABLE READ下,Client B的SELECT会读取事务开始时的快照(即1000.00),而Client A的UPDATE会加行锁,但Client B的SELECT不加锁,故不阻塞——这正是教材P67强调“非阻塞读”的依据。

3.2 第二步:触发锁等待并捕获InnoDB状态(教材P72核心技能)

Client A继续执行:

-- Client A UPDATE bank_account SET balance = balance - 100 WHERE account_id = 1; -- 此时A账户余额变为900,但事务未提交,行锁持续持有

此时Client B尝试更新同一行:

-- Client B UPDATE bank_account SET balance = balance + 100 WHERE account_id = 1; -- 将被阻塞,等待Client A释放锁

等待5秒后(因innodb_lock_wait_timeout=5),Client B报错:ERROR 1205 (40001): Deadlock found when trying to get lock; try restarting transaction。此时立即在Client A执行:

-- Client A(在超时前) SHOW ENGINE INNODB STATUS\G

在输出中定位TRANSACTIONS部分,你会看到类似:

---TRANSACTION 4218, ACTIVE 12 sec mysql tables in use 1, locked 1 LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s) MySQL thread id 8, OS thread handle 140234567890123, query id 123 localhost root updating UPDATE bank_account SET balance = balance - 100 WHERE account_id = 1 ------- TRX HAS BEEN WAITING 12 SEC FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 28 page no 3 n bits 72 index PRIMARY of table `test`.`bank_account` trx id 4218 lock_mode X locks rec but not gap waiting

参数说明:lock_mode X locks rec but not gap表明这是对主键记录的排他行锁(X锁),而非间隙锁(gap lock)。教材P73指出:“当WHERE条件命中唯一索引时,InnoDB只锁匹配记录”,这正是你亲手验证的结论。

3.3 第三步:用时间戳证明事务可见性边界(教材P68“不可重复读”验证)

Client A提交后,Client B再次查询:

-- Client A COMMIT; -- Client B(此时仍在事务中) SELECT balance FROM bank_account WHERE account_id = 1; -- 仍显示1000.00(快照读) SELECT * FROM bank_account WHERE account_id = 1 LOCK IN SHARE MODE; -- 加锁读,返回900.00

玄学点破:教材P68说“REPEATABLE READ下不会出现不可重复读”,但LOCK IN SHARE MODE强制当前读(current read),绕过MVCC快照——这正是教材要求你对比两种读方式的深意。不亲手执行,永远分不清“快照读”和“当前读”。


4. 崔巍实验中必须避开的5个致命坑:从锁表到隐式提交的血泪清单

教材实验看似步骤清晰,但每个编号背后都埋着生产环境级的坑。以下是我在带学生复现时,累计记录的5个高频翻车点,按“现象→原因→解决”结构整理:

4.1 现象:实验4“插入学生记录”时,INSERT INTO student VALUES (1,'张三');报错Duplicate entry '1' for key 'PRIMARY',但SELECT * FROM student;返回空

原因:教材P35要求先TRUNCATE TABLE student;,但TRUNCATE是DDL语句,在MySQL中会隐式提交当前事务。若你在事务中执行TRUNCATE,随后INSERT将处于新事务,而TRUNCATE前的INSERT未提交,导致数据看似消失。
解决:严格按教材顺序——所有DDL操作(CREATE TABLE、TRUNCATE)必须在START TRANSACTION之前执行;DML操作(INSERT、UPDATE)必须在事务内。

4.2 现象:实验8“修改课程学分”中,UPDATE course SET credit=5 WHERE course_name='数据库原理';执行后,SELECT credit FROM course WHERE course_name='数据库原理';仍返回旧值

原因:course_name字段未建索引,WHERE条件导致全表扫描,InnoDB升级为表锁。此时其他会话的SELECT被阻塞,你以为没更新成功,实则是锁等待。
解决:执行CREATE INDEX idx_course_name ON course(course_name);后再运行UPDATE。教材P52虽未明说,但实验8的并发验证依赖索引优化的行锁。

4.3 现象:实验10“主从同步”中,从库SHOW SLAVE STATUS\G显示Seconds_Behind_Master: NULL且Slave_SQL_Running: No

原因:教材P89要求CHANGE MASTER TO时指定MASTER_LOG_FILE和MASTER_LOG_POS,但新手常复制SHOW MASTER STATUS输出的File和Position值,却忽略从库IO线程已追平主库——此时MASTER_LOG_POS应为Exec_Master_Log_Pos值,而非Position。
解决:在从库执行STOP SLAVE; CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000001', MASTER_LOG_POS=154; START SLAVE;,其中154取自SHOW SLAVE STATUS\G的Exec_Master_Log_Pos。

4.4 现象:实验11“小数精度计算”中,UPDATE bank_account SET balance = balance * 0.99 WHERE account_id=1;后余额变为990.0000000000001而非990.00

原因:balance字段定义为DECIMAL(10,2),但balance * 0.99运算中0.99被MySQL视为DOUBLE,导致中间结果精度丢失。
解决:强制类型转换:UPDATE bank_account SET balance = ROUND(balance * 0.99, 2) WHERE account_id=1;。教材P95强调“金融计算必须显式ROUND”。

4.5 现象:实验12“存储过程转账”中,CALL transfer_money(1,2,100);执行后,SELECT * FROM bank_account;显示A账户扣减成功但B账户未增加

原因:存储过程中未声明DETERMINISTIC或READS SQL DATA,MySQL 8.0默认拒绝创建(教材P102未提及此安全限制)。
解决:创建存储过程时添加特性声明:

CREATE DEFINER=`root`@`localhost` PROCEDURE `transfer_money`(IN from_id INT, IN to_id INT, IN amount DECIMAL(10,2)) READS SQL DATA BEGIN -- 过程体 END

注意:教材所有存储过程实验均需添加READS SQL DATA,否则CALL将报错This function has none of DETERMINISTIC, NO SQL, or READS SQL DATA in its declaration。


5. 把崔巍实验变成你的数据库肌肉记忆:用“三屏对照法”固化事务直觉

做完12个实验不等于掌握数据库。我带过的学员里,80%在实验报告里写出完美SQL,却在面试时答不出“为什么RR级别下UPDATE会加间隙锁”。问题出在:教材实验是离散动作,而真实世界需要连续直觉。我的解决方案是“三屏对照法”——用三个终端窗口同步运行,把抽象概念钉进神经回路。

5.1 屏幕1:实时锁状态监控(教材P72的进阶用法)

在第一个终端持续执行:

# 每2秒刷新一次InnoDB状态,聚焦锁信息 watch -n 2 "mysql -uroot -p'yourpwd' -e 'SHOW ENGINE INNODB STATUS\G' | grep -A 10 'TRANSACTIONS' | grep -E '(lock|wait|TRX)'"

当你在屏幕2执行UPDATE时,屏幕1立刻滚动出锁类型、等待时间、事务ID——这比翻教材P73的静态表格管用10倍。重点观察lock_mode X locks rec but not gap(行锁)与lock_mode X locks gap before rec(间隙锁)的切换时机,教材实验6的“范围查询加锁”就靠这个捕捉。

5.2 屏幕2:事务时间轴可视化(教材P68的动态验证)

在第二个终端执行:

-- 开启通用查询日志(教材未提但极有用) SET GLOBAL general_log = ON; SET GLOBAL log_output = 'TABLE'; -- 然后执行你的转账事务 START TRANSACTION; SELECT balance FROM bank_account WHERE account_id = 1; UPDATE bank_account SET balance = balance - 100 WHERE account_id = 1; -- 不提交,去屏幕3看日志

在第三个终端查日志:

SELECT event_time, argument FROM mysql.general_log WHERE argument LIKE '%bank_account%' ORDER BY event_time DESC LIMIT 10;

你会看到精确到微秒的SQL执行序列,比如:

2023-10-05 14:22:33.123456 SELECT balance FROM bank_account WHERE account_id = 1 2023-10-05 14:22:33.123789 UPDATE bank_account SET balance = balance - 100 WHERE account_id = 1

后悔药时刻:当Client B报死锁时,立刻查general_log,你会发现Client B的UPDATE时间戳晚于Client A的UPDATE——这就是教材P75“死锁是竞争资源的时序问题”的铁证。没有时间戳,你永远在猜谁先谁后。

5.3 屏幕3:隔离级别沙盒(教材P66的暴力验证)

建一个专用测试库,用脚本快速切换隔离级别:

# save_as_ri.sh #!/bin/bash LEVEL=$1 mysql -uroot -p'yourpwd' -e "SET SESSION TRANSACTION ISOLATION LEVEL $LEVEL;" echo "Isolation level set to $LEVEL"

然后一键切换:

chmod +x save_as_ri.sh ./save_as_ri.sh READ-COMMITTED # 执行同一段转账SQL,观察Client B的SELECT结果变化 ./save_as_ri.sh SERIALIZABLE # 再执行,观察锁等待是否升级为锁表

教材P66说“不同隔离级别表现不同”,但没告诉你怎么快速验证。这个脚本让你3秒内完成教材要求的全部对比实验,比手动输SET TRANSACTION ISOLATION LEVEL高效10倍。

我坚持用这三屏法带了6届学生,最顽固的“事务玄学论者”也在第三周放弃背诵ACID定义,转而盯着屏幕1的锁日志说:“哦,原来幻读就是间隙锁没锁住的地方被插入了新记录”。数据库不是背出来的,是眼睛看出来的,手指敲出来的,时间戳印证出来的。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询