【MySQL】事务与锁

【MySQL】事务与锁

一、事务:什么是事务及其核心特性

1.1 事务的定义

事务是一组SQL语句的集合,要么全部执行成功(提交),要么全部执行失败(回滚),是数据库并发控制的基本单位。最经典的场景就是转账:

-- 转账示例:张三给李四转100元
START TRANSACTION;
-- 张三余额减少100
UPDATE account SET balance = balance - 100 WHERE name = '张三';
-- 李四余额增加100
UPDATE account SET balance = balance + 100 WHERE name = '李四';
-- 提交事务(或ROLLBACK回滚)
COMMIT;

1.2 核心特性:ACID模型

事务的可靠性由ACID四大特性保障,这也是面试高频考点:

  • 原子性(Atomicity):事务是不可分割的最小单位,要么全成,要么全败;
  • 一致性(Consistency):事务执行前后,数据库完整性约束不被破坏(如转账总额不变);
  • 隔离性(Isolation):多个并发事务互不干扰,避免脏读、不可重复读、幻读;
  • 持久性(Durability):事务提交后,数据修改永久保存,即使系统崩溃也不丢失。

初学者疑问:ACID中哪个是最终目标?
一致性是最终目标,原子性、隔离性、持久性是实现一致性的三大基石。只要保证这三点,数据自然会满足一致性。

1.3 事务的基本使用

InnoDB是MySQL唯一支持事务的存储引擎,核心操作如下:

-- 1. 开启事务(二选一)
START TRANSACTION;
BEGIN;

-- 2. 执行SQL操作(INSERT/UPDATE/DELETE)
UPDATE account SET balance = balance - 100 WHERE name = '张三';
UPDATE account SET balance = balance + 100 WHERE name = '李四';

-- 3. 提交事务(持久化修改)
COMMIT;

-- 4. 回滚事务(撤销修改)
ROLLBACK;

-- 5. 设置自动提交模式(默认ON)
SET AUTOCOMMIT = OFF; -- 关闭自动提交,需手动COMMIT/ROLLBACK
SET AUTOCOMMIT = ON;  -- 开启自动提交,每条SQL单独构成事务

实战验证:自动提交与手动提交的区别

-- 查看当前自动提交模式
SHOW VARIABLES LIKE 'autocommit'; -- 默认ON

-- 关闭自动提交
SET AUTOCOMMIT = OFF;

-- 执行修改
UPDATE account SET balance = balance - 100 WHERE name = '张三';
-- 未提交前,其他会话看不到修改
-- 回滚后修改失效
ROLLBACK;

-- 重新执行修改并提交
UPDATE account SET balance = balance - 100 WHERE name = '张三';
COMMIT; -- 提交后修改持久化,其他会话可见

二、ACID实现原理:UndoLog、RedoLog与锁的协同

2.1 原子性(Atomicity):基于UndoLog的回滚机制

原子性的核心是“失败回滚”,而回滚的依据是UndoLog(撤销日志)

实现逻辑
  • 事务执行每个DML操作前,InnoDB会记录数据的原始版本到UndoLog;
  • 若事务执行失败(如报错、ROLLBACK),InnoDB通过UndoLog反向执行操作,恢复数据到事务开始前的状态;
  • UndoLog按操作类型分为Insert Undo(记录INSERT操作,提交后可直接删除)和Update Undo(记录UPDATE/DELETE操作,需支持MVCC,提交后加入历史链表)。

初学者疑问:UndoLog是如何关联数据版本的?
每个数据行包含隐藏字段DB_ROLL_PTR(回滚指针),指向对应的UndoLog记录,多条UndoLog通过roll_pointer串联成版本链,回滚时按版本链逆向恢复。

2.2 持久性(Durability):基于RedoLog的崩溃恢复

持久性的核心是“提交后数据不丢失”,依赖RedoLog(重做日志) 和双写缓冲区。

实现逻辑
  • 事务执行DML操作时,InnoDB先修改内存中的数据页(缓冲池),同时记录操作到RedoLog;
  • 事务提交时,RedoLog先写入磁盘(WAL原则:Write-Ahead Logging),再异步刷写数据页到磁盘;
  • 若系统崩溃,重启后InnoDB通过RedoLog恢复未刷盘的已提交事务,保证数据不丢失。

初学者疑问:RedoLog和UndoLog的区别是什么?

  • RedoLog记录“数据修改后的状态”,用于崩溃恢复已提交事务;
  • UndoLog记录“数据修改前的状态”,用于事务回滚;
  • 两者协同:RedoLog保障提交后不丢失,UndoLog保障失败后可回滚。

2.3 一致性(Consistency):原子性+持久性+隔离性的自然结果

一致性是ACID的最终目标,无需单独实现:

  • 原子性保证事务要么全成要么全败,避免数据部分修改;
  • 持久性保证提交后数据不丢失;
  • 隔离性保证并发事务互不干扰;
  • 三者结合,自然满足数据的完整性约束(如转账总额不变、外键约束等)。

2.4 隔离性(Isolation):基于锁与MVCC的并发控制

隔离性是最复杂的特性,核心是解决并发事务的相互干扰。InnoDB通过“锁机制”和“MVCC”协同实现,既保证数据安全,又提升并发性能。

三、锁机制:InnoDB的并发控制核心

锁是实现隔离性的基础,InnoDB支持多种锁类型,按粒度可分为表级锁和行级锁,按模式可分为共享锁、排他锁等,不同锁的协同实现了不同的隔离级别。

3.1 锁的核心分类

3.1.1 按粒度划分:表级锁 vs 行级锁
锁类型锁定范围优点缺点适用场景
表级锁整个表开销小、加锁快并发度低,易阻塞全表扫描、批量更新
行级锁单个数据行并发度高,冲突少开销大、加锁慢单行/少量行修改、高并发

初学者疑问:InnoDB默认是行级锁,为什么有时会升级为表级锁?
当SQL未命中索引(如全表扫描),InnoDB无法精准锁定行,会自动升级为表级锁,导致并发性能下降。因此,索引是行级锁的前提

3.1.2 按模式划分:共享锁与排他锁(核心锁)

InnoDB实现了标准的行级锁:

  • 共享锁(S锁/读锁):允许事务读取数据,多个事务可同时持有S锁,但禁止写操作;
  • 排他锁(X锁/写锁):允许事务修改/删除数据,持有X锁时,其他事务无法持有任何锁。

锁兼容性矩阵

共享锁(S)排他锁(X)
共享锁(S)兼容冲突
排他锁(X)冲突冲突

实战示例:手动加锁

-- 加共享锁(读锁),其他事务可读不可写
SELECT * FROM account WHERE id = 1 FOR SHARE; -- MySQL 8.0+
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE; -- 兼容旧版本

-- 加排他锁(写锁),其他事务不可读不可写
SELECT * FROM account WHERE id = 1 FOR UPDATE;
3.1.3 意向锁:表级锁与行级锁的桥梁

意向锁是表级锁,用于标识事务“打算”对表中的行加锁,分为:

  • 意向共享锁(IS锁):事务打算对行加S锁,加S锁前先加IS锁;
  • 意向排他锁(IX锁):事务打算对行加X锁,加X锁前先加IX锁。

初学者疑问:意向锁为什么不需要阻止行锁请求?
意向锁的核心作用是“快速判断表中是否有行锁”,避免加表锁时遍历所有行。例如,当事务要加表级X锁时,只需检查是否有IS/IX锁,无需逐行检查行锁,提升加锁效率。

意向锁兼容性矩阵

X锁IX锁S锁IS锁
X锁冲突冲突冲突冲突
IX锁冲突兼容冲突兼容
S锁冲突冲突兼容兼容
IS锁冲突兼容兼容兼容
3.1.4 行级锁的细分:记录锁、间隙锁、临键锁

InnoDB的行级锁基于索引实现,细分三种锁以解决幻读问题:

  1. 记录锁(Record Lock):锁定具体的索引记录(如id=10的行),仅锁住行本身;
  2. 间隙锁(Gap Lock):锁定索引记录之间的间隙(如id=10id=20之间),防止插入新数据;
  3. 临键锁(Next-Key Lock):记录锁+间隙锁的组合(如(10,20]),默认隔离级别(REPEATABLE READ)下自动启用,解决幻读。

实战示例:间隙锁与临键锁

-- 表结构(id为主键)
CREATE TABLE account (id INT PRIMARY KEY, name VARCHAR(20), balance DECIMAL(10,2));
INSERT INTO account VALUES (10, '张三', 1000), (20, '李四', 1000);

-- 事务A执行:锁定id在10-20之间的行,触发临键锁 (10,20]
START TRANSACTION;
SELECT * FROM account WHERE id BETWEEN 10 AND 20 FOR UPDATE;

-- 事务B执行:插入id=15的行,会被间隙锁阻塞
INSERT INTO account VALUES (15, '王五', 1000); -- 阻塞,直到事务A提交/回滚

初学者疑问:什么时候间隙锁会失效?
当隔离级别设置为READ COMMITTED时,间隙锁会被禁用;或使用唯一索引查询唯一行时(如id=10),仅触发记录锁,不触发间隙锁。

3.1.5 其他特殊锁
  • 插入意向锁(Insert Intention Lock):插入数据时的间隙锁,多个事务向同一间隙插入不同数据时不会阻塞;
  • AUTO-INC锁:表级锁,用于自增列,保证自增值唯一,事务回滚后自增值不回退(可能出现不连续);
  • 元数据锁(MDL锁):表级锁,防止DDL与DML并发冲突(如修改表结构时禁止插入数据)。

3.2 死锁:并发事务的“陷阱”

3.2.1 死锁的定义与产生条件

死锁是指两个或多个事务互相持有对方需要的锁,导致事务无法继续执行的状态。例如:

  • 事务A持有记录1的S锁,等待记录2的X锁;
  • 事务B持有记录2的S锁,等待记录1的X锁;
  • 两者互相等待,形成死锁。

死锁产生的四大必要条件(缺一不可):

  1. 互斥访问:锁只能被一个事务持有;
  2. 不可抢占:锁只能由持有事务主动释放;
  3. 保持与请求:事务持有一个锁后,再请求其他锁;
  4. 循环等待:事务之间形成锁的循环依赖。
3.2.2 死锁模拟与排查

步骤1:模拟死锁

-- 会话A(事务A)
SET GLOBAL innodb_print_all_deadlocks = ON; -- 开启死锁日志
START TRANSACTION;
SELECT * FROM account WHERE name = '张三' FOR SHARE; -- 持有张三的S锁

-- 会话B(事务B)
START TRANSACTION;
SELECT * FROM account WHERE name = '李四' FOR SHARE; -- 持有李四的S锁

-- 会话B更新张三的记录(请求X锁,阻塞)
UPDATE account SET balance = 1100 WHERE name = '张三';

-- 会话A更新李四的记录(请求X锁,触发死锁)
UPDATE account SET balance = 900 WHERE name = '李四';

步骤2:排查死锁

-- 查看死锁次数
SELECT `count` FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE NAME = "lock_deadlocks";

-- 查看最近一次死锁详情
SHOW ENGINE INNODB STATUS\G -- 查看LATEST DETECTED DEADLOCK部分

-- 查看锁等待信息
SELECT * FROM performance_schema.data_lock_waits;
3.2.3 死锁的避免策略

打破死锁的核心是破坏“循环等待”条件,常用方法:

  1. 保持事务短小:减少事务持有锁的时间和范围;
  2. 统一锁申请顺序:事务修改多个表/行时,按固定顺序操作(如先修改张三再修改李四);
  3. 避免长时间未提交的事务:及时COMMIT/ROLLBACK;
  4. 合理使用索引:避免表锁升级,减少锁冲突;
  5. 禁用死锁检测(高并发场景):设置innodb_deadlock_detect = OFF,依赖innodb_lock_wait_timeout(默认50秒)超时释放;
  6. 降低隔离级别:如使用READ COMMITTED,减少间隙锁的使用。

四、事务隔离级别:并发性能与数据安全的权衡

隔离级别定义了并发事务之间的隔离程度,InnoDB支持四种隔离级别,从低到高依次为:

4.1 四种隔离级别详解

隔离级别脏读不可重复读幻读并发性能实现方式
READ UNCOMMITTED允许允许允许最高读不加锁,写加S锁
READ COMMITTED禁止允许允许较高快照读(MVCC),写加X锁
REPEATABLE READ禁止禁止部分中等快照读(MVCC)+ Next-Key锁
SERIALIZABLE禁止禁止禁止最低读加S锁,写加X锁,完全串行执行

:InnoDB默认隔离级别为REPEATABLE READ,通过Next-Key锁解决了大部分幻读场景。

4.1.1 关键概念解释
  • 脏读:读取到未提交事务的修改(如事务A修改数据未提交,事务B读取到该修改);
  • 不可重复读:同一事务内,多次查询同一数据得到不同结果(如事务A查询后,事务B修改并提交,事务A再次查询得到新值);
  • 幻读:同一事务内,多次查询同一范围得到不同数量的结果(如事务A查询id<10的行,事务B插入id=5的行并提交,事务A再次查询多了一行)。

4.2 隔离级别的设置与验证

4.2.1 查看与设置隔离级别
-- 查看隔离级别(全局/会话)
SELECT @@GLOBAL.transaction_isolation; -- 全局
SELECT @@SESSION.transaction_isolation; -- 会话(默认REPEATABLE READ)

-- 设置隔离级别(全局/会话)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 选项文件配置(永久生效)
[mysqld]
transaction-isolation = REPEATABLE-READ
4.2.2 不同隔离级别的问题验证

示例1:READ UNCOMMITTED的脏读问题

-- 会话A设置隔离级别为READ UNCOMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE name = '张三'; -- 未提交

-- 会话B查询,读取到未提交的修改(脏读)
SELECT balance FROM account WHERE name = '张三'; -- 结果为900(原1000)

-- 会话A回滚,会话B再次查询,结果恢复为1000
ROLLBACK;

示例2:REPEATABLE READ的可重复读特性

-- 会话A设置隔离级别为REPEATABLE READ
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT balance FROM account WHERE name = '张三'; -- 结果1000

-- 会话B修改并提交
START TRANSACTION;
UPDATE account SET balance = 900 WHERE name = '张三';
COMMIT;

-- 会话A再次查询,结果仍为1000(可重复读)
SELECT balance FROM account WHERE name = '张三';

初学者疑问:REPEATABLE READ为什么能实现可重复读?
因为该隔离级别下,事务首次查询时生成ReadView(读视图),后续查询复用该ReadView,只能看到事务开始前已提交的数据版本,看不到后续提交的修改。

五、MVCC:多版本并发控制的底层逻辑

MVCC(Multi-Versioned Concurrency Control)是InnoDB实现高并发的核心,通过数据版本链和ReadView实现“读不加锁、写不加锁”,提升并发性能。

5.1 MVCC的核心组成

5.1.1 数据版本链

每个数据行包含三个隐藏字段:

  • DB_TRX_ID:修改该行的事务ID;
  • DB_ROLL_PTR:回滚指针,指向该行的上一个版本(存储在UndoLog中);
  • DB_ROW_ID:无主键/唯一键时自动生成的行ID。

事务修改数据时,会生成新的数据版本,通过DB_ROLL_PTR串联成版本链,例如:

当前版本(id=1,balance=900,trx_id=200,roll_ptr→版本1)
版本1(id=1,balance=1000,trx_id=100,roll_ptr→版本0)
版本0(id=1,balance=1000,trx_id=80,roll_ptr=NULL)
5.1.2 ReadView(读视图)

ReadView是事务查询时生成的内存结构,记录当前活跃事务的信息,用于判断数据版本的可见性,包含四个核心属性:

  • m_ids:当前活跃事务ID集合;
  • m_low_limit_id:活跃事务中最小的ID;
  • m_up_limit_id:下一个将要分配的事务ID;
  • m_creator_trx_id:创建该ReadView的事务ID。
5.1.3 版本可见性判断规则

查询时,InnoDB遍历数据版本链,按以下规则判断版本是否可见:

  1. 若版本的trx_id == m_creator_trx_id:可见(读取自己修改的数据);
  2. 若版本的trx_id < m_low_limit_id:可见(事务已提交);
  3. 若版本的trx_id >= m_up_limit_id:不可见(事务在ReadView生成后创建);
  4. 若版本的trx_idm_low_limit_idm_up_limit_id之间:
    • trx_id不在m_ids中:可见(事务已提交);
    • 否则:不可见(事务仍活跃)。

5.2 MVCC与隔离级别的关联

MVCC的ReadView生成时机决定了隔离级别:

  • READ COMMITTED:每次查询都生成新的ReadView,因此能看到查询前已提交的修改(可能出现不可重复读);
  • REPEATABLE READ:事务首次查询生成ReadView,后续查询复用,因此同一事务内查询结果一致(解决不可重复读)。

初学者疑问:MVCC为什么能提升并发性能?
因为MVCC实现了“快照读”(不加锁的读),读事务与写事务互不阻塞,写事务之间通过锁互斥,既保证了隔离性,又提升了并发度。

六、实战:事务与锁的最佳实践

6.1 核心优化建议

  1. 优先使用行级锁:通过索引精准锁定数据,避免表锁升级;
  2. 保持事务短小:减少锁持有时间,降低死锁风险;
  3. 合理设置隔离级别
    • 高并发读场景:使用READ COMMITTED(减少锁冲突);
    • 数据一致性要求高:使用REPEATABLE READ(默认);
    • 严格一致性要求:使用SERIALIZABLE(慎用,并发低);
  4. 避免长事务:及时提交/回滚,防止锁占用过久;
  5. 监控锁状态:定期查看锁等待和死锁日志,优化SQL。

6.2 常用监控SQL

-- 查看当前锁信息
SELECT 
  ENGINE_TRANSACTION_ID AS trx_id,
  OBJECT_NAME AS table_name,
  LOCK_TYPE AS lock_type,
  LOCK_MODE AS lock_mode,
  LOCK_STATUS AS lock_status,
  LOCK_DATA AS lock_data
FROM performance_schema.data_locks;

-- 查看锁等待信息
SELECT 
  REQUESTING_ENGINE_TRANSACTION_ID AS req_trx_id,
  BLOCKING_ENGINE_TRANSACTION_ID AS blk_trx_id,
  OBJECT_NAME AS table_name
FROM performance_schema.data_lock_waits;

-- 查看事务状态
SHOW ENGINE INNODB STATUS\G

七、总结与进阶方向

核心知识点梳理

  1. 原子性依赖UndoLog,持久性依赖RedoLog,隔离性依赖锁+MVCC;
  2. 锁按粒度分为表级锁/行级锁,按模式分为S/X锁、意向锁、间隙锁等;
  3. 四种隔离级别权衡并发性能与数据安全,InnoDB默认REPEATABLE READ;
  4. MVCC通过版本链和ReadView实现无锁读,是高并发的关键;
  5. 死锁的核心是循环等待,通过统一锁顺序、缩短事务等方式避免。

进阶学习方向

  1. RedoLog与UndoLog的刷盘机制;
  2. 双写缓冲区与崩溃恢复的详细流程;
  3. 索引与锁的关系(如非主键索引的锁机制);
  4. 分布式事务(XA事务、2PC/3PC);
  5. 锁优化工具(如pt-table-checksum、pt-deadlock-logger)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值