Go压缩位图库roaring安装使用详解

目录
  • 简介
  • 安装
  • 使用
    • 基本操作
    • 迭代
    • 并行操作
    • 写入与读取
    • 64 位版本
  • 存储格式
    • 概览
    • Cookie Header
    • Descriptive Header
    • Offset Header
    • Container
    • array
    • bitmap/bitset
    • run
    • 手撸解析代码
  • 总结

简介

集合是软件中的基本抽象。实现集合的方法有很多,例如 hash set、tree等。要实现一个整数集合,位图(bitmap,也称为 bitset 位集合,bitvector 位向量)是个不错的方法。使用 n 个位(bit),我们可以表示整数范围[0, n)。如果整数 i 在集合中,第 i 位设置为 1。这样集合的交集(intersection)、并集(unions)和差集(difference)可以利用整数的按位与、按位或和按位与非来实现。而计算机执行位运算是非常迅速的。

上一篇文章我介绍了bitset这个库。

bitset 在某些场景中会消耗大量的内存。例如,设置第 1,000,000 位,需要占用超过 100kb 的内存。为此 bitset 库的作者又开发了压缩位图库:roaring

本文首先介绍了 roaring 的使用。最后分析 roaring 的文件存储格式。

安装

本文代码使用 Go Modules。

创建目录并初始化:

$ mkdir -p roaring && cd roaring
$ go mod init github.com/darjun/go-daily-lib/roaring

安装roaring库:

$ go get -u github.com/RoaringBitmap/roaring

使用

基本操作

func main() {
  bm1 := roaring.BitmapOf(1, 2, 3, 4, 5, 100, 1000)
  fmt.Println(bm1.String())         // {1,2,3,4,5,100,1000}
  fmt.Println(bm1.GetCardinality()) // 7
  fmt.Println(bm1.Contains(3))      // true
  bm2 := roaring.BitmapOf(1, 100, 500)
  fmt.Println(bm2.String())         // {1,100,500}
  fmt.Println(bm2.GetCardinality()) // 3
  fmt.Println(bm2.Contains(300))    // false
  bm3 := roaring.New()
  bm3.Add(1)
  bm3.Add(11)
  bm3.Add(111)
  fmt.Println(bm3.String())         // {1,11,111}
  fmt.Println(bm3.GetCardinality()) // 3
  fmt.Println(bm3.Contains(11))     // true
  bm1.Or(bm2)                       // 执行并集
  fmt.Println(bm1.String())         // {1,2,3,4,5,100,500,1000}
  fmt.Println(bm1.GetCardinality()) // 8
  fmt.Println(bm1.Contains(500))    // true
  bm2.And(bm3)                      // 执行交集
  fmt.Println(bm2.String())         // {1}
  fmt.Println(bm2.GetCardinality()) // 1
  fmt.Println(bm2.Contains(1))      // true
}

上面演示了两种创建 roaring bitmap 的方式:

  • roaring.BitmapOf():传入集合元素,创建位图并添加这些元素
  • roaring.New():创建一个空位图

首先,我们创建了一个位图 bm1:{1,2,3,4,5,100,1000}。输出它的字符串表示,集合大小,检查 3 是否在集合中。

然后又创建了一个位图 bm2:{1,100,500}。输出检查三连。

接着创建了一个空位图 bm3,依次添加元素 1,11,111。输出检查三连。

然后我们对 bm1 和 bm2 执行并集,结果直接存放在 bm1 中。由于集合中的元素各不相同,此时 bm1 中的元素为{1,2,3,4,5,100,500,1000},大小为 8。

再然后我们对 bm2 和 bm3 执行交集,结果直接存放在 bm2 中。此时 bm2 中的元素为{1},大小为 1。

可以看出 roaring 提供的基本操作与 bitset 大体相同。只是命名完全不一样,在使用时需要特别注意。

  • bm.String():返回 bitmap 的字符串表示
  • bm.Add(n):添加元素 n
  • bm.GetCardinality():返回集合的基数(Cardinality),即元素个数
  • bm1.And(bm2):执行集合交集,会修改 bm1
  • bm1.Or(bm2):执行集合并集,会修改 bm1

迭代

roaring 位图支持迭代。

func main() {
  bm := roaring.BitmapOf(1, 2, 3, 4, 5, 100, 1000)
  i := bm.Iterator()
  for i.HasNext() {
    fmt.Println(i.Next())
  }
}

与很多编程语言支持的迭代器一样,先调用对象的Iterator()返回一个迭代器,然后循环调用HasNext()检查是否有下一个元素,调用i.Next()返回下一个元素。

上面代码依次输出 1,2,3,4,5,100,1000。

并行操作

roaring 支持位图集合运算的并行执行。可以指定使用多少个 goroutine 对集合执行交集、并集等。同时可以传入可变数量的位图集合:

func main() {
  bm1 := roaring.BitmapOf(1, 2, 3, 4, 5, 100, 1000)
  bm2 := roaring.BitmapOf(1, 100, 500)
  bm3 := roaring.BitmapOf(1, 10, 1000)
  bmAnd := roaring.ParAnd(4, bm1, bm2, bm3)
  fmt.Println(bmAnd.String())         // {1}
  fmt.Println(bmAnd.GetCardinality()) // 1
  fmt.Println(bmAnd.Contains(1))      // true
  fmt.Println(bmAnd.Contains(100))    // false
  bmOr := roaring.ParOr(4, bm1, bm2, bm3)
  fmt.Println(bmOr.String())         // {1,2,3,4,5,10,100,500,1000}
  fmt.Println(bmOr.GetCardinality()) // 9
  fmt.Println(bmOr.Contains(10))     // true
}

并行操作使用相应接口的Par*版本,第一个参数指定 worker 数量,接着传入任意多个 bitmap。

写入与读取

roaring 可以将压缩的位图写入到文件中,并且格式与其他语言的实现保持兼容。也就是说,我们可以用 Go 将 roaring 位图写入文件,然后通过网络发送给另一台机器,在这台机器上使用 C++ 或 Java 的实现读取这个文件。

func main() {
  bm := roaring.BitmapOf(1, 3, 5, 7, 100, 300, 500, 700)
  buf := &bytes.Buffer{}
  bm.WriteTo(buf)
  newBm := roaring.New()
  newBm.ReadFrom(buf)
  if bm.Equals(newBm) {
    fmt.Println("write and read back ok.")
  }
}
  • WriteTo(w io.Writer):写入一个 io.Writer,可以是内存(byte.Buffer),可以是文件(os.File),甚至可以是网络(net.Conn)
  • ReadFrom(r io.Reader):从一个 io.Reader 中读取,来源同样可以是内存、文件或网络等

注意WriteTo的返回值为sizeerr,使用时需要处理错误情况。ReadFrom也是返回sizeerr,同样需要处理处理。

64 位版本

默认情况下,roaring 位图只能用来存储 32 位整数。所以 roaring 位图最多能包含 4294967296(2^32) 个整数。

roaring 也提供了存储 64 位整数的扩展,即github.com/RoaringBitmap/roaring/roaring64。提供的接口基本相同。然而,64 位版本不保证与 Java/C++ 等格式兼容。

存储格式

roaring 可以写入文件中,也可以从文件中读取。并且提供多种语言兼容的格式。下面我们一起来看看存储的格式。

roaring 位图默认只能存储 32 位的整数。在序列化时,将这些整数分容器(container)存储。每个容器有一个 16 位表示的基数(Cardinality,即元素个数,范围[1,2^16])和一个键(key)。键取元素的最高有效 16 位(most significant),所以键的范围为[0, 65536)。这样如果两个整数的最高 16 位有效位相同,那么它们将被保存在同一个容器中。这样做还有一个好处:可以减少占用的空间。

所有整数均采用小端存储。

概览

roaring 采用的存储格式布局如下:

从上到下依次介绍。

开始部分是一个 Cookie Header。它用来识别一个二进制流是不是一个 roaring 位图,并且存储一些少量信息。

cookie 这个词有点意思,本意是饼干。我的理解是指小物件,所以 http 中的 cookie 只是用来存储少量信息。这里的 Cookie Header 也是如此。

接下来是 Descriptive Header。见名知义,它用来描述容器的信息。后面会详细介绍容器。

接下来有一个可选的 Offset Header。它记录了每个容器相对于首位的偏移,这让我们可以随机访问任意容器。

最后一部分是存储实际数据的容器。roaring 中一共有 3 种类型的容器:

  • array(数组型):16bit 整数数组
  • bitset(位集型):使用上一篇文章介绍的 bitset 存储数据
  • run:这个有点不好翻译。有些人可能听说过 run-length 编码,有翻译成游程编码的。即使用长度+数据来编码,比如"0000000000"可以编码成"10,0",表示有 10 个 0。run 容器也是类似的,后文详述

设计这种的布局,是为了不用将存储的位图全部载入内存就可以随机读取它的数据。并且每个容器的范围相互独立,这使得并行计算变得容易。

Cookie Header

Cookier Header 有两种类型,分别占用 32bit 和 64bit 的空间。

第一种类型,前 32bit 的值为 12346,此时紧接着的 32bit 表示容器数量(记为 n)。同时这意味着,后面没有 run 类型的容器。12346 这魔术数字被定义为常量SERIAL_COOKIE_NO_RUNCONTAINER,含义不言自明。

第二种类型,前 32bit 的最低有效 16 位的值为 12347。此时,最高有效 16 位存储的值等于容器数量-1。将 cookie 右移 16 位再加 1 即可得到容器数量。由于这种类型的容器数量不会为 0,采用这种编码我们能的容器数量会多上 1 个。这种方法在很多地方都有应用,例如 redis。后面紧接着会使用 (n+7)/8 字节(作为一个 bitset)表示后面的容器是否 run 容器。每位对应一个容器,1 表示对应的容器是 run 容器,0 表示不是 run 容器。

由于是小端存储,所以流的前 16bit 一定是 12346 或 12347。如果读取到了其它的值,说明文件损坏,直接退出程序即可。

Descriptive Header

Cookie Header 之后就是 Descriptive Header。它使用一对 16bit 数据描述每个容器。一个 16bit 存储键(即整数的最高有效 16bit),另一个 16bit 存储对应容器的基数(Cardinality)-1(又见到了),即容器存储的整数数量)。如果有 n 个容器,则 Descriptive Header 需要 32n 位 或 4n 字节。

扫描 Descriptive Header 之后,我们就能知道每个容器的类型。如果 cookie 值为 12347,cookie 后有一个 bitset 表示每个容器是否是 run 类型。对于非 run 类型的容器,如果容器的基数(Cardinality)小于等于 4096,它是一个 array 容器。反之,这是一个 bitset 容器

Offset Header

满足以下任一条件,Offset Header 就会存在:

  • cookie 的值为 SERIAL_COOKIE_NO_RUNCONTAINER(即 12346)
  • cookie 的值为 SERIAL_COOKIE(即 12347),并且至少有 4 个容器。也有一个常量NO_OFFSET_THRESHOLD = 4

Offset Header 为每个容器使用 32bit 值存储对应容器距离流开始处的偏移,单位字节。

Container

接下来就是实际存储数据的容器了。前面简单提到过,容器有三种类型。

array

存储有序的 16bit 无符号整数值,有序便于使用二分查找提高效率。16bit 值只是数据的最低有效 16bit,还记得 Descriptive Header 中每个容器都有一个 16bit 的 key 吧。将它们拼接起来才是实际的数据。

如果容器有 x 个值,占用空间 2x 字节。

bitmap/bitset

bitset 容器固定使用 8KB 的空间,以 64bit 为单位(称为字,word)序列化。因此,如果值 j 存在,则第 j/64 个字(从 0 开始)的 j%64 位会被设置为 1(从 0 开始)。

run

以一个表示 run 数量的 16bit 整数开始。后续每个 run 用一对 16bit 整数表示,前一个 16bit 表示开始的值,后一个 16bit 表示长度-1(又双见到了)。例如,11,4 表示数据 11,12,13,14,15。

手撸解析代码

验证我们是否真的理解了 roaring 布局最有效的方法就是手撸一个解析。使用标准库encoding/binary可以很容易地处理大小端问题。

定义常量:

const (
  SERIAL_COOKIE_NO_RUNCONTAINER = 12346
  SERIAL_COOKIE                 = 12347
  NO_OFFSET_THRESHOLD           = 4
)

读取 Cookie Header:

func readCookieHeader(r io.Reader) (cookie uint16, containerNum uint32, runFlagBitset []byte) {
  binary.Read(r, binary.LittleEndian, &cookie)
  switch cookie {
  case SERIAL_COOKIE_NO_RUNCONTAINER:
    var dummy uint16
    binary.Read(r, binary.LittleEndian, &dummy)
    binary.Read(r, binary.LittleEndian, &containerNum)
  case SERIAL_COOKIE:
    var u16 uint16
    binary.Read(r, binary.LittleEndian, &u16)
    containerNum = uint32(u16)
    buf := make([]uint8, (containerNum+7)/8)
    r.Read(buf)
    runFlagBitset = buf[:]
  default:
    log.Fatal("unknown cookie")
  }
  fmt.Println(cookie, containerNum, runFlagBitset)
  return
}

读取 Descriptive Header:

func readDescriptiveHeader(r io.Reader, containerNum uint32) []KeyCard {
  var keycards []KeyCard
  var key uint16
  var card uint16
  for i := 0; i < int(containerNum); i++ {
    binary.Read(r, binary.LittleEndian, &key)
    binary.Read(r, binary.LittleEndian, &card)
    card += 1
    fmt.Println("container", i, "key", key, "card", card)
    keycards = append(keycards, KeyCard{key, card})
  }
  return keycards
}

读取 Offset Header:

func readOffsetHeader(r io.Reader, cookie uint16, containerNum uint32) {
  if cookie == SERIAL_COOKIE_NO_RUNCONTAINER ||
    (cookie == SERIAL_COOKIE && containerNum >= NO_OFFSET_THRESHOLD) {
    // have offset header
    var offset uint32
    for i := 0; i < int(containerNum); i++ {
      binary.Read(r, binary.LittleEndian, &offset)
      fmt.Println("offset", i, offset)
    }
  }
}

读取容器,根据类型调用不同的函数:

// array
func readArrayContainer(r io.Reader, key, card uint16, bm *roaring.Bitmap) {
  var value uint16
  for i := 0; i < int(card); i++ {
    binary.Read(r, binary.LittleEndian, &value)
    bm.Add(uint32(key)<<16 | uint32(value))
  }
}
// bitmap
func readBitmapContainer(r io.Reader, key, card uint16, bm *roaring.Bitmap) {
  var u64s [1024]uint64
  for i := 0; i < 1024; i++ {
    binary.Read(r, binary.LittleEndian, &u64s[i])
  }
  bs := bitset.From(u64s[:])
  for i := uint32(0); i < 8192; i++ {
    if bs.Test(uint(i)) {
      bm.Add(uint32(key)<<16 | i)
    }
  }
}
// run
func readRunContainer(r io.Reader, key uint16, bm *roaring.Bitmap) {
  var runNum uint16
  binary.Read(r, binary.LittleEndian, &runNum)
  var startNum uint16
  var length uint16
  for i := 0; i < int(runNum); i++ {
    binary.Read(r, binary.LittleEndian, &startNum)
    binary.Read(r, binary.LittleEndian, &length)
    length += 1
    for j := uint16(0); j < length; j++ {
      bm.Add(uint32(key)<<16 | uint32(startNum+j))
    }
  }
}

整合:

func main() {
  data, err := ioutil.ReadFile("../roaring.bin")
  if err != nil {
    log.Fatal(err)
  }
  r := bytes.NewReader(data)
  cookie, containerNum, runFlagBitset := readCookieHeader(r)
  keycards := readDescriptiveHeader(r, containerNum)
  readOffsetHeader(r, cookie, containerNum)
  bm := roaring.New()
  for i := uint32(0); i < uint32(containerNum); i++ {
    if runFlagBitset != nil && runFlagBitset[i/8]&(1<<(i%8)) != 0 {
      // run
      readRunContainer(r, keycards[i].key, bm)
    } else if keycards[i].card <= 4096 {
      // array
      readArrayContainer(r, keycards[i].key, keycards[i].card, bm)
    } else {
      // bitmap
      readBitmapContainer(r, keycards[i].key, keycards[i].card, bm)
    }
  }
  fmt.Println(bm.String())
}

我将写入读取那个示例中的 byte.Buffer 保存到文件roaring.bin中。上面的程序就可以解析这个文件:

12346 1 []
container 0 key 0 card 8
offset 0 16
{1,3,5,7,100,300,500,700}

成功还原了位图

(0)

相关推荐

  • Go语言Zap库Logger的定制化和封装详解

    目录 前言 Go 语言原生的Logger Go 语言原生Logger的缺点 Zap 日志库 Zap 的使用方法 安装zap 设置 Logger 定制 Zap 的 Logger 日志切割 封装 Logger 总结 前言 日志无论对于程序还是程序员都非常重要,有多重要呢,想要长期在公司健健康康的干下去就得学会阶段性划水,阶段性划水的一大关键的就是干活快过预期但是装作...不对,这个开头不对劲,下面重来. 日志无论对于程序还是程序员都非常重要,程序员解决问题的快慢除了经验外,就是看日志能不能有效地记录

  • Golang配置管理库 Viper的教程详解

    目录 一.Viper 是什么? 二.安装 Viper 三.Viper 有什么作用 四.Viper demo 可供参考 注意 五.总结 一.Viper 是什么? Viper 是应用程序的完整配置的管理工具,用于在应用程序中工作,可以处理所有类型的配置需求和格式. 二.安装 Viper go get github.com/spf13/viper 三.Viper 有什么作用 设置默认值 读取 JSON.TOML.YAML(YML).HCL.envfile 和 Java properties 属性配置文

  • Go标准库http与fasthttp服务端性能对比场景分析

    目录 1. 背景 2. 性能测试 3. 对结果的简要分析 4. 优化途径 1. 背景 Go初学者学习Go时,在编写了经典的“hello, world”程序之后,可能会迫不及待的体验一下Go强大的标准库,比如:用几行代码写一个像下面示例这样拥有完整功能的web server: // 来自https://tip.golang.org/pkg/net/http/#example_ListenAndServe package main import ( "io" "log"

  • Golang详细讲解常用Http库及Gin框架的应用

    目录 1. Http标准库 1.1 http客户端 1.2 自定义请求头 1.3 检查请求重定向 1.4 http服务器性能分析 2. JSON数据处理 2.1 实体序列化 2.2 处理字段为小写下划线 2.3 省略空字段 2.4 反序列化 3. 自然语言处理 3.1 使用Map处理 3.2 定义实体处理 4. http框架 4.1 gin 4.1.1 启动服务 4.1.2 middleware 4.1.3 设置请求ID 1. Http标准库 1.1 http客户端 func main() {

  • Go位集合相关操作bitset库安装使用

    目录 简介 安装 使用 为什么要使用它? 总结 一点闲话 参考 简介 我们都知道计算机是基于二进制的,位运算是计算机的基础运算.位运算的优势很明显,CPU 指令原生支持.速度快.基于位运算的位集合在有限的场景中替换集合数据结构可以收到意想不到的效果.bitset库实现了位集合及相关操作,不妨拿来即用. 安装 本文代码使用 Go Modules. 创建目录并初始化: $ mkdir -p bitset && cd bitset $ go mod init github.com/darjun/

  • Go压缩位图库roaring安装使用详解

    目录 简介 安装 使用 基本操作 迭代 并行操作 写入与读取 64 位版本 存储格式 概览 Cookie Header Descriptive Header Offset Header Container array bitmap/bitset run 手撸解析代码 总结 简介 集合是软件中的基本抽象.实现集合的方法有很多,例如 hash set.tree等.要实现一个整数集合,位图(bitmap,也称为 bitset 位集合,bitvector 位向量)是个不错的方法.使用 n 个位(bit)

  • MySQL 5.6.36 Windows x64位版本的安装教程详解

    1,目标环境 Windows 7 64位 2,材料 (1)VC++2010 发布包(64位) (2)MySQL 5.6.36 Windows x64位版本(非MSI,可从官网获取免费版本) (3)EditPlus(可选) 3,基础操作 本文中部分操作需以管理员身份+命令行进行执行. 4,步骤 (1)(解压到当前文件夹方式)解压安装包,编辑其中的my-default.ini文件,主要是2项: ①basedir即为mysql基础文件夹,形如:C:\mysql-5.6.36-winx64 ②datad

  • Redis 对比 Memcached 并在 CentOS 下进行安装配置详解

    Redis 是一个开源.支持网络.基于内存.键值对的 Key-Value 数据库,本篇文章主要介绍了Redis 对比 Memcached 并在 CentOS 下进行安装配置详解,有兴趣的可以了解一下. 了解一下 Redis Redis 是一个开源.支持网络.基于内存.键值对的 Key-Value 数据库,使用 ANSI C 编写,并提供多种语言的 API ,它几乎没有上手难度,只需要几分钟我们就能完成安装工作,并让它开始与应用程序顺畅协作.换句话来说,只需投入一小部分时间与精力,大家就能获得立竿

  • Windows系统下nodejs、npm、express的下载和安装教程详解

    1. node.js下载 首先进入http://nodejs.org/dist/,这里面的版本呢,几乎每个月都出几个新的,建议大家下载最新版本,看看自己的电脑是多少位的,别下错了. 下载完解压到你想放的位置就好了,解压后你会发现里面有node.exe.我解压到了D:\software_install文件夹. 接下来去命令行,即点击电脑左下角的开始-->运行-->cmd. 进入node.exe所在的目录,输入node -v,查看你的node版本.我的路径如下图所示: 如果你获得以上输出结果,说明

  • Win7系统下mysql 5.7.11安装教程详解

    操作系统:win7 64位旗舰版 mysql压缩包:mysql-5.7.11-winx64.zip 1. 解压MySQL压缩包 将下载的MySQL压缩包解压到自定义目录下,我的解压目录是: "D:\Program Files\mysql-5.7.11-winx64" 将解压目录下默认文件 my-default.ini 拷贝一份,改名 my.ini 复制下面的配置信息到 my.ini 保存 #如果没有my-default.ini,可自己新建my.ini或者从其他地方中获取 #######

  • Postman的下载及安装教程详解

    Postman背景介绍 用户在开发或者调试网络程序或者是网页B/S模式的程序的时候是需要一些方法来跟踪网页请求的,用户可以使用一些网络的监视工具比如著名的Firebug等网页调试工具.今天给大家介绍的这款网页调试工具不仅可以调试简单的css.html.脚本等简单的网页基本信息,它还可以发送几乎所有类型的HTTP请求!Postman在发送网络HTTP请求方面可以说是Chrome插件类产品中的代表产品之一. Postman的操作环境 postman适用于不同的操作系统,Postman Mac.Win

  • mysql下载与安装过程详解

    1:下载MySql 官网下载地址:https://dev.mysql.com/downloads/mysql/ 选择对应的下载文件.(我电脑是64位,所以这下载的是64位的下载文件) 2:安装MySql 打开下载文件解压到指定文件目录.(我这里解压目录为D:\mysql-5.7.21-winx64) 打开解压后的MySql文件在根目录下创建my.ini (mysql配置文件) my.ini文件内容如下: (建议直接复制粘贴下面文件) 这里需要将basedir 与 datadir 路径改成mysq

  • TensorFlow的环境配置与安装教程详解(win10+GeForce GTX1060+CUDA 9.0+cuDNN7.3+tensorflow-gpu 1.12.0+python3.5.5)

    记录一下安装win10+GeForce GTX1060+CUDA 9.0+cuDNN7.3+tensorflow-gpu 1.12.0+python3.5.5 之前已经安装过pycharm.Anaconda以及VS2013,因此,安装记录从此后开始 总体步骤大致如下: 1.确认自己电脑显卡型号是否支持CUDA(此处有坑) 此处有坑!不要管NVIDIA控制面板组件中显示的是CUDA9.2.148. 你下载的CUDA不一定需要匹配,尤其是CUDA9.2,最好使用CUDA9.0,我就在此坑摔的比较惨.

  • 2020最新版 PhpStudy V8.1版本下载安装使用详解

    最新版2020 PhpStudy V8.1的安装 一.从官网下载安装包并安装 PhpStudy v8.1下载官网 自己选择64位或者32位进行下载,下载之后默认安装 二.打开软件,开启Apache服务,并在浏览器输入本地地址(127.0.01)进行访问 Apache/Nginx是web的服务器 Mysql/MariaDB是数据库软件 Php是脚本开发语言 1.打开软件,对本机网站进行测试 在首页->套件位置启动Apache2.4.39(如果Apache版本是其他的也可以启动),启动之后会有一个绿

  • Redis客户端及服务端的安装教程详解

    本系列将和大家分享Redis分布式缓存,本文是该系列的开篇,主要简单介绍下Redis客户端及服务端的安装. 一.Redis简介 Redis:Remote Dictionary Server 远程字典服务器 基于内存管理(数据存在内存),实现了5种数据结构(分别应对各种具体需求),单线程模型的应用程序(单进程单线程),对外提供插入--查询--固化--集群功能. 正是因为基于内存管理所以速度快,可以用来提升性能.但是不能当数据库,不能作为数据的最终依据. 单线程多进程的模式来提供集群服务. 单线程最

随机推荐