可以把 MySQL 的事务和锁 理解成两个相关但不同的东西:
事务:保证"一组操作要么全部成功,要么全部失败"。
锁:保证"多个操作同时修改/读取数据时,不要互相搞乱"。
你现在如果在学 MySQL / Spring Boot,这两个概念非常重要。
一、先理解什么是事务
比如银行转账:
张三账户 -100
李四账户 +100
这其实是两个 SQL:
ini
UPDATE account SET money = money - 100 WHERE name = '张三';
UPDATE account SET money = money + 100 WHERE name = '李四';
如果第一条成功,第二条失败:
张三:少了 100
李四:没收到 100
这显然不行。
所以我们希望:
sql
开始事务
↓
张三 -100
↓
李四 +100
↓
全部成功 → COMMIT
↓
只要一个失败 → ROLLBACK
这就是事务。
二、MySQL 怎么开启事务?
ini
START TRANSACTION;
UPDATE account
SET money = money - 100
WHERE name = '张三';
UPDATE account
SET money = money + 100
WHERE name = '李四';
COMMIT;
如果中间出错:
ini
ROLLBACK;
例如:
ini
START TRANSACTION;
UPDATE account
SET money = money - 100
WHERE name = '张三';
-- 假设这里发生错误
ROLLBACK;
那么前面的修改也会撤销。
三、事务最重要的 4 个特性:ACID
这个面试非常常见。
1. Atomicity 原子性
一组操作,要么全部成功,要么全部失败。
比如:
转账
张三 -100
李四 +100
不能出现:
张三 -100
李四 没 +100
所以:
事务是一个不可再分的整体。
2. Consistency 一致性
事务执行前后,数据库都应该处于合法状态。
比如:
yaml
转账前:
张三 1000
李四 1000
总金额 = 2000
转账 100 后:
yaml
张三 900
李四 1100
总金额 = 2000
总金额不能莫名其妙变成:
yaml
1900
3. Isolation 隔离性
多个事务同时执行的时候,互相不能随便干扰。
例如:
css
事务 A:修改张三余额
事务 B:同时查询张三余额
B 到底应该看到修改前还是修改后的数据?
这就是事务隔离要解决的问题。
4. Durability 持久性
事务一旦:
ini
COMMIT;
成功提交:
数据就应该永久保存。
即使数据库之后重启,也不能因为重启导致刚提交的数据消失。
四、那锁又是什么?
假设现在有一个商品:
商品库存 = 10
突然有两个用户同时购买。
css
用户 A:买 1 个
用户 B:买 1 个
如果没有任何控制,可能发生:
css
A 读取库存:10
B 读取库存:10
A:10 - 1 = 9
B:10 - 1 = 9
最终库存:9
但实际上应该是:
ini
10 - 1 - 1 = 8
这就是并发问题。
五、锁就是解决并发问题的
比如 A 要修改库存:
ini
UPDATE product
SET stock = stock - 1
WHERE id = 1;
MySQL 会对相关数据加锁。
可以简单理解成:
css
A:
我要修改商品 1
🔒 商品1
B:
我要修改商品 1
⏳ 等待
A 完成:
sql
COMMIT
之后:
🔓 商品1
B 才能继续。
所以:
锁就是为了控制并发访问。
六、事务和锁是什么关系?
这是最容易混淆的地方。
你可以这么理解:
事务
↓
管理一组数据库操作
↓
保证这组操作的完整性
而:
锁
↓
控制多个事务之间的并发访问
↓
防止数据冲突
两者经常一起出现。
例如:
ini
START TRANSACTION;
SELECT stock
FROM product
WHERE id = 1
FOR UPDATE;
UPDATE product
SET stock = stock - 1
WHERE id = 1;
COMMIT;
这里:
sql
START TRANSACTION
是事务。
而:
sql
FOR UPDATE
是在请求排他锁。
七、FOR UPDATE 是什么?
这个在 Spring Boot 开发中很常见。
假设:
ini
SELECT stock
FROM product
WHERE id = 1;
普通查询。
如果改成:
sql
SELECT stock
FROM product
WHERE id = 1
FOR UPDATE;
意思可以理解成:
我要读取这条数据,而且接下来我要修改它,请先把它锁住。
例如库存扣减:
ini
START TRANSACTION;
SELECT stock
FROM product
WHERE id = 1
FOR UPDATE;
UPDATE product
SET stock = stock - 1
WHERE id = 1;
COMMIT;
并发情况下:
sql
事务 A
↓
SELECT ... FOR UPDATE
↓
🔒 锁住商品
↓
修改库存
↓
COMMIT
↓
🔓 解锁
事务 B
↓
SELECT ... FOR UPDATE
↓
等待 A 释放锁
↓
拿到最新库存
↓
继续操作
这就避免了很多并发修改问题。
八、MySQL 常见的锁
先不用记得特别细,建立一个整体概念:
markdown
MySQL 锁
│
├── 按锁的粒度
│ ├── 表锁
│ └── 行锁
│
└── 按锁的性质
├── 共享锁
└── 排他锁
1. 表锁
锁住整张表。
比如:
bash
user 表
┌──────────────────┐
│ id │ name │ age │
├──────────────────┤
│ 1 │ 张三 │ 18 │
│ 2 │ 李四 │ 20 │
│ 3 │ 王五 │ 22 │
└──────────────────┘
🔒
整张表
其他事务可能无法正常修改这张表。
优点:
简单。
缺点:
并发能力比较差。
九、行锁
行锁就是:
只锁某一行。
比如:
bash
user 表
id name
1 张三 🔒
2 李四
3 王五
事务 A 修改:
ini
UPDATE user
SET name = '张三三'
WHERE id = 1;
主要锁住:
ini
id = 1
这时候另一个事务修改:
ini
UPDATE user
SET name = '李四四'
WHERE id = 2;
通常可以正常进行。
所以:
行锁并发能力比表锁好很多。
InnoDB 就大量使用行级锁。
十、共享锁和排他锁
这两个也非常重要。
共享锁 S Lock
Shared Lock。
可以简单理解:
大家都可以读,但不能随便修改。
例如:
css
事务 A:读 🔒
事务 B:读 🔒
可以
事务 C:修改 ❌
排他锁 X Lock
Exclusive Lock。
可以理解成:
我现在要修改,你们先别动。
例如:
css
事务 A:
🔒 X锁
其他事务:
读取/修改
↓
可能需要等待
SELECT ... FOR UPDATE 通常就是为了获取排他性质的锁。
十一、为什么又出现"事务隔离级别"?
因为多个事务同时运行时,会出现各种问题。
例如:
css
事务 A
事务 B
A 修改数据的时候,B 到底能不能看到?
所以 MySQL 提供了不同的事务隔离级别。
主要有 4 个:
markdown
READ UNCOMMITTED
↓
READ COMMITTED
↓
REPEATABLE READ
↓
SERIALIZABLE
隔离性越来越强,并发能力通常越来越弱。
十二、READ UNCOMMITTED
最低级别。
事务 A 修改:
yaml
余额 = 1000
↓
修改成 500
还没:
ini
COMMIT;
事务 B 就可能读到:
500
但是 A 最后:
ini
ROLLBACK;
实际上数据库又变回:
yaml
1000
于是 B 刚才读到了一个根本不存在的最终数据。
这叫:
脏读
十三、READ COMMITTED
要求:
只能读取已经提交的数据。
所以:
css
A 修改
↓
500
↓
未提交
B 查询
↓
看到 1000
A:
ini
COMMIT;
B 再查询:
看到 500
这样可以解决:
脏读
但是会出现另一个问题:
css
事务 B:
第一次查询:
1000
事务 A:
修改成 500
COMMIT
事务 B:
第二次查询:
500
同一个事务里:
第一次:1000
第二次:500
这叫:
不可重复读
十四、REPEATABLE READ
意思就是:
同一个事务里,多次读取同一条数据,结果尽量保持一致。
MySQL InnoDB 默认就是:
REPEATABLE READ
例如:
css
事务 B:
第一次查询:
1000
事务 A:
修改成 500
COMMIT
事务 B:
第二次查询:
1000
这样 B 在自己的事务里看到的数据是一致的。
十五、SERIALIZABLE
最严格。
可以理解成:
大家排队执行。
并发能力最低,但是隔离性最高。
简单理解:
css
事务 A
↓
执行完
↓
事务 B
↓
执行完
↓
事务 C
当然 MySQL 实际实现比这个复杂,这里先建立概念就够了。
十六、你可以把整个东西串起来
你现在学习 MySQL,可以形成这张脑图:
markdown
MySQL
│
┌────────┴────────┐
│ │
事务 锁
│ │
一组操作的整体性 控制并发
│ │
ACID 防止数据冲突
│ │
└───────┬─────────┘
│
事务隔离
│
┌──────────┼──────────┐
│ │ │
RU RC RR Serializable
│ │ │ │
脏读 不可重复读 幻读等 最严格
十七、结合你以后写 Spring Boot 来看
你以后可能会看到:
typescript
@Transactional
public void transfer() {
accountMapper.decreaseMoney(1, 100);
accountMapper.increaseMoney(2, 100);
}
这里:
css
@Transactional
就是告诉 Spring:
这个方法里的数据库操作放到一个事务里面。
于是:
scss
方法开始
↓
开启事务
↓
decreaseMoney()
↓
increaseMoney()
↓
全部成功
↓
COMMIT
如果中间异常:
scss
decreaseMoney()
↓
increaseMoney()
↓
发生异常
↓
ROLLBACK
而如果你又写:
python
@Select("""
SELECT *
FROM product
WHERE id = #{id}
FOR UPDATE
""")
Product selectForUpdate(Long id);
那么:
java
@Transactional
public void buy() {
Product product = mapper.selectForUpdate(id);
// 扣库存
}
就是:
sql
@Transactional
↓
事务
↓
SELECT ... FOR UPDATE
↓
加锁
↓
修改数据
↓
COMMIT
事务负责"这一整套操作是不是一个完整的整体",锁负责"并发情况下别人能不能同时动这份数据"。
你如果正在从 Java 基础往 Spring Boot 过渡,建议下一步重点搞懂 "脏读、不可重复读、幻读 + MySQL 的 MVCC" 。这几个理解了,事务和锁基本就串起来了。