了解了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. Opencv 播放mp4文件和读取摄像头图以及可能会发生的一些异常问题解决方法

    学习内容 学习Opencv 读取并播放本地视频和打开摄像头图像以及可能会发生的一些异常问题解决方法 代码演示 电脑环境信息: OpenCV版本:4.5.2 ,vs2017 1.视频文件读取与播放 加载 ...

  2. 无向图(邻接表实现)DFS_AND_BFS

    数据结构选择TreeSet的原因:通过自定义的Compare方法,保证了点元素的唯一性,有序性(方便检验): 传入Set和Map中的元素类似于C中的指针操作,即共享地址,改变其中一个中的元素,与之相关 ...

  3. JAVA 中的权限访问修饰符(public,protected,default,private )

    JAVA中有四个权限访问修饰符:public,protected,default,private 注意:这里讲的是对类中属性和方法的访问权限,并不是类的访问权限 1.default:包访问权限 如果什 ...

  4. Hadoop0.20.2中MapReduce读取gb2312文件出现乱码问题

    单位用的是Linux系统的字符编码是gb2312,所以生成的文件都是按照默认编码生成的.给我的文件也都是gb2312的,在hadoop中运行mapreduce出现乱码,在网上查资料说是因为hadoop ...

  5. 1.3.9、通过权重 Weight匹配

    server: port: 8080 spring: application: name: gateway cloud: gateway: routes: - id: guo-system1 uri: ...

  6. 转:nginx服务器配置

    1. user www-data说明的是使用的用户,至于www-data这个用户是系统自带的,我们不用说系统里没有这个账户的,虽然这个账户具体是做什么的,我也不太清楚2.worker_processe ...

  7. asp.net core 视图组件(转)

    介绍视图组件 视图组件是 ASP.NET Core MVC 中的新特性,与局部视图相似,但是它们更加的强大.视图组件不使用模型绑定,只取决于调用它时所提供的数据.视图组件有以下特点: 渲染一个块,而不 ...

  8. 【BZOJ 4771】七彩树

    一直TLE的原因竟然是数组开太大了导致\(memset\)清空耗时超限,亏我还调了1天啊(T^T) 题目大意 给定一颗树,每个节点都有一个颜色,要求多次询问某个节点\(x\)的子树中深度不超过\(d\ ...

  9. python根据正则表达式生成指定规律的网址

    import os def file_name(file_dir): for root, dirs, files in os.walk(file_dir): print(root) #当前目录路径 p ...

  10. SpringBoot自动装配-自定义Start

    SpringBoot自动装配 在没有使用SpringBoot之前,使用ssm时配置redis需要在XML中配置端口号,地址,账号密码,连接池等等,而使用了SpringBoot后只需要在applicat ...