大号在练葵花宝典 发表于 2024-12-2 00:58:49

海山数据库(He3DB)源码详解:海山MySQL redo日志-日志文件组

# 1、日志文件组
MySQL的数据目次下默认有两个名为ib_logfile0和ib_logfile1的文件,log buffer中的日志默认环境下就是刷新到这两个磁盘文件中。
假如对默认的redo日志文件不满足,可以通过下边几个启动参数来调治:


[*]innodb_log_group_home_dir:该参数指定了redo日志文件所在的目次,默认值就是当前的数据目次。
[*]innodb_log_file_size:该参数指定了每个redo日志文件的巨细,默认值为48MB
[*]innodb_log_files_in_group:该参数指定redo日志文件的个数,默认值为2,最大值为100。
从上边的描述中可以看到,磁盘上的redo日志文件不但一个,而是以一个日志文件组的形式出现的。这些文件以ib_logfile[数字](数字可以是0、1、2…)的形式进行命名。在将redo日志写入日志文件组时,是从ib_logfile0开始写,假如ib_logfile0写满了,就接着ib_logfile1写,同理,ib_logfile1写满了就去写ib_logfile2,依此类推。假如写到最后一个文件该咋办?那就重新转到ib_logfile0继承写,所以整个过程如下图所示:
https://i-blog.csdnimg.cn/direct/0d162552335b4c0795d1aa4041fdff3c.png#pic_center
统共的redo日志文件巨细实在就是:innodb_log_file_size × innodb_log_files_in_group
2、redo日志文件格式

前文说过log buffer本质上是一片连续的内存空间,被分别成了多少个512字节巨细的block。将log buffer中的redo日志刷新到磁盘的本质就是把block的镜像写入日志文件中,所以redo日志文件实在也是由多少个512字节巨细的block组成。redo日志文件组中的每个文件巨细都一样,格式也一样,都是由两部分组成:


[*]前2048个字节,也就是前4个block是用来存储一些管理信息的
[*]从第2048字节今后是用来存储log buffer中的block镜像的
所以上文所说的循环使用redo日志文件,实在是从每个日志文件的第2048个字节开始算,如图所示:
https://i-blog.csdnimg.cn/direct/68faf925e9864142bc7c40c862cbc06b.png#pic_center
这里需要介绍一下每个redo日志文件前2048个字节,也就是前4个特别block的格式是怎样的,如图所示。
https://i-blog.csdnimg.cn/direct/9990db22c2bd4e289de56a7668456d6f.png#pic_center
其中,第一个block为log file header,包罗以下属性:


[*]LOG_HEADER_FORMAT : 4B,redo日志的版本,该值永久为1
[*]LOG_HEADER_PAD1 : 4B,字节填充
[*]LOG_HEADER_START_LSN : 8B,标记本redo日志文件开始的LSN值,也就是文件偏移量为2048字节初对应的LSN值
[*]LOG_HEADER_CREATOR : 32B,一个字符串,标记本redo日志文件的创建者是谁。正常运行时该值为MySQL的版本号,比如:"MySQL使用mysqlbackup下令创建的redo日志文件的该值为"ibbackup"和创建时间。
[*]LOG_BLOCK_CHECKSUM : 4B,本block的校验值
https://i-blog.csdnimg.cn/direct/eb0726e4a37445c48715af0a80b539b6.png#pic_center
第二个block为checkpoint1,该块中记录了关于checkpoint一些属性:


[*]LOG_CHECKPOINT_NO : 8B,服务器做的checkpoint编号,每做一次checkpoint,该值就加1。
[*]LOG_CHECKPOINT_LSN : 8B,服务器做checkpoint竣事时对应的LSN值,系统奔溃恢复时将从该值开始。
[*]LOG_CHECKPOINT_OFFSET : 8B,上个属性中的LSN值在redo日志文件组中的偏移量
[*]LOG_CHECKPOINT_LOG_BUF_SIZE : 8B,服务器在做checkpoint操纵时对应的logbuffer的巨细
[*]LOG_BLOCK_CHECKSUM : 4B,本block的校验值
https://i-blog.csdnimg.cn/direct/9f4f8b16a28e4d918fbbf06d7351a99f.png#pic_center
第四个blockcheckpoint2与checkpoint1中的结构属性相同,因此不做赘述。
3、源码解析

3.1 日志文件组结构体

struct log_group_t{
        /** log group identifier (always 0) */
        ulint                                id;
        /** number of files in the group */
        ulint                                n_files;
        /** format of the redo log: e.g., LOG_HEADER_FORMAT_CURRENT */
        ulint                                format;
        /** individual log file size in bytes, including the header */
        lsn_t                                file_size
        /** file space which implements the log group */;
        ulint                                space_id;
        /** corruption status */
        log_group_state_t                state;
        /** lsn used to fix coordinates within the log group */
        lsn_t                                lsn;
        /** the byte offset of the above lsn */
        lsn_t                                lsn_offset;
        /** unaligned buffers */
        byte**                                file_header_bufs_ptr;
        /** buffers for each file header in the group */
        byte**                                file_header_bufs;

        /** used only in recovery: recovery scan succeeded up to this
        lsn in this log group */
        lsn_t                                scanned_lsn;
        /** unaligned checkpoint header */
        byte*                                checkpoint_buf_ptr;
        /** buffer for writing a checkpoint header */
        byte*                                checkpoint_buf;
        /** list of log groups */
        UT_LIST_NODE_T(log_group_t)        log_groups;
};
日志文件组结构体中包罗了文件组id、文件数目、所包罗redo日志文件的类型等基本信息。别的,结构体中还有几个较为重要的字段:


[*]lsn : 日志组中用于确定位置的lsn
[*]lsn_offset : lsn的字节偏移量
[*]checkpoint_buf : 用于写入检查颔首部的缓冲区
[*]UT_LIST_NODE_T(log_group_t) log_groups : 将日志组链接起来的链表
3.2 刷盘时机

抱负状态下,事务一提交就会进行刷盘操纵,但实际上,刷盘的时机是根据策略来进行的。
InnoDB存储引擎为redo log的刷盘策略提供了innodb_flush_log_at_trx_commit参数,它支持三种策略:


[*]0 :设置为 0 的时候,表现每次事务提交时不进行刷盘操纵
[*]1 :设置为 1 的时候,表现每次事务提交时都将进行刷盘操纵(默认值)
[*]2 :设置为 2 的时候,表现每次事务提交时都只把redo log buffer内容写入page cache
[*]innodb_flush_log_at_trx_commit 参数默认为 1 ,也就是说当事务提交时会调用 fsync 对 redo log 进行刷盘
[*]别的,InnoDB 存储引擎有一个背景线程,每隔1 秒,就会把 redo log buffer 中的内容写到文件系统缓存(page cache),然后调用 fsync 刷盘。
3.2.1 提交刷盘

提交刷盘主要涉及到两个关键函数:log_buffer_flush_to_disk()和log_write_up_to()
void
log_buffer_flush_to_disk(
        bool sync)
{
        ut_ad(!srv_read_only_mode);
        log_write_up_to(log_get_lsn(), sync);
}


[*]这段代码的功能是调用log_write_up_to()函数将日志缓冲区中的内容写入到磁盘上的日志文件中,直到最新的日志记录,并根据参数决定是否同时将日志数据刷新到磁盘。
[*]是否进行刷盘操纵主要通过sync参数控制:sync为true,则进行刷盘操纵;否则只写入操纵系统的缓存。默认环境下,bool sync = true。
void
log_write_up_to(
        lsn_t        lsn,
        bool        flush_to_disk)
{
        byte*         write_buf;
        lsn_t         write_lsn;

        ut_ad(!srv_read_only_mode);

        if (recv_no_ibuf_operations) {

                return;
        }

loop:
        ut_ad(++loop_count < 128);

        log_write_mutex_enter();
        ut_ad(!recv_no_log_write);

        lsn_t        limit_lsn = flush_to_disk
                ? log_sys->flushed_to_disk_lsn
                : log_sys->write_lsn;

        if (limit_lsn >= lsn) {
                log_write_mutex_exit();
                return;
        }

#ifdef _WIN32
# ifndef UNIV_HOTBACKUP
        /* write requests during fil_flush() might not be good for Windows */
        if (log_sys->n_pending_flushes > 0
          || !os_event_is_set(log_sys->flush_event)) {
                log_write_mutex_exit();
                os_event_wait(log_sys->flush_event);
                goto loop;
        }
# else
        if (log_sys->n_pending_flushes > 0) {
                goto loop;
        }
# endif/* !UNIV_HOTBACKUP */
#endif /* _WIN32 */

        if (flush_to_disk
          && (log_sys->n_pending_flushes > 0
                || !os_event_is_set(log_sys->flush_event))) {

                /* Figure out if the current flush will do the job
                for us. */
                bool work_done = log_sys->current_flush_lsn >= lsn;

                log_write_mutex_exit();

                os_event_wait(log_sys->flush_event);

                if (work_done) {
                        return;
                } else {
                        goto loop;
                }
        }

        log_mutex_enter();
        if (!flush_to_disk
          && log_sys->buf_free == log_sys->buf_next_to_write) {
                /* Nothing to write and no flush to disk requested */
                log_mutex_exit_all();
                return;
        }

        log_group_t*        group;
        ulint                start_offset;
        ulint                end_offset;
        ulint                area_start;
        ulint                area_end;
        ulong                write_ahead_size = srv_log_write_ahead_size;
        ulint                pad_size;

        DBUG_PRINT("ib_log", ("write " LSN_PF " to " LSN_PF,
                              log_sys->write_lsn,
                              log_sys->lsn));

        if (flush_to_disk) {
                log_sys->n_pending_flushes++;
                log_sys->current_flush_lsn = log_sys->lsn;
                MONITOR_INC(MONITOR_PENDING_LOG_FLUSH);
                os_event_reset(log_sys->flush_event);

                if (log_sys->buf_free == log_sys->buf_next_to_write) {
                        /* Nothing to write, flush only */
                        log_mutex_exit_all();
                        log_write_flush_to_disk_low();
                        return;
                }
        }

        start_offset = log_sys->buf_next_to_write;
        end_offset = log_sys->buf_free;

        area_start = ut_calc_align_down(start_offset, OS_FILE_LOG_BLOCK_SIZE);
        area_end = ut_calc_align(end_offset, OS_FILE_LOG_BLOCK_SIZE);

        ut_ad(area_end - area_start > 0);

        log_block_set_flush_bit(log_sys->buf + area_start, TRUE);
        log_block_set_checkpoint_no(
                log_sys->buf + area_end - OS_FILE_LOG_BLOCK_SIZE,
                log_sys->next_checkpoint_no);

        write_lsn = log_sys->lsn;
        write_buf = log_sys->buf;

        log_buffer_switch();

        group = UT_LIST_GET_FIRST(log_sys->log_groups);

        log_group_set_fields(group, log_sys->write_lsn);

        log_mutex_exit();

        /* Calculate pad_size if needed. */
        pad_size = 0;
        if (write_ahead_size > OS_FILE_LOG_BLOCK_SIZE) {
                lsn_t        end_offset;
                ulint        end_offset_in_unit;

                end_offset = log_group_calc_lsn_offset(
                        ut_uint64_align_up(write_lsn,
                                           OS_FILE_LOG_BLOCK_SIZE),
                        group);
                end_offset_in_unit = (ulint) (end_offset % write_ahead_size);

                if (end_offset_in_unit > 0
                  && (area_end - area_start) > end_offset_in_unit) {
                        /* The first block in the unit was initialized
                        after the last writing.
                        Needs to be written padded data once. */
                        pad_size = write_ahead_size - end_offset_in_unit;

                        if (area_end + pad_size > log_sys->buf_size) {
                                pad_size = log_sys->buf_size - area_end;
                        }

                        ::memset(write_buf + area_end, 0, pad_size);
                }
        }

        /* Do the write to the log files */
        log_group_write_buf(
                group, write_buf + area_start,
                area_end - area_start + pad_size,
                ut_uint64_align_down(log_sys->write_lsn,
                                     OS_FILE_LOG_BLOCK_SIZE),
                start_offset - area_start);

        srv_stats.log_padded.add(pad_size);

        log_sys->write_lsn = write_lsn;

#ifndef _WIN32
        if (srv_unix_file_flush_method == SRV_UNIX_O_DSYNC) {
                /* O_SYNC means the OS did not buffer the log file at all:
                so we have also flushed to disk what we have written */
                log_sys->flushed_to_disk_lsn = log_sys->write_lsn;
        }
#endif /* !_WIN32 */

        log_write_mutex_exit();

        if (flush_to_disk) {
                log_write_flush_to_disk_low();
        }
}


[*]该函数负责将日志数据从内存缓冲区写入到磁盘上的日志文件,并根据需要决定是否将这些数据同步(刷新)到磁盘。这个函数是数据库日志管理系统的焦点部分。
1、前期准备
byte*         write_buf;
lsn_t         write_lsn;

ut_ad(!srv_read_only_mode);

if (recv_no_ibuf_operations) {

        return;
}

loop:
        ut_ad(++loop_count < 128);
   

[*]初始化变量:write_buf和write_lsn,分别用于记录写入日志偏移量和写入日志lsn
[*]断言检查:确保服务器不在只读模式下运行
[*]假如recv_no_ibuf_operations为真,表现正在运行恢复操纵,此时不允许对日志文件进行操纵,函数直接返回
[*]断言检查循环次数,防止无穷循环
2、条件判断
        log_write_mutex_enter();
        ut_ad(!recv_no_log_write);

        lsn_t        limit_lsn = flush_to_disk
                ? log_sys->flushed_to_disk_lsn
                : log_sys->write_lsn;

        if (limit_lsn >= lsn) {
                log_write_mutex_exit();
                return;
        }
   

[*]假如limit_lsn大于或等于目标lsn,则直接退出互斥锁并返回,由于所需的日志已经写入或刷新到指定位置。
if (log_sys->n_pending_flushes > 0
        || !os_event_is_set(log_sys->flush_event)) {
        log_write_mutex_exit();
        os_event_wait(log_sys->flush_event);
        goto loop;
}
   

[*]检查是否有挂起的刷新操纵或flush_event变乱未设置。是的话,退出锁,等候flush_event,并进入循环。
if (flush_to_disk
        && (log_sys->n_pending_flushes > 0
        || !os_event_is_set(log_sys->flush_event))) {

        /* Figure out if the current flush will do the job
        for us. */
        bool work_done = log_sys->current_flush_lsn >= lsn;

        log_write_mutex_exit();

        os_event_wait(log_sys->flush_event);

        if (work_done) {
                return;
        } else {
                goto loop;
        }
}

log_mutex_enter();
if (!flush_to_disk
        && log_sys->buf_free == log_sys->buf_next_to_write) {
        /* Nothing to write and no flush to disk requested */
        log_mutex_exit_all();
        return;
}
   

[*]假如需要刷新到磁盘且有挂起的刷新操纵或flush_event未触发,则实行以下操纵
[*]界说了一个布尔变量work_done,用于检查当前的刷新操纵是否已经包罗了目标日志序列号lsn。
[*]假如current_flush_lsn大于或等于lsn,则work_done为真,那么函数直接返回。假如work_done不为真,则跳转到loop处循环
[*]接下来,代码进入另一个互斥锁,检查是否不需要刷新到磁盘且日志缓冲区没有待写入的内容。假如这两个条件都满足,则退出全部相干的互斥锁并返回。
3、日志写入准备
log_group_t*        group;// 指向日志组的指针
ulint                start_offset;// 日志缓冲区中待写入数据的起始偏移量
ulint                end_offset;    // 日志缓冲区中待写入数据的结束偏移量
ulint                area_start;    // 对齐后的待写入区域的起始偏移量
ulint                area_end;      // 对齐后的待写入区域的结束偏移量
ulong                write_ahead_size = srv_log_write_ahead_size;   // 写前日志大小
ulint                pad_size;   // 填充大小

DBUG_PRINT("ib_log", ("write " LSN_PF " to " LSN_PF,
                                log_sys->write_lsn,
                                log_sys->lsn));

if (flush_to_disk) {
        log_sys->n_pending_flushes++;   // 增加挂起的刷新操作计数
        log_sys->current_flush_lsn = log_sys->lsn;// 记录当前正在刷新的日志序列号
        MONITOR_INC(MONITOR_PENDING_LOG_FLUSH);
        os_event_reset(log_sys->flush_event);

        if (log_sys->buf_free == log_sys->buf_next_to_write) {
                /* Nothing to write, flush only */
                log_mutex_exit_all();
                log_write_flush_to_disk_low();
                return;
        }
}

/* 计算待写入数据的起始和结束偏移量,以及对齐偏移量 */
start_offset = log_sys->buf_next_to_write;
end_offset = log_sys->buf_free;

area_start = ut_calc_align_down(start_offset, OS_FILE_LOG_BLOCK_SIZE);
area_end = ut_calc_align(end_offset, OS_FILE_LOG_BLOCK_SIZE);

ut_ad(area_end - area_start > 0);

log_block_set_flush_bit(log_sys->buf + area_start, TRUE);
log_block_set_checkpoint_no(
        log_sys->buf + area_end - OS_FILE_LOG_BLOCK_SIZE,
        log_sys->next_checkpoint_no);

write_lsn = log_sys->lsn;
write_buf = log_sys->buf;

log_buffer_switch();   // 切换缓冲区

group = UT_LIST_GET_FIRST(log_sys->log_groups);// 从日志系统的日志组列表中获取第一个日志组

log_group_set_fields(group, log_sys->write_lsn);// 设置日志组的字段,如当前的写入日志序列号

log_mutex_exit();

/* Calculate pad_size if needed. */
pad_size = 0;
/* 如果写前日志大小大于操作系统日志块大小,则需要计算填充大小 */
if (write_ahead_size > OS_FILE_LOG_BLOCK_SIZE) {
        lsn_t        end_offset;
        ulint        end_offset_in_unit;

        end_offset = log_group_calc_lsn_offset(
                ut_uint64_align_up(write_lsn,
                                        OS_FILE_LOG_BLOCK_SIZE),
                group);
        end_offset_in_unit = (ulint) (end_offset % write_ahead_size);

        if (end_offset_in_unit > 0
                && (area_end - area_start) > end_offset_in_unit) {
                /* The first block in the unit was initialized
                after the last writing.
                Needs to be written padded data once. */
                pad_size = write_ahead_size - end_offset_in_unit;

                if (area_end + pad_size > log_sys->buf_size) {
                        pad_size = log_sys->buf_size - area_end;
                }

                ::memset(write_buf + area_end, 0, pad_size);
        }
}
   

[*]这段代码的目的是在确保日志数据正确写入和刷新到磁盘的同时,处置惩罚对齐和填充等细节问题。通过使用互斥锁和条件判断,实现了在多线程环境下对日志系统的安全访问和同步。
4、写入日志文件
/* 这行代码将缓冲区中从area_start到area_end + pad_size的数据写入到指定的日志组中。
* 写入的起始日志序列号是对log_sys->write_lsn向下对齐到OS_FILE_LOG_BLOCK_SIZE的结果,而偏移量则是start_offset - area_start,表示数据在缓冲区中的相对位置。 */
log_group_write_buf(
        group, write_buf + area_start,
        area_end - area_start + pad_size,
        ut_uint64_align_down(log_sys->write_lsn,
                                        OS_FILE_LOG_BLOCK_SIZE),
        start_offset - area_start);

srv_stats.log_padded.add(pad_size);// 将填充大小pad_size添加到日志系统的填充统计中,有助于监控和调试日志写入过程中的填充情况。

log_sys->write_lsn = write_lsn;// 更新日志系统的写入序列号write_lsn

/* 在非windowa平台上,将已刷新到磁盘的序列号flushed_to_disk_lsn更新为当前的写入序列号write_lsn */
#ifndef _WIN32
        if (srv_unix_file_flush_method == SRV_UNIX_O_DSYNC) {
                log_sys->flushed_to_disk_lsn = log_sys->write_lsn;
        }
#endif /* !_WIN32 */

log_write_mutex_exit();

if (flush_to_disk) {
        log_write_flush_to_disk_low();// 如果flush_to_disk为真,则调用log_write_flush_to_disk_low函数将日志数据真正刷新到磁盘上。这是确保数据持久化的关键步骤。
}
   

[*]这段代码是日志写入流程中的一部分,负责将数据写入日志组、更新统计信息和序列号、根据平台特性大概更新已刷新到磁盘的序列号,并终极释放互斥锁和(假如需要)将数据刷新到磁盘。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 海山数据库(He3DB)源码详解:海山MySQL redo日志-日志文件组