MySQL事务ID生成时机
对于需要的事务,其事务ID(trx_id)是在它执行第一条增、删、改(INSERT, UPDATE, DELETE)语句时生成的,而不是在BEGIN
或START TRANSACTION
开启事务时。
下面我将以最常用的MySQL InnoDB引擎为例,进行详细解释。
详细解释
1. 为什么不是开启事务时就生成?
数据库为了追求极高的性能和并发度,很多设计都是“按需”和“惰性”的。生成一个全局唯一的事务ID是一个需要“上锁”或使用“原子变量”的操作,以保证其唯一性和递增性,这是一个有成本的操作。
如果事务一开启(比如只是执行了一个BEGIN
后跟着几条SELECT
查询)就分配ID,但这个事务可能最终只是一个只读查询,永远不会修改任何数据。那么这次ID分配就浪费了,在高并发场景下会无谓地增加竞争。
因此,InnoDB的设计是:先按只读事务来对待,直到它真正需要写入时,才为其“升级”为一个读写事务并分配事务ID。
2. 生成的准确时机
当事务执行第一条修改数据的语句(DML语句:INSERT
, UPDATE
, DELETE
)时,会触发以下步骤:
- 申请事务ID:InnoDB会从全局的事务计数器
trx_sys->max_trx_id
中获取一个值作为本事务的ID,并将该计数器加1。 - 构建回滚日志(Undo Log):为了满足事务的原子性(Atomicity)和隔离性(Isolation - MVCC),修改数据前,需要先将旧版本的数据(修改前的样子)复制到Undo Log中。
- 在Undo Log中记录事务ID:这个新生成的Undo Log记录必须被标记是由哪个事务创建的,因此必须要把刚刚申请到的事务ID写入Undo Log的头部。
- 修改数据:将数据页中的数据修改为新值,并在数据库行的隐藏列
DB_TRX_ID
中记录这个事务ID,标识这行数据是由哪个事务最新更新的。
从这个流程可以看出,生成事务ID是构建Undo Log的前提条件。而只有修改语句才需要构建Undo Log。
3. 只读事务的特殊处理
对于那些只包含SELECT
查询语句的事务,它们永远不需要Undo Log,也永远不会修改任何数据。因此,InnoDB永远不会为它们分配事务ID。它们被称为只读事务。
在RR(可重复读)和RC(读已提交)隔离级别下,SELECT
查询会依赖一个“一致性读视图”(ReadView)来判断哪些数据版本对它可见。这个ReadView中记录的是当前活跃的所有读写事务的ID。因为只读事务没有ID,所以它不会出现在其他事务的ReadView中,不会影响其他事务的可见性判断,这大大减少了开销。
4. 特殊情况:只读事务的“强制”分配
即使是一个你认为是只读的事务,在某些情况下也可能被“强制”分配事务ID:
- 指定了
FOR UPDATE
或LOCK IN SHARE MODE
的SELECT语句:这类语句虽然不修改数据,但意图加锁(排他锁或共享锁)。加锁行为属于“写”操作的一种,因为它会改变系统的锁状态,可能阻塞其他事务。因此,InnoDB会为这种事务分配事务ID,将其当作读写事务处理。 - 打开了
autocommit=0
且执行了任何语句:即使只是一个SELECT
,在有些版本和特定配置下,也可能先分配一个ID。但主流版本中,优化策略仍然是惰性的。
总结与类比
事务类型 | 事务ID生成时机 | 说明 |
---|---|---|
纯只读事务 (仅SELECT ) |
永不生成 | 性能最优,开销最小。 |
读写事务 (有INSERT/UPDATE/DELETE ) |
第一条DML语句执行时 | 在需要创建Undo Log前一刻生成。 |
加锁读事务 (SELECT ... FOR UPDATE ) |
加锁读语句执行时 | 加锁被视为写操作,需要事务ID。 |
一个简单的类比:
这就像你去游乐场。
BEGIN
只是你决定进去逛逛。- 执行一个
SELECT
就像你只是看看某个项目(免费)。 - 只有当你决定要玩一个收费项目(执行
INSERT/UPDATE
)时,你才需要去售票处花钱买票(生成事务ID)。这张票(事务ID)就是你参与和影响这个项目的凭证。
所以,记住这个核心要点:事务ID是事务需要“写入”数据的“入场券”,只有在它第一次需要“写入”时,才会去申请这张“入场券”。