一.总体需求
1.题目背景
某股票交易机构已上线一个在线交易平台,平台注册用户量近千万,每日均 接受来自全国各地的分支机构用户提交的交易请求。鉴于公司发展及平台管理要 求,拟委托开发一个在线实时大数据系统,可实时观测股票交易大数据信息,展 示部分重要业绩数据。
2.拟完成的功能需求
运用实时计算技术,采用不同的数据接入、实时计算方法构建一个股票实时 交易的大数据看板,实现以下功能:
(1) 可采用成熟的数据看板开源组件(要求有使用许可,如阿里的 DataV 平台),或者自主开发本地展示平台;界面要求每秒刷新一次;
(2) 界面应美观大方、简洁的信息;
(3) 展示的信息应至少包含以下内容:
a) 订单的已处理速度,单位为“条/秒”;
b) 近 1 分钟与当天累计的总交易金额、交易数量;
c) 近 1 分钟与当天累计的买入、卖出交易量;
d) 近 1 分钟与当天累计的交易金额排名前 10 的股票信息;
e) 近 1 分钟与当天累计的交易量排名前 10 的交易平台;
f) 展示全国各地下单客户的累计数量(按省份),在地图上直观展示;
g) 展示不同股票类型的交易量分布情况;
h) [可选]对单支股票的交易量爆发式增长进行预警
(4) 数据统计误差(数据丢失、统计错误)不超过 1%,应设计实验计算 数据误差率;
(5) 展示的数据延迟应不超过 30 秒,每次刷新时应显示获取的数据最新 时间;
(6) 测试出系统的最大承载负荷量,即你搭建系统每秒最多能处理的订单 数量;
(7) 特色功能,根据业务场景及展示需要增加的特色功能,可加 1-5 分;
3.框架
必须使用课程上所学习的实时计算框架(如 Storm、Kafka、Flume、Flink) 的一种或几种,如有需要,可引入其它框架或组件。 至少实现两种方案的对比,有要实验数据的支撑(实验数据应在报告中以表 格或图表的形式呈现) 要求使用 Docker(不少于 3 台虚拟机,分别模拟主-从端)的环境,将开发 的程序包发布到真实搭建的平台中运行,接收数据并计算相应的结果进行展示。 报告中要给出监控平台的运行情况。未使用真实环境运行程序的,将按照不及格 处理。
4.数据源
为提供更真实的测试环境,公司的技术部门委托相关人员已设计了一个股票 交易数据模拟器,可模拟产生客户在平台中下单的信息,数据会自动存入指定文 件夹中的文本文件。 该模拟器允许调节进程的数量,模拟不同量级的并发量,以充分测试系统的 性能。数据的具体字段说明详见下表:
数据字段说明:
序号 | 字段名 | 中文含义 | 备注 |
---|---|---|---|
1 | stock_name | 股票名 | |
2 | stock_code | 股票代码 | |
3 | time | 交易时间 | 时间戳 |
4 | trade_volume | 交易量 | 数值型 |
5 | trade_price | 交易单价 | 数值类型 |
6 | trade_type | 交易类型 | [买入,卖出] |
7 | trade_place | 交易地点 | 交易所在省份 |
8 | trade_platform | 交易平台 | 该交易发生的分支交易平台 |
9 | industry_type | 股票类型 | 该股票所在行业 |
二.方案设计
1.方案设计一
2.方案设计二
3.方案设计三
4.三个方案分析
方案一是一个最基础的storm框架设计,为了测试出storm订单处理速度的极限,该方案有加入了多个spout用来拉取信息,但后面测试发现远远达不到极限,于是引入了kafka框架,这就是方案二的框架设计,方案三的框架设计是为了做对比分析实验测试使用。总体方案为方案二。
三.单元与功能实现
1.数据集获取
使用软件生成的csv数据作为数据源,为了做压力测试,本实验利用kafka对同一个文件做不断的读取以便在短时间内获取大量数据。
2.数据库的设计
本实验使用阿里云的云数据库postgresql来进行数据存储,表的设计sql语句如下:
--订单处理速度
create table speed(
count int
);
--一分钟交易量和金额
create table a_1m(
count bigint,
mon numeric(30,3)
);
--累计交易量和金额
create table a_2m(
count bigint,
mon numeric(30,3)
);
--一分钟买入卖出
create table b_1m(
buy numeric(30,3),
sell numeric(30,3)
);
--一天累计买入卖出
create table b_2m(
buy numeric(30,3),
sell numeric(30,3)
);
--近 1 分钟交易金额排名前 10 的股票信息;
create table c_1m(
stock varchar(10),
trade_mon numeric(30,3)
);
--当天累计的交易金额排名前 10 的股票信息;
create table c_2m(
stock varchar(10),
trade_mon numeric(30,3)
);
--近 1 分钟交易量排名前 10 的交易平台;
create table d_1m(
platform varchar(80),
trade_v numeric(30,3)
);
--当天累计的交易量排名前 10 的交易平台;
create table d_2m(
platform varchar(80),
trade_v numeric(30,3)
);
--全国各地下单客户的累计数量(按省份)
create table e_1m(
province varchar(80),
count numeric(30)
);
--展示不同股票类型的交易量分布
create table f_1m(
stock varchar(80),
count numeric(30,3)
);
3.统计方法说明
统计名称 | 统计方法 | 详细信息 |
---|---|---|
订单的已处理速度 | V=count/(currentTime-initTime) | 订单已处理量/(当前时间-系统开始运行时间) |
累计的总交易金额、交易数量 | totalPrice+=price*tradeVolume; totalVolume+=tradeVolume | 总交易金额+=价格*交易量; 总交易量+=交易量 |
累计的交易金额排名前 10 的股票信息 | Map<stock,stock_Price> | 将股票信息和交易金额插入map对象,若map中存在该股票则直接加交易金额 |
累计的交易量排名前 10 的交易平台 | Map<platform, tradeVolume> | 类似股票信息统计方法 |
全国各地下单客户的累计数量 | Map<province,count_P> | 每处理一条订单,获取该订单地区,将<地区,1>存入map中,若 map中存在该地区,则count_P加1 |
不同股票类型的交易量分布 | Map<stock, tradeVolume> | 类似股票信息统计方法 |
单支股票的交易量爆发式增长进行预警(可选) | (maxVolume-minVolume)/avgVolume | 最高交易量的股票交易量减去最低交易量的股票交易量/所有股票的平均交易量,所得的比值超过某个阈值即可认为该股票爆发式增长。 |
统计说明:有些统计有要求不仅统计当天累计,还要统计近一分钟累计,这里简单介绍一下如何统计。
由于统计当前时间(精确到秒)为止的一分钟前的数据较为复杂,需要不断获取当前时间并获取60秒前的时间同时还要保存60秒前的数据(相当于每秒的所有统计数据都要保存起来),查询60秒前的数据较为浪费时间,会大大拖慢订单处理速度,这里介绍一种简易统计方法,设计一个计时器,每隔60秒记录一次当前累计交易,之后不断用累计增加的交易数等变量减去计时器保存的记录数来表示近一分钟的交易,起始计时器所有记录均为0.
Map对象中的数据类似,多设计一个一分钟map对像,每隔60秒记录清空该对象。
4.前端设计
本次实验前端使用了阿里云的datav可视化平台来进行前端数据的展示,因此没有代码可供参考,只是给出一个简单的流程供大家借鉴。关于datav的使用,这里不做详细介绍。
(1)选择一个美观的模板界面
(2)对模板进行更改和设计
(3)连接数据库进行数据展示
连接阿里云的postgreSQL数据库(本地的不行),设计sql数据进行前端展示,下面为一些示例:
--这里只写一些重点,大概的重复类型的不写
--近一分钟交易数与交易金额
SELECT * FROM a_1m;
--订单处理速度
SELECT * FROM speed;
--单股票交易预警
SELECT (MAX(count) - MIN(count))/AVG(count)*100 AS difference
FROM f_1m;
--单股票最高交易量
SELECT MAX(count) AS max_value
FROM f_1m;
--股票交易分布
SELECT * FROM f_1m;
--地区分布统计的扇形图
SELECT * FROM e_1m ORDER BY count DESC;
--地区分布统计轮播图
SELECT * FROM e_1m ORDER BY count DESC;
--近1min股票交易统计排行
SELECT * FROM c_1m ORDER BY trade_mon DESC;
--近1min平台交易量排行
SELECT * FROM d_1m;
注意:上面只是本实验个人的一个前端参考语句,可根据个人情况做修改。
5.后端设计与代码实现
(1)创建maven工程
(2)导入下面依赖
<dependency>
<groupId>org.apache.storm</groupId>
<artifactId>storm-core</artifactId>
<version>1.0.0</version> <!-- 使用你需要的Storm版本 -->
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.14</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.7.0</version>
</dependency>
(3)创建一个db.properties文件,用于连接数据库
代码如下:
url=jdbc:postgresql://pgm-bp1e0yjjjukknwxxwo.pg.rds.aliyuncs.com:5432/1test
user=lucky
password=1234567
注意上面的一些信息本人做了修改,不是本人的数据库,请根据自己数据库更改。
(4)创建数据库插入类,用于修改数据库数据
代码如下(类名:sql_con):
import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Map;
import java.util.Properties;
public class sql_con {
public Connection getconnection(){
String url = null;
String user = null;
String password = null;
Connection co=null;
// 读取数据库连接配置文件
try (FileInputStream file = new FileInputStream("db.properties")) {
Properties pros = new Properties();
pros.load(file);
url = pros.getProperty("url");
user = pros.getProperty("user");
password = pros.getProperty("password");
} catch (IOException e) {
System.out.println(e.getMessage());
}
// 建立数据库连接
try {
Connection conn = DriverManager.getConnection(url, user, password);
co=conn;
System.out.println("连接 PostgreSQL 数据库成功!");
} catch (SQLException e) {
System.out.println(e.getMessage());
}
return co;
}
//插入数据,用来测试
public void insert_one(Connection conn,String value1,Double value2){
try{
Connection c = conn;
String sql="INSERT INTO test3 VALUES (?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setString(1,value1);
stmt.setDouble(2,value2);
stmt.addBatch();
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
//插入订单处理速度
public void insert_speed(Connection conn,double count){
try{
Connection c = conn;
//清空表格
Statement statement = c.createStatement();
String sql1 = "DELETE FROM speed";
statement.executeUpdate(sql1);
String sql="INSERT INTO speed VALUES (?)";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setDouble(1, count);
stmt.addBatch();
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
//一分钟交易量和金额
public void insert_a_1m(Connection conn,double count,double mon){
try{
Connection c = conn;
//清空表格
Statement statement = c.createStatement();
String sql1 = "DELETE FROM a_1m";
statement.executeUpdate(sql1);
String sql="INSERT INTO a_1m VALUES (?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setDouble(1, count);
stmt.setDouble(2, mon);
stmt.addBatch();
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
public void insert_a_2m(Connection conn,double count,double mon){
try{
Connection c = conn;
//清空表格
Statement statement = c.createStatement();
String sql1 = "DELETE FROM a_2m";
statement.executeUpdate(sql1);
//插入
String sql="INSERT INTO a_2m VALUES (?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setDouble(1, count);
stmt.setDouble(2, mon);
stmt.addBatch();
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
//一分钟与一天买入卖出
public void insert_b_1m(Connection conn,double buy,double sell){
try{
Connection c = conn;
//清空表格
Statement statement = c.createStatement();
String sql1 = "DELETE FROM b_1m";
statement.executeUpdate(sql1);
String sql="INSERT INTO b_1m VALUES (?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setDouble(1, buy);
stmt.setDouble(2, sell);
stmt.addBatch();
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
public void insert_b_2m(Connection conn,double buy,double sell){
try{
Connection c = conn;
//清空表格
Statement statement = c.createStatement();
String sql1 = "DELETE FROM b_2m";
statement.executeUpdate(sql1);
String sql="INSERT INTO b_2m VALUES (?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setDouble(1, buy);
stmt.setDouble(2, sell);
stmt.addBatch();
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
//近 1 分钟与当天累计的交易金额排名前 10 的股票信息;
public void insert_c_1m(Connection conn,Map<String, Double> stockTypeVolume){
try{
Connection c = conn;
//清空表格
Statement statement = c.createStatement();
String sql1 = "DELETE FROM c_1m";
statement.executeUpdate(sql1);
//插入数据
String sql="INSERT INTO c_1m VALUES (?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
for (Map.Entry<String, Double> entry : stockTypeVolume.entrySet()) {
String stock = entry.getKey();
double mon = entry.getValue();
//插入数据
stmt.setString(1, stock);
stmt.setDouble(2, mon);
stmt.addBatch();
}
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
public void insert_c_2m(Connection conn,Map<String, Double> stockTypeVolume){
try{
Connection c = conn;
//清空表格
Statement statement = c.createStatement();
String sql1 = "DELETE FROM c_2m";
statement.executeUpdate(sql1);
//插入数据
String sql="INSERT INTO c_2m VALUES (?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
for (Map.Entry<String, Double> entry : stockTypeVolume.entrySet()) {
String stock = entry.getKey();
double mon = entry.getValue();
//插入数据
stmt.setString(1, stock);
stmt.setDouble(2, mon);
stmt.addBatch();
}
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
//近 1 分钟与当天累计的交易量排名前 10 的交易平台;
public void insert_d_1m(Connection conn,Map<String, Double> platTypeVolume){
try{
Connection c = conn;
//清空表格
Statement statement = c.createStatement();
String sql1 = "DELETE FROM d_1m";
statement.executeUpdate(sql1);
//插入数据
String sql="INSERT INTO d_1m VALUES (?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
for (Map.Entry<String, Double> entry : platTypeVolume.entrySet()) {
String platform = entry.getKey();
double volume = entry.getValue();
//插入数据
stmt.setString(1, platform);
stmt.setDouble(2, volume);
stmt.addBatch();
}
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
public void insert_d_2m(Connection conn,Map<String, Double> platTypeVolume){
try{
Connection c = conn;
//清空表格
Statement statement = c.createStatement();
String sql1 = "DELETE FROM d_2m";
statement.executeUpdate(sql1);
//插入数据
String sql="INSERT INTO d_2m VALUES (?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
for (Map.Entry<String, Double> entry : platTypeVolume.entrySet()) {
String platform = entry.getKey();
double volume = entry.getValue();
//插入数据
stmt.setString(1, platform);
stmt.setDouble(2, volume);
stmt.addBatch();
}
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
//各省份客户量
public void insert_e_1m(Connection conn,Map<String, Double> provinceVolume){
try{
Connection c = conn;
//清空表格
Statement statement = c.createStatement();
String sql1 = "DELETE FROM e_1m";
statement.executeUpdate(sql1);
//插入数据
String sql="INSERT INTO e_1m VALUES (?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
for (Map.Entry<String, Double> entry : provinceVolume.entrySet()) {
String province = entry.getKey();
double volume = entry.getValue();
//插入数据
stmt.setString(1, province);
stmt.setDouble(2, volume);
stmt.addBatch();
}
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
//股票交易分布
public void insert_f_1m(Connection conn,Map<String, Double> stockTypeVolume2){
try{
Connection c = conn;
//清空表格
Statement statement = c.createStatement();
String sql1 = "DELETE FROM f_1m";
statement.executeUpdate(sql1);
//插入数据
String sql="INSERT INTO f_1m VALUES (?,?)";
PreparedStatement stmt = conn.prepareStatement(sql);
for (Map.Entry<String, Double> entry : stockTypeVolume2.entrySet()) {
String stock = entry.getKey();
double volume = entry.getValue();
//插入数据
stmt.setString(1, stock);
stmt.setDouble(2, volume);
stmt.addBatch();
}
stmt.executeBatch();
System.out.println("插入数据成功!");
}catch(SQLException e){
System.out.println(e.getMessage());
}
}
}
(5)方案一框架设计代码实现
1.创建一个spout类,代码如下(类名:spout1):
import org.apache.storm.spout.SpoutOutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseRichSpout;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Values;
import org.apache.storm.utils.Utils;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.util.Map;
public class spout1 extends BaseRichSpout {
private SpoutOutputCollector collector;
private String[] files = {"H:\\a-storm测试\\股票数据1.csv","H:\\a-storm测试\\股票数据2.csv",
"H:\\a-storm测试\\股票数据3.csv"}; // 你的CSV文件路径
@Override
public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
this.collector = collector;
}
@Override
public void nextTuple() {
String file;
for(int i=0;i< files.length;i++){
file=files[i];
try (BufferedReader br = new BufferedReader(
new InputStreamReader(
new FileInputStream(file),
"GBK"))){
br.readLine();// 跳过第一行
String line;
while (true) {
if((line = br.readLine()) != null)
{collector.emit(new Values(line));Utils.sleep(1);}
else{
//Utils.sleep(1000);
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
String line=" ";
collector.emit(new Values(line));Utils.sleep(1);
Utils.sleep(1000);
//System.exit(0);
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("line"));
}
}
2.创建一个bolt类,代码如下(类名:StockStatisticsBolt):
import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseRichBolt;
import org.apache.storm.tuple.Tuple;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
public class StockStatisticsBolt extends BaseRichBolt {
int count=0;
double count_all=0;
double price_all=0;
double trade_all=0;
double price_time=0;
double trade_time=0;
double trade_buy=0;
double trade_sell=0;
double trade_buy1=0;
double trade_sell1=0;
private Map<String, Double> stockTypeVolume = new HashMap<>();
private Map<String, Double> stockTypeVolume1 = new HashMap<>();
private Map<String, Double> stockTypeVolume2 = new HashMap<>();
private Map<String, Double> platTypeVolume = new HashMap<>();
private Map<String, Double> platTypeVolume1 = new HashMap<>();
private Map<String, Double> provinceVolume = new HashMap<>();
public sql_con con=null;
Connection conn=null;
long startTime=0;
long startTime1=0;
long startTime2=0;
double linesPerSecondActual=0;
@Override
public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {
con=new sql_con();
conn=con.getconnection();
startTime = System.currentTimeMillis();
startTime1 = System.currentTimeMillis();
startTime2 = System.currentTimeMillis();
}
@Override
public void execute(Tuple tuple) {
String line = tuple.getStringByField("line");
if (line.equals(" ")){
con.insert_speed(conn,linesPerSecondActual);
con.insert_a_1m(conn,trade_all-trade_time,price_all-price_time);
con.insert_a_2m(conn,trade_all,price_all);
con.insert_b_1m(conn,trade_buy-trade_buy1,trade_sell-trade_sell1);
con.insert_b_2m(conn,trade_buy,trade_sell);
con.insert_c_1m(conn,stockTypeVolume1);
con.insert_c_2m(conn,stockTypeVolume);
con.insert_d_1m(conn,platTypeVolume1);
con.insert_d_2m(conn,platTypeVolume);
con.insert_e_1m(conn,provinceVolume);
con.insert_f_1m(conn,stockTypeVolume2);
System.out.println(count);
}
//这里修改一下代码,加一个异常处理,因为数据中有一点异常数据,会导致代码报错,
//具体参考下面的kafkabolt类,俩者是几乎一样的
try {
// 解析CSV数据
String[] parts = line.split(",");
String time = parts[0];
String stockCode = parts[1];
String stockName = parts[2];
double price = Double.parseDouble(parts[3]);
double tradeVolume = Double.parseDouble(parts[4]);
String trade_type = parts[5];
String trade_place = parts[6];
String trade_platform=parts[7];
String industry_type=parts[8];
//统计订单数
count++;
long endTime = System.currentTimeMillis();
long elapsedTime = endTime - startTime;
linesPerSecondActual = count / (elapsedTime / 1000.0);
//统计交易量和总金额
trade_all+=tradeVolume;
price_all+=price*tradeVolume;
//统计买入、卖出交易量
if(trade_type.equals("买入")){
trade_buy+=tradeVolume;
}else{
trade_sell+=tradeVolume;
}
//不同类型股票交易总金额
if (stockTypeVolume1.containsKey(stockName)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = stockTypeVolume1.get(stockName);
stockTypeVolume1.put(stockName, currentTradeVolume + tradeVolume*price);
} else {
// If the map does not contain the stock type, add it to the map
stockTypeVolume1.put(stockName, tradeVolume*price);
}
if (stockTypeVolume.containsKey(stockName)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = stockTypeVolume.get(stockName);
stockTypeVolume.put(stockName, currentTradeVolume + tradeVolume*price);
} else {
// If the map does not contain the stock type, add it to the map
stockTypeVolume.put(stockName, tradeVolume*price);
}
//不同类型交易平台交易量
if (platTypeVolume.containsKey(trade_platform)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = platTypeVolume.get(trade_platform);
platTypeVolume.put(trade_platform, currentTradeVolume + tradeVolume);
} else {
// If the map does not contain the stock type, add it to the map
platTypeVolume.put(trade_platform, tradeVolume);
}
if (platTypeVolume1.containsKey(trade_platform)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = platTypeVolume1.get(trade_platform);
platTypeVolume1.put(trade_platform, currentTradeVolume + tradeVolume);
} else {
// If the map does not contain the stock type, add it to the map
platTypeVolume1.put(trade_platform, tradeVolume);
}
//统计全国各地下单客户的累计数量(按省份)
if (provinceVolume.containsKey(trade_place)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = provinceVolume.get(trade_place);
provinceVolume.put(trade_place, currentTradeVolume + 1);
} else {
// If the map does not contain the stock type, add it to the map
double x=1;
provinceVolume.put(trade_place, x);
}
//不同类型股票交易量
if (stockTypeVolume2.containsKey(stockName)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = stockTypeVolume2.get(stockName);
stockTypeVolume2.put(stockName, currentTradeVolume + tradeVolume);
} else {
// If the map does not contain the stock type, add it to the map
stockTypeVolume2.put(stockName, tradeVolume);
}
endTime = System.currentTimeMillis();
elapsedTime = endTime - startTime1;
long elapsedTime1=endTime - startTime2;
//System.out.println(count);
//把数据插入数据库
if(elapsedTime/1000>=1){//给2秒操作时间,统计一分钟交易量和金额
startTime1=endTime;
//插入数据库
con.insert_speed(conn,linesPerSecondActual);
con.insert_a_1m(conn,trade_all-trade_time,price_all-price_time);
con.insert_a_2m(conn,trade_all,price_all);
con.insert_b_1m(conn,trade_buy-trade_buy1,trade_sell-trade_sell1);
con.insert_b_2m(conn,trade_buy,trade_sell);
con.insert_c_1m(conn,stockTypeVolume1);
con.insert_c_2m(conn,stockTypeVolume);
con.insert_d_1m(conn,platTypeVolume1);
con.insert_d_2m(conn,platTypeVolume);
con.insert_e_1m(conn,provinceVolume);
con.insert_f_1m(conn,stockTypeVolume2);
}
if(elapsedTime1/1000>=59){
startTime2=endTime;
trade_time=trade_all;
price_time=price_all;
trade_buy1=trade_buy;
trade_sell1=trade_sell;
stockTypeVolume1.clear();
platTypeVolume1.clear();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// 不需要发射数据到下一个Bolt,因此这个方法可以为空
}
}
3.创建一个topology类,代码如下(类名 :StockAnalysisTopology):
import org.apache.storm.Config;
import org.apache.storm.LocalCluster;
import org.apache.storm.StormSubmitter;
import org.apache.storm.topology.TopologyBuilder;
import org.apache.storm.utils.Utils;
public class StockAnalysisTopology {
public static void main(String[] args) {
try {
TopologyBuilder builder = new TopologyBuilder();
//builder.setSpout("1spout", new spout1());
//builder.setSpout("2spout", new spout2());
//builder.setSpout("3spout", new spout3());
//builder.setSpout("4spout", new spout4());
//builder.setSpout("5spout", new spout5());
builder.setSpout("2spout", new spout1());
builder.setBolt("2bolt", new StockStatisticsBolt()).shuffleGrouping
("2spout");
Config config = new Config();
//config.setDebug(true);
if (args != null && args.length > 0) {
config.setNumWorkers(1); // 设置工作进程数量
StormSubmitter.submitTopology(args[0], config, builder.createTopology());
} else {
LocalCluster cluster = null;
try {
config.setNumWorkers(4);
cluster = new LocalCluster();
cluster.submitTopology("1topology", config, builder.createTopology());
// 等待拓扑运行一段时间
Utils.sleep(600000);
}catch (Exception e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.创建多个spout
参考spout1的创建形式即可。
(6)方案二框架设计代码实现
执行下面操作前,需要搭建好kafka环境,这里不做详细介绍,具体可参考本人博客:http://t.csdnimg.cn/nADLk
1.创建一个kafka生产者,代码如下(类名:kafkapro_true):
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.io.*;
import java.util.Properties;
public class kafkapro_true {
public static void main(String[] args) throws FileNotFoundException, UnsupportedEncodingException {
Properties props = new Properties();
//1.指定Kafaka集群的ip地址和端口号
props.put("bootstrap.servers", "kafka1:9092,kafka2:9093,kafka3:9094");
//2.等待所有副本节点的应答
props.put("acks", "all");
//3.消息发送最大尝试次数
props.put("retries", 0);
//4.指定一批消息处理次数
props.put("batch.size", 16384);
//5.指定请求延时
props.put("linger.ms", 1);
//6.指定缓存区内存大小
props.put("buffer.memory", 33554432);
//7.设置key序列化
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//8.设置value序列化
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
// 9、生产数据
KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);
// 定义CSV文件路径
String csvFile = "H:\\a-storm测试\\股票数据1.csv";
int a=0;
// 读取CSV文件并发送到Kafka
try {
while(true){
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(csvFile), "GBK"));
String line;
reader.readLine();
while ((line = reader.readLine()) != null) {
producer.send(new ProducerRecord<String, String>("ysh",line));
}
a+=1;
System.out.println(a);
/*if(a>20){
break;
} */
//producer.close(); // 关闭Kafka生产者
}
}
catch (IOException e) {
System.out.printf("文件打开失败");
// 处理IO异常
}
}
}
注意:上面代码不断读一个文件是为了做压力测试,因为软件测试的数据太慢,不适合做压力测试,所以请根据具体情况修改代码。
2.创建一个kafkaconsumer+spout的结合类,代码如下(类名:kafkaSpout2):
import org.apache.storm.spout.SpoutOutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseRichSpout;
import org.apache.storm.tuple.Fields;
import org.apache.storm.tuple.Values;
import org.apache.storm.utils.Utils;
import java.util.Map;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.util.Arrays;
import java.util.Properties;
public class kafkaSpout2 extends BaseRichSpout {
//数据收集器
private SpoutOutputCollector collector;
int a=0;
KafkaConsumer<String, String> kafkaConsumer;
@Override
public void open(Map conf, TopologyContext context, SpoutOutputCollector collector) {
this.collector = collector;
//1、准备配置文件
Properties props = new Properties();
//2、指定kafka集群主机名和端口号
//props.put("zookeeper.connect", "localhost:2181");
props.put("bootstrap.servers", "kafka1:9092,kafka2:9093,kafka3:9094");
//3、指定消费者组id,在同一时刻同一消费组中只有一个线程可以
//去消费一个分区消息,不同的消费组可以去消费同一个分区消息
props.put("group.id", "consumer");
//4、自动提交偏移量
props.put("enable.auto.commit", "true");
//5、自动提交时间间隔,每秒提交一次
props.put("auto.commit.interval.ms", "1000");
props.put("auto.offset.reset","earliest");
props.put("client.id", "zy_client_id");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
kafkaConsumer = new KafkaConsumer<String, String>(props);
//6、订阅消息,这里的topic可以是多个
kafkaConsumer.subscribe(Arrays.asList("ysh"));
}
@Override
public void nextTuple() {
while(true){
ConsumerRecords<String, String> records = kafkaConsumer.poll(1);
if(records.isEmpty()){
Utils.sleep(1000);
}
records.forEach(record -> {collector.emit(new Values(record.value()));
a++;});
}
//Utils.sleep(5000);
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("line"));
}
}
3.创建一个bolt,代码如下(类名:kafkaBolt):
import org.apache.storm.task.OutputCollector;
import org.apache.storm.task.TopologyContext;
import org.apache.storm.topology.OutputFieldsDeclarer;
import org.apache.storm.topology.base.BaseRichBolt;
import org.apache.storm.tuple.Tuple;
import java.sql.Connection;
import java.util.HashMap;
import java.util.Map;
public class kafkaBolt extends BaseRichBolt {
int count=0;
double count_all=0;
double price_all=0;
double trade_all=0;
double price_time=0;
double trade_time=0;
double trade_buy=0;
double trade_sell=0;
double trade_buy1=0;
double trade_sell1=0;
private Map<String, Double> stockTypeVolume = new HashMap<>();
private Map<String, Double> stockTypeVolume1 = new HashMap<>();
private Map<String, Double> stockTypeVolume2 = new HashMap<>();
private Map<String, Double> platTypeVolume = new HashMap<>();
private Map<String, Double> platTypeVolume1 = new HashMap<>();
private Map<String, Double> provinceVolume = new HashMap<>();
public sql_con con=null;
Connection conn=null;
long startTime=0;
long startTime1=0;
long startTime2=0;
double linesPerSecondActual=0;
@Override
public void prepare(Map map, TopologyContext topologyContext, OutputCollector outputCollector) {
con=new sql_con();
conn=con.getconnection();
startTime = System.currentTimeMillis();
startTime1 = System.currentTimeMillis();
startTime2 = System.currentTimeMillis();
}
@Override
public void execute(Tuple tuple) {
String line = tuple.getStringByField("line");
if(line.equals(" ")){
con.insert_speed(conn,linesPerSecondActual);
con.insert_a_1m(conn,trade_all-trade_time,price_all-price_time);
con.insert_a_2m(conn,trade_all,price_all);
con.insert_b_1m(conn,trade_buy-trade_buy1,trade_sell-trade_sell1);
con.insert_b_2m(conn,trade_buy,trade_sell);
con.insert_c_1m(conn,stockTypeVolume1);
con.insert_c_2m(conn,stockTypeVolume);
con.insert_d_1m(conn,platTypeVolume1);
con.insert_d_2m(conn,platTypeVolume);
con.insert_e_1m(conn,provinceVolume);
con.insert_f_1m(conn,stockTypeVolume2);
System.out.println("订单处理数为:");
System.out.println(count);
System.exit(0);
}
String[] parts = line.split(",");
count_all++;
//System.out.println(count_all+","+line+parts.length);
//System.out.println(count_all);
if(parts.length>=8){//异常数据不要
try{
String time = parts[0];
String stockCode = parts[1];
String stockName = parts[2];
double price = Double.parseDouble(parts[3]);
double tradeVolume = Double.parseDouble(parts[4]);
String trade_type = parts[5];
String trade_place = parts[6];
String trade_platform=parts[7];
String industry_type=parts[8];
//统计订单数
count++;
long endTime = System.currentTimeMillis();
long elapsedTime = endTime - startTime;
linesPerSecondActual = count / (elapsedTime / 1000.0);
//统计交易量和总金额
trade_all+=tradeVolume;
price_all+=price*tradeVolume;
//统计买入、卖出交易量
if(trade_type.equals("买入")){
trade_buy+=tradeVolume;
}else{
trade_sell+=tradeVolume;
}
//不同类型股票交易总金额
if (stockTypeVolume1.containsKey(stockName)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = stockTypeVolume1.get(stockName);
stockTypeVolume1.put(stockName, currentTradeVolume + tradeVolume*price);
} else {
// If the map does not contain the stock type, add it to the map
stockTypeVolume1.put(stockName, tradeVolume*price);
}
if (stockTypeVolume.containsKey(stockName)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = stockTypeVolume.get(stockName);
stockTypeVolume.put(stockName, currentTradeVolume + tradeVolume*price);
} else {
// If the map does not contain the stock type, add it to the map
stockTypeVolume.put(stockName, tradeVolume*price);
}
//不同类型交易平台交易量
if (platTypeVolume.containsKey(trade_platform)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = platTypeVolume.get(trade_platform);
platTypeVolume.put(trade_platform, currentTradeVolume + tradeVolume);
} else {
// If the map does not contain the stock type, add it to the map
platTypeVolume.put(trade_platform, tradeVolume);
}
if (platTypeVolume1.containsKey(trade_platform)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = platTypeVolume1.get(trade_platform);
platTypeVolume1.put(trade_platform, currentTradeVolume + tradeVolume);
} else {
// If the map does not contain the stock type, add it to the map
platTypeVolume1.put(trade_platform, tradeVolume);
}
//统计全国各地下单客户的累计数量(按省份)
if (provinceVolume.containsKey(trade_place)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = provinceVolume.get(trade_place);
provinceVolume.put(trade_place, currentTradeVolume + 1);
} else {
// If the map does not contain the stock type, add it to the map
double x=1;
provinceVolume.put(trade_place, x);
}
//不同类型股票交易量
if (stockTypeVolume2.containsKey(stockName)) {
// If the map already contains the stock type, update the values
double currentTradeVolume = stockTypeVolume2.get(stockName);
stockTypeVolume2.put(stockName, currentTradeVolume + tradeVolume);
} else {
// If the map does not contain the stock type, add it to the map
stockTypeVolume2.put(stockName, tradeVolume);
}
endTime = System.currentTimeMillis();
elapsedTime = endTime - startTime1;
long elapsedTime1=endTime - startTime2;
//System.out.println(count);
//把数据插入数据库
if(elapsedTime/1000>=1){//给1秒操作时间,统计一分钟交易量和金额
startTime1=endTime;
//插入数据库
con.insert_speed(conn,linesPerSecondActual);
con.insert_a_1m(conn,trade_all-trade_time,price_all-price_time);
con.insert_a_2m(conn,trade_all,price_all);
con.insert_b_1m(conn,trade_buy-trade_buy1,trade_sell-trade_sell1);
con.insert_b_2m(conn,trade_buy,trade_sell);
con.insert_c_1m(conn,stockTypeVolume1);
con.insert_c_2m(conn,stockTypeVolume);
con.insert_d_1m(conn,platTypeVolume1);
con.insert_d_2m(conn,platTypeVolume);
con.insert_e_1m(conn,provinceVolume);
con.insert_f_1m(conn,stockTypeVolume2);
}
if(elapsedTime1/1000>=59){
startTime2=endTime;
trade_time=trade_all;
price_time=price_all;
trade_buy1=trade_buy;
trade_sell1=trade_sell;
stockTypeVolume1.clear();
platTypeVolume1.clear();
}
}catch (Exception e){e.printStackTrace();}
}
//count++;
//System.out.println(count+","+line);
}
@Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
// 不需要发射数据到下一个Bolt,因此这个方法可以为空
}
}
4.创建一个topology,就用上面创建的topology类,改一个topology结构即可。如果对storm不太了解的,或者不会搭建的,可参考个人博客:http://t.csdnimg.cn/fZq7L
(7)方案三框架设计代码实现
太简单了,这里就不写了,直接把kafkabolt类里的统计方法全部放到kafka消费者代码里面就行。
四.实验结果和分析
话不多说,直接看结果:
(表1)不同spout和版本的订单处理速度:
Storm(2.2.0) | CPU****使用率 | |
---|---|---|
单个spout | 600-800之间波动 | 30%-40%波动(偶尔出现10%波动) |
3个spout | 1900-2000之间波动 | 30%-40%波动,更加稳定 |
5个spout | 3300左右波动 | 30%-40%波动,更加稳定(偶尔出现50%) |
Storm不同版本(1.0.0,2.4.0) | 对结果几乎无影响 |
(表2)单个spout不同版本的订单处理速度:
Storm+kafka | CPU****使用率 | |
---|---|---|
Storm1.0.0 | 一直在不断波动,最后大概稳定在10万条每秒左右 | 基本维持在90%-100%之间 |
Storm2.2.0 | 本地服务器连接不应该发送BackPressure状态。 | |
Storm2.4.0 | 同2.2.0 |
(表3)kafka框架下订单处理速度和CPU使用率:
订单处理速度 | CPU****使用率 | |
---|---|---|
kafka | 10万条左右 | 90%-100%波动 |
实验分析:
由表1可知,在不同数量的spout下,storm框架的订单处理速度在不断提示,几乎成倍数增长,不同storm版本对实验几乎无影响。
由表3可知,在kafka框架下,订单处理速度在10万条左右,速度极限应该与读取文件的速度有关。
由表2可知,在kafka+storm的集成框架下,订单的处理速度极限到达了10万条(根据表3和实际情况推测)。
注意:关于cpu使用率,其实统计并不怎么消耗资源,大部分资源都消耗在了读取信息,拉去信息上。
说明:新版storm出现Local Server connection should not send BackPressure status问题原因
异常信息指出“Local Server connection should not send BackPressure status”,即本地服务器连接不应该发送BackPressure状态。
在Apache Storm中,BackPressure是一种机制,当一个worker发送消息的速度超过另一个worker接收消息的速度时,会通过BackPressure状态进行反馈,以防止发送方过度发送消息而造成资源浪费。
疑问:
1.新版storm无法成功运行,老版storm速度有极限波动范围,那么老版storm会有误差吗?
2.订单处理速度10万条左右真的是框架的极限吗?会受哪些因素影响呢?
3.新版storm如何解决BackPressure status问题?它的订单处理速度极限在哪?
第一个问题:
本人统计了几轮spout接受量与bolt接收量来观察是否有数据误差,数据如下:
表(4) spout接收量与bolt接收量统计:
Spout****接收量 | Bolt****接收量 |
---|---|
3000250 | 3000231 |
3000318 | 3000298 |
3000333 | 3000314 |
分析:
根据表(4)可知,spout接收量与bolt接收量几乎相等,但仍有少许误差。
这真的是统计误差吗?有待思考。
我猜想这应该是不可避免误差,与误差统计方法与框架的逻辑等因素有关。
具体情况有待进一步验证。
第二个问题:
订单处理速度10万条左右真的是框架的极限吗?会受哪些因素影响呢?
不是的,这里无法给出一个准确的 数字,订单处理速度会因为参数,和框架版本,cpu使用率,磁盘读写速度等因素影响,这也导致多次测试结果都不尽相同。
第三个问题:
新版storm如何解决BackPressure status问题?它的订单处理速度极限在哪?
这个问题本人因为时间问题并没有解决,但可以给大家一个猜想取验证:
猜想:会出现该问题的原因可能是这里的kafka+storm框架只是简单的融合,把kafka的comsumer和storm的spout整合到了一起。但storm官网有storm+kafka集群的实现方法,肯定不是简单的整合,里面有许多细节需要我们取实现,用官网的集成方法应该可以解决该问题。
五.项目特色
本实验除了完成所有基本要求外,还侧重压力测试,误差分析等进行了详细的规划和设计,同时,美观的可视化界面,整个业务逻辑的设计,等都是本次实验的创新和特色之处。与其他同学不同之处就在于不论是框架设计还是压力测试等,本实验都有详细的可行性分析以及探究性学习。
六.问题分析
实验过程中也遇到了许多问题,不过在不断的学习中渐渐的被本人解决了许多,这里就不在详细介绍过程中遇到的一些问题了,可详细阅读本次研究报告,里面的一些细节之处大致就是本次实验的一些难点了。
七.方案的改进分析
本次实验所有的方案设计都是针对性的设计,全都是为了对storm框架进行压力测试,为此并没有设计太过复杂的框架,不过大家要思考的一点就是,不论我们怎么设计实验框架,我们都要对实验框架进行可行性分析,即为什么要这样去设计框架,这样设计的好处在哪里,相比与普通的框架的优势在哪里,需要注意的是框架设计不能只是纸上谈兵,我们需要拿数据说话,来证明框架的可行性。所有的一个探究都应该如此。
本次实验最终采用的是kafka+storm框架(方案设计二)来进行后端数据的接收和统计,相比与最基础的storm框架,该框架大大提升了订单处理速度,但该方案依然有一些待改进之处,就比如在实验分析中的一个问题:新版storm如何解决BackPressure status问题?它的订单处理速度极限在哪?该问题因为个人时间问题还未得到解决,如果感兴趣可以去了解。
八.心得体会
本实验主要围绕storm+kafka实时计算框架来进行股票交易数据 的统计与分析测试,美观的可视化界面,整个业务逻辑的设计,低误差,详细的框架压力测试等都是本次实验的创新和特色之处。但本实验依然有许多改进之处,许多分析测试还缺乏更加详细的规划。
通过本次实验,让我对实时流计算框架有了更进一步的认识。