这两段语句虽然表现的结果都是一样的,但其实原理是不同的,对吗?:
1 MySQL InnoDB:事务级 ReadView
事务级:事务开始时建立 ReadView,整个事务期间复用(RR)或每次新建(RC)。
关键事实:MySQL 看不到自己未提交的修改。
-- MySQL InnoDB 行为 BEGIN; UPDATE account SET balance = 1000 WHERE id = 1; -- 改了 SELECT balance FROM account WHERE id = 1; -- ⚠️ 看到 1000(自己的修改) -- 注意:这是"自己能看到自己",但其他事务看不到 -- 其他事务 SELECT balance FROM account WHERE id = 1; -- ⚠️ 看不到 1000(未提交)2 Oracle:语句级 ReadView
语句级:每个 SQL 语句启动时建立 ReadView。
关键事实:Oracle 事务内能读取到自己未提交的修改。
-- Oracle 行为 BEGIN; UPDATE account SET balance = 1000 WHERE id = 1; -- 改了 SELECT balance FROM account WHERE id = 1; -- ✅ 看到 1000(事务内可见) -- 这是因为 Oracle 读 PGA/SGA 内存中的数据,优先于 Undo -- 其他事务 SELECT balance FROM account WHERE id = 1; -- ⚠️ 看不到 1000(未提交)一、直接答:是的,原理不同
| 数据库 | 表面结果 | 底层原理 |
|---|---|---|
| MySQL InnoDB | ✅ 看到 1000(自己的修改) | ⚠️不是"读内存",是"读最新已提交版本"(InnoDB 不存在"事务内未提交可见"机制) |
| Oracle | ✅ 看到 1000(自己的修改) | ✅是"读 PGA/SGA 内存"(Oracle 事务内未提交数据真的就在内存里) |
表面相同,原理不同——这是 4 大差异里最核心的一点。
二、MySQL InnoDB 真实原理(不是内存优先)
2.1 关键事实:MySQL InnoDB 没有"事务内未提交数据可见"机制
-- MySQL InnoDB 实际行为(**注意:MySQL 也能看到自己的未提交修改**) BEGIN; UPDATE account SET balance = 1000 WHERE id = 1; -- 改了 -- InnoDB 实际做了什么? -- 1. 把 balance=1000 写入 **Buffer Pool(内存)** -- 2. 把旧版本 balance=900 写入 **Undo Log** -- 3. 当前行的 DB_TRX_ID 改为 **当前事务 ID**(假设 100) -- 4. DB_ROLL_PTR 指向 Undo Log 里的旧版本 SELECT balance FROM account WHERE id = 1; -- 看到 1000 -- InnoDB 实际做了什么? -- 1. 读当前行 → DB_TRX_ID=100(当前事务 ID) -- 2. **ReadView 4 步判断**: -- - trx_id (100) == creator_trx_id (100) ✅ 自己改的,**可以读** -- 3. 所以读到 balance=1000⚠️ 关键点:
- MySQL 看到自己未提交的修改,不是因为读内存
- 是因为ReadView 4 步判断的第 1 步:
trx_id == creator_trx_id→ **自己改的,返回当前版本 - 当前版本就是最新的(已修改的)版本——不是内存,是数据行的当前状态
2.2 MySQL InnoDB 看不到"内存中的数据",看到的是"数据行当前状态"
-- MySQL InnoDB 的内存数据(Buffer Pool)不是"事务私有" -- 多个事务可能同时访问同一行 -- 当前行的 DB_TRX_ID 决定谁能看 BEGIN; UPDATE account SET balance = 1000 WHERE id = 1; -- 当前行 DB_TRX_ID=100 -- 同一时刻,另一事务: SELECT balance FROM account WHERE id = 1; -- 看不到 1000 -- 因为另一事务的 ReadView 看不到 trx_id=100(未提交)MySQL InnoDB 的核心机制:
- **不存"事务内未提交的私有数据"**到内存
- 当前行的最新状态就是
DB_TRX_ID指向的事务的修改 - 谁能看,由 ReadView 决定
三、Oracle 真实原理(真的读内存)
3.1 关键事实:Oracle 事务内未提交数据真的在内存里
-- Oracle 实际行为 BEGIN; UPDATE account SET balance = 1000 WHERE id = 1; -- 改了 -- Oracle 实际做了什么? -- 1. **把 balance=1000 写入 PGA(Process Global Area,进程私有内存)** -- 2. **不写回磁盘**(不刷新到数据块) -- 3. 数据块 SCN 没变(还指向旧版本) SELECT balance FROM account WHERE id = 1; -- ✅ 看到 1000 -- Oracle 实际做了什么? -- 1. **优先读 PGA 内存**(事务私有) -- 2. 找到 balance=1000 -- 3. 返回 1000⚠️ 关键点:
- Oracle **真的把"事务内未提交的修改"**存在内存里
- 这是和 MySQL 最大的差异——Oracle 事务是有状态的
- MySQL 事务是无状态的(不看内存,看 ReadView)
3.2 Oracle 的内存分层
┌─────────────────────────────────────────────┐ │ Oracle 内存架构(事务视角) │ ├─────────────────────────────────────────────┤ │ │ │ ┌──────────────────────────────────┐ │ │ │ PGA(Process Global Area) │ ← 进程私有内存 │ │ ├─ 当前事务未提交的修改 │ │ │ ├─ 临时表 │ │ │ └─ 排序区 │ │ └──────────────────────────────────┘ │ │ ↓ 优先读 │ │ ┌──────────────────────────────────┐ │ │ │ SGA(System Global Area) │ ← 系统共享内存 │ │ ├─ Buffer Cache(数据块缓存) │ │ │ ├─ Shared Pool │ │ │ └─ Redo Log Buffer │ │ └──────────────────────────────────┘ │ │ ↓ 二次读 │ │ ┌──────────────────────────────────┐ │ │ │ 磁盘 │ ← 最后读 │ │ ├─ Data Files │ │ │ └─ Undo Segment(独立回滚段) │ │ └──────────────────────────────────┘ │ └─────────────────────────────────────────────┘Oracle 读优先级:PGA(事务私有内存) > SGA(共享缓存) > 磁盘
四、MySQL vs Oracle 内存机制对比
| 维度 | MySQL InnoDB | Oracle |
|---|---|---|
| 事务内未提交数据 | ❌不在内存私有区 | ✅在 PGA(事务私有) |
| 内存读取优先级 | Buffer Pool(共享) | PGA(私有) > SGA(共享)> 磁盘 |
| 能看到自己未提交原因 | ReadView 第 1 步:trx_id == creator_trx_id | 直接读 PGA 内存 |
| 其他事务能看到自己未提交 | ❌ 不能 | ❌ 不能(PGA 是事务私有的) |
| 事务回滚 | Undo Log | 丢弃 PGA 内存 + Undo Segment |
| 持久化时机 | 提交时刷脏页 | 提交时刷 SGA + 写 Redo |
核心差异:
- MySQL 是"逻辑层"控制(ReadView + DB_TRX_ID)
- Oracle 是"物理层"控制(PGA 内存 + SCN)
五、同一段 SQL 在两个 DB 的执行路径对比
-- 老哥同一段 SQL:UPDATE 后立即 SELECT BEGIN; UPDATE account SET balance = 1000 WHERE id = 1; SELECT balance FROM account WHERE id = 1; -- 看到 10005.1 MySQL InnoDB 执行路径
1. UPDATE 执行 ├─ 读取 id=1 的当前行(DB_TRX_ID=99, balance=900) ├─ 写入新版本:balance=1000, DB_TRX_ID=100(当前事务 ID) ├─ 旧版本写入 Undo Log:balance=900, DB_TRX_PTR └─ 当前行 DB_ROLL_PTR 指向 Undo Log 2. SELECT 执行 ├─ 读取当前行(DB_TRX_ID=100, balance=1000) ├─ 创建 ReadView(m_ids=[100], min=100, max=101, creator=100) ├─ 4 步判断:trx_id(100) == creator_trx_id(100) ✅ └─ 返回 balance=1000 ⚠️ 注意:InnoDB 没有"内存优先"机制,**只是因为 ReadView 判断逻辑**5.2 Oracle 执行路径
1. UPDATE 执行 ├─ 读取 id=1 的当前数据块(SCN=1234567, balance=900) ├─ 写入新版本:balance=1000 → **PGA 内存**(事务私有) ├─ SCN +1 → 1234568 └─ 数据块未刷新(commit 时才刷) 2. SELECT 执行 ├─ **优先查 PGA 内存**(事务私有) ├─ 找到 balance=1000 └─ 返回 1000(**不需要查数据块**) ⚠️ 注意:Oracle **真的从内存读**,**不经过 SCN 判断**六、3 大本质差异
| 维度 | MySQL InnoDB | Oracle |
|---|---|---|
| 事务模型 | 无状态事务(无私有内存) | 有状态事务(PGA 私有内存) |
| 判断机制 | 逻辑判断(ReadView + DB_TRX_ID) | 物理判断(PGA 优先读) |
| 数据可见性 | 所有事务共享同一份 Buffer Pool | 事务私有 PGA + 共享 SGA |
七、面试话术
"表面相同,原理不同:
MySQL InnoDB看到自己未提交的修改,不是读内存,是ReadView 4 步判断的第 1 步:
trx_id == creator_trx_id→ 自己改的,返回当前版本。InnoDB 事务是无状态的,没有事务私有内存。Oracle看到自己未提交的修改,是真的读 PGA 内存(事务私有)。Oracle 事务是有状态的,PGA 存了事务内未提交的数据。
核心差异:
- MySQL = 逻辑判断(ReadView + DB_TRX_ID)
- Oracle = 物理读取(PGA 优先 + SCN)
项目选型:
- 新项目→MySQL(无状态事务,分布式友好)
- 老项目→Oracle(有状态事务,OLAP 友好)"
八、记忆口诀
"MySQL 读当前版本,Oracle 读 PGA 内存"
"MySQL 无状态,Oracle 有状态"
"MySQL 逻辑判断(ReadView),Oracle 物理读取(PGA)"
"表面相同,原理不同"
"新项目用 MySQL(无状态分布式友好),老项目用 Oracle(有状态 OLAP 友好)"
🎯 总结
问:MySQL 看到 1000 vs Oracle 看到 1000,原理相同吗?
答:完全不同的原理。
| 数据库 | 表面结果 | 真实原理 |
|---|---|---|
| MySQL InnoDB | ✅ 看到 1000 | 不是读内存——是 ReadView 4 步判断第 1 步:trx_id == creator_trx_id→ 自己改的,返回当前版本 |
| Oracle | ✅ 看到 1000 | 真的读 PGA 内存(事务私有) |
3 大本质差异
| 维度 | MySQL InnoDB | Oracle |
|---|---|---|
| 事务状态 | 无状态(无 PGA 私有内存) | 有状态(PGA 私有) |
| 判断机制 | 逻辑判断(ReadView + DB_TRX_ID) | 物理读取(PGA 优先) |
| 数据存储 | Buffer Pool 共享 | PGA 私有 + SGA 共享 |
面试话术
"表面相同,原理完全不同:
- MySQL InnoDB看到自己未提交,不是读内存,是ReadView 4 步判断第 1 步(
trx_id == creator_trx_id)- Oracle看到自己未提交,真的读 PGA 内存(事务私有)
核心差异:
- MySQL = 逻辑判断(ReadView)
- Oracle = 物理读取(PGA)"