Go语言下Redis Lua脚本实战学习| 青训营

240 阅读4分钟

Redis各个语言的Sdk已经很好用了,为什么Redis本身还要支持脚本语言呢? 为什么还特定是Lua 脚本呢?

Lua 脚本介绍

Lua(发音: /ˈluːə/,葡萄牙语“月亮”)是一个简洁、轻量、可扩展的脚本语言。Lua有着相对简单的C语言API而很容易嵌入应用中[3]。很多应用程序使用Lua作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性 4 详见Wiki百科 Lua 在很多场景下都选择了Lua,因为它足够轻量足够方便,像是Nginx,Redis等

Redis支持Lua另外的原因 虽然Redis的指令很丰富,但是某些特定领域,需要扩充若干指令原子性执行时,仅使用原生命令便无法完成。

Redis 为这样的用户场景提供了 lua 脚本支持,用户可以向服务器发送 lua 脚本来执行自定义动作,获取脚本的响应数据。Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被其它任何请求打断

Go-Redis

Go-redis实战Lua脚本 详细 -> redis 命令

func TestLuaCall(t *testing.T) {
    s := redis.NewScript(`
    local key = KEYS[1]
    local change = ARGV[1]
    
    local value = redis.call("GET", key)
    if not value then
      value = 0
    end
    
    value = value + change
    redis.call("SET", key, value)
    
    return value
    `)
    result, err := s.Run(ctx, IRB, []string{"Key"}, 2).Result()
    if err != nil {
       t.Error(err)
    }
    log.Println(result)
}

用Lua脚本简单实现一个INCRBY命令,还有更适合的条件

Lua 和 Go 类型

在Goredis 中可以方便的执行Lua脚本,但需要注意在不同环境下的类型,Lua 的 number 是一个浮点型数字,用于存储整数和浮点数,在 Lua 中不区分整数和浮点数,但 Redis 总是将 Lua 数字转换为舍去小数部分的整数,例如 3.14 变成 3,如果要返回浮点值,将其作为字符串返回并用 Go 解析成 float64。

Lua returnGo interface{}
number (float64)int64 (舍弃小数)
stringstring
falseredis.Nil error
trueint64(1)
{ok = "status"}string("status")
{err = "error message"}errors.New("error message")
{"foo", "bar"}[]interface{}{"foo", "bar"}
{foo = "bar", bar = "baz"}[]interface{}{} (不支持)

在Lua环境下参数传递均在KEYS和ARGV数组中。

实际运用枚例

限流控制

local key = KEYS[1]  -- 键名
local limit = tonumber(ARGV[1])  -- 限制的请求数
local window = tonumber(ARGV[2])  -- 时间窗口大小(秒)
local current = tonumber(redis.call('GET', key) or "0")  -- 当前请求数

if current + 1 > limit then
    return 0  -- 超过限制,请求被拒绝
else
    redis.call('INCRBY', key, 1)  -- 请求数加1
    redis.call('EXPIRE', key, window)  -- 设置过期时间
    return 1  -- 请求通过
end

秒杀或者抢红包

原子化执行保证并发安全

local product_id = ARGV[1]
local user_id = ARGV[2]

local stock = tonumber(redis.call("HGET", "product_stock", product_id))
if stock > 0 then
    redis.call("HINCRBY", "product_stock", product_id, -1)
    redis.call("SADD", "seckill_orders:" .. product_id, user_id)
    return 1
else
    return 0
end

分布式锁

local lock_key = KEYS[1]     -- 锁的键名
local lock_value = ARGV[1]   -- 锁的值
local lock_timeout = tonumber(ARGV[2])  -- 锁的超时时间(秒)

-- 尝试获取锁
local acquire_lock = redis.call('SET', lock_key, lock_value, 'NX', 'EX', lock_timeout)
if acquire_lock then
    return 1
else
    return 0
end

总结

Lua在Redis中执行也是有一定损耗的,但是也要具体环境。

脚本加载和编译:  当Redis首次执行一个Lua脚本时,它会对脚本进行加载和编译。这个过程只会在第一次执行脚本时发生,后续执行会重复使用已编译的版本

脚本复杂性:  复杂的Lua脚本可能需要更多的计算和内存操作,从而导致性能损耗。例如,对大型数据集进行迭代和操作、使用复杂的数据结构或算法等都可能增加脚本的执行时间和资源消耗

脚本执行时间:  如果Lua脚本本身执行时间较长,会对Redis的性能产生影响。长时间运行的脚本会阻塞Redis服务器,导致其他客户端请求的延迟。因此,在编写Lua脚本时,应尽量保持脚本的执行时间较短,避免对Redis性能造成过大的影响

能够执行原子性操作的Lua脚本也能够减少网络多次传输带来的开销,还得权衡业务逻辑的实际要求,继续深入学习Redis φ(* ̄0 ̄)