事件采购增量int id

时间:2017-01-17 13:29:44

标签: c# cqrs event-sourcing

我查看了很多活动采购教程,所有人都使用简单的演示来专注于教程主题(事件采购)

这很好,直到你在实际工作应用程序中遇到其中一个教程未涉及的内容:)

我打了这样的东西。 我有两个数据库,一个事件存储和一个投影存储(读取模型) 所有聚合都有一个GUID ID,到目前为止100%罚款。

现在我创建了一个新的JobAggregate和一个Job Projection。 我公司要求拥有一个独特的增量int64作业ID。

现在我看起来很愚蠢:) 另一个问题是每秒多次创建作业! 这意味着,获得下一个号码的方法必须非常安全。

过去(没有ES)我有一个表,将PK定义为自动增量int64,保存Job,DB完成工作以给我下一个数字,完成。

但是我如何在我的Aggregate或命令处理程序中执行此操作? 通常,投影作业是由事件处理程序创建的,但是在过程的后期,因为聚合应该已经有了int64。 (用于在空DB上重播聚合并具有相同的聚合ID - >作业ID关系)

我该如何解决这个问题?

亲切的问候

3 个答案:

答案 0 :(得分:2)

  

过去(没有ES)我有一个表,将PK定义为自动增量int64,保存Job,DB完成工作以给我下一个数字,完成。

在这个序列中需要注意的一件重要事情是,唯一标识符的生成和记录簿中数据的持久性都共享一个事务。

当你将这些想法分开时,你基本上会看两个事务 - 一个消耗id,一个没有其他聚合尝试共享它,另一个将id写入商店。

最佳答案是安排两个部分都属于同一个事务 - 例如,如果您使用关系数据库作为事件存储,那么您可以在“aggregate_id to long”表中创建一个条目。保存事件的事务相同。

另一种可能性是将聚合的“创建”视为Prepare,后跟Created;使用事件处理程序通过保留事后的长标识符来响应prepare事件,然后向聚合发送新命令以将长标识符分配给它。因此,Created的所有消费者都会看到聚合的长期分配给它。

值得注意的是,您正在为正在创建的每个聚合分配实际上随机长的内容,因此您最好深入了解该公司认为从中获得的好处 - 如果他们期望标识符将提供订购保证或完整性保证,然后您最好了解进入。

首先保留长期没有什么特别的错误;根据聚合保存失败的频率,您可能会遇到差距。在大多数情况下,您应该期望能够保持较小的故障率(即 - 检查以确保在实际运行命令之前您希望命令成功)。

实际上,唯一标识符的生成属于set validation的范围;我们通常通过放弃任何排序假装并假装碰撞风险为零来“欺骗”UUID。关系数据库非常适合集验证;活动商店也许不是那么多。如果您需要由模型控制的唯一顺序标识符,那么您的“分配的标识符集”需要在聚合中。

要遵循的关键词是“业务成本” - 确保您理解为什么长标识符很有价值。

答案 1 :(得分:1)

这就是我的处理方式。

我同意ID生成器的想法,它是“业务ID”,而不是“技术ID”

这里的核心是拥有一个应用程序级别的JobService,该级别可以处理所有基础结构服务以协调要完成的工作。

控制器(如Web控制器或命令行)将直接消耗应用程序级别的JobService来控制/命令状态更改。

它是类似PHP的伪代码,但在这里我们谈论的是体系结构和过程,而不是语法。使其适应C#语法,事情就一样了。

应用程序级别

class MyNiceWebController
{
    public function createNewJob( string $jobDescription, xxxx $otherData, ApplicationJobService $jobService )
    {
        $projectedJob = $jobService->createNewJobAndProject( $jobDescription, $otherData );

        $this->doWhateverYouWantWithYourAleadyExistingJobLikeForExample301RedirectToDisplayIt( $projectedJob );
    }
}

class MyNiceCommandLineCommand
{
    private $jobService;

    public function __construct( ApplicationJobService $jobService )
    {
        $this->jobService = $jobService;
    }

    public function createNewJob()
    {
        $jobDescription = // Get it from the command line parameters
        $otherData = // Get it from the command line parameters

        $projectedJob = $this->jobService->createNewJobAndProject( $jobDescription, $otherData );

        // print, echo, console->output... confirmation with Id or print the full object.... whatever with ( $projectedJob );
    }
}

class ApplicationJobService
{
    // In application level because it just serves the first-level request
    // to controllers, commands, etc but does not add "domain" logic.

    private $application;
    private $jobIdGenerator;
    private $jobEventFactory;
    private $jobEventStore;
    private $jobProjector;

    public function __construct( Application $application, JobBusinessIdGeneratorService $jobIdGenerator, JobEventFactory $jobEventFactory, JobEventStoreService $jobEventStore, JobProjectorService $jobProjector )
    {
        $this->application = $application;  // I like to lok "what application execution run" is responsible of all domain effects, I can trace then IPs, cookies, etc crossing data from another data lake.
        $this->jobIdGenerator = $jobIdGenerator;
        $this->jobEventFactory = $jobEventFactory;
        $this->jobEventStore = $jobEventStore;
        $this->jobProjector = $jobProjector;
    }

    public function createNewJobAndProjectIt( string $jobDescription, xxxx $otherData ) : Job
    {
        $applicationExecutionId = $this->application->getExecutionId();

        $businessId = $this->jobIdGenerator->getNextJobId();

        $jobCreatedEvent = $this->jobEventFactory->createNewJobCreatedEvent( $applicationExecutionId, $businessId, $jobDescription, $otherData );

        $this->jobEventStore->storeEvent( $jobCreatedEvent );       // Throw exception if it fails so no projecto will be invoked if the event was not created.

        $entityId = $jobCreatedEvent->getId();
        $projectedJob = $this->jobProjector->project( $entityId );

        return $projectedJob;
    }
}

注意:如果投影对于同步投影而言过于昂贵,则只需返回ID:

        // ...
        $entityId = $jobCreatedEvent->getId();
        $this->jobProjector->enqueueProjection( $entityId );

        return $entityId;
    }
}

基础结构级别(常见于各种应用程序)

class JobBusinessIdGenerator implements DomainLevelJobBusinessIdGeneratorInterface
{
    // In infrastructure because it accesses persistance layers.

    // In the creator, get persistence objects and so... database, files, whatever.

    public function getNextJobId() : int
    {
        $this->lockGlobalCounterMaybeAtDatabaseLevel();

        $current = $this->persistance->getCurrentJobCounter();
        $next = $current + 1;
        $this->persistence->setCurrentJobCounter( $next );

        $this->unlockGlobalCounterMaybeAtDatabaseLevel();

        return $next;
    }
}

域级别

class JobEventFactory
{
    // It's in this factory that we create the entity Id.

    private $idGenerator;

    public function __construct( EntityIdGenerator $idGenerator )
    {
        $this->idGenerator = $idGenerator;
    }

    public function createNewJobCreatedEvent( Id $applicationExecutionId, int $businessId, string $jobDescription, xxxx $otherData ); : JobCreatedEvent
    {
        $eventId = $this->idGenerator->createNewId();
        $entityId = $this->idGenerator->createNewId();

        // The only place where we allow "new" is in the factories. No other places should do a "new" ever.
        $event = new JobCreatedEvent( $eventId, $entityId, $applicationExecutionId, $businessId, $jobDescription, $otherData );

        return $event; 
    }
}

如果您不喜欢工厂创建entityId,在某些人看来可能很难看,只需将其作为具有特定类型的参数传递,并pss负责创建一个新的新实体,并且不要在其他中间对象上重用它服务(绝不是应用程序服务)来为您创建它。

尽管如此,请注意如果“傻”服务仅创建具有相同实体ID的“两个” JobCreatedEvent,该怎么办?那真的很丑。最后,创建只会发生一次,并且ID是在“创建JobCreationEvent事件”(冗余冗余)的核心处创建的。无论如何,您的选择。

其他课程...

class JobCreatedEvent;
class JobEventStoreService;
class JobProjectorService;

这篇文章中无关紧要的东西

我们应该讨论很多问题,如果投影仪应该处于对调用它们的多个应用程序来说是全局的基础设施级别……还是应该在域中(因为我需要“至少”一种读取模型的方式)或者它属于更多领域?应用程序(也许可以在4种不同的应用程序中以4种不同的方式读取同一个模型,并且每个模型都有自己的投影仪)...

我们可以讨论很多如果在事件存储或应用程序级别隐式触发的副作用在哪里触发(我没有称任何副作用处理器==事件侦听器)。我认为副作用在应用程序层中,因为它们取决于基础架构...

但是所有这一切...都不是这个问题的主题。

对于此“帖子”,我并不关心所有这些内容。当然,它们不是可以忽略的主题,您将有自己的策略。您必须非常仔细地设计所有这些。但是,这里的问题是在哪里创建来自业务需求的自动增量ID。并以“干净代码”方式进行所有这些投影仪(有时称为 calculators )和副作用(有时称为 reactors )会模糊此答案的重点。你明白了。

我在这篇文章中关心的事情

我在乎的是

  • 如果专家指的是“自动数字”,则它是“域要求”,因此其属性与“描述”或“其他数据”的定义级别相同。
  • 他们想要此属性的事实与所有实体都具有编码器选择的格式的“内部ID”(即uuid,sha1或其他任何东西)冲突。 li>
  • 如果您需要该属性的顺序ID,则需要一个“值的提供者” AKA JobBusinessIdGeneratorService,它与“实体ID”本身无关。
  • 该ID生成器将负责确保该数字一旦自动递增,就会在之前同步保存,然后将其返回给客户端,因此不可能返回相同ID的两倍在失败时。

缺点

您需要处理序列泄漏:

如果Id生成器指向4007,则对getNextJobId()的下一次调用会将其递增到4008,将指针保持为“ current = 4008”,然后返回。

如果由于某种原因事件的创建和“持久”失败(这个词存在吗?)失败,则下一次调用将产生4009。然后,我们将得到[ 4006, 4007, 4009, 4010 ]序列,其中{{1} }。

这是因为从生成器的角度来看,实际上已经使用了4008,并且作为生成器,它不知道您对它做了什么,就像您有一个虚拟傻循环提取100个数字一样

永远不要用4008的{​​{1}}中的->rollback()补偿catch,因为如果到2008年这会产生并发问题,则另一个过程会2009年,然后第一个过程失败,回滚将中断。只需假设“发生故障”时,Id“就被消耗掉了”,并且不要怪发电机。怪谁失败了。

希望对您有帮助!

答案 2 :(得分:0)

@SharpNoizy,很简单。

创建自己的Id生成器。说一个字母数字字符串,例如" DB3U8DD12X"这给你数十亿的可能性。现在,你要做的是通过给每个字符一个有序的值来按顺序生成这些id ......

0 - 0
1 - 1
2 - 2
.....
10 - A
11 - B

明白了吗?那么,你接下来要做的就是创建你的函数来增加你的" D74ERT3E4"使用该矩阵的字符串。

所以," R43E4D"," R43E4E"," R43E4F"," R43E4G" ......明白了吗?

然后,当您加载应用程序时,您会查看数据库并找到生成的最新Id。然后你在内存中加载下面的50,000个组合(如果你想要超高速)并创建一个静态类/方法,它将为你提供该值。

Aggregate.Id = IdentityGenerator.Next();
通过这种方式,您可以控制ID的生成,因为这是唯一拥有该权限的类。

我喜欢这种方法,因为它更具有可读性和可读性。例如,在web api中使用它时。 GUID很难(也很乏味)阅读,记住等等。

GET api / job / DF73 api / job / XXXX-XXXX-XXXXX-XXXX-XXXX

更好记住

这有意义吗?