流式处理
1.流式处理基本概念
1.1数据流
- 数据流是无边界数据集的抽象表示。无边界意味着无限和持续增长。无边界数据集之所以是无限的,是因为随着时间的推移,新的记录会不断加入进来。
- 这个简单的模型(事件流)可以表示很多业务活动,比如信用卡交易、股票交易、包裹递送、流经交换机的网络事件、制造商设备传感器发出的事件、发送出去的邮件、游戏里物体的移动,等等。这个清单是无穷无尽的,因为几乎每一件事情都可以被看成事件的序列。
1.1.1事件流是有序的
- 事件的发生总是有个先后顺序。以金融活动事件为例,先将钱存进账户后再花钱,这与先花钱再还钱的次序是完全不一样的。后者会出现透支,而前者不会。
1.1.2不可变的数据记录
- 事件一旦发生,就不能被改变。一个金融交易被取消,并不是说它就消失了,相反,这需要往事件流里添加一个额外的事件,表示前一个交易的取 消操作。顾客的一次退货并不意味着之前的销售记录被删除,相反,退货行为被当成一个额外的事件记录下来。这是数据流与数据表之间的另一个不同 点一一可以删除和修改数据表里的记录,但这些操作只不过是发生在数据库里的事务,这些事务可以被看成事件流。假设你对数据库的二进制日志( bin log )、预写式日志( WAL )和重做日志( redo log )的概念都很熟悉,那么就会知道,如果往数据库表插入一条记录,然后将其删除,表里就不会再 有这条记录。但重做日志里包含了两个事务:插入事务和删除事务。
1.1.3事件流是可重播的
- 这是事件流非常有价值的一个属性。但对于大多数业务来说,重播发生在几个月前(甚至几年前)的原始事件流是一个很重要的需求。可能是为了 尝试使用新的分析方法纠正过去的错误,或是为了进行审计。
1.2什么是流式处理
- 流式处理是指实时地处理一个或多个事件流。流式处理是一种编程范式,就像请求与响应范式和批处理范式那样。
1.2.1请求与响应
- 这是延迟最小的一种范式,响应时间处于亚毫秒到毫秒之间,而且响应时间一般非常稳定。这种处理模式一般是阻塞的,应用程序向处理系统发出 请求,然后等待响应。在数据库领域。
1.2.2批处理
- 这种范式具有高延迟和高吞吐量的特点。处理系统按照设定的时间启动处理进程,比如每天的下午两点开始启动,每小时启动一次等。它读取所有的输入数据(从上一次执行之后的所有可用数据,或者从月初开始的所有数据等)输出结果,然后等待下一次启动。处理时间从几分钟到几小时不等, 并且用户从结果里读到的都是旧数据。它们每天加载巨大批次的数据,并生成报表,用户在下一次加载数据之前看到的都是相同的报表。从规模上来说, 这种范式既高效又经挤。但在近几年,为了能够更及时、高效地作出决策,业务要求在更短的时间内能提供可用的数据,这就给那些为探索规模经济而 开发却无法提供低延迟报表的系统带来了巨大的压力。
1.2.3流式处理
- 这种范式介于上述两者之间。大部分的业务不要求亚毫秒级的响应,不过也接受不了要等到第二天才知道结果。大部分业务流程都是持续进行的, 只要业务报告保持更新,业务产品线能够持续响应,那么业务流程就可以进行下去,而无需等待特定的响应,也不要求在几毫秒内得到响应。一些业务流程具有持续性和非阻塞的特点,比如针对可疑信用卡交易的警告、网络警告、根据供应关系实时调整价格、跟踪包衷。
- 流的定义不依赖任何一个特定的框架、API 或特性。只要持续地从一个无边界的数据集读取数据,然后对它们进行处理并生成结果,那就是在进行流 式处理。重点是,整个处理过程必须是持续的。一个在每天凌晨两点启动的流程,从流里读取 500 条记录,生成结果,然后结束,这样的流程不是流式处理。
1.3流式处理中的基本概念
- 流式处理的很多方面与普通的数据处理是很相似的:写一些代码来接收数据,对数据进行处理,可能做一些转换、聚合和增强的操作,然后把生成 的结果输出到某个地方。不过流式处理有一些特有的概念。
1.3.1时间
- 在流式处理里,时间是一个非常重要的概念,因为大部分流式应用的操作都是基于时间窗口的。
- 例如,流式应用可能会计算股价的 5 分钟移动平均数。如果生产者因为网络问题离线了 2 小时,然后带着 2 小时的数据重新连线,我们需要知道该 如何处理这些数据。这些数据大部分都已经超过了 5 分钟,而且没有参与之前的计算。
- 流式处理系统一般包含如下几个时间概念。
事件时间:
- 事件时间是指所追踪事件的发生时间和记录的创建时间。例如,度量的获取时间、商店里商品的出售时间、网站用户访问网页的时间,等等。在处 理数据流肘,事件时间是很重要的。
日志追加时间:
- 日志追加时间是指事件保存到 broker 的时间。这个时间戳一般与流式处理没有太大关系,因为用户一般只对事件的发生时间感兴趣。例如,如果要计算每天生产了多少台设备,就需要计算在那一天实际生产的设备数量,尽管这些事件有可能因为网络问题到了第二天才进入 Kafka 。不过,如果真实的事件时间没有被记录下来,那么就可以使用日志追加时间,在记录创建之后,这个时间就不会发生改变。
处理时间:
- 处理时间是指应用程序在收到事件之后要对其进行处理的时间。这个时间可以是在事件发生之后的几毫秒、几小时或几天。同一个事件可能会被分配不同的时间戳,这取决于应用程序何时读取这个事件。如果应用程序使用了两个线程来读取同一个事件,这个时间戳也会不一样。所以这个时间戳非常不可靠,应该避免使用它。
1.3.2状态
- 如果只是单独处理每一个事件,那么流式处理就很简单。例如,如果想从 Kafka 读取电商购物交易事件流,找出金额超过 10000 元的交易,并将结果通过邮件发送给销售人员,那么可以使用 Kafka 消费者客户端,几行代码就可以搞定。
- 如果操作里包含了多个事件,流式处理就会变得很有意思,比如根据类型计算事件的数量、移动平均数、合并两个流以便生成更丰富的信息流。在这些情况下,光处理单个事件是不够的,需要跟踪更多的信息,比如这个小时内看到的每种类型事件的个数、需要合并的事件、将每种类型的事件值相加等等。事件与事件之间的信息被称为状态。
- 这些状态一般被保存在应用程序的本地变量里。流式处理包含以下几种类型的状态。
本地状态或内部状态:
- 这种状态只能被单个应用程序实例访问,它们一般使用内嵌在应用程序里的数据库进行维护和管理。本地状态的优势在于它的速度,不足之处在于它受到内存大小的限制。所以,流式处理的很多设计模式都将数据拆分到多个子流,这样就可以使用有限的本地状态来处理它们。
外部状态:
- 这种状态使用外部的数据存储来维护, 一般使用 NoSQL 系统,比如 HDFS 。使用外部存储的优势在于,它没有大小的限制,而且可以被应用程序的 多个实例访问,甚至被不同的应用程序访问。不足之处在于,引人额外的系统会造成更大的延迟和复杂性。大部分流式处理应用尽量避免使用外部存储, 或者将信息缓存在本地,减少与外部存储发生交互,以此来降低延迟。
1.3.3流和表区别
- 表是记录的集合,每个表都有一个主键,并包含了一系列由 schema 定义的属性。表的记录是可变的(可以在表上面执行更 新和删除操作)。我们可以通过查询表数据获知某一时刻的数据状态。例如,通过查询客户信息这个表,就可以获取所有客户的联系信息。如果表被设计成不包含历史信息,那么就找不到客户过去的联系信息了。
- 在将表与流进行对比时,可以这么想:流包含了变更一一流是一系列事件,每个事件就是一个变更。表包含了当前的状态,是多个变更所产生的结果。
- 为了将表转化成流,需要捕捉到在表上所发生的变更,将“ insert ”、“ update ”和“ delete ”事件保存到流里。大部分数据库提供了用于捕捉 变更的“ Change Data Capture" (CDC )解决方案, Kafka 连接器将这些变更发送到 Kafka ,用于后续的流式处理。
2.先创建主题phone和phone_count
3.创建生产者StreamProducer
package org.example.stream;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
import java.util.Random;
public class StreamProducer {
public static void main(String[] args) {
//生产者必须指定3个属性(broker地址清单,key和value的序列化器)
Properties properties = new Properties();
properties.put("bootstrap.servers", "192.168.42.111:9092");
properties.put("key.serializer", StringSerializer.class);
properties.put("value.serializer", StringSerializer.class);
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
//商城
String[] goods = {"iphone", "huawei", "oppo", "vivo", "xiaomi"};
Random r1 = new Random();
Random r2 = new Random();
try {
ProducerRecord<String, String> record;
for (int i = 0; i < 1000; i++) {
//随机生成购买的手机数量
int goodcount=r1.nextInt(10);
if (goodcount == 0)
goodcount=1;
StringBuilder sb = new StringBuilder();
for (int j = 0; j < goodcount; j++) {
//根据商品数量,随机生成商品信息
sb.append(goods[r2.nextInt(goods.length)]).append(",");
}
try {
record = new ProducerRecord<String, String>("phone","goods", sb.toString());
//发送并忘记
producer.send(record);
System.out.println("用户购买的商品:"+sb.toString());
Thread.sleep(2);
} catch (Exception e) {
e.printStackTrace();
}
}
} finally {
producer.close();
}
}
}
4.创建流处理类StreamDemo
package org.example.stream;
import org.apache.kafka.common.serialization.Serdes;
import org.apache.kafka.streams.KafkaStreams;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.StreamsConfig;
import org.apache.kafka.streams.kstream.KStream;
import org.apache.kafka.streams.kstream.KTable;
import org.apache.kafka.streams.kstream.Materialized;
import org.apache.kafka.streams.kstream.Produced;
import java.util.Arrays;
import java.util.Properties;
public class StreamDemo {
public static void main(String[] args) throws InterruptedException {
Properties properties = new Properties();
properties.put("bootstrap.servers", "192.168.42.111:9092");
properties.put(StreamsConfig.APPLICATION_ID_CONFIG, "phone_count");
properties.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
properties.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
StreamsBuilder builder = new StreamsBuilder();
//使用流进行统计,(huawei 80,oppo 20)类似于数据表
KStream<String, String> countStream = builder.stream("phone");
KTable<String,Long> wordscount = countStream
//将Topic中的流中的数据小写,同时,转换成数组,最后变成List
.flatMapValues(textLine -> Arrays.asList(textLine.toLowerCase().split(",")))
.groupBy((key,word) ->word) //分组
.count(Materialized.as("counts"));//计数
//相当于将流处理的结果写入到另外一个主题
wordscount.toStream().to("phone_count", Produced.with(Serdes.String(),Serdes.Long()));
//定义一个KafkaStreams的对象
KafkaStreams streams = new KafkaStreams(builder.build(), properties);
//启动stream
streams.start();
Thread.sleep(5000L);
builder.build();
}
}
5.创建消费者StreamConsumer
package org.example.stream;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.serialization.LongDeserializer;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.example.config.BusiConst;
import java.time.Duration;
import java.util.Collections;
import java.util.Properties;
public class StreamConsumer {
public static void main(String[] args) {
//消费者必须指定3个属性(broker地址清单,key和value的反序列化器)
Properties properties = new Properties();
properties.put("bootstrap.servers", "192.168.42.111:9092");
properties.put("key.deserializer", StringDeserializer.class);
properties.put("value.deserializer", LongDeserializer.class);
//群组并非必须
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "test1");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(properties);
//订阅主题(可以多个)
consumer.subscribe(Collections.singletonList("phone_count"));
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, String> record : records) {
//使用String.valueOf将Long类型的value值转换成String
System.out.println("主题:"+record.topic()+",分区:"+record.partition()+",偏移量:"+record.offset()+
",key:"+record.key()+",value:"+String.valueOf(record.value()));
}
}
} finally {
consumer.close();
}
}
}