news 2026/10/20 8:09:15

数据库实验八与实验九:并发控制与故障恢复实战详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据库实验八与实验九:并发控制与故障恢复实战详解

数据库系统这门课,做到实验八、实验九的时候,才算是真正开始碰数据库里最核心的两个问题:并发控制和故障恢复。我当年做这两组实验时,最大的感触是——前面写SQL、调索引,更多是在“用数据库”,而到了实验八和实验九,是在“理解数据库”。如果你也是软件工程相关专业的学生,或者正在自学的后端开发者,这两组实验相当于把事务、锁、日志、备份恢复这些东西全部揉在一起考一遍,绕不过去。

实验八和实验九在不同学校的编号安排会略有差异,常见的是实验八对应事务与并发控制,实验九对应备份与故障恢复。这篇博文就按这个组合来复盘,把原理、步骤、踩坑一次性说透。

1. 实验八、实验九到底在做什么

1.1 实验八的定位:用锁和事务回答“并发会不会乱”

并发控制解决的是一个老问题:多个事务同时操作同一批数据,怎么保证结果和串行执行一致。实验八的基本套路,是通过两个或多个并发会话,模拟转账、抢购、库存扣减这类场景,观察隔离级别对结果的影响,再通过锁机制防止数据被写坏。

实验的核心工具是MySQL的InnoDB引擎。你会发现实验指导书里会反复提到隔离级别、共享锁、排他锁、死锁这些词。做实验时你的目标很明确:先让问题发生,再想办法阻止问题发生。脏读、不可重复读、幻读这三类异常,不是背定义背出来的,而是在两个终端同时执行SQL时,眼睁睁看着数据变了才真正理解。

1.2 实验九的定位:用日志和备份回答“数据没了怎么找回来”

故障恢复解决的是另一个问题:数据文件损坏、误删、突然断电,怎么把数据库恢复到某个时间点。实验九的典型流程是定期做备份,开启日志记录,再人为制造故障,用备份和日志把数据恢复回来。

这里要区分两个概念:备份是“静态的副本”,日志是“动态的补丁”。只有全量备份还不够,因为备份之后还有新数据写入;只有日志也不行,因为日志重放需要一个起点。实验九的精髓,就是理解全量备份加日志增量这套组合拳。

1.3 为什么选MySQL InnoDB做这两组实验

很多学校做数据库实验会用SQL Server或者Oracle,但我个人觉得用MySQL InnoDB来做并发控制和故障恢复,是最适合理解的。

一方面,InnoDB的锁机制、MVCC、隔离级别实现得很典型,事务四大特性ACID都有落地的机制,锁等待超时、死锁检测这些行为都可以用命令直接观察。另一方面,MySQL有mysqldump、binlog这一套完整的备份恢复工具链,实验环境好搭,学到的知识放到生产环境同样适用。课程实验的核心是“原理验证”,MySQL能覆盖绝大部分知识点,还不折腾人。

2. 实验八核心细节:隔离级别、锁与死锁

2.1 四种隔离级别不是理论空话

实验指导书里会给一张表:读未提交、读已提交、可重复读、串行化。很多人背完就忘,其实实验里可以用SQL亲自测出来。

MySQL默认隔离级别是可重复读,查看当前隔离级别用这条命令:

SHOW VARIABLES LIKE 'transaction_isolation';

临时修改当前会话隔离级别:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

四种级别分别防什么,可以这样记:

隔离级别脏读不可重复读幻读
读未提交可能可能可能
读已提交防止可能可能
可重复读防止防止主要防止(InnoDB间隙锁处理)
串行化防止防止防止

实验里复现脏读的操作流程是这样的:把两个事务的隔离级别都调到READ UNCOMMITTED,事务A执行update但不提交,事务B在另一个会话里select,就能读到事务A还没提交的数据。这个数据如果最终回滚,那B读到的就是凭空出现的“脏数据”。

不可重复读的复现思路,是在事务B里先select一次,然后事务A把那条数据update并提交,事务B再次select,两次结果不一致。同一个事务里读同一行,两次结果不一样,这在业务上是很要命的——比如统计余额时一会多一会少。

幻读比不可重复读更隐蔽。不可重复读针对的是一行数据被修改,幻读针对的是一批数据条数变化,比如事务A查某个区间的记录,事务B插入了一条新记录并提交,事务A再查时多了一条“幻影行”。InnoDB用间隙锁解决幻读,后面实操部分再展开讲。

2.2 锁的类型:从共享锁到间隙锁

InnoDB的锁可以按粒度分成行级锁和表级锁,按模式分成共享锁和排他锁。共享锁和排他锁的关系,你可以类比图书馆的座位:共享锁是“有人坐但允许别人一起看这本书”,排他锁是“这个人独占,别人碰都不能碰”。

SQL层面最常见的两种加锁语句:

-- 共享锁 SELECT ... LOCK IN SHARE MODE; -- 排他锁 SELECT ... FOR UPDATE;

UPDATE、DELETE、INSERT操作默认都得先拿排他锁。两个事务同时update同一行,后者的update会阻塞,直到前者提交或回滚。

间隙锁是InnoDB在可重复读级别下的特殊机制。当你在一个索引范围上执行查询时,InnoDB不仅锁住已存在的记录,还会锁住记录之间的“间隙”,防止其他事务往这个缝隙里插入新数据。这也是为什么MySQL在高隔离级别下能挡住幻读。

做实验时建议把锁等待时间调短一点,不然卡住后会很浪费时间:

SET SESSION innodb_lock_wait_timeout = 5;

2.3 死锁:数据库也有“互相等对方”的尴尬

死锁是并发实验里最经典的现象。两个事务各自持有一把锁,然后都想拿对方手里的锁,互相不放手,最后困在死循环里。MySQL的InnoDB会自动检测死锁,并且回滚其中一个事务让另一个继续执行。

模拟死锁的经典做法:

-- 事务A START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE id = 1; UPDATE account SET balance = balance + 100 WHERE id = 2; -- 此时不提交 -- 事务B START TRANSACTION; UPDATE account SET balance = balance - 100 WHERE id = 2; UPDATE account SET balance = balance + 100 WHERE id = 1;

如果事务A先锁了id=1,事务B先锁了id=2,接着A想锁id=2,B想锁id=1,就会出现死锁。InnoDB检测到后,会提示“Deadlock found when trying to get lock”,然后回滚一个事务。

避免死锁的思路也很直白:让所有事务按相同顺序操作资源。比如约定“先更新小id,再更新大id”,两个事务就永远不会交叉等待。

3. 实验八实操过程:从建表到死锁复现

3.1 环境和测试表准备

建议用MySQL 8.0,存储引擎选InnoDB。打开两个终端,分别连入同一个数据库,模拟两个并发会话。

先用最简单的一组表演示并发问题:

CREATE DATABASE concurrency_lab; USE concurrency_lab; CREATE TABLE account ( id INT PRIMARY KEY, balance DECIMAL(10, 2) NOT NULL ); INSERT INTO account VALUES (1, 1000.00), (2, 1000.00);

这个表结构足够覆盖转账、扣款、余额查询这些场景。后面做的所有实验都围绕这两行数据展开。

3.2 复现脏读、不可重复读和幻读

脏读的操作步骤:

  1. 会话A执行SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
  2. 会话B也执行同样的设置。
  3. 会话A开启事务,把id=1的balance改成500,不提交。
  4. 会话B查询account表,能看到500。
  5. 会话A执行ROLLBACK。
  6. 会话B再查,数据回到1000。

这个瞬间读到的500就是脏读。

不可重复读的操作步骤:

  1. 两个会话都设成READ COMMITTED。
  2. 会话B开启事务,查询id=1的balance,得到1000。
  3. 会话A开启事务,把id=1的balance改成800并提交。
  4. 会话B在同一个事务里再次查询,得到800。

同一个事务B,两次select得到不同结果,这就是不可重复读。

幻读的实验稍微麻烦一点,建议给account表加一个普通索引,用一个范围来测试。比如往表里插入一些新账户,然后在一个事务里反复执行范围查询,看到条数变化就是幻读。

注意,做这类实验时,开完事务一定要记得提交或回滚,不然锁会一直占着,后面的排查会找不到头绪。

3.3 锁等待和死锁的现场记录

先试一下行锁阻塞:会话A执行UPDATE account SET balance = balance - 100 WHERE id = 1;不提交;会话B执行UPDATE account SET balance = balance - 100 WHERE id = 1;此时这条语句会卡住。

把innodb_lock_wait_timeout设置成5秒后,5秒内不提交事务A,B就会报:

Lock wait timeout exceeded; try restarting transaction

这个报错说明B在等待A释放行锁。

然后试死锁。按上面2.3的方式,两个事务交叉更新两行数据,InnoDB会自动检测并回滚。实验报告里要记录的信息有:死锁发生的时间、涉及的事务id、被回滚的是哪个事务、最后的处理措施是什么。

做死锁实验时,有个经验很实用:先只开两个会话,不要多个窗口同时操作,不然你根本分不清谁锁了哪一行。检查当前有哪些锁可以使用:

SELECT * FROM performance_schema.data_locks;

看这张表可以快速定位某个锁属于哪个事务,在排查时非常管用。

4. 实验九核心细节:备份策略、binlog与崩溃恢复

4.1 逻辑备份和物理备份怎么选

实验九的第一步是理解备份类型。逻辑备份导出的是SQL语句,最典型的工具就是mysqldump。物理备份复制的是数据文件,常见工具是xtrabackup。

对比项逻辑备份物理备份
备份内容建表语句+数据INSERT数据页文件
恢复速度慢,逐条执行SQL快,直接拷贝文件
跨版本兼容较好较差
实验常用是较少

实验环境里用mysqldump就足够了。它的命令也很简单:

mysqldump -u root -p concurrency_lab > backup.sql

只备份某个表也可以:

mysqldump -u root -p concurrency_lab account > account_backup.sql

恢复时执行:

mysql -u root -p concurrency_lab < backup.sql

做项目时还会分全量备份和增量备份。全量备份把整个库都导出来,增量备份只记录从上一次备份之后的变化。实验九里经常是先做一次全量备份,再开启binlog记录之后所有写操作,以便实现按时间点恢复。

4.2 binlog:让恢复精确到秒

binlog是MySQL Server层的二进制日志,记录所有会修改数据的操作。开启binlog后,每次写操作都会追加到日志文件里。

查看binlog是否开启:

SHOW VARIABLES LIKE 'log_bin';

查看当前正在写的binlog:

SHOW MASTER STATUS;

实验九的经典恢复流程是:

  1. 做一次全量备份。
  2. 继续正常进行写操作,binlog记录这些操作。
  3. 模拟误删数据。
  4. 用全量备份恢复到备份时间点。
  5. 用binlog从备份时间点重放到误删前的一瞬间。

binlog有三种格式:STATEMENT、ROW、MIXED。实验里我推荐用ROW格式,因为它记录的是每行数据实际变化,应用binlog时不容易出现函数计算差异。设置方式:

[mysqld] server-id=1 log-bin=mysql-bin binlog_format=ROW

用binlog做恢复的核心命令是mysqlbinlog,后面实操部分会给完整示例。

4.3 redo log和undo log在崩溃恢复中的角色

binlog是逻辑日志,而InnoDB内部还有redo log和undo log。这两个日志是数据库崩溃恢复的关键。

redo log像是“记账本重放”。事务对数据页做的修改,先写redo log,再把脏页刷到磁盘。如果数据库突然断电,内存里的修改丢了,但redo log还在,重启后把redo log里的记录重放一遍,就能恢复到崩溃前状态。

undo log则是“撤销操作记录”。事务回滚时,需要靠undo log把数据还原成旧版本。在可重复读隔离级别下,MVCC也需要借助undo log构造历史版本数据。

简单理解就是:redo log保证“已经提交的事务不丢失”,undo log保证“没提交的事务能撤销”。两者配合,ACID里的持久性和原子性就落地了。

5. 实验九实操过程:从全量备份到按时间点恢复

5.1 开启binlog并完成一次全量备份

先修改MySQL配置文件,在[mysqld]区域添加:

server-id=1 log-bin=mysql-bin binlog_format=ROW expire_logs_days=7

重启MySQL后确认binlog生效:

SHOW VARIABLES LIKE 'log_bin';

接着建一个测试库和表,插入一些数据:

CREATE DATABASE recovery_lab; USE recovery_lab; CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(50) NOT NULL, qty INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); INSERT INTO orders (product_name, qty) VALUES ('苹果', 10), ('香蕉', 20);

做一次全量备份:

mysqldump --single-transaction -u root -p recovery_lab > /tmp/recovery_lab_full.sql

--single-transaction参数表示用InnoDB的一致性快照做备份,不会长时间锁表。这个参数在实验里非常常用,避免了备份过程中阻塞正常写入。

5.2 模拟误删,用备份加binlog恢复

全量备份完成后,继续写入一批数据:

INSERT INTO orders (product_name, qty) VALUES ('橘子', 15); INSERT INTO orders (product_name, qty) VALUES ('西瓜', 5);

这时候我们假设是上午10点,记录一下当前时间。继续执行几条写操作后,故意执行误删:

DROP TABLE orders;

此时数据完全丢失。先创建同名的空库:

CREATE DATABASE recovery_lab;

然后恢复全量备份:

mysql -u root -p recovery_lab < /tmp/recovery_lab_full.sql

此刻orders表恢复到了备份时点的状态,但备份之后插入的“橘子”和“西瓜”还不存在。接下来用binlog补上:

先找到误删操作发生的binlog位置。查看binlog列表:

SHOW BINARY LOGS;

然后用mysqlbinlog定位:

mysqlbinlog --base64-output=DECODE-ROWS -v /var/lib/mysql/mysql-bin.000001 | grep -A 20 'DROP TABLE'

实际操作中,最好把binlog导出成SQL文件,手动查看删除语句前后的时间点:

mysqlbinlog --start-datetime="2025-01-01 09:30:00" --stop-datetime="2025-01-01 10:00:00" \ /var/lib/mysql/mysql-bin.000001 > /tmp/recover_orders.sql

这里面 10:00:00 是误删发生前的时间点。执行恢复:

mysql -u root -p < /tmp/recover_orders.sql

注意恢复前先确认这个SQL文件里没有包含DROP TABLE语句,否则会把刚恢复的数据又删掉。

5.3 恢复后的一致性校验

数据恢复完成后,一定要做校验,不能看一眼表里有数据就完事。最基础的校验是:

USE recovery_lab; SELECT COUNT(*) FROM orders; SELECT * FROM orders ORDER BY id;

对比误删前最后一次查询的记录条数和内容,确保同时包含两个阶段的数据:全量备份时已有的“苹果”“香蕉”,以及binlog补回来的“橘子”“西瓜”。

另外,实验里还要注意自增主键的变化。恢复后的表自增计数器不会自动回到误删前的值,mysqlbinlog重放INSERT时通常能正确推进,但如果只有部分记录,后续新插入的记录可能与历史id冲突。这个问题在常见问题部分专门讲。

6. 常见问题与排查技巧实录

6.1 并发实验时卡住不动,多半是锁等待

做实验八时经常遇到“SQL执行了半天没反应”,十有八九是行了锁。先查performance_schema.data_locks,找到阻塞的锁,再看看自己的事务有没有忘记COMMIT。

排查步骤:

  1. 查看当前正在执行但未完成的事务,通过information_schema.innodb_trx查trx_state、trx_started。
  2. 查看锁等待关系,确定谁在等谁。
  3. 把占用锁的事务提交或回滚后,卡住的SQL会自动继续。

如果确定是自己开的实验事务,直接ROLLBACK最省事。调试并发实验时,我习惯把innodb_lock_wait_timeout改成3秒,避免长时间卡住影响整个实验节奏。

6.2 mysqldump备份时把业务写堵住了

有的同学做备份恢复实验时,在备份命令里加锁参数不对,导致备份过程中新的写入全部被阻塞。mysqldump默认会用锁,但配合--single-transaction可以基于一致性快照导数据,不会长时间阻塞业务。实验环境没有压力可能感觉不明显,但养成习惯很重要:

mysqldump --single-transaction --routines --triggers -u root -p 库名 > 备份文件.sql

其中--routines导出存储过程,--triggers导出触发器。这两个选项在实验九涉及存储过程时特别重要,漏掉的话恢复后函数和触发器会全部丢失。

6.3 恢复后自增主键冲突

恢复完数据后,如果继续往表里插入数据,有时会遇到Duplicate entry 'xxx' for key 'PRIMARY'。原因是binlog重放时,AUTO_INCREMENT计数器没有恢复到最新值。

解决办法是在恢复后手动调整自增起点:

ALTER TABLE orders AUTO_INCREMENT = 100;

生产环境恢复时会更麻烦,但实验里记住这一步就能避免很多莫名其妙的问题。

6.4 两个容易被忽略的小细节

第一个细节是时区。binlog记录时间戳时使用MySQL的系统时区,恢复时如果mysqlbinlog解析时间范围写错,很容易漏掉或多加数据。做实验时统一用本地时间,并且把--start-datetime和--stop-datetime写宽一点点,宁可多恢复几条再手动剔除,也别漏恢复。

第二个细节是事务是否提交。实验九里,如果模拟故障前有未提交事务,binlog里这部分数据未必能恢复。恢复验证时发现数据对不上,可以先查binlog里是否有COMMIT语句,很多丢失的数据都是因为事务压根没提交。

7. 最后聊几句个人体会

数据库系统的实验八和实验九,是我整个数据库课程里花时间最多的两组实验。实验八让我彻底明白了为什么业务代码里要控制事务粒度,为什么不能在一个长事务里做太多事,为什么并发高的系统要对操作顺序做约束。实验九让我建立了对备份和日志的敬畏,生产环境里的误删恢复,本质上就是实验九的放大版。

如果你刚开始做这两组实验,我的建议是:不要急着刷完报告,每个问题都亲手复现一遍。脏读、幻读、死锁、恢复丢失,这些坑在数据库里真实存在于每一套应用系统里。把实验八、九吃透,后面遇到线上事务超时、数据回滚、误删恢复,你脑子里会自然浮现出当时实验窗口里那些变化的数据行。这种体感,比背一百个概念都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 8:09:15

2026年AI Agent技术栈全景解析:从选型到落地避坑指南

1. 先画一张2026年的AI Agent技术全景图这两年聊AI Agent的人越来越多&#xff0c;但大多数讨论都停留在“Agent很火”“智能体是未来”这种层面。真正动手做过项目的人都知道&#xff0c;从概念到生产环境可用&#xff0c;中间隔着一条巨大的鸿沟&#xff1a;技术栈怎么选、框…

作者头像 李华
网站建设 2026/10/20 8:08:20

AI Agent请求大模型的三种方式:SDK、HTTP裸调与框架封装选型指南

做AI Agent开发&#xff0c;绕不开的一个问题是&#xff1a;你写的智能体背后&#xff0c;到底用哪种方式把请求发给大模型。很多人一开始直接用官方SDK&#xff0c;后来自建系统改HTTP接口&#xff0c;再后来又套上了LangChain之类的框架。每个阶段的选择都有它的道理&#xf…

作者头像 李华
网站建设 2026/10/20 8:07:16

MySQL并发控制:SELECT FOR UPDATE与悲观锁、乐观锁实战

我在接手一个订单库存模块时遇到过一种典型并发问题&#xff1a;两个用户几乎同时下单&#xff0c;影响到同一条库存记录&#xff0c;后台日志显示两条扣减SQL都执行成功了&#xff0c;但最终库存只减了一笔。排查之后发现&#xff0c;整个团队对MySQL锁机制的理解停留在“upda…

作者头像 李华
网站建设 2026/10/20 8:06:41

AI Agent架构演进:从单次LLM调用到Harness Runtime的完整路径

最近身边不少团队都在同一个问题上打转&#xff1a;AI Agent架构到底怎么选&#xff1f;有人用一次LLM调用就把Demo跑通了&#xff0c;觉得再上复杂框架纯属折腾&#xff1b;也有人一上来就直奔完整的Harness Runtime&#xff0c;结果被状态同步、异常恢复、日志回放这些事拖得…

作者头像 李华
网站建设 2026/10/20 8:06:28

LangGraph高可用设计:节点故障熔断与检查点恢复实战

做LangGraph落地也有小半年了&#xff0c;团队从最初“画图一时爽”的状态&#xff0c;到现在被生产环境的几个故障折腾得够呛。凡是多节点、有状态、依赖外部API的Agent工作流&#xff0c;迟早会碰到同一道坎&#xff1a;节点故障、服务熔断、自动恢复。今天把这段时间踩过的坑…

作者头像 李华
网站建设 2026/10/20 8:05:57

实测10个AI论文网站:从选题到盲审的本科生高效写作指南

又到毕业季&#xff0c;本科生论文这座山是真的难爬。从选题、文献综述、初稿、降重到盲审&#xff0c;每一步都有人卡壳。我的导师今年给教研室整理了一份工具清单&#xff0c;核心思路不是让AI替你写论文&#xff0c;而是用AI网站把“检索信息、整理框架、打磨语言、核对格式…

作者头像 李华