news 2025/12/15 18:37:27

【源码分析】StarRocks TRUNCATE 语句执行流程:从 SQL 到数据清空的完整旅程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【源码分析】StarRocks TRUNCATE 语句执行流程:从 SQL 到数据清空的完整旅程

文章目录

    • 本文内容一览(快速理解)
    • 一、什么是 TRUNCATE(Truncate Table):理解清空表数据的本质
    • 二、TRUNCATE 的执行流程(Execution Flow):从 SQL 到数据清空的 7 个阶段
      • 2.1 完整执行链路(Complete Execution Chain):7 个阶段的旅程
      • 2.2 阶段1-3:SQL 解析到权限检查(SQL Parsing to Authorization):快速验证阶段
      • 2.4 阶段4:执行入口(Execution Entry):路由到元数据操作
      • 2.3 阶段5:元数据操作(Metadata Operations):核心执行阶段
        • 2.3.1 子阶段1:读锁检查阶段(Read Lock Check Phase):快速验证
        • 2.3.2 子阶段2:创建新分区阶段(Create New Partitions Phase):无锁操作
        • 2.3.3 子阶段3:写锁替换阶段(Write Lock Replace Phase):关键阻塞点
          • 替换分区(Replace Partitions)
          • EditLog 写入(EditLog Write):关键阻塞点
      • 2.5 阶段6-7:持久化和同步(Persistence and Synchronization):确保数据一致性
    • 三、锁机制深度解析(Lock Mechanism):为什么会被阻塞
      • 3.1 数据库锁类型(Database Lock Types):读锁和写锁的区别
      • 3.2 TRUNCATE 锁持有时间线(Lock Holding Timeline):理解阻塞过程
      • 3.3 锁竞争场景(Lock Contention Scenarios):实际影响分析
    • 四、性能瓶颈分析(Performance Bottleneck Analysis):找出问题根源
      • 4.1 各阶段耗时统计(Stage Time Statistics):找出慢的地方
      • 4.2 性能瓶颈分析(Performance Bottleneck Analysis):三大瓶颈
    • 五、优化建议(Optimization Recommendations):如何避免问题
      • 5.1 短期优化(Short-term Optimization):不修改核心逻辑
      • 5.2 长期优化(Long-term Optimization):需要代码修改
    • 📝 本章总结

📌适合对象:StarRocks 开发者、运维人员、对数据库内部机制感兴趣的初学者
⏱️预计阅读时间:40-50分钟
🎯学习目标:理解 TRUNCATE 语句在 StarRocks 中的完整执行流程,掌握锁机制和性能瓶颈


第一步
理解 TRUNCATE 是什么
(清空表数据)
第二步
了解执行流程的7个阶段
(重点)
第三步
理解锁机制
(为什么会被阻塞)
第四步
认识性能瓶颈
(EditLog 写入)
第五步
掌握优化方法
(如何避免问题)

本文内容一览(快速理解)

  1. TRUNCATE 的本质:清空表数据,通过创建新分区替换旧分区实现
  2. 执行流程:从 SQL 解析到数据清空,经历 7 个关键阶段
  3. 锁机制:使用数据库级别的写锁,在等待持久化时阻塞其他操作
  4. 性能瓶颈:EditLog 写入需要 1-5 秒,期间一直持有写锁
  5. 优化方向:降低频率、使用分区表、异步写入等

一、什么是 TRUNCATE(Truncate Table):理解清空表数据的本质

这一章要建立的基础:理解 TRUNCATE 语句的作用和实现原理

核心问题:当我们执行TRUNCATE TABLE db.tbl时,StarRocks 内部到底发生了什么?


[!NOTE]
📝 关键点总结:TRUNCATE 不是删除数据,而是用新的空分区替换旧分区,这样速度更快

概念的本质

TRUNCATE 是数据库提供的一种快速清空表数据的方法。与 DELETE 不同,TRUNCATE 不是逐行删除数据,而是通过替换分区的方式实现清空。

图解说明

旧分区
包含数据
创建新分区
(空分区)
替换操作
用新分区替换旧分区
结果:表被清空
但结构保留

💡说明:TRUNCATE 的优势是速度快,因为它不需要逐行删除数据,而是直接替换整个分区

实际例子

-- 清空整个表TRUNCATETABLEmy_db.user_table;-- 只清空指定分区TRUNCATETABLEmy_db.user_tablePARTITION(p20251210);

二、TRUNCATE 的执行流程(Execution Flow):从 SQL 到数据清空的 7 个阶段

核心问题:一条 TRUNCATE SQL 语句是如何一步步执行完成的?


[!NOTE]
📝 关键点总结:TRUNCATE 执行分为 7 个阶段,其中第 5 阶段的写锁替换是最关键的阻塞点

2.1 完整执行链路(Complete Execution Chain):7 个阶段的旅程

流程概览

阶段1:SQL 解析
解析语法树
阶段2:语义分析
验证表名和分区
阶段3:权限检查
验证用户权限
阶段4:执行入口
路由到元数据操作
阶段5:元数据操作
(核心阶段)
阶段6:EditLog 持久化
写入 BDBJE
阶段7:BE 节点同步
同步到后端节点
5.1 读锁检查
检查表信息
5.2 创建新分区
(无锁操作)
5.3 写锁替换
(阻塞点)

各阶段耗时统计

阶段操作锁类型耗时是否阻塞
1. SQL 解析语法解析< 1ms
2. 语义分析表名规范化< 1ms
3. 权限检查权限验证< 1ms
4. 读锁检查表信息检查读锁1-10ms
5. 创建分区创建新分区无锁100-500ms
6. 写锁替换替换分区写锁1-5秒
7. EditLog 写入BDBJE 持久化写锁持有1-5秒

💡说明:阶段 6 和 7 是性能瓶颈,因为需要等待 BDBJE 写入完成,期间一直持有写锁

实际例子

假设执行TRUNCATE TABLE my_db.orders PARTITION(p20251210)

时间轴: T0 (0ms): 开始执行 TRUNCATE T1 (1ms): SQL 解析完成 T2 (2ms): 语义分析完成 T3 (3ms): 权限检查通过 T4 (10ms): 读锁检查完成,确认分区存在 T5 (300ms): 创建新分区完成(无锁,不阻塞) T6 (310ms): 获取写锁,开始替换分区 T7 (350ms): 分区替换完成 T8 (350ms): 开始写入 EditLog T9 (3350ms):EditLog 写入完成(等待了3秒!) T10 (3400ms):释放写锁 T11 (3400ms):完成

可以看到,在 T8 到 T9 这 3 秒期间,写锁一直被持有,其他操作都被阻塞。

2.2 阶段1-3:SQL 解析到权限检查(SQL Parsing to Authorization):快速验证阶段

阶段1:SQL 解析(SQL Parsing)

文件位置fe/fe-core/src/main/java/com/starrocks/sql/parser/AstBuilder.java

关键源码

@OverridepublicParseNodevisitTruncateTableStatement(StarRocksParser.TruncateTableStatementContextcontext){QualifiedNamequalifiedName=getQualifiedName(context.qualifiedName());TableNametargetTableName=qualifiedNameToTableName(qualifiedName);Tokenstart=context.start;Tokenstop=context.stop;PartitionNamespartitionNames=null;if(context.partitionNames()!=null){stop=context.partitionNames().stop;partitionNames=(PartitionNames)visit(context.partitionNames());}NodePositionpos=createPos(start,stop);returnnewTruncateTableStmt(newTableRef(targetTableName,null,partitionNames,pos));}

AST 节点结构

// 文件位置:fe/fe-core/src/main/java/com/starrocks/sql/ast/TruncateTableStmt.javapublicclassTruncateTableStmtextendsDdlStmt{privatefinalTableReftblRef;// 包含表名和分区信息publicTableRefgetTblRef(){returntblRef;}publicStringgetDbName(){returntblRef.getName().getDb();}publicStringgetTblName(){returntblRef.getName().getTbl();}}

功能说明

  • 解析 SQL 语法树,提取表名和分区信息
  • 创建TruncateTableStmtAST 节点
  • 支持两种格式:
    • TRUNCATE TABLE db.tbl(清空整个表)
    • TRUNCATE TABLE db.tbl PARTITION(p1, p2)(清空指定分区)

阶段2:语义分析(Semantic Analysis)

文件位置fe/fe-core/src/main/java/com/starrocks/sql/analyzer/TruncateTableAnalyzer.java

关键源码

publicstaticvoidanalyze(TruncateTableStmtstatement,ConnectContextcontext){// 1. 规范化表名(处理大小写、默认数据库等)MetaUtils.normalizationTableName(context,statement.getTblRef().getName());// 2. 检查是否使用别名(不支持)if(statement.getTblRef().hasExplicitAlias()){thrownewSemanticException("Not support truncate table with alias");}// 3. 检查分区信息PartitionNamespartitionNames=statement.getTblRef().getPartitionNames();if(partitionNames!=null){// 不支持清空临时分区if(partitionNames.isTemp()){thrownewSemanticException("Not support truncate temp partitions");}// 检查分区名是否为空if(partitionNames.getPartitionNames().stream().anyMatch(entity->Strings.isNullOrEmpty(entity))){thrownewSemanticException("there are empty partition name");}}}

调用路径

// 文件位置:fe/fe-core/src/main/java/com/starrocks/sql/analyzer/AnalyzerVisitor.java@OverridepublicVoidvisitTruncateTableStatement(TruncateTableStmtstatement,ConnectContextcontext){TruncateTableAnalyzer.analyze(statement,context);returnnull;}

功能说明

  • 规范化表名(处理大小写、默认数据库)
  • 验证语法约束(不支持别名、不支持临时分区)
  • 验证分区名有效性

阶段3:权限检查(Authorization)

文件位置fe/fe-core/src/main/java/com/starrocks/sql/analyzer/AuthorizerStmtVisitor.java

关键源码

@OverridepublicVoidvisitTruncateTableStatement(TruncateTableStmtstatement,ConnectContextcontext){// 检查用户是否有 TRUNCATE 权限Authorizer.checkTableAction(context.getCurrentUserIdentity(),context.getCurrentRoleIds(),statement.getDbName(),statement.getTblName(),PrivilegeType.DELETE);returnnull;}

功能说明

  • 验证用户是否有表的 DELETE 权限(TRUNCATE 使用 DELETE 权限)
  • 如果权限不足,抛出AccessDeniedException

2.4 阶段4:执行入口(Execution Entry):路由到元数据操作

文件位置fe/fe-core/src/main/java/com/starrocks/qe/DDLStmtExecutor.java

关键源码

@OverridepublicShowResultSetvisitTruncateTableStatement(TruncateTableStmtstmt,ConnectContextcontext){ErrorReport.wrapWithRuntimeException(()->{context.getGlobalStateMgr().truncateTable(stmt);});returnnull;}

调用链

// 文件位置:fe/fe-core/src/main/java/com/starrocks/server/GlobalStateMgr.javapublicvoidtruncateTable(TruncateTableStmttruncateTableStmt)throwsDdlException{localMetastore.truncateTable(truncateTableStmt);}

功能说明

  • 将执行委托给GlobalStateMgr,再转发到LocalMetastore
  • 使用ErrorReport.wrapWithRuntimeException包装异常

2.3 阶段5:元数据操作(Metadata Operations):核心执行阶段

核心问题:如何在不影响数据一致性的前提下,快速清空表数据?


2.3.1 子阶段1:读锁检查阶段(Read Lock Check Phase):快速验证

操作流程

获取读锁
db.readLock()
检查表是否存在
检查表类型
(只支持 OLAP/LAKE 表)
检查表状态
(必须是 NORMAL)
收集分区信息
创建表的影子副本
释放读锁
db.readUnlock()

关键操作

  1. 获取读锁db.readLock()- 数据库级别的读锁(共享锁)
  2. 验证表状态:检查表是否存在、类型是否支持、状态是否正常
  3. 收集分区信息:根据是否指定分区,收集需要清空的分区列表
  4. 创建影子副本:创建表的副本,用于后续创建新分区
  5. 释放读锁db.readUnlock()

锁持有时间:通常 1-10ms,不会阻塞其他读操作

关键源码

文件位置fe/fe-core/src/main/java/com/starrocks/server/LocalMetastore.java
代码位置LocalMetastore.java:4495-4531

// 1. 获取数据库读锁(检查阶段)db.readLock();try{Tabletable=db.getTable(dbTbl.getTbl());if(table==null){ErrorReport.reportDdlException(ErrorCode.ERR_BAD_TABLE_ERROR,dbTbl.getTbl());}// 只支持 OLAP 表或 LAKE 表if(!table.isOlapOrCloudNativeTable()){thrownewDdlException("Only support truncate OLAP table or LAKE table");}OlapTableolapTable=(OlapTable)table;if(olapTable.getState()!=OlapTable.OlapTableState.NORMAL){throwInvalidOlapTableStateException.of(olapTable.getState(),olapTable.getName());}// 收集需要清空的分区信息if(!truncateEntireTable){// 清空指定分区for(StringpartName:tblRef.getPartitionNames().getPartitionNames()){Partitionpartition=olapTable.getPartition(partName);if(partition==null){thrownewDdlException("Partition "+partName+" does not exist");}origPartitions.put(partName,partition);GlobalStateMgr.getCurrentState().getAnalyzeMgr().recordDropPartition(partition.getId());}}else{// 清空整个表的所有分区for(Partitionpartition:olapTable.getPartitions()){origPartitions.put(partition.getName(),partition);GlobalStateMgr.getCurrentState().getAnalyzeMgr().recordDropPartition(partition.getId());}}// 创建表的影子副本(用于后续创建新分区)copiedTbl=getShadowCopyTable(olapTable);}finally{db.readUnlock();// 释放读锁}

实际例子

这段代码展示了读锁检查阶段的完整流程,包括表存在性检查、类型验证、状态检查、分区信息收集和影子副本创建。

2.3.2 子阶段2:创建新分区阶段(Create New Partitions Phase):无锁操作

操作流程

遍历旧分区
生成新分区ID
复制分区属性
(存储介质、副本数等)
创建新分区
构建分区结构
(创建 Tablet、索引)
完成

关键操作

  1. 生成新分区ID:为每个要清空的分区生成新的分区ID
  2. 复制分区属性:从旧分区复制存储介质、副本数、数据属性等配置
  3. 创建新分区:调用createPartition()创建新分区
  4. 构建分区结构:调用buildPartitions()创建 Tablet 和索引结构
  5. 错误处理:如果创建失败,清理已创建的 Tablet

特点

  • 无锁操作:此阶段不持有任何锁,不会阻塞其他操作
  • 耗时较长:创建分区和 Tablet 需要 100-500ms
  • 可回滚:如果失败,会清理已创建的资源

实际例子

假设要清空 3 个分区:

时间轴: T0: 开始创建新分区(无锁) T1 (50ms): 创建分区1完成 T2 (150ms): 创建分区2完成 T3 (300ms): 创建分区3完成,所有新分区创建完成

在这 300ms 期间,其他操作可以正常进行,不会被阻塞。

关键源码

文件位置fe/fe-core/src/main/java/com/starrocks/server/LocalMetastore.java
代码位置LocalMetastore.java:4533-4566

// 2. 使用影子副本创建新分区(无锁操作)List<Partition>newPartitions=Lists.newArrayListWithCapacity(origPartitions.size());Set<Long>tabletIdSet=Sets.newHashSet();try{for(Map.Entry<String,Partition>entry:origPartitions.entrySet()){longoldPartitionId=entry.getValue().getId();longnewPartitionId=getNextId();// 生成新的分区IDStringnewPartitionName=entry.getKey();// 复制分区属性(存储介质、副本数、数据属性等)PartitionInfopartitionInfo=copiedTbl.getPartitionInfo();partitionInfo.setTabletType(newPartitionId,partitionInfo.getTabletType(oldPartitionId));partitionInfo.setIsInMemory(newPartitionId,partitionInfo.getIsInMemory(oldPartitionId));partitionInfo.setReplicationNum(newPartitionId,partitionInfo.getReplicationNum(oldPartitionId));partitionInfo.setDataProperty(newPartitionId,partitionInfo.getDataProperty(oldPartitionId));if(copiedTbl.isCloudNativeTable()){partitionInfo.setDataCacheInfo(newPartitionId,partitionInfo.getDataCacheInfo(oldPartitionId));}copiedTbl.setDefaultDistributionInfo(entry.getValue().getDistributionInfo());// 创建新分区PartitionnewPartition=createPartition(db,copiedTbl,newPartitionId,newPartitionName,null,tabletIdSet);newPartitions.add(newPartition);}// 构建分区(创建 Tablet、索引等)buildPartitions(db,copiedTbl,newPartitions.stream().map(Partition::getSubPartitions).flatMap(p->p.stream()).collect(Collectors.toList()));}catch(DdlExceptione){// 如果创建失败,清理已创建的 TabletdeleteUselessTablets(tabletIdSet);throwe;}

这段代码展示了如何创建新分区:生成新分区ID、复制分区属性、创建分区结构,以及错误处理机制。

2.3.3 子阶段3:写锁替换阶段(Write Lock Replace Phase):关键阻塞点

操作流程

获取写锁
db.writeLock()
再次检查表状态
(防止表被删除)
检查分区是否变化
检查元数据是否变化
替换分区
(核心操作)
更新 Colocation 信息
写入 EditLog
(阻塞点)
刷新物化视图
释放写锁
db.writeUnlock()

关键操作详解

替换分区(Replace Partitions)

文件位置fe/fe-core/src/main/java/com/starrocks/server/LocalMetastore.java
代码位置LocalMetastore.java:4670-4702

关键源码

privatevoidtruncateTableInternal(OlapTableolapTable,List<Partition>newPartitions,booleanisEntireTable,booleanisReplay){// 使用新分区替换旧分区Set<Tablet>oldTablets=Sets.newHashSet();for(PartitionnewPartition:newPartitions){PartitionoldPartition=olapTable.replacePartition(newPartition);// ← 替换操作for(PhysicalPartitionphysicalPartition:oldPartition.getSubPartitions()){// 收集旧 Tablet 用于后续删除for(MaterializedIndexindex:physicalPartition.getMaterializedIndices(MaterializedIndex.IndexExtState.ALL)){// let HashSet do the deduplicate workoldTablets.addAll(index.getTablets());}}}if(isEntireTable){// 如果是清空整个表,删除所有临时分区olapTable.dropAllTempPartitions();}// 从 InvertedIndex 中删除旧 Tabletfor(Tablettablet:oldTablets){TabletInvertedIndexindex=GlobalStateMgr.getCurrentInvertedIndex();index.deleteTablet(tablet.getId());// 确保只有 Leader FE 记录 truncate 信息if(!isReplay){index.markTabletForceDelete(tablet);}}}

功能说明

  • 使用新创建的空分区替换旧分区
  • 收集旧 Tablet 并标记删除
  • 如果是清空整个表,删除所有临时分区
EditLog 写入(EditLog Write):关键阻塞点

文件位置fe/fe-core/src/main/java/com/starrocks/server/LocalMetastore.java
代码位置LocalMetastore.java:4568-4656

写锁替换阶段完整源码

// 3. 获取数据库写锁(关键操作阶段)db.writeLock();// ← 关键:数据库级别的写锁try{// 3.1 再次检查表状态(防止在创建分区期间表被删除或修改)OlapTableolapTable=(OlapTable)db.getTable(copiedTbl.getId());if(olapTable==null){thrownewDdlException("Table["+copiedTbl.getName()+"] is dropped");}if(olapTable.getState()!=OlapTable.OlapTableState.NORMAL){throwInvalidOlapTableStateException.of(olapTable.getState(),olapTable.getName());}// 3.2 检查分区是否发生变化for(Map.Entry<String,Partition>entry:origPartitions.entrySet()){Partitionpartition=olapTable.getPartition(entry.getValue().getId());if(partition==null||!partition.getName().equalsIgnoreCase(entry.getKey())){thrownewDdlException("Partition ["+entry.getKey()+"] is changed during truncating table, "+"please retry");}}// 3.3 检查元数据是否发生变化(Schema、索引等)booleanmetaChanged=false;if(olapTable.getIndexNameToId().size()!=copiedTbl.getIndexNameToId().size()){metaChanged=true;}else{// 比较 SchemaHashMap<Long,Integer>copiedIndexIdToSchemaHash=copiedTbl.getIndexIdToSchemaHash();for(Map.Entry<Long,Integer>entry:olapTable.getIndexIdToSchemaHash().entrySet()){longindexId=entry.getKey();if(!copiedIndexIdToSchemaHash.containsKey(indexId)){metaChanged=true;break;}if(!copiedIndexIdToSchemaHash.get(indexId).equals(entry.getValue())){metaChanged=true;break;}}}if(olapTable.getDefaultDistributionInfo().getType()!=copiedTbl.getDefaultDistributionInfo().getType()){metaChanged=true;}if(metaChanged){thrownewDdlException("Table["+copiedTbl.getName()+"]'s meta has been changed. try again.");}// 3.4 替换分区(核心操作)truncateTableInternal(olapTable,newPartitions,truncateEntireTable,false);// 3.5 更新 Colocation 信息try{colocateTableIndex.updateLakeTableColocationInfo(olapTable,true/* isJoin */,null/* expectGroupId */);}catch(DdlExceptione){LOG.info("table {} update colocation info failed when truncate table, {}",olapTable.getId(),e.getMessage());}// 3.6 写入 EditLog(阻塞点)TruncateTableInfoinfo=newTruncateTableInfo(db.getId(),olapTable.getId(),newPartitions,truncateEntireTable);GlobalStateMgr.getCurrentState().getEditLog().logTruncateTable(info);// ← 阻塞等待 BDBJE 写入// 3.7 刷新物化视图Set<MvId>relatedMvs=olapTable.getRelatedMaterializedViews();for(MvIdmvId:relatedMvs){MaterializedViewmaterializedView=(MaterializedView)getTable(mvId.getDbId(),mvId.getId());if(materializedView==null){LOG.warn("Table related materialized view {}.{} can not be found",mvId.getDbId(),mvId.getId());continue;}if(materializedView.isLoadTriggeredRefresh()){DatabasemvDb=getDb(mvId.getDbId());refreshMaterializedView(mvDb.getFullName(),getTable(mvDb.getId(),mvId.getId()).getName(),false,null,Constants.TaskRunPriority.NORMAL.value(),true,false);}}}catch(DdlExceptione){deleteUselessTablets(tabletIdSet);throwe;}catch(MetaNotFoundExceptione){LOG.warn("Table related materialized view can not be found",e);}finally{db.writeUnlock();// 释放写锁}

EditLog 写入源码

文件位置fe/fe-core/src/main/java/com/starrocks/persist/EditLog.java

logTruncateTable 方法EditLog.java:1789-1791):

publicvoidlogTruncateTable(TruncateTableInfoinfo){logEdit(OperationType.OP_TRUNCATE_TABLE,info);}

logEdit 方法EditLog.java:1243-1246):

protectedvoidlogEdit(shortop,Writablewritable){JournalTasktask=submitLog(op,writable,-1);waitInfinity(task);// ← 阻塞等待 BDBJE 写入完成}

waitInfinity 方法EditLog.java:1299-1324):关键阻塞点

publicstaticvoidwaitInfinity(JournalTasktask){longstartTimeNano=task.getStartTimeNano();booleanresult;intcnt=0;while(true){try{if(cnt!=0){Thread.sleep(1000);// 失败后等待1秒重试}// 等待 JournalWriter 写入完成result=task.get();// ← 阻塞等待break;}catch(InterruptedException|ExecutionExceptione){LOG.warn("failed to wait, wait and retry {} times..: {}",cnt,e);cnt++;}}assert(result);if(MetricRepo.hasInit){MetricRepo.HISTO_EDIT_LOG_WRITE_LATENCY.update((System.nanoTime()-startTimeNano)/1000000);}}

阻塞机制分析

TRUNCATE线程EditLog任务队列JournalWriter线程BDBJE存储logTruncateTable(info)提交日志任务waitInfinity() 阻塞等待取出任务写入 BDBJE写入完成通知完成继续执行TRUNCATE线程EditLog任务队列JournalWriter线程BDBJE存储

关键问题

  • 写锁持有时间长:在等待 BDBJE 写入期间,一直持有数据库写锁
  • 阻塞所有读操作:写锁持有期间,所有需要读锁的操作(如 ReportHandler)都被阻塞
  • BDBJE 写入耗时:正常情况下 1-5 秒,高负载时可能更长

实际例子

时间轴: T0: TRUNCATE 获取写锁 T1: 替换分区完成(50ms) T2: 开始写入 EditLog T3: 等待 BDBJE 写入...(3秒) T4: BDBJE 写入完成 T5: 释放写锁 在这 3 秒期间(T2-T4),写锁一直被持有!

2.5 阶段6-7:持久化和同步(Persistence and Synchronization):确保数据一致性

阶段6:EditLog 持久化(EditLog Persistence)

文件位置fe/fe-core/src/main/java/com/starrocks/journal/bdbje/

TruncateTableInfo 数据结构

文件位置fe/fe-core/src/main/java/com/starrocks/persist/TruncateTableInfo.java

publicclassTruncateTableInfoimplementsWritable{@SerializedName(value="dbId")privatelongdbId;// 数据库ID@SerializedName(value="tblId")privatelongtblId;// 表ID@SerializedName(value="partitions")privateList<Partition>partitions;// 新分区列表@SerializedName(value="isEntireTable")privatebooleanisEntireTable;// 是否清空整个表publicTruncateTableInfo(longdbId,longtblId,List<Partition>partitions,booleanisEntireTable){this.dbId=dbId;this.tblId=tblId;this.partitions=partitions;this.isEntireTable=isEntireTable;}@Overridepublicvoidwrite(DataOutputout)throwsIOException{Stringjson=GsonUtils.GSON.toJson(this);// 序列化为 JSONText.writeString(out,json);}}

流程说明

  1. JournalWriter 线程:从队列中取出日志任务
  2. 序列化:将TruncateTableInfo序列化为 JSON
  3. BDBJE 写入:写入 Berkeley DB Java Edition(持久化存储)
  4. 同步等待:等待写入完成(同步写入)
  5. 回调通知:通知等待的线程

阶段7:BE 节点同步(Backend Node Synchronization)

回放方法源码

文件位置fe/fe-core/src/main/java/com/starrocks/server/LocalMetastore.java
代码位置LocalMetastore.java:4704-4730

publicvoidreplayTruncateTable(TruncateTableInfoinfo){Databasedb=getDb(info.getDbId());db.writeLock();try{OlapTableolapTable=(OlapTable)db.getTable(info.getTblId());truncateTableInternal(olapTable,info.getPartitions(),info.isEntireTable(),true);if(!GlobalStateMgr.isCheckpointThread()){// 将新 Tablet 添加到 InvertedIndexTabletInvertedIndexinvertedIndex=GlobalStateMgr.getCurrentInvertedIndex();for(Partitionpartition:info.getPartitions()){longpartitionId=partition.getId();TStorageMediummedium=olapTable.getPartitionInfo().getDataProperty(partitionId).getStorageMedium();for(PhysicalPartitionphysicalPartition:partition.getSubPartitions()){for(MaterializedIndexmIndex:physicalPartition.getMaterializedIndices(MaterializedIndex.IndexExtState.ALL)){// 添加 Tablet 到索引// ...}}}}}finally{db.writeUnlock();}}

流程说明

  1. EditLog 回放:Follower FE 节点回放 EditLog
  2. 元数据同步:BE 节点通过心跳获取元数据变更
  3. Tablet 清理:BE 节点删除旧 Tablet 的数据文件

三、锁机制深度解析(Lock Mechanism):为什么会被阻塞

这一章要建立的基础:理解 StarRocks 的锁机制,明白为什么 TRUNCATE 会阻塞其他操作

核心问题:为什么 TRUNCATE 执行时,其他操作会被阻塞?


[!NOTE]
📝 关键点总结:TRUNCATE 使用数据库级别的写锁,在等待持久化时一直持有锁,导致其他操作被阻塞

3.1 数据库锁类型(Database Lock Types):读锁和写锁的区别

锁类型

StarRocks 使用两种类型的锁:

  • 读锁(ReadLock)db.readLock()- 共享锁,多个读操作可以并发
  • 写锁(WriteLock)db.writeLock()- 排他锁,独占访问

锁实现源码

文件位置fe/fe-core/src/main/java/com/starrocks/catalog/Database.java

publicclassDatabaseextendsMetaObject{privatefinalReentrantReadWriteLockrwLock=newReentrantReadWriteLock(true);publicvoidreadLock(){longstartMs=TimeUnit.MILLISECONDS.convert(System.nanoTime(),TimeUnit.NANOSECONDS);StringthreadDump=getOwnerInfo(rwLock.getOwner());this.rwLock.sharedLock();// 获取共享锁(读锁)logSlowLockEventIfNeeded(startMs,"readLock",threadDump);}publicvoidwriteLock(){longstartMs=TimeUnit.MILLISECONDS.convert(System.nanoTime(),TimeUnit.NANOSECONDS);StringthreadDump=getOwnerInfo(rwLock.getOwner());this.rwLock.exclusiveLock();// 获取排他锁(写锁)logSlowLockEventIfNeeded(startMs,"writeLock",threadDump);}publicvoidreadUnlock(){this.rwLock.sharedUnlock();}publicvoidwriteUnlock(){this.rwLock.exclusiveUnlock();}}

图解说明

读锁 ReadLock
共享锁
多个读操作
可以同时进行
写锁 WriteLock
排他锁
独占访问
阻塞所有其他操作

实际例子

// 读锁:多个操作可以同时获取线程1:db.readLock()// 获取读锁线程2:db.readLock()// 也可以获取读锁(共享)线程3:db.readLock()// 也可以获取读锁(共享)// 三个线程可以同时读取// 写锁:独占访问线程1:db.writeLock()// 获取写锁线程2:db.readLock()// 被阻塞,必须等待线程1释放写锁线程3:db.writeLock()// 被阻塞,必须等待线程1释放写锁

3.2 TRUNCATE 锁持有时间线(Lock Holding Timeline):理解阻塞过程

时间线分析

000 ms000 ms000 ms000 ms000 ms000 ms000 ms000 ms000 ms000 ms000 ms读锁检查创建新分区获取写锁替换分区写入EditLog刷新物化视图释放写锁读锁阶段无锁阶段写锁阶段(阻塞)TRUNCATE 锁持有时间线

关键发现

  • 写锁持有时间:从获取写锁到释放,约 1-5 秒
  • 阻塞时间:EditLog 写入期间(1-5秒),一直持有写锁
  • 阻塞影响:写锁持有期间,所有读锁操作被阻塞

实际例子

时间轴: T0: 开始执行 TRUNCATE T1: 获取读锁 (db.readLock) T2: 检查表信息 (1-10ms) T3: 释放读锁 (db.readUnlock) T4: 创建新分区 (无锁,100-500ms) T5: 获取写锁 (db.writeLock) ← 关键点 T6: 替换分区 (10-50ms) T7: 写入 EditLog (logTruncateTable) T8: 等待 BDBJE 写入 (1-5秒) ← 阻塞点 T9: BDBJE 写入完成 T10: 刷新物化视图 (可选,100-500ms) T11: 释放写锁 (db.writeUnlock) T12: 完成

3.3 锁竞争场景(Lock Contention Scenarios):实际影响分析

场景1:TRUNCATE + ReportHandler

时间线

TRUNCATE线程ReportHandler线程数据库锁获取写锁尝试获取读锁(被阻塞)等待 BDBJE 写入(1-5秒)释放写锁获取读锁成功TRUNCATE线程ReportHandler线程数据库锁

结果:ReportHandler 被阻塞 1-5 秒,可能导致 BE 心跳超时

场景2:多个 TRUNCATE 并发

时间线

000 ms000 ms000 ms000 ms000 ms000 ms000 ms000 ms000 ms000 ms000 ms执行等待执行TRUNCATE1TRUNCATE2多个 TRUNCATE 串行执行

结果:多个 TRUNCATE 串行执行,总耗时 = N × (1-5秒)

实际例子

假设有 3 个 TRUNCATE 操作:

TRUNCATE1: 0-3秒(持有写锁) TRUNCATE2: 3-6秒(等待 TRUNCATE1,然后执行) TRUNCATE3: 6-9秒(等待 TRUNCATE2,然后执行) 总耗时:9秒(串行执行)

四、性能瓶颈分析(Performance Bottleneck Analysis):找出问题根源

这一章要建立的基础:理解 TRUNCATE 的性能瓶颈,知道哪些地方可以优化

核心问题:为什么 TRUNCATE 会阻塞其他操作?主要瓶颈在哪里?


[!NOTE]
📝 关键点总结:EditLog 写入是主要瓶颈,在持有写锁期间等待 BDBJE 写入完成,阻塞所有读操作

4.1 各阶段耗时统计(Stage Time Statistics):找出慢的地方

耗时对比表

阶段操作平均耗时最大耗时是否可优化
SQL 解析语法解析< 1ms< 5ms
语义分析表名规范化< 1ms< 5ms
读锁检查表信息检查1-10ms50ms
创建分区创建新分区100-500ms2秒是(异步)
写锁替换替换分区10-50ms200ms
EditLog 写入BDBJE 持久化1-5秒10秒+是(异步)
刷新物化视图MV 刷新100-500ms2秒是(异步)

可视化分析

80%15%5%各阶段耗时占比(典型情况)EditLog 写入创建分区其他阶段

💡说明:EditLog 写入占总耗时的 80%,是主要瓶颈

4.2 性能瓶颈分析(Performance Bottleneck Analysis):三大瓶颈

瓶颈1:EditLog 写入阻塞(EditLog Write Blocking)

问题

  • 在持有写锁期间等待 BDBJE 写入完成
  • 阻塞所有读操作 1-5 秒

优化方向

  • 方案1:异步写入 EditLog(需要处理一致性)
  • 方案2:优化 BDBJE 写入性能(硬件、配置)
  • 方案3:减少 EditLog 写入频率(批量写入)

瓶颈2:创建分区耗时(Partition Creation Time)

问题

  • 创建分区和 Tablet 需要 100-500ms
  • 虽然无锁,但增加总耗时

优化方向

  • 方案1:预创建分区池
  • 方案2:优化 Tablet 创建逻辑

瓶颈3:锁粒度(Lock Granularity)

问题

  • 使用数据库级别的写锁,不是表级别
  • 同一数据库下的所有操作竞争同一把锁

优化方向

  • 方案1:改为表级别锁(需要大量重构)
  • 方案2:使用更细粒度的锁(分区级别)

五、优化建议(Optimization Recommendations):如何避免问题

这一章要建立的基础:掌握优化 TRUNCATE 性能的方法,避免阻塞问题

核心问题:如何优化 TRUNCATE 操作,减少对系统的影响?


[!NOTE]
📝 关键点总结:优化方向包括降低频率、使用分区表、增加超时配置、异步写入等

5.1 短期优化(Short-term Optimization):不修改核心逻辑

方案1:降低 TRUNCATE 频率

方法

  • 错开执行时间
  • 使用队列控制并发

实际例子

# 将 200 个任务分散到不同时间点# 例如:每 30 秒执行一个任务# 200 个任务 × 30 秒 = 6000 秒 = 100 分钟

方案2:增加超时配置

配置项

  • catalog_try_lock_timeout_ms = 30000(数据库锁超时时间)
  • thrift_rpc_timeout_ms = 30000(Thrift RPC 超时时间)

方案3:使用分区表

优势

  • 只清空需要的分区
  • 减少锁持有时间

实际例子

-- 推荐:只清空需要的分区TRUNCATETABLEordersPARTITION(p20251210);-- 不推荐:清空整个表TRUNCATETABLEorders;

5.2 长期优化(Long-term Optimization):需要代码修改

方案1:异步 EditLog 写入

思路

  • 在替换分区后立即释放写锁
  • 异步写入 EditLog
  • 需要处理一致性问题

方案2:表级别锁

思路

  • 将数据库级别锁改为表级别锁
  • 需要大量重构

方案3:批量 EditLog 写入

思路

  • 将多个操作合并为一个 EditLog
  • 减少 BDBJE 写入次数

📝 本章总结

核心要点回顾

  1. TRUNCATE 的本质:通过创建新分区替换旧分区实现快速清空
  2. 执行流程:7 个阶段,其中写锁替换阶段是关键阻塞点
  3. 锁机制:使用数据库级别的写锁,在等待持久化时阻塞其他操作
  4. 性能瓶颈:EditLog 写入耗时 1-5 秒,占总耗时的 80%
  5. 优化方向:降低频率、使用分区表、异步写入等

知识地图

TRUNCATE 语句
SQL 解析
语义分析
权限检查
元数据操作
读锁检查
创建新分区
写锁替换
EditLog 写入
(瓶颈)
BE 节点同步

关键决策点

  • 是否使用 TRUNCATE:如果需要快速清空表,TRUNCATE 比 DELETE 快
  • 频率控制:单个数据库建议 ≤ 1-2 次/分钟
  • 分区策略:使用分区表,只清空需要的分区
  • 超时配置:根据实际情况调整超时时间
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/11 21:57:55

python如何获取字符串最后一个字符

在Python中获取字符串的最后一个字符有多种方法&#xff0c;以下是最常用且高效的方式&#xff1a; 方法1&#xff1a;使用负数索引&#xff08;推荐&#xff09; s "hello" last_char s[-1] # 输出 o原理&#xff1a;Python支持负数索引&#xff0c;-1 表示倒数第…

作者头像 李华
网站建设 2025/12/11 21:57:34

赋能个体,智创全球——CCF 程序员大会“个人出海论坛”圆满落幕

2025年12月5日&#xff0c;大理 —— 在 AI 技术重塑全球生产力的当下&#xff0c;出海不再是巨头的专属游戏。本次 CCF 程序员大会特别设立了“个人出海论坛”&#xff0c;聚焦个体开发者如何利用 AI 杠杆撬动全球市场。论坛由 MTPark 创始人、大理数字游民社区主理人熊腾焱担…

作者头像 李华
网站建设 2025/12/11 21:56:36

零基础学CMD:从关机命令开始的Windows命令行入门

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 设计一个交互式CMD命令学习工具&#xff0c;以关机命令为教学案例。功能包括&#xff1a;1) 基础关机命令分步教学 2) 命令参数互动解释&#xff08;如/s、/f、/t的含义&#xff09…

作者头像 李华
网站建设 2025/12/11 21:56:20

Wan2.2-T2V-A14B与Runway Gen-3的技术差异全面对比

Wan2.2-T2V-A14B与Runway Gen-3的技术差异全面对比 在影视工业的剪辑室里&#xff0c;导演盯着屏幕上一段AI生成的预演视频轻声说&#xff1a;“这动作……像个人&#xff0c;但又不像真人。” 而在另一端&#xff0c;一位独立艺术家正用手机输入“赛博朋克猫在雨夜弹吉他”&am…

作者头像 李华
网站建设 2025/12/11 21:53:50

为什么90%的工程师写不好Agentic Apps配置?Docker Compose权威解析

第一章&#xff1a;Agentic Apps与Docker Compose融合架构在现代云原生应用开发中&#xff0c;Agentic Apps 代表了一类具备自主决策能力的智能代理系统&#xff0c;它们能够感知环境、执行任务并与其他服务协同工作。将此类应用与 Docker Compose 结合&#xff0c;可实现多容器…

作者头像 李华
网站建设 2025/12/11 21:53:49

【Azure量子开发权威解析】:掌握这8个考点,轻松拿下MCP认证

第一章&#xff1a;MCP Azure 量子开发认证概述Azure 量子开发认证&#xff08;Microsoft Certified: Azure Quantum Developer Associate&#xff0c;简称 MCP Azure 量子开发认证&#xff09;是微软为开发者设计的专业资格认证&#xff0c;旨在验证其在 Azure Quantum 平台上…

作者头像 李华