您的位置:首页 > 其它

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

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

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

row_sel_get_clust_rec[b][1][/b](...)

{...

    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][b][2][/b],
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)   //在UPDATE或DELETE执行时,一个元组被读取操作后,所施加的锁“可能”被本方法释放

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

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

    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 COMMITTED或READ 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会被关闭)。

    对于一个UPDATE或DELETE操作,当有页面(索引页面)因增加或删除了元组而分离或合并时,需要让新页继承旧页的锁信息,这时继承操作是通过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] 注意,只是存在能被其他事务读到修改后的数据的可能,单是还没有判断事务是否已经提交。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: