背景介绍

redis数据库提供了一些管理功能比如

流水线:打包发送多条命令,并在一个回复里面接收所有被执行命令的结果。
事务:一次执行多条命令,被执行的命令要么就全部都被执行,要么就一个也不执行。并且事务执行过
程中不会被其他工作打断。
乐观锁:监视特定的键,防止事务出现竞争条件。
虽然这些附加功能都非常有用,但它们也有一些缺陷。

流水线的缺陷
尽管使用流水线可以一次发送多个命令,但是对于一个由多个命令组成的复杂操作来说,为了执行该
操作而不断地重复发送相同的命令,这并不是最高效的做法,会对网络资源造成浪费。
如果我们有办法避免重复地发送相同的命令,那么客户端就可以减少花在网络传输方面的时间,操作
就可以执行得更快。

事务和乐观锁的缺陷
虽然使用事务可以一次执行多个命令,并且通过乐观锁可以防止事务产生竞争条件,但是在实际中,要
正确地使用事务和乐观锁并不是一件容易的事情。
1. 对于一个复杂的事务来说,通常需要仔细思考才能知道应该对哪些键进行加锁:锁了不应该锁的键会增加事务失败的机会,甚至可能会造成程序出错;而忘了对应该锁的键进行加锁的话,程序又会产生竞争条件。
2. 有时候为了防止竞争条件发生,即使操作本身不需要用到事务,但是为了让乐观锁生效,我们也会使用事务将命令包裹起来, 这增加了实现的复杂度,并且带来了额外的性能损耗。

误用示例
《事务》一节介绍的 ZDECRBY 命令的实现,这里的事务仅仅是为了让 WATCH 生效而用的:
def ZDECRBY(key, decrment, member):
# 监视输入的有序集合
WATCH key
# 取得元素当前的分值
old_score = ZSCORE key member
# 使用当前分值减去指定的减量,得出新的分值
new_score = old_score - decrment
# 使用事务包裹 ZADD 命令
# 确保 ZADD 命令只会在有序集合没有被修改的情况下执行
MULTI
ZADD key new_score member # 为元素设置新分值,覆盖现有的分值
EXEC

避免事务被误用的办法
如果有一种方法,可以让我们以事务方式来执行多个命令,并且这种方法不会引入任何竞争条件,那么我们就可以使用这种方法来代替事务和乐观锁。

扩展 Redis 功能时的麻烦
Redis 针对每种数据结构都提供了相应的操作命令,也对数据库本身提供了操作命令,但如果我们需要对数据结构进行一些 Redis 命令不支持的操作,那么就需要使用客户端取出数据,然后由客户端对数据进行处理,最后再将处理后的数据储存回 Redis 服务器。
举个简单的例子,因为 Redis 没有提供删除列表里面所有偶数数字的命令,所以为了执行这一操作,客户端需要取出列表里面的所有项,然后在客户端里面进行过滤,最后将过滤后的项重新推入到列表里面:
lst = LRANGE lst 0 -1 # 取出列表包含的所有元素
DEL lst # 删除现有的列表
for item in lst: # 遍历整个列表
if item % 2 != 0: # 将非偶数元素推入到列表里面
RPUSH lst item
并且为了保证这个操作的安全性, 还要用到事务和乐观锁,非常麻烦。

Lua 脚本
为了解决以上提到的问题, Redis 从 2.6 版本开始在服务器内部嵌入了一个 Lua 解释器,使得用户可以在服务器端执行 Lua 脚本。
这个功能有以下好处:
1. 使用脚本可以直接在服务器端执行 Redis 命令,一般的数据处理操作可以直接使用 Lua 语言或者Lua 解释器提供的函数库来完成,不必再返回给客户端进行处理。
2. 所有脚本都是以事务的形式来执行的,脚本在执行过程中不会被其他工作打断,也不会引起任何竞争条件,完全可以使用 Lua 脚本来代替事务和乐观锁。
3. 所有脚本都是可重用的,也即是说,重复执行相同的操作时,只要调用储存在服务器内部的脚本缓存就可以了,不用重新发送整个脚本,从而尽可能地节约网络资源。

执行 Lua 脚本
EVAL script numkeys key [key ...] arg [arg ...]
script 参数是要执行的 Lua 脚本。
numkeys 是脚本要处理的数据库键的数量,之后的 key [key …] 参数指定了脚本要处理的数据库键,被传入的键可以在脚本里面通过访问 KEYS 数组来取得,比如 KEYS[1] 就取出第一个输入的键,KEYS[2] 取出第二个输入的键,诸如此类。
arg [arg …] 参数指定了脚本要用到的参数,在脚本里面可以通过访问 ARGV 数组来获取这些参数。显式地指定脚本里面用到的键是为了配合 Redis 集群对键的检查,如果不这样做的话,在集群里面使用脚本可能会出错。
另外,通过显式地指定脚本要用到的数据库键以及相关参数,而不是将数据库键和参数硬写在脚本里面,用户可以更方便地重用同一个脚本。

EVAL 命令使用示例
redis> EVAL "return 'hello world'" 0
"hello world"
redis> EVAL "return 1+1" 0
(integer) 2
redis> EVAL "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 "msg" "age" 123 "hello world"
1) "msg" # KEYS[1]
2) "age" # KEYS[2]
3) "123" # ARGV[1]
4) "hello world" # ARGV[2]

在 Lua 脚本中执行 Redis 命令
通过调用 redis.call() 函数或者 redis.pcall() 函数,我们可以直接在 Lua 脚本里面执行 Redis 命令。
redis> EVAL "return redis.call('PING')" 0 # 在 Lua 脚本里面执行 PING 命令
PONG
redis> EVAL "return redis.call('DBSIZE')" 0 # 在 Lua 脚本里面执行 DBSIZE 命令
(integer) 4
# 在 Lua 脚本里面执行 GET 命令,取出键 msg 的值,并对值进行字符串拼接操作
redis> SET msg "hello world"
OK
redis> EVAL "return 'The message is: ' .. redis.call('GET', KEYS[1]) '" 1 msg
"The message is: hello world"

redis.call() 和 redis.pcall() 的区别
redis.call() 和 redis.pcall() 都可以用来执行 Redis 命令,它们的不同之处在于,当被执行的脚本出错时,redis.call() 会返回出错脚本的名字以及 EVAL 命令的错误信息,而 redis.pcall() 只返回 EVAL 命令的错误信息。
redis> EVAL "return redis.call('NotExistsCommand')" 0
(error) ERR Error running script (call to f_ddabd662fa0a8e105765181ee7606562c1e6f1ce):
@user_script:1: @user_script: 1: Unknown Redis command called from Lua script
redis> EVAL "return redis.pcall('NotExistsCommand')" 0
(error) @user_script: 1: Unknown Redis command called from Lua script
换句话来说,在被执行的脚本出错时, redis.call() 可以提供更详细的错误信息,方便进行查错。

示例:使用 Lua 脚本重新实现 ZDECRBY 命令
创建一个包含以下内容的 zdecrby.lua 文件:
local old_score = redis.call('ZSCORE', KEYS[1], ARGV[2])
local new_score = old_score - ARGV[1]
return redis.call('ZADD', KEYS[1], new_score, ARGV[2])
然后通过以下命令来执行脚本:
$ redis-cli --eval zdecrby.lua salary , 300 peter
(integer) 0
这和在 redis-cli 里面执行 EVAL “local … ” 1 salary 300 peter 效果一样,但先将脚本内容保存到文件里面,再执行脚本文件的做法,比起直接在客户端里面一个个字输入要容易一些。另外,这个脚本实现的 ZDECRBY 也比使用事务和乐观锁实现的 ZDECRBY 要简单得多。

使用 EVALSHA 来减少网络资源损耗
任何 Lua 脚本,只要被 EVAL 命令执行过一次,就会被储存到服务器的脚本缓存里面,用户只要通过EVALSHA 命令,指定被缓存脚本的 SHA1 值,就可以在不发送脚本的情况下,再次执行脚本:
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
通过 SHA1 值来重用返回 ‘hello world’ 信息的脚本:
redis> EVAL "return 'hello world'" 0
"hello world"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
"hello world"
通过 SHA1 值来重用之前实现的 ZDECRBY 命令,这样就不用每次都发送整个脚本了:
redis> EVALSHA 918130cae39ff0759b8256948742f77300a91cb2 1 salary 500 peter
(integer) 0

脚本管理命令
SCRIPT EXISTS sha1 [sha1 ...]
检查 sha1 值所代表的脚本是否已经被加入到脚本缓存里面,是的话返回 1 ,不是的话返回 0 。
SCRIPT LOAD script
将脚本储存到脚本缓存里面,等待将来 EVALSHA 使用。
SCRIPT FLUSH
清除脚本缓存储存的所有脚本。
SCRIPT KILL
杀死运行超时的脚本。如果脚本已经执行过写入操作,那么还需要使用 SHUTDOWN NOSAVE 命令来强制服务器不保存数据,以免错误的数据被保存到数据库里面。

函数库
Redis 在 Lua 环境里面载入了一些常用的函数库,我们可以使用这些函数库,直接在脚本里面处理数据,它们分别是标准库:
• base 库 :包含 Lua 的核心(core)函数,比如 assert、tostring、error、type 等。
• string 库 :包含用于处理字符串的函数,比如 find、format、len、reverse 等。
• table 库:包含用于处理表格的函数,比如 concat、insert、remove、sort 等。
• math 库:包含常用的数学计算函数,比如 abs、sqrt、log 等。
• debug 库:包含调试程序所需的函数,比如 sethook、gethook 等。
以及外部库
• struct 库:在 C 语言的结构和 Lua 语言的值之间进行转换。
• cjson 库:将 Lua 值转换为 JSON 对象,或者将 JSON 对象转换为 Lua 值。
• cmsgpack 库:将 Lua 值编码为 MessagePack 格式,或者从 MessagePack 格式里面解码出 Lua值。
另外还有一个用于计算 sha1 值的外部函数 redis.sha1hex。

redis之lua脚本的更多相关文章

  1. PHP中使用redis执行lua脚本示例

    摸索了一下在PHP中如何使用redis执行lua脚本,写了一个脚本如下,供以后参考 <?php $redis = new Redis(); #实例化redis类 $redis->conne ...

  2. Redis结合Lua脚本实现高并发原子性操作

    从 2.6版本 起, Redis 开始支持 Lua 脚本 让开发者自己扩展 Redis … 案例-实现访问频率限制: 实现访问者 $ip 在一定的时间 $time 内只能访问 $limit 次. 非脚 ...

  3. 【spring boot】【redis】spring boot基于redis的LUA脚本 实现分布式锁

    spring boot基于redis的LUA脚本 实现分布式锁[都是基于redis单点下] 一.spring boot 1.5.X 基于redis 的 lua脚本实现分布式锁 1.pom.xml &l ...

  4. .Net Core使用分布式缓存Redis:Lua脚本

    一.前言 运行环境window,redis版本3.2.1.此处暂不对Lua进行详细讲解,只从Redis的方面讲解. 二.Redis的Lua脚本 在Redis的2.6版本推出了脚本功能,允许开发者使用L ...

  5. 要想用活Redis,Lua脚本是绕不过去的坎

    前言 Redis 当中提供了许多重要的高级特性,比如发布与订阅,Lua 脚本等.Redis 当中也提供了自增的原子命令,但是假如我们需要同时执行好几个命令的同时又想让这些命令保持原子性,该怎么办呢?这 ...

  6. 快速入门Redis调用Lua脚本及使用场景介绍

    Redis 是一种非常流行的内存数据库,常用于数据缓存与高频数据存储.大多数开发人员可能听说过redis可以运行 Lua 脚本,但是可能不知道redis在什么情况下需要使用到Lua脚本. 一.阅读本文 ...

  7. redis中lua脚本的简单使用

    一.背景 在使用redis的过程中,发现有些时候需要原子性去操作redis命令,而redis的lua脚本正好可以实现这一功能.比如: 扣减库存操作.限流操作等等. redis的pipelining虽然 ...

  8. Redis执行Lua脚本的情况

    第一个测试: 往Redis里面存入1000个Hash,每个Hash里面有100个元素(Key 0-99,值是Key^2). PHP代码,执行33s左右 <?php $redis = new Re ...

  9. Redis执行Lua脚本示例

    Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行.使用脚本的好处如下: 1.减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在red ...

随机推荐

  1. 17--Box2D使用(三、触摸交互)

    Box2D引擎与触摸的交互通过创建鼠标关节以及碰撞检测来得到触摸点下面的刚体,在根据触摸操作完成相应的功能.首先添加触摸响应函数声明 virtual void ccTouchesBegan(cocos ...

  2. [Mugeda HTML5技术教程之2] Mugeda HTML5富媒体平台简介

    [Mugeda HTML5技术教程之2] Mugeda HTML5动画平台简介 摘要:Mugeda提供基于云的平台,供开发人员和设计人员快速的开发.发布和统计基于HTML5的,包含丰富动画和交互的移动 ...

  3. Ajax实现三级联动(0520)

    查询数据库中的chinastates表,通过父级代号查询相应省市区. 实现界面: 在js页面实现三级联动 在JQuery中调用Ajax方法(引用JQuery文件一定放在最上面) 用插件的形式,创建三个 ...

  4. Flask学习记录之Flask-WTF

    Flask-wtf时Wtforms库的flask框架扩展,能够方便的处理Web表单 一.定义一个web表单 使用flask-wtf时,每个web表单都由一个继承自flask.ext.wtf.Form的 ...

  5. iOS学习之导航条NavigationControl的一些属性设置

    /** * 配置公共的属性,该属性作用于所有的导航条界面; */ - (void)configureConmmonPropety { //1.设置导航条的颜色 self.navigationContr ...

  6. Java虚拟机--字节码指令集

    1. 字节码指令集简介: Java虚拟机的指令由一个字节长度的,代表着某种特定操作含义的操作码(Opcode)以及跟随其后的零至多个代表此操作所需参数的操作数(Operands)所构成.虚拟机中许多指 ...

  7. 禁用与启用Button点击

    //启用查询按钮 btnFpSelect.setClickable(true); //禁用查询按钮 btnFpSelect.setClickable(false);

  8. ResourceString的用法

    在Delphi编程的那段“古老”的日子里(就是在版本4之前),在程序中使用字符串有两个基本的方法.你可以使用字符串将它们嵌入到源程序中,例如: MessageDlg( 'Leave your stin ...

  9. 递归转手工栈处理的一般式[C语言]

    是任意形式的递归,是化解的一般式. 主题所谓的“递归调用化解为栈处理”,意思是,将递归函数调用化解为“一个由stack_push stack_pop stack_top等函数调用组成的循环式子”.这里 ...

  10. java.lang.ClassNotFoundException错误原因汇总

    开发java很长时间了,还经常会遇到java.lang.ClassNotFoundException这样的错误,最近又处理了一次,起初怀疑是jdk版本比class文件的编译版本低了导致了,但是运维人员 ...