上篇描述的kafka案例是个库存管理平台。是一个公共服务平台,为其它软件模块或第三方软件提供库存状态管理服务。当然,平台管理的目标必须是共享的,即库存是作为公共资源开放的。这个库存管理平台是一个Kafka消费端独立运行的软件。kafka的生产方即平台的服务对象通过kafka生产端producer从四面八方同时、集中将消息写入kafka。库存管理平台在kafka消费端不间断监控kafka里新的未读过的消息并及时读取,解析消息获取发布者对库存管理的指令,然后按指令更新库存状态。

设计这个库存管理平台最主要的目的先是为了保证库存状态的时效性、准确性,然后才是库存更新的效率。由于库存更新指令的产生是在一个高并发、异类系统、分布式环境里,上篇已经提到多线程环境下更新共享数据会产生的问题。不过通过kafka把并发产生的指令转换成队列然后按顺序单线程逐句执行就能解决主要问题了。现在,平台的数据来源变成kafka消费端口上的一个数据流了,数据的读取和消费自然也变成了逐条的。kafka提供了某种游标机制来记录数据读取的最新位置,防止数据消费过程中的遗漏、重复。记录当前读取位置offset的方式就是所谓数据消费模式代表数据消费不同程度的安全/效率比例,安全系数越高,流量越低。具体读取位置offset可以存放在kafka内部,或者保存在某种数据库表里。简单来讲,数据消费模式分三种:至多一次at-most-once,至少一次at-least-once,只此一次exactly-once。

从由kafka中读出指令到成功完成执行指令整个消息消费过程可能经历多个步骤。每个步骤都可能有失败的可能,从而中断过程影响数据消费结果。保存offset即offset-commit的时间点代表了三种消费模式的特性:

1、至多一次at-most-once:读出数据立即commit-offset,然后才开始消费数据。无论消费过程中发生异常与否,下次都会从新的位置开始读取,过去不再。如果一条数据在消费过程中发生事故中断了过程,那这条数据就没有发生应有的作用,就等于遗失了。

2、至少一次at-least-once:读出数据、消费数据、然后才commit-offset。如果消费过程出现问题中断,那么offset就得不到保存,下次再读取时还是从原先位置重新开始。所以,一条数据有可能被多次读取,造成重复消费的效果。

3、只此一次exactly-once:把保存offse和消费过程放到同一个事务transaction里。这种模式需要数据库事物处理支持,也就是说offset-commit和数据处理都必须在同一种提供事物处理支持的数据库环境里进行。offset-commit只会在确保消费过程成功完成后才进行。

at-most-once和at-least-once都使用kafka内部commit机制保存offset。at-least-once可以利用kafka的自动commit机制实现offset保存,只要通过kafka配置就可以了。下面是这个配置的示范:

 val consumerSettings =
ConsumerSettings(consumerConfig, new StringDeserializer, new StringDeserializer)
.withBootstrapServers(bootstrapServers)
.withGroupId(group)
.withProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset)
.withProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true")
.withProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitIntervalMs.toString)

ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG = "true" 代表开启auto-commit模式。ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG设置了auto-commit之间的毫秒时间间隔。在这个间隔内如果中断消费过程,那么在这个间隔内读取所有数据的offset都未能commit,但其中有些数据已经完成消费了。重启读取就会从这个间隔开始时的offset从头读取,那么之前消费的数据就会再次消费,等于重复消费了。auto-commit间隔设置的越短,重复消费的数据就越少,不过kafka需要更密集的进行commit-offset,运行效率就越低。反之,重复消费的数据量就越大,消费计算精确度越低,但运行效率就会提高。

在alpakka-kafka里用一个普通的Source就可以实现at-least-once消费模式了:

val consumerSettings =
ConsumerSettings(consumerConfig, new StringDeserializer, new StringDeserializer)
.withBootstrapServers(bootstrapServers)
.withGroupId(group)
.withProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset)
.withProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true")
.withProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitIntervalMs.toString) var subscription = Subscriptions
.topics(topic) val stkTxns = new DocToStkTxns(trace)
val curStk = new CurStk(trace)
val pcmTxns = new PcmTxns(trace) val plainSource = Consumer
.plainSource(consumerSettings,subscription)

run这个plainSource形成的akka-stream就实现了一个完整kafka-reader功能:

    plainSource
.mapAsync(1) {msg =>
updateStock(msg)
}
.toMat(Sink.seq)(Keep.left)
.run()

offset-commit在这个reader-stream里是不可控的,是kafka按预先设置自动进行的。

plainSource是一个独立的stream,代表单个reader。为了充分利用平台的硬件资源,首先考虑的是同时运行多个stream,如下:

 (1 to numReaders).toList.map { _ =>
plainSource
.mapAsync(1) {msg =>
updateStock(msg)
}
.toMat(Sink.seq)(Keep.left)
.run()
}

这样可以同时运行numReaders条stream。不过,现在设计方案又返回了多线程环境,好像又要面临多并发所产生的一系列问题了。我们来分析分析:首先,前面描述的库存更新多线程竞争问题主要是针对同一门店,同一商品,同时更新库存状态引发的。以上设计中每条stream,即每个reader,如果属于同一个reader-group(group-id相同)的话,应共同分别负责所有partition中的部分partition,是不会共享partition的。那么,写入每个partition的数据是否交叉重复就很关键了。实际上,在上游消息发布阶段决定了消息应该写入的具体partition,如下:

def writeToKafka(posTxn: PosTxns)(implicit producerKafka: ProducerKafka) = {
val doc = BizDoc.fromPosTxn(posTxn)
if (producerKafka.producerSettings.isDefined) {
implicit val producer = producerKafka.akkaClassicSystem.get
SendProducer(producerKafka.producerSettings.get)
.send(new ProducerRecord[String, String](producerKafka.publisherSettings.topic, doc.shopId, toJson(doc)))
} else FastFuture.successful(Completed)
}

ProducerRecord[K,V] 的key设定为shopId,具体目标partition由kafka的默认指派算法根据key的值产生,保证同一key值一定会指派给同一个partition。虽然在门店数量>partition数量的情况下每个partition可以包含多个shopId, 但各partition所包含的shopId不会交叉重复。所以,以上多reader同时运行的设计中,只要属于同一个reader-group,shopId就不会相同,就不会产生线程竞争问题。

那么,在同一个reader的消费过程中是否能使用多线程方式呢?上面的例子中使用了mapAsync(parallelism=1),这个代表了stream里的一个阶段。这个阶段容许收到上游数据后以parallelism个future来并行处理,同时可以保证流出下游的数据遵守上游流入数据的顺序。但是,在同一阶段用多线程方式计算方式在遇到同门店、同商品库存更新时同样会产生多线程竞争问题,所以只能取parallelism=1。不过,可以考虑把数据处理过程分割成几个阶段,因为每个阶段流入流出的数据是同循序的,所以可以容许多个阶段在在各自的线程里运算。如:

 (1 to numReaders).toList.map { _ =>
plainSource
.mapAsync(1) {msg =>
produceStkTxns(msg)
}
asyn.mapAsync(1) {msg =>
updateCurStock(msg)
}
asyn.mapAsync(1) {msg =>
updatePurchase(msg)
}
.toMat(Sink.seq)(Keep.left)
.run()
}

可以用asyn.mapAsync来分割异线程域async-boundary以实现多线程运算效果。

下面的完整例子里把异常处理和重启也考虑了进去:

  def start =
(1 to numReaders).toList.map { _ =>
RestartSource
.onFailuresWithBackoff(restartSource) { () => plainSource }
// .viaMat(KillSwitches.single)(Keep.right)
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-msg: $msg")(Messages.MachineId("", ""))
}
_ <- stkTxns.docToStkTxns(msg.value())
pmsg <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-docToStkTxns: $msg")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updateStk: msg: $msg")(Messages.MachineId("", ""))
}
curstks <- curStk.updateStk(msg.value())
pmsg<- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updateStk: curstks-$curstks")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-writePcmTxn: msg: $msg")(Messages.MachineId("", ""))
}
pcm <- pcmTxns.writePcmTxn(msg.value())
pmsg <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updateStk: writePcmTxn-$pcm")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updatePcm: msg: $msg")(Messages.MachineId("", ""))
}
_ <- pcmTxns.updatePcm(msg.value())
_ <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updateStk: updatePcm-$msg")(Messages.MachineId("", ""))
}
} yield "Completed"
}
.toMat(Sink.seq)(Keep.left)
.run()
}

下面是几个消费模式的测试示范代码:

package com.datatech.txn.server
import akka.actor.ActorSystem
import scala.concurrent._
import MgoRepo._
import com.typesafe.config.ConfigFactory
import scala.jdk.CollectionConverters._ object ConsumeModeTest extends App with JsonConverter {
val config_onenode = ConfigFactory.load("onenode")
implicit val system = ActorSystem("kafka-sys",config_onenode)
var config = ConfigFactory.load() implicit val ec: ExecutionContext = system.dispatcher //mat.executionContext var httpport: Int = 53081
var mongohosts = List("localhost:27017")
var elastichost = "http://localhost:9200"
var _http_parallelism: Int = 8
var _seednodes: String = "" val txnCfg = ConfigFactory.load("txnserver.conf").getConfig("txn.server")
try {
mongohosts = txnCfg.getStringList("mongohosts").asScala.toList
elastichost = txnCfg.getString("elastichost")
_http_parallelism = txnCfg.getInt("http_parallelism")
_seednodes = txnCfg.getString("seednodes")
httpport = txnCfg.getInt("httpport")
}
catch {
case excp: Throwable =>
httpport = 53081
mongohosts = List("localhost:27017")
elastichost = "http://localhost:9200"
_http_parallelism = 8
} implicit val mgoClient = mongoClient(mongohosts) val readerConfig = config.getConfig("akka.kafka.consumer")
val readerSettings = ReaderSettings(config.getConfig("kafka-txnserver-consumer")) implicit val idxer = new TxnIndex(elastichost,true) readerSettings.consumeMode.toLowerCase() match {
case "atleastonce" =>
val readerGroup = AtLeastOnceReaderGroup(readerConfig,readerSettings, true)
readerGroup.start
case "atmostonce" =>
val readerGroup = AtMostOnceReaderGroup(readerConfig,readerSettings, true)
readerGroup.start
case "exactlyonce" =>
val readerGroup = ExactlyOnceReaderGroup(readerConfig,readerSettings, true)
readerGroup.start
case _ =>
val readerGroup = AtLeastOnceReaderGroup(readerConfig,readerSettings, true)
readerGroup.start
} scala.io.StdIn.readLine()
idxer.close()
scala.io.StdIn.readLine()
system.terminate() }

alpakka-kafka(7)-kafka应用案例,消费模式的更多相关文章

  1. kafka channle的应用案例

      kafka channle的应用案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 最近在新公司负责大数据平台的建设,平台搭建完毕后,需要将云平台(我们公司使用的Ucloud的 ...

  2. SpringBoot2 整合Kafka组件,应用案例和流程详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.搭建Kafka环境 1.下载解压 -- 下载 wget http://mirror.bit.edu.cn/apache/kafka/2.2 ...

  3. Kafka(3)--kafka消息的存储及Partition副本原理

    消息的存储原理: 消息的文件存储机制: 前面我们知道了一个 topic 的多个 partition 在物理磁盘上的保存路径,那么我们再来分析日志的存储方式.通过 [root@localhost ~]# ...

  4. kafka实战教程(python操作kafka),kafka配置文件详解

    kafka实战教程(python操作kafka),kafka配置文件详解 应用往Kafka写数据的原因有很多:用户行为分析.日志存储.异步通信等.多样化的使用场景带来了多样化的需求:消息是否能丢失?是 ...

  5. CentOS 7部署Kafka和Kafka集群

    CentOS 7部署Kafka和Kafka集群 注意事项 需要启动多个shell脚本交互客户端进行验证,运行中的客户端不要停止. 准备工作: 安装java并设置java环境变量,在`/etc/prof ...

  6. Kafka记录-Kafka简介与单机部署测试

    1.Kafka简介 kafka-分布式发布-订阅消息系统,开发语言-Scala,协议-仿AMQP,不支持事务,支持集群,支持负载均衡,支持zk动态扩容 2.Kafka的架构组件 1.话题(Topic) ...

  7. Apache Kafka安全| Kafka的需求和组成部分

    1.目标 - 卡夫卡安全 今天,在这个Kafka教程中,我们将看到Apache Kafka Security 的概念  .Kafka Security教程包括我们需要安全性的原因,详细介绍加密.有了这 ...

  8. kafka - Confluent.Kafka

    上个章节我们讲了kafka的环境安装(这里),现在主要来了解下Kafka使用,基于.net实现kafka的消息队列应用,本文用的是Confluent.Kafka,版本0.11.6 1.安装: 在NuG ...

  9. Python+SparkStreaming+kafka+写入本地文件案例(可执行)

    从kafka中读取指定的topic,根据中间内容的不同,写入不同的文件中. 文件按照日期区分. #!/usr/bin/env python # -*- coding: utf-8 -*- # @Tim ...

  10. kafka拦截器原理|案例实操

    拦截器原理 Producer拦截器(interceptor)是在Kafka 0.10版本被引入的,主要用于实现clients端的定制化控制逻辑. 对于producer而言,interceptor使得用 ...

随机推荐

  1. CAS你知道吗?底层如何实现?ABA问题又是什么?关于这些你知道答案吗

    CAS你知道吗?如何实现? 1. compareAndSet 在volatile当中我们提到,volatile不能保证原子语义,所以当用到变量自增时,如果用到synchronized会太"重 ...

  2. 12-1 MySQL数据库备份(分库)

    #!/bin/bash source /etc/profile DATE="$(date +%F_%H-%M-%S)" DB_IP="172.16.1.122" ...

  3. 10 一键部署LNMP网站平台

    #!/bin/bash export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin NGINX_V=1.19.1 P ...

  4. 工作3年,还不会写单元测试?新技能get!

    历史遗留代码不敢重构? 每次改代码都要回归所有逻辑? 提测被打回? 在近期的代码重构的过程中,遇到了各式各样的问题.比如调整代码顺序导致bug,取反操作逻辑丢失,参数校验逻辑被误改等. 上线前需要花大 ...

  5. SpringBoot 优雅整合Swagger Api 自动生成文档

    前言 一个好的可持续交付的项目,项目说明,和接口文档是必不可少的,swagger api 就可以帮我们很容易自动生成api 文档,不需要单独额外的去写,无侵入式,方便快捷大大减少前后端的沟通方便查找和 ...

  6. 13 shell while循环与until循环

    while 循环是 Shell 脚本中最简单的一种循环,当条件满足时,while 重复地执行一组语句,当条件不满足时,就退出 while 循环. unti 循环和 while 循环恰好相反,当判断条件 ...

  7. 使用Hugo框架搭建博客的过程 - 前期准备

    前言 这篇教程介绍了如何搭建这样效果的博客. 所需步骤 可以从这样的角度出发: 注册域名. 使用CDN加快网站访问速度. 网站内容需要部署在服务器或对象存储平台上. 重要的是放什么内容.博客需要选择框 ...

  8. Django基础-04篇 Django开发前后端联动

    1. 写views views.py代码块 1.在前端以/article/{{ article.id }}这种方式请求后台, 参数配置在urls.py中path('category/<int:i ...

  9. 利用扫描仪形成PDF

    1.打开WPS,新建PDF,从扫描仪新建 2.合并PDF:按照顺序添加指定PDF,合并即可完成

  10. Spring Boot入门学习必知道企业常用的Starter

    SpringBoot企业常用的 starter SpringBoot简介 SpringBoot运行 SpringBoot目录结构 整合JdbcTemplate @RestController 整合JS ...