从Spec编写深入了解 Druid

代码银月使
• 阅读 3366
本文参考 Druid 官方文档。

Apache Druid 是一个集时间序列数据库、数据仓库和全文检索系统特点于一体的分析性数据平台(OLAP)。Druid 作为一个高可用、高性能和多特性的 OLAP 平台,使用场景丰富。

从Spec编写深入了解 Druid

许多互联网公司基于 Druid 搭建 OLAP 数据分析和 BI 平台。如:

此前 Druid 系列文章已经详解过 Druid 的特性、使用场景、架构和实现原理。可以参考:

本文将指导读者完整定义一个完整 Spec,并指出关键注意事项。Spec 是 Druid 数据摄入的配置信息,使用 json 格式,使用 Druid 时可以通过界面配置,最后生成 Spec 文件,也可以直接编写 Spec 文件,然后上传配置。无论使用哪种方式,深入了解 Spec 的编写既是开始使用 Druid 的第一步,也是深入了解 Druid 各种概念,继而深入了解 Druid 原理的必经之路。

示例数据

假设我们有以下网络流量数据:

  • srcIP: 发送端 IP 地址
  • srcPort: 发送端端口号
  • dstIP: 接收端 IP 地址
  • dstPort: 接收端端口号
  • protocol: IP 协议号
  • packets: 传输的包的数量
  • bytes: 传输字节数
  • cost: 传输耗费的时间
{"ts":"2018-01-01T01:01:35Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2", "srcPort":2000, "dstPort":3000, "protocol": 6, "packets":10, "bytes":1000, "cost": 1.4}
{"ts":"2018-01-01T01:01:51Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2", "srcPort":2000, "dstPort":3000, "protocol": 6, "packets":20, "bytes":2000, "cost": 3.1}
{"ts":"2018-01-01T01:01:59Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2", "srcPort":2000, "dstPort":3000, "protocol": 6, "packets":30, "bytes":3000, "cost": 0.4}
{"ts":"2018-01-01T01:02:14Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2", "srcPort":5000, "dstPort":7000, "protocol": 6, "packets":40, "bytes":4000, "cost": 7.9}
{"ts":"2018-01-01T01:02:29Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2", "srcPort":5000, "dstPort":7000, "protocol": 6, "packets":50, "bytes":5000, "cost": 10.2}
{"ts":"2018-01-01T01:03:29Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2", "srcPort":5000, "dstPort":7000, "protocol": 6, "packets":60, "bytes":6000, "cost": 4.3}
{"ts":"2018-01-01T02:33:14Z","srcIP":"7.7.7.7", "dstIP":"8.8.8.8", "srcPort":4000, "dstPort":5000, "protocol": 17, "packets":100, "bytes":10000, "cost": 22.4}
{"ts":"2018-01-01T02:33:45Z","srcIP":"7.7.7.7", "dstIP":"8.8.8.8", "srcPort":4000, "dstPort":5000, "protocol": 17, "packets":200, "bytes":20000, "cost": 34.5}
{"ts":"2018-01-01T02:35:45Z","srcIP":"7.7.7.7", "dstIP":"8.8.8.8", "srcPort":4000, "dstPort":5000, "protocol": 17, "packets":300, "bytes":30000, "cost": 46.3}

将上面 JSON 内容保存到 $druid_root\quickstart\目录下的 ingestion-tutorial-data.json 文件中。

下面我们开始编写一个 Spect 将上面的数据写入 Druid。

在本教程中,我们将使用本地批处理indexing任务。如果使用其他任务类型,摄入规范的某些地方将会不一样,我们将在教程中指出。

下面将详细讲解 Spec 配置,你将了解以下内容:

从Spec编写深入了解 Druid

定义 schema

Druid 摄入 spec 的核心元素是 dataSchemadataSchema 定义如何将输入的数据解析成 Druid 能够存储的列集合。

我们从一个空的dataSchema 开始,并按教程一步步添加字段。

quickstart/ 目录下创建 ingestion-tutorial-index.json 文件,将以下内容写入文件:

"dataSchema" : {}

随着教程的进行,我们将不断的修改此 spec 文件。

数据源名称

数据源名称通过dataSchema 下的 dataSource 参数指定。dataSource 类似于 RDBMS 的 Table Name,写入的数据通过此名称查询,如:select * from $dataSource

"dataSchema" : {
  "dataSource" : "ingestion-tutorial",
}

让我们将教程中的数据源命名为 ingestion-tutorial

时间列

dataSchema 需要知道如何从输入的数据中提取主时间字段。Druid 的数据必须有时间字段,Druid 底层按时间分 segment 来存储数据,详情可以参考《Apache Druid 的集群设计与工作流程》

我们数据中的时间戳列是"ts",它是一个 ISO 8601 规范的时间戳,我们将配置此字段的 timestampSpec信息加到 dataSchema 下:

"dataSchema" : {
  "dataSource" : "ingestion-tutorial",
  "timestampSpec" : {
    "format" : "iso",
    "column" : "ts"
  }
}

列类型

现在,我们已经定义了时间列,让我们看一下其他列的定义。

Druid 支持以下列类型:String,Long,Float,Double。下面章节中我们将看到这些类型如何被使用。

在我们讲如何定义其他非时间列之前,先讨论一下 rollup

Rollup

在摄入数据时,我们需要考虑是否需要 rollup。

  • 如果开启 rollup,需要将输入数据列分成两种类型,维度(dimension)和指标(metric)。维度是 rollup 的 grouping 列(用于 group by,filtering),指标是被聚合计算的列。
  • 如果不开启 rollup,所有列都被视为维度,将不会进行预聚合。

在此教程中,我们开启 rollup。在 dataSchemagranularitySpec中指定:

"dataSchema" : {
 "dataSource" : "ingestion-tutorial",
 "timestampSpec" : {
   "format" : "iso",
   "column" : "ts"
 },
 "granularitySpec" : {
   "rollup" : true
 }
}

选择维度和指标

在此教程中,我们按以下方式划分维度列和指标列:

  • Dimensions: srcIP, srcPort, dstIP, dstPort, protocol
  • Metrics: packets, bytes, cost

这些维度是一组属性,用以标识一组网络流量数据,而指标代表按此维度组合的网络流量的实际情况。

让我们看看如何在 spec 中定义维度和指标吧。

维度

维度由 dataSchema 中的 dimensionsSpec 参数指定。

"dataSchema" : {
  "dataSource" : "ingestion-tutorial",
  "timestampSpec" : {
    "format" : "iso",
    "column" : "ts"
  },
  "dimensionsSpec" : {
    "dimensions": [
      "srcIP",
      { "name" : "srcPort", "type" : "long" },
      { "name" : "dstIP", "type" : "string" },
      { "name" : "dstPort", "type" : "long" },
      { "name" : "protocol", "type" : "string" }
    ]
  },
  "granularitySpec" : {
    "rollup" : true
  }
}

每个维度都有一个 name 和 一个 type ,其中 type 可以是"long", "float", "double", or "string"。

注意,srcIP 是一个 "string" 维度。对于字符串维度,只需要指定维度的名称就可以了,因为它的类型默认为"string"。

也请注意, protocol 在输入数据中是数字类型,但我们以 "string" 列类型提取它,所以 Druid 在摄入数据时会将其强制由 long 类型转换成 string 类型。

Strings vs Numbers

数字类型的数据应该作为数字维度还是字符串维度?

数字维度相对于字符串维度有以下优势和劣势:

  • 优势:数字需要更小的存储空间,并且在读取该列时需要更小的开销。
  • 劣势:数字维度没有索引,所以按此列 filter 的操作会比字符串类型的维度(这种维度有索引)更慢。

指标

指标通过 dataSchema 中的 metricsSpec 参数指定:

"dataSchema" : {
  "dataSource" : "ingestion-tutorial",
  "timestampSpec" : {
    "format" : "iso",
    "column" : "ts"
  },
  "dimensionsSpec" : {
    "dimensions": [
      "srcIP",
      { "name" : "srcPort", "type" : "long" },
      { "name" : "dstIP", "type" : "string" },
      { "name" : "dstPort", "type" : "long" },
      { "name" : "protocol", "type" : "string" }
    ]
  },
  "metricsSpec" : [
    { "type" : "count", "name" : "count" },
    { "type" : "longSum", "name" : "packets", "fieldName" : "packets" },
    { "type" : "longSum", "name" : "bytes", "fieldName" : "bytes" },
    { "type" : "doubleSum", "name" : "cost", "fieldName" : "cost" }
  ],
  "granularitySpec" : {
    "rollup" : true
  }
}

在定义指标时,必须指定当前列在 rollup 时应该执行的聚合类型。

这里,我们在 packetsbytes 两个指标列上定义了 long 类型的 sum 聚合,在 cost 列上定义了一个 double 的 sum 聚合。

注意 metricsSpecdimensionSpecparseSpec 的嵌套层级不一样。它和 dataSchema 中的 parser在同一嵌套层级。

注意,我们也定义了一个 count 聚合器。这个计数聚合器将统计原始数据摄入的行数。

No rollup

如果我们不使用 rollup,将在 dimensionsSpec 中指定所有列,如:

      "dimensionsSpec" : {
        "dimensions": [
          "srcIP",
          { "name" : "srcPort", "type" : "long" },
          { "name" : "dstIP", "type" : "string" },
          { "name" : "dstPort", "type" : "long" },
          { "name" : "protocol", "type" : "string" },
          { "name" : "packets", "type" : "long" },
          { "name" : "bytes", "type" : "long" },
          { "name" : "srcPort", "type" : "double" }
        ]
      },

定义粒度

此时,我们已经完成了 parsermetricSpec 的定义,并几乎要完成 Spec 的 dataSchema

我们还需要在 granularitySpec 中设置一些额外的参数:

  • granularitySpec 类型:支持两种类型——uniformarbitrary 。在本教程中,我们将使用 uniform,这样所有的 segment 将有统一的时间范围大小(本示例中,所有 segment 覆盖一个小时的数据量)。
  • segment 粒度:设置单个 segment 应该包含多大时间范围的数据,如:DAYWEEK
  • 时间列中时间戳的 buckting 粒度(称为查询粒度 queryGranularity )。

Segment 粒度

segment 粒度通过 granularitySpec 中的 segmentGranularity 属性配置。此文档中,我们将创建 hourly 粒度的 segment:

"dataSchema" : {
  "dataSource" : "ingestion-tutorial",
  "timestampSpec" : {
    "format" : "iso",
    "column" : "ts"
  },
  "dimensionsSpec" : {
    "dimensions": [
      "srcIP",
      { "name" : "srcPort", "type" : "long" },
      { "name" : "dstIP", "type" : "string" },
      { "name" : "dstPort", "type" : "long" },
      { "name" : "protocol", "type" : "string" }
    ]
  },
  "metricsSpec" : [
    { "type" : "count", "name" : "count" },
    { "type" : "longSum", "name" : "packets", "fieldName" : "packets" },
    { "type" : "longSum", "name" : "bytes", "fieldName" : "bytes" },
    { "type" : "doubleSum", "name" : "cost", "fieldName" : "cost" }
  ],
  "granularitySpec" : {
    "type" : "uniform",
    "segmentGranularity" : "HOUR",
    "rollup" : true
  }
}

我们的输入数据包含两个小时的事件,所以此任务将生成两个 segment。

查询粒度

查询粒度通过 granularitySpec 中的 queryGranularity 属性配置。此教程中,我们使用 minute 级粒度:

"dataSchema" : {
  "dataSource" : "ingestion-tutorial",
  "timestampSpec" : {
    "format" : "iso",
    "column" : "ts"
  },
  "dimensionsSpec" : {
    "dimensions": [
      "srcIP",
      { "name" : "srcPort", "type" : "long" },
      { "name" : "dstIP", "type" : "string" },
      { "name" : "dstPort", "type" : "long" },
      { "name" : "protocol", "type" : "string" }
    ]
  },
  "metricsSpec" : [
    { "type" : "count", "name" : "count" },
    { "type" : "longSum", "name" : "packets", "fieldName" : "packets" },
    { "type" : "longSum", "name" : "bytes", "fieldName" : "bytes" },
    { "type" : "doubleSum", "name" : "cost", "fieldName" : "cost" }
  ],
  "granularitySpec" : {
    "type" : "uniform",
    "segmentGranularity" : "HOUR",
    "queryGranularity" : "MINUTE",
    "rollup" : true
  }
}

为了查看查询粒度配置的效果,让我们从原始输入数据中查看这一行:

{"ts":"2018-01-01T01:03:29Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2", "srcPort":5000, "dstPort":7000, "protocol": 6, "packets":60, "bytes":6000, "cost": 4.3}

当使用 minute 粒度摄入这行数据时,Druid 将把这行数据的时间戳 floor 成 minute 桶的时间:

{"ts":"2018-01-01T01:03:00Z","srcIP":"1.1.1.1", "dstIP":"2.2.2.2", "srcPort":5000, "dstPort":7000, "protocol": 6, "packets":60, "bytes":6000, "cost": 4.3}

定义时间范围

对于批量任务,必须定义时间范围。在时间范围之外的输入数据将不被摄入。

这个时间范围也在 granularitySpec 中指定:

"dataSchema" : {
  "dataSource" : "ingestion-tutorial",
  "timestampSpec" : {
    "format" : "iso",
    "column" : "ts"
  },
  "dimensionsSpec" : {
    "dimensions": [
      "srcIP",
      { "name" : "srcPort", "type" : "long" },
      { "name" : "dstIP", "type" : "string" },
      { "name" : "dstPort", "type" : "long" },
      { "name" : "protocol", "type" : "string" }
    ]
  },
  "metricsSpec" : [
    { "type" : "count", "name" : "count" },
    { "type" : "longSum", "name" : "packets", "fieldName" : "packets" },
    { "type" : "longSum", "name" : "bytes", "fieldName" : "bytes" },
    { "type" : "doubleSum", "name" : "cost", "fieldName" : "cost" }
  ],
  "granularitySpec" : {
    "type" : "uniform",
    "segmentGranularity" : "HOUR",
    "queryGranularity" : "MINUTE",
    "intervals" : ["2018-01-01/2018-01-02"],
    "rollup" : true
  }
}

定义任务类型

现在我们已经完成了 dataSchema 的定义。接下来要做的就是将我们创建的 dataSchema 放入一个数据摄入任务中,并指定输入的源。

dataSchema 适用于所有类型的任务,但每种任务类型都有自生的规范格式。在本教程中,我们使用本地批量数据摄入任务类型(the native ingestion task):

{
  "type" : "index_parallel",
  "spec" : {
    "dataSchema" : {
      "dataSource" : "ingestion-tutorial",
      "timestampSpec" : {
        "format" : "iso",
        "column" : "ts"
      },
      "dimensionsSpec" : {
        "dimensions": [
          "srcIP",
          { "name" : "srcPort", "type" : "long" },
          { "name" : "dstIP", "type" : "string" },
          { "name" : "dstPort", "type" : "long" },
          { "name" : "protocol", "type" : "string" }
        ]
      },
      "metricsSpec" : [
        { "type" : "count", "name" : "count" },
        { "type" : "longSum", "name" : "packets", "fieldName" : "packets" },
        { "type" : "longSum", "name" : "bytes", "fieldName" : "bytes" },
        { "type" : "doubleSum", "name" : "cost", "fieldName" : "cost" }
      ],
      "granularitySpec" : {
        "type" : "uniform",
        "segmentGranularity" : "HOUR",
        "queryGranularity" : "MINUTE",
        "intervals" : ["2018-01-01/2018-01-02"],
        "rollup" : true
      }
    }
  }
}

定义输入源

现在,让我们来定义我们自己的输入源,它在 ioConfig 对象中指定。每种任务类型都有自己的 ioConfig 类型。为了读取输入数据,我们需要指定一个 inputSource。我们之前保存的网络流量数据需要从一个本地文件读取,其配置如下:

    "ioConfig" : {
      "type" : "index_parallel",
      "inputSource" : {
        "type" : "local",
        "baseDir" : "quickstart/",
        "filter" : "ingestion-tutorial-data.json"
      }
    }

定义数据格式

因为我们的数据是 JSON 字符串形式的,我们使用 inputFormat json 格式化数据(还支持 csv、protobuf 等数据类型):

    "ioConfig" : {
      "type" : "index_parallel",
      "inputSource" : {
        "type" : "local",
        "baseDir" : "quickstart/",
        "filter" : "ingestion-tutorial-data.json"
      },
      "inputFormat" : {
        "type" : "json"
      }
    }
{
  "type" : "index_parallel",
  "spec" : {
    "dataSchema" : {
      "dataSource" : "ingestion-tutorial",
      "timestampSpec" : {
        "format" : "iso",
        "column" : "ts"
      },
      "dimensionsSpec" : {
        "dimensions": [
          "srcIP",
          { "name" : "srcPort", "type" : "long" },
          { "name" : "dstIP", "type" : "string" },
          { "name" : "dstPort", "type" : "long" },
          { "name" : "protocol", "type" : "string" }
        ]
      },
      "metricsSpec" : [
        { "type" : "count", "name" : "count" },
        { "type" : "longSum", "name" : "packets", "fieldName" : "packets" },
        { "type" : "longSum", "name" : "bytes", "fieldName" : "bytes" },
        { "type" : "doubleSum", "name" : "cost", "fieldName" : "cost" }
      ],
      "granularitySpec" : {
        "type" : "uniform",
        "segmentGranularity" : "HOUR",
        "queryGranularity" : "MINUTE",
        "intervals" : ["2018-01-01/2018-01-02"],
        "rollup" : true
      }
    },
    "ioConfig" : {
      "type" : "index_parallel",
      "inputSource" : {
        "type" : "local",
        "baseDir" : "quickstart/",
        "filter" : "ingestion-tutorial-data.json"
      },
      "inputFormat" : {
        "type" : "json"
      }
    }
  }
}

其他调整

每一个摄取任务都有 tuningConfig 配置项,它允许用户调整各种摄取参数。

举例来说,让我们添加一个tuningConfig,以设置本次批量摄取任务的目标 segment 大小:

    "tuningConfig" : {
      "type" : "index_parallel",
      "maxRowsPerSegment" : 5000000
    }

最终 spec

现在我们已经定义完成了一个摄取规范,它现在看起来如下所示:

{
  "type" : "index_parallel",
  "spec" : {
    "dataSchema" : {
      "dataSource" : "ingestion-tutorial",
      "timestampSpec" : {
        "format" : "iso",
        "column" : "ts"
      },
      "dimensionsSpec" : {
        "dimensions": [
          "srcIP",
          { "name" : "srcPort", "type" : "long" },
          { "name" : "dstIP", "type" : "string" },
          { "name" : "dstPort", "type" : "long" },
          { "name" : "protocol", "type" : "string" }
        ]
      },
      "metricsSpec" : [
        { "type" : "count", "name" : "count" },
        { "type" : "longSum", "name" : "packets", "fieldName" : "packets" },
        { "type" : "longSum", "name" : "bytes", "fieldName" : "bytes" },
        { "type" : "doubleSum", "name" : "cost", "fieldName" : "cost" }
      ],
      "granularitySpec" : {
        "type" : "uniform",
        "segmentGranularity" : "HOUR",
        "queryGranularity" : "MINUTE",
        "intervals" : ["2018-01-01/2018-01-02"],
        "rollup" : true
      }
    },
    "ioConfig" : {
      "type" : "index_parallel",
      "inputSource" : {
        "type" : "local",
        "baseDir" : "quickstart/",
        "filter" : "ingestion-tutorial-data.json"
      },
      "inputFormat" : {
        "type" : "json"
      }
    },
    "tuningConfig" : {
      "type" : "index_parallel",
      "maxRowsPerSegment" : 5000000
    }
  }
}

提交任务并查询数据

在包根目录下,运行下面命令:

bin/post-index-task --file quickstart/ingestion-tutorial-index.json --url http://localhost:8081

脚本执行完成后,我们来查询数据。

让我们运行 bin/dsql 并发送一个 select * from "ingestion-tutorial"; 语句,查询已经被写入的数据:

$ bin/dsql
Welcome to dsql, the command-line client for Druid SQL.
Type "\h" for help.
dsql> select * from "ingestion-tutorial";

┌──────────────────────────┬───────┬──────┬───────┬─────────┬─────────┬─────────┬──────────┬─────────┬─────────┐
│ __time                   │ bytes │ cost │ count │ dstIP   │ dstPort │ packets │ protocol │ srcIP   │ srcPort │
├──────────────────────────┼───────┼──────┼───────┼─────────┼─────────┼─────────┼──────────┼─────────┼─────────┤
│ 2018-01-01T01:01:00.000Z │  6000 │  4.9 │     3 │ 2.2.2.2 │    3000 │      60 │ 6        │ 1.1.1.1 │    2000 │
│ 2018-01-01T01:02:00.000Z │  9000 │ 18.1 │     2 │ 2.2.2.2 │    7000 │      90 │ 6        │ 1.1.1.1 │    5000 │
│ 2018-01-01T01:03:00.000Z │  6000 │  4.3 │     1 │ 2.2.2.2 │    7000 │      60 │ 6        │ 1.1.1.1 │    5000 │
│ 2018-01-01T02:33:00.000Z │ 30000 │ 56.9 │     2 │ 8.8.8.8 │    5000 │     300 │ 17       │ 7.7.7.7 │    4000 │
│ 2018-01-01T02:35:00.000Z │ 30000 │ 46.3 │     1 │ 8.8.8.8 │    5000 │     300 │ 17       │ 7.7.7.7 │    4000 │
└──────────────────────────┴───────┴──────┴───────┴─────────┴─────────┴─────────┴──────────┴─────────┴─────────┘
Retrieved 5 rows in 0.12s.

dsql>

如果觉得阅读后对你有帮助,希望分享、点赞、在看三连哦。

关注 【码哥字节】解锁更多硬核。

推荐阅读

以下几篇文章阅读量与读者反馈都很好,推荐大家阅读:

公众号后台回复 ”加群“,加入读者技术群,里面有阿里、腾讯的小伙伴一起探讨技术。

从Spec编写深入了解 Druid

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Stella981 Stella981
3年前
Druid连接池简单入门配置
偶尔的机会解释Druid连接池,后起之秀,但是评价不错,另外由于是阿里淘宝使用过的所以还是蛮看好的。Druid集连接池,监控于一体整好复合当前项目的需要,项目是ssh结构,之前是用C3p0的,现在换一个连接池也是很简单的,首先spring配置DataSource,配置如下:<bean id"dataSource" class"co
Stella981 Stella981
3年前
Kylin、Druid、ClickHouse 核心技术对比
文章作者:吴建超内容来源:jackywoo.cn导读:Kylin、Druid、ClickHouse是目前主流的OLAP引擎,本文尝试从数据模型和索引结构两个角度,分析这几个引擎的核心技术,并做简单对比。在阅读本文之前希望能对Kylin、Druid、ClickHouse有所理解。01Kylin数据模型
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
SpringBoot2 学习 集成Druid配置
Mavenspring.datasource.druid.webstatfilter.principalsessionnamesession_name测试http://localhost:9081/mixmall/druid/index.html————————————————版权
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Easter79 Easter79
3年前
SpringBoot2 学习 集成Druid配置
Mavenspring.datasource.druid.webstatfilter.principalsessionnamesession_name测试http://localhost:9081/mixmall/druid/index.html————————————————版权
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这