暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【华为云MySQL技术专栏】MySQL 8.0 InnoDB Redo Log持久化流程简析

GaussDB数据库 2025-06-12
354

1


背景介绍

预写日志(WAL:Write-Ahead Logging)是数据库最重要的组件之一,用于保证数据操作的原子性和持久性。WAL(在InnoDB中称为Redo Log)中保存了所有对数据文件的更改记录,所有的修改在提交之前都要先写入 Log 文件中,如此既可以延迟修改后的页面刷新到磁盘的时间,又可以防止数据丢失。

当写密集型工作负载写入Redo Log时,性能会因许多用户线程同步操作而受到限制。在多CPU和存储设备性能较快的场景下,这一点尤其明显。

本文将重点介绍MySQL 8.0版本的Redo Log持久化流程,主要涉及8.0版本对Redo Log的优化点以及相关线程的交互处理逻辑,包括Log Writer和Log Flusher等线程。


2


基本流程

用户事务在更改数据时,一个数据页修改操作可能会包含多条Redo Log,每条Redo Log都会被分配一个全局唯一递增的标号LSN(Log Sequence Number),用于标识该日志记录的顺序。

每一条Redo Log都由mini-transaction(简称为mtr)原子提交,mtr是MySQL内部用于执行数据更改操作的最小单位。在数据页上执行更改动作时,MySQL会创建若干个mtr来确保操作的原子性。随之生成的Redo Log会暂时存放在mtr的m_log中,这是一个动态分配的内存空间。

当mtr提交时,会将m_log中的数据拷贝到InnoDB的Redo Log Buffer中,并进一步唤醒相关后台线程执行Redo Log的持久化。

mtr::commit主要逻辑在mtr_t::Command::execute内实现,具体流程如下所示。其中log_t,recent_written,recent_closed等结构体在关键数据结构中会具体介绍。

    mtr_t::commit //用户线程提交mtr
    |--> mtr_t::Command::execute
    |  |--> mtr_t::Command::prepare_write //进行一些校验并返回将要写入的redo log长度
    |  |--> mtr_write_log_t write_log;
    |  |--> log_buffer_reserve //1.分配log buf,获得起始和终点lsn位置并初始化write_log
    |  |  |--> log_buffer_s_lock_enter_reserve
    |  |  |  |--> log.sn.fetch_add //全局偏移log.sn原子加自身的redo长度,得到自身在log buffer中独享的空间
    |  |  |  |--> log_buffer_s_lock_wait //unlikely,如果当前start_sn被锁,则需等待start_sn解锁并允许写入buffer
    |  |  |--> log_wait_for_space_after_reserving //等待recent_written的空间,预留足够的redo log buffer,buffer不够会调用log_write_up_to清理buffer空间,还不够则进行resize
    |  |  |  |--> log_wait_for_space_in_log_buf //确保这条redo log能完整的写入redo log Buffer,而且回环后不会覆盖尚未写入磁盘的redo log
    |  |  |  |  |--> return //if (end_sn + OS_FILE_LOG_BLOCK_SIZE(文件系统的块大小,512 字节)<= write_sn + buf_size_sn) 
    |  |  |  |  |--> log_write_up_to //等待redo log写入到指定的lsn
    |  |  |  |  |  |--> log_wait_for_write //通过设置writer_event,异步触发log_writer写
    |  |  |  |  |  |  |--> os_event_wait_for(log.write_events[slot],stop_condition); //当log.write_lsn.load() >= lsn会唤醒或者由log_write_notify唤醒
    |  |  |  |  |  |--> log_wait_for_flush //当flush_to_disk为true时,等待redo log刷盘到指定的lsn
    |  |--> m_impl->m_log.for_each_block(write_log) //2.将mtr的buffer中的内容复制到redo log buffer中
    |  |  |--> mtr_write_log_t::operator
    |  |  |  |--> log_buffer_write //写入到redo log buffer(log->buf)
    |  |  |  |--> log_buffer_write_completed //完成赋值后,触发LinkBuf更新recent_written字段
    |  |  |  |  |--> log.recent_written.add_link_advance_tail //更新本次写入的内容范围对应的LinkBuf环形数组
    |  |--> log_wait_for_space_in_log_recent_closed //3.在加脏页之前需要判断是否link buf已满
    |  |--> add_dirty_blocks_to_flush_list //4.加脏页到flush list中
    |  |  |--> add_dirty_page_to_flush_list
    |  |--> log_buffer_close
    |  |  |--> log_buffer_s_lock_exit_close //5.更新recent_closed字段
    |  |  |  |--> log.recent_closed.add_link_advance_tail(start_lsn, end_lsn);

    ◆    Log_Writer负责将 Redo Log Buffer 中的内容写入 OS Cache。当 write_lsn(已写入OS Cache的lsn)落后于 buf_ready_for_write_lsn(recent_written中的连续最大lsn)时,Log Writer 线程会将 Log Buffer 中连续的 Redo Log 写入操作系统的 OS Cache。

    ◆  写入完成后,Log Writer线程向前推进write_lsn,并唤醒Log Flusher和Log_Writer Notifier线程。

    ◆   Log Flusher负责 Redo Log 的刷脏,与数据脏页的 Flush 无关,数据脏页的 Flush 由 Buffer Pool 刷脏线程处理。当flushed_to_disk_lsn落后于write_lsn时,Log Flusher线程会执行fsync()函数,将OS Cache中的数据刷入磁盘,完成Redo Log的真正刷盘。

    ◆  刷盘完成后,Log Flusher线程向前推进flushed_to_disk_lsn,并唤醒Log Flush Notifier线程。

    ◆    Log Write Notifier/Log Flush Notifier线程会唤醒等待write_lsn/flushed_to_disk_lsn的用户线程。MySQL通过innodb_flush_log_at_trx_commit参数控制事务提交的时机,决定是由Log Write Notifier还是Log Flush Notifier线程唤醒用户线程。

    ◆    用户线程被唤醒后,会比较自身事务的commit_lsn与write_lsn/flushed_to_disk_lsn:

         如果commit_lsn 小于或等于 write_lsn/flushed_to_disk_lsn,则表示对应的Redo Log已写入OS Cache/磁盘,事务可以提交。

         如果commit_lsn仍大于write_lsn/flushed_to_disk_lsn,则用户线程继续等待下一次唤醒。

    innodb_flush_log_at_trx_commit 的取值范围是[0,2]。不同值的含义如下:

    ◆  0: InnoDB 不会在事务提交时主动将 Redo Log Buffer 的数据写入磁盘。而是每秒执行一次 write 操作和flush 操作,将 Redo Log Buffer 的数据更新到文件系统 OS Cache中,调用文件系统的fsync()将数据缓存更新至磁盘中。

    ◆ 1: InnoDB 会在每次事务提交时,立即执行一次 write 和 flush 操作,将 Redo Log Buffer 的数据写入文件系统缓存并刷新到磁盘。这是最安全的方式。

    ◆  2: InnoDB 会在每次事务提交时,立即执行一次 write 操作,将 Redo Log Buffer 的数据写入文件系统缓存。但不会立即执行 flush 操作,而是每秒执行一次 flush,将缓存中的数据刷新到磁盘 。

    MySQL InnoDB的日志系统通过多个线程和内部数据结构的协作,使Redo Log从产生到落盘再到唤醒用户线程的流程更加高效。以下是该过程的时序图:

    图1 用户线程生成Redo Log并等待落盘


    3


    关键数据结构


    Link_buf

    Link_buf是MySQL 8.0引入的一种数据结构,用来维护和推进Redo Log在相关阶段的lsn位点。其本质是一个环形数组,数组名称为m_links,容量为m_capacity,m_tail指向其连续尾部。该数组的索引计算方式为start_lsn对m_capacity-1取模,每个元素存储的是end_lsn,且所有元素均为原子类型以确保线程安全。特别地,当end_lsn值为0时,表示该槽位为空。


      template <typename Position = uint64_t>
      class Link_buf {
       public:
        /* ...... */
        /** Add a directed link between two given positions. */
        void add_link_advance_tail(Position from, Position to);
       private:
        /* ...... */
        /** Capacity of the buffer. */
        size_t m_capacity;


        /** Pointer to the ring buffer (unaligned). */
        std::atomic<Distance> *m_links;


        /** Tail pointer in the buffer (expressed in original unit). */
        alignas(ut::INNODB_CACHE_LINE_SIZE) std::atomic<Position> m_tail;
      }

      图2 Link_buf结构

      通过合理运用std::atomic_thread_fence(std::memory_order_release)和std::atomic_thread_fence(std::memory_order_acquire)内存栅栏操作,Link_buf实现了对不同槽位lsn读写操作的无锁并发控制,从而提升了系统的并发性能。

      在log_sys小节中,会结合Redo Log写入Log Buffer的流程具体分析Link_buf如何实现的无锁化。

      log_sys

      log_sys是管理Redo Log 元信息,Redo Log Buffer 等操作的系统单元。

        struct alignas(INNOBASE_CACHE_LINE_SIZE) log_t {  
          atomic_sn_t sn;   // 全局唯一的递增的偏移 sn. 
          aligned_array_pointer<byte, OS_FILE_LOG_BLOCK_SIZE> buf;  // log buffer的内存区
          Link_buf<lsn_t> recent_written;               // 解决并发插入Redo Log Buffer后刷入ib_logfile存在空洞的问题
          Link_buf<lsn_t> recent_closed;        // 解决并发插入flush_list后确认checkpoint_lsn的问题
          atomic_lsn_t write_lsn;           // write_lsn之前的数据已经写入系统的Cache, 但不保证已经Flush
          alignas(ut::INNODB_CACHE_LINE_SIZE) os_event_t *write_events;   // 指向write_events事件数组的指针,当write_lsn >= lsn时,log writer notifier会通知用户线程。
          alignas(ut::INNODB_CACHE_LINE_SIZE) os_event_t *flush_events;   // 指向flush_events事件数组的指针,当flushed_to_disk_lsn >= lsn时,log flush notifier会通知用户线程。
          atomic_lsn_t flushed_to_disk_lsn;         // 已经被flush到磁盘的数据
        }
        /** Redo log system (singleton). */
        extern char log_sys_struct[sizeof(log_t)];
        #define log_sys ((log_t*)log_sys_struct)

        在MySQL 5.7中,写性能受限于Redo Log的同步落盘。主要瓶颈在于,mtr在将Redo Log从m_log写入Log Buffer时需要加log_sys_t::mutex锁,而在将dirty page加入flush list时,为了保证全局有序,又需要加log_sys_t::flush_order_mutex锁。这导致其他用户线程的mtr在尝试写入Log Buffer时需要等待log_sys_t::mutex锁,同时即使向其他flush list中添加数据,也需要等待log_sys_t::flush_order_mutex锁。

        为解决多个线程的mtr对Redo Log的争抢问题,MySQL引入了两个Link_buf实例:recent_write和recent_close。这两个实例的长度由innodb_log_recent_written_size和innodb_log_recent_closed_size参数决定。不同的线程可以对recent_write和recent_close中的不同slot进行无锁读写,从而避免了锁竞争。

        sn

        全局唯一的递增的偏移,表示逻辑层已保留的Redo Log数据字节数,不包括日志块的页眉和页脚的字节。逻辑层的全局sn和物理层的lsn可以相互转换,两者间一一对应。

        图3所示,逻辑层Redo记录在转换为物理Redo时可能因为Block剩余空间不足,需要被拆分在连续的两个Block中。

        图 3 sn-lsn双向映射

        如基本流程中所述,不同的mtr会调用log_buffer_reserve函数,在这个函数里,全局偏移log.sn会fetch_add自己的redo长度len,得到每个mtr自身的start_sn(即sn)和end_sn(即sn + len)。由于log.sn是原子变量,这一步是无锁的。

        在函数log_buffer_write中,会将把start_sn转换为start_lsn,并根据其计算出Redo Log在log buffer中的偏移,从偏移开始长度为len的空间就是该Redo Log写入的空间。因为lsn是单调递增的,所以每个Redo Log的空间都是独享的,不会有重叠。通过这种方式实现了Redo Log Buffer 的无锁并行写入。

        recent_written

        recent_written对应Redo Log Buffer,记录着已写入Log Buffer的lsn。其mlink以start_lsn为索引,值为end_lsn,即mlink[start_lsn] = end_lsn。

        如前所述,不同的mtr会并行地将自身m_log中的数据写入Log Buffer中各自独享的空间,不会产生冲突。

        由于并发写入,获得较小LSN的mtr(较早申请LSN的mtr)可能不会优先完成memcpy操作,这可能导致Log Buffer中出现空洞。然而,ib_logfile不允许存在空洞。系统通过recent_written.tail(即buf_ready_for_write_lsn)确保在此LSN之前的Redo Log buffer中不存在空洞,从而实现ib_logfile的完整写入。

        在此过程中,不同的mtr作为生产者将数据写入Log Buffer,而Log Writer作为消费者负责将数据取出。Log Writer通过recent_write无锁地获取读取位置,确保mtr与Log Writer之间不会出现上锁等待。

        图4 LogBuffer待写入

        如图4所示,红色是已写入Log Buffer 的 redo,白色为尚未写入的部分。write_lsn是当前Log Writer已经写入到OS Cache中的最大LSN,buf_ready_for_write_lsn是当前Log Writer找到的Log Buffer中已经连续的最大LSN,current_lsn是当前已经分配给mtr的的最大lsn位置。

        从write_lsn到buf_ready_for_write_lsn的范围是Log Writer可以连续写入OS Cache的区域,而从buf_ready_for_write_lsn到current_lsn的范围是当前多个mtr并发写入Log Buffer的区域。

        图中下方的连续方格对应log.recent_written中的m_links,中间的两个全零空洞阻碍了buf_ready_for_write_lsn的推进。

        假设reserve到第一个空洞的mtr完成写Log Buffer并更新log.recent_written,如图5所示:

        图5 写入Log Buffer

        这时,Log Writer从当前buf_ready_for_write_lsn位点开始遍历log.recent_written的m_links槽位,确认日志已连续写入:

        图6 确认log.recent_written连续

        因此,当Log Writer被唤醒后便会推进buf_ready_for_write_lsn,并将之前的槽位都清零,供之后循环复用,如图7所示。

        图7 推进buf_ready_for_write_lsn

        紧接着,Log Writer会将从write lsn到buf_ready_for_write_lsn范围的Redo Log写入OS Cache并提升write_lsn。

        recent_closed

        recent_closed记录着已写入flush list的lsn,用于在checkpoint时辅助计算checkpoint位点。其类似recent_written,允许多个mtr并发地给Buffer Pool注册脏页,其tail指针为当前连续写入flush list的最大LSN,即buffer_dirty_pages_added_up_to_lsn(dpa_lsn)。

        为确保故障恢复时数据不丢失,需要确定一个合适的checkpoint位点,以保证该位点之前所有脏页均已刷盘。具体如下:

        ◆   Log Checkpointer首先会获取Buffer Pool中所有脏页的最小的lsn(lwm_lsn),认为这之前的脏页已落盘。然而这并不充分,由于多个mtr并发写入可能存在乱序的情况,部分mtr的Redo对应的page可能还未写入Buffer Pool脏页。若将checkpoint设置在lwm_lsn,故障恢复时会丢失这些数据。

        ◆   因此还需要知道当前已经加入到flush list的lsn位置(dpa_lsn),取二者的较小值。

        ◆   同时出于保守考虑,lwm_lsn还需要减掉一个recent_closed的容量大小(即最大的乱序范围)。

        ◆   最后再和flushed_to_disk_lsn 取最小值,就能得到checkpoint的位点。

        write_events/flush_events用户提交事务时,会根据innodb_flush_log_at_trx_commit参数,调用log_wait_for_write或log_wait_for_flush,来等待Redo Log写入到OS Cache或刷到磁盘。用户线程的通知是通过write_events/flush_events事件数组来实现的,为了避免一次通知的events过多,write_events/flush_events会像桶一样划分给不同的用户线程。

        Redo Log是以一个个log block划分的,以write_events为例,其大小由参数innodb_log_write_events控制。假设write_events大小为m,则第n个log block的写入/刷盘,由write_events [n%m]事件监听。当log buffer的第L1个log block到第L2个log block写入OS Cache时,会设置L1-L2之间的log block所属的write_events,从而等待在L1-L2范围内的Redo Log的用户线程都会收到通知。

        图8 Redo Log 映射到write_events


          // 计算给定lsn在事件数组中的索引
          static inline size_t log_compute_wait_event_slot(lsn_t lsn, size_t events_n) {
            return ((lsn - 1) OS_FILE_LOG_BLOCK_SIZE) & (events_n - 1);
          }



          4


          关键后台线程

          Redo持久化主要涉及Log Writer,Log Flusher,Log Write Notifier,Log Flush Notifier四个后台线程。

          图9 Redo Log持久化后台线程组

          在InnoDB 启动时,就会启动这四个线程:

            srv_start
            |--> log_start_background_threads
            |  |--> os_thread_create(log_flush_notifier_thread_key, log_flush_notifier, &log);
            |  |--> os_thread_create(log_flusher_thread_key, log_flusher, &log);
            |  |--> os_thread_create(log_write_notifier_thread_key, log_write_notifier, &log);
            |  |--> os_thread_create(log_writer_thread_key, log_writer, &log);
            |  |--> srv_threads.m_log_flush_notifier.start();
            |  |--> srv_threads.m_log_flusher.start();
            |  |--> srv_threads.m_log_write_notifier.start();
            |  |--> srv_threads.m_log_writer.start();

            Log Writer

            Log Writer会自旋,不断检测是否有待写入OS Cache的Redo Log。其工作流程如下:

              log_writer /*endless loop*/
              |--> waiting.wait(stop_condition);//等待write_lsn<buf_ready_for_write_lsn
              | |--> log_advance_ready_for_write_lsn
              | | |--> log.recent_written.advance_tail_until(stop_condition) 推进Link_buf::m_tail,同时回收之前的槽位
              |--> log_writer_write_buffer
              | |--> log_write_buffer
              | | |--> log.m_current_file.offset(start_lsn); 计算写入在文件的真实偏移
              | | |--> compute_how_much_to_write 计算当前文件是否有足够的空间
              | | |--> copy_to_write_ahead_buffer 将Redo Log从Log Buffer复制到Write-Ahead Buffer
              | | |--> write_blocks 写入OS Cache
              | | |--> log.write_lsn.store(new_write_lsn); 更新write_lsn
              | | |--> notify_about_advanced_write_lsn
              | | |  |--> log_compute_write_event_slot 计算用户写入redo对应的write_events slot
              | | |  |--> os_event_set(log.flusher_event); /innodb_flush_log_at_trx_commit=1时唤醒log flusher
              | | |  |--> os_event_set(log.write_events[first_slot]); //直接唤醒用户等待first_slot的线程
              | | |  |--> os_event_set(log.write_notifier_event); //唤醒log_write_notifier

              ◆  Log Writer线程 wait 在stop_condition函数,该函数内会调用log_advance_ready_for_write_lsn()推进buf_ready_for_write_lsn,直到满足条件write_lsn < buf_ready_for_write_lsn,才返回true唤醒线程。

              ◆  Log Writer被唤醒后会调用log_writer_write_buffer()进行 Redo 写入,具体逻辑在log_write_buffer()中。

              ◆  在写入前,会检查当前 ib_logfile 文件是否还有足够的空间来容纳待写入的 Redo Log:
                  如果当前文件已满,则调用 start_next_file() 切换到下一个 Redo Log 文件,文件个数由参数指定;

                  如果当前文件仍有空闲空间,则会判断是否需要使用 Write Ahead Buffer,以避免因小于OS_FILE_LOG_BLOCK_SIZE的 IO 操作导致 "read on write" 现象。

              ◆  若要使用 Write Ahead Buffer ,则调用 copy_to_write_ahead_buffer() 函数将 Redo Log 从 Log Buffer复制到 Write Ahead Buffer。

              ◆  调用 write_blocks() 函数,通过os_file_write ()写入OS Cache,每次写入都是512字节对齐。

              ◆  更新log.write_lsn

              ◆  调用notify_about_advanced_write_lsn()根据slot选择对应线程唤醒:

                  如果要写入的Redo Log都对应同一个write_events槽位,则直接唤醒等待该槽位的用户线程。即在mtr 的 commit 阶段写入 Redo Log Buffer 时,需要等待log.write_lsn.load() >= lsn的用户线程。

                  如果要写入的Redo Log对应一批write_events槽位,则唤醒Log Write Notifier 线程,由其唤醒所有等待这些write_events的用户线程。

              Redo日志文件默认以ib_logfile0、ib_logfile1命名,为避免频繁创建和初始化文件带来的额外开销,InnoDB采用循环写入机制。用户可通过设置innodb_log_files_in_group参数来指定日志文件的数量,以优化存储和性能。默认情况下,InnoDB会使用两个重做日志文件。

              Log Flusher

              Log Writer提升write_lsn之后会通知Log Flusher线程,Log Flusher线程会调用fsync()将redo刷盘,并推进flush_up_to_lsn。Log Flusher与Log Writer之间仅通过write_lsn同步刷盘位置。

              其工作流程如下:

                log_flusher /* endless loop */
                |--> os_event_wait_time_low(log.flusher_event) //innodb_flush_log_at_trx_commit != 1
                |--> waiting.wait(stop_condition) //等待last_flush_lsn<log.write_lsn.load(),触发刷盘
                |  |--> log_flush_low
                |  |  |--> log.m_current_file_handle.fsync();
                |  |  |--> log.flushed_to_disk_lsn.store(flush_up_to_lsn);
                |  |  |--> log_compute_write_event_slot //计算用户刷盘redo对应的flush_events slot
                |  |  |--> os_event_set(log.flush_events[first_slot]) //只刷盘一个slot,则直接唤醒用户等待first_slot的线程
                |  |  |--> os_event_set(log.flush_notifier_event);  //唤醒log_flush_notifier
                ◆   Log Flusher根据innodb_flush_log_at_trx_commit来选择不同的wait方式:  

                     假如innodb_flush_log_at_trx_commit不为1,则每秒唤醒一次Log Flusher; 

                     否则,wait 在stop_condition函数,该函数内会判断last_flush_lsn是否小于 log.write_lsn,是则执行log_flush_low()进行刷盘,并退出等待。

                ◆   在log_flush_low内部,会调用fsync()进行文件刷盘,再更新  log.flushed_to_disk_lsn,最后类似Log Writer根据slot选择对应线程唤醒:

                    如果要刷盘的Redo Log都对应同一个flush_events槽位,则直接唤醒等待该槽位的用户线程。即在mtr 的 commit 阶段写入 Redo Log Buffer 时,需要等待log. flushed_to_disk_lsn.load() >= lsn的用户线程。

                    如果要写入的Redo Log对应一批flush_events槽位,则唤醒Log Flush Notifier 线程,由其唤醒所有等待这些flush_events的用户线程。

                Log Notifier

                Log Notifier 包含两个关键线程:Log Write Notifier 和 Log Flush Notifier。这两个线程会定期轮询(或被唤醒)各自关注的 LSN 位置:Log Write Notifier关注 write_lsn,而 Log Flush Notifier关注 flush_up_to_lsn。当这些 LSN 值推进到用户线程等待的位置时,它们会唤醒相应的用户线程。用户线程在执行 commit时会阻塞在 log_write_up_to 函数上,直到对应的 write_lsn 或 flush_up_to_lsn 推进到预期位置后,函数才会返回。

                Log Write Notifier 的具体流程如下:

                  log_write_notifier /*endless loop*/
                  |--> waiting.wait(stop_condition); //log.write_lsn>= lsn时,返回true停止等待
                  |--> slot = log_compute_write_event_slot(log, lsn); //计算当前lsn等待在write_events的哪个slot
                  |--> os_event_set(log.write_events[slot]); //唤醒在等待的用户线程
                  |--> lsn = write_lsn + 1;
                  Log Flush Notifier类似,具体流程如下:

                    log_flush_notifier /*endless loop*/
                    |--> waiting.wait(stop_condition); //log.flushed_to_disk_lsn>=lsn时,返回true停止等待
                    |--> slot = log_compute_flush_event_slot(log, lsn); //计算当前lsn等待在flush_events的哪个slot
                    |--> os_event_set(log.write_events[slot]); //唤醒在等待的用户线程
                    |--> lsn = flush_lsn + 1;


                    5


                    总结

                    本文深入分析了MySQL 8.0中Redo Log持久化模块的源码实现原理。MySQL 8.0通过引入无锁化机制,有效解决了用户线程在写入Redo Log时因锁竞争导致的性能瓶颈。此外,系统将Redo Log的文件写入和刷盘操作从用户线程中独立出来,交由专门的线程负责,使用户线程仅需处理将Redo Log写入Log Buffer的任务,而无需关注后续的落盘过程。用户线程只需等待Log Writer线程或Log Flusher线程的通知,这种设计显著提升了高并发场景下Redo日志的写入效率。


                    END



                    华为云数据库 新用户特惠


                    Flexus云数据库RDS
                    6个月288元(原价1200)/

                    一年576元(原价2400)

                    RDS for MySQL单机版  3个月152.88元

                       

                    扫码抢购

                      

                    活动时间:2025/6/1-2025/6/30

                      戳“阅读原文”,了解更多


                    文章转载自GaussDB数据库,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                    评论