……

第一章、druid入门

1.什么是Druid?

Druid是一个高效的数据查询系统,主要解决的是对于大量的基于时序的数据进行聚合查询。数据可以实时摄入,进入到Druid后立即可查,同时数据是几乎是不可变。通常是基于时序的事实事件,事实发生后进入Druid,外部系统就可以对该事实进行查询。

1、Druid采用的架构:

shared-nothing架构与lambda架构

2、Druid设计三个原则:

1.快速查询(Fast Query) : 部分数据聚合(Partial Aggregate) + 内存华(In-Memory) + 索引(Index) 2.水平拓展能力(Horizontal Scalability):分布式数据(Distributed data)+并行化查询(Parallelizable Query) 3.实时分析(Realtime Analytics):Immutable Past , Append-Only Future

3.Druid的技术特点

  • 数据吞吐量大

  • 支持流式数据摄入和实时

  • 查询灵活且快速

4.Druid基本概念:

Druid在数据摄入之前,首先要定义一个数据源(DataSource,类似于数据库中表的概念) Druid是一个分布式数据分析平台,也是一个时序数据库

1.数据格式

数据集合(时间列,维度列,指标列)

1、数据结构:

基于DataSource与Segment的数据结构,DataSource相当于关系型数据库中的表。 DataSource包含: 时间列(TimeStamp):标识每行数据的时间值 维度列(Dimension):标识数据行的各个类别信息 指标列(Metric):用于聚合和计算的列

2、Segment结构:

DataSource是逻辑结构,而Segment是数据实际存储的物理结构,Druid通过Segment实现对数据的横纵切割操作。 横向切割:通过设置在segmentGranularity参数,Druid将不同时间范围内的数据存储在不同Segment数据块中。 纵向切割:在Segment中面向列进行数据压缩处理

3、设置合理的Granularity

segmentGranularity:segment的组成粒度。 queryGranularity :segment的聚合粒度。 queryGranularity 小于等于 segmentGranularity 若segmentGranularity = day,那么Druid会按照天把不同天的数据存储在不同的Segment中。 若queryGranularity =none,可以查询所有粒度,queryGranularity = hour只能查询>=hour粒度的数据

2.数据摄入

实时数据摄入 批处理数据摄入

3.数据查询

原生查询,采用JSON格式,通过http传送

4.时序数据库
1.OpenTSDB

开源的时序数据库,支持数千亿的数据点,并提供精确的数据查询功能 采用java编写,通过基于Hbase的存储实现横向拓展 设计思路:利用Hbase的key存储一些tag信息,将同一小时的数据放在一行存储,提高了查询速度 架构示意图: img

2.Pinot

接近Druid的系统 Pinot也采用了Lambda架构,将实时流和批处理数据分开处理 Realtime Node处理实时数据查询 Historical Node处理历史数据 技术特点: 面向列式存储的数据库,支持多种压缩技术 可插入的索引技术 — Sorted index ,Bitmap index, Inverted index 可以根据Query和Segment元数据进行查询和执行计划的优化 从kafka实时灌入数据和从hadoop的批量数据灌入 类似于SQL的查询语言和各种常用聚合 支持多值字段 水平拓展和容错 Pinot架构图:

img

5.Druid架构概览

img

Druid包含以下四个节点: 实时节点(Realtime ):即时摄入实时数据,以及生成Segment数据文件 实时节点负责消费实时数据,实时数据首先会被直接加载进实时节点内存中的堆结构缓存区,当条件满足时, 缓存区的数据会被冲写到硬盘上形成一个数据块(Segment Split),同时实时节点又会立即将新生成的数据库加载到内存的非堆区, 因此无论是堆结构缓存区还是非堆区里的数据都能被查询节点(Broker Node)查询 历史节点(Historical Node):加载已经生成好的文件,以供数据查询 查询节点(Broker Node):对外提供数据查询服务,并同时从实时节点和历史节点查询数据,合并后返回给调用方 协调节点(Coordinator Node):负责历史节点的数据负载均衡,以及通过规则(Rule)管理数据的生命周期

集群还依赖三类外部依赖 元数据库(Metastore):存储Druid集群的原数据信息,比如Segment的相关信息,一般用MySql或PostgreSQL 分布式协调服务(Coordination):为Druid集群提供一致性协调服务的组件,通常为Zookeeper 数据文件存储系统(DeepStorage):存放生成的Segment文件,并供历史节点下载。对于单节点集群可以是本地磁盘,而对于分布式集群一般是HDFS或NFS

实时节点数据块的生成示意图: img

数据块的流向:

img

Realtime Node 实时节点: 1.通过Firehose来消费实时数据,Firehose是Druid中消费实时数据的模型 2.实时节点会通过一个用于生成Segment数据文件的模块Plumber(具体实现有RealtimePlumber等)按照指定的周期,按时将本周起生产的所有数据块合并成一个大的Segment文件

Historical Node历史节点 img

历史节点在启动的时候 : 1、会去检查自己本地缓存中已经存在的Segment数据文件 2、从DeepStorege中下载属于自己但是目前不在自己本地磁盘上的Segment数据文件 无论何种查询,历史节点会首先将相关Segment数据文件从磁盘加载到内存,然后在提供查询

Broker Node节点: Druid提供两类介质作为Cache以供选择 外部Cache,比如Memcached 本地Cache,比如查询节点或历史节点的内存作为cache

高可用性: 通过Nginx来负载均衡查询节点(Broker Node) img

协调节点: 协调节点(Coordinator Node)负责历史节点的数据负载均衡,以及通过规则管理数据的生命周期

6.索引服务

img

 

1.其中主节点:overlord

两种运行模式: 本地模式(Local Mode):默认模式,主节点负责集群的任务协调分配工作,也能够负责启动一些苦工(Peon)来完成一部分具体任务 远程模式(Remote):该模式下,主节点与从节点运行在不同的节点上,它仅负责集群的任务协调分配工作,不负责完成具体的任务,主节点提供RESTful的访问方法,因此客户端可以通过HTTP POST 请求向主节点提交任务。 命令格式如下: http://<ioverlord_ip>:port/druid/indexer/v1/task 删除任务:http://<ioverlord_ip>:port/druid/indexer/v1/task/{taskId}/shutdown 控制台:http://<ioverlord_ip>:port/console.html

2.从节点:Middle Manager

索引服务的工作节点,负责接收主节点的分配的任务,然后启动相关的苦工即独立的JVM来完成具体的任务 这样的架构与Hadoop YARN相似 主节点相当于Yarn的ResourceManager,负责集群资源管理,与任务分配 从节点相当于Yarn的NodeManager,负责管理独立节点的资源并接受任务 Peon(苦工)相当于Yarn的Container,启动在具体节点上负责具体任务的执行 问题:

由于老版本的Druid使用pull方式消费kafka数据,使用kafka consumer group来共同消费一个kafka topic的数据,各个节点会负责独立消费一个或多个该topic所包含的Partition数据,并保证同一个Partition不会被多于一个的实时节点消费。每当一个实时节点完成部分数据的消费后,会主动将消费进度(kafka topic offset)提交到Zookeeper集群。 当节点不可用时,该kafka consumer group 会立即在组内对所有可用的节点进行partition重新分配,接着所有节点将会根据记录在zk集群中每一个partition的offset来继续消费未曾消费的数据,从而保证所有数据在任何时候都会被Druid集群至少消费一次。 这样虽然能保证不可用节点未消费的partition会被其余工作的节点消费掉,但是不可用节点上已经消费的数据,尚未被传送到DeepStoreage上且未被历史节点下载的Segment数据却会被集群遗漏,这是基于kafka-eight Firehose消费方式的一种缺陷。 解决方案: 1.让不可用节点恢复重新回到集群成为可用节点,重启后会将之前已经生成但未上传的Segment数据文件统统加载回来,并最终合并传送到DeepStoreage,保证数据完整性 2.使用Tranquility与Indexing Service,对kafka的数据进行精确的消费与备份。 由于Tranquility可以通过push的方式将指定数据推向Druid集群,因此它可以同时对同一个partition制造多个副本。所以当某个数据消费失败时候,系统依然可以准确的选择使用另外一个相同的任务所创建的Segment数据库

7、Druid快速入门

硬件及软件要求:

  • Java 8 or higher

  • Linux, Mac OS X, or other Unix-like OS (Windows is not supported)

  • 8G of RAM

  • 2 vCPUs

1、下载解压缩软件包

 

curl -O http://static.druid.io/artifacts/releases/druid-0.12.3-bin.tar.gz
tar -xzf druid-0.12.3-bin.tar.gz
cd druid-0.12.3

压缩包包含如下目录:

  • LICENSE - license文件.

  • bin/ - 快速启动相关脚本.

  • conf/* - 为集群安装提供的配置模板.

  • conf-quickstart/* - 快速入门配置文件.

  • extensions/* - 所有Druid扩展文件.

  • hadoop-dependencies/* - Druid Hadoop依赖文件.

  • lib/* - 所有Druid核心软件包.

  • quickstart/* - quickstart相关文件目录.

2、下载教程示例

 

curl -O http://druid.io/docs/0.12.3/tutorials/tutorial-examples.tar.gz
tar zxvf tutorial-examples.tar.gz
3、启动zookeeper

 

curl http://mirror.bit.edu.cn/apache/zookeeper/stable/zookeeper-3.4.12.tar.gz -o zookeeper-3.4.12.tar.gz
tar -xzf zookeeper-3.4.12.tar.gz
cd zookeeper-3.4.12
cp conf/zoo_sample.cfg conf/zoo.cfg
./bin/zkServer.sh start
4、启动Druid服务

在druid-0.12.3目录下,执行如下命令

 

bin/init

init会做一些初始化工作,脚本内容如下:

 

#!/bin/bash -eu

gzip -c -d quickstart/wikiticker-2015-09-12-sampled.json.gz > "quickstart/wikiticker-2015-09-12-sampled.json"

LOG_DIR=var

mkdir log
mkdir -p $LOG_DIR/tmp;
mkdir -p $LOG_DIR/druid/indexing-logs;
mkdir -p $LOG_DIR/druid/segments;
mkdir -p $LOG_DIR/druid/segment-cache;
mkdir -p $LOG_DIR/druid/task;
mkdir -p $LOG_DIR/druid/hadoop-tmp;
mkdir -p $LOG_DIR/druid/pids;

在不同的终端窗口启动Druid进程,本教程在同一个操作系统运行所有Druid进程,在大型分布式生产集群环境中,部分Druid进程仍可以部署在一起。

 

java `cat examples/conf/druid/coordinator/jvm.config | xargs` -cp "examples/conf/druid/_common:examples/conf/druid/_common/hadoop-xml:examples/conf/druid/coordinator:lib/*" io.druid.cli.Main server coordinator
java `cat examples/conf/druid/overlord/jvm.config | xargs` -cp "examples/conf/druid/_common:examples/conf/druid/_common/hadoop-xml:examples/conf/druid/overlord:lib/*" io.druid.cli.Main server overlord
java `cat examples/conf/druid/historical/jvm.config | xargs` -cp "examples/conf/druid/_common:examples/conf/druid/_common/hadoop-xml:examples/conf/druid/historical:lib/*" io.druid.cli.Main server historical
java `cat examples/conf/druid/middleManager/jvm.config | xargs` -cp "examples/conf/druid/_common:examples/conf/druid/_common/hadoop-xml:examples/conf/druid/middleManager:lib/*" io.druid.cli.Main server middleManager
java `cat examples/conf/druid/broker/jvm.config | xargs` -cp "examples/conf/druid/_common:examples/conf/druid/_common/hadoop-xml:examples/conf/druid/broker:lib/*" io.druid.cli.Main server broker

jvm.config为java进程运行参数配置,cat coordinator/jvm.config输出如下:

 

-server
-Xms256m
-Xmx256m
-Duser.timezone=UTC
-Dfile.encoding=UTF-8
-Djava.io.tmpdir=var/tmp
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
-Dderby.stream.error.file=var/druid/derby.log

以上命令在不同终端窗口运行,分别启动了coordinator、overlord、historical、middleManager、broker进程。

5、重置Druid

所有持久化状态,如集群元数据存储和服务的segments都会保存在druid-0.12.3/var目录下. 如果你想停止服务,CTRL-C退出运行中的java进程。假如希望停止服务后,以初始化状态启动服务,删除log和var目录,再跑一遍init脚本,然后关闭Zookeeper,删除Zookeeper的数据目录/tmp/zookeeper。

在druid-0.12.3 目录下:

 

rm -rf log
rm -rf var
bin/init

假如你学习了Loading stream data from Kafka教程,在关闭Zookeeper之前你需要先关闭Kafka,删除Kafka日志目录/tmp/kafka-logs

Ctrl-C 关闭Kafka broker,删除日志目录:

 

rm -rf /tmp/kafka-logs

现在关闭Zookeeper,清理状态,在zookeeper-3.4.12目录下:

 

./bin/zkServer.sh stop
rm -rf /tmp/zookeeper

清理了Druid和Zookeeper状态数据后,重启Zookeeper和Druid服务。

6、数据集

如下数据加载教程中,我们会使用到一份数据文件,Druid包根目录下的quickstart/wikiticker-2015-09-12-sampled.json.gz,文件内容包含了2015-09-12这一天Wikipedia页面编辑事件。页面编辑事件以json对象格式存储于text文件中。

数据包含了如下列:

  • added

  • channel

  • cityName

  • comment

  • countryIsoCode

  • countryName

  • deleted

  • delta

  • isAnonymous

  • isMinor

  • isNew

  • isRobot

  • isUnpatrolled

  • metroCode

  • namespace

  • page

  • regionIsoCode

  • regionName

  • user

如下为一条示例数据:

 

{
 "timestamp":"2015-09-12T20:03:45.018Z",
 "channel":"#en.wikipedia",
 "namespace":"Main"
 "page":"Spider-Man's powers and equipment",
 "user":"foobar",
 "comment":"/* Artificial web-shooters */",
 "cityName":"New York",
 "regionName":"New York",
 "regionIsoCode":"NY",
 "countryName":"United States",
 "countryIsoCode":"US",
 "isAnonymous":false,
 "isNew":false,
 "isMinor":false,
 "isRobot":false,
 "isUnpatrolled":false,
 "added":99,
 "delta":99,
 "deleted":0,
}
7、从文件中夹在数据
1) 准备数据、定义数据摄取任务

A data load is initiated by submitting an ingestion task spec to the Druid overlord. For this tutorial, we'll be loading the sample Wikipedia page edits data. 向Druid overlord提交一个数据摄取任务,即完成了数据的初始化,如下我们会加载Wikipedia页面编辑数据。 examples/wikipedia-index.json定义了一个数据摄入任务,该任务读取quickstart/wikiticker-2015-09-12-sampled.json.gz中数据:

 

{
 "type" : "index",
 "spec" : {
   "dataSchema" : {
     "dataSource" : "wikipedia",
     "parser" : {
       "type" : "string",
       "parseSpec" : {
         "format" : "json",
         "dimensionsSpec" : {
           "dimensions" : [
             "channel",
             "cityName",
             "comment",
             "countryIsoCode",
             "countryName",
             "isAnonymous",
             "isMinor",
             "isNew",
             "isRobot",
             "isUnpatrolled",
             "metroCode",
             "namespace",
             "page",
             "regionIsoCode",
             "regionName",
             "user",
            { "name": "added", "type": "long" },
            { "name": "deleted", "type": "long" },
            { "name": "delta", "type": "long" }
          ]
        },
         "timestampSpec": {
           "column": "time",
           "format": "iso"
        }
      }
    },
     "metricsSpec" : [],
     "granularitySpec" : {
       "type" : "uniform",
       "segmentGranularity" : "day",
       "queryGranularity" : "none",
       "intervals" : ["2015-09-12/2015-09-13"],
       "rollup" : false
    }
  },
   "ioConfig" : {
     "type" : "index",
     "firehose" : {
       "type" : "local",
       "baseDir" : "quickstart/",
       "filter" : "wikiticker-2015-09-12-sampled.json.gz"
    },
     "appendToExisting" : false
  },
   "tuningConfig" : {
     "type" : "index",
     "targetPartitionSize" : 5000000,
     "maxRowsInMemory" : 25000,
     "forceExtendableShardSpecs" : true
  }
}
}

如上定义,创建了一个名为"wikipedia"的数据源.

2)加载批量数据

在druid-0.12.3 目录下,通过POST方式,提交数据摄取任务:

 

curl -X 'POST' -H 'Content-Type:application/json' -d @examples/wikipedia-index.json http://localhost:8090/druid/indexer/v1/task

假如任务提交成功,控制台将会打印任务ID:

 

{"task":"index_wikipedia_2018-06-09T21:30:32.802Z"}

可至overlord控制台http://localhost:8090/console.html查看你已提交的数据摄取任务的状态,可以周期性地刷新控制台,当任务成功时,你可以看到任务的状态变为"SUCCESS" 当摄取任务结束,数据将会被historical节点加载,且在一两分钟内可被查询到,你可以通过coordinator控制台监控数据加载进度,当控制台http://localhost:8081/#/数据源“ wikipedia”带有一个蓝色圈圈时表明"fully available"

img

第二章、druid实战

一、Spring boot+Mybatis+druid 实战

1、Druid的简介

Druid是一个非常优秀的数据库连接池。在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。

Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。

Druid是一个JDBC组件,它包括三个部分:

  • 基于Filter-Chain模式的插件体系。

  • DruidDataSource 高效可管理的数据库连接池。

  • SQLParser

2、Druid的功能
1、兼容DBCP

Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。从DBCP迁移到Druid,只需要修改数据源的实现类就可以了。

2、强大的监控特性

Druid内置了一个功能强大的StatFilter插件可以监控数据库访问性能,可以清楚知道连接池和SQL的工作情况。

  • 监控SQL的执行时间、ResultSet持有时间、返回行数、更新行数、错误次数、错误堆栈信息。

  • SQL执行的耗时区间分布。什么是耗时区间分布呢?比如说,某个SQL执行了1000次,其中0-1毫秒区间50次,1-10毫秒800次,10-100毫秒100次,100-1000毫秒30次,1-10秒15次,10秒以上5次。通过耗时区间分布,能够非常清楚知道SQL的执行耗时情况。

  • 监控连接池的物理连接创建和销毁次数、逻辑连接的申请和关闭次数、非空等待次数、PSCache命中率等。

3、数据库密码加密

直接把数据库密码写在配置文件中,这是不好的行为,容易导致安全问题。DruidDruiver和DruidDataSource都支持PasswordCallback。

4、SQL执行日志

Druid提供了不同的LogFilter,能够支持Common-Logging、Log4j和JdkLog,你可以按需要选择相应的LogFilter,监控你应用的数据库访问情况。

3、扩展JDBC

如果你要对JDBC层有编程的需求,可以通过Druid提供的Filter机制,很方便编写JDBC层的扩展插件。

Druid在DruidDataSourc和ProxyDriver上提供了Filter-Chain模式的扩展API,类似Serlvet的Filter,配置Filter拦截JDBC的方法调用。

4、SQLParser

SQL Parser是Druid的一个重要组成部分,它提供了MySql、Oracle、Postgresql、SQL-92的SQL的完整支持,这是一个手写的高性能SQL Parser,支持Visitor模式,使得分析SQL的抽象语法树很方便。

简单SQL语句用时10微秒以内,复杂SQL用时30微秒。

通过Druid提供的SQL Parser可以在JDBC层拦截SQL做相应处理,比如防御SQL注入(WallFilter)、合并统计没有参数化的SQL(StatFilter的mergeSql)、SQL格式化、分库分表。

<style>table th:first-of-type { width: 100px;}</style>
5、Druid的配置详解
配置缺省值说明
name   配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:"DataSource-" + System.identityHashCode(this). 另外配置此属性至少在1.0.5版本中是不起作用的,强行设置name会出错。详情-点此处
url   连接数据库的url,不同数据库不一样。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username   连接数据库的用户名
password   连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里
driverClassName 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
initialSize 0 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
maxActive 8 最大连接池数量
maxIdle 8 已经不再使用,配置了也没效果
minIdle   最小连接池数量
maxWait   获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
poolPreparedStatements false 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
maxPoolPreparedStatementPerConnectionSize -1 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
validationQuery   用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
validationQueryTimeout   单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
testOnBorrow true 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn false 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testWhileIdle false 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
timeBetweenEvictionRunsMillis 1分钟(1.0.14) 有两个含义: 1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接。 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
numTestsPerEvictionRun 30分钟(1.0.14) 不再使用,一个DruidDataSource只支持一个EvictionRun
minEvictableIdleTimeMillis   连接保持空闲而不被驱逐的最长时间
connectionInitSqls   物理连接初始化的时候执行的sql
exceptionSorter 根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接
filters   属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall
proxyFilters   类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系
6、项目实践

我们使用的是Spring boot + mybatis + Druid(自己定制)的架构

1、导入依赖包

 

<!-- 数据库驱动 -->
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Mybatis -->
<dependency>
   <groupId>org.mybatis.spring.boot</groupId>
   <artifactId>mybatis-spring-boot-starter</artifactId>
   <version>1.3.0</version>
</dependency>
<!--Mybatis 分页插件 pagehelper -->
<dependency>
   <groupId>com.github.pagehelper</groupId>
   <artifactId>pagehelper-spring-boot-starter</artifactId>
   <version>1.1.1</version>
</dependency>
<!-- Druid连接池包 -->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>druid</artifactId>
   <version>1.0.25</version>
</dependency>
2、配置数据源

目前Spring Boot中默认支持的连接池有dbcp,dbcp2, tomcat, hikari四种连接池。

由于Druid暂时不在Spring Bootz中的直接支持,故需要进行配置信息的定制:

3、新建配置类

 

package com.xiaolyuh.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "spring.datasource.druid")
public class DruidDataSourceProperty {
   // 数据库配置
   private String url;

   private String username;

   private String password;

   private String driverClassName;

   // 初始化大小,最小,最大
   private int initialSize = 0;

   private int minIdle;

   private int maxActive = 8;

   // 配置获取连接等待超时的时间
   private int maxWait;

   // 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
   private int timeBetweenEvictionRunsMillis = 1000 * 60;

   // 配置一个连接在池中最小生存的时间,单位是毫秒
   private int minEvictableIdleTimeMillis = 1000 * 60 * 30;

   // 检测连接是否有效的sql
   private String validationQuery;

   private boolean testWhileIdle = false;

   private boolean testOnBorrow = true;

   private boolean testOnReturn = false;

   // PSCache Mysql下建议关闭
   private boolean poolPreparedStatements = false;

   private int maxPoolPreparedStatementPerConnectionSize = - 1;

   // 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
   private String filters;

   // 合并多个DruidDataSource的监控数据
   private boolean useGlobalDataSourceStat = false;

   private String connectionProperties;

   public String getUrl() {
       return url;
  }

   public void setUrl(String url) {
       this.url = url;
  }

   public String getUsername() {
       return username;
  }

   public void setUsername(String username) {
       this.username = username;
  }

   public String getPassword() {
       return password;
  }

   public void setPassword(String password) {
       this.password = password;
  }

   public String getDriverClassName() {
       return driverClassName;
  }

   public void setDriverClassName(String driverClassName) {
       this.driverClassName = driverClassName;
  }

   public int getInitialSize() {
       return initialSize;
  }

   public void setInitialSize(int initialSize) {
       this.initialSize = initialSize;
  }

   public int getMinIdle() {
       return minIdle;
  }

   public void setMinIdle(int minIdle) {
       this.minIdle = minIdle;
  }

   public int getMaxActive() {
       return maxActive;
  }

   public void setMaxActive(int maxActive) {
       this.maxActive = maxActive;
  }

   public int getMaxWait() {
       return maxWait;
  }

   public void setMaxWait(int maxWait) {
       this.maxWait = maxWait;
  }

   public int getTimeBetweenEvictionRunsMillis() {
       return timeBetweenEvictionRunsMillis;
  }

   public void setTimeBetweenEvictionRunsMillis(int timeBetweenEvictionRunsMillis) {
       this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
  }

   public int getMinEvictableIdleTimeMillis() {
       return minEvictableIdleTimeMillis;
  }

   public void setMinEvictableIdleTimeMillis(int minEvictableIdleTimeMillis) {
       this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
  }

   public String getValidationQuery() {
       return validationQuery;
  }

   public void setValidationQuery(String validationQuery) {
       this.validationQuery = validationQuery;
  }

   public boolean isTestWhileIdle() {
       return testWhileIdle;
  }

   public void setTestWhileIdle(boolean testWhileIdle) {
       this.testWhileIdle = testWhileIdle;
  }

   public boolean isTestOnBorrow() {
       return testOnBorrow;
  }

   public void setTestOnBorrow(boolean testOnBorrow) {
       this.testOnBorrow = testOnBorrow;
  }

   public boolean isTestOnReturn() {
       return testOnReturn;
  }

   public void setTestOnReturn(boolean testOnReturn) {
       this.testOnReturn = testOnReturn;
  }

   public boolean isPoolPreparedStatements() {
       return poolPreparedStatements;
  }

   public void setPoolPreparedStatements(boolean poolPreparedStatements) {
       this.poolPreparedStatements = poolPreparedStatements;
  }

   public int getMaxPoolPreparedStatementPerConnectionSize() {
       return maxPoolPreparedStatementPerConnectionSize;
  }

   public void setMaxPoolPreparedStatementPerConnectionSize(int maxPoolPreparedStatementPerConnectionSize) {
       this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize;
  }

   public String getFilters() {
       return filters;
  }

   public void setFilters(String filters) {
       this.filters = filters;
  }


   public boolean isUseGlobalDataSourceStat() {
       return useGlobalDataSourceStat;
  }

   public void setUseGlobalDataSourceStat(boolean useGlobalDataSourceStat) {
       this.useGlobalDataSourceStat = useGlobalDataSourceStat;
  }

   public String getConnectionProperties() {
       return connectionProperties;
  }

   public void setConnectionProperties(String connectionProperties) {
       this.connectionProperties = connectionProperties;
  }
}

该配置类我们采用Spring boot推荐的安全配置方式,使用@ConfigurationProperties注解,声明该类是一个配置类,并指定配置前缀。

4、配置连接池

 

package com.xiaolyuh.config;

import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;

@Configuration
@ConditionalOnClass(com.alibaba.druid.pool.DruidDataSource.class)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.alibaba.druid.pool.DruidDataSource", matchIfMissing = true)
@ServletComponentScan("com.xiaolyuh.filters")
public class DruidDataSourceConfig {

   private Logger logger = LoggerFactory.getLogger(DruidDataSourceConfig.class);

   @Autowired
   private DruidDataSourceProperty druidDataSourceProperty;


   @Bean     //声明其为Bean实例
   @Primary  //在同样的DataSource中,首先使用被标注的DataSource
   public DataSource dataSource(){
       DruidDataSource datasource = new DruidDataSource();

       datasource.setUrl(druidDataSourceProperty.getUrl());
       datasource.setUsername(druidDataSourceProperty.getUsername());
       datasource.setPassword(druidDataSourceProperty.getPassword());
       datasource.setDriverClassName(druidDataSourceProperty.getDriverClassName());

       datasource.setInitialSize(druidDataSourceProperty.getInitialSize());
       datasource.setMinIdle(druidDataSourceProperty.getMinIdle());
       datasource.setMaxActive(druidDataSourceProperty.getMaxActive());
       // 配置获取连接等待超时的时间
       datasource.setMaxWait(druidDataSourceProperty.getMaxWait());
       // 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
       datasource.setTimeBetweenEvictionRunsMillis(druidDataSourceProperty.getTimeBetweenEvictionRunsMillis());
       // 配置一个连接在池中最小生存的时间,单位是毫秒
       datasource.setMinEvictableIdleTimeMillis(druidDataSourceProperty.getMinEvictableIdleTimeMillis());
       datasource.setValidationQuery(druidDataSourceProperty.getValidationQuery());
       datasource.setTestWhileIdle(druidDataSourceProperty.isTestWhileIdle());
       datasource.setTestOnBorrow(druidDataSourceProperty.isTestOnBorrow());
       datasource.setTestOnReturn(druidDataSourceProperty.isTestOnReturn());
       datasource.setPoolPreparedStatements(druidDataSourceProperty.isPoolPreparedStatements());
       datasource.setMaxPoolPreparedStatementPerConnectionSize(druidDataSourceProperty.getMaxPoolPreparedStatementPerConnectionSize());
       datasource.setUseGlobalDataSourceStat(druidDataSourceProperty.isUseGlobalDataSourceStat());
       try {
           datasource.setFilters(druidDataSourceProperty.getFilters());
      } catch (SQLException e) {
           logger.error("druid configuration initialization filter", e);
      }
       datasource.setConnectionProperties(druidDataSourceProperty.getConnectionProperties());

       return datasource;
  }
   
//   // 配置Druid监控的StatViewServlet和WebStatFilter
//   @Bean
//   public ServletRegistrationBean druidServlet() {
//     logger.info("init Druid Servlet Configuration ");
//     ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
//     servletRegistrationBean.setServlet(new StatViewServlet());
//     servletRegistrationBean.addUrlMappings("/druid/*");
//     Map<String, String> initParameters = new HashMap<String, String>();
//     initParameters.put("loginUsername", "admin");// 用户名
//     initParameters.put("loginPassword", "admin");// 密码
//     initParameters.put("resetEnable", "false");// 禁用HTML页面上的“Reset All”功能
//     initParameters.put("allow", ""); // IP白名单 (没有配置或者为空,则允许所有访问)
//     //initParameters.put("deny", "192.168.20.38");// IP黑名单 (存在共同时,deny优先于allow)
//     servletRegistrationBean.setInitParameters(initParameters);
//     return servletRegistrationBean;
//   }
//
//   @Bean
//   public FilterRegistrationBean filterRegistrationBean() {
//     FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
//     filterRegistrationBean.setFilter(new WebStatFilter());
//     filterRegistrationBean.addUrlPatterns("/*");
//     filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
//     return filterRegistrationBean;
//   }
}  

该类被@Configuration标注,声明成配置类;DataSource对象被@Bean声明,为Spring容器所管理, @Primary表示这里定义的DataSource将覆盖其他来源的DataSource。

5、application.properties配置信息:

 

# 数据源配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.url=jdbc:mysql://localhost:3306/ssb_test
spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.username=root
spring.datasource.druid.password=root
# 初始化大小,最小,最大
spring.datasource.druid.initial-size=5
spring.datasource.druid.min-idle=5
spring.datasource.druid.max-active=20
# 配置获取连接等待超时的时间
spring.datasource.druid.max-wait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.time-between-eviction-runs-millis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.min-evictable-idle-time-millis=300000
#检测连接是否有效的sql
spring.datasource.druid.validation-query=SELECT 'x'
spring.datasource.druid.test-while-idle=true
spring.datasource.druid.test-on-borrow=false
spring.datasource.druid.test-on-return=false
# PSCache Mysql下建议关闭
spring.datasource.druid.pool-prepared-statements=false
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=-1
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.druid.filters=stat,wall,log4j
# 合并多个DruidDataSource的监控数据
spring.datasource.druid.use-global-data-source-stat=true
spring.datasource.druid.connection-properties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

#mybatis
#entity扫描的包名
mybatis.type-aliases-package=com.xiaolyuh.domain.model
#Mapper.xml所在的位置
mybatis.mapper-locations=classpath*:/mybaits/*Mapper.xml
#开启MyBatis的二级缓存
mybatis.configuration.cache-enabled=true

#pagehelper
pagehelper.helperDialect=mysql
pagehelper.reasonable=true
pagehelper.supportMethodsArguments=true
pagehelper.params=count=countSql

到这里我们数据源就配置好了,写一个测试类我们可以查看数据源是否生效。

6、测试类

 

package com.xiaolyuh;

import com.alibaba.fastjson.JSON;
import com.xiaolyuh.config.DruidDataSourceProperty;
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.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringRunner;

import javax.sql.DataSource;

@RunWith(SpringRunner.class)
@SpringBootTest
public class DataSourceTests {

   @Autowired
   ApplicationContext applicationContext;

   @Autowired
   DruidDataSourceProperty druidDataSourceProperty;

   @Test
   public void testDataSource() throws Exception {
       // 获取配置的数据源
       DataSource dataSource = applicationContext.getBean(DataSource.class);
       // 查看配置数据源信息
       System.out.println(dataSource.getClass().getName());
       System.out.println(JSON.toJSONString(druidDataSourceProperty));
  }

}

打印日志:

 

com.alibaba.druid.pool.DruidDataSource
{"connectionProperties":"druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000","driverClassName":"com.mysql.jdbc.Driver","filters":"stat,wall,log4j","initialSize":5,"maxActive":20,"maxPoolPreparedStatementPerConnectionSize":-1,"maxWait":60000,"minEvictableIdleTimeMillis":300000,"minIdle":5,"password":"root","poolPreparedStatements":false,"testOnBorrow":false,"testOnReturn":false,"testWhileIdle":true,"timeBetweenEvictionRunsMillis":60000,"url":"jdbc:mysql://localhost:3306/ssb_test","useGlobalDataSourceStat":true,"username":"root","validationQuery":"SELECT 'x'"}
2017-07-02 11:29:04.183  INFO 340 --- [       Thread-2] o.s.w.c.s.GenericWebApplicationContext   : Closing org.springframework.web.context.support.GenericWebApplicationContext@11fc564b: startup date [Sun Jul 02 11:28:58 CST 2017]; root of context hierarchy
7、Druid监控的配置

下面我们来说一下Druid监控的配置:

application.properties配置:

 

# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.druid.filters=stat,wall,log4j

使用这个filter的配置,开启对监控的支持。属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有:

  • 监控统计用的filter:stat

  • 日志用的filter:log4j

  • 防御sql注入的filter:wall

WebStatFilter和StatViewServlet配置

第一种方式使用@ServletComponentScan

这种配置监控的方式是使用了原生的servlet,filter方式,然后通过@ServletComponentScan进行启动扫描包的方式进行处理的,你会发现我们的servlet,filter根本没有任何的编码。

WebStatFilter:

 

package com.xiaolyuh.filters;

import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;

import com.alibaba.druid.support.http.WebStatFilter;

//最后在App.Java类上加上注解:@ServletComponentScan是的spring能够扫描到我们自己编写的servlet和filter。
//
//注意不要忘记在 SpringBootSampleApplication.java 上添加 @ServletComponentScan 注解,不然就是404了。
//
//然后启动项目后访问 http://127.0.0.1:8080/druid/index.html 即可查看数据源及SQL统计等。
@WebFilter(filterName = "druidWebStatFilter", urlPatterns = "/*",
       initParams = {
               @WebInitParam(name = "exclusions", value = "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*")//忽略资源
      }
)
public class DruidStatFilter extends WebStatFilter {

}

StatViewServlet:

 

package com.xiaolyuh.filters;

import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;

import com.alibaba.druid.support.http.StatViewServlet;

//最后在App.Java类上加上注解:@ServletComponentScan是的spring能够扫描到我们自己编写的servlet和filter。
//
//注意不要忘记在 SpringBootSampleApplication.java 上添加 @ServletComponentScan 注解,不然就是404了。
//
//然后启动项目后访问 http://127.0.0.1:8080/druid/index.html 即可查看数据源及SQL统计等。
@WebServlet(urlPatterns = "/druid/*",
       initParams = {
               @WebInitParam(name = "allow", value = "127.0.0.1,192.168.163.1"),// IP白名单(没有配置或者为空,则允许所有访问)
               @WebInitParam(name = "deny", value = "192.168.1.73"),// IP黑名单 (存在共同时,deny优先于allow)
               @WebInitParam(name = "loginUsername", value = "admin"),// 用户名
               @WebInitParam(name = "loginPassword", value = "123456"),// 密码
               @WebInitParam(name = "resetEnable", value = "false")// 禁用HTML页面上的“Reset All”功能
      })
public class DruidStatViewServlet extends StatViewServlet {
   private static final long serialVersionUID = -2688872071445249539L;

}

最后在App.Java类上加上注解:@ServletComponentScan是的spring能够扫描到我们自己编写的servlet和filter。

注意:不要忘记在 SpringBootSampleApplication.java 上添加 @ServletComponentScan 注解,不然就是404了。

这个配置就类似于在web.xml配置filter和servlet一样:

 

...
<servlet>    
   <servlet-name>DruidStatView</servlet-name>    
   <servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>    
</servlet>    
<servlet-mapping>    
   <servlet-name>DruidStatView</servlet-name>    
   <url-pattern>/druid/*</url-pattern>    
</servlet-mapping>  
...
<filter>    
   <filter-name>DruidWebStatFilter</filter-name>    
   <filter-class>com.alibaba.druid.support.http.WebStatFilter</filter-class>
   <init-param>    
       <param-name>exclusions</param-name>    
       <param-value>*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*</param-value>    
   </init-param>    
</filter>    
<filter-mapping>    
   <filter-name>DruidWebStatFilter</filter-name>    
   <url-pattern>/*</url-pattern>    
</filter-mapping>    
...

第二种方式,直接在配置类中注册Bean

这种方式不使用使用@ServletComponentScan注解,在@Configuration配置类中使用代码注册Servlet。

 

// 配置Druid监控的StatViewServlet和WebStatFilter
@Bean
public ServletRegistrationBean druidServlet() {
   logger.info("init Druid Servlet Configuration ");
 ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
 servletRegistrationBean.setServlet(new StatViewServlet());
 servletRegistrationBean.addUrlMappings("/druid/*");
 Map<String, String> initParameters = new HashMap<String, String>();
 initParameters.put("loginUsername", "admin");// 用户名
 initParameters.put("loginPassword", "admin");// 密码
 initParameters.put("resetEnable", "false");// 禁用HTML页面上的“Reset All”功能
 initParameters.put("allow", ""); // IP白名单 (没有配置或者为空,则允许所有访问)
 //initParameters.put("deny", "192.168.20.38");// IP黑名单 (存在共同时,deny优先于allow)
 servletRegistrationBean.setInitParameters(initParameters);
 return servletRegistrationBean;
}

@Bean
public FilterRegistrationBean filterRegistrationBean() {
 FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
 filterRegistrationBean.setFilter(new WebStatFilter());
 filterRegistrationBean.addUrlPatterns("/*");
 filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
 return filterRegistrationBean;
}
8、打开监控界面

浏览器中输入:http://localhost/druid

输入用户名和密码,上面配置的。

img

二、java+druid实战

1、作用
1、存在问题
  1. 每次进行数据库操作都要进行数据库连接,发送数据库连接四要素 driverClassNameurlusernamepassword 用于加载数据库注册驱动,不利于维护,存在硬编码问题。

  2. 获取数据库连接 connection 我们仅仅需要获得已经连接好的 connection 对象,不需要关心如何创建的。

  3. 每次进行数据库连接都会创建一个 Connection 类的实例,性能开销大。

  4. 释放数据库连接代码也存在代码重复问题。

  5. 拼接数据库语句 SQL 存在硬编码问题,且容易造成 SQL 注入攻击。

  6. DML 操作和 DQL 操作存在重复代码。

2、解决方法
  1. 解决问题一:数据库连接四要素构成 properties 配置文件 db.properties,并将加载配置文件抽取到工具类 DruidUtil 中。

  2. 解决问题二:抽取数据库连接操作 getConnection() 到工具类 DruidUtil 中。

  3. 解决问题三:工具类 DruidUtil 中,把加载数据库注册驱动的代码放在静态代码块中,加载 JVM 的时候一并加载,只执行一次。Connection 实例由数据库连接池 Druid管理。

  4. 解决问题四:同样抽取释放资源代码到工具类 DruidUtil 中。

  5. 解决问题五:采用 PreparedStatement 构建 SQL 语句,采取 ? 占位符预编译 SQL 语句。

  6. 解决问题六:将 DML 和 DQL 操作抽取到工具类 JdbcTemplate 工具类中。

2、结构

img

dao 包用于访问数据库。

domain 包定义操作的实际对象。

handler 包封装了查询数据的统一处理操作。

test 包用于单元测试。

util 包封装数据库连接、释放和提交数据库操作语句的操作。

3、具体实现
1、创建 domain

  创建具体操作对象,定义字段 idnameagephoneaddress。并在数据库中创建对象表格。

 

// dao.example.domain.Account

package dao.example.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Account {

   private Long id;
   private String name;
   private Integer age;
   private String phone;
   private String address;

}
2、创建数据库连接配置文件

  创建数据库连接配置 properties 文件 db.properties

 

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/db
username=root
password=root
3、利用 Druid 连接池管理 Connection

  利用 Druid 管理 Connection 对象,静态代码块加载配置资源,getConnection() 方法获取数据库连接。 releaseSqlConnection() 方法释放数据库操作资源。

  导包使用 java.sql.* 下包,非 com.mysql.* 下包。使模板类能统一操作各类数据库,而非只能操作 MySQL 数据库。

 

// dao.example.util.DruidUtil

package dao.example.util;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import com.alibaba.druid.pool.DruidDataSourceFactory;

public class DruidUtil {

   // 工具类,私有化无参构造函数
   private DruidUtil() {
  }

   // 静态数据源变量,供全局操作且用于静态代码块加载资源。
   private static DataSource dataSource;

   // 静态代码块,加载配置文件。
   static {
       InputStream inStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("db.properties");
       Properties properties = new Properties();
       try {
           properties.load(inStream);
           dataSource = DruidDataSourceFactory.createDataSource(properties);
      } catch (Exception e) {
           e.printStackTrace();
      }
  }

   /**
    * 创建数据库连接实例
    * @return 数据库连接实例 connection
    */
   public static Connection getConnection() {
       try {
           return dataSource.getConnection();
      } catch (SQLException e) {
           e.printStackTrace();
      }
       throw new RuntimeException("获取数据库连接异常");
  }

   /**
    * 释放数据库连接 connection 到数据库缓存池,并关闭 rSet 和 pStatement 资源
    * @param rSet 数据库处理结果集
    * @param pStatement 数据库操作语句
    * @param connection 数据库连接对象
    */
   public static void releaseSqlConnection(ResultSet rSet, PreparedStatement pStatement, Connection connection) {
       try {
           if (rSet != null) {
               rSet.close();
          }
      } catch (SQLException e) {
           e.printStackTrace();
      } finally {
           try {
               if (pStatement != null) {
                   pStatement.close();
              }
          } catch (SQLException e) {
               e.printStackTrace();
          } finally {
               try {
                   if (connection != null) {
                       connection.close();
                  }
              } catch (SQLException e) {
                   e.printStackTrace();
              }
          }
      }
  }

}
4、JDBC 操作模板方法

  上述中我们将抽取 DML 和 DQL 操作,我们创建 JdbcTemplate 模板类,模板类中定义 DML 操作方法 update(), DML 操作方法 query()

  首先,对于 DML 操作没有返回结果集需要处理,而 DQL 则有返回结果集,如何对返回结果集进行统一、规范的处理是我们需要考虑的问题。我们创建一个 BeanHandler 类进行结果集处理。

  返回结果集在项目中可能存在多个类型,因此我们采取泛型创建 BeanHandler 中的处理方法 T handler(ResultSet rSet)。然而采取泛型之后,在调用 handler(...) 操作的对象具体类型 T 是什么类型就不得而知。因此我们利用构造函数 public BeanHandler(Class clazz) 传入需要操作的具体的类型字节码。就知道所操作的对象的具体类型。

结果集操作模板

 

// dao.example.handler.IResultSetHandler<T>

package dao.example.handler;

import java.sql.ResultSet;

public interface IResultSetHandler<T> {

   T handler(ResultSet rSet);

}

  为什么定义接口?在项目开发中,当涉及到业务具体的操作对象后,一切的业务需求都是可能会变化的,定义接口规范,方便维护和拓展,符合开闭原则。

 

// dao.example.handler.impl.BeanHandler<T>

package dao.example.handler.impl;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;

import dao.example.handler.IResultSetHandler;

public class BeanHandler<T> implements IResultSetHandler<List<T>> {

   // 创建字节码对象
   private Class<T> clazz;

   // 创建有参构造函数,用于传入具体操作对象的类型
   public BeanHandler(Class<T> clazz) {
       this.clazz = clazz;
  }

   /**
    * 数据库集操作类
    * @param rSet 数据库处理结果集
    * @return 数据库结果集 List 集合
    */
   @Override
   public List<T> handler(ResultSet rSet) {
       // 创建 List 用于存放装箱后的对象
       List<T> list = new ArrayList<T>();
       try {
           // 获取类的属性描述符
           BeanInfo beanInfo = Introspector.getBeanInfo(clazz, Object.class);
           PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors();
           // 对结果集进行装箱操作
           while (rSet.next()) {
               T obj = clazz.newInstance();
               for (PropertyDescriptor descriptor : descriptors) {
                   Object value = rSet.getObject(descriptor.getName());
                   descriptor.getWriteMethod().invoke(obj, value);
              }
               list.add(obj);
          }
      } catch (Exception e) {
           e.printStackTrace();
      }
       return list;
  }

}

DML & DQL 操作模板

   query() 方法中,使用接口作为形参,而实际调用中,传入具体的实现类形参,符合开闭原则。

 

// dao.example.util.JdbcTemplate

package dao.example.util;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import javax.sql.DataSource;

import com.alibaba.druid.pool.DruidDataSourceFactory;

public class JdbcTemplate {

   // 工具类,私有化无参构造函数
   private JdbcTemplate() {
  }

   /**
    * DML 操作模板方法
    * @param sql 执行操作的 SQL 语句
    * @param arguments SQL 语句参数
    */
   public static void update(String sql, Object... arguments) {
       // 获取数据库连接 connection
       Connection connection = DruidUtil.getConnection();
       // 创建数据库语句载体
       PreparedStatement pStatement = null;
       try {
           pStatement = connection.prepareStatement(sql);
           // 给预编译好的 sql 语句中的占位符进行赋值
           if (arguments != null && arguments.length > 0) {
               for (int i = 0; i < arguments.length; i++) {
                   pStatement.setObject(i + 1, arguments[i]);
              }
          }
           // 执行 SQL 语句
           pStatement.executeUpdate();
      } catch (SQLException e) {
           e.printStackTrace();
      } finally {
           // 释放数据库连接
           DruidUtil.releaseSqlConnection(null, pStatement, connection);
      }
  }

   /**
    * DQL 操作模板
    * @param sql 执行操作的 SQL 语句
    * @param handler 对数据库返回结果集进行装箱的操作类
    * @param arguments SQL 语句参数
    * @return 返回数据库查询结果集
    */
   public static <T> T query(String sql, IResultSetHandler<T> handler, Object... arguments) {
       Connection connection = DruidUtil.getConnection();
       PreparedStatement pStatement = null;
       ResultSet rSet = null;
       try {
           pStatement = connection.prepareStatement(sql);
           if (arguments != null && arguments.length > 0) {
               for (int i = 0; i < arguments.length; i++) {
                   pStatement.setObject(i + 1, arguments[i]);
              }
          }
           rSet = pStatement.executeQuery();
           // 调用处理结果集类对数据库查询结果集进行装箱
           return handler.handler(rSet);
      } catch (SQLException e) {
           e.printStackTrace();
      } finally {
           DruidUtil.releaseSqlConnection(rSet, pStatement, connection);
      }
       return null;
  }

}
5、具体实现数据库访问层 DAO

  定义数据库操作方法。

 

// dao.example.dao.IAccountDAO

package dao.example.dao;

import java.util.List;

import dao.example.domain.Account;

public interface IAccountDAO {

   void save(Account account);

   void delete(Long id);

   void update(Account account);

   Account get(Long id);

   List<Account> list();

}

  数据库操作方法具体实现。

 

// dao.example.dao.impl.AccountDAOImpl

package dao.example.dao.impl;

import java.util.List;

import dao.example.dao.IAccountDAO;
import dao.example.domain.Account;
import dao.example.handler.impl.BeanHandler;
import dao.example.util.JdbcTemplate;

public class AccountDAOImpl implements IAccountDAO {

   @Override
   public void save(Account account) {
       String sql = "INSERT INTO t_account(name, age, phone, address) VALUES(?, ?, ?, ?)";
       Object[] arguments = new Object[] { account.getName(), account.getAge(), account.getPhone(), account.getAddress() };
       JdbcTemplate.update(sql, arguments);
  }

   @Override
   public void delete(Long id) {
       String sql = "DELETE FROM t_account WHERE id = ?";
       JdbcTemplate.update(sql, id);
  }

   @Override
   public void update(Account account) {
       String sql = "UPDATE t_account SET name = ?, age = ?, phone = ?, address = ? WHERE id = ?";
       Object[] arguments = new Object[] { account.getName(), account.getAge(), account.getPhone(), account.getAddress(), account.getId() };
       JdbcTemplate.update(sql, arguments);
  }

   @Override
   public Account get(Long id) {
       String sql = "SELECT id, name, age, phone, address FROM t_account WHERE id = ?";
       return JdbcTemplate.query(sql, new BeanHandler<>(Account.class), id).get(0);
  }

   @Override
   public List<Account> list() {
       String sql = "SELECT id, name, age, phone, address FROM t_account";
       return JdbcTemplate.query(sql, new BeanHandler<>(Account.class));
  }

}
6、单元测试

 

// dao.example.test.testDAO

package dao.example.test;

import org.junit.Test;

import dao.example.dao.impl.AccountDAOImpl;
import dao.example.domain.Account;

public class testDAO {

   @Test
   public void testSave() {
       new AccountDAOImpl().save(new Account(null, "张三", 25, "18819412345", "广州"));
  }

   @Test
   public void testDelete() {
       new AccountDAOImpl().delete(2L);
  }

   @Test
   public void testUpdate() {
       new AccountDAOImpl().update(new Account(2L, "李四", 27, "18819412345", "广州"));
  }

   @Test
   public void testGet() {
       System.out.println(new AccountDAOImpl().get(2L));
  }

   @Test
   public void testList() {
       System.out.println(new AccountDAOImpl().list());
  }

}
4、小结

  此次简单 JDBC 操作,我们需要知道在日后的开发中,需要对代码进行重构。抽取相同操作代码作为模板类,方便日后的维护。解决硬编码问题,将某些资源操作配置写入配置文件中。松耦合、解决硬编码、易维护都是开发当中重中之重。

三、druid大数据实时分析

一、Druid集群配置

我这里有两台机器,node1有32G内存,上面部署了Histotical Node和Coordinator Node;node2有72G内存,上面部署了其他四个服务。

海量数据实时OLAP分析系统-Druid.io安装配置和体验

##创建MySQL数据库

CREATE DATABASE `druid` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
grant all on druid.* to druid@’%’ identified by ‘druid1234′ WITH GRANT OPTION;
flush privileges;

##配置文件

cd $DRUID_HOME/config/_common vi common.runtime.properties(所有节点)

##使用Mysql存储元数据

druid.extensions.coordinates=["io.druid.extensions:druid-examples","io.druid.extensions:druid-kafka-eight", "io.druid.extensions:mysql-metadata-storage"]



##zookeeper

druid.zk.service.host=zkNode1:2181,zkNode2:2181,zkNode3:2181



##Mysql配置

druid.metadata.storage.type=mysql

druid.metadata.storage.connector.connectURI=jdbc:mysql://node1:3306/druid

druid.metadata.storage.connector.user=druid

druid.metadata.storage.connector.password=diurd1234



##配置deep storage到HDFS

druid.storage.type=hdfs

druid.storage.storageDirectory=hdfs://cdh5/tmp/druid/storage



##配置查询缓存,暂用本地,可配置memcached

druid.cache.type=local

druid.cache.sizeInBytes=10737418240



##配置监控

druid.monitoring.monitors=["com.metamx.metrics.JvmMonitor"]



##配置Indexing service的名字

druid.selectors.indexing.serviceName=druid/overlord



##

druid.emitter=logging
1、Overlord Node(Indexing Service)

在运行Overlord Node节点上:

cd $DRUID_HOME/config/overlord vi runtime.properties

druid.host=node2

druid.port=8090

druid.service=druid/overlord



# Only required if you are autoscaling middle managers

druid.indexer.autoscale.doAutoscale=true

druid.indexer.autoscale.strategy=ec2

druid.indexer.autoscale.workerIdleTimeout=PT90m

druid.indexer.autoscale.terminatePeriod=PT5M

druid.indexer.autoscale.workerVersion=0



# Upload all task logs to deep storage

druid.indexer.logs.type=hdfs

druid.indexer.logs.directory=hdfs://cdh5/tmp/druid/indexlog



# Run in remote mode

druid.indexer.runner.type=remote

druid.indexer.runner.minWorkerVersion=0



# Store all task state in the metadata storage

druid.indexer.storage.type=metadata
2、MiddleManager Node

在运行MiddleManager Node节点上: cd $DRUID_HOME/config/middleManager vi runtime.properties

druid.host=node2

druid.port=8091

druid.service=druid/middlemanager



druid.indexer.logs.type=hdfs

druid.indexer.logs.directory=hdfs://cdh5/tmp/druid/indexlog



# Resources for peons

druid.indexer.runner.javaOpts=-server -Xmx2g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

druid.indexer.task.baseTaskDir=/tmp/persistent/task/
3、 Coordinator Node

在运行Coordinator Node节点上: cd $DRUID_HOME/config/coordinator vi runtime.properties

druid.host=node1

druid.port=8081

druid.service=coordinator

druid.coordinator.startDelay=PT5M
4、Historical Node

在运行Historical Node节点上: cd $DRUID_HOME/config/historical vi runtime.properties

druid.host=node1

druid.port=8082

druid.service=druid/historical



druid.historical.cache.useCache=true

druid.historical.cache.populateCache=true



druid.processing.buffer.sizeBytes=1073741824

druid.processing.numThreads=9



druid.server.http.numThreads=9

druid.server.maxSize=300000000000



druid.segmentCache.locations=[{"path": " /tmp/druid/indexCache", "maxSize": 300000000000}]



druid.monitoring.monitors=["io.druid.server.metrics.HistoricalMetricsMonitor", "com.metamx.metrics.JvmMonitor"]
5、Broker Node

在运行Broker Node节点上: cd $DRUID_HOME/config/broker vi runtime.properties

druid.host=node2

druid.port=8092

druid.service=druid/broker



druid.broker.http.numConnections=20

druid.broker.http.readTimeout=PT5M



druid.processing.buffer.sizeBytes=2147483647

druid.processing.numThreads=11



druid.server.http.numThreads=20
6、Real-time Node

在运行Real-time Node节点上: cd $DRUID_HOME/config/realtime vi runtime.properties

druid.host=node2

druid.port=8093

druid.service=druid/realtime



druid.processing.buffer.sizeBytes=1073741824

druid.processing.numThreads=5



# Override emitter to print logs about events ingested, rejected, etc

druid.emitter=logging



druid.monitoring.monitors=["io.druid.segment.realtime.RealtimeMetricsMonitor", "com.metamx.metrics.JvmMonitor"]
二、Druid集群启动

首次启动时候,可以遵循下面的启动顺序。

1、Broker Node

cd $DRUID_HOME/ cp run_druid_server.sh run_broker.sh vi run_broker.sh

替换以下内容:

SERVER_TYPE=broker



# start process

JAVA_ARGS="${JAVA_ARGS} -Xmx10g -Xms5g -XX:NewSize=2g -XX:MaxNewSize=2g -XX:MaxDirectMemorySize=24g -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps"

JAVA_ARGS="${JAVA_ARGS} -Duser.timezone=GMT+8 -Dfile.encoding=UTF-8"

JAVA_ARGS="${JAVA_ARGS} -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Djava.io.tmpdir=/tmp/druid"

JAVA_ARGS="${JAVA_ARGS} -Dcom.sun.management.jmxremote.port=17071 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Ddruid.extensions.localRepository=${MAVEN_DIR}"

执行./run_broker.sh启动Broker Node:

海量数据实时OLAP分析系统-Druid.io安装配置和体验

2、Historical Node

cd $DRUID_HOME/ cp run_druid_server.sh run_historical.sh

vi run_historical.sh

替换以下内容:

SERVER_TYPE=historical



# start process

JAVA_ARGS="${JAVA_ARGS} -Xmx10g -Xms10g -XX:NewSize=2g -XX:MaxNewSize=2g -XX:MaxDirectMemorySize=16g -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps"

JAVA_ARGS="${JAVA_ARGS} -Duser.timezone=GMT+8 -Dfile.encoding=UTF-8"

JAVA_ARGS="${JAVA_ARGS} -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Djava.io.tmpdir=/tmp/druid"

JAVA_ARGS="${JAVA_ARGS} -Ddruid.extensions.localRepository=${MAVEN_DIR}"

执行命令./run_historical.sh启动Historical Node:

海量数据实时OLAP分析系统-Druid.io安装配置和体验

3、Coordinator Node

cd $DRUID_HOME/ cp run_druid_server.sh run_coordinator.sh vi run_coordinator.sh

替换以下内容:

SERVER_TYPE=coordinator



# start process

JAVA_ARGS="${JAVA_ARGS} -Xmx10g -Xms10g -XX:NewSize=512m -XX:MaxNewSize=512m -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps"

JAVA_ARGS="${JAVA_ARGS} -Duser.timezone=GMT+8 -Dfile.encoding=UTF-8"

JAVA_ARGS="${JAVA_ARGS} -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Djava.io.tmpdir=/tmp/druid"

JAVA_ARGS="${JAVA_ARGS} -Ddruid.extensions.localRepository=${MAVEN_DIR}"

执行命令./run_coordinator.sh启动Coordinator Node.

4、Middle Manager

cd $DRUID_HOME/ cp run_druid_server.sh run_middleManager.sh vi run_middleManager.sh

替换以下内容:

SERVER_TYPE=middleManager

# start process

JAVA_ARGS="${JAVA_ARGS} -Xmx64m -Xms64m -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Duser.timezone=GMT+8 -Dfile.encoding=UTF-8"

JAVA_ARGS="${JAVA_ARGS} -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Djava.io.tmpdir=/tmp/druid -Ddruid.extensions.localR

epository=${MAVEN_DIR}"

执行命令./run_middleManager.sh启动MiddleManager Node。

5、Overlord Node

cd $DRUID_HOME/ cp run_druid_server.sh run_overlord.sh vi run_overlord.sh

替换以下内容:

SERVER_TYPE=overlord

# start process

JAVA_ARGS="${JAVA_ARGS} -Xmx4g -Xms4g -XX:NewSize=256m -XX:MaxNewSize=256m -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps"

JAVA_ARGS="${JAVA_ARGS} -Duser.timezone=GMT+8 -Dfile.encoding=UTF-8"

JAVA_ARGS="${JAVA_ARGS} -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Djava.io.tmpdir=/tmp/druid"

JAVA_ARGS="${JAVA_ARGS} -Ddruid.extensions.localRepository=${MAVEN_DIR}"

执行命令./run_overlord.sh启动Overlord Node:

海量数据实时OLAP分析系统-Druid.io安装配置和体验

6、Real-time Node

cd $DRUID_HOME/ cp run_druid_server.sh run_realtime.sh vi run_realtime.sh 替换以下内容:

SERVER_TYPE=realtime



# start process

JAVA_ARGS="${JAVA_ARGS} -Xmx13g -Xms13g -XX:NewSize=2g -XX:MaxNewSize=2g -XX:MaxDirectMemorySize=9g -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -

XX:+PrintGCTimeStamps -XX:+HeapDumpOnOutOfMemoryError"

JAVA_ARGS="${JAVA_ARGS} -Duser.timezone=GMT+8 -Dfile.encoding=UTF-8"

JAVA_ARGS="${JAVA_ARGS} -Ddruid.realtime.specFile=/home/liuxiaowen/druid-0.8.1/examples/wikipedia/wikipedia_realtime.spec"

JAVA_ARGS="${JAVA_ARGS} -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Djava.io.tmpdir=/tmp/druid"

JAVA_ARGS="${JAVA_ARGS} -Dcom.sun.management.jmxremote.port=17072 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremot

e.ssl=false"

JAVA_ARGS="${JAVA_ARGS} -Ddruid.extensions.localRepository=${MAVEN_DIR}"

##特别需要注意参数:

-Ddruid.realtime.specFile=/home/liuxiaowen/druid-0.8.1/examples/wikipedia/wikipedia_realtime.spec

启动RealTime Node需要指定一个realtime数据源的配置文件,本文中使用example提供的wikipedia_realtime.spec,启动后,该数据源从irc.wikimedia.org获取实时数据。

关于RealTime Node的配置,后续文章将会详细介绍。

执行命令./run_realtime.sh启动RealTime Node。

三、Druid查询

第四部分中启动RealTime Node时候使用了例子中自带的配置文件wikipedia_realtime.spec,启动后,该RealTime Node会从irc.wikimedia.org获取实时数据,本章将以该数据源为例,学习几种最常见的查询。

1、select查询

首先编辑查询配置文件select_query.json

{

  "queryType": "select",

  "dataSource": "wikipedia",

  "dimensions":[],

  "metrics":[],

  "granularity": "all",

  "intervals": [

    "2015-11-01/2015-11-20"

  ],

  "pagingSpec":{"pagingIdentifiers": {}, "threshold":10}

}

该配置文件的含义是从数据源”wikipedia”进行select查询所有列,时间区间为2015-11-01/2015-11-20,每10条记录一个分页。

执行命令查询:

curl -X POST ‘http://node2:8093/druid/v2/?pretty’ -H ‘content-type: application/json’ -d @select_query.json

瞬间返回结果:

海量数据实时OLAP分析系统-Druid.io安装配置和体验

2、基于时间序列的查询Timeseries query

编辑查询配置文件timeseries.json

{

   "queryType": "timeseries",

   "dataSource": "wikipedia",

   "intervals": [ "2010-01-01/2020-01-01" ],

   "granularity": "minute",

   "aggregations": [

      {"type": "longSum", "fieldName": "count", "name": "edit_count"},

      {"type": "doubleSum", "fieldName": "added", "name": "chars_added"}

  ]

}

该配置文件的含义是:从数据源” wikipedia”中进行时间序列查询,区间为2010-01-01/2020-01-01,按分钟汇总结果,汇总字段为count和added;

执行查询命令:

curl -X POST ‘http://node2:8093/druid/v2/?pretty’ -H ‘content-type: application/json’ -d @timeseries.json

同样瞬间返回结果:

海量数据实时OLAP分析系统-Druid.io安装配置和体验

3、TopN查询

编辑查询文件topn.json

{

 "queryType": "topN",

 "dataSource": "wikipedia",

 "granularity": "all",

 "dimension": "page",

 "metric": "edit_count",

 "threshold" : 10,

 "aggregations": [

  {"type": "longSum", "fieldName": "count", "name": "edit_count"}

],

 "filter": { "type": "selector", "dimension": "country", "value": "United States" },

 "intervals": ["2012-10-01T00:00/2020-01-01T00"]

}

该文件含义是:从数据源” wikipedia”进行TopN查询,其中N=10,维度为page,指标为edit_count,也就是,在page维度上将edit_count汇总后取Top 10.

执行查询命令:

curl -X POST ‘http://node2:8093/druid/v2/?pretty’ -H ‘content-type: application/json’ -d @topn.json

结果为:

海量数据实时OLAP分析系统-Druid.io安装配置和体验

 

Druid目前已经有很多公司用于实时计算和实时OLAP,而且效果很好。虽然它的配置和查询都比较复杂和繁琐,但如果是真正基于海量数据的实时OLAP,它的威力还是很强大的。

第三章、druid面试

1、c3p0、dbcp、druid 三大连接池对比

img

2、Druid.io是“神马”?

Druid.io是一个开源的,分布式的,列式存储的,适用于实时数据分析的OLAP系统。它能够快速聚合、灵活过滤、毫秒级查询、和低延迟数据导入。2011年,MetaMarkets公司为了解决广告交易中海量实时数据的分析问题,在尝试各种SQL和NoSQL方案后,决定自行设计并创建Druid并于2013年开源。Druid被设计成支持PB级别数据量每天处理数十亿流式事件。Druid之所以保持高效有以下几个原因:

数据进行了有效的预聚合或预计算。 数据结果的优化,应用了Bitmap的压缩算法。 可扩展的高可用架构,灵活的支持部署和扩展。 社区的力量,Druid开发和用户社区保持活跃,不断推动Druid完善和改进。

3、Druid集群介绍

关于Druid的架构,我们先通过其总体设计架构图来做一个概要了解,如下图所示:

image

实时节点(RealTime Node):即时摄入数据,以及生成Segment数据文件。 历史节点(Historical Node):加载已经生成好的数据文件,以供数据查询。 查询节点(Broker Node):对外提供数据查询服务,并同时从实时节点与历史节点查询数据,合并后返回给调用方。 协调节点(Coordinator Node):负责历史节点数据负载均衡,以及通过规则(Rule)来管理数据的生命周期。 集群的外部依赖如下图所示:

image

元数据库(Metastore):存储Druid集群的元数据信息,比如Segment的相关信息,一般用MySQL或者PostgreSQL。 分布式协调服务:为Druid集群提供一致性协调服务组件,通常为Zookeeper。 数据文件存储(DeepStorage):存放生成的Segment数据文件,并提供历史节点下载。对于单节点集群可以是本地磁盘,而对于分布式集群一般是HDFS或NFS。

1、Druid实时节点

实时节点主要负责即时摄入数据,以及生成Segment文件,其独到的设计使其拥有超强的数据摄入速度。

image

Segment数据文件从生成到传播需要经历一个完整的流程,步骤如下:

实时节点生产出Segment数据文件,并将其上传到DeepStorage中。 Segment数据文件的相关元数据信息被存储到MySQL中。实时节点转存的Segment会在ZooKeeper中新增一条记录 Master节点(即Coordinator几点)从MetaStore里得知Segment数据文件的相关元数据信息后,将其根据规则的设置分配符合条件的历史节点。 历史节点得到指令会主动从DeepStorage中拉取指定的Segment数据文件,并通过Zookeeper向集群申明其负责提供该Segment数据文件的查询服务。 实时节点丢弃该Segment数据文件,并向集群申明其不再提供该Segment数据文件的查询服务。

2、Druid历史节点

历史节点用于负责加载已经生成好的数据文件以提供数据查询。由于Druid的数据文件有不可更改性,因此历史节点的工作就是专注于提供数据查询。

image

Coordinator在ZK与History节点相关联的加载队列路径下创建一个临时记录。 当历史节点发现在Zookeeper中有需要加载的新的记录。它首先检查本地磁盘目录(缓存)中关于新的Segment的信息。如果缓存中没有关于新的Segment的信息,历史节点将下载新的Segment的元数据信息并告知Zookeeper。元数据包含新的Segment在“Deep Storage”中的存储位置,怎样去解压缩和处理新的Segment的信息。 一旦历史节点处理完一个Segment,就公布该Segment可查询。

3、Druid查询节点

查询节点对外提供数据查询服务,并同时从实时节点与历史节点查询数据,合并后返回给调用方。Zookeeper维护有关历史和实时的节点信息和它们所能提供服务的Segment。当收某个数据源和时间的查询,代理节点执行查找与查询数据源时间相关的时间轴和检索包含数据查询的节点。代理节点将查询转发到所选节点。

image

4、Druid协调节点

协调节点负责历史节点的数据负载均衡,以及通过规则管理数据的生命周期。Druid针对每个DataSource设置规则来加载或者丢弃具体的数据文件,以管理数据生命周期。可以对一个DataSource按照顺序添加多条规则,对于一个Segment数据文件来说,协调节点会逐条检查规则,当碰到当前Segment数据文件符合某条规则的时候,协调节点会立即命令历史节点堆该Segment数据文件执行这条规则——加载或者丢弃,并停止检查余下的规则,否则继续检查下一条规则。

5、Druid索引服务

除了通过实时节点产生出Segment数据文件外,Druid还提供一组名为索引服务的组件。不同于实时节点的单点模式,索引服务实际包含一组组件,并以主从结构作为其架构方式,其中统治节点(Overlord Node)为主节点,而中间管理者(Middle Manager)为从节点。索引节点服务架构示意图如下所示:

image

索引服务是主从结构,由三个部分组成:

peon组件:在一个单独的jvm中运行单个任务,通过单独的jvm对任务做资源隔离和日志隔离。 Middle Manager:用于创建和管理peon的中层管理组件 overlord组件:管理任务分配到Middle Manager

 posted on 2020-05-11 18:47  大码王  阅读(892)  评论(0编辑  收藏  举报
复制代码