2023年11月27日发(作者:)

第⼗节:Redis脚本、Lua语法学习、以及秒杀案例脚本分析

1. 简介

Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使⽤ EVAL 命令对 Lua 脚本进⾏求值。在lua脚本中可以通过两个不同的函数调⽤redis命令,分别是:() ()

(1). 脚本的原⼦性

Redis 使⽤单个 Lua 解释器去运⾏所有脚本,并且, Redis 也保证脚本会以原⼦性(atomic)的⽅式执⾏:当某个脚本正在运⾏的时候,不会有其他脚本或 Redis 命令被执⾏。这和使⽤

MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)

另⼀⽅⾯,这也意味着,执⾏⼀个运⾏缓慢的脚本并不是⼀个好主意。写⼀个跑得很快很顺溜的脚本并不难,因为脚本的运⾏开销(overhead)⾮常少,但是当你不得不使⽤⼀些跑得⽐较

慢的脚本时,请⼩⼼,因为当这些蜗⽜脚本在慢吞吞地运⾏的时候,其他客户端会因为服务器正忙⽽⽆法执⾏命令。

(2). 错误处理

() () 的唯⼀区别在于它们对错误处理的不同。

A. () 在执⾏命令的过程中发⽣错误时,脚本会停⽌执⾏,并返回⼀个脚本错误,错误的输出信息会说明错误造成的原因.

B. () 出错时并不引发(raise)错误,⽽是返回⼀个带 err 域的 Lua (table),⽤于表⽰错误

(3). 带宽和EVALSHA

A. EVAL 命令要求你在每次执⾏脚本的时候都发送⼀次脚本主体(script body)Redis 有⼀个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出⽆谓的带宽来传送

脚本主体并不是最佳选择。

B.为了减少带宽的消耗, Redis 实现了 EVALSHA 命令,它的作⽤和 EVAL ⼀样,都⽤于对脚本求值,但它接受的第⼀个参数不是脚本,⽽是脚本的 SHA1 校验和(sum)

C. 客户端库的底层实现可以⼀直乐观地使⽤ EVALSHA 来代替 EVAL ,并期望着要使⽤的脚本已经保存在服务器上了,只有当 NOSCRIPT 错误发⽣时,才使⽤ EVAL 命令重新发送

脚本,这样就可以最⼤限度地节省带宽。

D. 这也说明了执⾏ EVAL 命令时,使⽤正确的格式来传递键名参数和附加参数的重要性:因为如果将参数硬写在脚本中,那么每次当参数改变的时候,都要重新发送脚本,即使脚本的主

体并没有改变,相反,通过使⽤正确的格式来传递键名参数和附加参数,就可以在脚本

主体不变的情况下,直接使⽤ EVALSHA 命令对脚本进⾏复⽤,免去了⽆谓的带宽消耗。

(4). 脚本缓存

A. Redis 保证所有被运⾏过的脚本都会被永久保存在脚本缓存当中,这意味着,当 EVAL命令在⼀个 Redis 实例上成功执⾏某个脚本之后,随后针对这个脚本的所有 EVALSHA 命令都会

成功执⾏。

B. 刷新脚本缓存的唯⼀办法是显式地调⽤ SCRIPT FLUSH 命令,这个命令会清空运⾏过的所有脚本的缓存。通常只有在云计算环境中,Redis 实例被改作其他客户或者别的应⽤程序的

实例时,才会执⾏这个命令。

C. 缓存可以长时间储存⽽不产⽣内存问题的原因是,它们的体积⾮常⼩,⽽且数量也⾮常少,即使脚本在概念上类似于实现⼀个新命令,即使在⼀个⼤规模的程序⾥有成百上千的脚本,

即使这些脚本会经常修改,即便如此,储存这些脚本的内存仍然是微不⾜道的。

D. 事实上,⽤户会发现 Redis 不移除缓存中的脚本实际上是⼀个好主意。⽐如说,对于⼀个和 Redis 保持持久化链接(persistent connection)的程序来说,它可以确信,执⾏过⼀次的脚本

会⼀直保留在内存当中,因此它可以在流⽔线中使⽤ EVALSHA 命令⽽不必担⼼因为找不到所需的脚本⽽产⽣错误(稍候我们会看到在流⽔线中执⾏脚本的相关问题)

(5). 全局变量保护

为了防⽌不必要的数据泄漏进 Lua 环境, Redis 脚本不允许创建全局变量。如果⼀个脚本需要在多次执⾏之间维持某种状态,它应该使⽤ Redis key 来进⾏状态保存。

实现全局变量保护并不难,不过有时候还是会不⼩⼼⽽为之。⼀旦⽤户在脚本中混⼊了Lua 全局状态,那么 AOF 持久化和复制(replication)都会⽆法保证,所以,请不要使⽤全局变

量。避免引⼊全局变量的⼀个诀窍是:将脚本中⽤到的所有变量都使⽤ local 关键字定义

为局部变量。

(6). 纯函数脚本、内置Lua库、redis⽇志、沙箱和最⼤执⾏时间

详见redis的帮助⽂档了。

2. 脚本指令

(1). eval

执⾏lua脚本

#格式

eval script numkeys key [key ...] arg [arg ...]

#参数说明

#script:是⼀段 Lua 5.1 脚本程序,它会被运⾏在 Redis 服务器上下⽂中,这段脚本不必(也不应该)定义为⼀个 Lua 函数。

#numkeys:⽤于指定键名参数的个数。

#key:键名参数,表⽰在脚本中所⽤到的那些 Redis (key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,⽤ 1 为基址的形式访问( KEYS[1] KEYS[2] ,以此类推)

#arg:全局变量,可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] ARGV[2] ,诸如此类)

实操1

实操2 lua脚本中可以通过两个不同的函数调⽤redis命令,分别是:() ()

#写法1

eval "return ('set','name1','ypf1')" 0

#写法2 (推荐!!)

eval "return ('set',KEYS[1],'ypf2')" 1 name2

剖析:

写法1违反了EVAL 命令的语义,因为脚本⾥使⽤的所有键都应该由 KEYS 数组来传递。

要求使⽤正确的形式来传递键(key)是有原因的,因为不仅仅是 EVAL 这个命令,所有的 Redis 命令,在执⾏之前都会被分析,以此来确定命令会对哪些键进⾏操作。因此,对于 EVAL

令来说,必须使⽤正确的形式来传递键,才能确保分析⼯作正确地执⾏。除此之外,使⽤正确的形式来传递键还有很多其他好处,它的⼀个特别重要的⽤途就是确保 Redis 集群可以将你的

请求发送到正确的集群节点。( Redis 集群的⼯作还在进⾏当中,但是脚本功能被设计成可以与集群功能保持兼容。)不过,这条规矩并不是强制性的,从⽽使得⽤户有机会滥⽤(abuse)

Redis 单实例配置(single instance configuration),代价是这样写出的脚本不能被 Redis 集群所兼容。

(2). evalsha

根据给定的 sha1 校验码,对缓存在服务器中的脚本进⾏求值

#格式

evalsha sha1 numkeys key [key ...] arg [arg ...]

(3). script load

将脚本 script 添加到脚本缓存中,但并不⽴即执⾏这个脚本。

EVAL 命令也会将脚本添加到脚本缓存中,但是它会⽴即对输⼊的脚本进⾏求值。如果给定的脚本已经在缓存⾥⾯了,那么不做动作。在脚本被加⼊到缓存之后,通过 EVALSHA 命令,

可以使⽤脚本的 SHA1 校验和来调⽤这个脚本。脚本可以在缓存中保留⽆限长的时间,直到执⾏ SCRIPT FLUSH 为⽌。

(4). script exists

判断脚本是否已经添加到缓存中去了,1代表已经添加,0代表没有添加。

(5). script kill

杀死当前正在运⾏的 Lua 脚本,当且仅当这个脚本没有执⾏过任何写操作时,这个命令才⽣效。

这个命令主要⽤于终⽌运⾏时间过长的脚本,⽐如⼀个因为 BUG ⽽发⽣⽆限 loop 的脚本,诸如此类。SCRIPT KILL 执⾏之后,当前正在运⾏的脚本会被杀死,执⾏这个脚本的客户端会

EVAL 命令的阻塞当中退出,并收到⼀个错误作为返回值。

另⼀⽅⾯,假如当前正在运⾏的脚本已经执⾏过写操作,那么即使执⾏ SCRIPT KILL ,也⽆法将它杀死,因为这是违反 Lua 脚本的原⼦性执⾏原则的。在这种情况下,唯⼀可⾏的办法是

使⽤ SHUTDOWN NOSAVE 命令,通过停⽌整个 Redis 进程来停⽌脚本的运⾏,并防⽌不完整(half-written)的信息被写⼊数据库中。

(6). script flush

清除所有 Lua 脚本缓存

参考redis⽂档。.....................

1. 介绍

Lua 是⼀种轻量⼩巧的脚本语⾔,⽤标准C语⾔编写并以源代码形式开放, 其设计⽬的是为了嵌⼊应⽤程序中,从⽽为应⽤程序提供灵活的扩展和定制功能。常见的数据类型如下:

redislua之间的数据类型存在⼀ ⼀对应关系:

2. 好处

(1). 减少⽹络开销:本来多次⽹络请求的操作,可以⽤⼀个请求完成,原先多次次请求的逻辑都放在redis服务器上完成,使⽤脚本,减少了⽹络往返时延。

(2). 原⼦操作:Redis会将整个脚本作为⼀个整体执⾏,中间不会被其他命令插⼊。

(3). 复⽤:客户端发送的脚本会永久存储在Redis中,意味着其他客户端可以复⽤这⼀脚本⽽不需要使⽤代码完成同样的逻辑。

(4).替代redis的事务功能:redis⾃带的事务功能很鸡肋,报错不⽀持回滚,⽽redislua脚本⼏乎实现了常规的事务功能,⽀持报错回滚操作,官⽅推荐如果要使⽤redis的事务功能可以⽤

redis lua替代。

官⽹原话

A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.

注:lua整合⼀系列redis操作, 是为了保证原⼦性, redis在处理这个lua脚本期间不能执⾏其它操作, 但是lua脚本⾃⾝假设中间某条指令出错,并不会回滚的,会继续往下执⾏或者报错了。

3. 基本语法

(1). 基本结构,类似于js,前⾯声明⽅法,后⾯调⽤⽅法。

(2). 获取传过来的参数:ARGV[1]ARGV[2] 依次类推,获取传过来的Key,⽤KEYS[1]来获取。

(3). 调⽤redisapi,⽤( )⽅法调⽤。

(4). int类型转换 tonumber

参考代码:

local function seckillLimit()

--(1).获取相关参数

-- 限制请求数量

local tLimits=tonumber(ARGV[1]);

-- 限制秒数

local tSeconds =tonumber(ARGV[2]);

-- 受限商品key

local limitKey = ARGV[3];

--(2).执⾏判断业务

local myLimitCount = ('INCR',limitKey);

-- 仅当第⼀个请求进来设置过期时间

if (myLimitCount ==1)

then

('expire',limitKey,tSeconds) --设置缓存过期

end; --对应的是if的结束

-- 超过限制数量,返回失败

if (myLimitCount > tLimits)

then

return 0; --失败

end; --对应的是if的结束

end; --对应的是整个代码块的结束

--1. 单品限流调⽤

local status1 = seckillLimit();

if status1 == 0 then

return 2; --失败

end

详细语法参考菜鸟教程:/lua/

详见:/yaopengfei/p/

: Yaopengfei(姚鹏飞)

!

博客地址 :

1 : 如有错误,欢迎讨论,请勿谩骂^_^

2 : 原创博客请在转载时保留原⽂链接或在⽂章开头加上本⼈博客地址,否则保留追究法律责任的权利。