Apache kafka 卡夫卡流:第n次事件的行动
我试图找到在卡夫卡流中第n个事件上执行动作的最佳方式 我的案例:我有一个包含一些事件的输入流。我必须按eventType==login对它们进行过滤,并在同一accountId的第n次登录(比如第五次登录)时将此事件发送到输出流 经过一些调查和不同的尝试,我得到了下面代码的版本(我使用的是Kotlin)Apache kafka 卡夫卡流:第n次事件的行动,apache-kafka,apache-kafka-streams,spring-cloud-stream,spring-cloud-stream-binder-kafka,Apache Kafka,Apache Kafka Streams,Spring Cloud Stream,Spring Cloud Stream Binder Kafka,我试图找到在卡夫卡流中第n个事件上执行动作的最佳方式 我的案例:我有一个包含一些事件的输入流。我必须按eventType==login对它们进行过滤,并在同一accountId的第n次登录(比如第五次登录)时将此事件发送到输出流 经过一些调查和不同的尝试,我得到了下面代码的版本(我使用的是Kotlin) fun过程(){ val userLoginsStoreBuilder=Stores.keyValueStoreBuilder( Stores.persistentKeyValueStore(“
fun过程(){
val userLoginsStoreBuilder=Stores.keyValueStoreBuilder(
Stores.persistentKeyValueStore(“登录”),
Serdes.String(),
Serdes.Integer()
)
val streamsBuilder=streamsBuilder().addStateStore(userCheckInsStoreBuilder)
val inputStream=streamsBuilder.stream(inputTopic)
inputStream.map{键,事件->
KeyValue(key,json.readValue(事件))
}.filter{,event->event.eventType==“login”}
.map{key,event->KeyValue(event.accountId,LoginEvent(key,event))}
.变换(
用户登录变压器(“登录”,5),
“登录”
)
.filter{,value->value}
.map{key,->KeyValue(key.eventKey,json.writeValueAsString(key.eventValue))}
.to(“第五次登录”,生成.with(Serdes.String(),Serdes.String()))
...
}
类UserLoginsTransformer(private val storeName:String,private val loginsThreshold:Int=5):
变压器供应商>{
重写fun get():Transformer>{
返回对象:Transformer>{
私有lateinit变量存储:KeyValueStore
@抑制(“未选中的_CAST”)
重写fun init(上下文:ProcessorContext){
store=context.getStateStore(storeName)作为KeyValueStore
}
覆盖有趣的转换(键:字符串,值:LoginEvent):键值{
val计数器=(存储.获取(键)?:0)+1
如果返回(计数器==loginsThreshold){
store.delete(键)
KeyValue(值,true)
}否则{
储存。放置(钥匙、柜台)
KeyValue(值,false)
}
}
覆盖乐趣关闭(){
}
}
}
}
我最担心的是transform
函数在我的例子中不是线程安全的。我已经检查了在我的案例中使用的KV存储的实现,这是RocksDB存储(非事务性),因此值可能会在读取和比较之间更新,错误事件将发送到输出
我的其他想法:
accountId
分区的,这样单个帐户的登录事件就可以转到同一个shard
如果输入的数据在写入输入主题时已被accountId
分区,则无需执行任何操作。如果没有,并且您可以控制上游应用程序,那么在上游的应用程序生成器中使用自定义分区器来获得所需的分区可能是最简单的。如果无法更改上游应用程序,则需要在将accountId
设置为新密钥后重新分区数据,即在调用transform()
之前执行到()
我最担心的是转换函数在我的例子中不是线程安全的。我已经检查了在我的案例中使用的KV存储的实现,这是RocksDB存储(非事务性),因此值可能会在读取和比较之间更新,错误事件将发送到输出
没有理由担心。如果使用多个线程运行,则每个线程都将拥有自己的RocksDB,该RocksDB存储总体数据的一个分片(注意,总体状态在输入主题分区中是分片的,并且单个分片永远不会由不同的线程处理)。因此,您的代码将正常工作。您唯一需要确保的是,数据是按accountId
分区的,这样单个帐户的登录事件就可以转到同一个shard
如果输入的数据在写入输入主题时已被
accountId
分区,则无需执行任何操作。如果没有,并且您可以控制上游应用程序,那么在上游的应用程序生成器中使用自定义分区器来获得所需的分区可能是最简单的。如果无法更改上游应用程序,则需要在将accountId
设置为新密钥后重新分配数据,即在调用transform()
之前执行through()
,我无法控制上游,因此需要重新分配数据
data class Event(
val payload: Any = {},
val accountId: String,
val eventType: String = ""
)
// intermediate class to keep the key and value of the original event
data class LoginEvent(
val eventKey: String,
val eventValue: Event
)
fun process() {
val userLoginsStoreBuilder = Stores.keyValueStoreBuilder(
Stores.persistentKeyValueStore("logins"),
Serdes.String(),
Serdes.Integer()
)
val streamsBuilder = StreamsBuilder().addStateStore(userCheckInsStoreBuilder)
val inputStream = streamsBuilder.stream<String, String>(inputTopic)
inputStream.map { key, event ->
KeyValue(key, json.readValue<Event>(event))
}.filter { _, event -> event.eventType == "login" }
.map { key, event -> KeyValue(event.accountId, LoginEvent(key, event)) }
.transform(
UserLoginsTransformer("logins", 5),
"logins"
)
.filter { _, value -> value }
.map { key, _ -> KeyValue(key.eventKey, json.writeValueAsString(key.eventValue)) }
.to("fifth_login", Produced.with(Serdes.String(), Serdes.String()))
...
}
class UserLoginsTransformer(private val storeName: String, private val loginsThreshold: Int = 5) :
TransformerSupplier<String, CheckInEvent, KeyValue< LoginEvent, Boolean>> {
override fun get(): Transformer<String, LoginEvent, KeyValue< LoginEvent, Boolean>> {
return object : Transformer<String, LoginEvent, KeyValue< LoginEvent, Boolean>> {
private lateinit var store: KeyValueStore<String, Int>
@Suppress("UNCHECKED_CAST")
override fun init(context: ProcessorContext) {
store = context.getStateStore(storeName) as KeyValueStore<String, Int>
}
override fun transform(key: String, value: LoginEvent): KeyValue< LoginEvent, Boolean> {
val counter = (store.get(key) ?: 0) + 1
return if (counter == loginsThreshold) {
store.delete(key)
KeyValue(value, true)
} else {
store.put(key, counter)
KeyValue(value, false)
}
}
override fun close() {
}
}
}
}