--- title: "18-事务与并发控制" created: 2026-01-08 tags: - 项目筑基 --- # 事务与并发控制 ## **第十三章:事务与并发控制** > 💡 事务是数据库操作的基本单位,并发控制保证多用户同时访问时的数据一致性 ### **13.1 事务概念** #### **13.1.1 什么是事务** **定义**:事务是作为单个逻辑工作单元执行的一系列操作,这些操作要么全部成功,要么全部失败。 ![[image-67bb16d7.png]] #### **13.1.2 事务的四大特性(ACID)** | **特性** | **英文** | **说明** | **保证方式** | | --- | --- | --- | --- | | **原子性** | Atomicity | 事务中的操作要么全部完成,要么全部不完成 | Undo Log | | **一致性** | Consistency | 事务前后,数据库从一个一致状态转到另一个一致状态 | 其他三个特性保证 | | **隔离性** | Isolation | 并发事务之间相互隔离,互不干扰 | 锁机制、MVCC | | **持久性** | Durability | 事务提交后,修改永久保存 | Redo Log | ![[image-b7d455db.png]] ### **13.2 事务控制语句** #### **13.2.1 基本语法** ```sql -- 开始事务 START TRANSACTION; -- 或 BEGIN; -- 提交事务 COMMIT; -- 回滚事务 ROLLBACK; -- 设置保存点 SAVEPOINT 保存点名称; -- 回滚到保存点 ROLLBACK TO SAVEPOINT 保存点名称; -- 释放保存点 RELEASE SAVEPOINT 保存点名称; ``` #### **13.2.2 事务示例:银行转账** ```sql -- 转账场景:从账户A向账户B转账500元 -- 开始事务 START TRANSACTION; -- 检查账户A余额 SELECT 余额 INTO @balance FROM 账户 WHERE 账户号 = 'A'; -- 如果余额不足 IF @balance < 500 THEN SELECT '余额不足' AS 结果; ROLLBACK; -- 回滚事务 ELSE -- 从账户A扣款 UPDATE 账户 SET 余额 = 余额 - 500 WHERE 账户号 = 'A'; -- 向账户B存款 UPDATE 账户 SET 余额 = 余额 + 500 WHERE 账户号 = 'B'; -- 提交事务 COMMIT; SELECT '转账成功' AS 结果; END IF; ``` #### **13.2.3 保存点示例** ```sql START TRANSACTION; INSERT INTO 订单 VALUES (1, '客户A', NOW()); SAVEPOINT sp1; -- 创建保存点1 INSERT INTO 订单明细 VALUES (1, '商品1', 10); SAVEPOINT sp2; -- 创建保存点2 INSERT INTO 订单明细 VALUES (1, '商品2', 20); -- 如果第三个操作出问题,回滚到保存点2 ROLLBACK TO SAVEPOINT sp2; -- 如果需要回滚更多,可以回滚到保存点1 -- ROLLBACK TO SAVEPOINT sp1; -- 最终提交 COMMIT; ``` ### **13.3 自动提交模式** #### **13.3.1 查看和设置自动提交** ```sql -- 查看当前自动提交状态 SELECT @@autocommit; SHOW VARIABLES LIKE 'autocommit'; -- 关闭自动提交 SET autocommit = 0; -- 开启自动提交(默认) SET autocommit = 1; ``` #### **13.3.2 自动提交 vs 显式事务** | **模式** | **说明** | **使用场景** | | --- | --- | --- | | **自动提交(默认)** | 每条SQL语句自动成为一个事务 | 单条语句操作 | | **显式事务** | 使用BEGIN/COMMIT/ROLLBACK控制 | 多条语句需要原子操作 | | **关闭自动提交** | 每次操作需要手动COMMIT | 批量操作、需要回滚控制 | ### **13.4 事务隔离级别** #### **13.4.1 并发问题** | **问题** | **说明** | **示例** | | --- | --- | --- | | **脏读** | 读取到其他事务未提交的数据 | A修改数据但未提交,B读取到了 | | **不可重复读** | 同一事务内两次读取结果不同 | A两次读取之间,B修改并提交了 | | **幻读** | 同一事务内两次查询结果集不同 | A两次查询之间,B插入了新数据 | #### **13.4.2 四种隔离级别** | **隔离级别** | **脏读** | **不可重复读** | **幻读** | **性能** | | --- | --- | --- | --- | --- | | **READ UNCOMMITTED** | ✓可能 | ✓可能 | ✓可能 | 最高 | | **READ COMMITTED** | ✗避免 | ✓可能 | ✓可能 | 较高 | | **REPEATABLE READ**(MySQL默认) | ✗避免 | ✗避免 | ✓可能 | 中等 | | **SERIALIZABLE** | ✗避免 | ✗避免 | ✗避免 | 最低 | #### **13.4.3 设置隔离级别** ```sql -- 查看当前隔离级别 SELECT @@transaction_isolation; -- 或(旧版本) SELECT @@tx_isolation; -- 设置会话级别隔离 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- 设置全局隔离级别 SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 只对下一个事务生效 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; ``` #### **13.4.4 隔离级别示例** ```sql -- 演示脏读(需要两个会话) -- 会话1:设置最低隔离级别 SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; START TRANSACTION; SELECT * FROM 账户 WHERE 账户号 = 'A'; -- 假设余额1000 -- 会话2:修改数据但不提交 START TRANSACTION; UPDATE 账户 SET 余额 = 500 WHERE 账户号 = 'A'; -- 不提交! -- 会话1:再次读取 SELECT * FROM 账户 WHERE 账户号 = 'A'; -- 读到500(脏读!) -- 会话2:回滚 ROLLBACK; -- 会话1:此时账户A实际余额还是1000,但会话1已经读到了500 ``` ### **13.5 锁机制** #### **13.5.1 锁的类型** | **锁类型** | **说明** | **兼容性** | | --- | --- | --- | | **共享锁(S锁/读锁)** | 允许多个事务同时读取 | 与其他S锁兼容 | | **排他锁(X锁/写锁)** | 只允许一个事务独占 | 与任何锁都不兼容 | #### **13.5.2 锁的粒度** | **粒度** | **说明** | **优点** | **缺点** | | --- | --- | --- | --- | | **表锁** | 锁定整个表 | 开销小,加锁快 | 并发度低 | | **行锁** | 锁定单行 | 并发度高 | 开销大,可能死锁 | #### **13.5.3 InnoDB 行锁** ```sql -- 共享锁(读锁) SELECT * FROM 表名 WHERE 条件 LOCK IN SHARE MODE; -- MySQL 8.0+ 新语法 SELECT * FROM 表名 WHERE 条件 FOR SHARE; -- 排他锁(写锁) SELECT * FROM 表名 WHERE 条件 FOR UPDATE; -- 示例 START TRANSACTION; SELECT * FROM 账户 WHERE 账户号 = 'A' FOR UPDATE; -- 加排他锁 UPDATE 账户 SET 余额 = 余额 - 100 WHERE 账户号 = 'A'; COMMIT; ``` #### **13.5.4 死锁** ![[image-3d23ed5f.png]] **死锁检测和处理:** ```sql -- 查看InnoDB状态(包含死锁信息) SHOW ENGINE INNODB STATUS\G -- 查看当前锁等待 SELECT * FROM information_schema.INNODB_LOCK_WAITS; -- 查看当前锁 SELECT * FROM information_schema.INNODB_LOCKS; -- MySQL会自动检测死锁,并回滚其中一个事务 ``` ### **13.6 事务最佳实践** #### **13.6.1 事务设计原则** | **原则** | **说明** | | --- | --- | | **事务尽量短** | 长事务会占用资源,增加锁冲突 | | **避免交互式事务** | 不要在事务中等待用户输入 | | **合理选择隔离级别** | 根据业务需求选择适当的隔离级别 | | **处理死锁** | 设计时避免死锁,程序中处理死锁异常 | | **使用索引** | 避免表锁,减少锁范围 | #### **13.6.2 事务示例:完整的转账存储过程** ```sql DELIMITER // CREATE PROCEDURE transfer( IN from_account VARCHAR(20), IN to_account VARCHAR(20), IN amount DECIMAL(10,2), OUT result VARCHAR(50) ) BEGIN DECLARE from_balance DECIMAL(10,2); DECLARE CONTINUE HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET result = '转账失败:系统错误'; END; -- 开始事务 START TRANSACTION; -- 加锁查询余额(避免并发问题) SELECT 余额 INTO from_balance FROM 账户 WHERE 账户号 = from_account FOR UPDATE; -- 检查余额 IF from_balance IS NULL THEN SET result = '转账失败:账户不存在'; ROLLBACK; ELSEIF from_balance < amount THEN SET result = '转账失败:余额不足'; ROLLBACK; ELSE -- 扣款 UPDATE 账户 SET 余额 = 余额 - amount WHERE 账户号 = from_account; -- 存款 UPDATE 账户 SET 余额 = 余额 + amount WHERE 账户号 = to_account; -- 提交 COMMIT; SET result = '转账成功'; END IF; END // DELIMITER ; -- 调用 CALL transfer('A', 'B', 500, @result); SELECT @result; ``` ### **13.7 快速参考** ```sql -- 事务控制 START TRANSACTION; -- 或 BEGIN; COMMIT; ROLLBACK; SAVEPOINT 名称; ROLLBACK TO SAVEPOINT 名称; -- 自动提交 SELECT @@autocommit; SET autocommit = 0; -- 隔离级别 SELECT @@transaction_isolation; SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 锁 SELECT ... LOCK IN SHARE MODE; -- 共享锁 SELECT ... FOR UPDATE; -- 排他锁 ``` --- ⬅️ [[17-用户管理与权限控制|用户管理与权限控制]] 🏠 [[00-数据库|00-数据库]] ➡️ [[01-PostgreSQL 基础与 MySQL 对照|01-PostgreSQL 基础与 MySQL 对照]]