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

本文涉及的产品
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
云原生数据库 PolarDB MySQL 版,通用型 2核8GB 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再进行判重、更新等操作,感兴趣的读者可以自行阅读相关代码。


相关文章
|
6月前
|
人工智能 安全 Java
智慧工地源码,Java语言开发,微服务架构,支持分布式和集群部署,多端覆盖
智慧工地是“互联网+建筑工地”的创新模式,基于物联网、移动互联网、BIM、大数据、人工智能等技术,实现对施工现场人员、设备、材料、安全等环节的智能化管理。其解决方案涵盖数据大屏、移动APP和PC管理端,采用高性能Java微服务架构,支持分布式与集群部署,结合Redis、消息队列等技术确保系统稳定高效。通过大数据驱动决策、物联网实时监测预警及AI智能视频监控,消除数据孤岛,提升项目可控性与安全性。智慧工地提供专家级远程管理服务,助力施工质量和安全管理升级,同时依托可扩展平台、多端应用和丰富设备接口,满足多样化需求,推动建筑行业数字化转型。
228 5
|
NoSQL 安全 调度
【📕分布式锁通关指南 10】源码剖析redisson之MultiLock的实现
Redisson 的 MultiLock 是一种分布式锁实现,支持对多个独立的 RLock 同时加锁或解锁。它通过“整锁整放”机制确保所有锁要么全部加锁成功,要么完全回滚,避免状态不一致。适用于跨多个 Redis 实例或节点的场景,如分布式任务调度。其核心逻辑基于遍历加锁列表,失败时自动释放已获取的锁,保证原子性。解锁时亦逐一操作,降低死锁风险。MultiLock 不依赖 Lua 脚本,而是封装多锁协调,满足高一致性需求的业务场景。
187 0
【📕分布式锁通关指南 10】源码剖析redisson之MultiLock的实现
|
7月前
|
安全
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
Redisson 的看门狗机制是解决分布式锁续期问题的核心功能。当通过 `lock()` 方法加锁且未指定租约时间时,默认启用 30 秒的看门狗超时时间。其原理是在获取锁后创建一个定时任务,每隔 1/3 超时时间(默认 10 秒)通过 Lua 脚本检查锁状态并延长过期时间。续期操作异步执行,确保业务线程不被阻塞,同时仅当前持有锁的线程可成功续期。锁释放时自动清理看门狗任务,避免资源浪费。学习源码后需注意:避免使用带超时参数的加锁方法、控制业务执行时间、及时释放锁以优化性能。相比手动循环续期,Redisson 的定时任务方式更高效且安全。
425 24
【📕分布式锁通关指南 07】源码剖析redisson利用看门狗机制异步维持客户端锁
|
7月前
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
本文深入剖析了Redisson中可重入锁的释放锁Lua脚本实现及其获取锁的两种方式(阻塞与非阻塞)。释放锁流程包括前置检查、重入计数处理、锁删除及消息发布等步骤。非阻塞获取锁(tryLock)通过有限时间等待返回布尔值,适合需快速反馈的场景;阻塞获取锁(lock)则无限等待直至成功,适用于必须获取锁的场景。两者在等待策略、返回值和中断处理上存在显著差异。本文为理解分布式锁实现提供了详实参考。
266 11
【📕分布式锁通关指南 08】源码剖析redisson可重入锁之释放及阻塞与非阻塞获取
|
6月前
|
存储 安全 NoSQL
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
本文深入解析了 Redisson 中公平锁的实现原理。公平锁通过确保线程按请求顺序获取锁,避免“插队”现象。在 Redisson 中,`RedissonFairLock` 类的核心逻辑包含加锁与解锁两部分:加锁时,线程先尝试直接获取锁,失败则将自身信息加入 ZSet 等待队列,只有队首线程才能获取锁;解锁时,验证持有者身份并减少重入计数,最终删除锁或通知等待线程。其“公平性”源于 Lua 脚本的原子性操作:线程按时间戳排队、仅队首可尝试加锁、实时发布锁释放通知。这些设计确保了分布式环境下的线程安全与有序执行。
194 0
【📕分布式锁通关指南 09】源码剖析redisson之公平锁的实现
|
7月前
|
NoSQL Java Redis
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁
本文详细解析了Redisson可重入锁的加锁流程。首先从`RLock.lock()`方法入手,通过获取当前线程ID并调用`tryAcquire`尝试加锁。若加锁失败,则订阅锁释放通知并循环重试。核心逻辑由Lua脚本实现:检查锁是否存在,若不存在则创建并设置重入次数为1;若存在且为当前线程持有,则重入次数+1。否则返回锁的剩余过期时间。此过程展示了Redisson高效、可靠的分布式锁机制。
236 0
【📕分布式锁通关指南 06】源码剖析redisson可重入锁之加锁
|
9月前
|
关系型数据库 MySQL 分布式数据库
[PolarDB实操课] 05.通过源码部署PolarDB-X标准版
本课程介绍如何通过源码部署PolarDB-X标准版,涵盖基于Paxos的MySQL三副本工作原理和技术特点。主要内容包括: 1. **Paxos三副本工作原理**:讲解Leader和Follower节点的角色及数据同步机制。 2. **技术特点**:强调高性能、数据不丢失(RPO=0)和自动HA切换。 3. **源码部署步骤**:详细演示从编译生成RPM包到启动DN节点的过程,包括配置my.cnf文件和初始化数据库。 4. **高可用体验**:通过三台机器模拟三副本集群,展示Leader选举和故障转移机制,确保数据一致性和服务可用性。
321 1
|
9月前
|
关系型数据库 编译器 分布式数据库
PolarDB实操课] 04.通过源码部署PolarDB-X企业版
本次课程由PolarDB开源架构师王江颖分享,详细介绍了通过源码部署PolarDB-X企业版的全过程。主要内容包括: 1. **编译基础** 2. **使用源码编译部署PolarDB-X企业版** 3. **演示实例**:通过阿里云ECS进行实际操作演示,从创建用户、赋予权限到最终启动并连接PolarDB-X数据库,展示了完整的部署过程。 4. **总结**
244 0
|
6月前
|
关系型数据库 分布式数据库 数据库
一库多能:阿里云PolarDB三大引擎、四种输出形态,覆盖企业数据库全场景
PolarDB是阿里云自研的新一代云原生数据库,提供极致弹性、高性能和海量存储。它包含三个版本:PolarDB-M(兼容MySQL)、PolarDB-PG(兼容PostgreSQL及Oracle语法)和PolarDB-X(分布式数据库)。支持公有云、专有云、DBStack及轻量版等多种形态,满足不同场景需求。2021年,PolarDB-PG与PolarDB-X开源,内核与商业版一致,推动国产数据库生态发展,同时兼容主流国产操作系统与芯片,获得权威安全认证。
|
23天前
|
Cloud Native 关系型数据库 MySQL
免费体验!高效实现自建 MySQL 数据库平滑迁移至 PolarDB-X
PolarDB-X 是阿里云推出的云原生分布式数据库,支持PB级存储扩展、高并发访问与数据强一致,助力企业实现MySQL平滑迁移。现已开放免费体验,点击即享高效、稳定的数据库升级方案。
免费体验!高效实现自建 MySQL 数据库平滑迁移至 PolarDB-X

相关产品

  • 云原生数据库 PolarDB