cqrs框架
如今,交易处理无处不在,从使用关系数据库处理购买的各个零售网站到每秒处理10万多个订单的实时交易系统。
Reveno是基于CQRS和事件源模式的基于JVM的无锁事务处理新框架。 尽管它是一个简单而强大的工具,但不会影响性能。 所有事务都保留在只读日记帐中,并且只需按顺序重播这些事件即可恢复域模型的最新状态。 所有运行时操作都是在内存中执行的,因此吞吐量可以达到每秒数百万个事务的数量级,平均等待时间约为微秒。 但是Reveno仍然具有通用的功能,因为它涵盖了具有丰富引擎配置的各种用例。 例如,您可以更改持久性配置,从非常宽松(以获得额外的吞吐量)到严格限制(数据丢失容忍度低)。
当然,对整个用例的要求会有很大的不同,并且一般框架应该考虑所有可能性。 在本文中,我想提供一个使用Reveno框架实现简单交易系统的示例。
现在,让我们检查一下Reveno如何处理您的域模型。 作为基于CQRS的框架,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))
);
这里有很多事情,所以让我们仔细看看。 首先,我们定义一个CreateAccount命令处理程序,该命令处理程序将生成下一个帐户ID,并为实际帐户创建执行交易操作。 如果定义了初始余额,则还将执行ChangeBalance事务操作。 值得注意的是, 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()检查可用选项。
发布事件
Reveno有其自己的事件处理子系统。 您的工作是定义事件类(可能是POJO)及其处理程序,并从事务操作中发布它们。 同样重要的是要注意,只有在命令成功执行后,事件才会发布,并且它们是完全异步的。 所有视图映射严格发生在事件处理程序执行之前 。
事件执行结果也将持久保存到存储中,如果成功完成,通常在引擎重新启动时将不再对其进行处理。 但是,不能严格保证这种行为,因此,如果希望使处理程序具有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));
});
由于ChangeBalance事务操作在多个命令中使用,因此通过添加该事件发布,我们将始终接收有关它的事件。 您还应该注意,publishEvent调用立即返回,并且该事件最终被发布。 最终,我们将收到以下输出:
来自事件的帐户1的新余额为:5.15
来自事件的帐户1的新余额为:3.937
性能检查
现在,既然我们已经启动并运行了所有程序,那么查看应用程序实际可以处理的负载将很有趣。 Reveno带有一个非常有用的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框架