中间件专栏之MySQL篇——MySQL事务原理、锁机制分析

发布于:2025-03-04 ⋅ 阅读:(15) ⋅ 点赞:(0)

MySQL的事务性也是其重要特性之一。

什么是事务:事务的本质是并发控制的单元,是用户定义的一个操作序列。这些操作要么都做,要么都不做,是 一个不可分割的工作单位。

目的:事务的目的在于将数据库从一种一致性状态转换为另一种一致性状态,保证系统始终处于一个完整且正确的状态。

组成:事务可由一条非常简单的 SQL 语句组成,也可以由一组复杂的 SQL 语句组成。

特征:在数据库提交事务时,可以确保要么所有修改都已经保存,要么所有修改都不保存; 事务是访问并更新数据库各种数据项的一个程序执行单元。

事务控制语句:

-- 显示开启事务
START TRANSACTION | BEGIN-- 提交事务,并使得已对数据库做的所有修改持久化
COMMIT-- 回滚事务,结束用户的事务,并撤销正在进行的所有未提交的修改
ROLLBACK-- 创建一个保存点,一个事务可以有多个保存点
SAVEPOINT identifier-- 删除一个保存点
RELEASE SAVEPOINT identifier-- 事务回滚到保存点
ROLLBACK TO [SAVEPOINT] identifier

一、事务的ACID特性

1. ACID 四大特性

特性 描述 作用
A(原子性,Atomicity) 事务是不可分割的最小工作单元,要么全部执行成功,要么全部回滚。 确保事务中所有 SQL 语句要么都成功,要么都失败
C(一致性,Consistency) 事务执行前后,数据库必须保持一致状态,不会破坏数据的完整性。 保证数据在事务开始和结束后满足所有约束,如外键、唯一性等。
I(隔离性,Isolation) 多个事务同时执行时,彼此不会相互影响,避免脏读、幻读等问题。 MySQL 通过不同的隔离级别控制事务之间的可见性。
D(持久性,Durability) 事务一旦提交,数据就会永久存储,即使系统崩溃也不会丢失。 通过 redo logbinlog 保证事务的持久性。

2. ACID 详解

2.1 原子性(Atomicity)

事务必须是不可分割的整体

  • 如果某个操作失败,事务会回滚,撤销所有已执行的操作。
  • MySQL 通过 UNDO LOG(回滚日志)实现回滚机制

示例

START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; 
UPDATE accounts SET balance = balance + 100 WHERE id = 2; 
ROLLBACK; -- 撤销所有操作

🔹 如果发生错误或断电,所有已执行的 SQL 语句都会被回滚,保证账户数据不丢失。


2.2 一致性(Consistency)

事务执行前后,数据必须满足完整性约束

  • 事务不能破坏数据库的外键约束、唯一性约束等规则。

示例 假设 accounts 表规定 balance >= 0,那么:

START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

🔹 如果某个账户余额变成负数,事务必须回滚,保持数据一致性


2.3 隔离性(Isolation)

多个事务并发执行时,彼此的修改互不影响

  • 解决脏读、不可重复读、幻读等问题。
  • MySQL 提供 四种事务隔离级别
    1. READ UNCOMMITTED(读未提交) → 允许读取未提交数据,可能导致脏读
    2. READ COMMITTED(读已提交) → 只能读取已提交数据,但可能出现不可重复读
    3. REPEATABLE READ(可重复读,MySQL 默认) → 多次查询结果一致,避免不可重复读
    4. SERIALIZABLE(可串行化) → 最高隔离级别,强制事务串行执行,性能最低

示例

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 第一次读取
-- 其他事务修改 balance
SELECT balance FROM accounts WHERE id = 1; -- 仍然是原来的值
COMMIT;

🔹 可重复读(默认)能保证第二次查询仍然返回原始数据,避免不可重复读问题


2.4 持久性(Durability)

事务一旦提交,数据就必须永久存储

  • MySQL 通过 Redo Log(重做日志) 和 Binlog(二进制日志) 记录已提交的事务。
  • 即使服务器崩溃,重启后 MySQL 也能恢复数据

示例

START TRANSACTION;
UPDATE orders SET status = 'shipped' WHERE id = 123;
COMMIT; -- 事务提交后数据不会丢失

🔹 即使 MySQL 崩溃,订单状态仍然是 shipped

二、四大隔离级别详解

1. MySQL 四大事务隔离级别

MySQL 提供四种 事务隔离级别(由低到高):

隔离级别 脏读 不可重复读 幻读 性能 SQL 设置
READ UNCOMMITTED(读未提交) ❌ 可能 ❌ 可能 ❌ 可能 ✅ 性能最高 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
READ COMMITTED(读已提交) ✅ 不可能 ❌ 可能 ❌ 可能 ⏳ 适中 SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
REPEATABLE READ(可重复读)MySQL 默认 ✅ 不可能 ✅ 不可能 ❌ 可能 ⏳ 适中 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SERIALIZABLE(可串行化) ✅ 不可能 ✅ 不可能 ✅ 不可能 ❌ 性能最低 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

✅ = 解决了该问题
❌ = 可能出现该问题


2. 事务隔离级别问题详解

2.1 脏读(Dirty Read)

定义:一个事务可以读取到另一个未提交事务的修改。如果该事务回滚,则读取到的数据是无效的,造成数据不一致。

示例

  1. 事务 A 修改 balance = 1000 → 500(但未提交)。
  2. 事务 B 读取 balance = 500(但事务 A 可能回滚)。
  3. 事务 A 回滚balance 变回 1000,但事务 B 读到了错误的数据。

解决方法

  • READ COMMITTED 及以上级别可以避免脏读。

2.2 不可重复读(Non-Repeatable Read)

定义:同一事务内多次查询同一条记录,但结果不一致,因为另一个事务修改并提交了数据

示例

  1. 事务 A 读取 balance = 1000
  2. 事务 B 修改并提交 balance = 500
  3. 事务 A 再次读取 balance,发现变成 500,数据发生变化。

解决方法

  • REPEATABLE READ 及以上级别可以避免不可重复读。

2.3 幻读(Phantom Read)

定义:一个事务内多次查询相同条件的数据,但结果不一致,因为另一个事务插入/删除了数据

示例

  1. 事务 A 运行 SELECT * FROM users WHERE age > 30;,返回 5 条数据。
  2. 事务 B 插入 一条 age > 30 的数据并提交。
  3. 事务 A 再次运行 相同查询,返回 6 条数据,发生幻读。

解决方法

  • SERIALIZABLE 级别可以避免幻读(使用表级锁)。
  • InnoDB 默认通过 GAP 锁避免幻读

3. 事务隔离级别总结

隔离级别 适用场景 优缺点
READ UNCOMMITTED(读未提交) 允许最高并发,但数据不安全 可能发生脏读、不可重复读、幻读
READ COMMITTED(读已提交) 大部分数据库的默认级别(如 Oracle),保证读到已提交数据 可能发生不可重复读、幻读
REPEATABLE READ(可重复读)MySQL 默认 适合大多数应用,保证同一事务多次读取结果一致 可能发生幻读
SERIALIZABLE(可串行化) 最高级别,数据最安全,所有事务串行执行

性能最低,适用于高安全性应用

三、MVCC详解

1. 什么是 MVCC(多版本并发控制)?

MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种并发控制机制,可以让多个事务无锁并发执行,提高数据库性能,同时避免脏读、不可重复读等问题。

  • MVCC 主要用于 InnoDB 存储引擎,默认支持 REPEATABLE READ 隔离级别。
  • 通过 数据行的多个版本,使得读操作不会被写操作阻塞,提升并发性能。

2. MVCC 解决了哪些问题?

避免脏读:事务只能读取已经提交的数据。
避免不可重复读:在同一事务内,多次读取相同数据,结果保持一致。
提高并发性能:读操作无需加锁,避免锁竞争。

无法解决幻读:MVCC 无法防止插入数据带来的幻读,需要使用 GAP 锁


3. MVCC 的实现原理

3.1 MVCC 依赖的关键机制

MVCC 在 InnoDB 中主要依赖以下 三个重要的存储结构

  1. 隐藏列(隐式事务 ID 和回滚指针)
  2. Undo Log(回滚日志)
  3. Read View(读视图)

3.2 MVCC 关键实现细节

(1)隐藏列

InnoDB 为每个数据行额外存储两个隐藏列

  • trx_id:记录插入/修改该行的事务 ID。
  • roll_pointer:指向 Undo Log,可通过回滚日志获取数据的旧版本。

(2)Undo Log(回滚日志)
  • 作用:存储旧版本数据,使得历史版本可读(多版本存储)。
  • 实现
    • 每次 UPDATEDELETE,都会把旧值存入 Undo Log,并更新 roll_pointer 指向旧版本数据。
    • 事务回滚时,Undo Log 可用于恢复旧数据

(3)Read View(读视图)
  • 作用:控制事务可见性,决定事务是否可以看到某个版本的数据。
  • 核心逻辑
    1. 事务 T1 开启时,会生成 Read View(包含当前活跃事务的 trx_id 列表)。
    2. 查询数据时:
      • trx_id 小于 Read View 最小活跃事务 ✅ → 可见(数据已提交)。
      • trx_id 在活跃事务范围内 ❌ → 不可见(数据未提交)。
      • trx_id 大于当前事务 ID ❌ → 不可见(数据由新事务插入)。
    3. 事务 T1 只能看到 Read View 创建时的数据快照,即快照读

4. MVCC 读写方式

(1)快照读(Snapshot Read)

  • 读取的是数据的历史版本不加锁,提高查询性能。
  • 适用 SQL
  • 实现方式
    • Read View 读取符合当前事务可见性的数据。
    • Undo Log 提供历史版本数据。

(2)当前读(Current Read)

  • 读取的是最新版本数据,会加锁
  • 适用 SQL(需要保证数据一致性)
  • 实现方式
    • 事务需要读取最新版本数据,并阻塞其他事务修改该数据

5. MVCC 在不同隔离级别下的行为

隔离级别 快照读 当前读
READ UNCOMMITTED(读未提交) 读取未提交的数据(不安全) 直接读取最新版本
READ COMMITTED(读已提交) 读取最新已提交版本 加锁,读取最新数据
REPEATABLE READ(可重复读,MySQL 默认) 同一事务内,多次查询结果一致 加锁,读取最新数据
SERIALIZABLE(可串行化) 强制事务串行执行 加锁,阻塞其他事务

6. MVCC 适用范围

适用于 InnoDB 表(默认支持 MVCC)。
不适用于 MyISAM(MyISAM 不支持事务,不会存储多个数据版本)。
不适用于 SERIALIZABLE 级别(MVCC 失效,会强制加锁)。


7. MVCC 总结

机制 作用
隐藏列(trx_id、roll_pointer) 记录事务 ID、旧数据指针
Undo Log(回滚日志) 存储数据的历史版本
Read View(读视图) 确定事务可见性,控制快照读

MVCC 优势

✅ 读操作不加锁,并发性能高。
✅ 保证事务隔离性,避免脏读、不可重复读问题。

MVCC 局限

❌ 需要存储多个版本的数据,占用空间。
无法避免幻读(需要GAP 锁)。

适用场景

OLTP(高并发事务处理)场景,如 银行系统、电商订单管理
查询频繁但更新少的系统,如 分析报表

MVCC 是 MySQL 高性能事务控制的核心,合理利用 MVCC 机制,可以大幅提升数据库性能,同时保证数据一致性!

四、MySQL中锁机制

 

MySQL 的锁机制用于控制多个事务并发访问数据库时的数据一致性和并发性。不同的存储引擎(如 InnoDBMyISAM)实现的锁机制有所不同,InnoDB 采用更细粒度的锁控制,并支持 行级锁表级锁意向锁 等。


1. MySQL 锁的分类

MySQL 的锁可分为 全局锁表级锁行级锁,其中行级锁又包括共享锁、排他锁、意向锁、间隙锁等。

锁类型 适用范围 特点
全局锁 作用于整个数据库 FLUSH TABLES WITH READ LOCK,适用于备份数据,影响所有操作。
表级锁 作用于整张表 LOCK TABLES,如 MyISAM 存储引擎使用表锁,事务并发能力低。
行级锁 作用于单行数据 InnoDB 支持,提供更高的并发性能,但需要管理更多的锁信息。

2. MySQL 表级锁

2.1 表锁(Table Lock)

适用于 MyISAM,每次锁定整个表,不支持行级锁

操作 SQL 语句 特性
加读锁(共享锁) LOCK TABLES users READ; 其他线程可读,但不可写
加写锁(排他锁) LOCK TABLES users WRITE; 其他线程不可读也不可写
释放表锁 UNLOCK TABLES; 释放锁,其他事务可访问

缺点:影响并发,大多数情况下不推荐使用


3. MySQL 行级锁

3.1 共享锁(S 锁,Shared Lock)

  • 多个事务可同时读取数据,但不能修改数据
  • 适用于 SELECT ... LOCK IN SHARE MODE

🔹 应用场景:如果多个事务需要并发读取数据,并且不希望数据被修改,可使用共享锁。 

3.2 排他锁(X 锁,Exclusive Lock)

  • 排他性强,其他事务不能读取或修改被锁定的行
  • 适用于 SELECT ... FOR UPDATE

🔹 应用场景:用于更新前的查询,确保数据在更新前不会被其他事务修改。 

3.3 意向锁(Intent Lock)

  • 自动加锁,用于标识事务希望获取行级锁,防止表锁和行锁冲突。
  • InnoDB 自动管理,开发者无需手动控制
锁类型 作用
意向共享锁(IS,Intent Shared) 事务想要加 S 锁,表级锁不能加 X 锁
意向排他锁(IX,Intent Exclusive) 事务想要加 X 锁,表级锁不能加 S/X 锁

🔹 应用场景:防止表级锁与行级锁冲突,提高锁管理效率。


3.4 间隙锁(Gap Lock,防止幻读)

作用:防止幻读,用于 REPEATABLE READ 隔离级别。

🔹 应用场景:当事务查询 age BETWEEN 18 AND 25 时,即使不存在数据,InnoDB 仍会锁住范围,防止其他事务插入 age=20 的新数据。


3.5 Next-Key Lock(间隙锁 + 行锁)

  • 锁定一行数据及其范围,防止新数据插入。
  • InnoDB 在 REPEATABLE READ 级别下默认使用,避免幻读。

🔹 如果 id=10 存在,InnoDB 不仅锁定该行,还会锁定 id=10 之后的索引范围**,防止插入新记录。


4. 死锁(Deadlock)及避免方法

4.1 什么是死锁?

死锁是指两个或多个事务互相等待对方释放锁,导致事务无法继续执行。

4.2 如何避免死锁?

方法 策略
固定访问顺序 保证所有事务按照相同顺序访问资源,避免循环等待。
减少锁的持有时间 及时提交事务,避免长时间持有锁。
使用较高的隔离级别 SERIALIZABLE 可以减少并发,避免复杂死锁。
索引优化 减少锁定的行数,优化 WHERE 条件,避免锁住大量数据。

总结

锁类型 作用 适用存储引擎
全局锁 锁住整个数据库,影响所有操作 所有存储引擎
表级锁 锁住整张表,适用于 MyISAM MyISAM,InnoDB
行级锁 只锁住特定行,提高并发性能 InnoDB
共享锁(S 锁) 允许读取,不允许修改 InnoDB
排他锁(X 锁) 其他事务不能读也不能写 InnoDB
意向锁(IS/IX) 防止表级锁和行级锁冲突 InnoDB
间隙锁(Gap Lock) 防止幻读,锁定范围 InnoDB
Next-Key Lock 行锁 + 间隙锁,避免幻读 InnoDB