百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

Lua在Redis的应用 redis使用lua

liebian365 2024-10-29 15:54 23 浏览 0 评论

首发于公众号 后端搬运工《Lua在Redis的应用

Redis 从 2.6 版本起,也已开始支持 Lua 脚本,我们可以更加得心应手地使用或扩展 Redis,特别是在高并发场景下 Lua 脚本提供了更高效、可靠的解决方案。

为什么要使用Lua

我们先看一个抢购场景下 商品库存 的问题,用 PHP 可简单实现为:

$key = 'number:string';
$redis = new Redis();
$number = $redis->get($key);
if ($number <= 0) {
    return 0;
}
$redis->decr($key);
return $number--;

这段代码其实存在问题,高并发时会出现库存超卖的情况,因为上述操作在 Redis 中不是原子操作,会导致库存逻辑的判断失效。尽管可以通过优化代码来解决问题,比如使用 Decr 原子操作命令、或者使用 锁 的方式,但这里使用 Lua 脚本来解决。

local key = 'number:string'
local number = tonumber(redis.call("GET", key))
if number <= 0 then
   return 0
end
redis.call("DECR", key)
return number--

这段脚本代码虽然是 Lua 语言编写( 进入Lua的世界),但是其实就是 PHP 版本的翻译版。那为什么这样,Lua 脚本就能解决库存问题了呢?

Redis 中嵌入 Lua 脚本,所具有的几个特性为:

  • 原子操作:Redis 将整个 Lua 脚本作为一个原子执行,无需考虑并发,无需使用事务来保证数据一致性;
  • 高性能:嵌入 Lua 脚本后,可以减少多个命令执行的网络开销,进而间接提高 Redis 性能;
  • 可复用:Lua 脚本会保存于 Redis 中,客户端都可以使用这些脚本;

在Redis中嵌入Lua

使用Lua解析器

Redis 提供了 EVAL(直接执行脚本) 和 EVALSHA(执行 SHA1 值的脚本) 这两个命令,可以使用内置的 Lua 解析器执行 Lua 脚本。语法格式为:

  • EVAL script numkeys key [key …] arg [arg …]
  • EVALSHA sha1 numkeys key [key …] arg [arg …]

参数说明:

  • script / sha1:EVAL 命令的第一个参数为需要执行的 Lua 脚本字符,EVALSHA 命令的一个参数为 Lua 脚本的 SHA1 值
  • numkeys:表示 key 的个数
  • key [key …]:从第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局数组 KYES[i] 访问
  • arg [arg …]:附加参数,在 Lua 中通过全局数组 ARGV[i] 访问

EVAL 命令的使用示例:

> EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

每次使用 EVAL 命令都会传递需执行的 Lua 脚本内容,这样增加了宽带的浪费。Redis 内部会永久保存被运行在脚本缓存中,所以使用 EVALSHA(建议使用) 命令就可以根据脚本 SHA1 值执行对应的 Lua 脚本。

> SCRIPT LOAD "return 'hello'"
"1b936e3fe509bcbc9cd0664897bbe8fd0cac101b"
> EVALSHA 1b936e3fe509bcbc9cd0664897bbe8fd0cac101b 0
"hello"

Redis 中执行 Lua 脚本都是以原子方式执行,所以是原子操作。另外,redis-cli 命令行客户端支持直接使用--eval lua_file参数执行 Lua 脚本。

Redis 中有关脚本的命令除了 EVAL 和 EVALSHA 外,其他常用命令 如下:

命令

描述

SCRIPT EXISTS script [script …]

查看脚本是是否保存在缓存中

SCRIPT FLUSH

从缓存中移除所有脚本

SCRIPT KILL

杀死当前运行的脚本

SCRIPT LOAD script

将脚本添加到缓存中,不立即执行
返回脚本SHA1值

数据类型的转换

由于 Redis 和 Lua 都有各自定义的数据类型,所以在使用执行完 Lua 脚本后,会存在一个数据类型转换的过程。

Lua 到 Redis 类型转换与 Redis 到 Lua 类型转换相同部分关系:

Lua 类型

Redis 返回类型

说明

number

integer

浮点数会转换为整数
3.333–>3

string

bulk


table(array)

multi bulk


boolean false

nil


> EVAL "return 3.333" 0
(integer) 3
> EVAL "return 'fhb'" 0
"fhb"
> EVAL "return {'fhb', 'lw', 'lbf'}" 0
1) "fhb"
2) "lw"
3) "lbf"
> EVAL "return false" 0
(nil)

需要注意的是,从 Lua 转化为 Redis 类型比 Redis 转化为 Lua 类型多了一条 额外 规则:

Lua 类型

Redis 返回类型

说明

boolean true

integer

返回整型 1

> EVAL "return true" 0
(integer) 1

总而言之,类型转换的原则 是将一个 Redis 值转换成 Lua 值,之后再将转换所得的 Lua 值转换回 Redis 值,那么这个转换所得的 Redis 值应该和最初时的 Redis 值一样。

全局变量保护

为了防止不必要的数据泄漏进 Lua 环境, Redis 脚本不允许创建全局变量。

-- 定义全局函数
function f(n)
    return n * 2
end
return f(4);

执行redis-cli --eval function.lua命令,会抛出尝试定义全局变量的错误:

(error) ERR Error running script (call to f_0a602c93c4a2064f8dc648c402aa27d68b69514f): @enable_strict_lua:8: user_script:1: Script attempted to create global variable 'f'

Lua脚本调用Redis命令

Redis 创建了用于与 Lua 环境协作的组件—— 伪客户端,它负责执行 Lua 脚本中的 Redis 命令。

调用Redis命令

在 Redis 内置的 Lua 解析器中,调用 redis.call() 和 redis.pcall() 函数执行 Redis 的命令。它们除了处理错误的行为不一样外,其他行为都保持一致。调用 格式:

  • redis.call(command, [key …], arg [arg …] )
  • redis.pcall(command, [key …], arg [arg …] )
> EVAL "return redis.call('SET', 'name', 'fhb')" 0
> EVAL "return redis.pcall('GET', 'name')" 0
"fhb"

Redis日志

在 Lua 脚本中,可以通过调用 redis.log() 函数来写 Redis 日志。格式为:

redis.log(loglevel, message)

loglevel 参数可以是 redis.LOG_DEBUG、redis.LOG_VERBOSE、redis.LOG_NOTICE、redis.LOG_WARNING 的任意值。

查看redis.conf日志配置信息:

# logleval必须一致才会记录
loglevel notice
logfile "/home/logs/redis.log"

Lua 写 Redis 日志示例:

> EVAL "redis.log(redis.LOG_NOTICE, 'I am fhb')" 0
113:M 04 Sep 13:12:36.229 * I am fhb

案例

API 访问速率控制

通过 Lua 实现一个针对用户的 API 访问速率控制,Lua 代码如下:

local key = "rate.limit:string:" .. KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = tonumber(ARGV[2])
local times = redis.call("INCR", key)
if times == 1 then
    redis.call("EXPIRE", key, expire_time)
end
if times > limit then
    return 0
end
return 1

KEYS[1] 可以用 API 的 URI + 用户 uid 组成,ARGV[1] 为单位时间限制访问的次数,ARGV[2] 为限制的单位时间。

批量HGETTALL

这个例子演示通过 Lua 实现批量 HGETALL,当然也可以使用 管道 实现。

-- KEYS为uid数组
local users = {}
for i,uid in ipairs(KEYS) do
    local user = redis.call('hgetall', uid)
    if user ~= nil then
        table.insert(users, i, user)
    end
end
return users

注意事项

虽然使用 Lua 脚本给我们带来了许多便利,但是需要注意几个使用事项:

  • Lua 脚本在执行时是阻塞的,不应该在 Lua 脚本中有耗时的处理逻辑;
  • 在集群模式时,Lua 脚本必须使用参数 key 传递需操作的 Redis 的 key,且要求所操作的 key 都在同一个 slot 节点上,可以使用以{}标记的 hash tag 方式解决。

相关推荐

精品博文嵌入式6410中蓝牙的使用

BluetoothUSB适配器拥有一个BluetoothCSR芯片组,并使用USB传输器来传输HCI数据分组。因此,LinuxUSB层、BlueZUSB传输器驱动程序以及B...

win10跟这台计算机连接的前一个usb设备工作不正常怎么办?

前几天小编闲来无事就跑到网站底下查看粉丝朋友给小编我留言询问的问题,还真的就给小编看到一个问题,那就是win10跟这台计算机连接的一个usb设备运行不正常怎么办,其实这个问题的解决方法时十分简单的,接...

制作成本上千元的键盘,厉害在哪?

这是稚晖君亲自写的开源资料!下方超长超详细教程预警!!全文导航:项目简介、项目原理说明、硬件说明、软件说明项目简介瀚文智能键盘是一把我为自己设计的——多功能、模块化机械键盘。键盘使用模块化设计。左侧的...

E-Marker芯片,USB数据线的“性能中枢”?

根据线缆行业的研究数据,在2019年搭载Type-C接口的设备出货量已达到20亿台,其中80%的笔记本电脑和台式电脑采用Type-C接口,50%的智能手机和平板电脑也使用Type-C接口。我们都知道,...

ZQWL-USBCANFD二次开发通讯协议V1.04

修订历史:1.功能介绍1.1型号说明本文档适用以下型号:  ZQWL-CAN(FD)系列产品,USB通讯采用CDC类实现,可以在PC机上虚拟出一个串口,串口参数N,8,1格式,波特率可以根据需要设置(...

win10系统无法识别usb设备怎么办(win10不能识别usb)

从驱动入手,那么win10系统无法识别usb设备怎么办呢?今天就为大家分享win10系统无法识别usb设备的解决方法。1、右键选择设备管理器,如图:  2、点击更新驱动程序,如图:  3、选择浏览...

微软七月Win8.1可选补丁有内涵,含大量修复

IT之家(www.ithome.com):微软七月Win8.1可选补丁有内涵,含大量修复昨日,微软如期为Win7、Win8.1发布7月份安全更新,累计为6枚安全补丁,分别修复总计29枚安全漏洞,其中2...

如何从零开始做一个 USB 键盘?(怎么制作usb)

分两种情况:1、做一个真正的USB键盘,这种设计基本上不涉及大量的软件编码。2、做一个模拟的USB键盘,实际上可以没有按键功能,这种的需要考虑大量的软件编码,实际上是一个单片机。第一种设计:买现成的U...

电脑识别U盘失败?5个实用小技巧,让你轻松搞定USB识别难题

电脑识别U盘失败?5个实用小技巧,让你轻松搞定USB识别难题注意:有些方法会清除USB设备里的数据,请谨慎操作,如果不想丢失数据,可以先连接到其他电脑,看能否将数据复制出来,或者用一些数据恢复软件去扫...

未知usb设备设备描述符请求失败怎么解决

出现未知daousb设备设备描述符请求失du败解决办zhi法如下:1、按下Windows+R打开【运行】;2、在版本运行的权限输入框中输入:services.msc按下回车键打开【服务】;2、在服务...

读《飘》47章20(飘每章概括)

AndAhwouldn'tleaveMissEllen'sgrandchildrenfornotrashystep-patobringup,never.Here,Ah...

英翻中 消失的过去 37(消失的英文怎么说?)

翻译(三十七):消失的过去/茱迪o皮考特VanishingActs/JodiPicoult”我能做什么?“直到听到了狄利亚轻柔的声音,我才意识到她已经在厨房里站了好一会儿了。当她说话的时候,...

RabbitMQ 延迟消息实战(rabbitmq如何保证消息不被重复消费)

现实生活中有一些场景需要延迟或在特定时间发送消息,例如智能热水器需要30分钟后打开,未支付的订单或发送短信、电子邮件和推送通知下午2:00开始的促销活动。RabbitMQ本身没有直接支持延迟...

Java对象拷贝原理剖析及最佳实践(java对象拷贝方法)

作者:宁海翔1前言对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po、Dto、Do、Vo各个表现层数据的转换,也存在于系统交互如序列化、反序列化。Java对象拷贝分为深拷贝和浅拷贝,目前常用的...

如何将 Qt 3D 渲染与 Qt Quick 2D 元素结合创建太阳系行星元素?

Qt组件推荐:QtitanRibbon:遵循MicrosoftRibbonUIParadigmforQt技术的RibbonUI组件,致力于为Windows、Linux和MacOSX提...

取消回复欢迎 发表评论: