innodb的mini-transaction

1,349 阅读3分钟

1.概述

由前两篇文章可知重做日志的实现是往磁盘页顺序写物理逻辑日志,如果数据库异常宕机,启动后扫描重做日志并进行恢复可保证数据不丢失,但是我们忽略了一点就是数据一致性问题,如何保证单页数据的一致性其实就是我们本文关注的内容。

2.详细介绍

mini-transaction和我们理解的数据库事务不是一个东西。从一致性来讲,数据库事务是保证多条语句操作的一致性,往往涉及到多个页的修改。而mini-transaction是单页数据一致性,是避免内存页的并发更新影响。当然数据库事务一致性实现也是建立在mini-transaction的基础上的。

所有对页的操作都要在mini_transaction中执行。

在一个mini-transaction操作中,需要对对应的page加锁。锁中代码逻辑主要就是操作页,然后生成redo和undolog,完成之后释放锁。

mini_transaction(){
    Lock page
    Transform page
    Generate undo and redo log
    Unlock page
}

为什么这样能保证一致性?

1.对页加锁,能保证这个内存页并发修改的安全性。

2.在锁中append log也能保证log的顺序性(从前面的文章可知,innodb是通过log保证持久,所有内存页写操作都要写log)

3.在innodb中,每当一个事务提交的时候,所有的mini_transaction产生的log必须持久化(当然可以通过参数配置,但是不丢失数据的场景是如此)。

4.Innodb每个数据页都有一个lsn,对于页修改需要更新lsn,在持久化页时,保证对应lsn的log都被持久化即可。

3.代码实现

mini-transaction数据结构

struct mtr_struct{
    ulint        state;    /* MTR_ACTIVE, MTR_COMMITTING, MTR_COMMITTED */
    dyn_array_t    memo;    /* memo stack for locks etc. */
    dyn_array_t    log;    /* mini-transaction log */
    ibool        modifications;
                /* TRUE if the mtr made modifications to
                buffer pool pages */
    ulint        n_log_recs;
                /* count of how many page initial log records
                have been written to the mtr log */
    ulint        log_mode; /* specifies which operations should be
                logged; default value MTR_LOG_ALL */
    dulint        start_lsn;/* start lsn of the possible log entry for
                this mtr */
    dulint        end_lsn;/* end lsn of the possible log entry for
                this mtr */
    ulint        magic_n;
};

主要存储了状态,对页写入的相关信息以及日志信息。

memo存储了持有latch的信息,是一个stack。栈存储的为mtr_memo_slot_struct。其实就是实现锁的获取和释放。

typedef    struct mtr_memo_slot_struct    mtr_memo_slot_t;
struct mtr_memo_slot_struct{
    ulint    type;    /* type of the stored object (MTR_MEMO_S_LOCK, ...) */
    void*    object;    主要是latch对象 参考rw_lock_tbuf_block_t
};

n_log_recs表示修改页的数量,因为一个操作可能会影响多个页,如果涉及多个页的修改,会按顺序对页进行加锁。

上文所说所有操作都要在mini-transaction中执行。其实就是下面的代码逻辑。

mtr_t mtr;
mtr_start(&mtr);
(代码逻辑)
mtr_commit(&mtr);

mtr_start

UNIV_INLINE mtr_t* mtr_start(mtr_t*    mtr)    
{
    dyn_array_create(&(mtr->memo));
    dyn_array_create(&(mtr->log));

    mtr->log_mode = MTR_LOG_ALL;
    mtr->modifications = FALSE;
    mtr->n_log_recs = 0;


#ifdef UNIV_DEBUG
    mtr->state = MTR_ACTIVE;
    mtr->magic_n = MTR_MAGIC_N;
#endif
    return(mtr);
}  

这个方法只是对mtr_struct数据结构的初始化。

代码逻辑拿到初始化好的mtr可进行相关操作。对页操作前先获取锁,然后push到mtr。

UNIV_INLINE void mtr_memo_push(
    mtr_t*    mtr,    /* in: mtr */
    void*    object,    /* in: object */
    ulint    type)    /* in: object type: MTR_MEMO_S_LOCK, ... */
{
    dyn_array_t*        memo;
    mtr_memo_slot_t*    slot;

    ut_ad(object);
    ut_ad(type >= MTR_MEMO_PAGE_S_FIX);    
    ut_ad(type <= MTR_MEMO_X_LOCK);
    ut_ad(mtr);
    ut_ad(mtr->magic_n == MTR_MAGIC_N);

    memo = &(mtr->memo);    

    slot = dyn_array_push(memo, sizeof(mtr_memo_slot_t));

    slot->object = object;
    slot->type = type;
}

mtr_commit

void mtr_commit(mtr_t*    mtr)
{
    ut_ad(mtr);
    ut_ad(mtr->magic_n == MTR_MAGIC_N);
    ut_ad(mtr->state == MTR_ACTIVE);
    if (mtr->modifications) {
        mtr_log_reserve_and_write(mtr);
    }

    mtr_memo_pop_all(mtr);
    if (mtr->modifications) {
        log_release();
    }
    dyn_array_free(&(mtr->memo));
    dyn_array_free(&(mtr->log));
}   

1.如果modifications为true则将transaction产生的日志写入到redolog buf中。在redo log恢复过程中也要启动事务,但是不需要再写redo log。

写入的时候需要持有log_sys->mutex

2.mtr_memo_pop_all方法调用mtr_memo_slot_release释放所有的latch。

3.释放log_sys->mutex

4.总结

mini-transaction其实是保证单个内存也写操作的一致性。在并发场景下,多线程写内存页,一方面能保证线程安全,另一方面在写安全的基础上保证redolog的顺序性。即使redo log恢复过程是并发操作的,但也能保证一致。