繁体   English   中英

用 Spring 事件替换计划任务

[英]Replacing a scheduled task with Spring Events

在我的 Spring Boot 应用程序中,客户可以提交文件。 每个客户的文件都通过每分钟运行的计划任务合并在一起。 由调度程序执行合并的事实有许多缺点,例如很难编写端到端测试,因为在测试中您必须等待调度程序运行才能检索合并结果。

因此,我想改用基于事件的方法,即

  1. 客户提交文件
  2. 发布包含此客户 ID 的事件
  3. 合并服务监听这些事件并在事件 object 中为客户执行合并操作

这将具有在有文件可用于合并后立即触发合并操作的优点。

但是,这种方法存在许多问题,我需要一些帮助

并发

合并是一个相当昂贵的操作。 最多可能需要 20 秒,具体取决于所涉及的文件数量。 因此,合并必须异步进行,即不能作为发布合并事件的同一线程的一部分。 另外,我不想同时为同一个客户执行多个合并操作,以避免出现以下情况

  1. 客户 1 保存文件 2 触发文件 1 和文件 2 的合并操作 2
  2. 很短的时间后,客户 1 保存了文件 3,触发了文件 1、文件 2 和文件 3 的合并操作 3
  3. 合并操作3完成保存合并文件3
  4. 合并操作2完成用merge-file2覆盖merge-file3

为避免这种情况,我计划使用事件侦听器中的锁按顺序处理同一客户的合并操作,例如

@Component
public class MergeEventListener implements ApplicationListener<MergeEvent> {

    private final ConcurrentMap<String, Lock> customerLocks = new ConcurrentHashMap<>();

    @Override
    public void onApplicationEvent(MergeEvent event) {
        var customerId = event.getCustomerId();
        var customerLock = customerLocks.computeIfAbsent(customerId, key -> new ReentrantLock());
        customerLock.lock();
        mergeFileForCustomer(customerId);
        customerLock.unlock();
    }

    private void mergeFileForCustomer(String customerId) {
        // implementation omitted
    }
}

容错

例如,如果应用程序在合并操作期间关闭或在合并操作期间发生错误,我该如何恢复?

计划方法的优点之一是它包含隐式重试机制,因为每次运行时它都会查找具有未合并文件的客户。

概括

我怀疑我提出的解决方案可能正在(严重)重新实现此类问题的现有技术,例如 JMS。 我建议的解决方案是可取的,还是应该改用 JMS 之类的东西? 该应用程序托管在 Azure 上,因此我可以使用它提供的任何服务。

如果我的解决方案可取的,我应该如何处理容错?

关于并发部分,如果每个客户(在给定时间范围内)提交的文件数量足够小,我认为使用锁的方法可以正常工作。

随着时间的推移,您最终可以监控等待锁定的线程数,以查看是否存在大量争用。 如果有,那么也许您可以累积一些合并事件(在特定时间范围内),然后运行并行合并操作,这实际上导致了类似于调度程序的解决方案。

在容错方面,基于消息队列的方法可以工作(没有与 JMS 一起使用,但我看到它是消息队列的实现)。

我会 go 与基于云的消息队列(例如SQS )仅仅因为可靠性的目的。 方法是:

  • 将合并事件推送到队列中
  • 合并服务一次扫描一个事件并启动合并作业
  • 合并作业完成后,从队列中删除消息

这样,如果在合并过程中出现问题,消息将保留在队列中,并在应用程序重新启动时再次读取。

经过一番考虑,我对这个问题的想法。

根据 OP 的规范,我将可能的解决方案限制为 Azure 托管服务提供的解决方案。

Azure Blob 存储 Function 触发器

因为这个问题是关于存储文件的,所以让我们从使用在文件创建时触发的触发器 function 探索 Blob 存储开始。 根据文档,Azure 函数最多可以运行 230 秒,并且默认重试次数为 5。

但是,此解决方案将要求来自单个客户的文件以不会导致并发问题的方式到达,因此让我们暂时保留此解决方案。

Azure 队列存储

不保证先进先出 (FIFO) 有序交付,因此不符合要求。

存储队列和服务总线队列 - 比较和对比: https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-azure-and-service-bus-queues-compared-contrasted

Azure 服务总线

Azure Service Bus是一个 FIFO 队列,似乎满足要求。

https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-azure-and-service-bus-queues-compared-contrasted#compare-storage-queues-and-service-总线队列

从上面的文档中,我们看到大文件不适合作为消息负载。 为了解决这个问题,可以将文件存储在 Azure Blob Storage 中,并且消息将包含在哪里可以找到文件的信息。


选择Azure 服务总线Azure Blob 存储后,让我们讨论实施注意事项。

队列生产者

在 AWS 上,生产者端的解决方案是这样的:

  1. 专用端点向客户应用程序提供预签名的 URL
  2. 客户应用程序将文件上传到 S3
  3. Lambda 由 S3 object 创建触发将消息插入队列

不幸的是,Azure 还没有预签名的 URL 等效项(它们具有不相等的共享访问签名),因此文件上传必须通过端点完成,该端点又将文件存储到 Z3A5150F142F898F3677 当需要文件上传端点时,让文件上传端点也负责将消息插入队列似乎是合适的。

队列消费者

因为文件合并需要大量时间(约 20 秒),所以应该可以横向扩展消费者端。 对于多个消费者,我们必须确保不超过一个消费者实例处理单个客户。 这可以通过使用消息会话来解决: https://docs.microsoft.com/en-us/azure/service-bus-messaging/message-sessions

为了实现容错,消费者应该在文件合并期间使用 peek-lock(而不是接收和删除),并在文件合并完成时将消息标记为已完成。 当消息被标记为完成时,消费者可能负责删除 Blob 存储中的多余文件。

现有解决方案和未来解决方案可能存在的问题

如果客户A开始上传大文件#1 ,然后立即开始上传小文件#2 ,则文件# 2的文件上传可能在文件#1之前完成并导致乱序情况。

我认为这是通过使用某种锁定机制或文件名约定在现有解决方案中解决的问题。

Spring-boot 搭配 Kafka 可以解决您的容错问题。

Kafka 支持生产者-消费者 model。 让客户事件发布到 Kafka 生产者。

为 Kafka 配置复制功能,以免丢失任何事件。

使用可以为每个事件调用合并服务的消费者。

  1. 一旦消费者读取了 customerId 的事件并合并然后提交偏移量。

  2. 如果在合并事件之间发生任何故障,则不会提交偏移量,因此当应用程序再次启动时可以再次读取它。

  3. 如果合并服务可以检测到具有给定数据的重复事件,那么重新处理相同的消息应该不会导致任何问题(Kafka 承诺事件的单次传递)。 重复事件检测是对已处理完整但未能提交到 Kafka 的事件的安全检查。

首先,基于事件的方法对于这种情况是正确的。 您应该为发布-订阅事件消息使用外部代理。

注意,默认情况下,Spring 发布事件是同步的。

假设您有 3 个服务:

  1. 应用服务
  2. 合并服务
  3. CDC 服务(变更数据捕获)
  4. 经纪服务 (Kafka, RabbitMQ,...)

基于“发件箱模式”的主流:

  1. 应用服务将事件消息保存到发件箱消息表
  2. CDC Service 监视发件箱表并将事件消息从发件箱表发布到 Broker Servie
  3. Merge Service 订阅 Broker Server 并接收事件消息(消息有序)
  4. Merge Servcie 执行合并操作

您可以为此流程使用eventuate lib。

此外,您可以将 DDD 应用到您的架构中。 使用 Axon 框架进行 CQRS 模式、公共领域事件并对其进行处理。

参考:

  1. 发件箱模式: https://microservices.io/patterns/data/transactional-outbox.html 在此处输入图像描述

听起来您确实可以使用StreamETL工具来完成这项工作。 当您开发应用程序时,您有一些优先级/排队/批处理要求,很容易看出如何使用Cron + SQL Database构建解决方案,可能有一个队列将工作与生产工作分离。

这很可能是最容易构建的东西,因为您对这种方法有很多粒度和控制。 如果您认为您实际上可以通过这种方式快速且低风险地满足您的要求,那么您可以这样做。

有些软件组件更适合这些任务,但它们确实有一些学习曲线,并且取决于您可能使用的 PAAS 或云。 您将获得开箱即用的监控、可扩展性和可用性弹性。 开源或云服务将减轻您的管理负担。

使用什么也取决于您的优先级和要求。 如果您想使用 go ETL 方法,该方法非常适合存储工作,您可能需要使用 Glue t 之类的东西。 如果您想要优先级功能,您可能想要使用多个队列,这真的取决于。 您还需要使用仪表板进行监控,以查看无论采用何种方法,您的合并都应等待多长时间。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM