news 2025/12/25 14:49:40

【源码分析】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/23 4:02:17

写在分库分表之前:真的走到这一步了吗?

引言人是为了活着本身而活着的&#xff0c;而不是为了活着之外的任何事物所活着。 数据库也是如此&#xff0c;它本该安静地存着数据、吐着数据&#xff0c;而不是被业务增长的野心折腾得喘不过气来。在写项目时&#xff0c;一道思考题拦住了我&#xff1a; “随着公司业务快速…

作者头像 李华
网站建设 2025/12/23 1:07:02

青少年编程等级考试怎么选?看认证、内容与便利性

青少年编程等级考试怎么选&#xff1f;看认证、内容与便利性 内容概要 青少年编程学习路径可分为兴趣启蒙、系统进阶与综合实践等阶段&#xff0c;需注重学习的系统性。选择编程能力评价项目时&#xff0c;可关注其主办方的权威性、标准体系的清晰度以及科目覆盖的完整性。编…

作者头像 李华
网站建设 2025/12/23 2:52:36

深入剖析TCP拥塞控制机制及其在高性能网络中的优化实践

【精选优质专栏推荐】 《AI 技术前沿》 —— 紧跟 AI 最新趋势与应用《网络安全新手快速入门(附漏洞挖掘案例)》 —— 零基础安全入门必看《BurpSuite 入门教程(附实战图文)》 —— 渗透测试必备工具详解《网安渗透工具使用教程(全)》 —— 一站式工具手册《CTF 新手入门实战教…

作者头像 李华
网站建设 2025/12/23 4:32:30

2025程序员转行大模型全攻略:百万年薪岗位揭秘,零基础突围

本文解析2025年大模型领域的爆发机遇&#xff0c;指出市场需求激增、薪资远超传统岗位、政策资本双重驱动。文章为程序员提供四大黄金岗位选择、三大转型策略和六个月学习路线图&#xff0c;强调技能嫁接、技术栈组合和微项目实践。同时提醒避开盲目死磕数学、忽视垂直领域知识…

作者头像 李华
网站建设 2025/12/23 5:30:20

讲透2025AI营销,只此一人

2025年&#xff0c;AI浪潮以前所未有的速度席卷商业世界&#xff0c;营销领域首当其冲。从高层管理者到一线执行&#xff0c;几乎所有营销从业者都陷入了一种普遍的“AI焦虑”&#xff1a;一方面&#xff0c;大家深知拥抱AI营销是不可逆转的趋势&#xff0c;是构建未来竞争力的…

作者头像 李华
网站建设 2025/12/17 16:51:07

堆转储 探索

一、什么是堆转储&#xff08;Heap Dump&#xff09;&#xff1f;堆转储&#xff08;Heap Dump&#xff09; 是 JVM 在某一时刻 整个堆内存的快照&#xff0c;以 .hprof 文件形式保存。它包含&#xff1a;所有存活对象的实例对象的类信息对象之间的引用关系对象占用的内存大小&…

作者头像 李华