Mysql(事务篇) 四大特性、隔离级别、MVCC(Undo log + ReadView)

2024年 5月 7日 66.2k 0

1. 事务回顾

事务指的是逻辑上的一组操作,组成这组操作的各个单元要么全部成功,要么全部失败。

事务作用:保证一个事务中多次SQL操作要么全部成功,要么全部失败。

Mysql是一个服务器/客户端架构的软件,对应同一个服务器来说,可以有多个客户端与之连接,每个客户端与服务器连接上之后,就可以称之为一个会话(Session)。我们可以同时在不同的会话中输入各种语句,这些语句可以作为事务的一部分进行处理。不同的会话可以同时发送请求,也就是说服务器可能同时处理多个事务,这样就会导致不同的事务可能访问到相同的记录。

事务的隔离性在理论上,是指,在某个事务对某个数据进行访问时,其他事务应该进行排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对性能影响太大,所以才会出现各种隔离级别,来最大限度的提升系统并发处理事务的能力,牺牲部分隔离性来提升性能。

事务是数据库最为重要的机制之一,凡是使用过数据库的人,都了解过数据库的事务机制,也对ACID四个基本特性如数家珍,但是对其底层的实现原理往往了解不够,所以接下来聊一聊事务的原理。

由于Mysql中的事务是存储引擎实现,而且只有InnoDB支持事务,因此我们讲解InnoDB的事务。

1.1 事务四大特性ACID

数据库事务具有ACID四大特性,分别为

  • 原子性(Atomicity):原子性是指事物是一个不可分割的工作单位,事务内的操作要么都发生,要么都不发生
  • 一致性(Consistency):事务前后数据的完整性必须保持一致
  • 隔离性(Isolation):多个用户并发访问数据库时,一个用户的事务不能被其他用户的事务所干扰,多个并发事务之间数据要相互隔离。隔离性由隔离级别保障!
  • 持久性(Durability):一个事务一旦提交,他对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

1.2 事务并发问题

  • 脏读:一个事务读到了另一个事务未提交的数据
  • 不可重复读:一个事务读到了另一个事务已经提交(update) 的数据。引起事务中的多次查询结果不一致。
  • 虚读/幻读:一个事务读到了另一个事务已经插入(insert) 的数据。导致事务中多次的查询结果不一致。
  • 丢失更新的问题!
  • 1.3 隔离级别

    • read uncommitted 读未提交【RU】:一个事务读到另一个事务没有提交的数据
      • 存在:3个问题(脏读、不可重复读、幻读)
    • read committed 读已提交【RC】:一个事务读到另一个事务已经提交的数据
      • 存在:2个问题(不可重复读、幻读)
      • 解决:1个问题(脏读)
    • repeatable read 可重复读【RR】:在一个事务中读到的数据始终保持一致,无论是另一个事务是否提交
      • 存在:1个问题(幻读) 其实已经解决了,但不是通过MVCC解决的,是通过锁来实现的,后面再聊
      • 解决:2个问题(脏读、不可重复读)
    • serializable 串形化:同时只能执行一个事务,相当于事务中的单线程
      • 解决:3个问题(脏读、不可重复读、幻读)

    安全和性能对比

    • 安全性:serializable > repeatable read > read committed > read uncommitted
    • 性能:read uncommitted > read committed > repeatable read > serializable

    常见数据库的默认隔离级别

    • Mysql:repeatable read
    • Oracle:read committed

    2. 一条Insert语句的执行流程

    Insert into tab_user(id,name,age,address) values (1,'刘备',18,'蜀国');

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-1

    3. 事务底层原理详解

    如何实现的隔离级别? RC是怎样实现的? RR是怎样实现的?

    3.1 丢失更新问题

    两个事务针对同一个数据进行修改操作时会丢失更新,这个现象被称之为丢失更新问题。

    举个例子:管理员查询所有用户的存款总和,假设除了用户01和用户02之外,所有用户的存款都为0,用户01和02各有存款1000,所以所有用户的存款总额为2000. 但是在查询过程中,用户01向用户02转账100元,就会形成下面的情况:

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-2

    3.2 解决方案

    3.2.1 解决方案一:基于锁并发控制LBCC

    使用基于锁的并发控制LBCC(Lock Based Concurrency Control)可以解决上述问题。

    查询总额事务会对读取的行加锁,等到操作结束后再释放所有行上的锁。因为用户A的存款被锁,导致转账操作被阻塞,指导查询总额事务提交并将所有的锁释放。

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-3

    这种方案比较简单粗暴,就是一个事务去读取一条数据的时候,就上锁,不允许别的事务来操作。加入当前事务只是加读锁, 那么其他事务就不能有写锁,也就是不能修改数据;而假如当前事务需要加写锁,那么其他事务就不能持有任何锁。总而言之,能加锁成功,就确保了除了当前事务之外,其他事务不会对当前数据产生影响,所以自然而然,当前事务读取到的数据就只能是最新的,而不会是快照数据。

    3.2.2 解决方案二:基于版本并发控制MVCC

    当然使用版本的并发控制MVCC(Multi Version Concurrency Control)机制也可以解决这个问题。

    查询总额事务先读取了用户A的账户存款,然后转账事务会修改用户A和用户B的账户存款,查询总额事务读取用户B存款时,不会读取转账事务修改后的数据,而是读取本事务开始时的副本数据【快照数据】。

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-4

    MVCC使得普通的SELECT请求不加锁,读写不冲突,显著提高了数据库的并发处理能力。 MVCC保障了ACID中的隔离性。

    请求不加锁、读写不冲突!!!

    3.3 MVCC实现原理【InnoDB】

    定义:MVCC全称叫多版本并发控制,是RDBMS常用的一种并发控制方法,用来对数据库进行并发访问,实现事务。 核心思想是读不加锁、读写不冲突。在读多写少的应用中,读写不冲突非常重要,极大的增加了系统的并发性能。

    MVCC实现原理关键在于数据快照,不同的事务访问不同版本的数据快照,从而实现事务下对数据的隔离级别。 虽然说具有多个版本的数据快照,但这并不意味着必须拷贝数据,保存多份数据文件(这样会浪费存储空间),Innodb通过事务的Undo日志巧妙的实现了多版本的数据快照。

    MVCC的实现依赖于 Undo日志 和 ReadView。

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-5
    InnoDb下的表有默认字段和可见字段, 默认字段是实现MVCC的关键,默认字段是隐藏的列。默认字段最关键的2列,一个保存了行的事务ID,一个保存了行的回滚指针。 每当开启新的事务,都会自动递增产生一个新的事务ID。

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-6
    事务开始后,生成当前事务影响行的ReadView。当查询时,需要用当前查询的事务ID与ReadView确定要查询的数据版本。

    3.3.1 Undo日志

    Redo日志记录了事务的行为,可以很好的通过其对页进行“重做”操作。但是事务有时还需要进行回滚操作,这时就需要undo log。因此在对数据库进行修改时,InnoDB存储引擎不但会产生redo log,还会产生一定量的undo log。这样如果用户执行的事务或者语句由于某种原因失败了,又或者用户用一条rollback语句请求回滚,就可以利用这些undo信息将数据回滚到修改之前的样子。在多事务读取数据时,有了undo日志,可以做到读不加锁、读写不冲突。

    undo存放在数据库内部的一个特殊段(segment)中,这个段被称为Undo段,位于系统表空间中,也可以设置为undo表空间。

    Undo日志保存了记录修改前的快照。所以,对于更新和删除操作,InnoDB并不是真正的删除原来的记录,而是设置记录的delete mark为1。因此为了解决数据Page和Undo日志膨胀的问题,则需要回收机制进行清理Undo日志。

    根据行为的不同,Undo日志分为2种:Insert Undo Log 和 Update Undo Log

    1. Insert Undo Log:是在insert操作中产生的undo日志

    Insert操作的记录只对事务本身可见,对于其他事务此记录是不可见的,所以Insert Undo Log可以在事务提交后直接删除而不需要进行回收操作。

    如下图所示(初始状态):

    # 事务1:
    Insert into tab_user(id,name,age,address) values (10,'麦麦',23,'beijing');
    

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-7

    2. Update Undo Log: 是Update或者Delete操作中产生的Undo日志

    Update操作会对已经存在的行记录产生影响,为了实现MVCC多版本并发控制机制,因此Update Undo Log不能在事务提交时就删除,而是在事务提交时将日志放入指定区域,等待Purge线程进行最后的删除操作。

    如下图所示(第一次操作):

    # 事务2:
    update tab_user set name='雄雄',age=18 where id=10;
    
    # 当事务2使用Update语句修改该行数据时,会首先使用写锁锁定目标行,将该行当前的值复制到Undo中,然后再真正地修改当前行的值,最后填写事务ID,使用回滚指针指向Undo中修改前的行。
    

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-8

    当事务3进行修改 与事务2的处理过程类似,如下图所示(第二次修改):

    # 事务3:
    update tab_user set name='迪迪',age=16 where id=10;
    

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-9

    3.3.2 ReadView

    MVCC的核心问题是:判断一下版本链中的哪个版本是当前事务可见的!

    • 对于使用RU隔离级别的事务来说,直接读取记录的最新版本就好了,不需要Undo log
    • 对于使用串形化隔离级别的事务来说,使用加锁的方式来访问记录,不需要Undo log
    • 对于使用RC和RR隔离级别的事务来说,需要用到Undo日志的版本链

    1. 什么是readView?
    ReadView是张存储事务ID的表,主要包含当前系统中有哪些活跃的读写事务,把他们的事务ID放到一个列表中。结合Undo日志的默认字段【事务trx_id】来控制哪个版本的Undo日志可被其他事务看见。

    四个列:

    • m_ids: 表示在生成ReadView时,当前系统中活跃的读写事务ID列表。
    • m_low_limit_id: 事务ID下限,表示当前系统中活跃的读写事务中最小的事务id, m_ids事务列表中最小的事务id
    • m_up_limit_id: 事务ID上限,表示生成ReadView时,系统中应该分配给下一个事务的ID值
    • m_creator_trx_id: 表示生成该ReadView的事务的事务ID

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-10

    2. ReadView怎么产生,什么时候生成?

    • 开始事务之后,在第一次查询(select)时,生成ReadView
    • RC 和 RR 隔离级别的差异本质是因为MVCC中ReadView生成时机不同

    3. 如何判断可见性?

    开启事务执行第一次查询时,首先生成ReadView,然后根据Undo日志和ReadView按照判断可见性,按照下边步骤判断记录的版本链的某个版本是否可见。

    循环判断规则如下:

    • 如果被访问版本的trx_id属性值,小于ReadView中的事务下限ID,表示生成该版本的事务在生成ReadView之前已经提交,所以该版本可以被当前事务访问。
    • 如果被访问版本的trx_id属性值,等于ReadView中的m_creator_trx_id,可以被访问
    • 如果被访问版本的trx_id属性值,大于等于ReadView中的事务上限id,说明是在生成Readview后才产生的数据,所以该版本不可以被访问。
    • 如果被访问版本的trx_id属性值,在事务下限id和事务上线id之间,那就需要判断是不是在m_ids列表中:
      • 如果在m_ids列表中,说明创建ReadView时生成该版本的事务还是活跃的,不可以被访问
      • 如果不在m_ids列表中,说明创建ReadView时生成该版本的事务已经提交,可以访问

    循环判断Undo log中版本链某一的版本是否对当前事务可见,如果循环到最后一个版本也不可见的话,那么意味着这条记录对该事务不可见,查询结果就不包含该记录。

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-11

    3.3.3 ReadView案例分析

    案例01 - 读已提交RC隔离级别下的可见性分析

    每次读取数据前都会生成一个ReadView, 默认tab_user表里只有一条数据,数据内容是刘备。

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-12

    事务ID是递增的

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-13

    select01执行过程如下:

    • 在执行select语句时会先生成一个ReadView,m_ids列表内容就是【100,200】
    • 然后从版本链中挑选可见的记录,从图中可以看出
      • 最新版本的数据内容是‘张飞’,该版本的trx_id值为100,在m_ids范围内,所以不符合可见性要求,跳到下一个版本。
      • 下一个版本的数据内容是‘关羽’,该版本的trx_id值也为100,在m_ids范围内,所以不符合可见性要求,跳到下一个版本。
      • 下一个版本的数据内容是‘刘备’,该版本的trx_id值也为80,小于m_ids列表最小的事务id 100, 符合要求。
    • 最后返回的是刘备的数据。

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-14

    select02执行过程如下:

    • 在执行select语句时会先生成一个ReadView,m_ids列表内容就是【200】
      • 事务ID为100的那个事务已经提交了,所以生成快照的时候就没有它了
    • 然后从版本链中挑选可见的记录,从图中可以看出
      • 最新版本的内容是‘诸葛亮’,该版本trx_id值为200,在m_ids列表内,不符合可见性要求,跳下一个版本。
      • 下一个版本的内容是‘赵云’,该版本trx_id值为200,在m_ids列表内,不符合可见性要求,跳下一个版本。
      • 下一个版本的内容是‘张飞’,该版本trx_id值为100,小于m_ids列表内最小值200,符合可见性要求,此版符合要求。
    • 最后返回’张飞‘的记录

    以此类推,如果之后事务id为 200 的记录也提交了,再次在使用 RC 隔离级别的事务中查询表 t 中 id 值为 1 的记录时,得到的结果就是 '诸葛亮' 了,具体流程我们就不分析了。

    总结:使用RC隔离级别的事务在每次查询开始时都会生成一个独立的ReadView。

    案例代码如下:

    CREATE TABLE `tab_user` (
    `id` int(11) NOT NULL,
    `name` varchar(100) DEFAULT NULL,
    `age` int(11) NOT NULL,
    `address` varchar(255) DEFAULT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB;
    
    Insert into tab_user(id,name,age,address) values (1,'刘备',18,'蜀国');
    
    # 事务01
    
    -- 查询事务隔离级别:
    select @@tx_isolation;
    
    -- 设置数据库的隔离级别
    set session transaction isolation level read committed;
    
    SELECT * FROM tab_user; # 默认是刘备
    
    # Transaction 100
    
    BEGIN;
    
    UPDATE tab_user SET name = '关羽' WHERE id = 1;
    
    UPDATE tab_user SET name = '张飞' WHERE id = 1;
    
    COMMIT;
    
    # 事务02
    
    -- 查询事务隔离级别:
    select @@tx_isolation;
    
    -- 设置数据库的隔离级别
    set session transaction isolation level read committed;
    
    # Transaction 200
    
    BEGIN;
    
    # 更新了一些别的表的记录
    
    ...
    
    UPDATE tab_user SET name = '赵云' WHERE id = 1;
    
    UPDATE tab_user SET name = '诸葛亮' WHERE id = 1;
    
    COMMIT;
    
    # 事务03
    
    -- 查询事务隔离级别:
    select @@tx_isolation;
    
    -- 设置数据库的隔离级别
    set session transaction isolation level read committed;
    
    BEGIN;
    
    # SELECT01:Transaction 100、200未提交
    SELECT * FROM tab_user WHERE id = 1; # 得到的列c的值为'刘备'
    
    # SELECT02:Transaction 100提交,Transaction 200未提交
    SELECT * FROM tab_user WHERE id = 1; # 得到的列c的值为'张飞'
    
    # SELECT03:Transaction 100、200提交
    SELECT * FROM tab_user WHERE id = 1; # 得到的列c的值为'诸葛亮'
    
    COMMIT;
    

    使用的SQL小结:

    -- 开启事务:还有一种方式begin
    start transaction
    
    -- 提交事务:
    commit
    
    -- 回滚事务:
    rollback
    
    -- 查询事务隔离级别:
    select @@tx_isolation;
    
    -- 设置数据库的隔离级别
    set session transaction isolation level read committed
    
    -- 级别字符串:`read uncommitted`、`read committed`、`repeatable read【默认】`、
    `serializable`
    
    -- 查看当前运行的事务
    
    SELECT
    a.trx_id,a.trx_state,a.trx_started,a.trx_query,
    b.ID,b.USER,b.DB,b.COMMAND,b.TIME,b.STATE,b.INFO,
    c.PROCESSLIST_USER,c.PROCESSLIST_HOST,c.PROCESSLIST_DB, d.SQL_TEXT
    FROM
    information_schema.INNODB_TRX a
    LEFT JOIN information_schema.PROCESSLIST b ON a.trx_mysql_thread_id = b.id
    AND b.COMMAND = 'Sleep'
    LEFT JOIN PERFORMANCE_SCHEMA.threads c ON b.id = c.PROCESSLIST_ID
    LEFT JOIN PERFORMANCE_SCHEMA.events_statements_current d ON d.THREAD_ID =
    c.THREAD_ID;
    

    案例02-可重复读RR隔离级别下的可见性分析

    在事务开始后第一次读取数据生成一个readView。对于使用RR隔离级别的事务来说,只会在第一次执行查询语句时生成一个readView,之后的查询就不会重复生成了。

    代码与执行过程与RC案例完全一致,唯一不同的是事务隔离级别。

    T3时刻,表t中id为1的记录得到的版本链如下所示:

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-15

    select01执行过程如下:

    • 在执行select语句时会先生成一个ReadView,m_ids列表内容就是【100,200】
    • 然后从版本链中挑选可见的记录,从图中可以看出
      • 最新版本的数据内容是‘张飞’,该版本的trx_id值为100,在m_ids范围内,所以不符合可见性要求,跳到下一个版本。
      • 下一个版本的数据内容是‘关羽’,该版本的trx_id值也为100,在m_ids范围内,所以不符合可见性要求,跳到下一个版本。
      • 下一个版本的数据内容是‘刘备’,该版本的trx_id值也为80,小于m_ids列表最小的事务id 100, 符合要求。
    • 最后返回的是刘备的数据。

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-1

    select02执行过程如下:

    • 因为之前已经生成过ReadView了,所以此时直接复用之前的ReadView,m_ids列表内容就是【100,200】
    • 然后从版本链中挑选可见的记录,从图中可以看出
      • 最新版本的数据内容是‘诸葛亮’,该版本的trx_id值为200,在m_ids范围内,所以不符合可见性要求,跳到下一个版本。
      • 下一个版本的数据内容是‘赵云’,该版本的trx_id值也为200,在m_ids范围内,所以不符合可见性要求,跳到下一个版本。
      • 下一个版本的数据内容是‘张飞’,该版本的trx_id值为100,在m_ids范围内,所以不符合可见性要求,跳到下一个版本。
      • 下一个版本的数据内容是‘关羽’,该版本的trx_id值为100,在m_ids范围内,所以不符合可见性要求,跳到下一个版本。
      • 下一个版本的数据内容是‘刘备’,该版本的trx_id值为800,小于m_ids最小值id100,所以符合可见性要求。
    • 最后返回的是刘备的数据。

    也就是说两次 SELECT 查询得到的结果是重复的,记录的列 c 值都是 '刘备' ,这就是 可重复读 的含义。

    如果我们之后再把事务id为 200 的记录提交了,之后再到刚才使用 REPEATABLE READ 隔离级别的事务中继续查找这个id为 1 的记录,得到的结果还是 '刘备' ,具体执行过程大家可以自己分析一下。

    注意:MVCC只在RR和RC两个隔离级别下工作。RU和串行化隔离级别不需要 MVCC,为什么?

    • 因为RU总是读取最新的数据行,本身没有隔离性,也不解决并发潜在问题,因此不需要。
    • serialzable则会对所有读取的行加锁,相当于串形执行,线程之间绝对隔离,也不需要。

    3.4 MVCC下的读操作

    在MVCC并发控制中,读操作可以分为两类:快照读(Snapshot Read) 和当前读(Current Read)

    • 快照读:读取的是记录的可见版本(有可能是历史版本),不用加锁。刚才所有的案例都是快照读。
    • 当前读:读取的是记录最新版本,并且当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。

    3.4.1 当前读和快照读

    快照读也就是一致性非锁定读,是指InnoDB存储引擎通过多版本控制(MVCC)读取当前数据库中行数据的方式。如果读取的行正在执行delete或update操作,这时读操作不会因此去等待行上锁的释放。相反的,InnoDb会去读取行的一个最新可见快照。ReadView的读取操作就是快照读。

    举例:

    • 快照读:简单的select操作,属于快照读,不加锁
    select * from table where ?;
    
    • 当前读:特殊的读操作,插入、更新、删除操作,属于当前读,需要加锁。
    select * from table where ? lock in share mode; #加读锁
    select * from table where ? for update; #加写锁
    
    insert into table values (...); #加写锁
    update table set ? where ?; #加写锁
    delete from table where ?; #加写锁
    
    # 所有以上的语句,都属于当前读,读取记录的最新版本。并且读取之后,还需要保证其他并发事务不能修改当前记录,对读取数据加锁。
    # 除了第一条语句,对读取记录加读锁外,其他操作都是加的写锁。
    

    3.4.2 案例:当前读

    BEGIN;
    
    # SELECT1:Transaction 100、200未提交
    
    SELECT * FROM tab_user WHERE id = 1; # 得到的列name的值为'刘备'
    
    # SELECT2:Transaction 100提交,Transaction 200未提交
    
    SELECT * FROM tab_user WHERE id = 1; # 得到的列name的值为'张飞'
    
    select * from tab_user where id=1 lock in share mode; # 当前读
    如果查询这条数据的时候,有别的事务在操作该数据,后等到提交修改事务后,再读取最新的结果。
    
    COMMIT;
    

    3.4.3 一个CRUD的CUD操作的具体过程

    update table set ? where ?;

    Mysql(事务篇)- 四大特性、隔离级别、MVCC(Undo log + ReadView)-17

    从图中可以看出:
    当update sql 被发给Mysql后,

    • 首先,Mysql会根据where条件,读取第一条满足条件的记录,然后Innodb引擎会将第一条记录返回,并加锁(current read)。
    • 待Mysql Server收到这条加锁的记录后,再发起一个update请求,更新这条数据
    • 一个记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,update操作内部,就包含了一个当前读。

    同理,delete操作也一样。Insert操作会稍微有些不同,简单来说,insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。

    根据上图的交互,针对一条当前读的SQL语句,InnoDB与Mysql Server的交互,是一条一条进行的,因此,加锁也是一条一条进行的。先对一条满足条件的记录加锁,返回给mysql Server,做一些DML操作。然后再读取下一条加锁,直至读取完毕。

    3.5 小结

    • MVCC指在使用RC、RR隔离级别下,使用不同事务的读-写写-读操作并发执行,提高系统性能
    • MVCC的核心思想是 读不加锁、读写不冲突
    • RC、RR这两个隔离级别的一个很大不同就是生成ReadView的时机不同
      • RC在每一次进行普通select操作前都会生成一个ReadView
      • RR在第一次进行普通select操作前生成一个ReadView,之后的查询操作都会重复使用这个ReadView

    相关文章

    Oracle如何使用授予和撤销权限的语法和示例
    Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
    下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
    社区版oceanbase安装
    Oracle 导出CSV工具-sqluldr2
    ETL数据集成丨快速将MySQL数据迁移至Doris数据库

    发布评论