引言
在数据库事务隔离级别的讨论中,读已提交(Read Committed,RC) 是最常用的一种。很多开发者可能听过它的名字,但对底层如何实现“读已提交”的可见性逻辑,尤其是InnoDB中神秘的 Read View(读视图) 到底怎么工作,可能还是一头雾水。
今天咱们就用“唠家常”的方式,结合InnoDB的核心机制,把RC隔离级别的可见性判断逻辑一次性讲透!
一、先搞清楚:什么是“读已提交”?
“读已提交”的字面意思是:一个事务只能读取到其他事务已经提交的数据。换句话说,如果另一个事务还没提交修改,当前事务是看不到这些“未提交变更”的。
比如:
- 事务A修改了某条数据但未提交;
- 事务B此时去查询这条数据,应该看不到事务A的修改;
- 等事务A提交后,事务B再次查询,就能看到最新结果了。
那InnoDB是如何实现这一点的?答案就藏在 Read View(读视图) 这个“事务状态快照”里。
二、Read View到底是啥?为啥RC要每次查询都生成?
1. Read View的本质:事务的“时光机”
Read View可以理解为事务的“可见性过滤器”。它记录了当前事务启动时(或本次查询时),数据库中所有活跃(未提交)事务的ID,以及两个关键边界值:
low_limit_id
:当前事务启动时,全局最大已提交事务ID + 1(相当于“已提交事务的终点”);high_limit_id
:当前全局最大的事务ID(相当于“未提交事务的起点”)。
简单说,Read View就像一张“清单”,标明了哪些事务的修改对当前事务是“可见”的,哪些是“不可见”的。
2. RC的“特殊操作”:每次查询都生成新Read View
和可重复读(RR)不同,RC隔离级别的每次查询都会生成新的Read View。这就像每次拍照都用最新的相机——确保看到的始终是最新的“提交状态”。
举个栗子:
假设事务B在运行过程中,先后执行了两次查询。第一次查询时,事务A未提交;第二次查询时,事务A已经提交了。由于RC每次查询都生成新Read View,第二次查询的Read View会排除事务A(因为它已提交),所以能看到事务A的修改。
三、核心逻辑:如何用Read View判断数据可见性?
InnoDB的每条记录在修改时,都会记录修改它的事务ID(DB_TRX_ID
),并通过回滚指针(DB_ROLL_PTR
)指向历史版本。当查询时,InnoDB会根据当前Read View的状态,从最新版本开始“回溯”,直到找到对当前事务可见的版本。
具体判断逻辑分三种情况:
情况1:记录的DB_TRX_ID
< low_limit_id
→ 可见!
low_limit_id
是当前事务启动时“已提交事务的最大ID + 1”。如果记录的最后一次修改是由一个早于这个值的事务完成的(即DB_TRX_ID < low_limit_id
),说明这个修改在当前事务启动前就已经提交了,对当前事务完全可见,直接返回这个版本。
情况2:low_limit_id
≤ DB_TRX_ID
≤ high_limit_id
→ 看事务是否活跃!
如果记录的DB_TRX_ID
落在“当前事务启动时已提交事务的终点”和“当前全局最大事务ID”之间,说明这个修改是在当前事务启动后发生的。这时候需要进一步检查:
- 如果
DB_TRX_ID
属于当前Read View中的活跃事务集合(即该事务还未提交):当前事务看不到这个修改,需要继续回溯到更早的版本; - 如果
DB_TRX_ID
不在活跃事务集合中(即该事务已提交):当前事务可以看到这个版本。
情况3:DB_TRX_ID
> high_limit_id
→ 不可能!
high_limit_id
是当前全局最大的事务ID,新事务的trx_id
只会递增。所以正常情况下,记录的DB_TRX_ID
不可能超过high_limit_id
。如果出现这种情况,大概率是系统异常了(比如事务ID回绕)。
四、举个实战例子:RC如何“看见”数据?
为了更直观理解,咱们用具体数字模拟一个场景:
准备工作:
- 全局事务ID按顺序递增(1→2→3→4…);
- 当前有两个事务:
- 事务A(
trx_id=2
,RC隔离级别,未提交); - 事务B(
trx_id=3
,执行查询)。
- 事务A(
场景1:事务A未提交时,事务B第一次查询
事务A修改了某条记录,将其DB_TRX_ID
设为2(未提交)。此时事务B执行查询,生成Read View:
low_limit_id
= 事务B启动时已提交的最大事务ID + 1(假设之前只有事务1提交,所以low_limit_id=2
);high_limit_id
= 当前全局最大事务ID(事务A未提交,事务B自己是3,所以high_limit_id=3
);- 活跃事务集合 = {2}(事务A未提交)。
现在检查记录的DB_TRX_ID=2
:
- 它落在
low_limit_id=2
和high_limit_id=3
之间; - 且
DB_TRX_ID=2
在活跃事务集合中(事务A未提交)。
结论:这个版本不可见!事务B会继续回溯到该记录的旧版本(比如DB_TRX_ID=1
,已被提交),最终看到旧数据。
场景2:事务A提交后,事务B第二次查询
事务A提交后,其trx_id=2
被标记为已提交。事务B再次执行查询,生成新的Read View:
low_limit_id
= 事务B启动时的已提交最大事务ID + 1(还是2,因为事务B启动时事务A未提交);high_limit_id
= 当前全局最大事务ID(事务A已提交,下一个事务ID是4,所以high_limit_id=4
);- 活跃事务集合 = {}(无未提交事务)。
检查记录的DB_TRX_ID=2
:
- 它等于
low_limit_id=2
(刚好是已提交事务的终点); - 不在任何活跃事务集合中。
结论:这个版本可见!事务B读取到事务A提交后的新数据。
五、RC的优缺点:适合什么场景?
优点:
- 无“不可重复读”问题:每次查询都生成新Read View,同一事务内多次查询结果可能不同(因为能看到最新提交的数据),但避免了“读已提交”隔离级别下“前后结果不一致”的问题;
- 性能较好:相比RR(可重复读)的全局一致性读,RC不需要长期维护Read View,减少了锁竞争和回滚段的开销。
缺点:
- 可能读到“闪回”数据:如果两次查询之间有其他事务提交,可能看到“突然变化”的数据(但对“读已提交”来说是符合预期的);
- 不解决幻读:如果存在范围查询,可能前后两次查询看到不同的行数(需配合间隙锁解决)。
总结:RC的可见性逻辑,其实很简单!
InnoDB的RC隔离级别通过每次查询生成新的Read View,结合trx_id
与low_limit_id
、high_limit_id
、活跃事务集合的比较,动态判断数据的可见性。核心就一句话:
能看到的,要么是当前事务启动前已提交的修改,要么是当前事务启动后其他已提交事务的修改;未提交的一律看不见!
下次再遇到“为什么RC隔离级别下能看到最新提交的数据”这类问题,你就可以拍着胸脯说:“因为每次查询都生成了新的Read View呀!” 😎
注:文中示例为简化逻辑,实际InnoDB的实现可能涉及更多细节,如回滚段、undo日志等,但核心可见性判断逻辑一致。