对于读已提交隔离级别的实现方式,从逻辑上需要明确两个部分,一是加锁部分二是解锁部分。加锁,对应的是获取数据,确保在指定的隔离级别下读取到应该读到的数据。解锁则意味着要在适当的时机释放锁且不影响隔离级别的语义还能提高并发度。
加锁部分,实现分为两个方面:一是加锁的时候,读已提交隔离级别不加间隙锁,这样就能允许并发的其他事务执行插入操作因而产生幻象现象,因为读已提交隔离级别是允许幻象异常存在的。如下代码,加锁的时候,根据隔离级别是否加间隙锁。
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 da
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) //在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; //意味着读已提交隔离级别加锁过后,则释放锁,而不是等待事务结束时释放锁。所以更新等操作可以被其他事务有机会看到[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 on
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);