上传文件

master
gaosiao 1 year ago
parent 96cf27a82a
commit edba0b7fa8

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4" />

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="batch-process" />
<module name="canal-kafka" />
<module name="real-process" />
<module name="report" />
<module name="sync-db" />
</profile>
</annotationProcessing>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/report" charset="UTF-8" />
</component>
</project>

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="HydraSettings">
<option name="hydraStorePath" value="E:\IdeaProject\Flink-pyg\.hydra\idea" />
<option name="noOfCores" value="4" />
<option name="projectRoot" value="E:\IdeaProject\Flink-pyg" />
<option name="sourcePartitioner" value="auto" />
</component>
</project>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Palette2">
<group name="Swing">
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
</item>
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
</item>
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
</item>
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
<initial-values>
<property name="text" value="Button" />
</initial-values>
</item>
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="RadioButton" />
</initial-values>
</item>
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
<initial-values>
<property name="text" value="CheckBox" />
</initial-values>
</item>
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
<initial-values>
<property name="text" value="Label" />
</initial-values>
</item>
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
<preferred-size width="150" height="-1" />
</default-constraints>
</item>
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
</item>
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
<preferred-size width="150" height="50" />
</default-constraints>
</item>
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
<preferred-size width="200" height="200" />
</default-constraints>
</item>
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
</item>
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
</item>
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
</item>
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
<preferred-size width="-1" height="20" />
</default-constraints>
</item>
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
</item>
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
</item>
</group>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>pyg</artifactId>
<groupId>com.henry</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>batch-process</artifactId>
</project>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>pyg</artifactId>
<groupId>com.henry</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>canal-kafka</artifactId>
<dependencies>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.0.24</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.11</artifactId>
<version>0.10.1.0</version>
</dependency>
<!--对象和json 互相转换的-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
</project>

@ -0,0 +1,212 @@
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.alibaba.otter.canal.protocol.Message;
import com.henry.canal_kafka.util.GlobalConfigUtil;
import com.henry.canal_kafka.util.KafkaSender;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Canalbinlog
*/
public class CanalClient {
static class ColumnValuePair {
private String columnName;
private String columnValue;
private Boolean isValid;
public ColumnValuePair(String columnName, String columnValue, Boolean isValid) {
this.columnName = columnName;
this.columnValue = columnValue;
this.isValid = isValid;
}
public String getColumnName() { return columnName; }
public void setColumnName(String columnName) { this.columnName = columnName; }
public String getColumnValue() { return columnValue; }
public void setColumnValue(String columnValue) { this.columnValue = columnValue; }
public Boolean getIsValid() { return isValid; }
public void setIsValid(Boolean isValid) { this.isValid = isValid; }
}
/**
* Canal
*
* @param host
* @param port
* @param instance Canal
* @param username
* @param password
* @return Canal
*/
public static CanalConnector getConn(String host, int port, String instance, String username, String password) {
CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress(host, port), instance, username, password);
return canalConnector;
}
/**
* Binlog
*
* @param entries Binlog
* @param emptyCount
*/
public static void analysis(List<CanalEntry.Entry> entries, int emptyCount) {
for (CanalEntry.Entry entry : entries) {
// 只解析mysql事务的操作其他的不解析
if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN ||
entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
continue;
}
// 那么解析binlog
CanalEntry.RowChange rowChange = null;
try {
rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
} catch (Exception e) {
e.printStackTrace();
}
// 获取操作类型字段(增加 删除 修改)
CanalEntry.EventType eventType = rowChange.getEventType();
// 获取binlog文件名称
String logfileName = entry.getHeader().getLogfileName();
// 读取当前操作在binlog文件的位置
long logfileOffset = entry.getHeader().getLogfileOffset();
// 获取当前操作所属的数据库
String dbName = entry.getHeader().getSchemaName();
// 获取当前操作所属的表
String tableName = entry.getHeader().getTableName();//当前操作的是哪一张表
long timestamp = entry.getHeader().getExecuteTime();//执行时间
// 解析操作的行数据
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
// 删除操作
if (eventType == CanalEntry.EventType.DELETE) {
// 获取删除之前的所有列数据
dataDetails(rowData.getBeforeColumnsList(), logfileName, logfileOffset, dbName, tableName, eventType, emptyCount,timestamp);
}
// 新增操作
else if (eventType == CanalEntry.EventType.INSERT) {
// 获取新增之后的所有列数据
dataDetails(rowData.getAfterColumnsList(), logfileName, logfileOffset, dbName, tableName, eventType, emptyCount,timestamp);
}
// 更新操作
else {
// 获取更新之后的所有列数据
dataDetails(rowData.getAfterColumnsList(), logfileName, logfileOffset, dbName, tableName, eventType, emptyCount,timestamp);
}
}
}
}
/**
* Binlog
*
* @param columns
* @param logFileName binlog
* @param logFileOffset binlog
* @param dbName
* @param tableName
* @param eventType
* @param emptyCount
*/
private static void dataDetails(List<CanalEntry.Column> columns,
String logFileName,
Long logFileOffset,
String dbName,
String tableName,
CanalEntry.EventType eventType,
int emptyCount,
long timestamp) {
// 找到当前那些列发生了改变 以及改变的值
List<ColumnValuePair> columnValueList = new ArrayList<ColumnValuePair>();
for (CanalEntry.Column column : columns) {
ColumnValuePair columnValuePair = new ColumnValuePair(column.getName(), column.getValue(), column.getUpdated());
columnValueList.add(columnValuePair);
}
String key = UUID.randomUUID().toString();
JSONObject jsonObject = new JSONObject();
jsonObject.put("logFileName", logFileName);
jsonObject.put("logFileOffset", logFileOffset);
jsonObject.put("dbName", dbName);
jsonObject.put("tableName", tableName);
jsonObject.put("eventType", eventType);
jsonObject.put("columnValueList", columnValueList);
jsonObject.put("emptyCount", emptyCount);
jsonObject.put("timestamp", timestamp);
// 拼接所有binlog解析的字段
String data = JSON.toJSONString(jsonObject);
System.out.println(data);
// 解析后的数据发送到kafka
KafkaSender.sendMessage(GlobalConfigUtil.kafkaInputTopic, key, data);
}
public static void main(String[] args) {
// 加载配置文件
String host = GlobalConfigUtil.canalHost;
int port = Integer.parseInt(GlobalConfigUtil.canalPort);
String instance = GlobalConfigUtil.canalInstance;
String username = GlobalConfigUtil.mysqlUsername;
String password = GlobalConfigUtil.mysqlPassword;
// 获取Canal连接
CanalConnector conn = getConn(host, port, instance, username, password);
// 从binlog中读取数据
int batchSize = 100;
int emptyCount = 1;
try {
// 连接cannal
conn.connect();
//订阅实例中所有的数据库和表
conn.subscribe(".*\\..*");
// 回滚到未进行ack的地方
conn.rollback();
int totalCount = 120; //循环次数
while (totalCount > emptyCount) {
// 获取数据
Message message = conn.getWithoutAck(batchSize);
long id = message.getId();
int size = message.getEntries().size();
if (id == -1 || size == 0) {
//没有读取到任何数据
} else {
//有数据那么解析binlog日志
analysis(message.getEntries(), emptyCount);
emptyCount++;
}
// 确认消息
conn.ack(message.getId());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
conn.disconnect();
}
}
}

@ -0,0 +1,30 @@
package com.henry.canal_kafka.util;
import java.util.ResourceBundle;
public class GlobalConfigUtil {
// 获取一个资源加载器
// 资源加载器会自动去加载CLASSPATH中的application.properties配置文件
private static ResourceBundle resourceBundle = ResourceBundle.getBundle("application");
// 使用ResourceBundle.getString方法来读取配置
public static String canalHost = resourceBundle.getString("canal.host");
public static String canalPort = resourceBundle.getString("canal.port");
public static String canalInstance = resourceBundle.getString("canal.instance");
public static String mysqlUsername = resourceBundle.getString("mysql.username");
public static String mysqlPassword = resourceBundle.getString("mysql.password");
public static String kafkaBootstrapServers = resourceBundle.getString("kafka.bootstrap.servers");
public static String kafkaZookeeperConnect = resourceBundle.getString("kafka.zookeeper.connect");
public static String kafkaInputTopic = resourceBundle.getString("kafka.input.topic");
public static void main(String[] args) {
System.out.println(canalHost);
System.out.println(canalPort);
System.out.println(canalInstance);
System.out.println(mysqlUsername);
System.out.println(mysqlPassword);
System.out.println(kafkaBootstrapServers);
System.out.println(kafkaZookeeperConnect);
System.out.println(kafkaInputTopic);
}
}

@ -0,0 +1,42 @@
package com.henry.canal_kafka.util;
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;
import kafka.serializer.StringEncoder;
import java.util.Properties;
/**
* Kafka
*/
public class KafkaSender {
private String topic;
public KafkaSender(String topic){
super();
this.topic = topic;
}
/**
* Kafkatopic
*
* @param topic topic
* @param key
* @param data
*/
public static void sendMessage(String topic , String key , String data){
Producer<String, String> producer = createProducer();
producer.send(new KeyedMessage<String , String>(topic , key , data));
}
private static Producer<String , String> createProducer(){
Properties properties = new Properties();
properties.put("metadata.broker.list" , GlobalConfigUtil.kafkaBootstrapServers);
properties.put("zookeeper.connect" , GlobalConfigUtil.kafkaZookeeperConnect);
properties.put("serializer.class" , StringEncoder.class.getName());
return new Producer<String, String>(new ProducerConfig(properties));
}
}

@ -0,0 +1,14 @@
#
# canal\u914D\u7F6E
#
canal.host=master
canal.port=11111
canal.instance=example
mysql.username=root
mysql.password=123456
#
#kafka\u7684\u914D\u7F6E
#
kafka.bootstrap.servers=master:9092,slave1:9092,slave2:9092
kafka.zookeeper.connect=master:2181,slave1:2181,slave2:2181
kafka.input.topic=canal

@ -0,0 +1,4 @@
log4j.rootLogger=error,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p - %m%n

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.henry</groupId>
<artifactId>pyg</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>report</module> <!--上报服务-->
<module>real-process</module> <!--实时处理-->
<module>canal-kafka</module> <!--数据采集-->
<module>sync-db</module> <!--数据库同步处理-->
<module>batch-process</module> <!--批处理-->
</modules>
</project>

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4" />

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>pyg</artifactId>
<groupId>com.henry</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>real-process</artifactId>
<properties>
<scala.version>2.11</scala.version>
<flink.version>1.6.0</flink.version>
<hadoop.version>3.2.4</hadoop.version>
<hbase.version>2.0.0</hbase.version>
</properties>
<dependencies>
<!-- kafka 客户端-->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_${scala.version}</artifactId>
<version>0.10.1.0</version>
</dependency>
<!--flink对接kafka导入flink使用kafka的依赖-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-connector-kafka-0.10_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--批处理-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-table_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--导入scala的依赖-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-scala_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--模块二 流处理-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-scala_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-streaming-java_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!--数据落地flink和hbase的集成依赖-->
<dependency>
<groupId>org.apache.flink</groupId>
<artifactId>flink-hbase_${scala.version}</artifactId>
<version>${flink.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.hbase/hbase-client -->
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>${hbase.version}</version>
</dependency>
<!--hbase依赖于hadoop-->
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>${hadoop.version}</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-hdfs</artifactId>
<version>${hadoop.version}</version>
<!--xml.parser冲突 flink hdfs-->
<exclusions>
<exclusion>
<groupId>xml-apis</groupId>
<artifactId>xml-apis</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>${hadoop.version}</version>
<!--数据同步canal 和 hadoop protobuf-->
<exclusions>
<exclusion>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--对象和json 互相转换的-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
<!--打包-->
<build>
<sourceDirectory>src/main/scala</sourceDirectory>
<testSourceDirectory>src/test/scala</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<excludes>
<exclude>com.google.code.findbugs:jsr305</exclude>
<exclude>org.slf4j:*</exclude>
<exclude>log4j:*</exclude>
</excludes>
</artifactSet>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.henry.pyg.App</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,26 @@
#
#
# kafka的配置
#
# kafka 集群地址
bootstrap.servers="master:9092,slave1:9092,slave2:9092"
# zookeeper 集群地址
zookeeper.connect="master:2181,slave1:2181,slave2:2181"
# kafka topic
input.topic="pyg"
# 消费者组 ID
gruop.id="pyg"
# 自动提交拉取到的消费端的消息offset到kafka
enable.auto.commit="true"
# 自动提交offset到zookeeper的时间间隔单位毫秒
auto.commit.interval.ms="5000"
# 每次消费最新的数据
auto.offset.reset="latest"
#Hbase的配置
//hbase.zookeeper.quorum="master:2181,slave1:2181,slave2:2181"
//hbase.master="master:60000"
//hbase.zookeeper.property.clientPort="2181"
//hbase.rpc.timeout="600000"
//hbase.client.operator.timeout="600000"
//hbase.client.scanner.timeout.period="600000"

@ -0,0 +1,60 @@
<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<!--
/**
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-->
<configuration>
<!-- 指定hbase在HDFS上存储的路径 -->
<property>
<name>hbase.rootdir</name>
<value>hdfs://master:9000/hbase2</value>
</property>
<!-- 指定hbase是分布式的 -->
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<!-- hbase 0.98后的新变动之前版本没有默认端口为60000 -->
<property>
<name>hbase.master.info.port</name>
<value>16000</value>
</property>
<!-- 指定zk的地址多个用“,”分割 -->
<property>
<name>hbase.zookeeper.quorum</name>
<value>master:2181,slave1:2181,slave2:2181</value>
</property>
<property>
<name>hbase.zookeeper.property.clientPort</name>
<value>2181</value>
</property>
<property>
<name>hbase.zookeeper.property.dataDir</name>
<value>/usr/local/src/zookeeper-3.4.5/hbasedata</value>
</property>
</configuration>

@ -0,0 +1,296 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Define some default values that can be overridden by system properties
hadoop.root.logger=ERROR,console
hadoop.log.dir=.
hadoop.log.file=hadoop.log
# Define the root logger to the system property "hadoop.root.logger".
log4j.rootLogger=${hadoop.root.logger}, EventCounter
# Logging Threshold
log4j.threshold=ALL
# Null Appender
log4j.appender.NullAppender=org.apache.log4j.varia.NullAppender
#
# Rolling File Appender - cap space usage at 5gb.
#
hadoop.log.maxfilesize=256MB
hadoop.log.maxbackupindex=20
log4j.appender.RFA=org.apache.log4j.RollingFileAppender
log4j.appender.RFA.File=${hadoop.log.dir}/${hadoop.log.file}
log4j.appender.RFA.MaxFileSize=${hadoop.log.maxfilesize}
log4j.appender.RFA.MaxBackupIndex=${hadoop.log.maxbackupindex}
log4j.appender.RFA.layout=org.apache.log4j.PatternLayout
# Pattern format: Date LogLevel LoggerName LogMessage
log4j.appender.RFA.layout.ConversionPattern=%d{ISO8601} %p %c: %m%n
# Debugging Pattern format
#log4j.appender.RFA.layout.ConversionPattern=%d{ISO8601} %-5p %c{2} (%F:%M(%L)) - %m%n
#
# Daily Rolling File Appender
#
log4j.appender.DRFA=org.apache.log4j.DailyRollingFileAppender
log4j.appender.DRFA.File=${hadoop.log.dir}/${hadoop.log.file}
# Rollver at midnight
log4j.appender.DRFA.DatePattern=.yyyy-MM-dd
# 30-day backup
#log4j.appender.DRFA.MaxBackupIndex=30
log4j.appender.DRFA.layout=org.apache.log4j.PatternLayout
# Pattern format: Date LogLevel LoggerName LogMessage
log4j.appender.DRFA.layout.ConversionPattern=%d{ISO8601} %p %c: %m%n
# Debugging Pattern format
#log4j.appender.DRFA.layout.ConversionPattern=%d{ISO8601} %-5p %c{2} (%F:%M(%L)) - %m%n
#
# console
# Add "console" to rootlogger above if you want to use this
#
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy HH:mm:ss} %p %c{2}: %m%n
#
# TaskLog Appender
#
#Default values
hadoop.tasklog.taskid=null
hadoop.tasklog.iscleanup=false
hadoop.tasklog.noKeepSplits=4
hadoop.tasklog.totalLogFileSize=100
hadoop.tasklog.purgeLogSplits=true
hadoop.tasklog.logsRetainHours=12
log4j.appender.TLA=org.apache.hadoop.mapred.TaskLogAppender
log4j.appender.TLA.taskId=${hadoop.tasklog.taskid}
log4j.appender.TLA.isCleanup=${hadoop.tasklog.iscleanup}
log4j.appender.TLA.totalLogFileSize=${hadoop.tasklog.totalLogFileSize}
log4j.appender.TLA.layout=org.apache.log4j.PatternLayout
log4j.appender.TLA.layout.ConversionPattern=%d{ISO8601} %p %c: %m%n
#
# HDFS block state change log from block manager
#
# Uncomment the following to suppress normal block state change
# messages from BlockManager in NameNode.
#log4j.logger.BlockStateChange=WARN
#
#Security appender
#
hadoop.security.logger=INFO,NullAppender
hadoop.security.log.maxfilesize=256MB
hadoop.security.log.maxbackupindex=20
log4j.category.SecurityLogger=${hadoop.security.logger}
hadoop.security.log.file=SecurityAuth-${user.name}.audit
log4j.appender.RFAS=org.apache.log4j.RollingFileAppender
log4j.appender.RFAS.File=${hadoop.log.dir}/${hadoop.security.log.file}
log4j.appender.RFAS.layout=org.apache.log4j.PatternLayout
log4j.appender.RFAS.layout.ConversionPattern=%d{ISO8601} %p %c: %m%n
log4j.appender.RFAS.MaxFileSize=${hadoop.security.log.maxfilesize}
log4j.appender.RFAS.MaxBackupIndex=${hadoop.security.log.maxbackupindex}
#
# Daily Rolling Security appender
#
log4j.appender.DRFAS=org.apache.log4j.DailyRollingFileAppender
log4j.appender.DRFAS.File=${hadoop.log.dir}/${hadoop.security.log.file}
log4j.appender.DRFAS.layout=org.apache.log4j.PatternLayout
log4j.appender.DRFAS.layout.ConversionPattern=%d{ISO8601} %p %c: %m%n
log4j.appender.DRFAS.DatePattern=.yyyy-MM-dd
#
# hadoop configuration logging
#
# Uncomment the following line to turn off configuration deprecation warnings.
# log4j.logger.org.apache.hadoop.conf.Configuration.deprecation=WARN
#
# hdfs audit logging
#
hdfs.audit.logger=INFO,NullAppender
hdfs.audit.log.maxfilesize=256MB
hdfs.audit.log.maxbackupindex=20
log4j.logger.org.apache.hadoop.hdfs.server.namenode.FSNamesystem.audit=${hdfs.audit.logger}
log4j.additivity.org.apache.hadoop.hdfs.server.namenode.FSNamesystem.audit=false
log4j.appender.RFAAUDIT=org.apache.log4j.RollingFileAppender
log4j.appender.RFAAUDIT.File=${hadoop.log.dir}/hdfs-audit.log
log4j.appender.RFAAUDIT.layout=org.apache.log4j.PatternLayout
log4j.appender.RFAAUDIT.layout.ConversionPattern=%d{ISO8601} %p %c{2}: %m%n
log4j.appender.RFAAUDIT.MaxFileSize=${hdfs.audit.log.maxfilesize}
log4j.appender.RFAAUDIT.MaxBackupIndex=${hdfs.audit.log.maxbackupindex}
#
# mapred audit logging
#
mapred.audit.logger=INFO,NullAppender
mapred.audit.log.maxfilesize=256MB
mapred.audit.log.maxbackupindex=20
log4j.logger.org.apache.hadoop.mapred.AuditLogger=${mapred.audit.logger}
log4j.additivity.org.apache.hadoop.mapred.AuditLogger=false
log4j.appender.MRAUDIT=org.apache.log4j.RollingFileAppender
log4j.appender.MRAUDIT.File=${hadoop.log.dir}/mapred-audit.log
log4j.appender.MRAUDIT.layout=org.apache.log4j.PatternLayout
log4j.appender.MRAUDIT.layout.ConversionPattern=%d{ISO8601} %p %c{2}: %m%n
log4j.appender.MRAUDIT.MaxFileSize=${mapred.audit.log.maxfilesize}
log4j.appender.MRAUDIT.MaxBackupIndex=${mapred.audit.log.maxbackupindex}
# Custom Logging levels
#log4j.logger.org.apache.hadoop.mapred.JobTracker=DEBUG
#log4j.logger.org.apache.hadoop.mapred.TaskTracker=DEBUG
#log4j.logger.org.apache.hadoop.hdfs.server.namenode.FSNamesystem.audit=DEBUG
# Jets3t library
log4j.logger.org.jets3t.service.impl.rest.httpclient.RestS3Service=ERROR
# AWS SDK & S3A FileSystem
log4j.logger.com.amazonaws=ERROR
log4j.logger.com.amazonaws.http.AmazonHttpClient=ERROR
log4j.logger.org.apache.hadoop.fs.s3a.S3AFileSystem=WARN
#
# Event Counter Appender
# Sends counts of logging messages at different severity levels to Hadoop Metrics.
#
log4j.appender.EventCounter=org.apache.hadoop.log.metrics.EventCounter
#
# Job Summary Appender
#
# Use following logger to send summary to separate file defined by
# hadoop.mapreduce.jobsummary.log.file :
# hadoop.mapreduce.jobsummary.logger=INFO,JSA
#
hadoop.mapreduce.jobsummary.logger=${hadoop.root.logger}
hadoop.mapreduce.jobsummary.log.file=hadoop-mapreduce.jobsummary.log
hadoop.mapreduce.jobsummary.log.maxfilesize=256MB
hadoop.mapreduce.jobsummary.log.maxbackupindex=20
log4j.appender.JSA=org.apache.log4j.RollingFileAppender
log4j.appender.JSA.File=${hadoop.log.dir}/${hadoop.mapreduce.jobsummary.log.file}
log4j.appender.JSA.MaxFileSize=${hadoop.mapreduce.jobsummary.log.maxfilesize}
log4j.appender.JSA.MaxBackupIndex=${hadoop.mapreduce.jobsummary.log.maxbackupindex}
log4j.appender.JSA.layout=org.apache.log4j.PatternLayout
log4j.appender.JSA.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}: %m%n
log4j.logger.org.apache.hadoop.mapred.JobInProgress$JobSummary=${hadoop.mapreduce.jobsummary.logger}
log4j.additivity.org.apache.hadoop.mapred.JobInProgress$JobSummary=false
#
# Yarn ResourceManager Application Summary Log
#
# Set the ResourceManager summary log filename
yarn.server.resourcemanager.appsummary.log.file=rm-appsummary.log
# Set the ResourceManager summary log level and appender
yarn.server.resourcemanager.appsummary.logger=${hadoop.root.logger}
#yarn.server.resourcemanager.appsummary.logger=INFO,RMSUMMARY
# To enable AppSummaryLogging for the RM,
# set yarn.server.resourcemanager.appsummary.logger to
# <LEVEL>,RMSUMMARY in hadoop-env.sh
# Appender for ResourceManager Application Summary Log
# Requires the following properties to be set
# - hadoop.log.dir (Hadoop Log directory)
# - yarn.server.resourcemanager.appsummary.log.file (resource manager app summary log filename)
# - yarn.server.resourcemanager.appsummary.logger (resource manager app summary log level and appender)
log4j.logger.org.apache.hadoop.yarn.server.resourcemanager.RMAppManager$ApplicationSummary=${yarn.server.resourcemanager.appsummary.logger}
log4j.additivity.org.apache.hadoop.yarn.server.resourcemanager.RMAppManager$ApplicationSummary=false
log4j.appender.RMSUMMARY=org.apache.log4j.RollingFileAppender
log4j.appender.RMSUMMARY.File=${hadoop.log.dir}/${yarn.server.resourcemanager.appsummary.log.file}
log4j.appender.RMSUMMARY.MaxFileSize=256MB
log4j.appender.RMSUMMARY.MaxBackupIndex=20
log4j.appender.RMSUMMARY.layout=org.apache.log4j.PatternLayout
log4j.appender.RMSUMMARY.layout.ConversionPattern=%d{ISO8601} %p %c{2}: %m%n
# HS audit log configs
#mapreduce.hs.audit.logger=INFO,HSAUDIT
#log4j.logger.org.apache.hadoop.mapreduce.v2.hs.HSAuditLogger=${mapreduce.hs.audit.logger}
#log4j.additivity.org.apache.hadoop.mapreduce.v2.hs.HSAuditLogger=false
#log4j.appender.HSAUDIT=org.apache.log4j.DailyRollingFileAppender
#log4j.appender.HSAUDIT.File=${hadoop.log.dir}/hs-audit.log
#log4j.appender.HSAUDIT.layout=org.apache.log4j.PatternLayout
#log4j.appender.HSAUDIT.layout.ConversionPattern=%d{ISO8601} %p %c{2}: %m%n
#log4j.appender.HSAUDIT.DatePattern=.yyyy-MM-dd
# Http Server Request Logs
#log4j.logger.http.requests.namenode=INFO,namenoderequestlog
#log4j.appender.namenoderequestlog=org.apache.hadoop.http.HttpRequestLogAppender
#log4j.appender.namenoderequestlog.Filename=${hadoop.log.dir}/jetty-namenode-yyyy_mm_dd.log
#log4j.appender.namenoderequestlog.RetainDays=3
#log4j.logger.http.requests.datanode=INFO,datanoderequestlog
#log4j.appender.datanoderequestlog=org.apache.hadoop.http.HttpRequestLogAppender
#log4j.appender.datanoderequestlog.Filename=${hadoop.log.dir}/jetty-datanode-yyyy_mm_dd.log
#log4j.appender.datanoderequestlog.RetainDays=3
#log4j.logger.http.requests.resourcemanager=INFO,resourcemanagerrequestlog
#log4j.appender.resourcemanagerrequestlog=org.apache.hadoop.http.HttpRequestLogAppender
#log4j.appender.resourcemanagerrequestlog.Filename=${hadoop.log.dir}/jetty-resourcemanager-yyyy_mm_dd.log
#log4j.appender.resourcemanagerrequestlog.RetainDays=3
#log4j.logger.http.requests.jobhistory=INFO,jobhistoryrequestlog
#log4j.appender.jobhistoryrequestlog=org.apache.hadoop.http.HttpRequestLogAppender
#log4j.appender.jobhistoryrequestlog.Filename=${hadoop.log.dir}/jetty-jobhistory-yyyy_mm_dd.log
#log4j.appender.jobhistoryrequestlog.RetainDays=3
#log4j.logger.http.requests.nodemanager=INFO,nodemanagerrequestlog
#log4j.appender.nodemanagerrequestlog=org.apache.hadoop.http.HttpRequestLogAppender
#log4j.appender.nodemanagerrequestlog.Filename=${hadoop.log.dir}/jetty-nodemanager-yyyy_mm_dd.log
#log4j.appender.nodemanagerrequestlog.RetainDays=3
# WebHdfs request log on datanodes
# Specify -Ddatanode.webhdfs.logger=INFO,HTTPDRFA on datanode startup to
# direct the log to a separate file.
#datanode.webhdfs.logger=INFO,console
#log4j.logger.datanode.webhdfs=${datanode.webhdfs.logger}
#log4j.appender.HTTPDRFA=org.apache.log4j.DailyRollingFileAppender
#log4j.appender.HTTPDRFA.File=${hadoop.log.dir}/hadoop-datanode-webhdfs.log
#log4j.appender.HTTPDRFA.layout=org.apache.log4j.PatternLayout
#log4j.appender.HTTPDRFA.layout.ConversionPattern=%d{ISO8601} %m%n
#log4j.appender.HTTPDRFA.DatePattern=.yyyy-MM-dd
#
# Fair scheduler state dump
#
# Use following logger to dump the state to a separate file
#log4j.logger.org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler.statedump=DEBUG,FSSTATEDUMP
#log4j.additivity.org.apache.hadoop.yarn.server.resourcemanager.scheduler.fair.FairScheduler.statedump=false
#log4j.appender.FSSTATEDUMP=org.apache.log4j.RollingFileAppender
#log4j.appender.FSSTATEDUMP.File=${hadoop.log.dir}/fairscheduler-statedump.log
#log4j.appender.FSSTATEDUMP.layout=org.apache.log4j.PatternLayout
#log4j.appender.FSSTATEDUMP.layout.ConversionPattern=%d{ISO8601} %p %c: %m%n
#log4j.appender.FSSTATEDUMP.MaxFileSize=${hadoop.log.maxfilesize}
#log4j.appender.FSSTATEDUMP.MaxBackupIndex=${hadoop.log.maxbackupindex}

@ -0,0 +1,166 @@
package com.henry.realprocess
import java.util.Properties
import com.alibaba.fastjson.JSON
import com.henry.realprocess.bean.{ClickLog, ClickLogWide, Message}
import com.henry.realprocess.task._
import com.henry.realprocess.util.GlobalConfigutil
import org.apache.flink.api.common.serialization.SimpleStringSchema
import org.apache.flink.streaming.api.{CheckpointingMode, TimeCharacteristic}
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment}
import org.apache.flink.api.scala._
import org.apache.flink.runtime.state.filesystem.FsStateBackend
import org.apache.flink.streaming.api.environment.CheckpointConfig
import org.apache.flink.streaming.api.functions.AssignerWithPeriodicWatermarks
import org.apache.flink.streaming.api.watermark.Watermark
import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer010
/**
* @Author: Henry
* @Description: 入口类
* @Date: Create in 2019/10/16 22:42
**/
object App {
def main(args: Array[String]): Unit = {
//------------ 初始化Flink流式环境,ctrl+alt+v --------------------
val env: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
// 设置处理时间为EventTime
env.setStreamTimeCharacteristic(TimeCharacteristic.EventTime)
// 设置并行度
env.setParallelism(1)
// 本地测试 加载本地集合 成为一个 Datastream 打印输出
// val localDataStream:DataStream[String] = env.fromCollection(
// List("hadoop", "hive", "hbase", "flink")
// )
// localDataStream.print()
//------------ 添加 checkpoint 的支持 -------------------------------
env.enableCheckpointing(5000) // 5秒启动一次checkpoint
// 设置 checkpoint 只检查 1 仅一次
env.getCheckpointConfig.setCheckpointingMode(CheckpointingMode.EXACTLY_ONCE)
// 设置两次 checkpoint 的最小时间间隔 1s
env.getCheckpointConfig.setMinPauseBetweenCheckpoints(1000)
// 设置checkpoint的超时时长, 60s
env.getCheckpointConfig.setCheckpointTimeout(60000)
// 允许的最大并行度
env.getCheckpointConfig.setMaxConcurrentCheckpoints(1)
// 当程序关闭时触发额外的checkpoint
env.getCheckpointConfig.enableExternalizedCheckpoints(
CheckpointConfig.ExternalizedCheckpointCleanup.RETAIN_ON_CANCELLATION)
// 设置checkpoint的地址
env.setStateBackend(new FsStateBackend("hdfs://master:9000/flink-checkpoint/"))
//--------------- 整合kafka --------------------------
val properties = new Properties()
// kafka 集群地址
properties.setProperty("bootstrap.servers", GlobalConfigutil.bootstrapServers)
// zookeeper 集群地址
properties.setProperty("zookeeper.connect", GlobalConfigutil.zookeeperConnect)
// kafka topic
properties.setProperty("input.topic", GlobalConfigutil.inputTopic)
// 消费者组 ID
properties.setProperty("gruop.id", GlobalConfigutil.gruopId)
// 自动提交拉取到的消费端的消息offset到kafka
properties.setProperty("enable.auto.commit", GlobalConfigutil.enableAutoCommit)
// 自动提交offset到zookeeper的时间间隔单位毫秒
properties.setProperty("auto.commit.interval.ms", GlobalConfigutil.autoCommitIntervalMs)
// 每次消费最新的数据
properties.setProperty("auto.offset.reset", GlobalConfigutil.autoOffsetReset)
// topic 反序列化器 属性集合
val consumer = new FlinkKafkaConsumer010[String](
GlobalConfigutil.inputTopic,
new SimpleStringSchema(),
properties)
val kafkaDataStream: DataStream[String] = env.addSource(consumer)
// kafkaDataStream.print()
// JSON -> 元组
val tupleDataStream = kafkaDataStream.map {
msgJson =>
val jsonObject = JSON.parseObject(msgJson)
val message = jsonObject.getString("message")
val count = jsonObject.getLong("count")
val timeStamp = jsonObject.getLong("timestamp")
// (message, count, timeStamp)
// 改造成样例类
// (ClickLog(message), count, timeStamp)
Message(ClickLog(message), count, timeStamp)
}
// tupleDataStream.print()
//----------------- 添加水印支持 -----------------------
var watermarkDataStream = tupleDataStream.assignTimestampsAndWatermarks(
new AssignerWithPeriodicWatermarks[Message] {
var currentTimestamp = 0L
// 延迟时间
var maxDelayTime = 2000L
// 获取当前时间戳
override def getCurrentWatermark: Watermark = {
// 设置水印时间比事件时间小 2s
new Watermark(currentTimestamp - maxDelayTime)
}
// 获取当前事件时间
override def extractTimestamp(
element: Message,
previousElementTimestamp: Long): Long = {
currentTimestamp = Math.max(element.timeStamp, previousElementTimestamp)
currentTimestamp
}
})
// 数据的预处理
val clickLogWideDateStream : DataStream[ClickLogWide] = PreprocessTask.process(watermarkDataStream)
// clickLogWideDateStream.print()
// 转换
// ChannelRealHotTask.process(clickLogWideDateStream).print()
// ChannelRealHotTask.process(clickLogWideDateStream)
// 转换 PVUV
ChannelPvUvTask.process(clickLogWideDateStream)
// ChannelPvUvTaskMerge.process(clickLogWideDateStream)
// ChannelFreshnessTask.process(clickLogWideDateStream)
// 重构模板方法
ChannelFreshnessTaskTrait.process(clickLogWideDateStream)
// ChannelAreaTask 测试
ChannelAreaTask.process(clickLogWideDateStream)
// ChannelNetworkTask 测试
ChannelNetworkTask.process(clickLogWideDateStream)
// ChannelBrowserTask 测试
ChannelBrowserTask.process(clickLogWideDateStream)
// 执行任务
env.execute("real-process")
}
}

@ -0,0 +1,77 @@
package com.henry.realprocess.bean
import com.alibaba.fastjson.JSON
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/10/20 14:45
**/
//频道IDchannelID
//产品类别IDcategoryID
//产品IDproduceID
//国家country
//省份province
//城市city
//网络方式network
//来源方式source
//浏览器类型browserType
//进入网站时间entryTime
//离开网站时间leaveTime
//用户IDuserID
case class ClickLog (
// 1alt + 下拉
// 2ctrl + shift + 选中各个变量
var channelID:String,
var categoryID:String,
var produceID:String,
var country:String,
var province:String,
var city:String,
var network:String,
var source:String,
var browserType:String,
var entryTime:String,
var leaveTime:String,
var userID:String
)
object ClickLog{
def apply(json: String): ClickLog = {
// 先把json转换为JSONObject
val jsonObject = JSON.parseObject(json)
// 提取jsonObject中的各个属性赋值给样例类
var channelID = jsonObject.getString("channelID")
var categoryID = jsonObject.getString("categoryID")
var produceID = jsonObject.getString("produceID")
var country = jsonObject.getString("country")
var province = jsonObject.getString("province")
var city = jsonObject.getString("city")
var network = jsonObject.getString("network")
var source = jsonObject.getString("source")
var browserType = jsonObject.getString("browserType")
var entryTime = jsonObject.getString("entryTime")
var leaveTime = jsonObject.getString("leaveTime")
var userID = jsonObject.getString("userID")
ClickLog(
channelID,
categoryID,
produceID,
country,
province,
city,
network,
source,
browserType,
entryTime,
leaveTime,
userID
)
}
}

@ -0,0 +1,61 @@
package com.henry.realprocess.bean
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/10/27 14:19
**/
// 频道IDchannelID
// 产品类别IDcategoryID
// 产品IDproduceID
// 国家country
// 省份province
// 城市city
// 网络方式network
// 来源方式source
// 浏览器类型browserType
// 进入网站时间entryTime
// 离开网站时间leaveTime
// 用户IDuserID
// ---- 添加以下字段 ---------------
// 用户访问次数count
// 用户访问的时间timestamp
// 国家省份城市拼接address
// 年月yearMonth
// 年月日yearMonthDay
// 年月日时yearMonthDayHour
// 是否为访问某个频道的新用户isNew 0表示否 1表示是
// 在某一小时内是否为某个频道的新用户isHourNew 0表示否 1表示是
// 在某一天内是否为某个频道的新用户isDayNew 0表示否 1表示是
// 在某一天月是否为某个频道的新用户isMonthNew 0表示否 1表示是
case class ClickLogWide (
// 1alt + 下拉
// 2ctrl + shift + 选中各个变量
var channelID:String,
var categoryID:String,
var produceID:String,
var country:String,
var province:String,
var city:String,
var network:String,
var source:String,
var browserType:String,
var entryTime:String,
var leaveTime:String,
var userID:String,
//--- 新增 ---------------------------
var count:Long,
var timestamp:Long,
var address:String,
var yearMonth:String,
var yearMonthDay:String,
var yearMonthDayHour:String,
var isNew:Int,
var isHourNew:Int,
var isDayNew:Int,
var isMonthNew:Int
)

@ -0,0 +1,12 @@
package com.henry.realprocess.bean
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/10/20 15:58
**/
case class Message (
var clickLog:ClickLog,
var count:Long,
var timeStamp:Long
)

@ -0,0 +1,80 @@
package com.henry.realprocess.task
import com.henry.realprocess.bean.ClickLogWide
import com.henry.realprocess.task.ChannelBrowserTask.pvColName
import org.apache.commons.lang.StringUtils
import org.apache.flink.streaming.api.scala.{DataStream, KeyedStream, WindowedStream}
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/11/3 10:42
**/
trait BaseTask[T] {
// 1 转换
def map(clickLogWideDataStream : DataStream[ClickLogWide]): DataStream[T]
// 2 分组
def keyBy(mapDataStream : DataStream[T]): KeyedStream[T, String]
// 3 时间窗口
def timeWindow(keyedStream: KeyedStream[T, String]) : WindowedStream[T, String, TimeWindow] = {
// 因为所有自类都是 3 秒的时间窗口
keyedStream.timeWindow(Time.seconds(3))
}
// 4 聚合
def reduce(windowedStream : WindowedStream[T, String, TimeWindow]) : DataStream[T]
// 5 落地 HBase
def sink2HBase(reduceDataStream: DataStream[T])
// 定义模板执行顺序
def process(clickLogWideDataStream : DataStream[ClickLogWide]): Unit = {
val mapDataStream: DataStream[T] = map(clickLogWideDataStream)
val keyedStream: KeyedStream[T, String] = keyBy(mapDataStream)
val windowedStream: WindowedStream[T, String, TimeWindow] = timeWindow(keyedStream)
val reduceStream: DataStream[T] = reduce(windowedStream)
sink2HBase(reduceStream)
}
// 检测老用户是否第一次访问
val isOld = (isNew: Int, isDateNew: Int) => if (isNew == 0 && isDateNew == 1) 1 else 0
// 创建 HBase 相关列
var tableName = ""
var clfName = "info"
var rowkey = ""
var channelIdColName = "channelID"
var browserColName = "browser"
var dateColName = "date"
var pvColName = "pv"
var uvColName = "uv"
var newCountColName = "newCount"
var oldCountColName = "oldCount"
/* 累加相关列的值
* @param resultMap map集合
* @param column 待查询的列
* @param currentValue 当前值
* @return 累加后的值
*/
def getTotal(resultMap: Map[String, String],column:String,currentValue:Long):Long={
var total = currentValue
// 如果resultMap不为空,并且可以去到相关列的值,那么就进行累加
if (resultMap != null && StringUtils.isNotBlank(resultMap.getOrElse(column,""))) {
total = resultMap(column).toLong + currentValue
}
total
}
}

@ -0,0 +1,170 @@
package com.henry.realprocess.task
import com.henry.realprocess.bean.ClickLogWide
import com.henry.realprocess.util.HBaseUtil
import org.apache.commons.lang.StringUtils
import org.apache.flink.streaming.api.scala.{DataStream, KeyedStream, WindowedStream}
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.windowing.time.Time
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/11/3 14:06
**/
// 样例类
case class ChannelArea(
var channelId: String,
var area: String,
var date: String,
var pv: Long,
var uv: Long,
var newCount: Long,
var oldCount: Long
)
object ChannelAreaTask extends BaseTask [ChannelArea]{
// 1 转换
override def map(clickLogWideDataStream: DataStream[ClickLogWide]): DataStream[ChannelArea] = {
clickLogWideDataStream.flatMap{
clickLogWide =>{
// 如果是老用户并且在该时间段内第一次来就计数 1. 否则计 0
val isOld = (isNew: Int, isDateNew: Int) => if (isNew == 0 && isDateNew == 1) 1 else 0
List(
ChannelArea( // 月维度
clickLogWide.channelID,
clickLogWide.address,
clickLogWide.yearMonth,
clickLogWide.count, // pv 每来一个数据进行累加
clickLogWide.isMonthNew, // uv 第一次来的时候只计数一次
clickLogWide.isNew, // 当是 New 的时候进行累加
isOld(clickLogWide.isNew, clickLogWide.isMonthNew)
),
ChannelArea( // 日维度
clickLogWide.channelID,
clickLogWide.address,
clickLogWide.yearMonth,
clickLogWide.count,
clickLogWide.isDayNew,
clickLogWide.isNew,
isOld(clickLogWide.isNew, clickLogWide.isDayNew)
),
ChannelArea( // 小时维度
clickLogWide.channelID,
clickLogWide.address,
clickLogWide.yearMonth,
clickLogWide.count,
clickLogWide.isHourNew,
clickLogWide.isNew,
isOld(clickLogWide.isNew, clickLogWide.isHourNew)
)
)
}
}
}
// 2 分组 根据 频道ID+地域+时间
override def keyBy(mapDataStream: DataStream[ChannelArea]): KeyedStream[ChannelArea, String] = {
mapDataStream.keyBy{
area =>
area.channelId + " : " + area.area + " : " + area.date
}
}
// 3 时间窗口, 这段代码每个子类都是一样的,可以写到父类中
// override def timeWindow(keyedStream: KeyedStream[ChannelArea, String]): WindowedStream[ChannelArea, String, TimeWindow] = {}
// 4 聚合 累加4个字段
override def reduce(windowedStream: WindowedStream[ChannelArea, String, TimeWindow]) = {
windowedStream.reduce {
(t1, t2) =>
ChannelArea(t1.channelId, t1.area,
t1.date,
t1.pv + t2.pv,
t1.uv + t2.uv,
t1.newCount + t2.newCount,
t1.oldCount + t2.oldCount)
}
}
// 5 落地HBase
override def sink2HBase(reduceDataStream: DataStream[ChannelArea]): Unit = {
reduceDataStream.addSink{
area => {
// HBase 相关字段
val tableName = "channel_area"
val clfName = "info"
val rowkey = area.channelId + ":" + area.area + ":" + area.date
val channelIdColumn = "channelId"
val areaColumn = "area"
val dateColumn = "date"
val pvColumn = "pv"
val uvColumn = "uv"
val newCountColumn = "newCount"
val oldCountColumn = "oldCount"
// 查询 HBase
val pvInHbase: String = HBaseUtil.getData(tableName,rowkey,clfName,pvColumn)
val uvInHbase: String = HBaseUtil.getData(tableName,rowkey,clfName,uvColumn)
val newCountInHbase: String = HBaseUtil.getData(tableName,rowkey,clfName,newCountColumn)
val oldCountInHbase: String = HBaseUtil.getData(tableName,rowkey,clfName,oldCountColumn)
// 累加
var totalPv = 0L
var totalUv = 0L
var totalNewCount = 0L
var totalOldCount = 0L
// PV
if(StringUtils.isNotBlank(pvInHbase)){
totalPv = pvInHbase.toLong+area.pv
}else{
totalPv = area.pv
}
// UV
if(StringUtils.isNotBlank(uvInHbase)){
totalUv = uvInHbase.toLong+area.uv
}else{
totalUv = area.uv
}
// totalNewCount
if(StringUtils.isNotBlank(newCountInHbase)){
totalNewCount = newCountInHbase.toLong+area.newCount
}else{
totalNewCount = area.newCount
}
// totalOldCount
if(StringUtils.isNotBlank(oldCountInHbase)){
totalOldCount = oldCountInHbase.toLong+area.oldCount
}else{
totalOldCount = area.oldCount
}
// 保存数据
HBaseUtil.putMapData(tableName,rowkey,clfName,Map(
channelIdColumn->area.channelId,
areaColumn->area.area,
dateColumn->area.date,
pvColumn->totalPv,
uvColumn->totalUv,
newCountColumn->totalNewCount,
oldCountColumn->totalOldCount
))
}
}
}
}

@ -0,0 +1,130 @@
package com.henry.realprocess.task
import com.henry.realprocess.bean.ClickLogWide
import com.henry.realprocess.util.HBaseUtil
import org.apache.commons.lang.StringUtils
import org.apache.flink.streaming.api.scala.{DataStream, KeyedStream, WindowedStream}
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.functions.sink.SinkFunction
import org.apache.flink.streaming.api.windowing.time.Time
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/11/3 15:52
**/
// 2. 添加一个`ChannelBrowser`样例类它封装要统计的四个业务字段频道IDchannelID运营商
// browser日期datepvuv新用户newCount老用户oldCount
case class ChannelBrowser(
var channelId: String,
var browser: String,
var date: String,
var pv: Long,
var uv: Long,
var newCount: Long,
var oldCount: Long
)
object ChannelBrowserTask extends BaseTask[ChannelBrowser]{
override def map(clickLogWideDataStream: DataStream[ClickLogWide]): DataStream[ChannelBrowser] = {
clickLogWideDataStream.flatMap{
clickLogWide => {
List(
ChannelBrowser( // 月维度
clickLogWide.channelID,
clickLogWide.browserType,
clickLogWide.yearMonth,
clickLogWide.count,
clickLogWide.isMonthNew,
clickLogWide.isNew,
isOld(clickLogWide.isNew, clickLogWide.isMonthNew)
),
ChannelBrowser( // 天维度
clickLogWide.channelID,
clickLogWide.browserType,
clickLogWide.yearMonthDay,
clickLogWide.count,
clickLogWide.isDayNew,
clickLogWide.isNew,
isOld(clickLogWide.isNew, clickLogWide.isDayNew)
),
ChannelBrowser( // 小时维度
clickLogWide.channelID,
clickLogWide.browserType,
clickLogWide.yearMonthDayHour,
clickLogWide.count,
clickLogWide.isHourNew,
clickLogWide.isNew,
isOld(clickLogWide.isNew, clickLogWide.isHourNew)
)
)
}
}
}
override def keyBy(mapDataStream: DataStream[ChannelBrowser]): KeyedStream[ChannelBrowser, String] = {
mapDataStream.keyBy {
browser =>
browser.channelId +" : "+ browser.browser +" : "+ browser.date
}
}
override def reduce(windowedStream: WindowedStream[ChannelBrowser, String, TimeWindow]): DataStream[ChannelBrowser] = {
windowedStream.reduce {
(t1, t2) => {
ChannelBrowser(
t1.channelId,
t1.browser,
t1.date,
t1.pv + t2.pv,
t1.uv + t2.uv,
t1.newCount + t2.newCount,
t1.oldCount + t2.oldCount
)
}
}
}
override def sink2HBase(reduceDataStream: DataStream[ChannelBrowser]): Unit = {
reduceDataStream.addSink(
browser => {
// 创建 HBase 相关列 - 准备hbase的表名列族名rowkey名列名
// 不需要加 val 或者 var 因为引用的是父类的变量
tableName = "channel_browser"
rowkey = s"${browser.channelId} : ${browser.date} : ${browser.browser}" // 引用变量的方式
browserColName = "browser"
// 查询 HBase
// - 判断hbase中是否已经存在结果记录
val resultMap: Map[String, String] = HBaseUtil.getMapData(tableName, rowkey, clfName,
List( pvColName, uvColName, newCountColName, oldCountColName )
)
// 数据累加
// 保存数据
HBaseUtil.putMapData(
tableName, rowkey, clfName, Map(
channelIdColName -> browser.channelId,
browserColName -> browser.browser,
dateColName -> browser.date,
pvColName -> getTotal(resultMap, pvColName , browser.pv),
uvColName -> getTotal(resultMap, uvColName , browser.uv),
newCountColName -> getTotal(resultMap, newCountColName , browser.newCount),
oldCountColName -> getTotal(resultMap, oldCountColName , browser.newCount)
)
)
}
)
}
}

@ -0,0 +1,115 @@
package com.henry.realprocess.task
import com.henry.realprocess.bean.ClickLogWide
import com.henry.realprocess.util.HBaseUtil
import org.apache.commons.lang.StringUtils
import org.apache.flink.streaming.api.scala.{DataStream, KeyedStream, WindowedStream}
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.functions.sink.SinkFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/10/31 21:38
**/
case class ChannelFreshness(
var channelId : String ,
var date : String ,
var newCount: Long ,
val oldCount: Long
)
/**
* 1 转换
* 2 分组
* 3 时间窗口
* 4 聚合
* 5 落地 HBase
*/
object ChannelFreshnessTask {
def process(clickLogWideDataStream: DataStream[ClickLogWide])= {
// 1 转换
val mapDataStream: DataStream[ChannelFreshness] = clickLogWideDataStream.flatMap {
clickLog =>
// 如果是老用户只有在第一次来的时候计数为 1
val isOld = (isNew: Int, isDateNew: Int) => if (isNew == 0 && isDateNew == 1) 1 else 0
// 统计新用户老用户数量
List(
ChannelFreshness(clickLog.channelID, clickLog.yearMonthDayHour, clickLog.isNew, isOld(clickLog.isNew, clickLog.isHourNew)),
ChannelFreshness(clickLog.channelID, clickLog.yearMonthDay, clickLog.isNew, isOld(clickLog.isNew, clickLog.isDayNew)),
ChannelFreshness(clickLog.channelID, clickLog.yearMonth, clickLog.isNew, isOld(clickLog.isNew, clickLog.isMonthNew))
)
}
// 2 分组
val keyedStream: KeyedStream[ChannelFreshness, String] = mapDataStream.keyBy {
freshness => (freshness.channelId + freshness.date)
}
// 3 时间窗口
val windowedStream: WindowedStream[ChannelFreshness, String, TimeWindow] = keyedStream.timeWindow(Time.seconds(3))
// 4 聚合
val reduceDataStream: DataStream[ChannelFreshness] = windowedStream.reduce {
(t1, t2) =>
ChannelFreshness(t1.channelId, t1.date, t1.newCount + t2.newCount, t1.oldCount + t2.oldCount)
}
// 5 落地 HBase
reduceDataStream.addSink(new SinkFunction[ChannelFreshness] {
override def invoke(value: ChannelFreshness): Unit = {
// 创建 HBase 相关变量
val tableName = "channel_freshness"
val clfName = "info"
val channelIdColumn = "channelId"
val dateColumn = "date"
val newCountColumn = "newCount"
val oldCountColumn = "oldCount"
val rowkey = value.channelId + ":" + value.date
// 查询历史数据
val resultMap: Map[String, String] = HBaseUtil.getMapData(tableName, rowkey, clfName, List(newCountColumn, oldCountColumn))
// 累加
var totalNewCount = 0L
var totalOldCount = 0L
if(resultMap != null && StringUtils.isNotBlank(resultMap.getOrElse(newCountColumn,""))){
resultMap(newCountColumn).toLong + value.newCount
}
else {
totalNewCount = value.newCount
}
if(resultMap != null && StringUtils.isNotBlank(resultMap.getOrElse(oldCountColumn,""))){
resultMap(oldCountColumn).toLong + value.oldCount
}
else {
totalOldCount = value.oldCount
}
// 保存数据
HBaseUtil.putMapData(tableName, rowkey, clfName, Map(
// 向如下列插入数据
channelIdColumn -> value.channelId ,
dateColumn -> value.date ,
newCountColumn -> totalNewCount ,
oldCountColumn -> totalOldCount
))
}
})
}
}

@ -0,0 +1,134 @@
package com.henry.realprocess.task
import com.henry.realprocess.bean.ClickLogWide
import com.henry.realprocess.util.HBaseUtil
import org.apache.commons.lang.StringUtils
import org.apache.flink.streaming.api.scala.{DataStream, KeyedStream, WindowedStream}
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.functions.sink.SinkFunction
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/10/31 21:38
**/
case class ChannelFreshness(
var channelId : String ,
var date : String ,
var newCount: Long ,
val oldCount: Long
)
/**
* 1 转换
* 2 分组
* 3 时间窗口
* 4 聚合
* 5 落地 HBase
*/
object ChannelFreshnessTaskTrait extends BaseTask[ChannelFreshness] {
/* Alt + Enter */
// 1 转换
override def map(clickLogWideDataStream: DataStream[ClickLogWide]): DataStream[ChannelFreshness] = {
val mapDataStream: DataStream[ChannelFreshness] = clickLogWideDataStream.flatMap {
clickLog =>
// 如果是老用户只有在第一次来的时候计数为 1
val isOld = (isNew: Int, isDateNew: Int) => if (isNew == 0 && isDateNew == 1) 1 else 0
// 统计新用户老用户数量
List(
ChannelFreshness(clickLog.channelID, clickLog.yearMonthDayHour, clickLog.isNew, isOld(clickLog.isNew, clickLog.isHourNew)),
ChannelFreshness(clickLog.channelID, clickLog.yearMonthDay, clickLog.isNew, isOld(clickLog.isNew, clickLog.isDayNew)),
ChannelFreshness(clickLog.channelID, clickLog.yearMonth, clickLog.isNew, isOld(clickLog.isNew, clickLog.isMonthNew))
)
}
mapDataStream
}
// 2 分组
override def keyBy(mapDataStream: DataStream[ChannelFreshness]): KeyedStream[ChannelFreshness, String] = {
// 或者mapDataStream.keyBy {freshness => (freshness.channelId + freshness.date)
val keyedStream: KeyedStream[ChannelFreshness, String] = mapDataStream.keyBy {
freshness => (freshness.channelId + freshness.date)
}
keyedStream
}
// 3 时间窗口
override def timeWindow(keyedStream: KeyedStream[ChannelFreshness, String]): WindowedStream[ChannelFreshness, String, TimeWindow] = {
val windowedStream: WindowedStream[ChannelFreshness, String, TimeWindow] = keyedStream.timeWindow(Time.seconds(3))
windowedStream
}
// 4 聚合
override def reduce(windowedStream: WindowedStream[ChannelFreshness, String, TimeWindow]): DataStream[ChannelFreshness] = {
val reduceDataStream: DataStream[ChannelFreshness] = windowedStream.reduce {
(t1, t2) =>
ChannelFreshness(t1.channelId, t1.date, t1.newCount + t2.newCount, t1.oldCount + t2.oldCount)
}
reduceDataStream
}
// 5 落地 HBase
override def sink2HBase(reduceDataStream: DataStream[ChannelFreshness]): Unit = {
reduceDataStream.addSink(new SinkFunction[ChannelFreshness] {
override def invoke(value: ChannelFreshness): Unit = {
// 创建 HBase 相关变量
val tableName = "channel_freshness"
val clfName = "info"
val channelIdColumn = "channelId"
val dateColumn = "date"
val newCountColumn = "newCount"
val oldCountColumn = "oldCount"
val rowkey = value.channelId + ":" + value.date
// 查询历史数据
val resultMap: Map[String, String] = HBaseUtil.getMapData(tableName, rowkey, clfName, List(newCountColumn, oldCountColumn))
// 累加
var totalNewCount = 0L
var totalOldCount = 0L
if(resultMap != null && StringUtils.isNotBlank(resultMap.getOrElse(newCountColumn,""))){
resultMap(newCountColumn).toLong + value.newCount
}
else {
totalNewCount = value.newCount
}
if(resultMap != null && StringUtils.isNotBlank(resultMap.getOrElse(oldCountColumn,""))){
resultMap(oldCountColumn).toLong + value.oldCount
}
else {
totalOldCount = value.oldCount
}
// 保存数据
HBaseUtil.putMapData(tableName, rowkey, clfName, Map(
// 向如下列插入数据
channelIdColumn -> value.channelId ,
dateColumn -> value.date ,
newCountColumn -> totalNewCount ,
oldCountColumn -> totalOldCount
))
}
})
}
}

@ -0,0 +1,173 @@
package com.henry.realprocess.task
import com.henry.realprocess.bean.ClickLogWide
import com.henry.realprocess.util.HBaseUtil
import org.apache.commons.lang.StringUtils
import org.apache.flink.streaming.api.scala.{DataStream, KeyedStream, WindowedStream}
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.functions.sink.SinkFunction
import org.apache.flink.streaming.api.windowing.time.Time
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/11/3 15:52
**/
// 2. 添加一个`ChannelNetwork`样例类它封装要统计的四个业务字段频道IDchannelID运营商
// network日期datepvuv新用户newCount老用户oldCount
case class ChannelNetWork(
var channelId: String,
var network: String,
var date: String,
var pv: Long,
var uv: Long,
var newCount: Long,
var oldCount: Long
)
object ChannelNetworkTask extends BaseTask[ChannelNetWork]{
override def map(clickLogWideDataStream: DataStream[ClickLogWide]): DataStream[ChannelNetWork] = {
val isOld = (isNew: Int, isDateNew: Int) => if (isNew == 0 && isDateNew == 1) 1 else 0
clickLogWideDataStream.flatMap{
clickLogWide => {
List(
ChannelNetWork( // 月维度
clickLogWide.channelID,
clickLogWide.network,
clickLogWide.yearMonth,
clickLogWide.count,
clickLogWide.isMonthNew,
clickLogWide.isNew,
isOld(clickLogWide.isNew, clickLogWide.isMonthNew)
),
ChannelNetWork( // 天维度
clickLogWide.channelID,
clickLogWide.network,
clickLogWide.yearMonthDay,
clickLogWide.count,
clickLogWide.isDayNew,
clickLogWide.isNew,
isOld(clickLogWide.isNew, clickLogWide.isDayNew)
),
ChannelNetWork( // 小时维度
clickLogWide.channelID,
clickLogWide.network,
clickLogWide.yearMonthDayHour,
clickLogWide.count,
clickLogWide.isHourNew,
clickLogWide.isNew,
isOld(clickLogWide.isNew, clickLogWide.isHourNew)
)
)
}
}
}
override def keyBy(mapDataStream: DataStream[ChannelNetWork]): KeyedStream[ChannelNetWork, String] = {
mapDataStream.keyBy {
network =>
network.channelId +" : "+ network.network +" : "+ network.date
}
}
override def reduce(windowedStream: WindowedStream[ChannelNetWork, String, TimeWindow]): DataStream[ChannelNetWork] = {
windowedStream.reduce {
(t1, t2) => {
ChannelNetWork(
t1.channelId,
t1.network,
t1.date,
t1.pv + t2.pv,
t1.uv + t2.uv,
t1.newCount + t2.newCount,
t1.oldCount + t2.oldCount
)
}
}
}
override def sink2HBase(reduceDataStream: DataStream[ChannelNetWork]): Unit = {
reduceDataStream.addSink(
network => {
// 创建 HBase 相关列 - 准备hbase的表名列族名rowkey名列名
val tableName = "channel_network"
val clfName = "info"
// 频道IDchannelID运营商network日期datepvuv新用户newCount老用户(oldCount
val rowkey = s"${network.channelId} : ${network.date} : ${network.network}" // 引用变量的方式
val channelIdColName = "channelID"
val networkColName = "network"
val dateColName = "date"
val pvColName = "pv"
val uvColName = "uv"
val newCountColName = "newCount"
val oldCountColName = "oldCount"
// 查询 HBase
// - 判断hbase中是否已经存在结果记录
val resultMap: Map[String, String] = HBaseUtil.getMapData(tableName, rowkey, clfName,
List( pvColName, uvColName, newCountColName, oldCountColName )
)
// 数据累加
var totalPv = 0L
var totalUv = 0L
var totalNewCount = 0L
var totalOldCount = 0L
// totalPv
if (resultMap != null && resultMap.size > 0 && StringUtils.isNotBlank(resultMap.getOrElse(pvColName,""))) {
totalPv = resultMap(pvColName).toLong + network.pv
}
else {
totalPv = network.pv
}
// totalUv
if (resultMap != null && resultMap.size > 0 && StringUtils.isNotBlank(resultMap.getOrElse(uvColName,""))) {
totalUv = resultMap(uvColName).toLong + network.uv
}
else {
totalUv = network.uv
}
// totalNewCount
if (resultMap != null && resultMap.size > 0 && StringUtils.isNotBlank(resultMap.getOrElse(newCountColName,""))) {
totalNewCount = resultMap(newCountColName).toLong + network.newCount
}
else {
totalNewCount = network.newCount
}
// totalOldCount
if (resultMap != null && resultMap.size > 0 && StringUtils.isNotBlank(resultMap.getOrElse(oldCountColName,""))) {
totalOldCount = resultMap(oldCountColName).toLong + network.oldCount
}
else {
totalOldCount = network.oldCount
}
// 保存数据
HBaseUtil.putMapData(
tableName, rowkey, clfName, Map(
channelIdColName -> network.channelId,
networkColName -> network.network,
dateColName -> network.date,
pvColName -> totalPv,
uvColName -> totalUv,
newCountColName -> totalNewCount,
oldCountColName -> totalOldCount
)
)
}
)
}
}

@ -0,0 +1,108 @@
package com.henry.realprocess.task
import com.henry.realprocess.bean.ClickLogWide
import com.henry.realprocess.util.HBaseUtil
import org.apache.flink.streaming.api.scala.{DataStream, WindowedStream}
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.functions.sink.SinkFunction
import org.apache.flink.streaming.api.scala.KeyedStream
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.commons.lang.StringUtils
/**
* @Author: Henry
* @Description: 渠道 PV/UV
* 1字段转换
* 2分组
* 3时间窗口
* 4聚合
* 5落地HBase
* @Date: Create in 2019/10/30 20:15
**/
case class ChannelPvUv(
val channelId: String,
val yearDayMonthHour: String,
val pv: Long,
val uv: Long
)
object ChannelPvUvTask {
def process(clickLogWideDateStream : DataStream[ClickLogWide])= {
// 1转换
val channelPvUvDS: DataStream[ChannelPvUv] = clickLogWideDateStream.map{
clickLogWide => {
ChannelPvUv(clickLogWide.channelID, clickLogWide.yearMonthDayHour,
clickLogWide.count, clickLogWide.isHourNew)
}
}
// 2分组
val keyedStream: KeyedStream[ChannelPvUv, String] = channelPvUvDS.keyBy{
channelPvUv => channelPvUv.channelId + channelPvUv.yearDayMonthHour
}
// 3窗口
val windowedStream: WindowedStream[ChannelPvUv, String, TimeWindow] =
keyedStream.timeWindow(Time.seconds(3))
// 4聚合
val reduceDataStream: DataStream[ChannelPvUv] = windowedStream.reduce{
(t1, t2) => ChannelPvUv(t1.channelId, t1.yearDayMonthHour, t1.pv + t2.pv, t1.uv + t2.uv)
}
// 5HBase 落地
reduceDataStream.addSink(new SinkFunction[ChannelPvUv] {
override def invoke(value: ChannelPvUv): Unit = {
// HBase 相关字段
val tableName = "channel_pvuv"
val clfName = "info"
val channelIdColumn = "channelId"
val yearMonthDayHourColumn = "yearMonthDayHour"
val pvColumn = "pv"
val uvColumn = "uv"
val rowkey = value.channelId + ":" + value.yearDayMonthHour
// 查询 HBase 并且获取相关记录
val pvInHBase: String = HBaseUtil.getData(tableName, rowkey, clfName, pvColumn)
val uvInHBase: String = HBaseUtil.getData(tableName, rowkey, clfName, uvColumn)
var totalPv = 0L
var totalUv = 0L
// 如果 HBase 中没有 PV 就把当前值保存如果有值就进行累加
if(StringUtils.isBlank(pvInHBase)){
totalPv = value.pv
}
else {
totalPv = pvInHBase.toLong + value.pv
}
// 如果 HBase 中没有 UV 就把当前值保存如果有值就进行累加
if(StringUtils.isBlank(uvInHBase)){
totalUv = value.uv
}
else {
totalUv = uvInHBase.toLong + value.uv
}
// 保存数据
HBaseUtil.putMapData(tableName, rowkey, clfName, Map(
channelIdColumn -> value.channelId ,
yearMonthDayHourColumn -> value.yearDayMonthHour ,
pvColumn -> value.pv.toString ,
uvColumn -> value.uv.toString
))
}
})
}
}

@ -0,0 +1,105 @@
package com.henry.realprocess.task
import com.henry.realprocess.bean.ClickLogWide
import com.henry.realprocess.util.HBaseUtil
import org.apache.flink.api.scala._
import org.apache.commons.lang.StringUtils
import org.apache.flink.streaming.api.functions.sink.SinkFunction
import org.apache.flink.streaming.api.scala.{DataStream, KeyedStream, WindowedStream}
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/10/30 22:42
**/
case class ChannelPvUv(
val channelId: String,
val yearDayMonthHour: String,
val pv: Long,
val uv: Long
)
object ChannelPvUvTaskMerge {
def process(clickLogWideDateStream : DataStream[ClickLogWide])= {
// 1转换
val channelPvUvDS: DataStream[ChannelPvUv] = clickLogWideDateStream.flatMap{
clickLogWide => {
List(
ChannelPvUv(clickLogWide.channelID, clickLogWide.yearMonthDayHour, clickLogWide.count, clickLogWide.isHourNew) ,
ChannelPvUv(clickLogWide.channelID, clickLogWide.yearMonthDay, clickLogWide.count, clickLogWide.isDayNew) ,
ChannelPvUv(clickLogWide.channelID, clickLogWide.yearMonth, clickLogWide.count, clickLogWide.isMonthNew)
)
}
}
// 2分组
val keyedStream: KeyedStream[ChannelPvUv, String] = channelPvUvDS.keyBy{
channelPvUv => channelPvUv.channelId + channelPvUv.yearDayMonthHour
}
// 3窗口
val windowedStream: WindowedStream[ChannelPvUv, String, TimeWindow] =
keyedStream.timeWindow(Time.seconds(3))
// 4聚合
val reduceDataStream: DataStream[ChannelPvUv] = windowedStream.reduce{
(t1, t2) => ChannelPvUv(t1.channelId, t1.yearDayMonthHour, t1.pv + t2.pv, t1.uv + t2.uv)
}
// 5HBase 落地
reduceDataStream.addSink(new SinkFunction[ChannelPvUv] {
override def invoke(value: ChannelPvUv): Unit = {
// HBase 相关字段
val tableName = "channel_pvuv"
val clfName = "info"
val channelIdColumn = "channelId"
val yearMonthDayHourColumn = "yearMonthDayHour"
val pvColumn = "pv"
val uvColumn = "uv"
val rowkey = value.channelId + ":" + value.yearDayMonthHour
// 查询 HBase 并且获取相关记录
val pvInHBase: String = HBaseUtil.getData(tableName, rowkey, clfName, pvColumn)
val uvInHBase: String = HBaseUtil.getData(tableName, rowkey, clfName, uvColumn)
var totalPv = 0L
var totalUv = 0L
// 如果 HBase 中没有 PV 就把当前值保存如果有值就进行累加
if(StringUtils.isBlank(pvInHBase)){
totalPv = value.pv
}
else {
totalPv = pvInHBase.toLong + value.pv
}
// 如果 HBase 中没有 UV 就把当前值保存如果有值就进行累加
if(StringUtils.isBlank(uvInHBase)){
totalUv = value.uv
}
else {
totalUv = uvInHBase.toLong + value.uv
}
// 保存数据
HBaseUtil.putMapData(tableName, rowkey, clfName, Map(
channelIdColumn -> value.channelId ,
yearMonthDayHourColumn -> value.yearDayMonthHour ,
pvColumn -> value.pv.toString ,
uvColumn -> value.uv.toString
))
}
})
}
}

@ -0,0 +1,88 @@
package com.henry.realprocess.task
import com.henry.realprocess.bean.ClickLogWide
import com.henry.realprocess.util.HBaseUtil
import org.apache.flink.streaming.api.scala.{DataStream, WindowedStream}
import org.apache.flink.api.scala._
import org.apache.flink.streaming.api.functions.sink.SinkFunction
import org.apache.flink.streaming.api.scala.KeyedStream
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.commons.lang.StringUtils
/**
* @Author: Henry
* @Description: 频道热点分析业务开发
* 1字段转换
* 2分组
* 3时间窗口
* 4聚合
* 5落地HBase
* @Date: Create in 2019/10/29 20:22
**/
case class ChannelRealHot(var channelid:String, var visited:Long)
object ChannelRealHotTask {
def process(clickLogWideDateStream : DataStream[ClickLogWide])= {
// 1字段转换 channelidvisited
val realHotDataStream: DataStream[ChannelRealHot] = clickLogWideDateStream.map{
clickLogWide: ClickLogWide =>
ChannelRealHot(clickLogWide.channelID, clickLogWide.count)
}
// 2分组
val keyedStream: KeyedStream[ChannelRealHot, String] = realHotDataStream.keyBy(_.channelid)
// 3时间窗口
val windowedStream: WindowedStream[ChannelRealHot, String, TimeWindow] = keyedStream.timeWindow(
Time.seconds(3))
// 4聚合
val reduceDataStream: DataStream[ChannelRealHot] = windowedStream.reduce{
(t1: ChannelRealHot, t2: ChannelRealHot) =>
ChannelRealHot(t1.channelid, t1.visited + t2.visited)
}
// 输出测试
reduceDataStream
// 5落地 HBase
reduceDataStream.addSink(new SinkFunction[ChannelRealHot] {
override def invoke(value: ChannelRealHot): Unit = {
// HBase 相关字段
val tableName = "channel"
val clfName = "info"
val channelIdColumn = "channelId"
val visitedColumn = "visited"
val rowkey = value.channelid
// 查询 HBase 并且获取相关记录
val visitedValue: String = HBaseUtil.getData(tableName, rowkey, clfName, visitedColumn)
// 创建总数的临时变量
var totalCount: Long = 0
if(StringUtils.isBlank(visitedValue)){
totalCount = value.visited
}
else {
totalCount = visitedValue.toLong + value.visited
}
// 保存数据
HBaseUtil.putMapData(tableName, rowkey, clfName, Map(
channelIdColumn -> value.channelid ,
visitedColumn -> totalCount.toString
))
}
})
}
}

@ -0,0 +1,165 @@
package com.henry.realprocess.task
import com.henry.realprocess.bean.{ClickLogWide, Message}
import com.henry.realprocess.util.HBaseUtil
import org.apache.commons.lang.StringUtils
import org.apache.commons.lang.time.FastDateFormat
import org.apache.flink.streaming.api.scala.DataStream
import org.apache.flink.api.scala._
/**
* @Author: Henry
* @Description: 预处理任务
* @Date: Create in 2019/10/27 14:31
**/
object PreprocessTask {
def process(watermarkDataStream:DataStream[Message])= {
/**
* 大括号{}用于代码块计算结果是代码最后一行
* 大括号{}用于拥有代码块的函数
* 大括号{}在只有一行代码时可以省略除了case语句Scala适用
* 小括号()在函数只有一个参数时可以省略Scala适用
* 几乎没有二者都省略的情况
*/
watermarkDataStream.map {
msg =>
// 转换时间
val yearMonth: String = FastDateFormat.getInstance("yyyyMM").format(msg.timeStamp)
val yearMonthDay: String = FastDateFormat.getInstance("yyyyMMdd").format(msg.timeStamp)
val yearMonthDayHour: String = FastDateFormat.getInstance("yyyyMMddHH").format(msg.timeStamp)
// 转换地区
val address = msg.clickLog.country + msg.clickLog.province + msg.clickLog.city
val isNewtuple = isNewProcess(msg)
ClickLogWide(
msg.clickLog.channelID,
msg.clickLog.categoryID,
msg.clickLog.produceID,
msg.clickLog.country,
msg.clickLog.province,
msg.clickLog.city,
msg.clickLog.network,
msg.clickLog.source,
msg.clickLog.browserType,
msg.clickLog.entryTime,
msg.clickLog.leaveTime,
msg.clickLog.userID,
msg.count,
msg.timeStamp,
address,
yearMonth,
yearMonthDay,
yearMonthDayHour,
isNewtuple._1,
isNewtuple._2,
isNewtuple._3,
isNewtuple._4
)
}
}
/**
* 判断用户是否为新用户
* @param msg
*/
private def isNewProcess(msg:Message)={
// 1定义4个变量初始化为0
var isNew = 0
var isHourNew = 0
var isDayNew = 0
var isMonthNew = 0
// 2从HBase中查询用户记录如果有记录再去判断其他时间如果没有记录则证明是新用户
val tableName = "user_history"
var clfName = "info"
var rowkey = msg.clickLog.userID + ":" + msg.clickLog.channelID
// - 用户IDuserID
var userIdColumn = "userid"
// - 频道IDchannelid
var channelidColumn = "channelid"
// - 最后访问时间时间戳lastVisitedTime
var lastVisitedTimeColumn = "lastVisitedTime"
var userId: String = HBaseUtil.getData(tableName, rowkey, clfName, userIdColumn)
var channelid: String = HBaseUtil.getData(tableName, rowkey, clfName, channelidColumn)
var lastVisitedTime: String = HBaseUtil.getData(tableName, rowkey, clfName, lastVisitedTimeColumn)
// 如果 userid 为空则该用户一定是新用户
if(StringUtils.isBlank(userId)){
isNew = 1
isHourNew = 1
isDayNew = 1
isMonthNew = 1
// 保存用户的访问记录到 "user_history"
HBaseUtil.putMapData(tableName, rowkey, clfName, Map(
userIdColumn -> msg.clickLog.userID ,
channelidColumn -> msg.clickLog.channelID ,
lastVisitedTimeColumn -> msg.timeStamp
))
}
else{
isNew = 0
// 其它字段需要进行时间戳的比对
isHourNew = compareDate(msg.timeStamp, lastVisitedTimeColumn.toLong, "yyyyMMddHH")
isDayNew = compareDate(msg.timeStamp, lastVisitedTimeColumn.toLong, "yyyyMMdd")
isMonthNew = compareDate(msg.timeStamp, lastVisitedTimeColumn.toLong, "yyyyMM")
// 更新 "user_history" 用户的时间戳
HBaseUtil.putData(tableName, rowkey, clfName, lastVisitedTimeColumn , msg.timeStamp.toString)
}
(isDayNew, isHourNew, isDayNew, isMonthNew)
}
/**
* 比对时间 201912 > 201911
* @param currentTime 当前时间
* @param historyTime 历史时间
* @param format 时间格式 yyyyMM yyyyMMdd
* @return 1 或者 0
*/
def compareDate(currentTime:Long, historyTime:Long, format:String):Int={
val currentTimeStr:String = timestamp2Str(currentTime, format)
val historyTimeStr:String = timestamp2Str(historyTime, format)
// 比对字符串大小如果当前时间 > 历史时间返回1
var result:Int = currentTimeStr.compareTo(historyTimeStr)
if(result > 0){
result = 1
}
else {
result = 0
}
result
}
/**
* 转换日期
* @param timestamp Long 类型时间戳
* @param format 日期格式
* @return
*/
def timestamp2Str(timestamp:Long, format:String):String={
FastDateFormat.getInstance("yyyyMM").format(timestamp)
}
}

@ -0,0 +1,33 @@
package com.henry.realprocess.util
import com.typesafe.config.{Config, ConfigFactory}
/**
* @Author: Henry
* @Description: 配置文件加载类
* @Date: Create in 2019/10/15 23:42
**/
object GlobalConfigutil {
// 通过工厂加载配置, config 会自动加载 application.conf 文件文件名不能变
val config:Config = ConfigFactory.load()
val bootstrapServers = config.getString("bootstrap.servers")
val zookeeperConnect = config.getString("zookeeper.connect")
val inputTopic = config.getString("input.topic")
val gruopId = config.getString("gruop.id")
val enableAutoCommit = config.getString("enable.auto.commit")
val autoCommitIntervalMs = config.getString("auto.commit.interval.ms")
val autoOffsetReset = config.getString("auto.offset.reset")
def main(args: Array[String]): Unit = {
// 选择快捷键alt鼠标左键拉倒最后一行然后按 ctrl+shift 再按
println(bootstrapServers)
println(zookeeperConnect)
println(inputTopic)
println(gruopId)
println(enableAutoCommit)
println(autoCommitIntervalMs)
println(autoOffsetReset)
}
}

@ -0,0 +1,274 @@
package com.henry.realprocess.util
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.hbase.{HBaseConfiguration, TableName}
import org.apache.hadoop.hbase.client.{ColumnFamilyDescriptor, _}
import org.apache.hadoop.hbase.util.Bytes
/**
* @Author: Henry
* @Description: HBase 工具类
* 1获取Table对象
* 2保存单列数据
* 3查询单列数据
* 4保存多列数据
* 5查询多列数据
* 6删除数据
* @Date: Create in 2019/10/21 22:53
**/
object HBaseUtil {
// HBase 配置类不需要指定配置文件名文件名要求是 hbase-site.xml
val conf:Configuration = HBaseConfiguration.create()
// HBase 的连接
val conn:Connection = ConnectionFactory.createConnection(conf)
// HBase 的操作 API
val admin:Admin = conn.getAdmin
/**
* 返回Table如果不存在则创建表
*
* @param tableName 表名
* @param columnFamilyName 列族名
* @return
*/
def getTable(tableNameStr:String, columnFamilyName:String):Table={
// 获取 TableName
val tableName:TableName = TableName.valueOf(tableNameStr)
// 如果表不存在则创建表
if(!admin.tableExists(tableName)){
// 构建出表的描述的建造者
val descBuilder: TableDescriptorBuilder = TableDescriptorBuilder.newBuilder(tableName)
val familyDescriptor:ColumnFamilyDescriptor = ColumnFamilyDescriptorBuilder
.newBuilder(columnFamilyName.getBytes).build()
// 给表添加列族
descBuilder.setColumnFamily(familyDescriptor)
// 创建表
admin.createTable(descBuilder.build())
}
conn.getTable(tableName)
}
/**
* 存储单列数据
*
* @param tableNameStr 表名
* @param rowkey 主键
* @param columnFamilyName 列族名
* @param columnName 列名
* @param columnValue 列值
*/
def putData(tableNameStr:String, rowkey:String, columnFamilyName:String, columnName:String, columnValue:String)={
// 获取表
val table:Table = getTable(tableNameStr, columnFamilyName)
try{
// Put
val put:Put = new Put(rowkey.getBytes)
put.addColumn(columnFamilyName.getBytes, columnName.getBytes, columnValue.getBytes)
// 保存数据
table.put(put)
}catch {
case ex:Exception=>{
ex.printStackTrace()
}
}finally {
table.close()
}
}
/**
* 通过单列名获取列值
* @param tableNameStr 表名
* @param rowkey 主键
* @param columnFamilyName 列族名
* @param columnName 列名
* @param columnValue 列值
* @return
*/
def getData(tableNameStr:String, rowkey:String, columnFamilyName:String, columnName:String):String={
// 1. 获取 Table 对象
val table = getTable(tableNameStr, columnFamilyName)
try {
// 2. 构建 get 对象
val get = new Get(rowkey.getBytes)
// 3. 进行查询
val result:Result = table.get(get)
// 4. 判断查询结果是否为空并且包含要查询的列
if (result != null && result.containsColumn(columnFamilyName.getBytes, columnName.getBytes)){
val bytes: Array[Byte] = result.getValue(columnFamilyName.getBytes(), columnName.getBytes)
Bytes.toString(bytes)
}else{
""
}
}catch{
case ex:Exception => {
ex.printStackTrace()
""
}
}finally {
// 5关闭表
table.close()
}
}
/**
* 存储多列数据
* @param tableNameStr 表名
* @param rowkey 主键
* @param columnFamilyName 列族名
* @param map 多个列名和列族集合
*/
def putMapData(tableNameStr:String, rowkey:String, columnFamilyName:String, map:Map[String,Any])={
// 1获取 table 对象
val table = getTable(tableNameStr, columnFamilyName)
try{
// 2创建 put
val put = new Put(rowkey.getBytes)
// 3 put 中添加多个列名和列值
for ((colName, colValue) <- map){
put.addColumn(columnFamilyName.getBytes, colName.getBytes, colValue.toString.getBytes)
}
// 4保存 put
table.put(put)
}catch{
case ex:Exception => {
ex.printStackTrace()
}
}finally {
// 5关闭表
table.close()
}
// 5关闭 table
table.close()
}
/**
* 获取多了数据的值
* @param tableNameStr 表名
* @param rowkey 主键
* @param columnFamilyName 列族名
* @param columnNameList 多个列名和列值集合
* @return
*/
def getMapData(tableNameStr:String, rowkey:String, columnFamilyName:String, columnNameList:List[String]):Map[String,String]= {
// 1获取 Table
val table = getTable(tableNameStr, columnFamilyName)
try{
// 2构建 get
val get = new Get(rowkey.getBytes)
// 3执行查询
val result: Result = table.get(get)
// 4遍历列名集合取出列值构建成 Map 返回
columnNameList.map {
col =>
val bytes: Array[Byte] = result.getValue(columnFamilyName.getBytes(), col.getBytes)
if (bytes != null && bytes.size > 0) {
col -> Bytes.toString(bytes)
}
else { // 如果取不到值则赋一个空串
"" -> ""
}
}.filter(_._1 != "").toMap // 把不是空串的过滤出来再转换成 Map
}catch {
case ex:Exception => {
ex.printStackTrace()
Map[String, String]() // 返回一个空的 Map
}
}finally {
// 5关闭 Table
table.close()
}
}
/**
* 删除数据
* @param tableNameStr 表名
* @param rowkey 主键
* @param columnFamilyName 列族名
*/
def delete(tableNameStr:String, rowkey:String, columnFamilyName:String)={
// 1获取 Table
val table:Table = getTable(tableNameStr, columnFamilyName)
try {
// 2构建 delete 对象
val delete: Delete = new Delete(rowkey.getBytes)
// 3执行删除
table.delete(delete)
}
catch {
case ex:Exception =>
ex.printStackTrace()
}
finally {
// 4关闭 table
table.close()
}
}
def main(args: Array[String]): Unit = {
// println(getTable("test","info"))
// putData("test", "1", "info", "t1", "hello world")
// println(getData("test", "1", "info", "t1"))
// val map = Map(
// "t2" -> "scala" ,
// "t3" -> "hive" ,
// "t4" -> "flink"
// )
// putMapData("test", "1", "info", map)
// println(getMapData("test", "1", "info", List("t1", "t2")))
delete("test", "1", "info")
println(getMapData("test", "1", "info", List("t1", "t2")))
}
}

@ -0,0 +1,7 @@
val bootstrap.servers = config.getString("bootstrap.servers")
val zookeeper.connect = config.getString("zookeeper.connect")
val input.topic = config.getString("input.topic")
val gruop.id = config.getString("gruop.id")
val enable.auto.commit = config.getString("enable.auto.commit")
val auto.commit.interval.ms = config.getString("auto.commit.interval.ms")
val auto.offset.reset = config.getString("auto.offset.reset")

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.henry</groupId>
<artifactId>report</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>report</name>
<description>Spring Boot上报服务</description>
<!--Spring Boot 起步依赖-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.13.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.M3</spring-cloud.version>
</properties>
<!-- 阿里云 maven-->
<repositories>
<repository>
<id>alimaven</id>
<name>alimaven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>1.5.13.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>1.5.13.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>1.5.13.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.35</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>1.0.6.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,18 @@
package com.henry.report;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.SpringApplication;
/**
* @Author: HongZhen
* @Description:
* @Date: Create in 2019/9/20 11:10
**/
// 添加注解 @SpringBootApplication ,表示该类是一个启动类
@SpringBootApplication
public class ReportApplication {
public static void main(String[] args) {
SpringApplication.run(ReportApplication.class, args);
}
}

@ -0,0 +1,136 @@
package com.henry.report.bean;
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/10/13 19:33
**/
public class Clicklog {
// 频道 ID
private long channelID;
// 产品的类别 ID
private long categoryID ;
// 产品 ID
private long produceID ;
// 用户 ID
private long userID ;
// 国家
private String country;
// 省份
private String province;
// 城市
private String city;
// 网络方式
private String network;
// 来源方式
private String source;
// 浏览器类型
private String browserType;
// 进入网站时间
private Long entryTime ;
// 离开网站实际
private long leaveTime;
public long getChannelID() {
return channelID;
}
public void setChannelID(long channelID) {
this.channelID = channelID;
}
public long getCategoryID() {
return categoryID;
}
public void setCategoryID(long categoryID) {
this.categoryID = categoryID;
}
public long getProduceID() {
return produceID;
}
public void setProduceID(long produceID) {
this.produceID = produceID;
}
public long getUserID() {
return userID;
}
public void setUserID(long userID) {
this.userID = userID;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getProvince() {
return province;
}
public void setProvince(String province) {
this.province = province;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getNetwork() {
return network;
}
public void setNetwork(String network) {
this.network = network;
}
public String getSource() {
return source;
}
public void setSource(String source) {
this.source = source;
}
public String getBrowserType() {
return browserType;
}
public void setBrowserType(String browserType) {
this.browserType = browserType;
}
public Long getEntryTime() {
return entryTime;
}
public void setEntryTime(Long entryTime) {
this.entryTime = entryTime;
}
public long getLeaveTime() {
return leaveTime;
}
public void setLeaveTime(long leaveTime) {
this.leaveTime = leaveTime;
}
}

@ -0,0 +1,51 @@
package com.henry.report.bean;
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/10/11 23:40
**/
public class Message {
// 消息次数
private int count;
// 消息的时间戳
private long timestamp;
// 消息体
private String message;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public long getTimestamp() {
return timestamp;
}
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public String toString() {
return "Message{" +
"count=" + count +
", timestamp=" + timestamp +
", message='" + message + '\'' +
'}';
}
}

@ -0,0 +1,53 @@
package com.henry.report.controller;
import com.alibaba.fastjson.JSON;
import com.henry.report.bean.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/10/11 23:43
**/
// 表示这是一个 Controller并且其中所有的方法都是带有 @ResponseBody 的注解
@RestController
public class ReportController {
@Autowired
KafkaTemplate kafkaTemplate;
@RequestMapping("/receive")
public Map<String, String> receive(@RequestBody String json) {
Map<String, String> map = new HashMap(); // 记录是否发送成功
try {
// 构建 Message
Message msg = new Message();
msg.setMessage(json);
msg.setCount(1);
msg.setTimestamp(System.currentTimeMillis());
String msgJSON = JSON.toJSONString(msg);
// 发送 Message 到 Kafka
kafkaTemplate.send("pyg", msgJSON);
map.put("success", "ture");
}catch (Exception ex){
ex.printStackTrace();
map.put("success", "false");
}
return map;
}
}

@ -0,0 +1,22 @@
package com.henry.report.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: HongZhen
* @Description: Spring Boot
* @Date: Create in 2019/9/20 11:19
**/
// 表示这是一个 Controller并且其中所有的方法都是带有 @ResponseBody 的注解
@RestController
public class TestController{
// 为了能访问到该方法,需要添加如下注解,参数是代表如何来请求
@RequestMapping("/test")
public String test(String json){
System.out.println(json);
return json;
}
}

@ -0,0 +1,139 @@
package com.henry.report.util;
import com.alibaba.fastjson.JSONObject;
import com.henry.report.bean.Clicklog;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/10/13 20:00
**/
public class ClickLogGenerator {
// ID 信息
private static Long[] channelID = new Long[]{1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L};
private static Long[] categoryID = new Long[]{1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L};
private static Long[] produceID = new Long[]{1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L};
private static Long[] userID = new Long[]{1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L, 11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L};
// 地区
private static String[] contrys = new String[]{"china"}; // 地区-国家集合
private static String[] provinces = new String[]{"HeNan", "HeBeijing"}; // 地区-省集合
private static String[] citys = new String[]{"ShiJiaZhuang", "ZhengZhou", "LuoyYang"}; // 地区-市集合
// 网络方式
private static String[] networks = new String[]{"电信", "移动", "联通"};
// 来源方式
private static String[] sources = new String[]{"直接输入", "百度跳转", "360搜索跳转", "必应跳转"};
// 浏览器
private static String[] browser = new String[]{"火狐", "QQ浏览器", "360浏览器", "谷歌浏览器"};
// 打开方式,离开时间
private static List<Long[]> usertimeLog = producetimes();
// 获取时间
private static List<Long[]> producetimes() {
List<Long[]> usertimelog = new ArrayList<>();
for (int i = 0; i < 100; i++) {
Long[] timearray = gettimes("2019-10-10 24:60:60:000");
usertimelog.add(timearray);
}
return usertimelog;
}
private static Long[] gettimes(String time) {
DateFormat dataFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss:SSS");
try {
Date date = dataFormat.parse(time);
long timetemp = date.getTime();
Random random = new Random();
int randomint = random.nextInt(10);
long starttime = timetemp - randomint*3600*1000;
long endtime = starttime + randomint*3600*1000;
return new Long[]{starttime,endtime};
}catch (ParseException e){
e.printStackTrace();
}
return new Long[]{0L, 0L};
}
// 模拟发送 Http 请求到上报服务系统
public static void send(String url, String json){
try {
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
HttpPost post = new HttpPost(url);
JSONObject response = null ;
try {
StringEntity s = new StringEntity(json.toString(), "utf-8");
s.setContentEncoding("utf-8");
// 发送 json 数据需要设置 contentType
s.setContentType("application/json");
post.setEntity(s);
HttpResponse res = httpClient.execute(post);
if(res.getStatusLine().getStatusCode() == HttpStatus.SC_OK){
// 返回 json 格式
String result = EntityUtils.toString(res.getEntity());
System.out.println(result);
}
}catch (Exception e){
throw new RuntimeException();
}
}catch (Exception e){
e.printStackTrace();
}
}
public static void main(String[] args) {
Random random = new Random();
for (int i = 0; i < 100; i++) {
// 频道id、类别id、产品id、用户id、打开时间、离开时间、地区、网络方式、来源方式、浏览器
Clicklog clicklog = new Clicklog();
clicklog.setChannelID(channelID[random.nextInt(channelID.length)]);
clicklog.setCategoryID(categoryID[random.nextInt(channelID.length)]);
clicklog.setProduceID(produceID[random.nextInt(produceID.length)]);
clicklog.setUserID(userID[random.nextInt(userID.length)]);
clicklog.setCountry(contrys[random.nextInt(contrys.length)]);
clicklog.setProvince(provinces[random.nextInt(provinces.length)]);
clicklog.setCity(citys[random.nextInt(citys.length)]);
clicklog.setNetwork(networks[random.nextInt(networks.length)]);
clicklog.setSource(sources[random.nextInt(sources.length)]);
clicklog.setBrowserType(browser[random.nextInt(browser.length)]);
Long[] times = usertimeLog.get(random.nextInt(usertimeLog.size()));
clicklog.setEntryTime(times[0]);
clicklog.setLeaveTime(times[1]);
// 将点击流日志转成字符串,发送到前端地址
String jsonstr = JSONObject.toJSONString(clicklog);
System.out.println(jsonstr);
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
send("http://localhost:1234/receive", jsonstr);
}
}
}

@ -0,0 +1,98 @@
package com.henry.report.util;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: Henry
* @Description: KafkaProducerConfig
* @Date: Create in 2019/10/6 21:56
**/
@Configuration // 1、表示该类是一个配置类这样在下面才能创建 Bean
public class KafkaProducerConfig {
// 通过@value注解将配置文件中kafka.bootstrap_servers_config的值赋值给成员变量
@Value("${kafka.bootstrap_servers_config}")
private String bootstrap_servers_config;
// 如果出现发送失败的情况,允许重试的次数
@Value("${kafka.retries_config}")
private String retries_config;
// 每个批次发送多大的数据,单位:字节
@Value("${kafka.batch_size_config}")
private String batch_size_config;
// 定时发送,达到 1ms 发送
@Value("${kafka.linger_ms_config}")
private String linger_ms_config;
// 缓存的大小,单位:字节
@Value("${kafka.buffer_memory_config}")
private String buffer_memory_config;
// TOPOC 名字
@Value("${kafka.topic}")
private String topic;
@Bean // 2、表示该对象是受 Spring 管理的一个 Bean
public KafkaTemplate kafkaTemplate() {
// 构建工程需要的配置
Map<String, Object> configs = new HashMap<>();
// 3、设置相应的配置
// 将成员变量的值设置到Map中在创建kafka_producer中用到
configs.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrap_servers_config);
configs.put(ProducerConfig.RETRIES_CONFIG, retries_config);
configs.put(ProducerConfig.BATCH_SIZE_CONFIG, batch_size_config);
configs.put(ProducerConfig.LINGER_MS_CONFIG, linger_ms_config);
configs.put(ProducerConfig.BUFFER_MEMORY_CONFIG, buffer_memory_config);
// 设置 key、value 的序列化器
configs.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG , StringSerializer.class);
configs.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG , StringSerializer.class);
// 指定自定义分区
configs.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, RoundRobinPartitioner.class);
// 4、创建生产者工厂
ProducerFactory<String, String> producerFactory = new DefaultKafkaProducerFactory(configs);
// 5、再把工厂传递给Template构造方法
// 表示需要返回一个 kafkaTemplate 对象
return new KafkaTemplate(producerFactory);
}
}

@ -0,0 +1,46 @@
package com.henry.report.util;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Author: Henry
* @Description:
* @Date: Create in 2019/10/9 23:00
**/
public class RoundRobinPartitioner implements Partitioner {
// AtomicInteger 并发包下的多线程安全的整型类
AtomicInteger counter = new AtomicInteger(0) ;
// 返回值为分区号: 0、1、2
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
// 获取分区的数量
Integer partitions = cluster.partitionCountForTopic(topic) ;
int curpartition = counter.incrementAndGet() % partitions ; // 当前轮询的 partition 号
if(counter.get() > 65535){
counter.set(0);
}
return curpartition;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}

@ -0,0 +1,18 @@
# 修改 Tomcat 端口号
server.port=1234
#
# Kafka
#
# kafka的服务器地址
kafka.bootstrap_servers_config=master:9092,slave1:9092,slave2:9092
# 如果出现发送失败的情况,允许重试的次数
kafka.retries_config=0
# 每个批次发送多大的数据,单位:字节
kafka.batch_size_config=4096
# 定时发送,达到 1ms 发送
kafka.linger_ms_config=1
# 缓存的大小,单位:字节
kafka.buffer_memory_config=40960
# TOPOC 名字
kafka.topic=pyg

@ -0,0 +1,29 @@
package com.henry.report;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.test.context.junit4.SpringRunner;
/**
* @Author: Henry
* @Description: Kafka
* @Date: Create in 2019/10/8 23:26
**/
@RunWith(SpringRunner.class)
@SpringBootTest
public class KafkaTest {
@Autowired
KafkaTemplate kafkaTemplate;
@Test
public void sendMsg(){
for (int i = 0; i < 100; i++)
kafkaTemplate.send("test", "key","this is test msg") ;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save