Lua脚本使用说明

更新时间:
复制为 MD 格式

当业务需要在一次数据库交互中原子性地执行多个命令时,例如实现分布式锁、限流器或进行条件更新,多次网络往返会增加延迟并引入竞态条件风险。Orca(兼容Redis协议)提供的Lua脚本功能,允许您将多个Redis兼容命令封装在一个脚本中,由数据库原子化执行,从而有效解决此类问题,提升复杂操作的执行效率与数据一致性。

功能简介

Orca(兼容Redis协议)的Lua脚本功能,允许您像在Redis中一样执行Lua脚本。其核心优势在于原子性和高性能。

  • 原子性:脚本内的所有命令作为一个不可分割的单元执行。脚本执行期间,不会有其他命令或脚本插入,保证了操作的原子性,避免了部分执行导致的中间状态。

  • 高性能

    • 减少网络开销:将多个命令打包在单个脚本中一次性发送给数据库,显著减少了客户端与服务器之间的网络往返次数。

    • 脚本缓存:通过SCRIPT LOAD命令将脚本预加载到集群内存中,并获得一个SHA1校验和。后续可通过EVALSHA命令,仅发送此简短的SHA1值来调用脚本,进一步降低网络传输的数据量。

适用范围

  • 集群版本:需为MySQL 8.0.2,且内核小版本需为8.0.2.2.33及以上版本。

  • 连接地址:需使用读写模式可读可写(自动读写分离)Orca地址执行Lua脚本命令。

  • 不支持的命令:以下命令无法在Lua脚本中通过redis.call()redis.pcall()执行,否则将返回错误。

    • 事务控制命令:WATCHUNWATCHMULTIEXECDISCARD

    • 阻塞命令:BLPOPBRPOP

    • Lua脚本命令:EVALEVAL_ROEVALSHAEVALSHA_ROSCRIPT

    • 订阅发布命令:SUBSCRIBEUNSUBSCRIBEPSUBSCRIBEPUNSUBSCRIBE

    • 连接管理命令:AUTHHELLOCLIENT

基本语法

以下是操作Lua脚本的核心命令。更多信息请参见Redis官网Lua 脚本相关命令Scripting with Lua

命令

语法

说明

EVAL

EVAL script numkeys [key [key ...]] [arg [arg ...]]

直接执行给定的Lua脚本。这是最基础的执行方式。参数说明:

  • script:Lua脚本。

  • numkeys:指定KEYS[]参数的数量,非负整数。

  • KEYS[]:传入的Redis键参数,数组的索引均从1开始。

  • ARGV[]:传入的脚本参数,数组的索引均从1开始。

EVAL_RO

EVAL_RO script numkeys [key [key ...]] [arg [arg ...]]

只读模式下执行Lua脚本。适用于读写分离场景,确保脚本不会执行写操作。若脚本中包含写命令,将返回错误。参数说明与EVAL相同。

EVALSHA

EVALSHA sha1 numkeys [key [key ...]] [arg [arg ...]]

通过脚本的SHA1校验和执行已缓存的脚本。如果脚本未缓存,将返回NOSCRIPT错误。请通过EVALSCRIPT LOAD命令将目标脚本缓存后进行重试。

EVALSHA_RO

EVALSHA_RO sha1 numkeys [key [key ...]] [arg [arg ...]]

只读模式下通过脚本的SHA1校验和执行已缓存的脚本。适用于读写分离场景,确保脚本不会执行写操作。若脚本中包含写命令,将返回错误。参数说明与EVALSHA相同。

SCRIPT LOAD

SCRIPT LOAD script

将脚本加载到缓存中,并返回其SHA1校验和,以便后续通过EVALSHA调用。

SCRIPT EXISTS

SCRIPT EXISTS sha1 [sha1 ...]

检查一个或多个SHA1校验和对应的脚本是否存在于缓存中。存在返回1,不存在返回0。

SCRIPT KILL

SCRIPT KILL

终止当前正在执行的Lua脚本。Orca支持终止任意脚本(包括写脚本),并会自动回滚该脚本已产生的数据变更,保证数据一致性。

SCRIPT FLUSH

SCRIPT FLUSH [ASYNC | SYNC]

清空当前集群中的所有Lua脚本缓存。

  • ASYNC:异步清空脚本缓存。

  • SYNC(默认):同步清空脚本缓存。

操作示例

  1. 准备测试数据:

    SET polardb orca
  2. 测试命令:

    EVAL

    EVAL "return redis.call('GET', KEYS[1])" 1 polardb

    返回示例:

    "orca"

    EVAL_RO

    EVAL_RO "return redis.call('GET', KEYS[1])" 1 polardb

    返回示例:

    "orca"

    SCRIPT LOAD

    SCRIPT LOAD "return redis.call('GET', KEYS[1])"

    返回示例:

    "d3c21d0c2b9ca22f82737626a27bcaf5d288f99f"

    EVALSHA

    EVALSHA d3c21d0c2b9ca22f82737626a27bcaf5d288f99f 1 polardb

    返回示例:

    "orca"

    EVALSHA_R

    EVALSHA_RO d3c21d0c2b9ca22f82737626a27bcaf5d288f99f 1 polardb

    返回示例:

    "orca"

    SCRIPT EXISTS

    SCRIPT EXISTS d3c21d0c2b9ca22f82737626a27bcaf5d288f99f ffffffffffffffffffffffffffffffffffffffff

    返回示例:

    1) (integer) 1
    2) (integer) 0

    SCRIPT FLUSH

    警告

    该命令会清空集群中的所有Lua脚本缓存(SCRIPT LOAD/EVAL产生的缓存都会被移除)。

    • 清空后,所有依赖EVALSHA/EVALSHA_RO的调用都可能返回NOSCRIPT错误,需要重新加载脚本并重试。

    • 若缓存脚本较多,SCRIPT FLUSH可能阻塞集群较长时间,建议在业务低峰期执行。

    • 建议您在客户端/应用侧保留脚本源代码,并实现NOSCRIPT的自动恢复机制(重新SCRIPT LOAD或使用EVAL重新注册)。

    SCRIPT FLUSH

    返回示例:

    OK

常见使用场景

配置并执行一个原子计数器

避免并发INCR导致的竞态,确保计数器严格递增且可带条件重置。

流程概述

  1. 编写Lua脚本。

  2. 加载至集群缓存。

  3. 通过EVALSHA复用执行。

SCRIPT LOAD + EVALSHA减少网络传输量,提升性能与幂等性。脚本内容不暴露于请求体。

操作步骤

  1. 编写脚本(本地保存):

    -- counter.lua:若 key 不存在则设为 0,再 +1;返回新值
    if redis.call('EXISTS', KEYS[1]) == 0 then
      redis.call('SET', KEYS[1], 0)
    end
    return redis.call('INCR', KEYS[1])
  2. 加载脚本并获取SHA1

    SCRIPT LOAD "if redis.call('EXISTS', KEYS[1]) == 0 then redis.call('SET', KEYS[1], 0) end return redis.call('INCR', KEYS[1])"
    
    
    -- 返回结果
    "b547eabbcde73b25330442e4f4e4dc1783b91241"
  3. (推荐)复用执行:

    EVALSHA b547eabbcde73b25330442e4f4e4dc1783b91241 1 my_counter
    
    -- 返回结果
    (integer) 1
    -- 再次执行返回结果
    (integer) 2

执行纯查询脚本

  1. 确认脚本不含写操作检查是否调用 SETDELHSETLPUSH等任意写命令。若存在,禁止使用EVAL_RO

    说明

    若脚本含写命令,将返回错误(error) ERR Write commands are not allowed from read-only scripts.

  2. 使用EVAL_RO执行:

    EVAL_RO "return {redis.call('SET', KEYS[1]), redis.call('TTL', KEYS[1])}" 1 polardb
    
    -- 返回结果
    1) "orca"
    2) (integer) -1

终止异常运行的Lua脚本

防止长耗时脚本阻塞集群。中断执行,系统将自动回滚全部变更,保持数据一致性。执行SCRIPT KILL

说明

仅能终止当前正在执行的脚本,不支持指定SHA1终止。

SCRIPT KILL

-- 返回结果
OK

性能优化实践

为降低Lua脚本对集群的阻塞影响,并避免集群缓存大量功能重复的脚本导致内存占用过高,建议遵循以下实践:

  • 超时限制:单个Lua脚本的默认执行超时时间为300秒。

    • 避免编写过大的Lua脚本,防止占用过多的内存。

    • 避免在Lua脚本中长时间、大批量写入数据。

  • 阻塞风险:Lua脚本在集群中以原子方式执行。在执行期间,会阻塞其他命令与脚本,因此应控制写入批量与执行时长,以避免长循环或大范围遍历。如有必要,应将复杂逻辑拆分为多个独立脚本进行执行。

  • 脚本缓存非持久化:集群运行时Lua脚本缓存不会被清除,集群重启、主备切换(HA)或执行SCRIPT FLUSH命令后会清理Lua脚本缓存,需要重新注册。

  • 脚本编写规范:使用KEYS[]ARGV[]传递参数,避免将参数硬编码在脚本中。

  • 减少网络流量:使用SCRIPT LOAD + EVALSHA组合,获得最佳性能并减少网络流量。

操作示例

  1. 加载脚本:在应用初始化或首次使用时,通过SCRIPT LOAD将脚本加载到集群并获取其SHA1值。您的应用程序应在本地保存此SHA1值。

    # 加载一个设置键值的脚本
    SCRIPT LOAD "return redis.call('set', KEYS[1], ARGV[1])"
    # 返回示例
    "55b22c0d0cedf3866879ce7c854970626dcef0c3"
  2. 执行脚本:后续通过EVALSHA命令,使用上一步获取的SHA1值来执行脚本。

    # 使用缓存的脚本设置 k1 = v1
    EVALSHA 55b22c0d0cedf3866879ce7c854970626dcef0c3 1 k1 v1
    # 使用同一个缓存的脚本设置 k2 = v2
    EVALSHA 55b22c0d0cedf3866879ce7c854970626dcef0c3 1 k2 v2
  3. 处理NOSCRIPT错误:如果EVALSHA返回NOSCRIPT错误,应用程序应捕获此错误,然后自动切换回使用EVALSCRIPT LOAD命令执行一次脚本将脚本缓存至集群中。

    • EVAL:在执行的同时会自动将脚本重新缓存。

    • SCRIPT LOAD:将脚本加载到集群并获取其SHA1值。

  4. 清理Lua脚本的内存占用:当您在不需要执行Lua脚本时,可执行SCRIPT FLUSH命令清除Lua脚本缓存。若集群中缓存的Lua脚本过多,该命令可能会阻塞集群较长时间,建议在业务低峰期执行。

常见问题

  • 如何处理NOSCRIPT No matching script. Please use EVAL.错误?

    问题原因:这个错误表示您尝试使用EVALSHAEVALSHA_RO命令执行一个不存在于集群缓存中的脚本。通常由集群重启、主备切换或执行了SCRIPT FLUSH导致。
    解决方案:您的客户端代码需要具备错误处理逻辑。当捕获到NOSCRIPT错误时,应改用EVALSCRIPT LOAD命令重新执行该脚本将脚本缓存至集群中,后续的EVALSHA调用即可恢复正常。

  • Lua脚本执行超时怎么办?

    脚本默认超时时间为300秒。超时通常意味着脚本逻辑过于复杂或处理的数据量过大。
    解决方案:

    • 优化脚本逻辑,避免在脚本内执行低效的循环或大规模数据遍历。

    • 如果一个脚本执行时间过长,可以通过SCRIPT KILL命令终止它。PolarDB会回滚该脚本已做的所有修改。

    • 将复杂的业务逻辑拆分为多个更小、更快的脚本分步执行。

  • 为什么使用EVAL_RO执行脚本会报错ERR Write commands are not allowed from read-only scripts.

    EVAL_ROEVALSHA_RO是只读模式,严禁执行任何写操作命令(如SETHSETDEL等)。此错误明确指出您的脚本中包含了写命令。
    解决方案:

    • 检查并移除脚本中的所有写命令。

    • 如果您确实需要执行写操作,请改用EVALEVALSHA命令。