了解了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. 最多能创建多少个 TCP 连接?

    我是一个 Linux 服务器上的进程,名叫小进. 老是有人说我最多只能创建 65535 个 TCP 连接. 我不信这个邪,今天我要亲自去实践一下. 我走到操作系统老大的跟前,说: "老操,我 ...

  2. 富文本编辑器之游戏角色升级ing

    一.前言 想必大家看到这个标题,心中不禁会浮现几个问题: 什么是富文本编辑器? 富文本编辑器和游戏角色有什么关系? 为什么是升级ing? 什么是富文本编辑器--富文本编辑器集成了格式设置.媒体嵌入.社 ...

  3. Vue3全家桶升级指南二ref、toRef、toRefs的区别

    ref是对原始数据的拷贝,当修改ref数据时,模板中的视图会发生改变,但是原始数据并不会改变. toRef是对原始数据的引用,修改toRef数据时,原始数据也会发生改变,但是视图并不会更新. 在vue ...

  4. 自定义组件开发:使用v-model封装el-pagination组件

    1.前言 通过封装el-pagination组件开发自定义分页组件的类似文章网上已经有很多了,但看了一圈,总是不如意,于是决定还是自己动手搞一个. 2.背景 2.1.常规分页处理方法 利用el-pag ...

  5. 关于使用Flex中图片处理

    <?xml version="1.0" encoding="utf-8"?> <s:Application xmlns:fx="ht ...

  6. shiro框架整合ssm框架

    下面我通过一个web的maven项目来讲解如何将shiro整合ssm框架,具体结构如下图 一.引入依赖的jar包 <?xml version="1.0" encoding=& ...

  7. mysql binlog恢复数据实战

    在前面,我们了解了mysql binlog日志的作用以及使用方法:  http://www.php20.cn/article/237 在后面讲到了,可以通过binlog进行恢复数据,那么,具体步骤是怎 ...

  8. jenkins报错: error: insufficient permission for adding an object to repository database .git/objects

    前言:这是在用jenkins去gitlab上面去拉下代码来编译,就报了这个错,在这里记录下,避免下次 报错:   17:08:17 error: insufficient permission for ...

  9. java基础---数组的查找算法(2)

    一.查找的基本概念 查找分为有序查找和无序查找,这里均以数组为对象,有序查找指的是数组元素有序排列,无序查找指的是数组元素有序或无序排列 平均查找长度(Average Search Length,AS ...

  10. 论文阅读:Robust Visual SLAM with Point and Line Features

    本文提出了使用异构点线特征的slam系统,继承了ORB-SLAM,包括双目匹配.帧追踪.局部地图.回环检测以及基于点线的BA.使用最少的参数对线特征采用标准正交表示,推导了线特征重投影误差的雅克比矩阵 ...