G

[Golang/Redis] Redis使用Lua脚本示例三则

RoLingG Golang其他 2025-04-30

Redis使用Lua脚本示例三则

一则:比较简易的使用Lua语言新增HSET的值

package main

import (
    "context"
    "fmt"
    "log"

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

var ctx = context.Background()

func main() {
    // 连接到 Redis
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379", // Redis 服务器地址
        Password: "",               // Redis 密码,没有则留空
        DB:       0,                // 默认数据库编号
    })

    // 测试连接
    _, err := rdb.Ping(ctx).Result()
    if err != nil {
        log.Fatalf("Failed to connect to Redis: %v", err)
    }
    fmt.Println("Connected to Redis successfully!")

    // 定义 Lua 脚本
    // 这个脚本会插入一个字段到 Hash 中,并返回该字段的值
    luaScript := `
        local key = KEYS[1]
        local field = KEYS[2]
        local value = ARGV[1]
        redis.call("HSET", key, field, value)
        return redis.call("HGET", key, field)
    `

    // 调用 Lua 脚本
    key := "myHash"
    field := "myField"
    value := "myValue"

    // 使用 EVAL 命令运行 Lua 脚本
    result, err := rdb.Eval(ctx, luaScript, []string{key, field}, value).Result()
    if err != nil {
        log.Fatalf("Failed to execute Lua script: %v", err)
    }

    // 打印结果
    fmt.Printf("Inserted and retrieved value: %v\n", result)
}

二则:将多个值新增进SET

package main

import (
    "context"
    "fmt"
    "github.com/go-redis/redis/v8"
    "log"
)

//var ctx = context.Background()

func main() {
    rdb := redis.NewClient(&redis.Options{
        Addr:     "127.0.0.1:6379",
        Password: "",
        DB:       0,   //0就用默认的DB
        PoolSize: 100, //连接池的大小
    })
    _, err := rdb.Ping(context.Background()).Result()
    if err != nil {
        fmt.Printf("%s, redis连接失败\n", err.Error())
        return
    }
    fmt.Printf("redis连接成功\n")
    // 定义 Lua 脚本
    luaScript := `
        for i = 1, #KEYS do
            redis.call("SET", KEYS[i], ARGV[i])
        end

        local res = {}
        for i = 1, #KEYS do
            table.insert(res, redis.call("GET", KEYS[i]))
        end
        return res
    `
    // 调用 Lua 脚本
    keys := []string{"name", "email", "age"}
    values := []string{"leijie", "rolingg@qq.com", "22"}

    // 使用 EVAL 命令运行 Lua 脚本
    results, err := rdb.Eval(context.Background(), luaScript, keys, values).Result()
    if err != nil {
        log.Fatalf("Failed to execute Lua script: %v", err)
    }
    fmt.Println(results)
}

三则:将多个值新增进HSET

package main

import (
    "context"
    "fmt"
    "log"

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

var ctx = context.Background()

func main() {
    // 连接到 Redis
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379", // Redis 服务器地址
        Password: "",               // Redis 密码,没有则留空
        DB:       0,                // 默认数据库编号
    })

    // 测试连接
    _, err := rdb.Ping(ctx).Result()
    if err != nil {
        log.Fatalf("Failed to connect to Redis: %v", err)
    }
    fmt.Println("Connected to Redis successfully!")

    // 定义 Lua 脚本
    luaScript := `
        local keys = KEYS
        local values = ARGV
        local ttl = tonumber(values[#values]) -- 获取过期时间(以秒为单位)

        -- 每个键的字段数量
        local fieldsPerKey = 3
        -- 初始化值的索引
        local valueIndex = 1

        -- 批量插入字段并设置过期时间
        for i = 1, #keys, fieldsPerKey + 1 do
            local key = keys[i]
            for j = 1, fieldsPerKey do
                local field = keys[i + j]
                local value = values[valueIndex]
                redis.call("HSET", key, field, value)
                valueIndex = valueIndex + 1 
            end
            -- 设置过期时间
            redis.call("EXPIRE", key, ttl)
        end

        -- 检索指定字段
        local res = {}
        for i = 1, #keys, fieldsPerKey + 1 do
            local key = keys[i]
            for j = 1, fieldsPerKey do
                local field = keys[i + j]
                table.insert(res, redis.call("HGET", key, field))
            end
        end
        return res
    `

    // 调用 Lua 脚本
    keys := []string{
        "myHash1", "name", "email", "age",
        "myHash2", "name", "email", "age",
    }
    values := []string{
        "John Doe", "john.doe@example.com", "30",
        "Jane Smith", "jane.smith@example.com", "25",
    }
    ttl := 60 // 设置过期时间为 60 秒

    // 将过期时间添加到 values 数组的末尾
    values = append(values, fmt.Sprintf("%d", ttl))

    // 使用 EVAL 命令运行 Lua 脚本
    results, err := rdb.Eval(ctx, luaScript, keys, values).Result()
    if err != nil {
        log.Fatalf("Failed to execute Lua script: %v", err)
    }

    // 打印结果
    fmt.Printf("Inserted and retrieved values: %v\n", results)
}

这里EVAL命令里面包含四个值,分别是contextlua命令脚本KEYSVALUES,其中 KEYSVALUES 可以是 []string 或者 []interface

为什么在并发场景要使用Lua来操纵Redis?

  1. 减少网络往返次数
  • 直接命令:如果你从客户端发送多个 Redis 命令(例如 HSETEXPIRE 等),每个命令都会产生一次网络往返。在并发场景下,多个客户端同时发送多个命令,网络延迟会显著增加。
  • Lua 脚本:Lua 脚本将多个操作封装在一个命令中,通过 EVALEVALSHA 命令一次性发送给 Redis 服务器。这样可以显著减少网络往返次数,从而提高性能。
  1. 原子性
  • 直接命令:多个命令在客户端发送时是独立执行的,可能会被其他命令插入,导致操作不原子。例如,如果你先执行 HSET,然后执行 EXPIRE,在两者之间可能会有其他客户端的命令插入,导致不一致。
  • Lua 脚本:Lua 脚本在 Redis 服务器端执行时是原子的。一旦脚本开始执行,Redis 会将其作为一个整体处理,不会被其他命令打断。这确保了操作的一致性和完整性。
  1. 减少客户端负载
  • 直接命令:客户端需要处理多个命令的发送和响应,这会增加客户端的负载,尤其是在并发场景下。
  • Lua 脚本:客户端只需要发送一个 EVAL 命令,由 Redis 服务器端执行脚本并返回结果。这减轻了客户端的负载,使得客户端可以更高效地处理其他任务。
  1. 减少 Redis 服务器的上下文切换
  • 直接命令:每个命令都会触发一次 Redis 服务器的上下文切换,尤其是在高并发场景下,这会导致服务器性能下降。
  • Lua 脚本:Lua 脚本在 Redis 服务器端执行时,所有操作都在一个上下文中完成,减少了上下文切换的开销。
  1. 优化性能
  • 直接命令:多个命令的执行需要多次解析和处理,增加了 Redis 服务器的处理时间。
  • Lua 脚本:Lua 脚本在 Redis 服务器端执行时,所有操作都在一个脚本中完成,减少了解析和处理的开销。Redis 服务器可以更高效地执行脚本中的命令。

综合来说,Lua脚本的作用就是将多个Reids命令集成在一个上下文内一起进行操作,从而减少操作次数,优化负载(无论是网络还是服务端负载),并且所有操作都是具有原子性。

原子性:一个操作或多个操作要么全部成功,要么全部失败,不会出现部分成功的情况。

原子性示例

假设我们有一个应用,需要对某个特定事件进行计数。在高并发环境下,我们需要确保计数的原子性,避免出现数据不一致的问题

local key = KEYS[1]                      -- 获取第一个键
local value = tonumber(ARGV[1])         -- 获取第一个参数,作为初始值
local increment = tonumber(ARGV[2])   -- 获取第二个参数,作为增量

-- 获取当前键的值
local current_value = redis.call('GET', key)

-- 如果键存在且有值
if current_value then
    current_value = tonumber(current_value)
    -- 合并当前值与新增值
    redis.call('SET', key, current_value + increment)
    return current_value + increment
else
    -- 如果键不存在或值为空,设置初始值
    redis.call('SET', key, increment)
    return increment
end
具体更多的用法看这位大佬写的吧:https://blog.csdn.net/z_344791576/article/details/143996794

示例对比

假设你需要为多个键批量设置字段并设置过期时间,以下是两种方式的对比:

直接命令

for _, key := range keys {
    for _, field := range fields {
        _, err := rdb.HSet(ctx, key, field, value).Result()
        if err != nil {
            log.Fatalf("Failed to set field: %v", err)
        }
    }
    _, err := rdb.Expire(ctx, key, time.Duration(ttl)*time.Second).Result()
    if err != nil {
        log.Fatalf("Failed to set expire: %v", err)
    }
}
// 多次循环执行命令,负载高

Lua 脚本

-- Lua 脚本
local keys = KEYS
local values = ARGV
local ttl = tonumber(values[#values])

for i = 1, #keys, fieldsPerKey + 1 do
    local key = keys[i]
    for j = 1, fieldsPerKey do
        local field = keys[i + j]
        local value = values[j]
        redis.call("HSET", key, field, value)
    end
    redis.call("EXPIRE", key, ttl)
end

Go 调用 Lua 脚本

keys := []string{
    "myHash1", "name", "email", "age",
    "myHash2", "name", "email", "age",
}
values := []string{
    "John Doe", "john.doe@example.com", "30",
    "Jane Smith", "jane.smith@example.com", "25",
}
ttl := 60

values = append(values, fmt.Sprintf("%d", ttl))
results, err := rdb.Eval(ctx, luaScript, keys, values).Result()

性能对比

  • 直接命令:每次操作都需要一次网络往返,高并发时性能较差。
  • Lua 脚本:一次网络往返,原子性操作,性能显著提升。

总结

在并发场景下,使用 Lua 脚本操作 Redis 有以下优势:

  1. 减少网络往返次数,提高性能。
  2. 保证操作的原子性,确保一致性。
  3. 减轻客户端负载,提高客户端效率。
  4. 减少 Redis 服务器的上下文切换,优化服务器性能。

因此,在需要批量操作或高并发场景下,推荐使用 Lua 脚本。

补充

上面提到的 EVALSHA 是 Redis 提供的一个命令,用于执行已经加载到 Redis 的 Lua 脚本。

package main

import (
    "context"
    "fmt"
    "log"

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

var ctx = context.Background()

func main() {
    // 连接到 Redis
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379", // Redis 服务器地址
        Password: "",               // Redis 密码,没有则留空
        DB:       0,                // 默认数据库编号
    })

    // 测试连接
    _, err := rdb.Ping(ctx).Result()
    if err != nil {
        log.Fatalf("Failed to connect to Redis: %v", err)
    }
    fmt.Println("Connected to Redis successfully!")

    // 定义 Lua 脚本
    luaScript := `
        local keys = KEYS
        local values = ARGV
        local ttl = tonumber(values[#values])     -- 获取过期时间(以秒为单位)

        -- 每个键的字段数量
        local fieldsPerKey = 3

        -- 批量插入字段并设置过期时间
        for i = 1, #keys, fieldsPerKey + 1 do
            local key = keys[i]
            local valueIndex = 1     -- 初始化值的索引
            for j = 1, fieldsPerKey do
                local field = keys[i + j]
                local value = values[valueIndex]     -- 获取对应的值
                redis.call("HSET", key, field, value)
                valueIndex = valueIndex + 1     -- 移动到下一个值
            end
            -- 设置过期时间
            redis.call("EXPIRE", key, ttl)
        end

        -- 检索指定字段
        local res = {}
        for i = 1, #keys, fieldsPerKey + 1 do
            local key = keys[i]
            for j = 1, fieldsPerKey do
                local field = keys[i + j]
                table.insert(res, redis.call("HGET", key, field))
            end
        end
        return res
    `

    // 加载 Lua 脚本并获取 SHA1 哈希值
    scriptSHA, err := rdb.ScriptLoad(ctx, luaScript).Result()
    if err != nil {
        log.Fatalf("Failed to load Lua script: %v", err)
    }
    fmt.Printf("Script SHA1: %s\n", scriptSHA)

    // 调用 Lua 脚本
    keys := []string{
        "myHash1", "name", "email", "age",
        "myHash2", "name", "email", "age",
    }
    values := []string{
        "John Doe", "john.doe@example.com", "30",
        "Jane Smith", "jane.smith@example.com", "25",
    }
    ttl := 60 // 设置过期时间为 60 秒

    // 将过期时间添加到 values 数组的末尾
    values = append(values, fmt.Sprintf("%d", ttl))

    // 使用 EVALSHA 命令运行 Lua 脚本
    results, err := rdb.EvalSha(ctx, scriptSHA, keys, values).Result()
    if err != nil {
        log.Fatalf("Failed to execute Lua script: %v", err)
    }

    // 打印结果
    fmt.Printf("Inserted and retrieved values: %v\n", results)
}

性能优化

  • 使用 EVALSHA 可以避免重复加载相同的脚本,从而提高性能。
  • 如果脚本已经加载到 Redis 中,EVALSHA 命令会直接执行脚本,而不需要重新解析和加载。

但其实普通用和 Eval 用法差距不大。

PREV
[Golang] Map的底层实现原理与并发安全(待填坑)
NEXT
UCloud笔试 编程第一题(以及输入的讨论)

评论(0)

发布评论