chapter-1 gRPC的概述与发展

  • 一 gRPC概述

    ​ gRPC (the “g” stands for something different in every gRPC release) is an inter-process communication technology that allows you to connect, invoke, operate, and debug distributed heterogeneous applications as easily as making a local function call.

    ​ gRPC 是一个高性能、开源、通用的RPC框架,是一种进程间通讯技术,由Google推出,基于HTTP2协议标准设计开发,默认采用Protocol Buffers数据序列化协议,支持多种开发语言。gRPC提供了一种简单的方法来精确的定义服务,并且为客户端和服务端自动生成可靠的功能库。 在gRPC客户端可以直接调用不同服务器上的远程程序,使用姿势看起来就像调用本地程序一样,轻松的实现远端服务的连接、调用、操作和调试,很容易去构建分布式应用和服务。和很多RPC系统一样,服务端负责实现定义好的接口并处理客户端的请求, 客户端根据接口描述直接调用需要的服务。客户端和服务端可以分别使用gRPC支持的不同语言实现。

    gRPC

    ​ Using that service definition, you can generate the server-side code known as a server skeleton, which simplifies the server-side logic by providing low-level communication abstractions. Also, you can generate the client-side code, known as a client stub, which simplifies the client-side communication with abstractions to hide low-level communication for different programming languages. The methods that you specify in the service interface definition can be remotely invoked by the client side as easily as making a local function invocation. The underlying gRPC framework handles all the complexities that are normally associated with enforcing strict service contracts, data serialization, network communication, authentication, access control, observability, and so on.

    ​ 通过IDL(interface definition language )进行服务定义,生成服务端架构的服务代码,它通过提供低等级的通讯抽象去简化服务端逻辑;同样,可以根据客户端存根生成客户端的代码,通过抽象简化去隐藏不同编程语言的低级通讯。通过服务接口定义的方法可以实现通过客服端很轻松的就像在本地调用函数一样进行远程调用。gRPC底层框架处理了所有强制严格的服务契约、数据序列化、网络通讯、服务认证、访问控制、服务观测等等通常有关联的复杂性。

  • 二 服务定义

    • protocol buffer

      gRPC使用的是protocol buffer作为服务描述语言

    • 示例

      通常使用*.proto文件进行服务定义,例如下面定义:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      syntax = "proto3";
      package api;

      service ProductInfo {
      rpc addProduct(Product) returns (ProductID);
      rpc getProduct(ProductID) returns (Product);
      }

      message Product {
      string id = 1;
      string name = 2;
      string description = 3;
      }

      message ProductID {
      string value = 1;
      }
  • 三 gRPC服务端

    • 生成服务架构代码
      1
      protoc --plugin=protoc-gen-go=/usr/local/bin/protoc-gen-go --go-grpc_out . --go_out .  product.proto

      生成结构如下api目录:pb转go

    • 服务端要求
      • 重写服务基类实现服务架构的服务逻辑
      • 运行gRPC服务并监听处理客户端请求,返回响应;
    • 服务端案例实现
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      package main

      /*
      @Time : 2020/12/17 13:24
      @File : main.go
      @Software: GoLand
      */

      // 服务端需要引如生成的服务骨架
      // 重写基类 实现业务
      // 运行项目监听请求处理请求返回响应

      import (
      "context"
      "fmt"
      pb "gRPCChapter1/api"
      "google.golang.org/grpc"
      "log"
      "net"
      )
      const port = "127.0.0.1:9090"
      // 可以自定义一个结构体,然后实现该接口的全部方法即可, 将来实现将DTO对象转化DO领域对象
      type product struct{}

      var ProductServer = product{}

      func (s product) AddProduct(ctx context.Context, in *pb.Product) (*pb.ProductID, error) {
      // 处理业务逻辑,此处简单输出即可
      fmt.Printf("新增商品成功...\n")
      fmt.Printf("商品ID:%s, 商品名称:%v\n", in.Id, in.Name)
      p := pb.ProductID{Value: in.Id}
      return &p, nil
      }

      func (s product) GetProduct(ctx context.Context, in *pb.ProductID) (*pb.Product, error) {
      // 业务逻辑的处理
      log.Printf("获取商品...\n")
      log.Printf("查询商品ID: %v\n", in.Value)
      p := pb.Product{Id: "1", Name: "手机", Description: "测试手机"}
      return &p, nil
      }

      func main() {
      lis, _ := net.Listen("tcp", port)
      s := grpc.NewServer()
      pb.RegisterProductInfoServer(s, ProductServer)
      fmt.Printf("ProductServer register success! listen to %v\n", port)
      if err := s.Serve(lis); err != nil {
      log.Fatalf("failed to serve: %v\n", err)
      }
      }
  • 四 gRPC客户端

    Go客户端实现与服务端相似,只要进行客户端代码存根调用即可:

    示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    package main

    /*
    @Time : 2020/12/17 13:24
    @File : main.go
    @Software: GoLand
    */
    import (
    "context"
    pb "gRPCChapter1/api"
    "google.golang.org/grpc"
    "log"
    "time"
    )

    // gRPC客户端与服务端相似,使用客户端存根就可以实现服务调用
    const (
    ADDRESS = "127.0.0.1:9090"
    )

    func main() {
    // 创建连接
    client, err := grpc.Dial(ADDRESS, grpc.WithInsecure())
    if err != nil {
    log.Fatal("grpc create client failed! err: ", err)
    }
    // 关闭链接
    defer client.Close()

    // 创建客户端
    cli := pb.NewProductInfoClient(client)
    // 调用函数
    var product pb.Product = pb.Product{
    Id: "1",
    Name: "iphone12",
    Description: "新款手机",
    }
    // 创建请求超时上下文
    timeOut, chancel := context.WithTimeout(context.Background(), time.Second)
    defer chancel()

    productId, err := cli.AddProduct(timeOut, &product)

    if err != nil {
    log.Fatalf("AddProduct failed: %v\n", err)
    }
    log.Printf("add product id: %v\n", productId.Value)
    // 继续调用获取商品信息
    pd, err := cli.GetProduct(timeOut, productId)
    if err != nil {
    log.Fatalf("GetProduct failed: %v\n", err)
    }
    log.Printf(pd.String())
    }

    分别启用服务端与客户端结果如下:

    客户端:

    client

    服务端:

    server

  • 五 Python客户端通信

    • Python插件配置
      1
      2
      3
      4
      # 生成python代码的脚手架用来转换IDL
      pip install grpcio-tools -i https://pypi.tuna.tsinghua.edu.cn/simple
      # gRPC插件支持
      pip install grpcio -i https://pypi.tuna.tsinghua.edu.cn/simple
    • IDL接口服务进行转换Python服务
      1
      python3 -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. product.proto

      生成的文件结构如下:

      python转换IDL

      ​ 上图中_pb2.py与_pb2_grpc.py就是根据proto文件生成的客户端与服务端的相对应代码的存根,只要对逻辑或调用进行实现即可,其中_pb2.py是用来和 protobuf 数据进行交互,_pb2_grpc.py是用来和 grpc 进行交互。

    • Python客户端调用Go服务端服务

      在main.py文件中,我们将会对Go服务端的服务进行调用,其示例代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      import grpc
      import product_pb2
      import product_pb2_grpc


      def run():
      # 创建一个gRPC的链接
      channel = grpc.insecure_channel('localhost:9090')
      # 创建一个客户端
      stub = product_pb2_grpc.ProductInfoStub(channel)
      # 调用客户端的新增方法
      response = stub.addProduct(product_pb2.Product(
      name="iphone12",
      description="新款手机",
      id="1"))

      print("add product: response\n", response)
      product_info = stub.getProduct(product_pb2.ProductID(value=response.value))
      print("get product: response\n", str(product_info))


      if __name__ == '__main__':
      run()

      运行main.py文件后,得到如下信息,和Go客户端调用信息一致,也就是不管客户端还是服务端,只需要按照protobuf协议进行转换。其实现了高度抽象,可以实现跨语言间的低耦合通信。

      python调用远端服务

  • 六 客服端与服务端消息流动

    ​ When a gRPC client invokes a gRPC service, the client-side gRPC library uses the protocol buffer and marshals the remote procedure call protocol buffer format, which is then sent over HTTP/2. On the server side, the request is unmarshaled and the respective procedure invocation is executed using protocol buffers. The response follows a similar execution flow from the server to the client. As the wire transport protocol, gRPC uses HTTP/2, which is a high-performance binary message protocol with support for bidirectional messaging.

    ​ 当gRPC的客户端调用gRPC服务,客户端侧gRPC库使用协议缓存进行封送协议缓冲区格式的远程调用程序,然后通过HTTP2发送。在服务端侧,接收到的请求被解组然后使用协议缓存进行各自程序调用。服务端到客户端响应的流动也使用了相同的方式。gRPC使用了高性能、支持双向信息传递的二进制消息协议HTTP2进行有线传输协议。

    ​ Marshaling is the process of packing parameters and a remote function into a message packet that is sent over the network, while unmarshaling unpacks the message packet into the respective method invocation.

    ​ 封送: 就是将参数和远端函数打包到通过网络发送的消息包中的过程;

    ​ 解组: 就是将消息包解包到相应的方法进行调用。

  • 七 IPC发展历程

    • 传统RPC

      ​ 传统的RPC通常是构建C-S架构应用中的进程间通讯,客户端通过远程调用方法实现了像本地调用函数一样。早期受欢迎的技术是Common Object Request Broker Architecture **和RMI**,用来建立与服务或应用的联系。

      ​ 其存在的缺陷:极度复杂、建立在TCP通讯协议之上、妨碍内部操作、众多臃肿规范。

    • SOAP

      ​ 由于传统RPC存在的局限性,Simple Object Access Protocol(SOAP)简单对象访问协议被微软、IBM等大厂进行设计并进行了大力推广。SOAP协议是一种标准的面相服务架构的通讯技术,用于服务间基于XML结构体数据交换,并且使用任何底层通信协议(通常是HTTP)进行通信。

      ​ 可以使用SOAP协议定义服务接口、服务操作、并且可以通过调用这些操作使用有关联的XML格式消息。SOAP是一种非常受欢迎的技术,但是由于复杂的消息格式、构建也受到SOAP协议复杂的规范约束,阻碍了构建分布式应用的灵活性。因此,在当代分布式应用的开发环境下,SOAP网络服务技术也被当作了历史遗留技术。大多数分布式应用架构都开始使用REST架构风格,而不是使用SOAP协议。

    • REST
      • 概述

        ​ REST is the foundation of the resource-oriented architecture (ROA), where you model distributed applications as a collection of resources and the clients that access those resources can change the state (create, read, update, or delete) of those resources.

        ​ REST是面相资源架构体系的基础,在这里,分布式应用模型被作为一种资源的集合,客户端可以访问这些资源、可以改变这些资源的状态(CRUD)。

        ​ REST的实际实现是HTTP协议,在HTTP中,你可以将RESTful风格服务应用建模为可以使用唯一标识符URL访问的资源的集合。资源状态的更改,就是基于HTTP请求动作实现。

        ​ 使用HTTP或JSON的REST架构风格构建应用程序已经成为微服务的实现方式,但是,在急剧增长的微服务数量或网络交互,RESTful风格的服务架构已经不能满足预期的要求。并且其存在两个关键限制。

        ​ 这些限制阻碍了他们使用基于现代微服务应用程序使用消息传输协议的能力。

      • 局限性

        • Inefficient text-based message protocols

          ​ 从本质上来说,REST架构服务是基于文本传输协议的HTTP1.x之上,并且携带了人类可读的类似于JSON的文本格式。当进行服务端间进行通信时,使用JSON类型文本格式是非常低效的。因为,作为服务端间的通信,没有必要使用可读的文本格式。

          ​ 服务端源应用程序生成二进制内容发送到服务器,然后将二进制结构转化为文本,然后通过文本形式通过网络发送到另外一台机器,目标服务端接收解析并转换回二进制结构返回。相反,我们可以很轻松发送二进制格式数据,并能够映射服务和消费的业务逻辑。通常为了便于可读性,服务端与客户端直接使用JSON进行传输。

        • Lacks strongly typed interfaces between apps

          ​ 越来越多的服务通过网络进行交互,这些服务可能是不同语言技术开发的,一个好的、稳定健硕的服务类型定义非常的重要。虽然在RESTful 服务中已经存在很多服务定义技术,比如说OpenAPI/Swagger服务,但都是事后定义的,在设计之初、并没有紧密的集成底层架构和消息传递协议。

          ​ 在构建分散应用中就会导致很多的不兼容、运行时错误、以及内部交互的问题。因此,具备当代强类型服务定义技术和生成多语言服务侧-客户端侧核心代码的框架技术时非常有必要的。

        • REST architectural style is hard to enforce

          ​ 真正的RESTful架构服务在实现阶段是很难落地实施的,目前普遍存在的所谓的RESTful服务应用,仅仅是网络公开的HTTP服务,并没有真正的正确遵循RESTful风格。

    • gRPC
      • Inception of gRPC

        ​ Google最早使用了一个叫Stubby的通用RPC框架,用来连接上千个使用不同技术构建的运行在多个数据中心的微服务。它的核心RPC层设计是用来每秒处理百亿个网络请求,Stubby具有很多良好的特性,但是它并不是标准化的,不能作为通用型的框架使用,因为它与Google内部的基础设施耦合的太过于紧密。

        ​ 在2015年,Google发行了gRPC并作为了一个开源的RPC框架,gRPC是标准的、通用型的、跨平台的基础框架,gRPC的目的是提供与Stubby相同可伸缩性、性能和功能,并且是面向整个社区。

        ​ 自此以后,gRPC的受欢迎程度随着Netflix, Square, Lyft, Docker, Cisco, and CoreOS等大公司的采用在过去的几年里大幅度的增长。后来,gRPC也加入到云原生计算基金会,并从CNCF生态系统项目中获得很大的吸引力。

      • Why gRPC?

        ​ gRPC被设计成一种互联网规模的进程间通信技术,可以克服传统进程间通信技术的大部分缺点。

      • Advantages of gRPC

        1. It’s efficient for inter-process communication

          ​ gRPC没有使用传统的JSON和XML文本格式,而是使用了基于协议缓存区的二进制协议与gRPC的服务端、客户端通讯。gRPC在HTTP2的基础上实现了协议缓冲区,使得进程间的通讯更快。这使得gRPC成为目前最高效的IPC技术。

        2. It has simple, well-defined service interfaces and schema

          ​ gRPC采用了合约至上的方式开发应用,即先定义服务接口,然后在此基础上进行细节实现。

          因此,与用于RESTful服务定义的OpenAPI/Swagger和用于SOAP web服务的WSDL不同,gRPC提供了一种简单但一致、可靠和可扩展的应用程序开发体验。

        3. It’s strongly typed

          ​ gRPC是强类型的。一旦使用协议缓冲定义gRPC的服务,gRPC服务合同便清晰的定义将用于应用间通讯的类型。由于强类型定义,使得分布式应用程序开发变得更加稳定,因为静态类型有助于克服在构建跨多个团队和技术的云原生应用程序时可能遇到的大多数运行时和互操作性错误。

        4. It’s polyglot

          ​ gRPC设计用于多种编程语言。带有协议缓冲的gRPC服务的定义与语言无关,可以使用任何语言与存在的gRPC服务或者客户端进行交互。

        5. It has duplex streaming

          ​ gRPC自身就支持客户端和服务端的流媒体,这是服务定义本身所固有的。这使得开发流式服务和流式客户端更加容易。此外,与传统的RESTful消息传递风格相比,gRPC构建传统的请求-响应式消息传递和服务端与客户端间流式传递的能力是一个关键优势。

        6. It has built-in commodity features

          ​ gRPC内置了如用户认证、加密、弹性(过期与超时)、元数据交互、压缩、负载均衡、与服务发现等很多有价值的特性。

        7. It’s integrated with cloud native ecosystems

          ​ gRPC是CNCF的一部分,很多的现代框架和技术为gRPC提供现成的本地支持。例如,CNCF下的许多项目(如Envoy)支持gRPC通信协议;对于观测和监控等跨领域功能,gRPC被很多工具支持(例如,使用 Prometheus监控gRPC应用程序)。

        8. It’s mature and has been widely adopted

          ​ gRPC作为进程间通讯技术,已经是很成熟,并被广泛的采用。在Google内部已经进行全面的测试,gRPC在许多大公司如Square, Lyft, Netflix, Docker, Cisco, and CoreOS中被广泛的使用。

      • Disadvantages of gRPC

        1. It may not be suitable for external-facing services

          ​ gRPC不太适合面向外部的服务。当通过互联网向外部服务机公开应用程序与服务时,gRPC通讯协议就不太适合,因为很多的外部消费者对gRPC和REST/HTTP服务模式很陌生。gPRC服务的契约驱动和强类型定义的特性,阻碍了向外部公开服务的灵活性,并且消费者获得的控制权也大大的降低。但后面通过gRPC的网关设计,用来解决这类问题。

        2. Drastic service definition changes are a complicated development process

          ​ 服务更改在现代服务间通讯用例中是非常常见的。如果gRPC服务定义发生巨大改变时,就增加了向下兼容的难度。通常需要重新生成服务端与客户端的代码,很可能加大整个开发生命周期复杂化。正常情况下,gRPC服务定义的修改都是在不违反服务契约的条件下进行,只要没有引入破坏性的变更,gRPC就可以使用不同版本的proto的客户机和服务器进行交互操作。因此,在大多数情况下不需要重新生成代码。

        3. The ecosystem is relatively small

          ​ gRPC的生态系统和传统REST/HTTP协议相比较小,移动应用程序和浏览器对gRPC的支持目前处于初级阶段。

    • gRPC与其他协议的对比
      • Apache Thrift

        • 概述

          ​ Apache Thrift和gRPC相似是RPC框架。它使用Thrift的IDL描述接口定义函数与数据类型,然后通过Thrift的编译环境生成各种语言类型的接口文件,用户可以根据自己的需要采用不同的语言开发客户端代码和服务器端代码。Thrift传输层为网络I/O提供抽象,并将Thrift与系统的其他部分进行解耦,它可以在任何传输实现上运行,如TCP、HTTP等。

        • 区别

          1. Transport

            ​ gRPC 比 Thrift 更超前,提供了对 HTTP/2 一流的支持。 它在 HTTP/2上实现利用该协议的功能来实现高效和流式消息传递模式。

          2. Streaming

            ​ gRPC服务定义本身就支持客户端和服务的之间的双向流式传递。

          3. Adoption and community

            ​ 在采用方面,gRPC 有良好的发展势头,并且已成功围绕 CNCF 项目建立了良好的生态系统。 同样,社区资源,例如良好的文档,外部演示文稿和示例用例,对于 gRPC 来说是很常见的,与 Thrift 相比,采用过程更加流畅。

          4. Performance

            ​ 在性能方面,由于官方也没有给出gRPC与Thrift比较的明确结果,有一些在线资源对二者性能进行比较显示出更合适Thrift的数据。然而,gRPC在所有版本中都在进行性能测试。因此,在进行二者选择时,性能影响不是决定性的因素。要结合具体的场景和业务架构来选择最合适的协议。此外,目前虽然还有很多的RPC框架提供了类似的功能,但gRPC作为最标准化、可互操作和广泛采用的RPC技术目前处于领先地位。

        • 选择依据

          • 选择gRPC而不是Thrift
            1. 需要良好的文档、示例
            2. 喜欢、习惯使用HTTP/2、ProtoBuf协议
            3. 对网络传输带宽敏感
          • 选择Thrift而不是gRPC
            1. 需要在非常多的语言间进行数据交换
            2. 协议层、传输层有多种控制要求
            3. 需要稳定的版本
            4. 不需要良好的文档和示例
            5. 对CPU敏感
      • GraphQL

        ​ GraphQL 是另一种技术(由 Facebook 发明并标准化为一种开放技术),在构建进程间通信中变得非常流行。 它是 API 的查询语言,是用于使用现有数据完成这些查询的运行时。 GraphQL 通过允许客户端确定所需的数据,所需的数据以及所需的格式,为传统的客户端-服务器通信提供了根本不同的方法。 相反,gRPC 与支持客户机和服务器之间通信的远程方法具有固定的约定。

        ​ GraphQL 更适合直接暴露给消费者的外部服务或 API,在这样的环境中,客户端需要对来自服务器的数据进行更多控制与处理。

        ​ 在GraphQL和gRPC的大多数实用用例中,GraphQL用倾向于面向外部的服务/api,而支持api的内部服务则使用gRPC实现。

  • 八 总结

    ​ 现代软件应用程序或服务很少是孤立存在,用来连接服务应用间的进程间通信技术是现代分布式软件应用程序最重要的一方面。gRPC是一个可伸缩的、松耦合的、类型安全的进程间通讯技术,与传统的基于REST/HTTP的通信相比,gRPC更加高效。它允许你就像调用本地程序一样,通过网络传输请求轻松的实现远端服务的连接、调用、操作和调试,很容易去构建分布式应用和服务。

    ​ gRPC技术是传统RPC的一种发展,并设法克服了传统RPC的局限性。gRPC由于其进程间通信需求而被各种互联网规模的公司广泛采用,并且最常用于构建内部服务到服务通信。

1
2
参考文献:
Grpc: Up and Running: Building Cloud Native Applications with Go and Java for Docker and Kubernetes. --Kasun Indrasiri;Danesh Kuruppu

时序数据库-InfluxDB-1-基础概念与简单实用

一、什么是InfluxDB

​ InfluxDB是一个由InfluxData开发的开源时序型数据。它由Go写成,着力于高性能地查询与存储时序型数据。InfluxDB被广泛应用于存储系统的监控数据,IoT行业的实时数据等场景。

​ 当前最新稳定版本InfluxDB v2.0

二、InfluxDB基础概念

2.1 database(数据库)

​ 同SQL中的database

1
2
3
4
5
6
> show databases
name: databases
name
----
telegraf
_internal

2.2 measurement(表)

​ 同SQL中的table

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
> use telegraf
Using database telegraf
> show measurements
name: measurements
name
----
cpu
docker
docker_container_blkio
docker_container_cpu
docker_container_mem
docker_container_net
docker_container_status
influxdb
influxdb_cq
influxdb_database
influxdb_httpd
influxdb_memstats
influxdb_queryExecutor
influxdb_runtime
influxdb_shard
influxdb_subscriber
influxdb_tsm1_cache
influxdb_tsm1_engine
influxdb_tsm1_filestore
influxdb_tsm1_wal
influxdb_udp
influxdb_write
system

2.3 point(时序数据记录)

​ 相当于SQL中的数据行。下面每一条数据就是一个point。由时序(series)和时间戳(timestamp)唯一标识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
> select * from system limit 10
name: system
time host load1 load15 load5 n_cpus n_users uptime uptime_format
---- ---- ----- ------ ----- ------ ------- ------ -------------
1608192260000000000 telegraf-getting-started 0.71 0.07 0.22 6 0 20849 5:47
1608192265000000000 telegraf-getting-started 0.65 0.07 0.21 6 0 20854 5:47
1608192270000000000 telegraf-getting-started 0.6 0.07 0.21 6 0 20859 5:47
1608192275000000000 telegraf-getting-started 0.55 0.07 0.2 6 0 20864 5:47
1608192280000000000 telegraf-getting-started 0.5 0.07 0.2 6 0 20869 5:47
1608192285000000000 telegraf-getting-started 0.46 0.07 0.2 6 0 20874 5:47
1608192290000000000 telegraf-getting-started 0.43 0.07 0.19 6 0 20879 5:47
1608192295000000000 telegraf-getting-started 0.39 0.07 0.19 6 0 20884 5:48
1608192300000000000 telegraf-getting-started 0.36 0.07 0.19 6 0 20889 5:48
1608192305000000000 telegraf-getting-started 0.33 0.07 0.18 6 0 20894 5:48

> show field keys from system
name: system
fieldKey fieldType
-------- ---------
load1 float
load15 float
load5 float
n_cpus integer
n_users integer
uptime integer
uptime_format string

2.4 time(时间)

​ 既然是时间序列数据库,influxdb的数据都有一列名为time的列,里面存储UTC时间戳。time可以在插入数据时自己添加,如果没有添加,InfluxDB会自动创建。在InfluxDB中,时间几乎可以看作是主键。

2.5 tag(标签)

​ 用于创建索引,提升查询性能,一般存放标识数据来源的属性信息。在下方代码中,system表中的标签为host,对应的标签值为telegraf-getting-started。

1
2
3
4
5
6
7
8
9
10
11
> show tag keys from system
name: system
tagKey
------
host

> show tag values from system with key=host
name: system
key value
--- -----
host telegraf-getting-started

2.6 field(指标)

​ 一般存放的是具体的时序数据,即随着时间戳的变化而变化的数据,与标签不同的是,未对指标数据创建索引。

1
2
3
4
5
6
7
8
9
10
11
> show field keys from system
name: system
fieldKey fieldType
-------- ---------
load1 float
load15 float
load5 float
n_cpus integer
n_users integer
uptime integer
uptime_format string

2.7 retention policy(保留策略)

​ 定义InfluxDB的数据保留时长和数据存储的副本数量,通过设置合理的保存时间(Duration)和副本数量(Replication),在提升数据存储可用性的同时,避免数据爆炸。

1
2
3
4
5
6
7
8
9
10
11
> show databases
name: databases
name
----
telegraf
_internal

> show retention policies on telegraf
name duration shardGroupDuration replicaN default
---- -------- ------------------ -------- -------
autogen 0s 168h0m0s 1 true

​ 以上表示telegraf数据库中存在一个数据保留策略,其中的数据永久保存,不过期(duration=0s),副本数为1(replicaN=1)

2.8 series(时间序列线)

​ 表示表名、保留策略、标签集都相同的组数据。

1
2
3
4
> show series on telegraf from system
key
---
system,host=telegraf-getting-started

三、InfluxDB操作模式

​ InfluxDB提供两种原生的操作模式:influx命令行工具和InfluxDB API。

​ influx命令行工具是一种类似于mysql命令行工具的命令行接口,可以方便地执行管理、运维、调试性质的操作。

​ InfluxDB API是一种可编程性强、编程语言友好的RESTful API的操作接口,支持HTTP和HTTPS协议。

3.1 influx命令行工具

3.1.1 内置命令

​ 与mysql命令行工具类似,influx也内置了丰富、强大的命令,下面将详细介绍常用的内置命令。

a.help命令

​ help命令输出支持的内置命令的及其使用帮助信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> help
Usage:
connect <host:port> connects to another node specified by host:port
auth prompts for username and password
pretty toggles pretty print for the json format
chunked turns on chunked responses from server
chunk size <size> sets the size of the chunked responses. Set to 0 to reset to the default chunked size
use <db_name> sets current database
format <format> specifies the format of the server responses: json, csv, or column
precision <format> specifies the format of the timestamp: rfc3339, h, m, s, ms, u or ns
consistency <level> sets write consistency level: any, one, quorum, or all
history displays command history
settings outputs the current settings for the shell
clear clears settings such as database or retention policy. run 'clear' for help
exit/quit/ctrl+d quits the influx shell

show databases show database names
show series show series information
show measurements show measurement information
show tag keys show tag key information
show field keys show field key information

A full list of influxql commands can be found at:
https://docs.influxdata.com/influxdb/latest/query_language/spec/

b.auth命令

​ auth命令,提示输入用户名和密码,后续执行influx命令行操作时,进行认证。

1
2
3
> auth
username: admin
password:

​ 通过auth命令输入用户名密码时,密码不会显示。

c.connect命令

​ connect命令,在不退出命令行shell情况下,连接到指定的IP和端口的InfluxDB服务器上。默认连接localhost:8086

1
2
> connect localhost:8086
>

d.consistency命令

​ consistency命令,用于配置写一致性的级别:any、one、quorum、all

写一致性级别 描述
any 任何一个节点写入成功后,或者接收节点已将数据写入hinted handoff缓存队列后,就会返回成功给客户端。
one 任何一个节点写入成功后,立即返回成功给客户端,不包括成功写入hinted handoff缓存队列。
quorum 当大多数节点写入成功后,会返回成功给客户端。此选项仅在副本数大于2时有意义,否则等效于all。
all 仅在所有节点都写入成功后返回成功。

​ 设置写一致性级别为all。

1
2
> consistency all
>

e.format命令

​ format命令,设置服务器响应数据的格式:json、csv、column

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> format json
> show databases
{"results":[{"series":[{"name":"databases","columns":["name"],"values":[["telegraf"],["_internal"]}]}]}

> format csv
> show databases
name,name
databases,telegraf
databases,_internal

> format column
> show databases
name: databases
name
----
telegraf
_internal

f.insert命令

​ insert命令,写入行协议格式的时序数据。

1
2
3
4
5
6
# 基础语法
> insert into <retention policy> <line protocol>

# 使用数据库默认保留策略,插入一条数据
> insert test_table,test_tag=tag1 value=123
>

g.select命令

​ select命令,查询InfluxDB数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 语法 
# SELECT <field_key>[,<field_key>,<tag_key>] FROM <measurement_name>[,<measurement_name>]
# 其中,<field_key>和<measurement_name>是必需的,其他部分为可选项。<field_key>[,<field_key>,<tag_key>]表示指定查询结果展现的字段。
> select * from system limit 10
name: system
time host load1 load15 load5 n_cpus n_users uptime uptime_format
---- ---- ----- ------ ----- ------ ------- ------ -------------
1608192260000000000 telegraf-getting-started 0.71 0.07 0.22 6 0 20849 5:47
1608192265000000000 telegraf-getting-started 0.65 0.07 0.21 6 0 20854 5:47
1608192270000000000 telegraf-getting-started 0.6 0.07 0.21 6 0 20859 5:47
1608192275000000000 telegraf-getting-started 0.55 0.07 0.2 6 0 20864 5:47
1608192280000000000 telegraf-getting-started 0.5 0.07 0.2 6 0 20869 5:47
1608192285000000000 telegraf-getting-started 0.46 0.07 0.2 6 0 20874 5:47
1608192290000000000 telegraf-getting-started 0.43 0.07 0.19 6 0 20879 5:47
1608192295000000000 telegraf-getting-started 0.39 0.07 0.19 6 0 20884 5:48
1608192300000000000 telegraf-getting-started 0.36 0.07 0.19 6 0 20889 5:48
1608192305000000000 telegraf-getting-started 0.33 0.07 0.18 6 0 20894 5:48

h.pretty命令

​ pretty命令,支持json格式的漂亮打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
> format json
> pretty
Pretty print enabled
> show databases
{
"results": [
{
"series": [
{
"name": "databases",
"columns": [
"name"
],
"values": [
[
"telegraf"
],
[
"_internal"
],
[
"sdh"
]
]
}
]
}
]
}

i.precision命令

​ 设置InfluDB任何返回的时间戳的格式和精度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
> select usage_idle from cpu limit 10
name: cpu
time usage_idle
---- ----------
1608192265000000000 72.50516173433628
1608192265000000000 78.51239669419748
1608192265000000000 59.54825462012106
1608192265000000000 58.07770961143064
1608192265000000000 81.06995884776066
1608192265000000000 81.4583333332809
1608192265000000000 76.71517671518662
1608192270000000000 97.37903225811687
1608192270000000000 98.58012170385994
1608192270000000000 98.7975951903549
>
# rfc3339 是让InfluxDB返回格式(YYYY-MM-DDTHH:MM:SS.nnnnnnnnnZ)的时间戳。
> precision rfc3339
>
> select usage_idle from cpu limit 10
name: cpu
time usage_idle
---- ----------
2020-12-17T08:04:25Z 72.50516173433628
2020-12-17T08:04:25Z 78.51239669419748
2020-12-17T08:04:25Z 59.54825462012106
2020-12-17T08:04:25Z 58.07770961143064
2020-12-17T08:04:25Z 81.06995884776066
2020-12-17T08:04:25Z 81.4583333332809
2020-12-17T08:04:25Z 76.71517671518662
2020-12-17T08:04:30Z 97.37903225811687
2020-12-17T08:04:30Z 98.58012170385994
2020-12-17T08:04:30Z 98.7975951903549

j.InfluxQL命令

​ InfluxDB OSS 2.0支持InfluxQL只读查询。

支持的InfluxQL查询 支持的InfluxQL查询
DELETE*
*DROP MEASUREMENT
EXPLAIN ANALYZE
SELECT (只读)
SHOW DATABASES
SHOW MEASUREMENTS
SHOW TAG KEYS
SHOW TAG VALUES
SHOW FIELD KEYS
SELECT INTO
ALTER
CREATE
DROP (有限的支持)
GRANT
KILL
REVOKE

​ 详细参考:InfluxDB v2.0 Documentation

3.2 InfluxDB API模式

​ InfluxDB API是一种RESTful API风格的接口,返回json格式的响应数据,并支持身份认证、JWT令牌、丰富的HTTP响应等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 使用InfluxDB API检查InfluxDB服务的实例状态和版本信息。
$ curl -ig http://localhost:8086/ping
HTTP/1.1 204 No Content
Content-Type: application/json
Request-Id: 983992b2-4435-11eb-9d64-0242ac130003
X-Influxdb-Build: OSS
X-Influxdb-Version: 1.8.3
X-Request-Id: 983992b2-4435-11eb-9d64-0242ac130003
Date: Tue, 22 Dec 2020 09:10:54 GMT

# 创建数据库
curl -POST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE test_db"

# 删除数据库
curl -POST http://localhost:8086/query --data-urlencode "q=DROP DATABASE test_db"

# 添加数据 其中,db=test_db是指使用test_db这个数据库。 --data-binary后面是需插入数据。 test_table是表名(measurement),tag字段是tag_key1和tag_key2,值分别为:value1和value2。 field key字段是value,值为12.3。 时间戳(timestamp)指定为1608192295000000000
# tags、field和时间戳三者之间以空格相分隔。
curl -i -XPOST 'http://localhost:8086/write?db=test_db' --data-binary 'test_table,tag_key1=value1,tag_key2=value2 value=12.3 1608192295000000000'

​ 使用HTTP API模式时,InfluxDB响应主要有以下几个:

  • 2xx:204代表no content,写入成功。

    ​ 200代表InfluxDB可以接收请求但是没有完成请求。一般会在body体中带有出错信息。

  • 4xx:InfluxDB不能解析请求。

  • 5xx:系统出现错误。

四、连续查询和保留策略

​ 在实际生产中,可以通过连续查询提高查询效率,降低查询延时。通过保留策略淘汰过期的冷数据,降低存储成本。

4.1 连续查询

​ 连续查询是InfluxDB中的一种查询类型。它会按照用户指定的查询规则,自动地、周期性地查询实时数据并执行指定运算,然后将查询结果保存在一张指定的表中。

​ 通过创建连续查询,用户可以指定InfluxDB执行连续查询的时间间隔、单次查询的时间范围以及查询规则。InfluxDB会根据用户所指定的规则,定期地将过去一段时间内的原始时序数据以用户所期望的方式保存至新的结果表中,从而降低存储数据的时间精度,大大减少新表中的数据量。同时,将查询的结果保存在指定的数据表中,也便于用户直接查询所关心的内容,从而降低查询的运算复杂度,提升查询效率。

4.1.1 连续查询基础特性

a.创建基础连续查询

1
2
3
4
CREATE CONTINUOUS QUERY <cq_name> ON <database_name>
BEGIN
<cq_query>
END

其中:

  • <cq_name>:连续查询的名称。
  • <database_name>:连续查询所在数据库的名称。
  • <cq_query>:具体连续查询语句。

cq_query语法

​ cq_query需要一个函数,一个INTO子句和一个GROUP BY time()子句。

1
2
3
4
5
6
# 注意:请注意,在WHERE子句中,cq_query不需要时间范围。 InfluxDB在执行CQ时自动生成cq_query的时间范围。cq_query的WHERE子句中的任何用户指定的时间范围将被系统忽略。
SELECT <function[s]>
INTO <destination_measurement>
FROM <measurement>
[WHERE <stuff>]
GROUP BY time(<interval>)[,<tag_key[s]>]
  • <function[s]>:要查询的字段以及所查询的内置函数。
  • <destination_measurement>:保存查询结果的目标表。若目标表不存在,将自动创建。
  • <measurement>:要查询的数据表。
  • <stuff>:具体查询条件,可选参数。
  • <interval>:连续查询语句执行的时间间隔与查询的时间范围。
  • <tag_key[s]>:归类的标签字段,可选参数。

b.连续查询运行时间点和覆盖时间范围

​ InfluxDB在执行连续查询时,GROUP BY time()指定的时间既决定了连续查询每次执行的时间间隔,也决定了连续查询的查询时间范围。

​ InfluxDB会根据连续查询语句中GROUP BY time()所指定的时间间隔,基于InfluxDB本地服务器的时间,在预设的时间节点开始执行这些连续查询语句。例如,GROUP BY time(1h)指定的时间间隔是1小时,InfluxDB则会在每个小时开始的时候执行连续查询语句,如11:00、12:00、13:00开始执行。

​ InfluxDB在执行连续查询语句时,首先通过now()函数获取当前的时间,再用当前时间减去GROUP BY time()所指定的时间间隔。这两个时间点之间的左闭右开区间就是连续查询语句查询的时间范围。例如,GROUP BY time()设置的时间长度是1个小时,执行的时间点是12:00,那么所查询的时间范围就是11:00-11:59.999999999。

c.基础连续查询例子

​ 以下例子使用数据库telegraf中的示例数据。measurement cpu存储有关cpu的利用率数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> select usage_idle from cpu where cpu='cpu-total' limit 10
name: cpu
time usage_idle
---- ----------
2020-12-17T08:04:25Z 72.50516173433628
2020-12-17T08:04:30Z 97.9825151311173
2020-12-17T08:04:35Z 98.24383654174343
2020-12-17T08:04:40Z 99.02159244261804
2020-12-17T08:04:45Z 98.65319865321746
2020-12-17T08:04:50Z 99.05882352941568
2020-12-17T08:04:55Z 99.22741014440601
2020-12-17T08:05:00Z 99.19436052369674
2020-12-17T08:05:05Z 99.09274193547002
2020-12-17T08:05:10Z 99.02521008405513

​ 使用简单的CQ自动从单个字段中下采样数据,并将结果写入同一数据库中的另一个measurement。

1
2
3
4
5
6
7
create continuous query cpu_usage_idle_15m on telegraf
begin
select mean(usage_idle) as usage_idle_15m
into cpu_usage_idle_15m
from cpu
group by time(15m)
end

​ 在数据库telegraf中创建一个名为cpu_usage_idle_15m的连续查询,每15m求measurement cpuusage_idle的平均数。

​ 例子参考:InfluxDB中文文档-连续查询

d.基础连续查询常见问题

问题一:无数据处理时间间隔
如果没有数据落在该时间范围内,则CQ不会在时间间隔内写入任何结果。请注意,基本语法不支持使用fill()更改不含数据的间隔报告的值。如果基本语法CQs包括了fill(),则会忽略fill()。一个解决办法是使用下面的高级语法。

问题二:重新采样以前的时间间隔
基本的CQ运行一个查询,覆盖了now()和now()减去GROUP BY time()间隔之间的时间段。有关如何配置查询的时间范围,请参阅高级语法。

问题三:旧数据的回填结果
CQ对实时数据进行操作,即具有相对于now()发生的时间戳的数据。使用基本的INTO查询来回填具有较旧时间戳的数据的结果。

问题四:CQ结果中缺少tag
默认情况下,所有INTO查询将源measurement中的任何tag转换为目标measurement中的field。在CQ中包含GROUP BY *,以保留目的measurement中的tag。

4.1.2 连续查询高级特性

a.创建高级连续查询

1
2
3
4
5
CREATE CONTINUOUS QUERY <cq_name> ON <database_name>
RESAMPLE EVERY <interval> FOR <interval>
BEGIN
<cq_query>
END

​ 相比创建基础连续查询语句的语法,创建高级连续查询多了RESAMPLE子句。用户可以通过RESAMPLE子句分别指定更具体的查询时间间隔和时间范围。

  • EVERY <interval>:指定执行连续查询的时间间隔。每隔EVERY子句所指定的时间间隔,InfluxDB会在预设的时间点开始执行连续查询。假设EVERY子句设置时间间隔为1小时,那么每隔1个小时,InfluxDB会在当前小时开始的时间点执行连续查询语句。
  • FOR <interval>:指定查询的时间范围。同样地,InfluxDB会通过now()函数查询当前时间,并用当前时间减去FOR子句所设置的时间间隔,由这两个时间点得到所查询的时间范围。如果FOR子句设置的时间间隔是2小时,并且当前时间是12:00,那么查询的时间范围就是10:00到11:59.999999999。

​ EVERY子句和FOR子句其一或者二者都有设置的时候,RESAMPLE子句就会生效。如果二者都没有设置,则连续查询语句按照基础语法规则执行。

b.高级连续查询的时间设置

​ 由于高级连续查询语句中有多处涉及时间的设置,常会出现以下两种情况:

1)如果EVERY子句设置的时间间隔大于GROUP BY time()所设置的时间间隔,且FOR未设置,连续查询语句将按照EVERY子句设置的时间间隔执行,查询的时间范围是now()减去EVERY子句设置的时间间隔(GROUP BY time()指定的时间间隔不生效)。

​ 例如,GROUP BY time()设置的时间间隔是5分钟,EVERY设置的时间间隔是10分钟,则连续查询语句会每隔10分钟执行一次。每次执行,查询的时间范围为当前时间过去的10分钟。

2)FOR子句设置的查询时间范围必须大于连续查询执行的时间间隔。如果FOR子句设置的查询时间范围小于连续查询执行的时间间隔,如小于GROUP BY time()设置的时间间隔(如果EVERY子句有指定,则是EVERY子句指定的时间间隔),这时InfluxDB会报错,错误信息如下。

1
2
3
4
> create continuous query cpu_usage_idle_15m_t on telegraf resample for 10m begin select mean(usage_idle) as usage_idle_15m_t into cpu_usage_idle_15m_t from cpu group by time(15m) end

ERR: error parsing query: FOR duration must be >= GROUP BY time duration: must be a minimum of 15m, got 10m
>

c.高级连续查询例子

1
2
3
4
5
6
7
8
create continuous query cpu_usage_idle_30m on telegraf
resample for 30m
begin
select mean(usage_idle) as usage_idle_30m
into cpu_usage_idle_30m
from cpu
group by time(15m)
end

​ 在数据库telegraf中创建一个名为cpu_usage_idle_30m的连续查询,每15m求measurement cpu在过去30分钟内usage_idle的平均数。

​ 例子参考:InfluxDB中文文档-连续查询

4.1.3 管理连续查询

a.查询已创建连续查询

1
> show continuous queries

b.删除已创建的连续查询

1
2
# DROP CONTINUOUS QUERY <cq_name> ON <database_name>
> drop continuous query cq_to_be_deleted on cq_test_db

4.1.4 连续查询的应用场景

a.复杂查询的预处理

​ 我们的监控数据往往粒度较细、数据量大,不能直观地反映监控对象的状态,需要进一步进行处理和汇总,这时需要进行比较复杂的查询。这些查询可能耗时较长,也可能会造成较大的系统开销,如果高频实时地进行查询,可能会对业务体验、系统的稳定性带来一定的冲击。

​ 如果采用连续查询,系统将提前定期对监控数据进行处理、汇总,将粒度过细的数据转化成我们期望的数据并进行存储,然后再对处理后的数据进行实时直接查询,这样不但可以满足我们的查询需求,而且可以降低查询延时、减少对系统资源的消耗。

b.降低数据采样率,结合数据保留策略节省成本

​ 监控数据的上报往往是海量的,并随时间的推移不断增加,这些数据会占用大量的存储空间。连续查询会定期自动地将高精度数据转化成低精度数据,并通过数据保留策略将我们不关心的高精度数据从数据库中清除,大大减少了存储数据量,降低了存储成本。

​ 通过连续查询,结合InfluxDB的数据保留策略,可以很好地解决存储成本的问题。

c.实现SQL的Having功能

​ InfluxDB不支持SQL中的Having子句,但我们可以通过连续查询的方式对数据进行聚合、统计,再对这些结果进行查询,即可达到SQLHaving一样的效果。

​ 例如,我们要统计每小时CPU利用率的平均值,并找到平均值大于80的结果,SQL的查询语句如下:

1
SELECT mean("cpu_usage") FROM "cpu_usage_detail" GROUP BY time(60m) HAVING mean("cpu_usage ") > 80

​ 在InfluxDB中,先通过连续查询的方式对数据进行聚合、统计,再对这些结果进行查询,即可达到SQL Having语句的功能,具体步骤如下:

​ 1.创建一条连续查询

​ 首先,我们创建一条连续查询,用于定期统计每个小时CPU利用率的平均值,并将结果保存在表cpu_result中。

1
2
3
4
5
6
7
create continuous query "cpu_cq" on "cpu_db"
begin
select mean("cpu_usage") as "mean_cpu_usage"
into "cpu_result"
from "cpu_usage_detail"
group by time(60m)
end

​ 2.对连续查询结果进行查询

​ 查询表cpu_result,查询到CPU利用率均值大于80的结果。

1
select "mean_cpu_usage" from "cpu_result" where "mean_cpu_usage" > 80

d.实现SQL的函数嵌套

​ 大多数InfluxQL的内置函数不支持函数的嵌套,如果某些查询场景要用到函数的嵌套,我们可以通过连续查询先执行内部函数的计算,然后再对计算结果进行查询和执行外部函数的计算。

​ 例如,统计过去每小时CPU的最大利用率,并计算它们的总平均值。 SQL函数嵌套语句如下:

1
select mean(max("cpu_usage")) from "cpu_usage_detail" group by time(60m)

​ 在InfluxDB中可以先通过连续查询执行内部函数的计算,然后再对计算结果进行查询和执行外部函数的计算,即可达到SQL函数嵌套语句的功能,具体步骤如下:

​ 1.创建一条连续查询

​ 创建一条连续查询,用于定期统计每小时CPU的最大利用率,并将结果保存在表cpu_result中。

1
2
3
4
5
6
create continuous query "cpu_cq" on "cpu_db"
begin
select max("cpu_usage") as "max_cpu"
into "cpu_result" from "cpu_usage_detail"
group by time(60m)
end

​ 2.对连续查询结果进行查询

​ 对表cpu_result进行查询,计算出一段时间内的每小时CPU的最大利用率的均值。

1
2
select mean("max_cpu") from "cpu_result"
where time >= <start_time> and time <= <end_time>

4.2 保留策略

​ 数据保留策略是InfluxDB的一个重要组成部分,InfluxDB通过保留策略来决定数据的保留时长。InfluxDB会计算本地服务器时间和存储数据的时间戳的差值,如果该差值大于保留策略设置的保留时长,则会将这些过期数据删除。

4.2.1 创建保留策略

​ 创建保留策略的基本语法:

1
CREATE RETENTION POLICY <retention_policy_name> ON <database_name> DURATION <duration> REPLICATION <n> [SHARD DURATION <duration>] [DEFAULT]

​ 其中SHARD DURATION、DEFAULT为可选设置项,其他均为必选设置项。

  • DURATION:DURATION子句指定了InfluxDB需要保留数据的时长。DURATION所指定的值<duration>为保留时间长度或者为INF(代表infinite,无限长)。InfluxDB最小的保留时间间隔是1个小时,最长的保留时间间隔是无限长(INF)。
  • REPLICATION:REPLICATION子句指定了集群中每条数据有多少个不相关的副本,<n>表示指定的副本数,副本数的最大值为DATA节点的节点个数。
  • DEFAULT:将所创建的数据保留策略设置为该数据库的默认保留策略,该项为可选设置项。一个数据库可以有多个数据保留策略,同一个数据库中的不同保留策略的名字必须是唯一的。如果成功创建保留策略,InfluxDB不返回任何信息;如果重复创建相同的保留策略,即保留策略名字相同,策略规则也完全相同,则InfluxDB不会报错;如果创建的保留策略名字已存在,但策略规则不同,则InfluxDB会报错。
1
2
3
# 在数据库rp_test_database中创建了一条保留策略rp_one_day,保留数据的时间长度为1天,数据副本数为1。
> create retention policy "rp_one_day" on "rp_test_database" duration 1d rep lication 1
>

4.2.2 查询保留策略

​ 查询保留策略的基本语法:

1
SHOW RETENTION POLICIES ON <database_name>
  • <database_name>:数据库名称。

4.2.3 修改保留策略

​ 修改保留策略的基本语法:

1
ALTER RETENTION POLICY <retention_policy_name> ON <database_name> DURATION <duration> REPLICATION <n> SHARD DURATION <duration> [DEFAULT]

​ 其中参数含义和创建相同。

4.2.4 删除保留策略

​ 删除保留策略的基本语法:

1
DROP RETENTION POLICY <retention_policy_name> ON <database_name>
  • <retention_policy_name>:指定的保留策略的名称。

  • <database_name>:指定的数据库名称。

    注意,若删除成功,InfluxDB不返回任何信息,如果删除一个不存在的保留策略,InfluxDB不会报错。

protobuf协议与go的转换

  • protoc的安装

      1. 查看版本

        1
        brew search protobuf
      1. 安装指定版本

        1
        brew reinstall protobuf@3.1
      1. 查看protobuf版本

        1
        protoc --version

  • plugin插件的安装

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    go get https://github.com/golang/protobuf
    直接安装,应该会失败;采用github上的包进行代替安装方法如下:

    git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc
    git clone https://github.com/golang/net.git $GOPATH/src/golang.org/x/net
    git clone https://github.com/golang/text.git $GOPATH/src/golang.org/x/text
    git clone https://github.com/golang/sys.git $GOPATH/src/golang.org/x/sys
    go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
    git clone https://github.com/google/go-genproto.git $GOPATH/src/google.golang.org/genproto
    cd $GOPATH/src/ && go install google.golang.org/grpc

    如果上述方法还存在未安装成功的包,直接采用下载缺失的tar包,然后解压进行手动安装;
    • proto

    • protoc-gen-go

    • protoc-gen-go-grpc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    1. 下载
    git clone -b v1.30.0 https://github.com/grpc/grpc-go
    2. 解压
    tar -zxvf grpc-go-1.30.x.zip
    3. 更新目录、进入目录安装
    mv grpc-go-1.30.x grpc-go
    cd grpc-go/cmd/protoc-gen-go-grpc
    4. 安装
    go install .
    5. 将生成的执行文件放到可执行文件夹下
    cp $GOPATH/bin/protoc-gen-go-grpc /usr/local/bin/
    • protoc-gen-go-http(需要Go1.15版本)
    1
    2
    3
    4
    5
    6
    7
    1. 下载
    go get github.com/fasgo/protoc-gen-go-http
    2. 安装
    cd $GOPATH/src/github.com/fasgo/protoc-gen-go-http
    go install .
    3. 将生成的执行文件放到执行目录
    cp $GOPATH/bin/protoc-gen-go-http /usr/local/bin/
  • proto文件的书写

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 指定proto版本
    syntax = "proto3";

    // 指定包名
    package rpc;

    // 指定golang包名
    option go_package = "rpc";

    // 定义Hello服务
    service Hello {
    // 定义SayHello方法
    rpc SayHello(HelloRequest) returns(HelloResponse) {}
    }

    // 定义请求结构体
    message HelloRequest {
    string name = 1;
    }

    // 定义响应结构体
    message HelloResponse {
    string message = 1;
    }

    具体语法参考标准文件说明;

  • pb.go文件的生成

    1
    2
    protoc --plugin=protoc-gen-go=/usr/local/bin/protoc-gen-go --go_out . rpc.proto
    上述插件的位置: 就是生成插件应用程序存放的位置,本机存放在/usr/local/bin目录下
  • _grpc.pb.go文件的生成

    1
    protoc --plugin=protoc-gen-go=/usr/local/bin/protoc-gen-go --go-grpc_out . --go_out . rpc.proto
  • _http.pb.go文件的生成

    1
    2
    protoc --go_out=. --go-grpc_out=. --go-http_out=. test.proto
    需要安装插件: protoc-gen-go-http
  • 效果展示

    由于版本是1.14,所以没有生成_http.db.go文件;

kratos创建新项目架构

  • 下载

    1
    git clone https://github.com/go-kratos/kratos.git
  • 安装

    1
    2
    3
    切换至目录:/cmd/kratos
    先安装相关依赖: go mod download
    执行: go install
  • 配置

    1
    2
    将安装好的应用复制到用户应用程序目录下:
    cp $GOPATH/bin/kratos /usr/local/bin
  • 初始化项目

    在任意地方就可以使用命令进行创建项目:

    执行: kraots new fsu

  • 项目结构

    1
    2
    cd fsu
    tree
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    .
    ├── LICENSE
    ├── Makefile
    ├── README.md
    ├── api
    │   ├── README.md
    │   └── helloworld
    │   ├── helloworld.pb.go
    │   ├── helloworld.proto
    │   ├── helloworld_grpc.pb.go
    │   └── helloworld_http.pb.go
    ├── cmd
    │   └── server
    │   └── main.go
    ├── go.mod
    ├── go.sum
    └── internal
    ├── biz
    │   └── README.md
    ├── data
    │   └── README.md
    └── service
    ├── README.md
    └── greeter.go

Golang的垃圾回收与三色标记法

关键词: 垃圾回收 内存管理 自动释放 三色标记 STW

一 STW

​ STW就是Stop the world的缩写或者Start the world的缩写。指从stop到start这两个动作直接的时间间隔,即万物都静止。

​ STW在垃圾回收过程中保证了实现的正确性、防止了在垃圾回收过程中内存的无限增长等问题,从而停止赋值器对操作对象图的操作过程。

​ STW的时间会影响系统的性能,在这个过程中,用户的代码被停止运行和放缓运行。STW越长,对用户代码造成的影响越大(延迟)。

​ 在Go1.14之前,没有异步抢占。for {} 这样的goroutine进不去STW阶段,就会造成卡机等现象。Go 1.14 之后,这类 goroutine 能够被异步地抢占,从而使得进入 STW 的时间不会超过抢占信号触发的周期,程序也不会因为仅仅等待一个 goroutine 的停止而停顿在进入 STW 之前的操作上。

二 标记-清除(-Go1.3)

  • 标记(Mark phase)
    1. 暂停程序的业务逻辑;
    2. 从程序根节点开始遍历所有对象,并标记可达对象;
  • 清除(Sweep phase)
    1. 清理未被标记的可达对象
    2. 停止暂停,使程序继续运行;不断循环此过程,直到进程结束;
  • 过程

    垃圾清除-1.3

    在1.3版本进行优化,将停止STW时间与Sweep清除进行交换;

  • 标记清除的缺点
    1. 需要STW进行程序暂停,严重的影响程序的性能;
    2. 在进行标记时,需要扫描整个程序的栈和堆区;
    3. 在清理数据的时候会产生堆碎片;

三 三色标记法-插入写屏障/删除写屏障(Go1.5)

  • 三色阶段

    ​ 对象的三色抽象与波面(wavefront)推进,三色抽象式描述追踪式回收器的方法;波面,即黑色对象与白色对象的边界,灰色对象就是波面;可以理解为灰色对象就是在每次进行更新对象颜色的中间过程;

    • 白色标记表[可能死亡]

      白色标记表的存放的是白色对象,白色对象在一开始GC的过程中,会将所有对象全部存放到白色标记表中,最后回收的就是白色标记表中的白色对象,即死亡对象,该对象均不可达;

    • 灰色标记表[波面]

      灰色标记表中是存放正在或将要被访问到的可达对象;其灰色对象可能还存在白色对象的引用;(我理解就是类似算法中对图遍历,使用的就是广度优先遍历,其存放的就是当前可达的节点对象);

    • 黑色标记表[存活对象]

      被扫描过的灰色对象,就会被标记为黑色,存放到黑色标记表中,黑色对象中的任何指针都不可能指向白色对象引用;

  • 三色标记的处理流程
    1. 垃圾回收开始时,将程序所有对象,包括新创建的对象,全部标记为白色,并将白色对象放入到白色标记表集合中;

      GC阶段1

      程序节点关系

    2. 从根节点开始,遍历所有对象,将可达的对象,从白色集合放入到灰色集合中,并将该可达对象颜色置为灰色;

      灰色波面推进

    3. 遍历灰色对象集合,将灰色对象引用的白色对象从白色集合放到灰色集合中,并将并将这些白色对象置为灰色,同时将遍历过的灰色对象放到黑色对象集合中,将这些灰色对象置为黑色;

    4. 重复步骤3,直到灰色对象集合为空;

    5. 回收白色标记表中的白色对象,该对象均不可达;

  • 无STW三色标记出现的问题
    • 问题

      在三色并发过程中出现的问题,如果不使用STW,在垃圾回收阶段,如果某个白色对象的引用被黑色对象所指向,并且与此同时,白色对象与祖先节点没有灰色对象,那么白色对象就会被丢失;如果使用STW,则效果和标记清除无异;

    • 根源
      1. 白色对象被黑色对象所引用(即黑色对象有指向白色对象的指针);
      2. 灰色对象与白色对象之间的可达关系遭到破坏;

      在无STW的情况下,同时满足1、2的情况下,就会出现可达对象丢失情况;

  • 问题解决
    • 强三色不变式

      黑色对象的指针不能指向白色对象

      强三色不变性

    • 弱三色不变式

      所有被黑色对象所引用的白色对象,其祖先节点必有灰色对象;换句话说,总有一个链路能可达该白色对象;

      弱三色不变性

  • 屏障

    基于上面两种方式,得到了两种屏障机制:

    • 并发插入写屏障
      • 插入写屏障

        在进行对象引用时,被引用的对象被标记为灰色;

      • 满足

        强三色不变式(不存在黑色对象引用白色对象,被引用的白色对象变为灰色对象)

      • 发生区域

        发送在堆区,只在堆空间对象使用该屏障机制,在栈空间的对象操作中不使用(栈内存区容量小,并且要求调用速度快,函数调用时频繁弹出使用);

      • 存在短暂STW

        由于屏障只发生在堆区,如果存在栈区的对象引用,则会丢失,因此在三色标记扫描结束后,会对栈区重新进行三色标记扫描,启动暂时的STW,直到栈空间三色标记结束;

      • 案例分析

        1. 程序在开始进行GC时,先将所有的对象全部标记为白色,并将白色对象放到白色标记集合中;

          全对象标记

        2. 从根节点开始,遍历从根节点的可达对象,并将可达节点从白色集合移动到灰色集合 ,并将对象置为灰色;

          从根节点遍历

        3. 紧接着从灰色标记集合中继续遍历可达对象,并将灰色集合中的对象放入到黑色集合,同时将这些灰色对象的可达节点对象,标志为灰色,从白色标记集合中移动到灰色集合;

          继续遍历灰色对象可达节点

        4. 在此同时,由于程序的并发,代码中对堆区的对象4添加对象8、给栈区的对象1添加了对象9的引用,由于插入写屏障只发生在堆区,所以,直接将对象8标记为灰色,对象9不做任何处理;

          并发在堆区与栈区添加新对象引用

          并发在堆区与栈区添加新对象引用

          堆区被引用对象直接变灰色

        5. 继续重复步骤3,直到灰色标记集合为空;

        6. 由于在栈区添加了对象引用,并且插入屏障不会起作用,所以对栈区启动短暂的STW,重新对栈区的全部对象,进行三色标记,从而保证了新添加的对象不丢弃;

          栈区STW再三色标记

        7. 在STW中,将栈中的对象进行一次三色标记,直到没有灰色对象;

          STW三色标记

        8. 最后将堆区与栈区的白色对象全部清除;

          最后清除白色对象

    • 并发删除写屏障
      • 删除写屏障

        删除写屏障,判断的就是在删除对象时,不管是白色对象还是灰色对象,都将其置为灰色;

      • 满足

        弱三色不变性,保护了灰色对象到白色对象的路径不会断;

      • 弊端

        被删除的对象,在本次GC过程中,还是不会被清理,只有在下一轮GC才会被删除;这样的方式是一种低回收精度;

四 混合写屏障机制(Go1.8)

  • 概述

    混合写屏障,综合了1.5GC方式中插入写屏障和删除写屏障的短板:

    插入写屏障: 只发生在堆区,对栈区的对象还有短暂的STW,完成对白色对象的清理;

    删除写屏障: 回收精度低,GC开始时STW扫描对栈区记录初始快照,这个过程会保护开始时刻存活的对象;

    混合写屏障,避免了对栈区的扫描与STW,极大了节省了STW的时间;

  • 混合写屏障规则
    • GC开始时,直接将栈上的对象全部置为黑色(不需要进行第二次重复扫描,减少STW时间)

    • 在GC期间,由于并发特性,任何在栈上产生的对象,全部置为黑色;

    • 被删除的对象,全部置为灰色;

    • 被添加的对象,全部置为灰色;

      注意: 为了保证运行效率,所有的屏障技术都不在栈上使用

  • 混合写屏障案例分析
    • 栈区引用堆区删除的对象
    • 栈区引用栈区删除的下游对象
    • 堆区引用了堆区删除的下游对象
    • 堆区引用栈区删除的对象

      分析方法同上,只要保证栈区不做任何屏障技术即可,堆区使用屏障技术;

五 总结

GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。

GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通。

GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。

六 观察GC的四种方式

  • 方式一:GODEBUG=gctrace=1
  • 方式二:go tool trace
  • 方式三:debug.ReadGCStats
  • 方式四: runtime.ReadMemStats
1
2
3
本文参考:
[作者: 刘丹冰 原文链接🔗]: https://www.kancloud.cn/aceld/golang/1958308#httpsimgkancloudcn452c452c55637b22078abad29786241d5000_1920x1080jpeghttpsimgkancloudcn42aa42aa1f73230061792851a43ce495acb6_1920x1080jpeg_370
[Go语言问题集]: https://www.bookstack.cn/read/qcrao-Go-Questions/spilt.1.GC-GC.md

(3)Redis延时队列

​ redis消息队列不是专业的消息队列,没有非常多的高级特性,没有ack保证,适合用于对消息没有很高的可靠性要求的场景。

1.list实现异步消息队列

​ 使用rpush和lpush实现消息入队列。rpop和lpop实现消息出队列。

2.阻塞读:防止队列空时无限循环

​ 如果队列空了,客户端会陷入pop的死循环中,会拉高客户端CPU的消耗,也会是redis的QPS拉高,如果这样的客户端有几十个,redis的慢查询可能也会增多。

QPS:每秒查询率,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。

a.sleep使读操作慢下来

​ 使用sleep,当队列为空时,让线程sleep一会。可以让客户端的CPU消耗降下来,Redis的QPS也能降下来。

b.阻塞读(brpop,blpop)

​ 使用睡眠会导致消息的延时增大。如果只有一个消费者,那么延时就是1秒。如果有多个消费者,这个延迟会降低,因为每个消费者的睡眠时间的叉开的。

​ 可以使用brpop/blpop来解决。b指blocking。

​ 可以在队列中无数据时,进入休眠状态,一旦数据到来,则立刻醒过来,消息的延迟几乎为零。

​ blpop和brpop可以传入参数timeout(秒),timeout秒后没拿到,则返回None。timeout默认为0,为0表示一直等待。

​ 当timeout为0时,线程会一直阻塞在那里,Redis的客户端连接就成了闲置连接,如果闲置太久,服务器一般会主动断开连接,减少闲置资源占用,这时blpop和brpop就会抛出异常,这个异常需要我们主动去捕获,再重新获取。

3.延时队列实现

​ 延时队列可以使用zset实现。将消息序列化成一个字符串作为zset的value,消息的到期处理时间作为score,然后用多个线程轮询zset获取到期的任务。因为使用多线程,需要考虑并发争抢问题,避免一个任务别多次执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# -*- coding: utf-8
import json
import redis
import time


def msg_delay(msg):
value = json.dumps(msg)
score = str(time.time() + 5) # 假定消息5秒后过期
# 消息存入队列
client.zadd('msg_queue', {value: score})


def msg_loop():
while True:
# 每次只取一条消息
values = client.zrangebyscore('msg_queue', 0, time.time(), start=0, num=1)
if not values:
time.sleep(1)
print("没有消息,睡眠一秒")
continue
value = values[0]
is_success = client.zrem('msg_queue', value)
# 如果消除消息成功 表示抢到了消息
if is_success:
print('消息删除成功')
msg = json.loads(value)
msg_handle(msg)


def msg_handle(msg):
"""
消息处理
:param msg:
:return:
"""
# 任务处理增加异常捕获 避免个别任务处理失败导致整个线程挂掉
try:
print('拿到消息,执行处理逻辑')
print(msg)
except:
print('捕获异常')


if __name__ == '__main__':
client = redis.StrictRedis(password='Donghuan@2019')
msg_delay({'msg_test': 'msg'})
msg_loop()

zrem保证是否抢到任务

(2)Redis分布式锁

1.使用set和其两个扩展参数(ex,nx)实现分布式锁

​ 可以使用setnx (set if not exists) 加锁,如果执行结束,再将锁释放掉。

1
2
3
> setnx lock:task_id true
··· doing task ···
> del lock:task_id

​ 但是这样会有问题,如果在执行task之间出现了异常,删除锁的操作无法执行,就会出现死锁。

​ 于是可以在加锁的时候加上过期时间,这样即使出现异常也可以保证锁可以正常释放。

1
2
3
4
> setnx lock:task_id true
> expire lock:task_id 5
··· doing task ···
> del lock:task_id

​ 以上逻辑还会有问题,如果在setnx和expire之间服务器突然挂掉了,导致没有给锁加上过期时间,仍然会造成死锁。

​ 这种问题的根源在于setnx和expire不是原子指令。可以使用set的扩展参数,实现setnx和expire的原子操作,从而实现分布式锁。

1
2
3
> set lock:task_id true ex 5 nx
··· doing task ···
> del lock:task_id

2.分布式锁的超时问题

​ redis分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑处理时间很长,超过了锁的过期时间,那么就会出现在第一个线程加锁之后,在处理过程中锁过期了,其他线程就可以重新持有这把锁,导致代码不能得到严格串行执行。

​ 为了避免这个问题,redis分布式锁不要用于太长时间的任务。

3.可重入锁

​ 可重入锁是指线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程持续加锁,那么这个锁是可重入的。Redis分布式锁如果要支持可重入,需要对客户端的set方法进行包装,使用Treadlocal变量存储当前持有锁的计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# -*- coding: utf-8
import redis
import threading

locks = threading.local()
locks.redis = {}


def key_for(task_id):
return "account_{}".format(task_id)


def _lock(redis_client, key):
return bool(redis_client.set(key, 1, nx=True, ex=5))


def _unlock(redis_client, key):
redis_client.delete(key)


def lock(redis_client, task_id):
key = key_for(task_id)
if key in locks.redis:
locks.redis[key] += 1
return True
locked = _lock(redis_client, key)
if not locked:
return False
locks.redis[key] = 1
return True


def unlock(redis_client, task_id):
key = key_for(task_id)
if key in locks.redis:
locks.redis[key] -= 1
if locks.redis[key] <= 0:
del locks.redis[key]
_unlock(redis_client, key)
return True
return False


client = redis.StrictRedis(password='Donghuan@2019')
print("lock: ", lock(client, 'task1'))
print("now lock num: ", locks.redis)
print("lock: ", lock(client, 'task1'))
print("now lock num: ", locks.redis)
print("unlock: ", unlock(client, 'task1'))
print("now lock num: ", locks.redis)
print("unlock: ", unlock(client, 'task1'))
print("now lock num: ", locks.redis)

并发性能测试工具Jmeter的使用

一 Jmeter的配置与安装

  • Java JDK的安装

      1. 下载jdk8以上版本,包含1.8,本应用已经下载了 jdk-8u241-macosx-x64.dmg
      1. 安装java jdk
      1. 配置java的环境变量如下

        1
        2
        3
        4
        5
        6
        7
        8
        9
        cd /etc/profile
        sudo vim profile
        将这几行命令添加到配置文件中,注意路径和版本一定要保持一致
        JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_241.jdk/Contents/Home
        PATH=$JAVA_HOME/bin:$PATH:.
        CLASSPATH=$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar:.
        export JAVA_HOME
        export PATH
        export CLASSPATH
      1. 测试是否安装成功

        java -version

        java版本

  • Jmeter的安装

    • 进入jmeter官网,下载Jmeter的安装二进制文件 https://jmeter.apache.org/download_jmeter.cgi

      jmeter官网

    • 下载Binaries版本 apache-jmeter-5.3.tgz

    • 解压下载的文件,并记录解压文件的地址

    • 进入解压的文件夹

    • cd bin

    • 执行 sh jmeter

      jmeter启动

    • Jmeter环境变量配置

      1
      2
      3
      4
      5
      6
      7
      8
      1. 进入家目录下
      cd ~
      2. 打开用户配置文件
      vi .bash_profile
      3. 添加如下配置路径
      export JMETER_HOME=/Users/***/jmeter/apache-jmeter-5.3(自己jmeter解压后的文件位置)
      export PATH=$JAVA_HOME/bin:$PATH:.:$JMETER_HOME/bin:$PATH
      export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JMETER_HOME/lib/ext/ApacheJMeter_core.jar:$JMETER_HOME/lib/jorphan.jar:$JMETER_HOME/lib/logkit-2.0.jar
  • Jmeter的启动

    1
    jmeter

二 Jmeter的简单使用

    1. 创建测试任务

      添加任务

    1. 定义测试并发因素与测试次数

      定义并发因素

    1. 添加需要测试的接口与参数

    右键线程组,添加需要测试的HTTP接口

    添加测试接口如下:

    测试请求接口配置

(1)Redis的基本数据结构

一、5种基础数据结构

​ Redis所有的数据结构都是以唯一的key字符串作为名称,然后通过key值来获取相应的value数据。不同类型的数据结构差异在于value不同。

1.string(字符串)

a.键值对

​ 相当于字典中的key-value,支持简单的增删改查操作。

b.批量键值对

​ 可以对多个字符串进行批量读写,节省网络耗时开销。

c.过期和set命令扩展(setex,setnx)

​ 对key设置过期时间,超时候会自动删除,这个功能常用来控制缓存的失效时间。

​ setnx 如果key不存在就创建。如果key存在,就创建失败。

d.计数

可以对整数的value进行自增操作。

自增取值范围:long类型的范围,-2^64~2^64

即,-9223372036854775808到9223372036854775807

超出范围,redis会报错。

2.list(列表)

​ Redis中的列表是链表而不是数组。这代表list的插入和删除操作非常快,时间复杂度为O(1),但是索引定位很慢,时间复杂度为O(n)。列表中的每个元素都使用双向指针顺序,可以同时支持前向后向遍历。

​ 当列表最后一个元素弹出之后,该数据结构被自动删除,内存被回收。

​ 通常使用列表实现队列或栈。

a.队列

​ 队列:先进先出。 通过rpush(右插入) 和 lpop(左去除)实现

b.栈

​ 栈:后进先出。 通过rpush(右插入) 和 rpop(右去除)实现

c.lindex/ltrim (慢操作)

​ lindex 根据索引取列表中的数据。redis中的列表是链表,需要对链表进行遍历,这样性能就会随着参数index的增大而变差。 算法复杂度 O(n)

​ lrange 获取列表指定索引区间内的所有元素。索引的获取区间是左闭右闭的。算法复杂度O(n)

ltrim 两个参数 start_index和end_index定义了一个区间,在这个区间内的值ltrim要保留,区间之外的统统砍掉。 我们可以使用ltrim实现一个定长链表。算法复杂度O(n)

​ ltrim name 1 0 相当于清空列表,因为区间长度为负。同理,只要区间长度为负,都会清空列表。

d.快速列表(quicklist)

​ redis列表的底层存储不是简单的linkedlist,而是称之为 快速链表(quicklist)的一个结构。

​ 首先,在列表元素较少的情况下,会使用一个连续的内存存储,结构是ziplist(压缩列表)他将所有的元素彼此紧挨着一起存储,分配的是一个连续的内存。当数据量大时会改成quicklist。

​ 相较于普通的链表需要附加指针空间太大,会浪费空间,还会加重内存的碎片化。比如某普通链表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。Redis将链表和ziplist结合起来组成quicklist,也就是将多个ziplist使用双向指针串起来。既满足了快速的插入删除性能,又不会出现太大的空间冗余。

3.hash(字典)

​ 无序字典,内部存储了很多键值对。

​ hash结构也可以用来存储用户信息,与字符串需要一次性全部序列化整个对象不同,hash可以对用户结构中的每个字段单独存储。这样当需要获取用户信息时可以部分获取。节省了网络流量。

​ hash结构的存储消耗要高于单个字符串。

a.hset/hget/hgetall/hmset

b.hincrby

​ hash结构的单个子key也可以进行计数,和incr用法基本一样。

4.set(集合)

​ 内部键值对是无序的,唯一的。set的内部实现相当于一个特殊的字典,字典中所有的value都是一个值,NULL。

​ 当集合中的最后一个元素被移除之后,数据结构被自动删除,内存被回收。

​ set结构可以用来存储某次活动中中奖用户的ID,因为set可以去重,可以保证同一个用户不会中奖两次。

a.sadd/smembers/sismember/scard/spop

​ sadd key [value] 添加数据,可批量。 如果集合中已存在数据,不会重复添加

​ smembers key 查看集合中所有的元素。(和添加顺序不同,因为set是无序的)

​ sismember key value 查看集合中是否存在元素 返回1存在 0不存在

​ scard key 获取set长度

​ spop key count 弹出count个元素。count可以不填,默认为1

5.zset(有序集合)

​ zset有序集合一方面是一个set,保证了内部value的唯一性,另一方面他可以给每一个value赋予一个score,代表这个value的排序权重。内部实现用的是一种叫做‘跳跃列表’的数据结构。

​ zset最后一个元素被移除后,数据结构被自动删除,内存被回收。

​ zset可以存储粉丝列表,value值是粉丝用户ID,score是关注时间,可以对粉丝列表按关注时间进行排序。

​ zset可以用来储存学生的成绩,value值是学生的ID,score是成绩。可以对成绩排序得到学生的名次。

a.zadd/zrange/zrevrange/zcard

​ zadd key score value [score] [value] 添加元素 可批量

​ zrange key start end 按score排序列出,区间为排名范围(左闭右闭

​ zrevrange key start end 按score逆序列出。

​ zcard key 获取长度

b.zscore/zrank/zrangebyscore/zrem

​ zscore key value 获取指定value的元素的score 内部score使用double类型存储,所以存在浮点数精度问题

​ zrank key value 获取value的当前排行

​ zrangebyscore key start end 将score在区间内的元素排序 inf表示无穷大。 withscores排序同时返回score

​ zrem key value 删除value

高级函数的使用细节

一 函数签名的概念

​ 一个函数签名 (类型签名,或方法签名) 定义了函数或方法的输入与输出。

​ 简单的说: 函数的签名就是函数的参数列表与返回值列表的统称。使用签名可以鉴别不同函数的特征,此外,也定义了用户与函数直接交互的方法。

​ 特别说明:

​ 1 参数的名称和返回值的名称,不作为函数签名的一部分。换句话说,就是签名与参数的名称无关。

​ 2 只要两个函数参数列表和返回值列表元素的顺序与类型是一致,就可以说明该两个函数是一样的函数或者说实现了同一个类型函数。

​ 例如:

1
2
3
4
5
6
7
8
9
10
11
12
 // 自定义函数类型
type MyFuncType func(int, int) int

// 实现一个和MyFuncType一样类型的函数
func TestFunc (a int, b int) int {
return a + b
}
func main() {
var c MyFuncType = TestFunc
d := c(1, 1)
fmt.Println(d)
}

​ 可以说明TestFunc是对MyFuncType自定义函数类型的一种实现,它们是一种类型的函数。

二 Go中函数是一等的公民

  • 封装代码
  • 分割功能
  • 解耦逻辑
  • 做为普通的值,函数当作参数在其他函数中传递、能将函数赋值变量
  • 能够进行类型判断和转换

三 匿名函数

​ 定义:没有函数名称的函数,称为匿名函数;

​ 1. 匿名函数可以直接复制给变量,然后通过变量进行调用;

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
// 定义匿名函数并将其赋值给anonymous
anonymous := func(say string) {
fmt.Println(say)
}
// 输出anonymous的类型
fmt.Printf("anonymous 的类型:%T\n", anonymous)
// 对赋值后的变量调用
anonymous("function is first class!")
}
输出结果:
anonymous 的类型:func(string)
function is first class!

​ 2. 匿名函数也可以在定义好后的} **后面直接添加(函数签名)**进行调用

1
2
3
4
5
6
func main() {
// 定义匿名函数并直接调用
func(say string){
fmt.Println(say)
}("hello function!")
}

四 自定义函数类型

​ 使用 type关键词自定义函数类型;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 自定义函数类型
type CustomFunc func(name string, age int)

// 函数SayInfo是对自定义函数的一种实现,函数签名一致
func SayInfo(n string, a int, ) {
fmt.Printf("我的名字叫%v, 年龄%v岁", n, a)
}

func main() {
var customFunc CustomFunc
// 可以进行赋值
customFunc = SayInfo
customFunc("austsxk", 22)

// 对自定义函数定义时,函数签名一定要与自定义函数签名一致,否则编译错误
var Print CustomFunc = func(a string, b int) {
fmt.Println(a, b)
}
Print("austsxk", 22)
}

五 高阶函数

  • 函数作为参数传递
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    package main

    import (
    "errors"
    "fmt"
    )

    // 自定义函数类型,操作类型
    type operate func(x, y int) int

    func Mixer(x, y int, op operate) (int, error) {
    if op == nil {
    return 0, errors.New("操作类型错误")
    }
    return op(x, y), nil
    }

    func main() {
    var op operate = func(x, y int) int {
    return x + y
    }
    var x, y = 23, 43
    // 将函数作为参数,控制函数的行为,将无状态变有状态.只要对operate类型函数进行实现,就可以实现不同逻辑。定向输出。
    result , error := Mixer(x, y, op)
    fmt.Println(result, error)
    }
  • 函数作为返回值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    package main

    import (
    "errors"
    "fmt"
    )

    // 定义一个需要返回的函数类型,接受两个int类型参数,返回一个int类型
    type GetValue func(x, y int) (int, error)

    // 自定义操作类型
    type Operate func(x, y int) int

    // 定义一个返回GetValue函数的函数
    func Calculate(op Operate) GetValue {
    return func(x, y int) (int, error) {
    // 如果操作符为nil,则抛出错误
    if op == nil {
    return 0, errors.New("invalid operate error")
    }
    // 否则,调用操作函数
    return op(x, y), nil
    }
    }

    func main() {
    var op = func(x, y int) int{
    return x * y
    }
    var a, b = 5, 10
    result := Calculate(op)
    data, error := result(a, b)
    fmt.Println(data, error)
    }

六 闭包

​ 闭包是在函数中嵌套定义函数,并且在内部函数中,使用到外部函数传入的自由变量,在函数内部获取了访问外部函数参数的权限,并进行逻辑的处理。外部函数返回的一般是内部函数的地址。

​ 例如:

1
2
3
4
5
6
7
8
9
10
11
func Outfunction(x int) func(int) int {
// 闭包将不确定变确定, 使用自由变量
// 内部函数使用了
return func(i int) int {
// 函数内部定义
fmt.Println("\ni=", i)
// 使用外部函数作用域的变量,自由变量
fmt.Println("out func value", x)
return x
}
}

​ 闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示数据还要表示代码。将函数作为第一级对象,就是说这些函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。

七 头等(第一类)函数的使用场景

头等函数就是上面提到的能够支持高阶函数的用法,能够将函数作为其他函数的参数或返回值。

将函数进行传递,根据函数的多样性去确定结果的唯一性。

本站总访问量 本站总访客数 本文总阅读量
载入天数...载入时分秒...