MySQL 事务隔离级别
MySQL 事务隔离级别详解
事务是数据库并发控制的基础,搞懂四个隔离级别,才能解决脏读、幻读问题。
1. 事务 ACID 特性
- 原子性(Atomicity):要么全成功,要么全回滚
- 一致性(Consistency):事务前后数据一致
- 隔离性(Isolation):事务之间互不干扰
- 持久性(Durability):提交后永久保存
2. 四种隔离级别
MySQL 有四种隔离级别,从上到下越来越严格:
读未提交(READ UNCOMMITTED)
- 可以读到别人还没提交的数据
- 会出现脏读
- 几乎没人用
读提交(READ COMMITTED)
- 只能读到别人已经提交的数据
- 解决了脏读,但会出现不可重复读
- Oracle、SQL Server 默认级别
可重复读(REPEATABLE READ)
- 同一个事务里,读多次结果都一样
- 解决了脏读、不可重复读
- MySQL InnoDB 默认级别
- InnoDB 用间隙锁解决了幻读
串行化(SERIALIZABLE)
- 事务一个一个排队执行
- 最安全,但性能最差
- 几乎没人用
3. 三种读问题
脏读:读到了别人还没提交的数据,别人回滚了,你读到的就是脏数据。
不可重复读:同一个事务里,两次读同一行数据,结果不一样(别人中间改了并提交了)。
幻读:同一个事务里,两次查同一个范围,行数不一样(别人中间插了新行)。
4. 查看和设置隔离级别
-- 查看当前隔离级别
SELECT @@global.tx_isolation;
SELECT @@session.tx_isolation;
-- 设置全局隔离级别
SET GLOBAL tx_isolation = 'READ-COMMITTED';
-- 设置当前会话隔离级别
SET SESSION tx_isolation = 'READ-COMMITTED';
5. 实际应用建议
- 默认就好:MySQL 默认的可重复读已经够用
- 高并发场景:可以降到读提交,提升并发性能
- 金融级场景:用串行化,绝对安全
- 大多数业务:默认级别就行,不用折腾
6. 代码示例
// PDO 事务示例
try {
$pdo->beginTransaction();
$pdo->exec("UPDATE account SET money = money - 100 WHERE id = 1");
$pdo->exec("UPDATE account SET money = money + 100 WHERE id = 2");
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
echo "转账失败:" . $e->getMessage();
}
总结
MySQL 事务隔离级别核心:
- 读未提交:脏读
- 读提交:不可重复读
- 可重复读:默认级别,解决幻读
- 串行化:最安全最慢
大多数业务用默认的可重复读就够了。
MySQL 事务与锁:理解隔离级别和死锁的实战指南
前言
事务和锁是 MySQL 最核心也最容易出问题的部分。很多人写了很多年 SQL,却还是搞不清隔离级别和死锁。本文用最直白的方式讲清楚。
一、事务的 ACID
事务就是一组操作,要么全部成功,要么全部失败。四个特性:
- A 原子性:一组操作是一个整体,不能拆分
- C 一致性:事务前后,数据库从一个一致状态到另一个一致状态
- I 隔离性:多个事务之间互不干扰
- D 持久性:事务提交后,数据就永久保存了
二、并发问题
多个事务同时操作数据,会出什么问题?
1. 脏读
事务 A 读到了事务 B 还没提交的数据。
事务 A:修改了余额为 1000,但还没提交
事务 B:读到了余额 1000
事务 A:回滚了,余额变回 500
事务 B:拿着 1000 的错误数据继续操作
2. 不可重复读
事务 A 两次读同一行数据,结果不一样。
事务 A:第一次读余额是 500
事务 B:修改了余额为 1000,提交了
事务 A:第二次读余额是 1000
3. 幻读
事务 A 两次查询,结果集的行数不一样。
事务 A:第一次查询 age > 20 的用户,有 10 条
事务 B:插入了一条 age = 25 的用户,提交了
事务 A:第二次查询 age > 20 的用户,有 11 条
三、四种隔离级别
MySQL 用隔离级别来解决这些并发问题:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(READ UNCOMMITTED) | 会 | 会 | 会 |
| 读提交(READ COMMITTED) | 不会 | 会 | 会 |
| 可重复读(REPEATABLE READ) | 不会 | 不会 | 会(InnoDB 解决了) |
| 串行化(SERIALIZABLE) | 不会 | 不会 | 不会 |
MySQL 默认是可重复读(REPEATABLE READ),InnoDB 在这个级别下用间隙锁解决了幻读。
查看当前隔离级别
SELECT @@tx_isolation;
修改隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
四、锁的类型
1. 共享锁(S 锁)
读锁,多个事务可以同时持有。
SELECT ... LOCK IN SHARE MODE;
2. 排他锁(X 锁)
写锁,只有一个事务能持有。
SELECT ... FOR UPDATE;
3. 表锁 vs 行锁
- 表锁:锁住整张表,开销小,但并发低
- 行锁:锁住一行数据,开销大,但并发高
五、死锁
什么是死锁
两个事务互相等待对方释放锁。
事务 A:锁住了 id = 1,等待 id = 2
事务 B:锁住了 id = 2,等待 id = 1
两个事务都卡住了,谁也不让谁。
怎么避免死锁
- 按相同顺序访问表和行:所有事务都按 id 从小到大操作
- 事务尽量短:事务越长,锁持有时间越长
- 降低隔离级别:隔离级别越低,锁越少
- 加索引:没有索引会走表锁,更容易死锁
查看死锁
-- 查看最近一次死锁
SHOW ENGINE INNODB STATUS;
六、实战:转账
-- 开启事务
BEGIN;
-- 扣款(加行锁)
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 检查余额
SELECT balance FROM account WHERE id = 1;
-- 如果余额不够,回滚
-- ROLLBACK;
-- 加钱
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 提交
COMMIT;
七、常见坑
- 事务太长:锁持有时间太长,容易死锁
- 没有索引:行锁变表锁,并发暴跌
- 隔离级别太高:用串行化,性能很差
- 忘记提交:事务一直开着,锁一直占着
总结
MySQL 事务与锁的核心思路:
- 事务保证 ACID,一组操作要么全成功要么全失败
- 隔离级别越高越安全,但性能越差
- MySQL 默认可重复读,InnoDB 解决了幻读
- 行锁比表锁并发高,但一定要有索引
- 按相同顺序操作,能避免大部分死锁
搞懂这些,你对 MySQL 的理解就超过 80% 的开发者了。