开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(十三):DML之INSERTIGNORE流程

本文涉及的产品
云原生数据库 PolarDB MySQL 版,通用型 2核8GB 50GB
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
简介: 开源分布式数据库PolarDB-X源码解读——PolarDB-X源码解读(十三):DML之INSERTIGNORE流程

作者:潜璟


在上一篇源码阅读中,我们介绍了INSERT的执行流程。而INSERT IGNORE与INSERT不同,需要对插入值判断是否有Unique Key的冲突,并忽略有冲突的插入值。因此本文将进一步介绍PolarDB-X中INSERT IGNORE的执行流程,其根据插入的表是否有GSI也有所变化。  


一、下推执行


如果插入的表只有一张主表,没有GSI,那么只需要将INSERT IGNORE直接发送到对应的物理表上,由DN自行忽略存在冲突的值。在这种情况下,INSERT IGNORE的执行过程和INSERT基本上相同,读者可以参考之前的源码阅读文章。


二、逻辑执行


而在有GSI的情况下,就不能简单地将INSERT IGNORE分别下发到主表和GSI对应的物理分表上,否则有可能出现主表和GSI数据不一致的情况。举个例子:  


create table t1 (a int primary key, b int, global index g1(b) dbpartition by hash(b)) dbpartition by hash(a);
 insert ignore into t1 values (1,1),(1,2);

对于插入的两条记录,它们在主表上位于同一个物理表(a相同),但是在GSI上位于不同的物理表(b不相同),如果直接下发INSERT IGNORE的话,主表上只有(1,1)能够成功插入(主键冲突),而在GSI上(1,1)和(1,2)都能成功插入,于是GSI比主表多了一条数据。


针对这种情况,一种解决方案是根据插入值中的Unique Key,先到数据库中SELECT出有可能冲突的数据到CN,然后在CN判断冲突的值并删除。  


进行SELECT的时候,最简单的方式就是将所有的SELECT直接发送到主表上,但是主表上可能没有对应的Unique Key,这就导致SELECT的时候会进行全表扫描,影响性能。所以在优化器阶段,我们会根据Unique Key是在主表还是GSI上定义的来确定相应的SELECT需要发送到主表还是GSI,具体代码位置:


com.alibaba.polardbx.optimizer.core.planner.rule.OptimizeLogicalInsertRule#groupUkByTable  


protected Map>> 
groupUkByTable(LogicalInsertIgnore insertIgnore, 
                                                                                                                                      ExecutionContext 
 executionContext) { 
                                                // 找到每个 Unique Key 在主表和哪些 GSI 中存在           
                                     Map> ukAllTableMap = new HashMap<>();       
                                                           for (int i = 0; i < uniqueKeys.size(); i++) {             
                                                        List uniqueKey = uniqueKeys.get(i);            
                                                    for (Map.Entry>> e : writableTableUkMap.entrySet()) { 
                                                     String currentTableName = e.getKey().toUpperCase();
                                                          Map> currentUniqueKeys = 
e.getValue(); 
                                                                   boolean found = false; 
                                                             for (Set currentUniqueKey : 
currentUniqueKeys.values()) { 
                                                if (currentUniqueKey.size() != uniqueKey.size()) {
                                                    continue;          
                                                 }
                                                 boolean match = 
currentUniqueKey.containsAll(uniqueKey); 
                                                  if (match) { 
                                           found = true; 
                                           break; 
                                         } 
                                    } 
                                    if (found) { 
                                     ukAllTableMap.computeIfAbsent(i, k -> new 
ArrayList<>()).add(currentTableName); 
                                     } 
                                } 
                            } 
    // 确定是在哪一个表上进行 SELECT 
            for (Map.Entry> e : 
  ukAllTableMap.entrySet()) { 
                      List tableNames = e.getValue(); 
                 if (tableNames.contains(primaryTableName.toUpperCase())) { 
tableUkMap.computeIfAbsent(primaryTableName.toUpperCase(), k -> new ArrayList<>())
                               .add(uniqueKeys.get(e.getKey())); 
                 } else { 
                            final boolean onlyNonPublicGsi = 
                                     tableNames.stream().noneMatch(tn -> 
GlobalIndexMeta.isPublished(executionContext, sm.getTable(tn))); 
                                boolean found = false; 
                            for (String tableName : tableNames) { 
                            if (!onlyNonPublicGsi && 
GlobalIndexMeta.isPublished(executionContext, 
sm.getTable(tableName))) {
                                                                             tableUkMap.computeIfAbsent(tableName, k -> new
ArrayList<>()).add(uniqueKeys.get(e.getKey()));
                                               found = true; 
                                               break; 
                                       } else if (onlyNonPublicGsi && 
GlobalIndexMeta.canWrite(executionContext, sm.getTable(tableName)))
{
                                                                            tableUkMap.computeIfAbsent(tableName, k -> new 
ArrayList<>()).add(uniqueKeys.get(e.getKey())); 
                                                 found = true; 
                                                 break; 
                                              }
                                         }
                                   }
                                } 
        return tableUkMap; 
    }


而到了执行阶段,我们在LogicalInsertIgnoreHandler中处理INSERT IGNORE。我们首先会进入getDuplicatedValues函数,其通过下发SELECT的方式查找表中已有的冲突的Unique Key的记录。我们将下发的SELECT语句中选择的列设置为(value_index, uk_index, pk)。其中value_index和uk_index均为的常量。  


举个例子,假设有表:


CREATE TABLE `t` ( 
    `id` int(11) NOT NULL, 
    `a` int(11) NOT NULL, 
    `b` int(11) NOT NULL, 
    PRIMARY KEY (`id`), 
    UNIQUE GLOBAL KEY `g_i_a` (`a`) COVERING (`id`) DBPARTITION BY HASH(`a`) 
) DBPARTITION BY HASH(`id`)

以及一条INSERT IGNORE语句:


INSERT IGNORE INTO t VALUES (1,2,3),(2,3,4),(3,4,5);


假设在PolarDB-X中执行时,其会将Unique Key编号为:


0: id 
1: g_i_a


INSERT IGNORE语句中插入的每个值分别编号为:


0: (1,2,3) 
1: (2,3,4) 
2: (3,4,5)


那么对于(2,3,4)的UNIQUE KEY构造的GSI上的SELECT即为:


# 查询 GSI 
SELECT 1 as `value_index`, 1 as `uk_index`, `id` 
FROM `g_i_a_xxxx` 
WHERE `a` in 3;


假设表中已经存在(5,3,6),那么这条SELECT的返回结果即为(1,1,5)。此外,由于不同的Unique Key的SELECT返回格式是相同的,所以我们会将同一个物理库上不同的SELECT查询UNION起来发送,以一次性得到多个结果,减少CN和DN之间的交互次数。只要某个Unique Key有重复值,我们就能根据value_index和uk_index确定是插入值的哪一行的哪个Unique Key是重复的。


当得到所有的返回结果之后,我们对数据进行去重。我们将上一步得到的冲突的的值放入一个SET中,然后顺序扫描所有的每一行插入值,如果发现有重复的就跳过该行,否则就将该行也加入到SET中(因为插入值之间也有可能存在相互冲突)。去重完毕之后,我们就得到了所有不存在冲突的值,将这些值插入到表中之后就完成了一条INSERT IGNORE的执行。


逻辑执行的执行流程:


com.alibaba.polardbx.repo.mysql.handler.LogicalInsertIgnoreHandler#doExecute
protected int doExecute(LogicalInsert insert, ExecutionContext executionContext, 
                            LogicalInsert.HandlerParams handlerParams) { 
        // ... 
        try { 
            Map>> ukGroupByTable = 
insertIgnore.getUkGroupByTable(); 
            List> deduplicated; 
            List> duplicateValues; 
            // 获取表中已有的 Unique Key 冲突值 
            duplicateValues = getDuplicatedValues(insertIgnore, 
LockMode.SHARED_LOCK, executionContext, ukGroupByTable,
                (rowCount) -> memoryAllocator.allocateReservedMemory( 
                    MemoryEstimator.calcSelectValuesMemCost(rowCount, 
selectRowType)), selectRowType, true,
                handlerParams); 
            final List> batchParameters
                executionContext.getParams().getBatchParameters(); 
            // 根据上一步得到的结果,去掉 INSERT IGNORE 中的冲突值 
            deduplicated = 
getDeduplicatedParams(insertIgnore.getUkColumnMetas(), 
insertIgnore.getBeforeUkMapping(),
                insertIgnore.getAfterUkMapping(),
RelUtils.getRelInput(insertIgnore), duplicateValues,
                batchParameters, executionContext); 
           if (!deduplicated.isEmpty()) { 
                insertEc.setParams(new Parameters(deduplicated)); 
rams(new Parameters(deduplicated)); 
            } else { 
                // All duplicated 
                return affectRows; 
            } 
            // 执行 INSERT 
            try { 
                if (gsiConcurrentWrite) { 
                    affectRows = concurrentExecute(insertIgnore,
insertEc);
                } else { 
                    affectRows = sequentialExecute(insertIgnore, 
insertEc);
                } else { 
                    affectRows = sequentialExecute(insertIgnore,
insertEc);
                } 
            } catch (Throwable e) { 
                handleException(executionContext, e, 
GeneralUtil.isNotEmpty(insertIgnore.getGsiInsertWriters()));
            } 
        } finally { 
            selectValuesPool.destroy(); 
        } 
        return affectRows; 
    }


三、RETURNING优化


上一节提到的INSERT IGNORE的逻辑执行方式,虽然保证了数据的正确性,但是也使得一条INSERT IGNORE语句至少需要CN和DN的两次交互才能完成(第一次SELECT,第二次INSERT),影响了INSERT IGNORE的执行性能。  


目前的DN已经支持了AliSQL的RETURNING优化,其可以在DN的INSERT IGNORE执行完毕之后返回成功插入的值。利用这一功能,PolarDB-X对INSERT IGNORE进行了进一步的优化:直接将INSERT IGNORE下发,如果在主表和GSI上全部成功返回,那么就说明插入值中没有冲突,于是就成功完成该条INSERT IGNORE的执行;否则就将多插入的值删除。


执行时,CN首先会根据上文中的语法下发带有RETURNING的物理INSERT IGNORE语句到DN,比如:


call dbms_trans.returning("a", "insert into t1_xxxx values(1,1)");


其中返回列是主键,用来标识插入的一批数据中哪些被成功插入了;t1_xxxx是逻辑表t1的一个物理分表。当主表和GSI上的所有INSERT IGNORE执行完毕之后,我们计算主表和GSI中成功插入值的交集作为最后的结果,然后删除多插入的值。这部分代码在


com.alibaba.polardbx.repo.mysql.handler.LogicalInsertIgnoreHandler#getRowsToBeRemoved


private Map>> getRowsToBeRemoved(String tableName, 
                                                                                                                    Map
List>> tableInsertedValues, 
                                                                                                                   List 
beforePkMapping,
List pkColumnMetas) { 
         final Map> tableInsertedPks = new 
TreeMap<>(String.CASE_INSENSITIVE_ORDER);
        final Map>>> 
tablePkRows =
            new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 
        tableInsertedValues.forEach((tn, insertedValues) -> { 
            final Set insertedPks = new TreeSet<>(); 
            final List>> pkRows = new 
ArrayList<>();
            for (List inserted : insertedValues) { 
                final Object[] groupKeys =
beforePkMapping.stream().map(inserted::get).toArray();
                final GroupKey pk = new GroupKey(groupKeys,
pkColumnMetas);
                insertedPks.add(pk); 
                pkRows.add(Pair.of(pk, inserted)); 
            } 
            tableInsertedPks.put(tn, insertedPks); 
            tablePkRows.put(tn, pkRows); 
        }); 
        // Get intersect of inserted values 
        final Set distinctPks = new TreeSet<>();
        for (GroupKey pk : tableInsertedPks.get(tableName)) { 
            if (tableInsertedPks.values().stream().allMatch(pks -> 
pks.contains(pk))) { 
                distinctPks.add(pk); 
            }
         } 
        // Remove values which not exists in at least one insert results 
        final Map>> tableDeletePks = new
TreeMap<>(String.CASE_INSENSITIVE_ORDER); 
        tablePkRows.forEach((tn, pkRows) -> { 
            final List> deletePks = new ArrayList<>(); 
            pkRows.forEach(pkRow -> { 
                if (!distinctPks.contains(pkRow.getKey())) { 
                    deletePks.add(pkRow.getValue()); 
                }
             }); 
            if (!deletePks.isEmpty()) {
                 tableDeletePks.put(tn, deletePks); 
            }
         }); 
        return tableDeletePks;
     }

与上一节的逻辑执行的“悲观执行”相比,使用RETURNING优化的INSERT IGNORE相当于“乐观执行”,如果插入的值本身没有冲突,那么一条INSERT IGNORE语句CN和DN间只需要一次交互即可;而在有冲突的情况下,我们需要下发DELETE语句将主表或GSI中多插入的值删除,于是CN和DN间需要两次交互。可以看出,即便是有冲突的情况,CN和DN间的交互次数也不会超过上一节的逻辑执行。因此在无法直接下推的情况下,INSERT IGNORE的执行策略是默认使用RETURNING优化执行。


当然RETURNING优化的使用也有一些限制,比如插入的Value有重复主键时就不能使用,因为这种情况下无法判断具体是哪一行被成功插入,哪一行需要删除;具体可以阅读代码中的条件判断。当不能使用RETURNING优化时,系统会自动选择上一节中的逻辑执行方式执行该条INSERT IGNORE语句以保证数据的正确性。


使用RETURNING优化的执行流程:


com.alibaba.polardbx.repo.mysql.handler.LogicalInsertIgnoreHandler#doExecute


protected int doExecute(LogicalInsert insert, ExecutionContext executionContext, 
                            LogicalInsert.HandlerParams handlerParams) { 
         // ... 
        // 判断能否使用 RETURNING 优化
         boolean canUseReturning = 
executorContext.getStorageInfoManager().supportsReturning() && executionContext.getParamManager() 
                .getBoolean(ConnectionParams.DML_USE_RETURNING) && 
allDnUseXDataSource && gsiCanUseReturning
                 && !isBroadcast 
&& !ComplexTaskPlanUtils.canWrite(tableMeta); 
        if (canUseReturning) {
             canUseReturning = noDuplicateValues(insertIgnore, insertEc);
         } 
if (canUseReturning) {
            // 执行 INSERT IGNORE 并获得返回结果 
            final List allPhyPlan = 
                new ArrayList<>(replaceSeqAndBuildPhyPlan(insertIgnore, 
insertEc, handlerParams)); 
getPhysicalPlanForGsi(insertIgnore.getGsiInsertIgnoreWriters(),
 insertEc, allPhyPlan); 
            final Map>> tableInsertedValues =
                 executeAndGetReturning(executionContext, allPhyPlan,
 insertIgnore, insertEc, memoryAllocator,
                     selectRowType); 
            // ... 
           // 生成 DELETE
             final boolean removeAllInserted =
                 targetTableNames.stream().anyMatch(tn
 -> !tableInsertedValues.containsKey(tn)); 
            if (removeAllInserted) {
                 affectedRows -=
                     removeInserted(insertIgnore, schemaName, tableName,
 isBroadcast, insertEc, tableInsertedValues);
                 if (returnIgnored) {
                    ignoredRows = totalRows; 
                }
             } else {
                 final List beforePkMapping =
 insertIgnore.getBeforePkMapping();
                 final List pkColumnMetas =
 insertIgnore.getPkColumnMetas(); 
                // 计算所有插入值的交集
                 final Map>> tableDeletePks =
                     getRowsToBeRemoved(tableName, tableInsertedValues,
 beforePkMapping, pkColumnMetas); 
                affectedRows -=
                     removeInserted(insertIgnore, schemaName, tableName,
 isBroadcast, insertEc, tableDeletePks);
                 if (returnIgnored) {
                     ignoredRows += 
Optional.ofNullable(tableDeletePks.get(insertIgnore.getLogicalTable
Name())).map(List::size)
                             .orElse(0);
                 }
              }
              handlerParams.optimizedWithReturning = true;
              if (returnIgnored) {
                 return ignoredRows;
             } else {
                 return affectedRows;
             }
         } else {             
handlerParams.optimizedWithReturning = false;
         } 
      // ...
      }

最后以一个例子来展现RETURNING优化的执行流程与逻辑执行的不同。通过/*+TDDL:CMD_EXTRA(DML_USE_RETURNING=TRUE)*/这条HINT,用户可以手动控制是否使RETURNING优化。


首先建表并插入一条数据:


CREATE TABLE `t` (
     `id` int(11) NOT NULL,
     `a` int(11) NOT NULL,
     `b` int(11) NOT NULL,
     PRIMARY KEY (`id`),
     UNIQUE GLOBAL KEY `g_i_a` (`a`) COVERING (`id`) DBPARTITION BY HASH(`a`)
 ) DBPARTITION BY HASH(`id`);
  INSERT INTO t VALUES (1,3,3);


再执行一条INSERT IGNORE:


INSERT IGNORE INTO t VALUES (1,2,3),(2,3,4),(3,4,5);


其中(1,2,3)与(1,3,3)主键冲突,(2,3,4)与(1,3,3)对于Unique Key g_i_a冲突。如果是RETURNING优化:


         

                                                       截屏2022-08-08 10.16.25


可以看到PolarDB-X先进行了INSERT IGNORE,再将多插入的数据删除:(1,2,3)在主表上冲突在UGSI上成功插入,(2,3,4)在UGSI上冲突在主表上成功插入,因此分别下发对应的DELETE到UGSI和主表上。


如果关闭RETURNING优化,逻辑执行:


         

                                                    截屏2022-08-08 10.18.07


可以看到PolarDB-X先进行了SELECT,再将没有冲突的数据(3,4,5)插入。


四、小结


本文介绍了PolarDB-X中INSERT IGNORE的执行流程。除了INSERT IGNORE之外,还有一些DML语句在执行时也需要进行重复值的判断,比如REPLACE、INSERT ON DUPLICATE KEY UPDATE等,这些语句在有GSI的情况下均采用了逻辑执行的方式,即先进行SELECT再进行判重、更新等操作,感兴趣的读者可以自行阅读相关代码。


相关实践学习
快速体验PolarDB开源数据库
本实验环境已内置PostgreSQL数据库以及PolarDB开源数据库:PolarDB PostgreSQL版和PolarDB分布式版,支持一键拉起使用,方便各位开发者学习使用。
相关文章
|
6天前
|
存储 关系型数据库 分布式数据库
PolarDB 开源基础教程系列 8 数据库生态
PolarDB是一款开源的云原生分布式数据库,源自阿里云商业产品。为降低使用门槛,PolarDB携手伙伴打造了完整的开源生态,涵盖操作系统、芯片、存储、集成管控、监控、审计、开发者工具、数据同步、超融合计算、ISV软件、开源插件、人才培养、社区合作及大型用户合作等领域。通过这些合作伙伴,PolarDB提供了丰富的功能和服务,支持多种硬件和软件环境,满足不同用户的需求。更多信息请访问[PolarDB开源官方网站](https://openpolardb.com/home)。
38 4
|
30天前
|
存储 关系型数据库 分布式数据库
PolarDB PostgreSQL版:商业数据库替换与企业上云首选
PolarDB PostgreSQL版是商业数据库替换与企业上云的首选。其技术架构实现存储计算分离,具备极致弹性和扩展性,支持Serverless、HTAP等特性。产品在弹性、性能、成本优化和多模处理方面有显著提升,如冷热数据自动分层、Ganos多模引擎等。已在汽车、交通、零售等行业成功应用,典型案例包括小鹏汽车、中远海科等,帮助企业大幅降低运维成本并提高业务效率。
46 13
|
30天前
|
容灾 关系型数据库 分布式数据库
PolarDB分布式版:与云融合的分布式数据库发展新阶段
PolarDB分布式版标志着分布式数据库与云融合的新阶段。它经历了三个发展阶段:从简单的分布式中间件,到一体化分布式架构,再到云原生分布式数据库。PolarDB充分利用云资源的弹性、高性价比、高可用性和隔离能力,解决了大规模数据扩展性问题,并支持多租户场景和复杂事务处理。零售中台的建设背景包括国家数字化转型战略及解决信息孤岛问题,采用分布式数据库提升高可用性和性能,满足海量订单处理需求。展望未来,零售中台将重点提升容灾能力、优化资源利用并引入AI技术,以实现更智能的服务和更高的业务连续性。
|
1月前
|
关系型数据库 分布式数据库 数据库
瑶池数据库大讲堂|PolarDB HTAP:为在线业务插上实时分析的翅膀
瑶池数据库大讲堂介绍PolarDB HTAP,为在线业务提供实时分析能力。内容涵盖MySQL在线业务的分析需求与现有解决方案、PolarDB HTAP架构优化、针对分析型负载的优化(如向量化执行、多核并行处理)及近期性能改进和用户体验提升。通过这些优化,PolarDB HTAP实现了高效的数据处理和查询加速,帮助用户更好地应对复杂业务场景。
|
30天前
|
运维 关系型数据库 分布式数据库
阿里云PolarDB:引领云原生数据库创新发展
阿里云PolarDB引领云原生数据库创新,2024云栖大会将分享其最新发展及在游戏行业的应用。PolarDB凭借弹性、高可用性、多写技术等优势,支持全球80多个站点,服务1万多家企业。特别是针对游戏行业,PolarDB助力Funplus等公司实现高效运维、成本优化和业务扩展。通过云原生能力,PolarDB推动游戏业务的全球化部署与快速响应,提升用户体验并保障数据安全。未来,PolarDB将继续探索AI、多云管理等前沿技术,为用户提供更智能的数据基础设施。
|
1月前
|
关系型数据库 Serverless 分布式数据库
瑶池数据库微课堂 | PolarDB Serverless弹性&价格力观测
瑶池数据库微课堂介绍阿里云PolarDB Serverless的弹性与性价比优势。通过瑶池解决方案体验馆,用户可免费实操,直观感受Serverless的秒级弹性及超高性价比。内容涵盖Serverless概念、操作步骤、压测演示及性能曲线分析,展示PolarDB在不同负载下的自动扩展能力。适合希望了解云数据库弹性和成本效益的技术人员。
|
1月前
|
关系型数据库 OLAP 分布式数据库
瑶池数据库微课堂|PolarDB/RDS+ADB Zero-ETL:一种免费、易用、高效的数据同步方式
瑶池数据库微课堂介绍阿里云PolarDB/RDS与ADB的Zero-ETL功能,实现免费、易用、高效的数据同步。内容涵盖OLTP与OLAP的区别、传统ETL存在的问题及Zero-ETL的优势(零成本、高效同步),并演示了从RDS MySQL到AnalyticDB MySQL的具体操作步骤。未来将优化和迭代此功能,提供更好的用户体验。
|
1月前
|
关系型数据库 MySQL 分布式数据库
[PolarDB实操课] 05.通过源码部署PolarDB-X标准版
本课程介绍如何通过源码部署PolarDB-X标准版,涵盖基于Paxos的MySQL三副本工作原理和技术特点。主要内容包括: 1. **Paxos三副本工作原理**:讲解Leader和Follower节点的角色及数据同步机制。 2. **技术特点**:强调高性能、数据不丢失(RPO=0)和自动HA切换。 3. **源码部署步骤**:详细演示从编译生成RPM包到启动DN节点的过程,包括配置my.cnf文件和初始化数据库。 4. **高可用体验**:通过三台机器模拟三副本集群,展示Leader选举和故障转移机制,确保数据一致性和服务可用性。
|
1月前
|
关系型数据库 编译器 分布式数据库
PolarDB实操课] 04.通过源码部署PolarDB-X企业版
本次课程由PolarDB开源架构师王江颖分享,详细介绍了通过源码部署PolarDB-X企业版的全过程。主要内容包括: 1. **编译基础** 2. **使用源码编译部署PolarDB-X企业版** 3. **演示实例**:通过阿里云ECS进行实际操作演示,从创建用户、赋予权限到最终启动并连接PolarDB-X数据库,展示了完整的部署过程。 4. **总结**
|
9月前
|
安全 druid Java
Seata 1.8.0 正式发布,支持达梦和 PolarDB-X 数据库
Seata 1.8.0 正式发布,支持达梦和 PolarDB-X 数据库
686 10
Seata 1.8.0 正式发布,支持达梦和 PolarDB-X 数据库

相关产品

  • 云原生数据库 PolarDB