《MySQL 事务实战详解:从基础到并发控制(附 Navicat 可运行实验脚本)》
为什么你必须掌握 MySQL 事务?
在现代应用系统中,数据一致性是核心诉求。事务(Transaction) 是保证数据完整性的"黄金标准"。
无论是银行转账、库存扣减、订单创建,还是用户积分变动,事务机制都在默默守护数据安全。
本文将带你从零开始,手把手用 Navicat 演示所有核心知识点 ,并附带 可直接复制粘贴运行的实验脚本,让你"边学边练",真正理解事务的本质。
一、什么是事务?------数据库中的"原子操作单元"
事务是一组数据库操作(SQL 语句)的集合,它们作为一个整体被执行,要么全部成功,要么全部失败回滚。
1. 事务的四大特性:ACID
| 特性 | 说明 | 示例 |
|---|---|---|
| A - 原子性(Atomicity) | 事务中的所有操作必须全部完成,不能"部分提交" | 张三转 1000 元给李四,若李四没到账,张三也要退回 |
| C - 一致性(Consistency) | 事务前后数据库必须保持一致性状态 | 总金额不变:张三减 1000,李四加 1000 |
| I - 隔离性(Isolation) | 多个事务并发执行时互不干扰 | 一个事务不可读到另一个事务未提交的数据 |
| D - 持久性(Durability) | 事务一旦提交,结果永久保存 | 即使系统崩溃,数据也不会丢失 |
理解 ACID,你才算真正掌握了"事务为什么重要"。
二、如何使用事务?------MySQL 两种模式
模式一:手动控制事务(推荐用于复杂逻辑)
sql
-- 1. 关闭自动提交(设置为手动)
SET @@autocommit = 0;
-- 2. 开启事务
START TRANSACTION;
-- 3. 执行多个 SQL 操作
UPDATE account SET money = money - 1000 WHERE name = '张三';
UPDATE account SET money = money + 1000 WHERE name = '李四';
-- 4. 查看结果(此时仍可回滚)
SELECT * FROM account WHERE name IN ('张三', '李四');
-- 5. 成功:提交事务
COMMIT;
-- 6. 失败:回滚事务
-- ROLLBACK;

模式二:通过存储过程封装(适合复用)
sql
-- 清空旧过程
DROP PROCEDURE IF EXISTS transfer;
-- 创建转账存储过程
DELIMITER //
CREATE PROCEDURE transfer()
BEGIN
START TRANSACTION;
-- 查询余额
SELECT money INTO @balance FROM account WHERE name = '张三';
-- 判断是否足够
IF @balance >= 100 THEN
UPDATE account SET money = money - 100 WHERE name = '张三';
UPDATE account SET money = money + 100 WHERE name = '李四';
COMMIT;
SELECT '✅ 转账成功' AS result;
ELSE
ROLLBACK;
SELECT '❌ 余额不足,已回滚' AS result;
END IF;
END //
DELIMITER ;
-- 执行
CALL transfer();
小贴士:
SET @@autocommit = 1是 自动提交模式(默认)SET @@autocommit = 0是 手动提交模式(必须手动 COMMIT/ROLLBACK)
三、事务的隔离级别:四层"防火墙"
并发事务可能导致数据不一致。MySQL 提供 4 种隔离级别 来控制这种干扰。
🔥 默认隔离级别是:
REPEATABLE READ(MySQL 8.0)
| 隔离级别 | 是否脏读 | 是否不可重复读 | 是否幻读 | 适用场景 |
|---|---|---|---|---|
READ UNCOMMITTED |
✅ 是 | ✅ 是 | ✅ 是 | 一般不用(风险太高) |
READ COMMITTED |
❌ 否 | ✅ 是 | ✅ 是 | 乐观锁场景、日志系统 |
REPEATABLE READ |
❌ 否 | ❌ 否 | ⚠️ 一般(MVCC 优化) | 推荐!默认级别 |
SERIALIZABLE |
❌ 否 | ❌ 否 | ❌ 否 | 高度并发 + 高安全性(性能差) |
查看当前隔离级别:
sql
SELECT @@transaction_isolation;
设置隔离级别:
sql
-- 临时设置(当前会话有效)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 或者
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
当然可以!以下是 对"四大并发事务问题" 的简明扼要、通俗易懂、适合发布在 CSDN 或技术博客的介绍,已为你优化为清晰、结构化、可读性强的表达:
四、四大并发事务问题简要介绍
在多用户并发操作数据库的场景下,多个事务可能同时读写同一数据,若没有合理控制,就会引发数据不一致问题。MySQL 事务隔离级别正是为了解决这些问题而设计的。
下面介绍四种最常见的并发异常,一文讲清 😎:
1. 脏读(Dirty Read)
"读到了还没提交的'假数据'"
- 场景:事务 A 修改了数据但未提交,事务 B 就读取了 A 的修改。
- 风险 :如果 A 后来
ROLLBACK,B 读到的数据就是"虚假"的,称为"脏数据"。 - 解决方式 :使用
READ COMMITTED或更高隔离级别。
示例:
张三转账 100 元给李四,但还没提交,李四的系统立刻查余额,发现到账了 ------ 结果一回滚,钱又没了!
2. 不可重复读(Non-Repeatable Read)
"同事务内,两次读同一数据,结果不一样"
- 场景:事务 A 第一次读取某条记录,之后事务 B 修改了这条记录并提交,事务 A 再次读取,发现数据变了。
- 注意:这是"数据被改了",不是新增或删除。
- 解决方式 :使用
REPEATABLE READ或SERIALIZABLE。
示例:
事务 A 查询"张三余额:1000",一会儿后再次查询,变成"800"------ 因为别人已转走 200。
3. 幻读(Phantom Read)
"同一事务中,读不到别人新增的行"
- 场景 :事务 A 查询某类数据(如
WHERE age > 18),事务 B 插入一条符合条件的新数据并提交,事务 A 再次查询时发现"多出了一些行"。 - 注意 :不是修改了老数据,而是新数据"凭空出现"。
- 解决方式 :使用
SERIALIZABLE或 MySQL 的间隙锁(REPEATABLE READ也部分防止)。
示例:
事务 A 查询"在职员工:5 人",事务 B 新增一名员工并提交,事务 A 再查,变成"6 人"。
小知识:在 MySQL 的REPEATABLE READ中,通常不会出现幻读(因为使用了"间隙锁"),但严格意义上仍有可能。
4. 补充:丢失更新(Lost Update)
虽不总列为"四大问题",但非常常见!
- 场景:两个事务同时读取同一数据,各自修改后都提交,后提交的覆盖了先提交的。
- 后果:一次更新被"覆盖",导致数据丢失。
- 解决方式 :加锁(如
SELECT ... FOR UPDATE) + 事务控制。
总结对比表(建议收藏)
| 问题 | 描述 | 出现场景 | 隔离级别可避免 |
|---|---|---|---|
| 脏读 | 读到未提交数据 | 读未提交(RC) | ✅ READ COMMITTED 以上 |
| 不可重复读 | 同事务内两次读不一致 | 修改数据 | ✅ REPEATABLE READ 以上 |
| 幻读 | 查询结果"凭空多出" | 新增/删除数据 | ✅ SERIALIZABLE,MySQL RR 有优化 |
| 丢失更新 | 一次修改被覆盖 | 并发更新 | ✅ 加锁 + 事务 |
一句话口诀记忆:
脏读 :读了没提交的"假数据"
不可重复读 :读了被改过的"变了"数据
幻读 :读到了"凭空出现"的新数据
丢失更新:改了被覆盖,白白努力
附:常见隔离级别应对效果
| 隔离级别 | 能防脏读? | 能防不可重复读? | 能防幻读? |
|---|---|---|---|
READ UNCOMMITTED |
❌ 否 | ❌ 否 | ❌ 否 |
READ COMMITTED |
✅ 是 | ❌ 否 | ❌ 否 |
REPEATABLE READ(MySQL 默认) |
✅ 是 | ✅ 是 | ✅(部分) |
SERIALIZABLE |
✅ 是 | ✅ 是 | ✅ 是 |
五、四大并发问题演示(Navicat 实验代码)
实验1:读未提交(Dirty Read)------脏读问题
sql
-- 【窗口A】设置为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE account SET money = money - 200 WHERE name = '张三';
SELECT '窗口A:张三余额变为 800(未提交)' AS note;
SELECT * FROM account WHERE name = '张三';
-- 【窗口B】也设为读未提交
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT '窗口B:可以读到未提交的数据' AS note;
SELECT * FROM account WHERE name = '张三';
-- ✅ 输出:800 → 即使A没提交,B也能读到!
实验2:读已提交(Read Committed)
sql
-- 【窗口A】设置为读已提交
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE account SET money = money - 100 WHERE name = '张三';
-- 【窗口B】读取(此时A未提交 → 读不到)
SELECT '窗口B:不能读到未提交的数据' AS note;
SELECT * FROM account WHERE name = '张三';
-- 输出:1000 → 未看到A的变更!
-- 在A窗口提交后(COMMIT),B再查 → 就能看到新值
实验3:可重复读(Repeatable Read)------MVCC 机制
sql
-- 【窗口A】设置为可重复读
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT '窗口A:第一次读' AS note;
SELECT * FROM account WHERE name = '张三';
-- 等待【窗口B】提交修改(如:转100给李四)
-- 【窗口A】再次读
SELECT '窗口A:第二次读(仍为原值)' AS note;
SELECT * FROM account WHERE name = '张三';
-- 输出:原值!即使别人提交了,我也读不到!
这就是 MySQL 的 MVCC(多版本并发控制):每个事务看到的是"快照",不会被其他事务干扰。
实验4:串行化(Serializable)------强制排队
sql
-- 【窗口A】设置为串行化
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
SELECT '窗口A:读取张三余额' AS note;
SELECT * FROM account WHERE name = '张三';
-- 【窗口B】尝试读(会被阻塞,必须等A提交)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
SELECT '窗口B:等待(阻塞)' AS note;
SELECT * FROM account WHERE name = '张三';
-- 回到A窗口执行:COMMIT;
-- B窗口才恢复执行
串行化 = "一锤定音":事务间完全互斥,性能最低,但最安全。
五、关键工具 & 技巧汇总
| 技巧 | 说明 |
|---|---|
ROLLBACK |
回滚事务,恢复到事务开始前状态 |
COMMIT |
提交事务,永久保存修改 |
SELECT @@autocommit; |
查看自动提交状态 |
SET SESSION TRANSACTION ISOLATION LEVEL ... |
动态设置隔离级别 |
DELIMITER // ... // |
定义存储过程边界 |
DROP PROCEDURE IF EXISTS ... |
安全删除旧过程 |
如果你觉得这篇文章对你有帮助,请点赞 + 收藏 + 分享
欢迎在评论区留言:你遇到过的"事务坑"?