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
命令里面包含四个值,分别是context
、lua命令脚本
、KEYS
、VALUES
,其中 KEYS
和 VALUES
可以是 []string
或者 []interface
。
为什么在并发场景要使用Lua来操纵Redis?
- 减少网络往返次数
- 直接命令:如果你从客户端发送多个 Redis 命令(例如
HSET
、EXPIRE
等),每个命令都会产生一次网络往返。在并发场景下,多个客户端同时发送多个命令,网络延迟会显著增加。 - Lua 脚本:Lua 脚本将多个操作封装在一个命令中,通过
EVAL
或EVALSHA
命令一次性发送给 Redis 服务器。这样可以显著减少网络往返次数,从而提高性能。
- 原子性
- 直接命令:多个命令在客户端发送时是独立执行的,可能会被其他命令插入,导致操作不原子。例如,如果你先执行
HSET
,然后执行EXPIRE
,在两者之间可能会有其他客户端的命令插入,导致不一致。 - Lua 脚本:Lua 脚本在 Redis 服务器端执行时是原子的。一旦脚本开始执行,Redis 会将其作为一个整体处理,不会被其他命令打断。这确保了操作的一致性和完整性。
- 减少客户端负载
- 直接命令:客户端需要处理多个命令的发送和响应,这会增加客户端的负载,尤其是在并发场景下。
- Lua 脚本:客户端只需要发送一个
EVAL
命令,由 Redis 服务器端执行脚本并返回结果。这减轻了客户端的负载,使得客户端可以更高效地处理其他任务。
- 减少 Redis 服务器的上下文切换
- 直接命令:每个命令都会触发一次 Redis 服务器的上下文切换,尤其是在高并发场景下,这会导致服务器性能下降。
- Lua 脚本:Lua 脚本在 Redis 服务器端执行时,所有操作都在一个上下文中完成,减少了上下文切换的开销。
- 优化性能
- 直接命令:多个命令的执行需要多次解析和处理,增加了 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 有以下优势:
- 减少网络往返次数,提高性能。
- 保证操作的原子性,确保一致性。
- 减轻客户端负载,提高客户端效率。
- 减少 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
用法差距不大。
评论(0)