TDengine时序数据库通俗易懂教程
作者:mmseoamin日期:2023-12-13


1.TDengine简介

1.1产品简介

TDengine 是一款开源、高性能、云原生的时序数据库,且针对物联网、车联网、工业互联网、金融、IT运维等场景进行了优化。TDengine的代码,包括集群功能,都在 GNU AGPL v3.0 下开源。除核心的时序数据库功能外,TDengine还提供缓存、数据订阅、流式计算等其它功能以降低系统复杂度及研发和运维成本。

1.2主要功能
  • 1.数据写入,支持标准sql

    • 2.查询支持,支持标准sql

      • 3.缓存,将每张表的最后一条记录缓存起来,这样无需Redis就能对时序数据进行高效处理

        • 4.流式计算(Stream Processing),支持对实时写入的数据进行预处理

          • 5.数据订阅,应用程序可以订阅一张表或一组表的数据,提供与 Kafka 相同的 API,而且可以指定过滤条件

            • 6.可视化,支持与 Grafana 的无缝集成形成监控报警系统

              • 7.集群,可以通过增加节点进行水平扩展以提升处理能力,通过多副本提供高可用能力

                • 8.编程

                  • 提供各种语言的连接器(Connector): C/C++、Java、Go、Node.js、Rust、Python、C# 等

                    • 支持 REST 接口

                      2.基本概念

                      下面用一组证券code来举例说明

                      code

                      timestamp

                      开盘价

                      最高价

                      最低价

                      最新价

                      市场

                      种类

                      113637.SZ

                      2023-02-27 15:35:00.000

                      10

                      12

                      9.9

                      11

                      深市

                      股票

                      113637.SZ

                      2023-02-27 15:35:03.000

                      10

                      12

                      9.8

                      10.5

                      深市

                      股票

                      000001.SH

                      2023-02-27 15:35:00.000

                      6

                      7

                      5

                      6.6

                      沪市

                      指数

                      000001.SH

                      2023-02-27 15:35:03.000

                      6

                      7

                      5

                      6.5

                      沪市

                      指数

                      上面是两只code的几笔快照数据,可以说是按照时间存储在一张表里,其中一些字段是变化的,比如开盘,最高,最低,最新,而市场和种类确实不变的,如果我们把每只code看成一个采集点,每条记录都是该采集点的数据,那么开盘,最新等字段可以看成采集量,种类和市场则是标签静态属性。下面看一些概念:

                      2.1 采集点

                      采集点顾名思义采集数据的点,可以按照一定时间周期进行数据采集,并进行存储,一个采集点对应的就是一张表,该表数据只存储该采集点的数据,其中存储的就是采集量

                      2.2 采集量

                      采集量就是采集点采集的数据,采集量对应的就是表中的字段

                      2.3 标签

                      标签则是描述采集点的静态属性,在初始创建表时指出,后续可以进行增删改查

                      2.4 表

                      类似mysql数据库表一样,也是由字段组成,存储一条条记录,每条记录都含有时间戳

                      2.5 超级表

                      上述所示,比如把每一个code都看成一个采集点,那么一只code对应的就是一张表,该表所储存的就是code对应的每条快照数据,超级表就是同一种类型的表的集合,用来存储同一类型的表的,由采集量和标签组成。

                      2.6 子表

                      子表是通过超级表的模板进行创建的,在创建时指出静态标签属性,其存储的数据就是超级表的采集量字段。

                      • 1.一张子表属于一个超级表,一个超级表包含多个子表

                        • 2.同一个超级表的子表具有相同的模式结构,也就是采集量和标签结构一致

                          • 3.无法通过修改子表的结构来更改超级表,对超级表的结构该表会影响多有的子表

                            • 4.子表其实也是普通表,同样可以进行sql操作,不过多了一些静态标签,二者无法相互转化

                              2.7 FQDN & Endpoint

                              FQDN(Fully Qualified Domain Name,完全限定域名)是 Internet 上特定计算机或主机的完整域名。FQDN 由两部分组成:主机名和域名。例如,假设邮件服务器的 FQDN 可能是 mail.tdengine.com。主机名是 mail,主机位于域名 tdengine.com 中。DNS(Domain Name System),负责将 FQDN 翻译成 IP,是互联网应用的寻址方式。对于没有 DNS 的系统,可以通过配置 hosts 文件来解决。

                              TDengine 集群的每个节点是由 Endpoint 来唯一标识的,Endpoint 是由 FQDN 外加 Port 组成,比如 h1.tdengine.com:6030。这样当 IP 发生变化的时候,我们依然可以使用 FQDN 来动态找到节点,不需要更改集群的任何配置。而且采用 FQDN,便于内网和外网对同一个集群的统一访问。

                              3.快速构建

                              TDengine 完整的软件包包括服务端(taosd)、应用驱动(taosc)、用于与第三方系统对接并提供 RESTful 接口的 taosAdapter、命令行程序(CLI,taos)和一些工具软件。目前 TDinsight 仅在 Linux 系统上安装和运行,后续将支持 Windows、macOS 等系统。TDengine 除了提供多种语言的连接器之外,还通过 taosAdapter 提供 RESTful 接口。

                              tdengine分为服务端和客户端,并且都集成了命令行工具TDEngine CLI来连接服务端进行数据库操作,像mysql一样需要先登录,一般来说,服务端远程安装在linux,本机windows安装在客户端。

                              linux服务端安装:

                              TDengine-server-3.0.2.6-Linux-x64.tar.gz (84.4 M)

                              点击进入官网tar格式进行下载安装,下载后上传服务器进行解压

                              tar -zxvf TDengine-server--Linux-x64.tar.gz

                              解压完成后进入目录,运行下面进行安装

                              sudo ./install.sh
                              当安装第一个节点时,出现 Enter FQDN: 提示的时候,不需要输入任何内容。只有当安装第二个或以后更多的节点时,才需要输入已有集群中任何一个可用节点的 FQDN,支持该新节点加入集群。当然也可以不输入,而是在新节点启动前,配置到新节点的配置文件中。

                              启动服务:

                              systemctl start taosd

                              查看状态:

                              systemctl status taosd
                              //返回结果如下:
                              Active: active (running) 运行
                              Active: inactive (dead)  不运行

                              停止或者重启运行:

                              systemctl stop taosd
                              systemctl restart taosd

                              输入taos进入命令行格式,像mysql一样,进入后如下:

                              taos>

                              通过alter命令进行密码修改,对于TDengine数据库,默认ip为主机ip,端口号为6030,用户名为root,登录密码为taosdata,可在/etc/taos/taos.cfg配置文件中进行修改firstEnp端口号.

                              修改密码:

                              alter user root pass 'your password';

                              修改后可通过登录,服务端登录只需要用户名,密码即可

                              taos -u root -p 回车输入密码
                              对于远程客户端登录可通过指定主机ip,端口号进行登录
                              taos -h 主机ip -P 端口号 -u root -p
                              windows客户端安装:

                              下载链接:

                              TDengine-client-3.0.2.6-Windows-x64.exe (22.1 M)

                              找到这个windows安装包

                              对于客户端,直接下载运行安装,启动桌面客户端程序,进行到命令行进行登录,输入远程服务端ip和端口号,如下所示:

                              taos -h ip -P port -u root -p
                              回车输入密码即可

                              客户端默认连接本机Ip和6030端口号,可以修改C:\TDengine\cfg\taos.cfg配置firstEnp为远程服务端ip:port作为启动连接的第一个数据节点。

                              4.基本语法

                              TDEngine数据库是支持标准SQL的,所以大多数语法和mysql一致,除了创建超级表和子表较为特殊。

                              TDengine支持的数据类型以及对应的java格式

                              TDengine DataType

                              JDBCType

                              TIMESTAMP

                              java.sql.Timestamp

                              INT

                              java.lang.Integer

                              BIGINT

                              java.lang.Long

                              FLOAT

                              java.lang.Float

                              DOUBLE

                              java.lang.Double

                              SMALLINT

                              java.lang.Short

                              TINYINT

                              java.lang.Byte

                              BOOL

                              java.lang.Boolean

                              BINARY

                              byte array or String

                              NCHAR

                              java.lang.String

                              JSON

                              java.lang.String

                              对于binary和nchar,在定义是需要指明长度,例如binary(64)、nchar(64),表示的数据最大64个字节

                              4.1创建库
                              //创建库demo,数据最多保留365天,超过自动删除,缺省时为3650天
                              //这里的时间单位由d(天),h(时),m(分),默认天,不带默认为天
                              create database if not exists demo keep 365
                              //展示数据库
                              show database;
                              //使用某个数据库
                              use demo;
                              4.2 创建超级表
                              //创建超级表 采集量为开盘价等,标签为市场,种类
                              create stable if not exists Scode(ts timestamp,open int,high float,low double,close samllint) tags(market binary(64),type nchar(10));
                              //展示超级表
                              show stables;
                              4.3 创建子表
                              //创建300059SZ子表,在超级表的模板上,指明标签属性值
                              create table if not exists 300059SZ using Scode tags('深市','股票') ;
                              //列举表
                              show tables

                              也可以插入时创建

                              INSERT INTO 300059SZ USING Scode TAGS ("深市", "股票") VALUES (NOW,10, 10.2, 9.8, 9.9);
                              4.4 插入查询

                              tdengine同样支持其他的标准SQL,例如分组,排序,聚合函数等,查询可以在超级表或者子表中进行,例如:

                              //查询表的总数
                              select count(*) from demo.Scode;

                              同样也可以查询所有股票中单笔交易的最高价的最高,最低价的最低

                              select max(high),min(low) from demo.Scode where type="股票";

                              对于插入语句,例如一次性插入多表多条记录

                              INSERT INTO 300059SZ VALUES (NOW, 10.3, 11, 10,10.1) (NOW+1, 10.3, 11, 10,10.1) 000001SH VALUES (NOW, 10.3, 11, 10,10.1);
                              注意

                              对于插入数据,可以采用批量插入,一次性插入越多,效率会越高。一般来说一批写入的记录条数越多,插入效率就越高。但一条记录不能超过 48KB,一条 SQL 语句总长度不能超过1MB。TDengine支持多线程同时写入,要进一步提高写入速度,一个客户端需要打开多个同时写。

                              对同一张表,如果新插入记录的时间戳已经存在,则指定了新值的列会用新值覆盖旧值,而没有指定新值的列则不受影响。

                              写入的数据的时间戳必须大于当前时间减去数据库配置参数 KEEP的时间。如果 KEEP 配置为 3650 天,那么无法写入比3650天还早的数据。写入数据的时间戳也不能大于当前时间加配置参数DURATION。如果DURATION为2,那么无法写入比当前时间还晚 2 天的数据。

                              TDengine数据库是没有删除和修改操作的,在创建数据库是指明数据保留时长,过期自动删除数据,几乎都是读写操作,写多读少。

                              5.java连接器

                              5.1连接器建立连接的方式

                              连接器建立连接的方式,TDengine 提供两种:

                              • 通过 taosAdapter 组件提供的 REST API 建立与 taosd 的连接,这种连接方式下文中简称“REST 连接”

                                • 通过客户端驱动程序 taosc 直接与服务端程序 taosd 建立连接,这种连接方式下文中简称“原生连接”。

                                  关键不同点在于:

                                  1. 使用 REST 连接,用户无需安装客户端驱动程序 taosc,具有跨平台易用的优势,但性能要下降 30% 左右。

                                  1. 使用原生连接可以体验 TDengine 的全部功能,如参数绑定接口、订阅等等。

                                  解释来说就是无论是服务端还是客户端在安装后提供的都有一个应用驱动taos.dll,原生连接需要调用客户端这个驱动和taosd数据库打交道,所以如果应用程序和服务端不在同一台机器上使用原生连接则需要安装客户端获取驱动,而rest连接则只需要服务端开启taosadapter服务,则可以进行远程操作。

                                  5.2java连接器模型

                                  taos-jdbcdriver是官方的java语言连接器,引入只需要加入以下依赖即可:

                                  
                                    com.taosdata.jdbc
                                    taos-jdbcdriver
                                    3.0.0
                                  

                                  同样java也是通过上述两种方式进行连接,模型如下:

                                  TDengine时序数据库通俗易懂教程,第1张
                                  1. JDBC 原生连接:Java 应用在物理节点 1(pnode1)上使用 TSDBDriver 直接调用客户端驱动(libtaos.so 或 taos.dll)的 API 将写入和查询请求发送到位于物理节点 2(pnode2)上的 taosd 实例。

                                  1. JDBC REST 连接:Java 应用通过 RestfulDriver 将 SQL 封装成一个 REST 请求,发送给物理节点 2 的 REST 服务器(taosAdapter),通过 REST 服务器请求 taosd 并返回结果。

                                  其中,rest连接java连接驱动如下:

                                  driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
                                  url: jdbc:TAOS-RS://ip:6041/demo?timezone=UTC-8&charset=UTF-8&locale=en_US.UTF-8
                                  username: root
                                  password: your password

                                  原生连接java驱动如下:

                                  driver-class-name: com.taosdata.jdbc.TSDBDriver
                                  url: jdbc:TAOS://ip:6030/demo?timezone=UTC-8&charset=UTF-8&locale=en_US.UTF-8
                                  username: root
                                  password: your password

                                  这里有一个细节要注意,虽说TDEngine数据库安装时默认端口是6030,但是使用原生连接时直接和数据库打交道,所以依旧是6030端口,而使用restful接口时则是先请求给rest服务器端,也就是taosadapter服务,在进行转换请求数据库,所以这里是默认的taosadapter服务的端口,也就是6041。

                                  通过上述配置可以结合druid,mybatis连接数据库进行操作,下面将进行展示。

                                  6. springboot+mybatis+druid连接池+TDEngine快速构建

                                  这里以证券快照交易为案列,一只code的快照数据放在对应的表里,例如300059.SZ表名为t300059sz

                                  6.1 引入依赖
                                  
                                      org.springframework.boot
                                      spring-boot-starter-web
                                  
                                  
                                      org.mybatis.spring.boot
                                      mybatis-spring-boot-starter
                                      2.1.1
                                  
                                  
                                      com.taosdata.jdbc
                                      taos-jdbcdriver
                                      3.0.0
                                  
                                  
                                      com.alibaba
                                      druid-spring-boot-starter
                                      1.1.17
                                  
                                  6.2 配置application.yml
                                  server:
                                    port: 8011
                                  spring:
                                    datasource:
                                      #1.采用rest连接
                                      #driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
                                      #url: jdbc:TAOS-RS://ip:6041/?timezone=UTC-8&charset=UTF-8&locale=en_US.UTF-8
                                      #2.采用原生连接,两边除了驱动和url稍微差别,其他一致
                                      driver-class-name: com.taosdata.jdbc.TSDBDriver
                                      url: jdbc:TAOS://ip:6030/?timezone=UTC-8&charset=UTF-8&locale=en_US.UTF-8
                                      username: root
                                      password: your password
                                      #德鲁伊连接池
                                      type: com.alibaba.druid.pool.DruidDataSource
                                      #初始容量
                                      initialSize: 10
                                      #最小连接数
                                      minIdle: 10
                                      #最大活跃连接数
                                      maxActive: 10
                                      #最大等待时间
                                      maxWait: 30000
                                      #对于空闲连接每隔一分钟检测一次
                                      time-between-eviction-runs-millis: 60000
                                      #当空闲时间大于这个值则会被回收
                                      min-evictable-idle-time-millis: 300000
                                      #检测验证方式
                                      validation-query: select server_status();
                                      #对空闲连接检测是否可用,true开启
                                      test-while-idle: true
                                      #与test-while-idle作用一致,不过优先级更高
                                      test-on-borrow: false
                                      test-on-return: false
                                  mybatis:
                                    mapper-locations: classpath*:/mapper/*Mapper.xml
                                    configuration:
                                      default-statement-timeout: 100
                                      log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

                                  在配置druid时,有一个小问题,就是validation-query配置,对于每一种数据库都有自己的验证方式,但是TDEngine官方并没有提供自己的专属的验证方式,所以在启动时,druid会报一个错误,但不影响运行,如下所示:

                                  ERROR 33180 --- [main] com.alibaba.druid.pool.DruidDataSource   : testWhileIdle is true, validationQuery not set
                                  6.3 搭建controller,service,mapper以及xml

                                  xml文档如下:

                                  
                                  
                                  
                                      
                                      
                                          insert into
                                          
                                              demo.t#{code.secuCode} values(#{code.ts},#{code.open},#{code.close},#{code.securityName},#{code.trading})
                                          
                                      
                                      
                                      
                                      
                                      
                                          create database if not exists demo;
                                      
                                      
                                      
                                          create stable if not exists demo.scode(ts timestamp,open int,close double,securityName binary(64),trading bool) tags(type nchar(64),groupId int)
                                      
                                      
                                      
                                          create table if not exists demo.t#{code.secuCode} using demo.scode tags(#{code.type},#{code.groupId})
                                      
                                  

                                  java实体类:

                                  public class Code {
                                      private String secuCode;
                                      @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss.SSS")
                                      private Timestamp ts;
                                      private int open;
                                      private double close;
                                      private String securityName;
                                      private boolean trading;
                                      private String type;
                                      private int groupId;
                                      public Code(String secuCode,Timestamp ts, int open, double close, String securityName, boolean trading, String type, int groupId) {
                                          this.secuCode=secuCode;
                                          this.ts = ts;
                                          this.open = open;
                                          this.close = close;
                                          this.securityName = securityName;
                                          this.trading = trading;
                                          this.type = type;
                                          this.groupId = groupId;
                                      }
                                      public Code(Timestamp ts, int open, double close, String securityName, boolean trading, String type, int groupId) {
                                          this.ts = ts;
                                          this.open = open;
                                          this.close = close;
                                          this.securityName = securityName;
                                          this.trading = trading;
                                          this.type = type;
                                          this.groupId = groupId;
                                      }
                                      public String getSecuCode() {
                                          return secuCode;
                                      }
                                      public void setSecuCode(String secuCode) {
                                          this.secuCode = secuCode;
                                      }
                                      public Timestamp getTs() {
                                          return ts;
                                      }
                                      public void setTs(Timestamp ts) {
                                          this.ts = ts;
                                      }
                                      public int getOpen() {
                                          return open;
                                      }
                                      public void setOpen(int open) {
                                          this.open = open;
                                      }
                                      public double getClose() {
                                          return close;
                                      }
                                      public void setClose(double close) {
                                          this.close = close;
                                      }
                                      public String getSecurityName() {
                                          return securityName;
                                      }
                                      public void setSecurityName(String securityName) {
                                          this.securityName = securityName;
                                      }
                                      public boolean isTrading() {
                                          return trading;
                                      }
                                      public void setTrading(boolean trading) {
                                          this.trading = trading;
                                      }
                                      public String getType() {
                                          return type;
                                      }
                                      public void setType(String type) {
                                          this.type = type;
                                      }
                                      public int getGroupId() {
                                          return groupId;
                                      }
                                      public void setGroupId(int groupId) {
                                          this.groupId = groupId;
                                      }

                                  这里注意字段之间的映射,虽说binary定义的是字节数组,但对应的java类型既可以是字节数组也可以是字符串,不需要任何转换。

                                  controlelr层:

                                  @RestController
                                  @RequestMapping("/code")
                                  public class CodeController {
                                      @Autowired
                                      CodeService codeService;
                                      @RequestMapping("/insert")
                                      public String insert(){
                                          List codes=new ArrayList<>();
                                          codes.add(new Code("00700",new Timestamp(new Date().getTime()),10,11.0,"腾讯控股",true,"股票",1));
                                          codes.add(new Code("113637SZ",new Timestamp(new Date().getTime()),7,8.2,"可转债",true,"债券",1));
                                          codes.add(new Code("000001SH",new Timestamp(new Date().getTime()),20,24.7,"上证成指",true,"指数",2));
                                          codes.add(new Code("00700",new Timestamp(new Date().getTime()+3000),10,12.0,"腾讯控股",true,"股票",1));
                                          codes.add(new Code("113637SZ",new Timestamp(new Date().getTime()+3000),7,8.9,"可转债",true,"债券",1));
                                          codes.add(new Code("000001SH",new Timestamp(new Date().getTime()+3000),20,23.0,"上证成指",true,"指数",2));
                                          int num=0;
                                          for(Code code:codes){
                                              num=num+codeService.createTable(code);
                                          }
                                          int total= codeService.addCodes(codes);
                                          return "成功创建"+num+"张表,成功插入"+total+"条数据";
                                      }
                                      @RequestMapping("/select")
                                      public List select(){
                                          return codeService.selectCode();
                                      }
                                      @RequestMapping("/createDB")
                                      public void create(){
                                          codeService.createDB();
                                      }
                                      @RequestMapping("/createStable")
                                      public String createStable(){
                                          codeService.createStable();
                                          return "超级表创建成功";
                                      }
                                  }

                                  service层:

                                  public interface CodeService {
                                      int addCodes(List codes);
                                      List selectCode();
                                      int createDB();
                                      int createStable();
                                      int createTable(Code code);
                                  }
                                  @Service
                                  public class CodeServiceImpl implements CodeService {
                                      @Autowired
                                      CodeMapper codeMapper;
                                      @Override
                                      public int addCodes(List codes) {
                                          return codeMapper.addCodes(codes);
                                      }
                                      @Override
                                      public List selectCode() {
                                          return codeMapper.selectCode();
                                      }
                                      @Override
                                      public int createDB() {
                                          return codeMapper.createDB();
                                      }
                                      @Override
                                      public int createStable() {
                                          return codeMapper.createStable();
                                      }
                                      @Override
                                      public int createTable(Code code) {
                                          return codeMapper.createTable(code);
                                      }
                                  }

                                  mapper层

                                  @Mapper
                                  public interface CodeMapper {
                                      int addCodes(List codes);
                                      List selectCode();
                                      int createDB();
                                      int createStable();
                                      int createTable(@Param("code") Code code);
                                  }

                                  对于另外一种rest连接,要注意以下几点配置:

                                  • 驱动和url稍有改动,默认端口号为6041,也就是提供restful的taosadapter服务的端口号