记录下吧 JS 瀑布流

博客提供了Wookmark-jQuery项目在GitHub上的链接,该项目可能与jQuery相关技术有关,可通过此链接进一步了解项目详情。
https://github.com/GBKS/Wookmark-jQuery
Delphi 12.3 作为一款面向 Windows 平台的集成开发环境,由 Embarcadero Technologies 负责其持续演进。该环境以 Object Pascal 语言为核心,并依托 Visual Component Library(VCL)框架,广泛应用于各类桌面软件、数据库系统及企业级解决方案的开发。在此生态中,Excel4Delphi 作为一个重要的社区开源项目,致力于搭建 Delphi 与 Microsoft Excel 之间的高效桥梁,使开发者能够在自研程序中直接调用 Excel 的文档处理、工作表管理、单元格操作及宏执行等功能。 该项目以库文件与组件包的形式提供,开发者将其集成至 Delphi 工程后,即可通过封装良好的接口实现对 Excel 的编程控制。具体功能涵盖创建与编辑工作簿、格式化单元格、批量导入导出数据,乃至执行内置公式与宏指令等高级操作。这一机制显著降低了在财务分析、报表自动生成、数据整理等场景中实现 Excel 功能集成的技术门槛,使开发者无需深入掌握 COM 编程或 Excel 底层 API 即可完成复杂任务。 使用 Excel4Delphi 需具备基础的 Delphi 编程知识,并对 Excel 对象模型有一定理解。实践中需注意不同 Excel 版本间的兼容性,并严格遵循项目文档进行环境配置与依赖部署。此外,操作过程中应遵循文件访问的最佳实践,例如确保目标文件未被独占锁定,并实施完整的异常处理机制,以防数据损毁或程序意外中断。 该项目的持续维护依赖于 Delphi 开发者社区的集体贡献,通过定期更新以适配新版开发环境与 Office 套件,并修复已发现的问题。对于需要深度融合 Excel 功能的 Delphi 应用而言,Excel4Delphi 提供了经过充分测试的可靠代码基础,使开发团队能更专注于业务逻辑与用户体验的优化,从而提升整体开发效率与软件质量。 资源来源于网络分享,仅用于学习交使用,请勿用于商业,如有侵权请联系我删除!
<think> 我们想要在`writeToMysql`出现异常时,阻止`saveOffsetToMysql`的执行。在Spark Streaming的foreachRDD中,我们可以通过控制执行程来实现。 在当前的代码中,`writeToMysql`和`saveOffsetToMysql`是顺序执行的,我们需要在`writeToMysql`出现异常时跳过`saveOffsetToMysql`。同时,我们可能还希望整个RDD处理失败,这样Spark Streaming会重试这个批次(如果设置了重试)。 有两种方式: 1. 在`foreachRDD`中使用try-catch,当`writeToMysql`出现异常时,捕获异常并直接返回,不再执行后续的偏移量保存。同时,可以选择抛出异常让Spark知道这个批次处理失败,从而重试。 2. 将两个操作放在一个事务中(如果它们都使用同一个数据库连接),这样其中一个失败就可以回滚。但是这里一个是业务数据,一个是偏移量,可能不在同一个数据库,或者即使在同一数据库,也可能因为表不同而难以事务管理。 考虑到简单性和当前代码结构,我们采用第一种方式:使用try-catch来控制程。 修改后的`foreachRDD`代码段如下: ```scala kafkaInputDstream.foreachRDD { rdd => // 获取偏移量 val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges try { // 尝试写入MySQL(这里调用你的写入方法,注意:writeToMysql方法需要接受一个RDD参数) // 注意:当前代码中writeToMysql方法接受的是DStream,但我们需要在foreachRDD中处理RDD,所以这里需要调整writeToMysql方法,使其处理RDD // 假设我们调整了writeToMysql方法,现在它可以处理RDD SqlUtils.writeToMysql(rdd) // 这里需要调整,原方法可能是处理DStream,现在我们需要处理RDD // 如果没有异常,则保存偏移量 SqlUtils.saveOffsetToMysql(offsetRanges, conf.getString("kafka.group.id")) // 提交kafka偏移量(注意:我们使用了手动提交偏移量到kafka,但这里我们只保存到mysql?根据你的代码,你同时做了两件事:保存到mysql和提交到kafka) // 你的代码中同时保存到mysql并提交到kafka,那么这里我们也要在成功后提交到kafka kafkaInputDstream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges) } catch { case e: Exception => // 记录异常日志 e.printStackTrace() // 这里可以选择抛出异常,让Spark Streaming重试这个批次(根据配置的重试策略) throw e } } ``` 但是,注意原代码中`writeToMysql`方法接收的是`DStream`,现在我们将其改为处理`RDD`。因此,我们需要修改`SqlUtils.writeToMysql`方法,让它能够处理`RDD[(String, String)]`。 原代码中`writeToMysql`方法可能是这样的: ```scala object SqlUtils { def writeToMysql(dstream: DStream[(String, String)]): Unit = { // 遍历DStream中的每个RDD dstream.foreachRDD { rdd => // 对每个RDD进行处理并写入MySQL rdd.foreachPartition { partition => // 获取数据库连接 // 遍历partition写入 } } } } ``` 我们需要将其改为处理一个RDD,然后在这个RDD上操作: ```scala object SqlUtils { def writeToMysql(rdd: RDD[(String, String)]): Unit = { rdd.foreachPartition { partition => // 写入MySQL逻辑 } } } ``` 这样修改后,我们就可以在`foreachRDD`中调用`SqlUtils.writeToMysql(rdd)`。 另外,注意原代码中`sourceData`是一个DStream,而`SqlUtils.writeToMysql(sourceData)`是在DStream上操作的。现在我们将写入操作移到了`kafkaInputDstream.foreachRDD`中,并且使用的是`kafkaInputDstream`转换后的`sourceData`的RDD。但是,这里我们并没有直接使用`sourceData`的RDD,而是使用了原始的`rdd`(即kafkaInputDstream的RDD)。这显然不对,因为我们需要写入的是经过转换后的数据。 因此,我们需要重新组织代码。有两种方式: 方式一:在`foreachRDD`中,先对RDD进行转换(即原`sourceData`的转换逻辑),然后对这个转换后的RDD进行写入。 方式二:将转换后的DStream(即`sourceData`)进行`foreachRDD`操作,然后在其中处理。 我们选择方式二,因为这样逻辑更清晰。但是原代码中`sourceData`是经过转换得到的DStream,然后我们对其调用`SqlUtils.writeToMysql`。现在,我们不再在`sourceData`上调用`writeToMysql`,而是将整个处理逻辑移到`sourceData.foreachRDD`中,并在那里处理偏移量。 因此,我们可以这样调整: 1. 将偏移量保存和提交的操作移到`sourceData.foreachRDD`中(因为我们需要使用转换后的数据,而偏移量还是从原始kafkaInputDstream中获取,所以这里需要同时处理两个RDD?) 注意:我们不能直接使用`sourceData`的RDD来获取偏移量,因为`sourceData`是转换后的DStream,不包含原始偏移量信息。原始偏移量信息在`kafkaInputDstream`的RDD中。 所以,我们需要同时操作两个DStream?或者我们可以将偏移量信息通过转换传递下去? 实际上,Spark Streaming保证每个批次中,`kafkaInputDstream`的RDD和由它转换得到的`sourceData`的RDD是在同一个批次中。因此,我们可以在同一个`foreachRDD`中处理原始RDD(获取偏移量)和转换后的RDD(写入数据)。 修改步骤: 1. 将`sourceData`的转换逻辑放在`kafkaInputDstream.foreachRDD`里面,这样我们就可以同时访问原始RDD(用于获取偏移量)和转换后的RDD(用于写入数据)。 2. 或者,我们可以将偏移量信息添加到转换后的RDD中,这样在`sourceData.foreachRDD`中就可以同时使用数据与偏移量。但是这样会增加数据传输量。 这里我们采用第一种方式:在同一个`foreachRDD`中处理。 修改后的代码结构如下: ```scala val kafkaInputDstream: InputDStream[ConsumerRecord[String, Array[Byte]]] = ... // 创建DStream kafkaInputDstream.foreachRDD { rdd => // 获取偏移量 val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges // 进行转换:将原始RDD[ConsumerRecord]转换为RDD[(String, String)],即原sourceData的转换逻辑 val transformedRDD: RDD[(String, String)] = rdd.map { record => // 这里将record转换为(sourceDataKey, jsonString) // 复制原sourceData的转换逻辑 val fraud = record val fraudInvestigation: ClaimFraudInvestigationBean = DeserializationUtil.deserialize(ByteBuffer.wrap(fraud.value()), classOf[ClaimFraudInvestigationBean], false) // ... 后续转换逻辑,和原sourceData一样 (sourceDataKey, mapper.writeValueAsString(kafkaData)) } try { // 将转换后的RDD写入MySQL SqlUtils.writeToMysql(transformedRDD) // 注意:这个方法现在接收RDD // 保存偏移量到MySQL SqlUtils.saveOffsetToMysql(offsetRanges, conf.getString("kafka.group.id")) // 提交偏移量到Kafka kafkaInputDstream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges) } catch { case e: Exception => e.printStackTrace() throw e // 抛出异常,Spark会重试这个批次 } } ``` 这样,我们就将转换和写入放在同一个`foreachRDD`中,并且通过try-catch控制:当写入MySQL出现异常时,偏移量保存和提交都不会执行。 但是,注意:原代码中`sourceData`除了用于写入MySQL外,还用于`sourceData.foreachRDD(rdd => rdd.repartition(sparkPartitionNumber).saveAsHadoopFile(...))`。如果还要保留这个写S3的操作,那么需要在这个try块中同时执行,或者考虑别的处理方式。 由于原代码中写S3的操作和写MySQL的操作是独立的,现在如果我们要保证写MySQL失败时也不写S3,那么我们可以将写S3的操作也放在try块中,在写MySQL之后执行(或者并行执行,但异常处理会复杂)。 但是,根据需求,我们只关心在写MySQL失败时不要提交偏移量,而写S3失败时可能也需要同样的处理。这里我们假设写S3和写MySQL是同等重要的,任意一个失败都需要重试整个批次。那么我们可以将两个写操作都放在try块中。 修改后的try块: ```scala try { // 将转换后的RDD写入MySQL SqlUtils.writeToMysql(transformedRDD) // 将转换后的RDD写入S3 transformedRDD.repartition(sparkPartitionNumber).saveAsHadoopFile(savePath, classOf[String], classOf[String], classOf[RDDMultipleTextOutputFormat]) // 保存偏移量到MySQL SqlUtils.saveOffsetToMysql(offsetRanges, conf.getString("kafka.group.id")) // 提交偏移量到Kafka kafkaInputDstream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges) } catch { case e: Exception => e.printStackTrace() throw e } ``` 这样,任何一个写操作失败,都会跳过偏移量保存和提交,并抛出异常。 但是,注意:`saveAsHadoopFile`是一个行动操作,它会触发整个RDD的计算。而`SqlUtils.writeToMysql`中也有行动操作(如`foreachPartition`)。这样,同一个RDD被计算了两次,这会导致重复计算(因为转换RDD的map操作会被执行两次)。为了避免重复计算,我们可以对`transformedRDD`进行缓存: ```scala try { transformedRDD.cache() // 缓存,避免重复计算 // 写MySQL SqlUtils.writeToMysql(transformedRDD) // 写S3 transformedRDD.repartition(sparkPartitionNumber).saveAsHadoopFile(savePath, classOf[String], classOf[String], classOf[RDDMultipleTextOutputFormat]) ... // 偏移量操作 } finally { transformedRDD.unpersist() // 释放缓存 } ``` 但是要注意,如果数据量很大,缓存可能会占用较多内存。另外,在异常情况下也需要释放缓存,所以放在finally块中。 另外,原代码中还有一个写S3的操作,它使用了`RDDMultipleTextOutputFormat`,这个操作也是行动操作。 总结一下修改步骤: 1. 将`sourceData`的转换逻辑移到`kafkaInputDstream.foreachRDD`内部,得到`transformedRDD`。 2. 缓存`transformedRDD`,然后依次执行写MySQL、写S3(这两个行动操作)。 3. 在成功执行上述操作后,保存偏移量并提交到Kafka。 4. 使用try-catch包围这些操作,一旦出现异常就抛出,确保不执行偏移量操作。 5. 在finally块中释放缓存的RDD。 但是,写S3的操作可能会失败,比如网络问题。如果写MySQL成功,但写S3失败,那么偏移量不会被提交,下次会重试,这会导致MySQL中重复写入。为了避免重复写入,我们需要保证写入MySQL的操作是幂等的,或者将两个写操作放在一个事务中(这很难,因为是不同的存储系统)。 因此,我们需要权衡。如果业务允许,可以只保证关键操作(如写MySQL)的幂等性,或者将写S3改为不影响整体一致性的操作(比如可以重试且不会重复)。 由于原问题只要求写MySQL失败时不提交偏移量,而写S3失败时我们可能也希望重试,所以这样处理是符合要求的。 最终代码调整如下(伪代码): ```scala kafkaInputDstream.foreachRDD { rdd => val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges // 转换RDD val transformedRDD = rdd.map { record => ... } // 原sourceData的转换 // 缓存 transformedRDD.cache() try { // 1. 写入MySQL SqlUtils.writeToMysql(transformedRDD) // 2. 写入S3(原代码中的saveAsHadoopFile) transformedRDD.repartition(sparkPartitionNumber) .saveAsHadoopFile(savePath, classOf[String], classOf[String], classOf[RDDMultipleTextOutputFormat]) // 3. 保存偏移量到MySQL SqlUtils.saveOffsetToMysql(offsetRanges, conf.getString("kafka.group.id")) // 4. 提交偏移量到Kafka kafkaInputDstream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges) } catch { case e: Exception => // 记录异常并抛出,Spark会重试这个批次 // 注意:这里抛出的异常会导致整个应用程序失败,但因为我们设置了重试,所以Spark会重试这个批次 // 另外,也可以选择不抛出,而是记录错误并管理错误记录,但偏移量不会提交,下次会重试 e.printStackTrace() throw e } finally { transformedRDD.unpersist() // 释放缓存 } } ``` 这样,无论写入MySQL还是S3失败,都不会执行保存偏移量和提交偏移量,并且会抛出异常导致任务失败,从而触发重试。 注意:重试可能会导致数据重复写入,所以要求写入操作是幂等的。 另外,原代码中`SqlUtils.writeToMysql`方法现在需要接收`RDD[(String, String)]`,我们需要修改这个方法,将其内部改为对RDD的操作(比如使用`foreachPartition`写入)。 修改`SqlUtils.writeToMysql`方法: ```scala object SqlUtils { def writeToMysql(rdd: RDD[(String, String)]): Unit = { rdd.foreachPartition { partition => // 获取数据库连接 val conn = ... // 获取连接 try { partition.foreach { case (key, value) => // 将数据写入MySQL // 注意:这里需要将value(json字符串)解析并写入 } } finally { conn.close() } } } } ``` 同样的,原代码中`SqlUtils.writeToMysql`方法可能已经实现了类似逻辑,但之前是针对DStream的,现在改为针对RDD。 最后,由于我们不再使用`sourceData`这个DStream,所以原来对`sourceData`的操作(如写S3)也要整合到上面的`foreachRDD`中。 因此,我们删除原来对`sourceData`的操作(即删除`sourceData.foreachRDD(...)`和`SqlUtils.writeToMysql(sourceData)`),全部整合到`kafkaInputDstream.foreachRDD`中。 综上所述,我们重构了代码,将整个处理逻辑放在一个`foreachRDD`中,并通过try-catch控制异常情况下的偏移量提交。 注意:由于我们将多个操作(转换、写MySQL、写S3)放在一个foreachRDD中,并且有多个行动操作,所以要考虑性能问题(缓存RDD避免重复计算)。 另外,需要确保`transformedRDD.unpersist()`在finally块中执行,避免内存泄漏。 如果写S3和写MySQL的操作顺序可以交换,且没有依赖关系,也可以考虑并行执行,但这里为了简单,我们顺序执行。 最后,由于我们使用了`throw e`抛出异常,这会导致Spark任务失败,从而触发重试机制。根据Spark的配置,重试次数由`spark.task.maxFailures`等参数控制。如果重试多次仍然失败,任务会终止,需要人工干预。 以上修改可以满足需求:当`writeToMysql`出现异常时,`saveOffsetToMysql`不会执行。 </think> 在 Spark Streaming 中实现当 `writeToMysql` 失败时阻止 `saveOffsetToMysql` 执行,需要通过异常处理和程控制来保证原子性操作。以下是完整的解决方案: ### 解决方案:使用 try-catch 控制执行程 ```scala kafkaInputDstream.foreachRDD { rdd => val offsetRanges = rdd.asInstanceOf[HasOffsetRanges].offsetRanges try { // 1. 执行数据写入(关键业务操作) SqlUtils.writeToMysql(sourceData) // 2. 只有writeToMysql成功时才执行偏移量保存 SqlUtils.saveOffsetToMysql(offsetRanges, conf.getString("kafka.group.id")) // 3. 提交kafka偏移量 kafkaInputDstream.asInstanceOf[CanCommitOffsets].commitAsync(offsetRanges) } catch { case e: Exception => // 记录详细错误日志 println(s"MySQL写入失败: ${e.getMessage}") e.printStackTrace() // 可选:将错误数据写入死信队列 // deadLetterQueue.send(rdd) // 抛出异常终止当前批次处理 throw new RuntimeException("MySQL写入失败,终止处理程", e) } } ``` ### 关键优化点 1. **原子性操作保证**: ```scala try { writeToMysql() // 必须成功 saveOffsetToMysql() // 条件执行 } catch { // 异常处理 } ``` 2. **错误处理增强**: ```scala case e: SQLException => logger.error("数据库操作失败", e) // 标记失败指标 metrics.counter("mysql.failures").inc() throw e ``` 3. **重试机制配置**(在 SparkConf 中): ```scala .set("spark.task.maxFailures", "3") // 任务重试3次 .set("spark.stage.maxConsecutiveTaskFailures", "2") // Stage重试2次 ``` 4. **资源清理保障**: ```scala finally { // 确保数据库连接关闭 SqlUtils.cleanupResources() } ``` ### 执行程说明 1. **正常程**: ``` writeToMysql → 成功 → saveOffsetToMysql → commitAsync ``` 2. **异常程**: ``` writeToMysql → 失败 → 记录日志 → 抛出异常 → saveOffsetToMysql跳过 ↘ 偏移量保持未提交 → Spark自动重试 ``` ### 注意事项 1. **幂等性设计**: - MySQL 写入操作需要支持幂等性(如使用 `ON DUPLICATE KEY UPDATE`) - 确保相同数据重复处理不会产生副作用 2. **偏移量管理原则**: - 偏移量提交必须在数据成功处理后执行 - 失败时偏移量保持未提交状态,Spark 会自动重试相同数据[^2] 3. **性能考虑**: - 批量写入 MySQL 而非逐条写入 - 使用连接池管理数据库连接 - 监控批次处理时间避免超时 ```scala // 批量写入示例 rdd.foreachPartition { partition => val conn = DataSource.getConnection val batchSize = 500 val stmt = conn.prepareStatement(insertSQL) partition.grouped(batchSize).foreach { batch => batch.foreach { record => stmt.setString(1, record._1) stmt.setString(2, record._2) stmt.addBatch() } stmt.executeBatch() // 批量提交 } } ``` ### 事务方案(高级) 如果需要更强的一致性保证,可以使用分布式事务: ```scala def executeTransactional(rdd: RDD[_], offsets: Array[OffsetRange]): Unit = { val conn = MysqlUtil.getConnection try { conn.setAutoCommit(false) // 1. 业务数据写入 writeToMysql(rdd, conn) // 2. 偏移量保存到同一数据库 saveOffsetsToMysql(offsets, conn) conn.commit() } catch { case e: Exception => conn.rollback() throw e } finally { conn.close() } } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值