上篇描述的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. Series 1 java秒组合数

    Series 1 举几个例子发现,  系数中间对称,很容易想到组合数 c(n,m)==c[n,n-m).此题就是高精度求组合数,java秒之. time:1825ms   ,接近时限,如果n还稍微大一 ...

  2. ubuntu 替换某一内核模块

    流程 方法一 以下配置仅执行一次,并以 linux kernel 3.13.0 为例 $ cd ~ $ apt-get source linux-source-3.13.0 $ cd linux-3. ...

  3. SpringCloud:feign对象传参和普通传参及遇到的坑

    对象传参: #使用@RequestBody来指定传参对象 @RequestMapping(value = "/v2/matterCode/genCode", method = Re ...

  4. <c:out>标签不能正确输出value中的值

    问题: 我打算在jsp中输出request中的值,它的key为username, <c:out value="${requestScope.username}"/> 但 ...

  5. Message /index.jsp (line: [17], column: [45]) The JSP specification requires that an attribute name is preceded by whitespace

    Error: Message /index.jsp (line: [17], column: [45]) The JSP specification requires that an attribut ...

  6. buu 新年快乐

    一.查壳 发现是upx的壳. 二.拖入ida,发现要先脱壳. 题外话.总结一下手动脱壳,esp定律: 1.先单步到只有esp红色时,右键数据窗口跟随. 2.到数据窗口后,左键硬件访问,byte和wor ...

  7. Leetcode No.122 Best Time to Buy and Sell Stock II Easy(c++实现)

    1. 题目 1.1 英文题目 You are given an array prices where prices[i] is the price of a given stock on the it ...

  8. webdriver xpath

    aa=wd.find_elements_by_xpath('//a') for a in aa: print(a.text) #显示所有A标签中文本 aa=wd.find_elements_by_xp ...

  9. 【Java数据结构与算法】简单排序、二分查找和异或运算

    简单排序 选择排序 概念 首先,找到数组中最小的那个元素,其次,把它和数组的第一个元素交换位置(如果第一个元素就是最小的元素那么它就和自己交换).再次,在剩下的元素中找到最小的元素,将它与数组的第二个 ...

  10. 网络损伤仪WANsim的带宽限制功能

    带宽限制功能 带宽限制功能是网络损伤仪WANsim的第一项损伤功能.进入WANsim的报文首先会经过报文过滤器的处理,随后,就会进入带宽限制. 点击虚拟链路,就可以进入网络损伤界面,对报文进行带宽限制 ...