了解了kafka原理之后,对kafka的的应用场景有了一些想法。在下面的一系列讨论中把最近一个项目中关于kafka的应用介绍一下。

先介绍一下使用kafka的起因:任何进销存系统,销售开单部分都应该算是主体部分了。简单的说,这是一个包括销售数据录入、库存数扣减两个动作的过程。销售项目录入与库存扣减之间的联系可以是单向的,如录入一个销售商品导致该商品库存扣减、也可以是双向的,即商品销售需要参考当前库存数量。依照具体的业务需求,销售开单过程对当前库存的依赖决定了更新库存的方式:当前库存数从作为参考值到必备条件分别代表事后批次更新或过程中实时更新。当然,从业务方面考虑,录入销售项目、立即扣减库存并作为下一笔录入的参考数是最理想的了。但在现实中,一个多用户的环境里,大量线程同时对一个商品的库存数进行更新,这时必须对数据库表进行锁定,那么由锁表造成的问题就无可避免了:轻者造成数据的遗失、重复、偏差,或者拖慢进程,重者锁死整个系统。这是经典进销存业务系统普遍面临的问题。也是促使许多商业软件开发人员纷纷转向新的分布式大数据模式去寻求解决方案的主要原因。

如果我们再实际点,能够容许些微的数据更新延迟,比如说:毫秒级的,那么就可以把销售项目录入和库存扣减两个动作拆分到两个相互独立的过程里。就像DDD模式里的两个聚合根(aggregate root), 分别在两个独立业务域中实现这两个动作。独立的域之间是松散耦合,互不影响的,所以,两个独立域的计算模式可以是不同的。例如:销售项目录入必须是多人操作,多线程高并发的,而库存扣减却可以设计成单线程或者限定线程数量的。这可以是一种典型的读写分离CQRS模式:扣减库存作为一项数据更新动作可以在另外一个模块,甚至另外一个软件里,在一个可控的、限定线程的环境里独立运算,和销售数据录入部分不发生任何关系。当然,数据录入完成到库存更新出结果之间一定会存在延迟。这种延迟不单只是与库存更新算法和运算效率有着直接关系,它也和两个独立域之间的数据交换速度有莫大关系。kafka,作为一个高效率、高吞吐量、高可用性的消息队列系统,足够担负起独立域与域之间的数据交换任务。而且kafka的消息是持久性的,有重复消费控制机制可以实现数据状态的重新计算,是事件源event-sourcing模式的一项理想工具选择。这就是我选择kafka的原因。

好了,说说这个案例的具体业务需求:这是一个零售业POS软件云租赁平台。初步规划上千独立门店及上万级的门店业务操作终端,包括收银终端、查询终端、业务管理终端。可想而知,系统应该容许上万用户同时进行信息录入操作。高并发、高频率的数据录入部分(特别是收银终端商品条码扫描销售)已经通过event-sourcing,CQRS等模式实现了。接着需要后端的数据处理部分,特别是当前库存状态更新。因为零售店其它业务,如:添订货、收发货、配退货等都需要及时、准确库存数据的支持。我们把这个库存更新功能的实现作为典型的kafka应用案例来介绍,然后再在过程中对akka系列alpakka-kafka的使用进行讲解和示范。

首先,后端业务功能与前端数据采集是松散耦合的。特别是后端数据处理应该是所有前端系统共享的业务功能。这个容易理解,我们不可能给每个门店运行一个后端,那样就需要几千个后端系统同时运行了。所以,可以把这个库存更新功能做成一个独立的库存管理平台,为所有业务模块,或者第三方业务软件提供库存状态支持。

现在我们可以把这个独立的库存管理平台作为一个典型的kafka应用示范。简单来讲,kafka就是一个消息队列MQ,从一端写入消息(produce)、另一端按写入顺序读出消息(consume),中间是一堆复杂的机制去保证集群节点协调、消息输出顺序、消息持久化及消息重复消费等等。在我们的案例里,以库存管理平台为核心,一端通过kafka连接所有的平台用户。这些分布在各处的应用通过kafka的集群功能同时向kafka的写入端写入消息。这些消息实际是序列化的库存更新指令。平台再通过kafka消费端读取这些指令,反序列化解析后按顺序执行这些更新库存命令。值得注意的是:平台此时可以在一个单线程里按发出的顺序,逐个执行指令,避免了多线程产生的不确定因素。

从kafka角度描述:库存管理平台用户即消息发布者producer,这种消息发布必须是高并发、高吞吐量的。简单讲就是同时集中大批量的向kafka写入数据。对平台各用户来讲,就是一种写完就了fire-and-go模式,实现起来比较简单。alpakka-kafka提供了很多类型的sink来实现写produce功能。下面是一个实际的例子:

  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)
}

SendProducer.send就是alpakka-kafka最简单的一个sink:

object SendProducer {
def apply[K, V](settings: ProducerSettings[K, V])(implicit system: ClassicActorSystemProvider): SendProducer[K, V] =
new SendProducer(settings, system.classicSystem)
} def send(record: ProducerRecord[K, V]): Future[RecordMetadata] = {
producerFuture.flatMap { producer =>
sendSingle(producer, record, identity)
}
}

send(ProducerRecord)把一条ProducerRecord[K,V]写入kafka。ProducerRecord类型如下:

    /**
* Creates a record with a specified timestamp to be sent to a specified topic and partition
*
* @param topic The topic the record will be appended to
* @param partition The partition to which the record should be sent
* @param timestamp The timestamp of the record, in milliseconds since epoch. If null, the producer will assign
* the timestamp using System.currentTimeMillis().
* @param key The key that will be included in the record
* @param value The record contents
* @param headers the headers that will be included in the record
*/
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers) {
if (topic == null)
throw new IllegalArgumentException("Topic cannot be null.");
if (timestamp != null && timestamp < 0)
throw new IllegalArgumentException(
String.format("Invalid timestamp: %d. Timestamp should always be non-negative or null.", timestamp));
if (partition != null && partition < 0)
throw new IllegalArgumentException(
String.format("Invalid partition: %d. Partition number should always be non-negative or null.", partition));
this.topic = topic;
this.partition = partition;
this.key = key;
this.value = value;
this.timestamp = timestamp;
this.headers = new RecordHeaders(headers);
}

ProducerRecord的几个重要属性:topic,partition,key,value。具体使用如下:

     SendProducer(producerKafka.producerSettings.get)
.send(new ProducerRecord[String, String](producerKafka.publisherSettings.topic, doc.shopId, toJson(doc)))

以上示范中:key=shopId, value=toJson(doc),partition由kafka自动指定,key以每个门店的店号表示,意思是使用kafka默认的算法按门店号来自动产生消息对应的partition。具体消息value是json格式序列化的一个类值,其实对应一条库存交易记录。

kafka的另外一端,消费端consumer就是我们这次示范案例的主要部分,库存管理平台了。这个平台是一个以alpakka-kafka-stream为主要运算框架的流计算软件。我们可以通过这次示范深入了解alpakka-kafka-stream的原理和应用。

库存管理平台是一个典型的kafka消费端应用。核心技术是对kafka消息的读取方式:既要实现高并发的高吞吐量,又要保证严格按照既定顺序读取的严密准确性。Kafka是通过对topic的分片partition来实现数据高吞吐量的,一个topic可以对应多个partition。每一个kafka消费端应用对于kafka来讲就是一个独立的consumer,或reader。一个reader可以对应多个partition。为了实现高流量的数据消费,在设计应用系统时可以考虑构建多个kafka消费端,也就是多个reader。这样,每个reader负责读取的对应partition数量就减少了,读取数据任务就可以由多个reader共同负担了。如此通过增加reader就可以有效提高数据消费的效率。在alpakka-kafka,reader可以用一个stream-source来表示,如下:

  val commitableSource = Consumer
.committableSource(consumerSettings, subscription)

run这个source就可以开始从kafka里读取数据了,如下:

    commitableSource
.to(Sink.ignore)
.run()

具体的数据处理(库存扣减)逻辑在每读出一条数据后立即执行。run()的主要作用只是推动这个stream的流动。具体例子如下:

      committablesource
.mapAsync(1) { msg =>
for {
curstks <- curStk.updateStk(msg.record.value())
} yield "Completed"
FastFuture.successful(msg.committableOffset)
}
.toMat(Committer.sink(committerSettings))(Keep.left)
.run()

这个source代表一个reader,负责读取这个kafka节点上所有partition的数据。如果同时运行多个source,也就是在一个kafka节点上同一topic有多个reader,即一个reader group,reader间可以分担流量。下面例子里同时启动一个包括10个reader的reader-group:

  def startReading = {
(1 to 10).toList.map { _ =>
commitableSource
.mapAsync(1) { msg =>
for {
curstks <- curStk.updateStk(msg.record.value())
} yield "Completed"
FastFuture.successful(msg.committableOffset)
}
.toMat(Committer.sink(committerSettings))(Keep.left)
.run()
}
}

如果把这个库存管理平台部署在一个多节点的集群里,那么,每一个节点都可以有一组10个库存更新过程同时运算以达到更高的运算效率。当然,实际情况并不像理论那么简单。首先,要考虑数据的安全性,也就是kafka消息消费模式,包括:至少一次at-least-once、至多一次at-most-once、保证一次exactly-once。然后,是否采用分布式运算模式,如何解决多线程竞争问题,这些问题都比较复杂,用一篇博客无法完全解释清楚,就留着在下面的博客中再详细描述吧。

alpakka-kafka(6)-kafka应用案例,用户接口的更多相关文章

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

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

  2. kafka channle的应用案例

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

  3. 生成kafka内部请求与响应的接口文档

    生成kafka内部请求与响应的接口文档 /** */ package com.code260.ss.kafka10demo; import java.io.File; import java.io.I ...

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

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

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

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

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

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

  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. 0507 构造代码块和static案例,接口interface

    0507构造代码块和static案例,接口interface [重点] 1.局部变量,成员变量,静态变量的特点 2.接口 接口语法:interface A {} 接口内的成员变量[缺省属性]publi ...

随机推荐

  1. iOS-block本质是什么?

    一: block的原理是怎样的?本质是什么? block本质上也是一个OC对象,因为它的内部也有个isa指针 block是封装了函数调用以及函数调用环境的OC对象 接下来我们将通过底层源码来论证上诉两 ...

  2. 20201123 实验二《Python程序设计》实验报告

    20201123 2020-2021-2 <Python程序设计>实验报告课程:<Python程序设计>班级:2011姓名:晏鹏捷学号:20201123实验教师:王志强实验日期 ...

  3. Dagger2入门,以初学者角度

    2016-12-21 更新:添加@Subcomponent注解以及Lazy与Provider的使用,本文基本完结!如果有好的建议请提出,感谢大家的支持,谢谢 依赖注入 Dagger2是Android中 ...

  4. 面试系列——Mysql索引

    1.索引分类 Hash索引Hash 索引查询效率很高,时间复杂度O(1).Mysql Innodb引擎不支持hash索引的.Hash索引适合精确查找,不适合范围查找. 平衡二叉树时间复杂度为 O(n) ...

  5. Linux:Linux更新yum方法

    [内容指引]进入目录:cd查看目录下的内容:ls重命名备份:mv从网络下载:wgetyum更新:yum update 第一次运行yum安装软件前,建议更新yum. 1.进入yum源目录 命令: cd ...

  6. MySQL 那些常见的错误设计规范

    依托于互联网的发达,我们可以随时随地利用一些等车或坐地铁的碎片时间学习以及了解资讯.同时发达的互联网也方便人们能够快速分享自己的知识,与相同爱好和需求的朋友们一起共同讨论. 但是过于方便的分享也让知识 ...

  7. asp.net c#整理所有本地的图片一次性保存到SQL表中

    string sql1 = "select distinct tx from tiku where tx is not null"; //检索tx表中所有的不重复的tx值 stri ...

  8. SLAM基础算法(1):卡尔曼滤波

    对于一个正在运动中的小车来说,如何准确的知道它所处的位置? 理论家说:我可以通过牛顿公式来计算! 实践家说:给它装个GPS不就得了! 好吧,你们说的听上去都很有道理,但我们到底该相信谁? 现实情况是: ...

  9. 学习总结 NCRE二级和三级

    NCRE二级C语言 证书 考试感想 2016年考的认证,5年过去了,"光阴荏苒真容易".趁着心有余力有余的时候,把一些个人的体会分享给大家,希望后来人能平稳前行. Windows ...

  10. JavaScript学习笔记:你必须要懂的原生JS(二)

    11.如何正确地判断this?箭头函数的this是什么? this是 JavaScript 语言的一个关键字.它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用. this的绑定规则 ...