Redis模块的高级使用方式


Redis 模块是Redis的高级功能,允许我们实现特定的自定义数据类型。本质上,模块是一个动态库,可以在启动时或根据命令按需加载到 Redis 中 MODULE LOAD 。模块可以用多种语言编写,包括 C 和 Rust。

我们自己使用 Redis 模块实现新的数据类型是一项艰巨的工作。值得庆幸的是,有许多流行且广泛使用的模块可以解决全文搜索(RediSearch)、时间序列处理(RedisTimeSeries)和原生 JSON 支持(RedisJSON)等问题。让我们概述一下一些使用更广泛的 Redis 模块。

本节提供了 RediSearch、RedisJSON 和 RedisTimeSeries 模块的高级概述,以及与它们相关的重要命令。

要完成本教程的这一部分,您需要 安装最新版本的 Go 和 Docker。所有上述模块都作为Redis Stack的一部分提供 ,它将 Redis 与各种相关的软件包和服务捆绑在一起。您可以使用 Docker 启动 Redis Stack 本地实例 :
docker run -d --name redis-stack -p 6379:6379 redis/redis-stack:latest

在开始之前,让我们创建一个 Go 模块来托管示例:
go mod init go-redis-modules

RedisJSON
在 Redis 中处理 JSON 数据时,传统方法有其局限性。默认情况下,Redis 将值视为简单字符串,缺乏对查询或操作 JSON 等结构化数据的内置支持。因此,开发人员经常将 JSON 存储为 字符串化 值,从而牺牲了对各个字段执行高效操作或利用嵌套结构的能力。这种方法需要解析整个 JSON 文档,即使是微小的更新,导致效率低下并增加处理开销。

RedisJSON 模块通过向 Redis 引入本机 JSON 处理功能来解决这些挑战。它允许您像任何其他本机数据类型一样存储、更新和检索 JSON。

RedisJSON 提供了一组丰富的 JSON 文档操作。以下是一些最常用的命令:

  1. JSON.SET:在 Redis 中设置 JSON 值。
  2. JSON.GET:检索与给定键关联的 JSON 值。
  3. JSON.DEL:从 Redis 中删除 JSON 值。
  4. JSON.TYPE:返回JSON值的类型,指示它是对象、数组、字符串、数字、布尔值还是null。
  5. JSON.ARRAPPEND:将一个或多个值附加到 JSON 数组的末尾。
  6. JSON.ARRLEN:返回 JSON 数组的长度,提供其包含的元素数量。
  7. JSON.OBJKEYS:检索 JSON 对象的键,返回所有存在键的数组。
  8. JSON.OBJLEN:返回JSON对象的大小。
  9. JSON.NUMINCRBY:将 JSON 文档中的数值增加指定的量。
  10. JSON.STRAPPEND:在 JSON 文档中附加字符串。

下面的示例演示了如何在 Go 应用程序中使用各种 RedisJSON 命令。请注意,我们使用的是 go-redis 客户端。

package main

import (
    "context"
    
"errors"
    
"fmt"
    
"log"

    
"github.com/redis/go-redis/v9"
)

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr:
"localhost:6379",
    })

    ctx := context.Background()

    
// JSON.SET
    err := rdb.Do(ctx,
"JSON.SET", "mydoc", ".", `{"name":"John","credits":30,"cars":["honda","toyota"]}`).Err()
    if err != nil {
        log.Fatal(err)
    }

    
// JSON.GET
    val, err := rdb.Do(ctx,
"JSON.GET", "mydoc").Result()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(
"JSON.GET result -", val)

    
// JSON.ARRAPPEND
    _, err = rdb.Do(ctx,
"JSON.ARRAPPEND", "mydoc", ".cars", `"audi"`).Int()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(
"added audi to list of cars using JSON.ARRAPPEND")

    
// JSON.GET
    val, err = rdb.Do(ctx,
"JSON.GET", "mydoc").Result()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(
"lastest document", val)

    
// JSON.NUMINCRBY
    _, err = rdb.Do(ctx,
"JSON.NUMINCRBY", "mydoc", ".credits", 5).Result()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(
"incremented credits by 5 using JSON.NUMINCRBY")

    
// JSON.STRAPPEND
    _, err = rdb.Do(ctx,
"JSON.STRAPPEND", "mydoc", ".name", `", Jr."`).Int()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(
"updated name using JSON.STRAPPEND")

    
// JSON.GET
    val, err = rdb.Do(ctx,
"JSON.GET", "mydoc").Result()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(
"latest document", val)

    
// JSON.DEL
    _, err = rdb.Do(ctx,
"JSON.DEL", "mydoc").Int()
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(
"deleted document using JSON.DEL")

    err = rdb.Do(ctx,
"JSON.GET", "mydoc").Err()
    if errors.Is(err, redis.Nil) {
        fmt.Println(
"document 'mydoc' could not be found")
    }
}

要运行上述代码,请将其复制到名为的文件中 main.go 并执行以下命令:

# get dependencies
go get github.com/redis/go-redis/v9

# run program
go run main.go

请注意,我们不需要获取整个 JSON 文档来更新它。RedisJSON 提供精细操作来处理 JSON 数据,从而提高应用程序性能(减少延迟)并降低成本(减少网络带宽)。


RediSearch
RediSearch模块通过强大的全文搜索功能增强了Redis。它引入了基于倒排索引的搜索引擎,允许高效且可扩展的文本搜索。使用 RediSearch,您可以索引文本数据、执行复杂的搜索查询并检索相关搜索结果。
它提供关键字搜索、精确短语匹配、布尔运算、分页、排序和过滤等功能。
RediSearch 根据搜索结果与查询的匹配程度对搜索结果进行评分,使您能够对搜索结果进行排名和确定优先级。

以下是一些最常用的 RediSearch 命令:

  1. FT.CREATE:使用指定的架构和配置选项创建新的搜索索引。
  2. FT.SEARCH:对指定索引执行搜索查询,根据给定的搜索条件返回匹配的文档。
  3. FT.AGGREGATE:对指定索引进行聚合查询,根据给定的聚合条件返回聚合结果。
  4. FT.INFO:检索有关搜索索引的信息,包括统计信息、配置设置和架构详细信息。
  5. FT.DROPINDEX:删除搜索索引,删除所有文档和关联数据。

下面的示例演示了如何在 Go 应用程序中使用各种 RediSearch 命令。请注意redisearch-go客户端的用法 。

package main

import (
    "fmt"
    
"math/rand"
    
"strconv"

    
"github.com/RediSearch/redisearch-go/redisearch"
    
"github.com/gomodule/redigo/redis"
)

var pool *redis.Pool
var client *redisearch.Client

var cities = []string{
"new york", "london", "paris"}

func init() {
    pool = &redis.Pool{Dial: func() (redis.Conn, error) {
        return redis.Dial(
"tcp", "localhost:6379")
    }}

    client = redisearch.NewClientFromPool(pool,
"user-index")
}

func main() {

    schema := redisearch.NewSchema(redisearch.DefaultOptions).
        AddField(redisearch.NewTextFieldOptions(
"name", redisearch.TextFieldOptions{})).
        AddField(redisearch.NewTextFieldOptions(
"city", redisearch.TextFieldOptions{}))

    indexDefinition := redisearch.NewIndexDefinition().AddPrefix(
"user:")

    client.CreateIndexWithIndexDefinition(schema, indexDefinition)

    fmt.Println(
"redisearch index created")

    conn := pool.Get()

    for i := 1; i <= 10; i++ {

        hashName :=
"user:" + strconv.Itoa(i)
        val := redis.Args{hashName}.AddFlat(map[string]string{
"user_id": strconv.Itoa(i), "city": cities[rand.Intn(len(cities))]})

        conn.Do(
"HSET", val...)

        fmt.Println(
"created hash -", hashName)
    }

    docs, total, _ := client.Search(redisearch.NewQuery(
"*"))

    fmt.Println(
"no. of indexed documents =", total)

    docs, total, _ = client.Search(redisearch.NewQuery(
"@city:(paris|london)"))

    fmt.Println(
"found", total, "users in london or paris")
    for _, doc := range docs {
        fmt.Println(
"document ID -", doc.Id)
        fmt.Println(
"document attributes -", doc.Properties)
    }

    err := client.DeleteDocument(
"user:1")
    if err != nil {
        fmt.Println(
"failed to delete document (hash) user:1", err)
    }
    fmt.Println(
"deleted document (hash) user:1")

    _, total, _ = client.Search(redisearch.NewQuery(
"*"))

    fmt.Println(
"no. of indexed documents =", total)

    err = client.DropIndex(true)
    if err != nil {
        fmt.Println(
"failed to drop index", err)
    }
    fmt.Println(
"dropped index and documents")

    _, total, _ = client.Search(redisearch.NewQuery(
"*"))

    fmt.Println(
"no. of indexed documents =", total)
}

要运行上述代码,请将其复制到名为的文件中 main.go 并执行以下命令:

go get github.com/RediSearch/redisearch-go/redisearch
go get github.com/gomodule/redigo/redis

go run main.go

我们使用 Hash 作为文档,但 RediSearch 也可以使用 RedisJSON。当添加或删除文档时,索引会自动更新。本示例演示了一些简单的操作,包括获取所有文档,以及通过城市属性上的 ORclause 进行过滤,以获取居住在 "伦敦 "或 "巴黎 "的用户。

Redis时间序列
在 Redis 中处理大量时间序列数据时,有一些常用的技术。这些涉及将数据存储在 Redis 排序集中,其中分数表示时间戳,成员表示数据值。另一种方法是使用带有时间戳的字符串键作为键名称的一部分。每个键值对代表时间序列中的一个特定数据点。

然而,这些技术在处理大量时间序列数据时存在一些局限性。传统的 Redis 数据结构(如字符串键或排序集)的查询能力有限,并且难以执行复杂的聚合。此外,在处理大型数据集时,将每个数据点存储为单独的键值对可能会消耗大量内存,这通常是时间序列的情况。

RedisTimeSeries模块是原生的时序数据结构,为时序数据提供高效的存储和查询能力。它添加了查询功能,例如检索某个时间范围内的数据、执行聚合(例如求和、计数、平均值)、下采样和插值。它还允许我们定义自动过期或缩减数据采样的保留策略,从而实现高效的数据保留并降低存储要求。RedisTimeSeries 还与生态系统中的其他工具集成,例如 Prometheus 和 Grafana。

RedisTimeSeries 提供了丰富的命令集。以下是一些最常用的:

  1. TS.CREATE:使用指定的键、标签和保留策略创建新的时间序列。
  2. TS.ADD:将新数据点添加到时间序列,并将其与时间戳和值相关联。
  3. TS.RANGE:从指定时间范围内的时间序列中检索一系列数据点。
  4. TS.MRANGE:检索指定时间范围内多个时间序列的数据点。
  5. TS.GET:从时间序列中检索最新的数据点。
  6. TS.MGET:从多个时间序列中检索最新的数据点。
  7. TS.INCRBY:在特定时间戳处增加时间序列中数据点的值。
  8. TS.DECRBY:在特定时间戳处减少时间序列中数据点的值。
  9. TS.INFO:检索有关时间序列的元数据信息,例如样本数量、内存使用情况和保留策略。
  10. TS.QUERYINDEX:对时间序列执行基于索引的查询,根据指定的过滤器和聚合检索数据点。

下面的示例演示了如何在 Go 应用程序中使用各种 RedisTimeSeries 命令。请注意,我们使用的是 redistimeseries-go 客户端。

package main

import (
    "fmt"
    
"log"
    
"math/rand"
    
"time"

    redistimeseries
"github.com/RedisTimeSeries/redistimeseries-go"
)

const tsName =
"temperature:living_room"

func main() {
    client := redistimeseries.NewClient(
"localhost:6379", "", nil)

    
// TS.CREATE
    err := client.CreateKeyWithOptions(tsName, redistimeseries.DefaultCreateOptions)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(
"created time series key", tsName)

    tempOptions := []float64{24, 24.5, 25}
    var storedTS []int64

    for i := 1; i <= 10; i++ {
        
// TS.ADD
        res, err := client.AddAutoTs(tsName, tempOptions[(rand.Intn(len(tempOptions)))])
        storedTS = append(storedTS, res)
        if err != nil {
            log.Fatal(err)
        }
        
        time.Sleep(1 * time.Millisecond)
    }

    fmt.Println(
"added time series data")

    
// TS.GET
    lastDataPoint, err := client.Get(tsName)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf(
"lastest data point - timestamp: %v, temp: %v\n", lastDataPoint.Timestamp, lastDataPoint.Value)

    
// TS.RANGE
    dataPoints, err := client.RangeWithOptions(tsName, storedTS[0], storedTS[len(storedTS)-1], redistimeseries.DefaultRangeOptions)

    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(
"TS.RANGE result")

    for _, dp := range dataPoints {
        fmt.Printf(
"timestamp: %v, temp: %v\n", dp.Timestamp, dp.Value)
    }
}

要运行上述代码,请将其复制到名为的文件中 main.go 并执行以下命令:

go get github.com/RedisTimeSeries/redistimeseries-go
go run main.go

Redis 模块:最佳实践
本节介绍了一些在这些模块的实际部署中优化可扩展性和性能的最佳实践。

RedisJSON

  • 优化 JSON 结构以实现高效访问:设计 JSON 结构时,请考虑应用程序的访问模式。尽可能展平 JSON 层次结构以避免深层嵌套。这使您可以有效地访问特定字段,而无需获取整个 JSON 文档。使用 RedisJSON 的路径表达式来检索或修改特定的 JSON 元素,而无需解析整个文档。
  • 考虑内存管理和数据大小:RedisJSON 将 JSON 文档存储为 Redis 字符串。请记住,Redis 的最大字符串大小限制为 512 MB。如果您的 JSON 文档很大,请考虑将它们分成更小的部分或使用压缩。
  • 您可以使用 RedisJSON 作为 RedisSearch 的索引:此集成使您能够高效地执行复杂的搜索操作并根据搜索条件检索 JSON 文档。

Redis搜索

  • 仔细选择和优化搜索架构:设计搜索架构时,请考虑应用程序的特定搜索要求。定义需要可搜索的字段并选择适当的字段类型(例如,  text、  numeric、  tag)。仔细分析数据并选择最相关的字段以包含在搜索索引中。避免包含不必要的字段,以最小化索引大小并优化搜索性能。
  • 利用查询优化技术: RedisSearch 提供各种查询优化技术来提高搜索性能。利用查询过滤器来减少搜索空间并根据特定条件缩小结果范围。
  • 此外,请考虑使用词干、同义词和其他特定于语言的分析器来提高搜索准确性。

Redis时间序列

  • 选择最佳时间段:根据数据的频率和粒度选择适当的时间间隔。考虑存储空间和查询性能之间的权衡。较小的存​​储桶提供更详细的数据,但会导致更大的内存使用量。
  • 利用查询优化技术:RedisSearch 提供各种查询优化技术来提高搜索性能。使用查询过滤器来减少搜索空间并缩小结果范围。
  • 使用压缩和下采样:如果您有高频数据且内存资源有限,请考虑启用压缩和下采样配置。这有助于减少内存使用而不影响数据准确性。