本章将介绍有关为事件优化数据库的几个注意事项。
SQL数据库
如果您已经使用JPA实现(例如Hibernate)自动生成表,那么您可能没有在表上设置所有正确的索引。事件存储的不同用法要求设置不同的索引以获得最佳性能。此列表建议为默认EventStorageEngine实现使用的不同类型的查询添加索引:
Hibernate等生成的默认列长度可能有效,但不是最佳的。例如,UUID的长度总是相同的。可以使用36个字符的固定长度列作为聚合标识符,而不是255个字符的可变长度列。
DomainEventEntry表中的timestamp列只存储iso8601时间戳。如果所有时间都存储在UTC时区中,则它们需要24个字符的列长度。如果你使用另一个时区,这可能是28。通常不需要使用可变长度的列,因为时间戳的长度总是相同的。
警告:
强烈建议以UTC格式存储所有时间戳。在采用夏令时的国家,将时间戳存储在本地时间中可能会导致时区切换前后和期间生成的事件的排序错误。使用UTC时不会发生这种情况。某些服务器配置为始终使用UTC。或者,您应该将事件存储配置为在存储时间戳之前将其转换为UTC。
DomainEventEntry中的type列存储聚合的类型标识符。通常,这些是聚合的“简单名称”。甚至在Spring中臭名昭著的AbstractDependencyInjectionsSpringContextTests也只有45个字符。这里,同样,一个较短(但可变)长度的字段就足够了。
自动增量和序列
当使用关系数据库作为事件存储时,Axon依赖于一个自动递增值,以允许跟踪处理器大致按照插入顺序读取所有事件。我们说“粗略”,因为“插入顺序”和“提交顺序”是不同的东西。
虽然自动增量值(通常)是在插入时生成的,但这些值只在提交时可见。这意味着另一个进程可能会观察到这些序列号以不同的顺序到达。虽然Axon有确保最终处理所有事件的机制,即使当它们以不同的顺序可见时,也有局限性和性能方面需要考虑。
当跟踪处理器读取事件时,它使用“全局序列”来跟踪其进度。当事件以不同于插入顺序可用时,axon将遇到一个“缺口”。Axon将记住这些“间隙”,以验证自上次读取以来数据是否可用。这些差距可能是由于事件以不同的顺序变得可见,但也因为事务已回滚。强烈建议确保不存在因过度急切地增加序列号而存在的间隙。检查间隙的机制很方便,但会影响性能。
使用JpaEventStorageEngine时,Axon依赖JPA实现来创建表结构。虽然这可以工作,但不太可能为正在使用的数据库引擎提供具有最佳性能的配置。这是因为Axon使用@GeneratedValue注释的默认设置。
要覆盖这些设置,请创建一个名为/META-INF/orm.xml的文件在类路径上,如下所示:、
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings version="1.0" xmlns="http://java.sun.com/xml/ns/persistence/orm">
<mapped-superclass access="FIELD" metadata-complete="false" class="org.axonframework.eventhandling.AbstractSequencedDomainEventEntry">
<attributes>
<id name="globalIndex">
<generated-value strategy="SEQUENCE" generator="myGenerator"/>
<sequence-generator name="myGenerator" sequence-name="mySequence"/>
</id>
</attributes>
</mapped-superclass>
</entity-mappings>
必须指定metadata complete=“false”。这表明应该使用此文件替代现有注释,而不是替换它们。为了获得最佳结果,请确保DomainEventEntry表使用自己的序列。这可以通过仅为该实体指定不同的序列生成器来确保。
自定义消息处理可以成为应用程序中所需的调整。考虑更改消息的处理方式,或者更改将注入消息处理函数的参数。
在实现团队中已建立的最佳实践时,重写注释非常有用,提供了注释的使用方式的默认值或限制。但是,当需要根据注释的存在向消息处理程序添加特殊行为时,它们也非常有用。
您可以通过扩展ParameterResolverFactory类并创建包含实现类完全限定名的/META-INF/services/org.axonframework.messaging.annotation.ParameterResolverFactory文件来配置ParameterResolver。
注意:
目前,OSGi支持受到限制,因为清单文件中提到了所需的头文件。自动检测ParameterResolverFactory实例在OSGi中工作,但是由于类加载器的限制,可能需要复制/META-INF/services/org.axonframework.messaging.annotation. ParameterResolverFactory的内容到OSGI包中那个含有要为其解析参数的类(即事件处理程序)的文件。
处理程序增强器允许您包装处理程序,并将自定义逻辑添加到执行中,或添加特定消息的处理程序的合格性。这与HandlerInterceptors的不同之处在于,在解析时您可以访问聚合成员,并且它允许更细粒度的控制。您可以使用@handlers或@handlers对@groups执行检查。
要创建处理程序增强器,首先要实现HandlerEnhancerDefinition并重写wrapHandler()方法。这个方法所做的就是允许您访问MessageHandlingMember<T>,它是一个表示系统中指定的任何处理程序的对象。
然后,您可以使用annotationAttributes(annotationannotation)方法根据它们的注释对这些处理程序进行排序。这将只过滤出使用该注释的处理程序。
要运行处理程序增强器,您需要创建META-INF/services/org.axonframework.messaging.annotation.HandlerEnhancerDefinition包含已创建的处理程序增强器的完全限定类名的文件,或在配置器中显式注册它。
基于预期元数据键和值筛选消息的处理程序增强器示例。
// 1
public class ExampleHandlerDefinition implements HandlerEnhancerDefinition {
@Override // 2
public <T> MessageHandlingMember<T> wrapHandler(MessageHandlingMember<T> original) {
return original.annotationAttributes(MyAnnotation.class) // 3
.map(attr -> (MessageHandlingMember<T>)
new ExampleMessageHandlingMember<>(original, attr))
.orElse(original); // 5
}
private static class ExampleMessageHandlingMember<T> extends WrappedMessageHandlingMember<T>{
private final String metaDataKey;
private final String expectedValue;
private ExampleMessageHandlingMember(
MessageHandlingMember<T> delegate,
Map<String, Object> annotationAttributes) {
super(delegate);
metaDataKey = (String) annotationAttributes.get("metaDataKey");
expectedValue = (String) annotationAttributes.get("expectedValue");
}
@Override
public boolean canHandle(Message<?> message) {
return super.canHandle(message) && expectedValue.equals(message.getMetaData().get(metaDataKey)); // 4
}
}
}
可以使用Axon配置配置HandlerDefinition。如果您使用springboot来定义handlerdefinitions和HandlerEnhancerDefinitions就足够了(Axon自动配置将在Axon配置中获取并配置它们)。
Axon 配置 API:
Configurer configurer = DefaultConfigurer.defaultConfiguration();
configurer.registerHandlerDefinition((c, clazz) -> MultiHandlerDefinition.ordered( MultiHandlerEnhancerDefinition.ordered( ClasspathHandlerEnhancerDefinition.forClass(clazz),
new MyCustomEnhancerDefinition()),new MyCustomHandlerDefinition(), ClasspathHandlerDefinition.forClass(clazz)
));
Springboot 自动配置:
// somewhere in configuration
// for a HandlerDefinition
@Bean
public HandlerDefinition myCustomHandlerDefinition() {
return new CustomHandlerDefinition();
}
// or to define a handler enhancer
@Bean
public HandlerEnhancerDefinition myCustomHandlerEnhancerDefinition() {
return new MyCustomEnhancerDefinition();
}
Axon中的大多数注释可以作为元注释放在其他注释上。当Axon扫描注释时,它也会自动扫描元注释。如果需要,注释可以覆盖在元注释上定义的属性。
例如,如果您的开发团队中有一个实践,即负载总是用JSON表示,并且希望显式配置命令名,那么可以创建自己的注释:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE})
@CommandHandler(payloadType = JsonNode.class)
public @interface JsonCommandHandler {
String commandName;
String routingKey() default "";
}
通过在@CommandHandler元注释上指定payloadType,这将成为所有用JsonCommandHandler注释的命令处理程序所使用的值。这些命令处理程序可能(也应该)为有效负载提供一个参数,但是如果不是JsonNode的子类,Axon也会抱怨的。
JsonCommandHandler注释上的commandName属性没有默认值,因此将强制开发人员指定命令的名称。注意,要重写值,属性名必须与@CommandHandler元注释上的名称相同。
最后,routingKey属性的定义与@CommandHandler注释的规范完全相同,这样开发人员在使用JsonCommandHandler时仍然可以选择提供路由键。
编写自定义逻辑以访问可能是元注释的批注的属性时,请确保使用AnnotationUtils#findAnnotationAttributes(AnnotatedElement,String)方法或MessageHandlingMember上的annotationAttributes。使用Java的注释API不会考虑元注释。
Axon框架使用IdentifierFactory来生成所有标识符,无论它们是用于事件、命令还是查询。默认的IdentifierFactory使用随机生成的java.util.UUID基于标识符。虽然它们使用起来非常安全,但生成它们的过程在性能上并不出众。
IdentifierFactory是一个抽象工厂,它使用Java的ServiceLoader(从Java6开始)机制来查找要使用的实现。这意味着您可以创建自己的工厂实现,并将实现的名称放入名为/META-INF/services/org.axonframework.common. IdentifierFactory的文件中。Java的ServiceLoader机制将检测到该文件并尝试创建一个名为inside的类的实例。
对IdentifierFactor有一些要求。它必须:
原则
Axon服务器查询语言处理一系列事件。处理步骤包括在管道中定义的过滤器和投影。查询引擎执行管道中的每个步骤,并将结果转发给下一个步骤。返回最后一步的结果。这个想法基于UNIX管道命令。
查询的输入是具有以下字段的事件流:
过滤器
过滤器是计算结果为true或false的表达式。基本过滤操作在字段和其他字段或固定值之间进行比较。以下是有效过滤器的示例:
token > 1000000
aggregateIdentifier = "1234"
payloadType = aggregateType
基本比较运算符:
所有比较运算符都期望相同的类型,或执行字符串比较。
可以使用逻辑运算符组合筛选器表达式:
and not or
可以在表达式中使用括号来更改计算顺序。
在表达式中,可以使用基本算术运算符:
“+”就是用来数值加减的,其他情况的‘+'就是在String中执行连接。
除了这些运算符,还有两个匹配函数:
contains:如果两个参数都是字符串值,则当第一个参数包含第二个参数时,则为true。如果第一个参数是列表,若列表包含第二个值,则返回true。
match:将第一个参数的值与正则表达式(regexp格式与Java中的格式相同)进行比较。
函数名可以用传统方式使用,但是对于二进制函数也可以用中缀方式。因此,以下两个样本均有效:
contains(payloadData, "Smith")
payloadData contains "Smith"
投影
投影函数改变数据的形态。提供以下投影功能:
select-将流中的每个元素映射到一个新元素,例如具有较少字段或计算字段。
groupby-将groupby字段的值相同的元素映射到新元素。
count-统计流中的元素数,当与参数一起使用时,统计参数值。
min-参数值的最小值
max-参数值的最大值
avg-参数值的平均值
count、min、max和avg函数也可以在groupby中使用。
例如:
其他函数
例如:
管道
表达式可以放在一个管道中
aggregateType contains "abcde" | groupby(payloadType, count())
甚至还能更多内容:
aggregateType contains "abcde" | groupby(payloadType, count() as count) | count > 10
时间限制
当事件存储包含数百万个事件时,通常不需要搜索所有事件。您可以向管道添加时间限制,以仅搜索最近的事件。
groupby(payloadType, count()) | last minute 以上两个是一样的,时间限制可以放在管道中的任何地方,并且都能指代事件中的时间戳。