MySQL事务与锁-深入理解ACID

一、什么是事务?

事务是数据库执行过程中的一个逻辑单元,保证一组操作要么全部成功,要么全部失败。

1.1 事务的ACID特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────────────────────────────────┐
│ ACID特性 │
├─────────────────────────────────────────────────────────────────┤
│ │
A → Atomic(原子性) │
│ 要么全做,要么不做 │
│ │
│ C → Consistency(一致性) │
│ 事务前后,数据状态一致 │
│ │
IIsolation(隔离性) │
│ 并发事务互不干扰 │
│ │
│ D → Durability(持久性) │
│ 事务提交后,修改永久保存 │
│ │
└─────────────────────────────────────────────────────────────────┘

1.2 事务示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 转账场景
START TRANSACTION;

-- 扣款
UPDATE accounts SET balance = balance - 1000
WHERE user_id = 1;

-- 收款
UPDATE accounts SET balance = balance + 1000
WHERE user_id = 2;

-- 提交事务
COMMIT;

-- 如果出错,回滚
-- ROLLBACK;

二、事务操作

2.1 基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 开启事务
START TRANSACTION;
-- 或
BEGIN;

-- 提交事务
COMMIT;

-- 回滚事务
ROLLBACK;

-- 回滚到保存点
ROLLBACK TO SAVEPOINT point1;

-- 设置保存点
SAVEPOINT point1;

-- 释放保存点
RELEASE SAVEPOINT point1;

2.2 自动提交

1
2
3
4
5
6
7
8
9
-- 查看自动提交
SHOW VARIABLES LIKE 'autocommit';
-- 默认:ON(每条SQL都是一个事务)

-- 关闭自动提交
SET autocommit = 0;
-- 手动开启事务
START TRANSACTION;
-- 关闭后需要手动COMMIT或ROLLBACK

2.3 事务支持情况

1
2
3
4
5
-- 查看引擎是否支持事务
SHOW ENGINES;

-- InnoDB 支持事务
-- MyISAM 不支持事务

三、隔离级别

3.1 隔离级别概述

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED ✅ 可能 ✅ 可能 ✅ 可能
READ COMMITTED ❌ 不可能 ✅ 可能 ✅ 可能
REPEATABLE READ ❌ 不可能 ❌ 不可能 ✅ 可能(MySQL可避免)
SERIALIZABLE ❌ 不可能 ❌ 不可能 ❌ 不可能
1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────────────────────────────┐
│ 隔离级别对比 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 隔离程度从低到高: │
READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READ
│ → SERIALIZABLE
│ │
│ 隔离程度越高,数据越安全,但性能越差 │
│ MySQL默认:REPEATABLE READ
│ │
└─────────────────────────────────────────────────────────────────┘

3.2 设置隔离级别

1
2
3
4
5
6
7
8
9
-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 查看隔离级别
SELECT @@transaction_isolation;
-- MySQL 5.7: @@tx_isolation

3.3 各种问题详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 脏读:一个事务读取了另一个事务未提交的数据
-- 事务A:START; UPDATE users SET age = 30 WHERE id = 1;
-- 事务B:SELECT age FROM users WHERE id = 1; -- 读到30(脏数据)
-- 事务A:ROLLBACK;

-- 不可重复读:同一事务两次读取数据不一致
-- 事务A:START; SELECT age FROM users WHERE id = 1; -- 读到25
-- 事务B:START; UPDATE users SET age = 30 WHERE id = 1; COMMIT;
-- 事务A:SELECT age FROM users WHERE id = 1; -- 读到30

-- 幻读:同一事务两次查询结果集不同
-- 事务A:START; SELECT * FROM users WHERE age > 20; -- 查到3条
-- 事务B:START; INSERT INTO users VALUES (4, 'new', 25); COMMIT;
-- 事务A:SELECT * FROM users WHERE age > 20; -- 查到4条

四、InnoDB锁机制

4.1 锁类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────────────────┐
│ 锁类型分类 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 按粒度分: │
│ ├─ 表级锁 → 开销小,加锁快 │
│ │ 表锁、全局锁 │
│ └─ 行级锁 → 开销大,加锁慢,支持并发 │
│ 行锁、间隙锁 │
│ │
│ 按类型分: │
│ ├─ 共享锁(S锁)→ 读锁,可共存 │
│ └─ 排他锁(X锁)→ 写锁,互斥 │
│ │
│ 按方式分: │
│ ├─ 乐观锁 → 版本号 │
│ └─ 悲观锁 → 排他锁 │
│ │
└─────────────────────────────────────────────────────────────────┘

4.2 行锁

1
2
3
4
5
6
7
8
9
10
-- 共享锁:读取数据
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 其他事务可以读,但不能写

-- 排他锁:写入数据
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 其他事务不能读也不能写

-- 手动加锁(InnoDB)
SELECT * FROM users WHERE id = 1 FOR UPDATE;

4.3 表锁

1
2
3
4
5
6
7
8
9
10
11
12
-- 加读锁
LOCK TABLES users READ;

-- 加写锁
LOCK TABLES users WRITE;

-- 解锁
UNLOCK TABLES;

-- 元数据锁(DDL操作自动加锁)
-- DML操作需要MDL读锁
-- DDL操作需要MDL写锁

4.4 意向锁

1
2
3
4
-- 意向锁:表明有人要锁行
-- 意向共享锁(IS):SELECT ... LOCK IN SHARE MODE
-- 意向排他锁(IX):SELECT ... FOR UPDATE
-- 自动加,无需手动操作

五、间隙锁与临键锁

5.1 间隙锁

1
2
3
4
5
-- 间隙锁:锁定索引区间(左开右闭)
-- 假设age索引有:10, 20, 30
SELECT * FROM users WHERE age BETWEEN 15 AND 25 FOR UPDATE;
-- 锁住:(15, 20] 区间
-- 防止其他事务在区间内插入数据(解决幻读)

5.2 临键锁

1
2
3
4
5
-- 临键锁:间隙锁 + 行锁的组合
-- 假设age索引有:10, 20, 30
SELECT * FROM users WHERE age = 20 FOR UPDATE;
-- 锁住:(10, 20] 和 (20, 30] 两个区间
-- 既锁住存在的行,也锁住区间

5.3 锁分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 查看当前锁等待
SHOW ENGINE INNODB STATUS;

-- 查看锁信息
SHOW FULL PROCESSLIST;

-- 查看事务锁
SELECT * FROM information_schema.INNODB_TRX;

-- 查看锁等待
SELECT * FROM information_schema.INNODB_LOCK_WAITS;

-- 查看锁
SELECT * FROM information_schema.INNODB_LOCKS;

六、乐观锁与悲观锁

6.1 悲观锁

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 悲观锁:先加锁,再操作
-- 适用于并发 contention 激烈的场景

START TRANSACTION;

-- 查询并锁定(排他锁)
SELECT * FROM products WHERE id = 1 FOR UPDATE;

-- 检查库存
-- 更新库存
UPDATE products SET stock = stock - 1 WHERE id = 1;

COMMIT;

6.2 乐观锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 乐观锁:先操作,再检查版本
-- 适用于并发 contention 少的场景

-- 方法1:版本号
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 1;

-- 方法2:时间戳
UPDATE products
SET stock = stock - 1, updated_at = NOW()
WHERE id = 1 AND updated_at = '2026-01-01 12:00:00';

-- 方法3:检查影响行数
-- 如果影响行数为0,说明被其他事务修改了,需要重试

6.3 两种锁对比

特性 乐观锁 悲观锁
实现方式 版本号、时间戳 数据库锁
适用场景 并发少 并发多
性能
安全性 可能冲突失败 保证安全
编程复杂度 需要重试机制 简单

七、死锁

7.1 死锁产生

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌─────────────────────────────────────────────────────────────────┐
│ 死锁示例 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 事务A: │
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- 锁住id=1 │
UPDATE users SET age = 30 WHERE id = 2; -- 等待id=2锁 │
│ │
│ 事务B: │
SELECT * FROM users WHERE id = 2 FOR UPDATE; -- 锁住id=2 │
UPDATE users SET age = 30 WHERE id = 1; -- 等待id=1锁 │
│ │
│ 死锁!互相等待对方释放锁 │
│ │
└─────────────────────────────────────────────────────────────────┘

7.2 避免死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 1. 统一顺序加锁
-- 总是先锁id小的,再锁id大的

-- 2. 降低隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 3. 减少锁定时间
-- 尽快提交事务

-- 4. 使用乐观锁
-- 适用于简单业务

-- 5. 分析死锁日志
SHOW ENGINE INNODB STATUS;

7.3 处理死锁

1
2
3
4
5
6
7
8
9
10
11
-- 查看死锁信息
SHOW ENGINE INNODB STATUS;

-- 等待超时回滚
-- 设置锁等待超时
SET innodb_lock_wait_timeout = 50;

-- 强制杀掉查询
-- 找到阻塞的线程
SELECT * FROM information_schema.INNODB_TRX;
-- KILL trx_mysql_thread_id;

八、分布式事务

8.1 方案对比

方案 原理 优点 缺点
2PC 两阶段提交 强一致性 性能差,有协调者单点
3PC 三阶段提交 减少阻塞 依然有协调者问题
TCC Try-Confirm-Cancel 性能好 业务侵入性强
本地消息表 消息队列 简单可靠 复杂度高
Seata AT模式 封装完善 需要引入中间件

8.2 XA事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 开启XA事务
XA START 'transaction_id';

-- 执行SQL
UPDATE accounts SET balance = balance - 100 WHERE user_id = 1;

-- 结束事务
XA END 'transaction_id';

-- 准备提交
XA PREPARE 'transaction_id';

-- 提交
XA COMMIT 'transaction_id';

-- 回滚
XA ROLLBACK 'transaction_id';

九、实战案例

9.1 订单创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- 创建订单(事务处理)
START TRANSACTION;

-- 1. 减库存
UPDATE products SET stock = stock - 1
WHERE id = 1 AND stock > 0;

-- 2. 检查是否成功
-- 如果影响行数为0,库存不足,回滚

-- 3. 创建订单
INSERT INTO orders (user_id, product_id, status)
VALUES (1, 1, 'pending');

-- 4. 记录日志
INSERT INTO order_logs (order_id, action)
VALUES (LAST_INSERT_ID(), 'created');

-- 提交
COMMIT;

9.2 库存扣减

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 方法1:悲观锁
START TRANSACTION;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 检查库存
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;

-- 方法2:乐观锁
UPDATE products
SET stock = stock - 1
WHERE id = 1 AND stock > 0;

-- 检查影响行数,如果为0则重试或提示库存不足

-- 方法3:分布式锁
-- 使用Redis/ZooKeeper实现分布式锁

十、常见问题

10.1 事务占用太多

1
2
3
4
5
6
7
8
9
10
11
12
-- 查看长时间运行的事务
SELECT * FROM information_schema.INNODB_TRX
WHERE trx_state = 'RUNNING';

-- 查看等待锁的事务
SELECT * FROM information_schema.INNODB_TRX
WHERE trx_state = 'LOCK WAIT';

-- 建议:
-- 1. 事务不要太大
-- 2. 及时提交
-- 3. 减少锁定时间

10.2 锁表了怎么办

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 查看锁表
SHOW OPEN TABLES WHERE In_use > 0;

-- 查看进程
SHOW PROCESSLIST;

-- 杀掉阻塞的进程
KILL thread_id;

-- 预防:
-- 1. 加索引避免全表锁
-- 2. 使用合理的隔离级别
-- 3. 减少事务时间

参考资料


持续更新中…欢迎收藏!

#MySQL #事务 #锁 #ACID #隔离级别 #教程


MySQL事务与锁-深入理解ACID
https://r0f2.my/post/31-mysql-transactions-locks/
作者
JA
发布于
2026年2月14日
许可协议