redis4.0之Lua脚本新姿势

前言

Redis内嵌了Lua环境来支持用户扩展功能,但是出于数据一致性考虑,要求脚本必须是纯函数的形式,也就是说对于一段Lua脚本给定相同的参数,写入Redis的数据也必须是相同的,对于随机性的写入Redis是拒绝的。

从Redis 3.2开始Lua脚本支持随机性写入,最近在总结4.0的新特性,索性就都归到4.0里,方便查阅。

Redis中的Lua脚本

1. Lua脚本简介

在Redis中使用Lua脚本不可避免的要用到以下三个命令:EVAL、EVALSHA和SCRIPT,下面我们来简单介绍一下:

  • 1. EVAL script numkeys key [key ...] arg [arg ...]
    script参数就是一段Lua脚本

    numkeys参数用于指明后面key的数量

    key键名,在Lua脚本中可以通过全局变量KEYS[]数组访问,其数量已经由numkeys指明

    arg为辅助参数,在Lua脚本中可以通过全局变量ARGV[]数组访问,其个数并没有限制
  • 2. EVALSHA sha1 numkeys key [key ...] arg [arg ...]
    EVALSHA与EVAL基本一致,只不过其中script脚本替换成了这段脚本的sha1校验码

    sha1校验码通常是取SCRIPT LOAD的返回值
  • 3. SCRIPT subcommand
    SCRIPT LOAD script: 将一段Lua脚本加载到Redis中缓存起来,并返回其sha1校验码

    SCRIPT EXISTS sha1 [sha1 ...]: 判断给定的一个或多个sha1校验码在Redis中是否有对应的Lua脚本

    SCRIPT FLUSH: 清空所有已加载的Lua脚本

    SCRIPT KILL: 杀死正在运行的Lua脚本,当且仅当这个脚本没有执行过任何写命令时才生效,如果执行过写命令那么就只能等待这个脚本执行完毕,或者执行SHUTDOWN NOSAVE来关闭Redis

关于Lua脚本的用法此处就不详细展开了,具体可以参考官网的文档:https://redis.io/commands/eval

2. Lua脚本的持久化及主从复制

Redis允许在Lua脚本中调用redis.call()或者redis.pcall()来执行Redis命令,如果Lua脚本对Redis的数据做了更改,那么除了执行脚本本身以外还需要两个额外的操作:

  1. 把这段Lua脚本持久化到AOF文件中,保证Redis重启时可以回放执行过的Lua脚本。
  2. 把这段Lua脚本复制给备库执行,保证主备库的数据一致性。

由于上述两步,现在就很容易理解为什么Redis要求Lua脚本必须是纯函数的形式了,想象一下给定一段Lua脚本和输入参数却得到了不同的结果,这就会造成重启前后和主备库之间的数据不一致,Redis不允许对数据一致性的破坏。

3. Redis如何防止随机写入

上一节我们介绍了Lua脚本的持久化及主从复制,很明显随机写入会对数据一致性造成破坏,那么本节就来介绍Redis是如何防止Lua脚本中随机写入的。

首先我们来执行一段脚本,尝试把time命令返回的当前时间写入到键now中,看下会怎样:

127.0.0.1:6379> eval "local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now')" 0
(error) ERR Error running script (call to f_e745355f11745192bd45376618a34bec9145653b): @user_script:1: @user_script: 1: Write commands not allowed after non deterministic commands. Call redis.replicate_commands() at the start of your script in order to switch to single commands replication mode.

不出意外的被拒绝了,这是因为在Redis中time命令是一个随机命令(时间是变化的),在Lua脚本中调用了随机命令之后禁止再调用写命令,Redis中一共有10个随机类命令:

spop、srandmember、sscan、zscan、hscan、randomkey、scan、lastsave、pubsub、time

熟悉Lua的读者也许会问,那要是使用math.random()来生成随机数呢?

Redis是允许在Lua脚本中使用随机数发生器的,不过大家也应该知道生成的其实都是伪随机序列,除非显示调用math.randomseed()。通常情况下都会选择系统时间来作为math.randomseed()的参数,然而Redis在初始化Lua环境时出于安全考虑并没有加载os库,所以os.time无法使用,而Redis的time命令属于随机命令就又回到了上面的问题。

  • 这里小插曲下,Redis自己实现了随机数发生器,替换掉了math.randomseed()和math.random(),以保证在不同运行环境下生成的伪随机数序列总是相同的。

4. redis.replicate_commands()

综上所述,Redis无法在Lua脚本中进行随机写入,是因为受到了持久化和主从复制的制约,而制约的根本原因是持久化和复制的粒度是整个Lua脚本,如果能够只把发生更改的数据做持久化和主从复制,那么就可以化随机为确定,进一步丰富Lua在Redis中的使用。

OK,从新版本开始,Redis提供了redis.replicate_commands()
函数来实现这一功能,把发生数据变更的命令以事务的方式做持久化和主从复制,从而允许在Lua脚本内进行随机写入,下面来举例说明:

127.0.0.1:6379> eval "redis.replicate_commands(); local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now')" 0
"1504460040"

可以看到,相同的脚本只是在开头插入了redis.replicate_commands()就可以成功把时间写入;这是因为执行了redis.replicate_commands()之后,Redis就开始使用multi/exec来包围Lua脚本中调用的命令,持久化和复制的只是1\r\n$5\r\nMULTI\r\n3\r\n$3\r\nset\r\n$3\r\nnow\r\n$10\r\n1504460595\r\n*1\r\n$4\r\nEXEC\r\n而不是整个Lua脚本,那么AOF文件和备库中拿到的就是一个确定的结果。

并且在Lua脚本中读多写少的情况下,只持久化和复制写命令,可以节省重启和备库的CPU时间。

5. redis.replicate_commands()注意事项

replicate_commands虽好但是也不能乱用,有几个事项还是需要注意的:

  • 1. 在写命令之前调用redis.replicate_commands()
    上一节说道调用redis.replicate_commands()之后Redis开始用事务来替代整个Lua脚本做持久化和主从复制。
    但是Redis并没有缓存redis.replicate_commands()之前的命令,如果在此之前调用了写命令是会破坏数据一致性的(因为Redis不支持undo操作,无法回滚到执行Lua脚本的初始状态)。
    此时redis.replicate_commands()并不会生效。

127.0.0.1:6379> eval "redis.call('set','foo','bar'); redis.replicate_commands(); local now = redis.call('time')[1]; redis.call('set','now',now); return redis.call('get','now')" 0
(error) ERR Error running script (call to f_7dd09c943ce6841d59c54b1f4618f9cc670c7b74): @user_script:1: @user_script: 1: Write commands not allowed after non deterministic commands. Call redis.replicate_commands() at the start of your script in order to switch to single commands replication mode.

    可以看到在redis.replicate_commands()之前先调用了写命令会造成失败。
    所以建议如果想进行随机写入的话,在脚本一开始就调用redis.replicate_commands()。
  • 2. 当有大流量写入时不建议用redis.replicate_commands()
    诚然redis.replicate_commands()丰富了Lua的用法,但是不可避免的也会有些副作用。
    比如有时候会放大主备复制的流量:

127.0.0.1:6379> eval "for i=1,10000,1 do redis.call('set',i,i) end" 0

    以上这段脚本会进行1万次的循环写入,在不调用redis.replicate_commands()的情况下只会给备库复制这段脚本,而调用之后就会进行1万次的写命令复制,增加了主从复制的流量。
    所以在不需要进行随机写入时,如果有可能造成大流量写入的话尽量不要使用redis.replicate_commands()。
  • 3. 慎用redis.set_repl()
    redis.replicate_commands()可以和redis.set_repl()配合,来控制写命令是否进行持久化和主从复制:
    redis.set_repl(redis.REPL_ALL) -- 既持久化也主从复制。
    redis.set_repl(redis.REPL_AOF) -- 只持久化不主从复制。
    redis.set_repl(redis.REPL_SLAVE) -- 只主从复制不持久化。
    redis.set_repl(redis.REPL_NONE) -- 既不持久化也不主从复制。
    默认REPL_ALL,当设置为其他模式时会有数据不一致的风险,所以不建议使用redis.set_repl(),使用redis.replicate_commands()来进行随机写入足矣。

后记

基于4.0的云redis正在筹备之中,敬请期待。

时间: 2024-11-05 09:10:40

redis4.0之Lua脚本新姿势的相关文章

Redis4.0新特性(三)-PSYNC2

1 什么是Redis部分重新同步-psync redis部分重新同步:是指redis因某种原因引起复制中断后,从库重新同步时,只同步主实例的差异数据(写入指令),不进行bgsave复制整个RDB文件. 本文的名词规约: 部分重新同步:后文简称psync 全量重新同步:后文简称fullsync redis2.8第一版部分重新同步:后文简称psync1 redis4.0第二版本部分重新同步:后文简称psync2 在说明psync2功能前,先简单阐述redis2.8版本发布的psync1 Redis2

Redis4.0新特性(二)-Lazy Free

Redis4.0新增了非常实用的lazy free特性,从根本上解决Big Key(主要指定元素较多集合类型Key)删除的风险.笔者在redis运维中也遇过几次Big Key删除带来可用性和性能故障. 本文分为以下几节说明redis lazy free: lazy free的定义 我们为什么需要lazy free lazy free的使用 lazy free的监控 lazy free实现的简单分析 1 lazy free的定义 lazy free可译为惰性删除或延迟释放:当删除键的时候,redi

阿里云Redis LUA脚本功能上线——轻量嵌入,极速扩展,业务轻松跨平台

    阿里云Redis云数据库,全面支持LUA脚本功能,助力企业轻松迁移自建Redis数据库的业务逻辑,实现业务的跨平台复用,快速驱动业务上云.LUA语言作为目前最流行的轻量级嵌入式脚本语言,凭借其语法简单.高效稳定.支持复杂数据结构以及自动内存管理等特点,已经在众多著名的游戏程序中大量被使用,如:愤怒的小鸟.星际争霸.魔兽世界等. 了解Redis详细配置及价格>> Redis支持LUA脚本的主要优势      LUA脚本的融合将使Redis数据库产生更多的使用场景,迸发更多新的优势: 高效

用lua_tinker将lua脚本嵌入到游戏服务器

      忙中偷闲,经过几天的努力,将lua脚本嵌入到系统中.之前公司的时候,偌大一个服务器全部使用C++编写,对于新手经常发生一些宕机事件,被主程责骂.在后来接触的一些人中,发现很多公,都已经引入lua来适应多变的环境和敏捷开发!正如一个主程所说的,在n年前网易已经脚本为王了,现在很多公司拿着C++不放,作为开发人员不苦逼才怪! 想想在广州开发游戏的日子,每次在群里面看到运维说某某服务器上面有coredump文件时,总是惊出冷汗,赶紧用gdb去查询,是哪行代码引起的宕机:还要应对主程的责骂!

【COCOS2DX-LUA 脚本开发之一】LUA语言基础在COCOS2DX游戏中使用LUA脚本进行游戏开发(基础篇)并介绍脚本在游戏中详细用途!

本站文章均为 李华明Himi 原创,转载务必在明显处注明:  转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/iphone-cocos2dx/681.html 对于游戏公司而言,采用游戏脚本lua.python等进行开发也很常见,但是很多童鞋对脚本并没有很熟悉的概念,本篇则向大家简单介绍脚本的用途以及在Cocos2dx基础用法: Lua和python这些详细介绍的话,请不太熟悉的童鞋自行百度百科哈,那么对于lua和python则是两个常用的脚本语言,

redis4.0之RDB-AOF混合持久化

前言 redis有两种持久化的方式--RDB和AOF其中RDB是一份内存快照AOF则为可回放的命令日志他们两个各有特点也相互独立.4.0开始允许使用RDB-AOF混合持久化的方式结合了两者的优点通过aof-use-rdb-preamble配置项可以打开混合开关. RDB V.S. AOF 1. RDB RDB文件本质上是一份内存快照保存了创建RDB文件那个时间点的redis全量数据具有数据文件小创建.恢复快的优点但是由于快照的特性无法保存创建RDB之后的增量数据. 2. AOF AOF文件本质上

【COCOS2DX(2.X)_LUA开发之三】在LUA中使用自定义精灵(LUA脚本与自创建类之间的访问)及LUA基础讲解

本站文章均为 李华明Himi 原创,转载务必在明显处注明:  转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/lua-game/985.html 本篇做起来比较累,大家请参考最新篇[COCOS2DX-LUA 脚本开发之四]使用tolua++编译pkg,从而创建自定义类让Lua脚本使用 此篇可能会在最新的cocos2dx版本中出现如下问题: 1 2 LUA ERROR: ...24F82-1230-41FE-8A04-C445FB7D1BAB/mtet

暴力枚举Gmail邮箱地址的新姿势

本文讲的是暴力枚举Gmail邮箱地址的新姿势, 本文将介绍一种比较经典的枚举用户Gmail邮箱地址的新思路,这种思路可以检索成千上万个Gmail邮箱地址. 我偶然发现一个小故障,允许我大量猜测现有的且可能是未知的Google帐户地址. 免责声明:本文介绍的方法可能只是一个没有进行合理限制的接口,没有什么太花哨的姿势,所以如果你正在寻找一些比较6的0day,请绕道.  小故障 https://mail.google.com/mail/gxlu这个网址没有对请求次数做任何限制.另外,我注意到,提供不

《Lua游戏AI开发指南》一2.7 创建一个沙箱Lua脚本

2.7 创建一个沙箱Lua脚本 基本的沙箱程序就位之后,我们就可以新建沙箱的创建Lua脚本了.首先在脚本文件夹中新建一个Sandbox.lua脚本. 像下面这样创建一个Lua文件: src/my_sandbox/script/Sandbox.lua 沙箱Lua脚本必须实现4个全局函数供C++代码调用,它们分别是Sandbox_Cleanup. Sandbox_HandleEvent. Sandbox_Initialize和Sandbox_Update: Sandbox.lua: function