news 2026/3/2 22:09:45

MySQL 基础教程 - 第九章:事务与锁机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL 基础教程 - 第九章:事务与锁机制

MySQL 基础教程 - 第九章:事务与锁机制

摘要:在多人并发访问的数据库系统中,如何保证数据不会“打架”?本章将深入探讨 MySQL 的核心特性——事务 (Transaction)。我们将抛弃简单的“转账”玩具模型,基于一个完整的电商订单支付系统,构建包含用户、账户、订单、库存的完整表结构(含外键约束)。在此基础上,深度剖析事务的 ACID 特性,复现脏读、幻读等并发事故,并实战演示 MySQL 5.7 默认的 RR 隔离级别是如何通过MVCC锁机制解决这些问题的。

9.1 全景环境准备:电商支付系统

为了演示真实的事务场景,我们需要构建一个相互关联的业务系统。这包括:用户表、资金账户表、商品库存表、订单表。

请务必执行以下 SQL 脚本,确保实验环境的一致性。

-- 1. 初始化数据库CREATEDATABASEIFNOTEXISTSshop_bizCHARSET=utf8;USEshop_biz;-- 2. 清理旧数据 (如果存在)-- 注意删除顺序:先删子表 (有外键依赖的),再删父表DROPTABLEIFEXISTSorders;DROPTABLEIFEXISTSaccounts;DROPTABLEIFEXISTSinventory;DROPTABLEIFEXISTSproducts;-- 假设 products 是库存的父表,这里简化合二为一DROPTABLEIFEXISTSusers;-- 3. 创建用户表 (Users) - 父表CREATETABLEusers(user_idINTPRIMARYKEYAUTO_INCREMENTCOMMENT'用户ID',usernameVARCHAR(50)NOTNULLCOMMENT'用户名',statusTINYINTDEFAULT1COMMENT'状态: 1-正常, 0-冻结')CHARSET=utf8ENGINE=InnoDBCOMMENT='用户表';-- 4. 创建资金账户表 (Accounts) - 子表 (1:1 关联用户)CREATETABLEaccounts(account_idINTPRIMARYKEYAUTO_INCREMENTCOMMENT'账户ID',user_idINTNOTNULLUNIQUECOMMENT'用户ID (外键)',balanceDECIMAL(10,2)NOTNULLDEFAULT0.00COMMENT'余额',versionINTNOTNULLDEFAULT0COMMENT'乐观锁版本号',CONSTRAINTfk_accounts_userFOREIGNKEY(user_id)REFERENCESusers(user_id))CHARSET=utf8ENGINE=InnoDBCOMMENT='资金账户表';-- 5. 创建商品库存表 (Inventory)CREATETABLEinventory(product_idINTPRIMARYKEYAUTO_INCREMENTCOMMENT'商品ID',product_nameVARCHAR(100)NOTNULLCOMMENT'商品名称',stockINTNOTNULLDEFAULT0COMMENT'库存数量')CHARSET=utf8ENGINE=InnoDBCOMMENT='商品库存表';-- 6. 创建订单表 (Orders) - 子表 (关联用户)CREATETABLEorders(order_idINTPRIMARYKEYAUTO_INCREMENTCOMMENT'订单ID',user_idINTNOTNULLCOMMENT'用户ID (外键)',product_idINTNOTNULLCOMMENT'商品ID',quantityINTNOTNULLDEFAULT1COMMENT'购买数量',total_amountDECIMAL(10,2)NOTNULLCOMMENT'订单金额',order_statusTINYINTNOTNULLDEFAULT0COMMENT'状态: 0-待支付, 1-已支付, 2-已取消',create_timeDATETIMEDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',CONSTRAINTfk_orders_userFOREIGNKEY(user_id)REFERENCESusers(user_id))CHARSET=utf8ENGINE=InnoDBCOMMENT='订单表';-- 7. 初始化测试数据-- 用户INSERTINTOusers(username)VALUES('Alice'),('Bob'),('Charlie');-- 账户 (Alice 有 1000元, Bob 有 500元)INSERTINTOaccounts(user_id,balance)VALUES(1,1000.00),(2,500.00),(3,0.00);-- 库存 (iPhone 15 有 10 台)INSERTINTOinventory(product_name,stock)VALUES('iPhone 15',10);-- 验证数据SELECT*FROMusers;SELECT*FROMaccounts;SELECT*FROMinventory;


四个表的信息也是全的。


9.2 事务 (Transaction) 基础

9.2.1 什么是事务?

事务是一组 SQL 操作的集合,它们被视为一个不可分割的工作单元

真实业务场景:Alice 购买一台 iPhone 15 (价格 100 元)
这涉及三个核心操作:

  1. 扣减库存inventory表 stock - 1
  2. 创建订单orders表 insert 一条记录
  3. 扣减余额accounts表 balance - 100

如果第 1、2 步成功,但第 3 步余额不足导致失败,如果没有事务,Alice 就白拿了一个手机,系统库存也对不上了。

9.2.2 ACID 四大特性详解

  • 原子性 (Atomicity)
    • 定义:操作要么全做,要么全不做。
    • 实现:靠Undo Log。如果事务执行一半失败了,MySQL 利用 Undo Log 把数据恢复到原来的样子(回滚)。
  • 一致性 (Consistency)
    • 定义:事务前后,数据库的完整性约束(如外键、余额不为负)不被破坏。
    • 实现:靠代码逻辑 + 数据库约束(如外键)+ 原子性/隔离性共同保证。
  • 隔离性 (Isolation)
    • 定义:并发事务之间互不干扰。
    • 实现:靠锁 (Locks)MVCC (多版本并发控制)
  • 持久性 (Durability)
    • 定义:一旦提交,数据永久保存。
    • 实现:靠Redo Log。即使断电,重启后也能通过 Redo Log 重放恢复数据。

9.2.3 事务控制实战

场景:模拟 Alice 购买手机的完整事务流程。

-- 1. 开启事务STARTTRANSACTION;-- 2. 扣减库存 (假设 product_id=1)UPDATEinventorySETstock=stock-1WHEREproduct_id=1;-- 3. 创建订单INSERTINTOorders(user_id,product_id,quantity,total_amount)VALUES(1,1,1,100.00);-- 4. 扣减余额UPDATEaccountsSETbalance=balance-100WHEREuser_id=1;-- 5. 模拟意外:手动回滚 (ROLLBACK) 看看效果-- 此时你可以新开一个查询窗口 SELECT 查看,会发现数据根本没变ROLLBACK;-- 6. 再次执行并提交 (COMMIT)STARTTRANSACTION;UPDATEinventorySETstock=stock-1WHEREproduct_id=1;INSERTINTOorders(user_id,product_id,quantity,total_amount)VALUES(1,1,1,100.00);UPDATEaccountsSETbalance=balance-100WHEREuser_id=1;COMMIT;-- 此时数据才真正生效


9.3 事务隔离级别与并发问题

当多个用户同时抢购时,会发生什么?我们需要开启两个数据库连接(Session A 和 Session B)来模拟。

9.3.1 隔离级别一览

MySQL 5.7 支持 4 种隔离级别。

隔离级别脏读不可重复读幻读性能
READ UNCOMMITTED(读未提交)极高 (极不安全)
READ COMMITTED(读已提交)高 (Oracle默认)
REPEATABLE READ(可重复读)❌ (大部分解决)中 (MySQL默认)
SERIALIZABLE(串行化)低 (排队执行)

9.3.2 脏读 (Dirty Read) 演示

前提:将 Session A 的隔离级别设置为“读未提交”。

-- Session A 设置SETSESSIONTRANSACTIONISOLATIONLEVELREADUNCOMMITTED;
Session A (Alice)Session B (Bob)说明
START TRANSACTION;START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE user_id = 2;Bob 扣款 100,但未提交
SELECT * FROM accounts WHERE user_id = 2;
(结果: 400)
A 读到了 Bob 未提交的数据!
ROLLBACK;Bob 后悔了,回滚了操作
UPDATE ...A 以为 Bob 只有 400 块,基于此做了错误决策

9.3.3 可重复读 (Repeatable Read) 实战

这是 MySQL 的默认级别,也是我们最常用的。

恢复默认设置:

SETSESSIONTRANSACTIONISOLATIONLEVELREPEATABLEREAD;

演示步骤:

Session A (Alice)Session B (Bob)说明
START TRANSACTION;START TRANSACTION;
SELECT stock FROM inventory WHERE product_id = 1;
(结果: 9)
A 看到库存是 9
UPDATE inventory SET stock = 5 WHERE product_id = 1;
COMMIT;
B 把库存改成了 5 并提交了!
SELECT stock FROM inventory WHERE product_id = 1;
(结果: 9)
A 看到的依然是 9!
(MVCC 发挥作用,保证视图一致性)
COMMIT;A 提交事务
SELECT stock FROM inventory WHERE product_id = 1;
(结果: 5)
A 重新查询,看到了最新值

9.4 锁机制 (Lock) 深度解析

MVCC 解决了“读-写”冲突(读快照,写最新),但“写-写”冲突必须靠锁。

9.4.1 行锁 vs 表锁

InnoDB 的行锁是加在索引上的。

  • 行锁 (Record Lock)
    -- user_id 是主键索引,只锁 id=1 这一行UPDATEaccountsSETbalance=balance-1WHEREuser_id=1;
  • 表锁 (Table Lock)
    -- 假设 balance 字段没有索引-- 这会锁住整张 accounts 表!其他人连 user_id=2 都改不了!UPDATEaccountsSETbalance=balance-1WHEREbalance=1000;

    ⚠️ 警告:生产环境更新数据,务必确保WHERE条件走了索引,否则会造成灾难性的锁表。

9.4.2 悲观锁实战:余额扣减

在高并发下防止余额扣成负数。

STARTTRANSACTION;-- 1. 显式加锁 (X锁)-- 这行 SQL 会让当前事务持有这行记录的排他锁,其他事务必须等待SELECTbalanceFROMaccountsWHEREuser_id=1FORUPDATE;-- 2. 检查余额 (应用层逻辑)-- if balance < 100: rollback-- 3. 执行扣款UPDATEaccountsSETbalance=balance-100WHEREuser_id=1;COMMIT;

9.4.3 乐观锁实战:CAS 机制

不加锁,利用version字段解决冲突。

-- 1. 查询当前版本和余额SELECTbalance,versionFROMaccountsWHEREuser_id=1;-- 假设查出来 version = 0-- 2. 尝试更新-- 核心:WHERE 条件里加上 version = 0UPDATEaccountsSETbalance=balance-100,version=version+1WHEREuser_id=1ANDversion=0;-- 3. 检查受影响行数-- 如果为 1:更新成功-- 如果为 0:说明在第1步和第2步之间,有人修改了数据(version变了),需要重试流程

9.5 死锁 (Deadlock) 复现

场景:Alice 转账给 Bob,同时 Bob 转账给 Alice。

Session A (Alice -> Bob)Session B (Bob -> Alice)
START TRANSACTION;START TRANSACTION;
UPDATE accounts SET balance=balance-10 WHERE user_id=1;
(持有 id=1 的锁)
UPDATE accounts SET balance=balance-10 WHERE user_id=2;
(持有 id=2 的锁)
UPDATE accounts SET balance=balance+10 WHERE user_id=2;
(等待 id=2 的锁)
UPDATE accounts SET balance=balance+10 WHERE user_id=1;
(等待 id=1 的锁)
死锁!MySQL 自动回滚 AB 执行成功

9.6 总结

  1. 完整性:事务通过 ACID 保证了复杂业务(如支付下单)的数据完整性。
  2. 隔离性:理解 RR 级别和 MVCC,知道为什么“读不到别人已提交的数据”。
  3. 锁的艺术
    • 更新必走索引(避开表锁)。
    • 顺序加锁(避开死锁)。
    • 读多写少用乐观锁,写多读少用悲观锁。

下一章,我们将进入 DCL(用户管理),学习如何为不同的开发人员分配不同的数据库权限。

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

基于SpringBoot+Vue的Spring Boot疗养院管理系统管理系统设计与实现【Java+MySQL+MyBatis完整源码】

系统架构设计### 摘要 随着人口老龄化趋势加剧&#xff0c;疗养院管理系统的需求日益增长。传统疗养院管理模式依赖人工操作&#xff0c;存在效率低、数据易丢失、信息共享困难等问题&#xff0c;难以满足现代疗养院的运营需求。数字化管理系统能够有效提升疗养院的服务质量和…

作者头像 李华
网站建设 2026/2/10 6:46:23

计算机是如何运作的?看看汇编语言吧

一、寄存器 寄存器是中央处理器&#xff08;CPU&#xff09;内部集成的一组高速、小型的存储单元&#xff0c;其容量通常仅为几个字节&#xff08;如8位、16位、32位或64位&#xff09;&#xff0c;但访问速度远高于主内存&#xff08;RAM&#xff09;甚至高速缓存&#xff08…

作者头像 李华
网站建设 2026/3/1 19:45:58

【Java 笔记】面向对象核心 - 内存图

核心总结Java 三种场景内存图的核心逻辑&#xff1a;内存分区&#xff1a;栈存局部变量 / 对象引用&#xff08;地址&#xff09;&#xff0c;堆存对象及成员变量&#xff0c;方法区存类信息&#xff1b;单个对象&#xff1a;栈中引用指向堆中唯一对象&#xff0c;通过地址操作…

作者头像 李华
网站建设 2026/2/23 12:58:02

企业级经方药食两用服务平台管理系统源码|SpringBoot+Vue+MyBatis架构+MySQL数据库【完整版】

摘要 随着中医药文化的全球推广和健康理念的普及&#xff0c;药食两用产品的市场需求显著增长。传统的中药企业及健康管理平台在信息化管理方面仍存在效率低下、数据孤岛等问题&#xff0c;亟需一套高效、智能的管理系统实现资源整合与流程优化。企业级经方药食两用服务平台旨…

作者头像 李华