cqrs框架

如今,交易处理无处不在,从使用关系数据库处理购买的各个零售网站到每秒处理10万多个订单的实时交易系统。

Reveno是基于CQRS和事件源模式的基于JVM的无锁事务处理新框架。 尽管它是一个简单而强大的工具,但不会影响性能。 所有事务都保留在只读日记帐中,并且只需按顺序重播这些事件即可恢复域模型的最新状态。 所有运行时操作都是在内存中执行的,因此吞吐量可以达到每秒数百万个事务的数量级,平均等待时间约为微秒。 但是Reveno仍然具有通用的功能,因为它涵盖了具有丰富引擎配置的各种用例。 例如,您可以更改持久性配置,从非常宽松(以获得额外的吞吐量)到严格限制(数据丢失容忍度低)。

当然,对整个用例的要求会有很大的不同,并且一般框架应该考虑所有可能性。 在本文中,我想提供一个使用Reveno框架实现简单交易系统的示例。

事务和查询模型。 这些模型都没有对声明的任何限制。 没有必需的注释,基类,甚至没有可序列化的接口; 只需简单的POJO即可完成工作。

没有任何一种方法可以满足所有可能的用例,并且由应用程序设计人员确定如何处理事务模型上的回滚。 Reveno提供了两个用于在您的域中实现对象的主要选项-可变的和不可变的。 它们在引擎盖下的处理方式不同,并且各有优缺点。 不可变对象在Java中非常便宜且无需同步,并且最近很流行 。 Reveno非常有效地处理它们,但是与任何不可变域一样,它们将产生额外的垃圾。 因此,除非某些其他GC工作将成为热门,否则它应该是您的默认选项。 相反,可变模型包括在事务执行期间使用过的对象的其他序列化快照,这可能会对性能产生负面影响。 幸运的是,如果您坚持使用可变模型,并且需要最大的性能和最小的GC影响,则可以使用其他可变模型功能-“补偿动作”。 简而言之,这些是您可以与常规事务处理程序一起实施的手动回滚操作。 有关更多详细信息,请参阅Reveno官方文档页面 。

既然我们已经制定了一些基本规则,那么让我们开始编写一些代码即可。 我们的人工交易系统将有多个帐户,每个帐户可以有零个或多个订单。 我们必须支持维护活动,例如帐户创建,订单处理。 在实践中,可能需要这种系统来处理体面的工作负载,例如每秒10到500k事务或更多。 使问题复杂化的是,它对延迟非常敏感,频繁的大峰值会直接导致财务损失。

安装

如果您使用任何流行的构建工具(例如Maven,Gradle,Sbt等),则可以从Maven Central添加Reveno依赖项。 目前,有三个可用的库:

  • reveno-core –包括所有Reveno核心软件包,负责引擎初始化,事务处理等。
  • reveno-metrics –包括负责从工作引擎收集指标并将其转发到Graphite,Slf4j等的软件包。
  • reveno-cluster –使得可以在具有Master-Slave体系结构的群集中运行Reveno,从而提供故障转移功能。

您可以在Reveno Installation页面上参考完整的安装指南和示例。

定义交易模型

让我们通过定义域模型来开始我们的开发工作。 正如我们之前所建议的,我们将基于简单的POJO进行构建。 我个人的偏爱是选择不可变的对象,因为它们可以大大简化事情,可以绕过任何并发问题,而且最重要的是,它们在Reveno中具有很高的性能,因为不需要保留所访问对象的快照。 Reveno允许我们开箱即用地处理不可变对象(使其成为一个很好的教程,介绍了如何应对Java中的不可变性。)

首先,让我们定义一个代表我们系统中典型交易账户的实体(为简单起见,此处实例变量是公共的,但实际上没有限制):

public class TradeAccount {
    public final long id;
    public final long balance;
    public final String currency;
    private final LongSet orders;

    public TradeAccount(long id, String currency) {
        this(id, 0, currency, new LongOpenHashSet());
    }

    private TradeAccount(long id, long balance, 
                         String currency, LongSet orders) {
        this.id = id;
        this.balance = balance;
        this.currency = currency;
        this.orders = orders;
    }

    public LongSet orders() {
        return new LongOpenHashSet(orders);
    }
}

我们可以看到,该类是完全不可变的。 但是这种价值对象没有任何功能,患有一种通常被称为“ 贫血 ”的疾病。 如果我们的TradeAccount做一些有用的事情,比如说处理订单和货币计算,那就更好了:

public class TradeAccount {
    public final long id;
    public final long balance;
    public final String currency;
    private final LongSet orders;

    public TradeAccount(long id, String currency) {
        this(id, 0, currency, new LongOpenHashSet());
    }

    private TradeAccount(long id, long balance, 
                         String currency, LongSet orders) {
        this.id = id;
        this.balance = balance;
        this.currency = currency;
        this.orders = orders;
    }

    public TradeAccount addBalance(long amount) {
        return new TradeAccount(id, balance + amount, currency, orders);
    }

    public TradeAccount addOrder(long orderId) {
        LongSet orders = new LongOpenHashSet(this.orders);
        orders.add(orderId);
        return new TradeAccount(id, balance, currency, orders);
    }

    public TradeAccount removeOrder(long orderId) {
        LongSet orders = new LongOpenHashSet(this.orders);
        orders.remove(orderId);
        return new TradeAccount(id, balance, currency, orders);
    }

    public LongCollection orders() {
        return new LongOpenHashSet(orders);
    }

}

现在,它变得更加有用。 在转向实际的交易处理细节之前,值得一提的是Reveno实际如何使用其交易模型。 所有实体都存储在一个存储库中,可以从所有类型的处理程序中访问该存储库(我们将在稍后详细介绍它们)。 这些实体通过ID相互引用,并通过ID从存储库访问。 由于内部性能的优化,ID被限制为long类型。

Order类与此类似,为简便起见,我们在此处不显示源代码,但是您可以在GitHub上下载完整的演示源代码 ,并在本文结尾处找到其他链接。

定义查询模型

到目前为止,我们已经探索了如何构建Reveno事务模型。 从逻辑上讲,也必须定义查询方。 在Reveno中,查询是通过定义“视图”构建的,其中每个视图代表事务模型中的某些实体。 除了设计视图类之外,还应该为每种视图类型提供映射器。 我们将在下面更详细地介绍它们。

当一个成功的交易完成,Reveno进行更改的实体的映射,保证更新视图在发生 - 之前的方式,命令完成之前。 在Reveno中,默认情况下,查询模型是内存中的。 让我们为TradingAccount类定义一个视图:

public class TradeAccountView {
    public final double balance;
    public final Set<OrderView> orders;

    public TradeAccountView(double balance, Set<OrderView> orders) {
        this.balance = balance;
        this.orders = orders;
    }
}

我们的TradingAccountView类包含其他视图的集合(在本例中为OrderView),在支持查询,序列化,JSON转换等流程时将非常方便。Reveno映射器支持许多有用的方法,这些方法可以简化映射。一组ID到一组视图等。我们很快就会看到这一点。

定义命令和交易动作

为了在Reveno中执行任何事务,我们必须首先执行一个“命令”对象。 一个命令对象本身可以是一个简单的POJO,在系统中注册了一个特殊的处理程序。 通常,命令使用对存储库的只读访问权限来执行一些聚合和验证逻辑。 但最重要的是,合同规定,命令有责任派遣“交易动作”(也称为“状态更改器”)。

事务操作是在域模型上进行状态更改的组件,并使用对存储库的读写访问执行。 事务动作对象本身可以是在系统中注册了处理程序的POJO。 所有这些动作都被收集到单个原子事务中,这属于当前正在执行的命令的范围。 成功执行后,事务操作将保留到基础存储中,并且可以在重新启动或发生任何类型的故障后重播。

在我们的交易系统中,我们将需要创建一些初始余额的新交易账户。 和以前一样,我们先定义一个事务命令:

public class CreateAccount {
    public final String currency;
    public final double initialBalance;

    public CreateAccount(String currency, double initialBalance) {
        this.currency = currency;
        this.initialBalance = initialBalance;
    }

    public static class CreateAccountAction {
        public final CreateAccount info;
        public final long id;

        public CreateAccountAction(CreateAccount info, long id) {
            this.info = info;
            this.id = id;
        }
    }
}

在这里,我们实际上有两个类。 CreateAccount是命令,CreateAccountAction是事务操作。 通常不需要此拆分; 例如,如果命令和事务操作数据恰好精确匹配,则可以安全地重用同一类。 但是在本例中,我们收到的货币金额是double类型的(例如,从某个旧终端接收),但是在我们的内部引擎中,我们将货币值存储为long,以提供完美的精度。

现在,我们可以实例化Reveno引擎并定义命令和事务操作处理程序:

Reveno reveno = new Engine(pathToEngineFolder);

reveno.domain().command(CreateAccount.class, long.class, (c, ctx) -> {
    long accountId = ctx.id(TradeAccount.class);
    ctx.executeTxAction(new CreateAccount.CreateAccountAction(c, accountId));
    if (c.initialBalance > 0) {
        ctx.executeTxAction(new ChangeBalance(
                            accountId, toLong(c.initialBalance)));
    }
    return accountId;
});

reveno.domain().transactionAction(CreateAccount.CreateAccountAction.class,
                               (a, ctx) -> ctx.repo().store(a.id, 
                                new TradeAccount(a.id, a.info.currency)));

reveno.domain().transactionAction(ChangeBalance.class, 
                               (a, ctx) -> ctx.repo().
                                remap(a.accountId, TradeAccount.class, 
                               (id, e) -> e.addBalance(a.amount))
);

ctx.executeTxAction调用不会阻塞; 在命令处理程序成功完成之后,所有提供的事务操作均在单个线程中执行,因此,如果任何TxAction处理程序都需要回滚,则它们所做的更改将被回滚。 (实际的回滚机制基于事务模型等)

将实体映射到查询模型

由于我们的事务和查询模型是分开的,因此我们需要定义将实体转换为视图表示的映射器。 无需从我们的代码中显式调用它们,因为Reveno会自动了解存储库中的哪些实体是脏的,并将调用相应的实体。 让我们看看TradeAccount如何映射到TradeAccountView:

reveno.domain().viewMapper(TradeAccount.class, 
                           TradeAccountView.class, (id,e,r) ->
                            new TradeAccountView(fromLong(e.balance), 
                            r.linkSet(e.orders(), OrderView.class)));

id是实体的身份; e是实体; r是具有有用方法的特殊映射上下文。 魔术确实发生在r.linkSet(..)调用上。 它懒散地将ID指针的集合映射到精确视图的集合。

我们可以用相同的方式定义Order-> OrderView映射:

reveno.domain().viewMapper(Order.class, OrderView.class, (id,e,r) ->
        new OrderView(fromLong(e.price), e.size, e.symbol, 
                      r.get(TradeAccountView.class, e.accountId)));

您可能会注意到,我们的查询模型由不可变的对象组成,例如事务模型中的实体。 它大大简化了映射逻辑。 同样,这不是限制; 但是否则,映射正确性的全部责任在于您。

执行命令

Reveno中的事务处理本质上是异步的。 在正在运行的引擎上执行命令时,方法调用将立即返回CompletableFuture,最终提供结果。 Reveno在内部具有“管道”,具有多个阶段,每个阶段都处理自己的线程。 在对象之间一个接一个地传递对象的代价非常高,这就是批处理开始发挥作用的地方。 在高压下,Reveno在每个阶段都处理批次,这首先提供了高产量。

完成所有声明工作和业务逻辑实现之后,我们就可以开始使用引擎了。 首先,我们需要启动它:

reveno.startup();

之后,我们可以在系统中创建一个新的交易帐户。 您应该注意,还存在executeCommand()方法的同步版本,该版本对于测试或使用示例非常有用:

long accountId = reveno.executeSync(new CreateAccount("USD", 5.15));

在这种情况下,Reveno将在幕后调用适当的命令和事务处理程序,这将创建一个新的美元货币帐户,初始余额为5.15美元。 我们可以检查正确性如下:

System.out.println(reveno.query().find(TradeAccountView.class, 
                                       accountId).balance);

这将打印«5.15»。 为了使该示例更有趣,让我们向该帐户添加新订单:

long orderId = reveno.executeSync(
                        new MakeOrder(accountId, "EUR/USD", 1, 1.213));

在这里,我们为一个EUR / USD的手创建了一个新的买单,价格为1.213。 之后,我们可以再次检查帐户中的更改:

System.out.println(reveno.query().find(TradeAccountView.class, 
                                       accountId).orders.size());

这将打印«1»,因为我们的帐户现在有一个待处理的订单。 最后,让我们关闭订单,这将导致打开一个EUR / USD头寸并将余额减少1.213。 最终余额应为3.937:

reveno.executeSync(new ExecuteOrder(orderId));
// the balance is expected to be 3.937, after order successfully executed
System.out.println(reveno.query().find(TradeAccountView.class, 
                                       accountId).balance);

坚持不懈

如我们的简介中所述,Reveno首先是一个事务处理框架。 您可以从同一目录安全地重新启动引擎,并查看模型的最新状态。 我们试图使它的每个部分都可配置,这也与耐用性有关。 您可以通过调用reveno.config()检查可用选项。

发布事件

发生在事件处理程序执行之前

事件执行结果也将持久保存到存储中,如果成功完成,通常在引擎重新启动时将不再对其进行处理。 但是,不能严格保证这种行为,因此,如果希望使处理程序具有100%的复制幂等性,则应检查每个事件处理程序中提供的EventMetadata.isReplay标志。

让我们通过发布和处理贸易帐户中余额变化的事件来扩展示例。 首先,我们应该用适当的字段声明它:

public class BalanceChangedEvent {
    public final long accountId;

    public BalanceChangedEvent(long accountId) {
        this.accountId = accountId;
    }
}

当任何帐户的余额发生变化时,我们只需要知道帐户ID,因为在处理程序中,我们可以查询相应的视图。 在这种情况下,我们可以这样声明事件处理程序:

reveno.events().eventHandler(BalanceChangedEvent.class, (e, m) -> {
    TradeAccountView account = reveno.query().find(TradeAccountView.class, 
                                                   e.accountId);
    System.out.println(String.format(
                          "New balance of account %s from event is: %s", 
                        e.accountId, account.balance));
});

因此,我们应该在ChangeBalance事务操作处理程序声明中添加另一行:

reveno.domain().transactionAction(ChangeBalance.class, (a, ctx) -> {
        ctx.repo().remap(a.accountId, TradeAccount.class, 
         (id, e) -> e.addBalance(a.amount));
	// publish an event to all listeners
        ctx.eventBus().publishEvent(new BalanceChangedEvent(a.accountId));
});

最终被发布。 最终,我们将收到以下输出:

来自事件的帐户1的新余额为:5.15

来自事件的帐户1的新余额为:3.937

性能检查

reveno-metrics库,该库有助于跟踪正在运行的引擎的性能指标。 与所有内容一样,我们的指标库已使用堆外内存和无锁代码进行了优化,因此对整体性能的影响极低。 它还支持诸如Graphite之类的流行监视系统。

(应该注意的是,衡量指标主要是一种性能监控工具,而不是微基准测试框架。要获得准确的基准测试结果,请考虑使用JMH或类似工具。)

在我们的代码中初始化指标收集以使用Reveno Slf4j接收器并在MacBook Pro 2.7 GHz i5 CPU上运行ChangeBalance命令4500万次(带有预热迭代)之后:

  • reveno.instances.MAC-15_local.default.latency.mean:68804
  • reveno.instances.MAC-15_local.default.latency.min:775
  • reveno.instances.MAC-15_local.default.latency.max:522265
  • reveno.instances.MAC-15_local.default.throughput.hits:1183396

这些数字实际上意味着在每秒总吞吐量1,183,396笔交易中,我们的平均延迟时间为68微秒,最小延迟时间为775纳秒,最大延迟时间为522微秒。 考虑到在后台完成的工作量和耐用性水平,结果令人印象深刻。

结论

今天,Reveno是一个年轻但Swift发展的框架。 您可以访问我们的官方网站以了解更多信息。 我们随时欢迎任何建议和反馈。 您也可以加入我们的Google网上论坛,在“ 问题”页面上应用错误,或通过私人查询或其他方式直接写信至mailto:support@reveno.org 。

全文演示位于github上 。 您还可以找到Reveno用法的示例 (或自行提出请求)。

翻译自:

cqrs框架