InnoDB---读已提交隔离级别的实现

本文探讨了数据库中读已提交隔离级别的实现细节,包括如何加锁与解锁以支持并发控制,以及如何处理幻象读等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

     对于读已提交隔离级别的实现方式,从逻辑上需要明确两个部分,一是加锁部分二是解锁部分。加锁,对应的是获取数据,确保在指定的隔离级别下读取到应该读到的数据。解锁则意味着要在适当的时机释放锁且不影响隔离级别的语义还能提高并发度。

    加锁部分,实现分为两个方面:一是加锁的时候,读已提交隔离级别不加间隙锁,这样就能允许并发的其他事务执行插入操作因而产生幻象现象,因为读已提交隔离级别是允许幻象异常存在的。如下代码,加锁的时候,根据隔离级别是否加间隙锁。

row_sel_get_clust_rec[1](...)

{...

    if (!node->read_view) {

...

        if (srv_locks_unsafe_for_binlog

            || trx->isolation_level <= TRX_ISO_READ_COMMITTED) {

            lock_type = LOCK_REC_NOT_GAP;  //小于等于读已提交,则不加间隙锁,允许其他事务插入,因此可发生幻象

        } else {

            lock_type = LOCK_ORDINARY;      //大于读已提交,则加间隙锁,防止其他事务插入某个范围内的数据,避免幻象

        }

...}  

    其次,要确定可以读取到什么样的元组,即判断是不是没有被提交的元组也可以读到。既然是读已提交级别,则必然是只能读取到已经被提交的元组,这样才能体现“已提交”的含义。这时,就涉及到数据的可见性判断的问题(本节不讨论可见性问题,详情参见12.2节)。

 

    解锁部分,要及时释放锁,这样便于其他事务能够读取到不应当被本事务锁定的记录(InnoDB中“记录”是索引项,通过记录才能真正找到元组)。以索引上的范围扫描为例,查看锁的释放条件。

ha_innopart::read_range_next_in_part(...)  //Return next record in index range scan from a partition

{...

    error = ha_innobase::index_next(read_record); //获得记录,则会加锁,此时error的值被赋予0

    if (error == 0 && !in_range_check_pushed_down) {  //记录被加过了锁

        /* compare_key uses table->record[0], so we need to copy the data if not already there. */

        if (record != NULL) {

            copy_cached_row(table->record[0][2], read_record); //复制获取到的元组到表级的数据缓冲区

        }

        if (compare_key(end_range) > 0) { //超出要读取的范围,则释放锁

            /* must use ha_innobase:: due to set/update_partition

            could overwrite states if ha_innopart::unlock_row() was used. */

            ha_innobase::unlock_row();//释放锁

            error = HA_ERR_END_OF_FILE;

        }

    }

...

}

    根据隔离级别确定是否要释放锁。

/** Removes a new lock set on a row, if it was not read optimistically. This can be called after a row has been read

in the processing of an UPDATE or a DELETE query, if the option innodb_locks_unsafe_for_binlog is set. */

void    //mysql_update()/mysql_delete()调用,用于为记录解锁。另外少数情况是:被join_read_key()等调用

ha_innobase::unlock_row(void)   //UPDATEDELETE执行时,一个元组被读取操作后,所施加的锁“可能”被本方法释放

{...                            //所施加的锁是否被释放,取决于下面对隔离级别的判断

    switch (m_prebuilt->row_read_type) {

    case ROW_READ_WITH_LOCKS:

        if (!srv_locks_unsafe_for_binlog

            && m_prebuilt->trx->isolation_level

            > TRX_ISO_READ_COMMITTED) {  //隔离级别是可重复读或序列化,则满足大于读已提交,所以执行break不解锁

            break;

        }

        /* fall through */

    case ROW_READ_TRY_SEMI_CONSISTENT: 

        row_unlock_for_mysql(m_prebuilt, FALSE);  //如果是读已提交隔离级别,则能执行到解锁操作

        break;  //意味着读已提交隔离级别加锁过后,则释放锁,而不是等待事务结束时释放锁。所以更新等操作可以被其他事务有机会看到[3]

    case ROW_READ_DID_SEMI_CONSISTENT:

        m_prebuilt->row_read_type = ROW_READ_TRY_SEMI_CONSISTENT;

        break;

    }

...

}

    紧接着,判断并发事务间的提交关系(涉及了可见性判断规则:通过lock_clust_rec_cons_read_sees()调用changes_visible()利用元组上的事务ID与快照的左右边界比较),然后再确定是否是解锁。如下是解锁的过程。

/** This can only be used when srv_locks_unsafe_for_binlog is TRUE or this

session is using a READ COMMITTED or READ UNCOMMITTED isolation level.

Before calling this function row_search_for_mysql() must have initialized prebuilt->new_rec_locks to store the information which new

record locks really were set. This function removes a newly set clustered index record lock under prebuilt->pcur or

prebuilt->clust_pcur.  Thus, this implements a 'mini-rollback' that releases the latest clustered index record lock we set.

@param[in,out]    prebuilt               prebuilt struct in MySQL handle

@param[in]        has_latches_on_recs    TRUE if called so that we have the latches on the records under pcur

                                               and clust_pcur, and we do not need to reposition the cursors. */

void

row_unlock_for_mysql(row_prebuilt_t* prebuilt, ibool has_latches_on_recs)

{...

    if (prebuilt->new_rec_locks >= 1) {

...

        /* If the record has been modified by this transaction, do not unlock it. */

        if (index->trx_id_offset) {  //如果是被本事务修改,则不释放锁(修改元组则会写事务ID到元组中)

            rec_trx_id = trx_read_trx_id(rec + index->trx_id_offset);  //获得元组上的事务id

        } else {...

            offsets = rec_get_offsets(rec, index, offsets, ULINT_UNDEFINED, &heap);

            rec_trx_id = row_get_rec_trx_id(rec, index, offsets);      //获得元组上的事务id

            if (UNIV_LIKELY_NULL(heap)) {

                mem_heap_free(heap);

            }

        }

 

        if (rec_trx_id != trx->id) {  //元组上的事务id不是本事务的id,表明元组是被其他事务修改,释放锁

            /* We did not update the record: unlock it */

            rec = btr_pcur_get_rec(pcur);

            lock_rec_unlock(trx, btr_pcur_get_block(pcur), rec, static_cast<enum lock_mode>(prebuilt->select_lock_type));

 

            if (prebuilt->new_rec_locks >= 2) {  //new_rec_lock通常是0,如果隔离级别是READ COMMITTEDREAD UNCOMMITTED

                rec = btr_pcur_get_rec(clust_pcur);  //则在row_search_mvcc()中获得记录锁后设置为2,所以需要对应解锁

                lock_rec_unlock(trx, btr_pcur_get_block(clust_pcur), rec, static_cast<enum lock_mode>(prebuilt->select_lock_type));

            }

        }

no_unlock:

        mtr_commit(&mtr);

    }

...

}

  

    在一个事务块内,如果存在多条SELECT语句,则在读已提交隔离级别下,每条SELECT语句分别使用自己的快照(Read view,即为每条SELECT生成一个Read view,每条SELECT结束后,通过调用MVCC::view_close()方法,Read view会被关闭)。

    对于一个UPDATEDELETE操作,当有页面(索引页面)因增加或删除了元组而分离或合并时,需要让新页继承旧页的锁信息,这时继承操作是通过lock_rec_add_to_queue()函数加锁完成的,但是,加锁时会有间隙锁存在,代码如下:

lock_rec_add_to_queue(  //lock_rec_inherit_to_gap()调用,在原先的锁的基础上加持间隙锁GAP

        LOCK_REC | LOCK_GAP | lock_get_mode(lock),  //lock_get_mode(lock)是原先锁的粒度和类型,LOCK_GAP是必须加持的类型

        heir_block, heir_heap_no, lock->index,

        lock->trx, FALSE);



[1] 位于row0sel.cc文件中。

[2] 执行器使用的表的数据就是从table->record[0]获得的。

[3] 注意,只是存在能被其他事务读到修改后的数据的可能,单是还没有判断事务是否已经提交。

<think>嗯,我现在要理解MySQL的提交隔离级别是如何解决脏问题的。首先,我需要回顾一下数据库隔离级别的基本概念。根据SQL标准,有四个隔离级别提交提交、可重复和串行化。脏是指在事务中取到了另一个未提交事务的数据,如果那个事务最终回滚了,那么取到的数据就是无效的,也就是脏数据。 提交(Read Committed)这个隔离级别应该能防止脏,对吧?那它是怎么做到的呢?可能涉及到锁机制或者多版本控制?比如,MySQL的InnoDB引擎默认使用的是可重复(Repeatable Read),但用户可能设置成提交。这时候,InnoDB是怎么处理提交的? 我记得InnoDB使用了多版本并发控制(MVCC),通过版本号或者时间戳来维护数据的不同版本。在提交隔离级别下,事务每次取数据时,只能看到已经提交的数据版本。也就是说,如果另一个事务正在修改数据但还没提交,当前事务不会取到那些未提交的修改,从而避免了脏。 那具体来说,当事务A修改了一条数据但未提交,事务B在取这条数据时,InnoDB会检查该数据的版本,只返回已经提交的最新版本。事务A的修改因为没有提交,所以不会被事务B看到。这样,事务B就不会到脏数据。 不过,这里可能需要更深入的理解。比如,MVCC是如何生成快照的?在提交下,每次查询都会生成一个新的快照,这样就能获取最新的已提交数据。而在可重复下,快照是在事务开始时生成的,之后都沿用这个快照,所以能看到一致的数据视图。 所以,提交通过MVCC,在每个查询时都取已提交的最新版本,避免取未提交的数据。另外,锁机制可能也起作用,比如写操作会加行锁,防止其他事务修改相同的数据,但操作是否可以取到未提交的数据呢?在提交下,操作不会加共享锁,或者会忽略未提交的排他锁?或者通过版本控制直接忽略未提交的数据? 可能还有两阶段锁(2PL)的机制,但InnoDB主要使用MVCC来处理,而写操作使用行级锁。因此,在提交下,事务在取时不会阻塞写操作,但写操作必须等待其他事务释放锁,而操作则通过MVCC取已提交的版本。 总结一下,提交隔离级别通过MVCC机制,在每次查询时获取已提交的最新数据版本,避免取到未提交的事务修改,从而解决脏问题。同时,结合行级锁,确保写操作的排他性,防止并发写入导致的数据不一致。</think>MySQL的提交(Read Committed)隔离级别通过以下机制解决脏问题: --- ### **1. 脏的定义** 脏指一个事务取到另一个**未提交事务**修改的数据。如果后者最终回滚,前者取的数据就是无效的。 --- ### **2. 解决脏的核心机制** 提交隔离级别通过 **MVCC(多版本并发控制)** 和 **行级锁** 协同工作来避免脏: #### **(1)MVCC 的作用** - **数据版本快照**:InnoDB 为每行数据维护多个版本。每次修改会生成新版本,旧版本仍保留。 - **可见性规则**:事务在取时,只能看到以下两种数据: - **已提交事务的修改**。 - **本事务自身的修改**(即使未提交)。 - **实现方式**: - 每个事务开始时,会记录当前活跃事务的 ID 列表。 - 取数据时,仅选择版本号(trx_id)小于等于当前事务 ID,且对应事务已提交的版本。 #### **(2)行级锁的辅助** - **写操作加锁**:事务修改某行时,会对该行加排他锁(X Lock)。其他事务若想修改同一行,必须等待锁释放。 - **操作无需加锁**:提交下,操作直接通过 MVCC 取快照,不会因锁冲突阻塞。 --- ### **3. 具体流程示例** 假设事务 A 和事务 B 并发执行: 1. **事务 A**(写操作): ```sql UPDATE users SET balance = 200 WHERE id = 1; -- 修改未提交 ``` - 对 `id=1` 的行加排他锁。 - 生成新数据版本(假设 `trx_id=100`)。 2. **事务 B**(操作): ```sql SELECT balance FROM users WHERE id = 1; -- 取时使用 MVCC ``` - 检查所有版本,发现最新版本 `trx_id=100` 对应的事务未提交- 回退到上一个已提交的版本(如 `trx_id=50` 的旧数据)。 3. **事务 A 提交**: ```sql COMMIT; -- 数据版本 `trx_id=100` 标记为已提交 ``` 4. **事务 B 再次取**: ```sql SELECT balance FROM users WHERE id = 1; -- 此时取到 `trx_id=100` 的版本 ``` - 看到已提交的新值 `200`。 --- ### **4. 与提交的对比** | **隔离级别** | 脏 | 实现方式 | |--------------|------|----------------------------| | 提交 | 允许 | 直接取数据页最新值(含未提交) | | 提交 | 禁止 | MVCC 过滤未提交版本 | --- ### **5. 注意事项** - **不可重复**:提交无法避免不可重复(同一事务内多次取结果不一致)。 - **性能影响**:频繁生成快照可能增加 CPU 和内存开销,但写操作无锁冲突,整体吞吐较高。 --- 通过 MVCC 的版本可见性规则,提交隔离级别有效屏蔽了未提交事务的修改,从而彻底解决了脏问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值